• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""A module defining clippy rules"""
16
17load("//rust/private:common.bzl", "rust_common")
18load("//rust/private:providers.bzl", "CaptureClippyOutputInfo", "ClippyInfo")
19load(
20    "//rust/private:rustc.bzl",
21    "collect_deps",
22    "collect_inputs",
23    "construct_arguments",
24)
25load(
26    "//rust/private:utils.bzl",
27    "determine_output_hash",
28    "find_cc_toolchain",
29    "find_toolchain",
30)
31
32ClippyFlagsInfo = provider(
33    doc = "Pass each value as an additional flag to clippy invocations",
34    fields = {"clippy_flags": "List[string] Flags to pass to clippy"},
35)
36
37def _clippy_flags_impl(ctx):
38    return ClippyFlagsInfo(clippy_flags = ctx.build_setting_value)
39
40clippy_flags = rule(
41    doc = (
42        "Add custom clippy flags from the command line with `--@rules_rust//:clippy_flags`."
43    ),
44    implementation = _clippy_flags_impl,
45    build_setting = config.string_list(flag = True),
46)
47
48def _get_clippy_ready_crate_info(target, aspect_ctx = None):
49    """Check that a target is suitable for clippy and extract the `CrateInfo` provider from it.
50
51    Args:
52        target (Target): The target the aspect is running on.
53        aspect_ctx (ctx, optional): The aspect's context object.
54
55    Returns:
56        CrateInfo, optional: A `CrateInfo` provider if clippy should be run or `None`.
57    """
58
59    # Ignore external targets
60    if target.label.workspace_root.startswith("external"):
61        return None
62
63    # Targets with specific tags will not be formatted
64    if aspect_ctx:
65        ignore_tags = [
66            "noclippy",
67            "no-clippy",
68        ]
69
70        for tag in ignore_tags:
71            if tag in aspect_ctx.rule.attr.tags:
72                return None
73
74    # Obviously ignore any targets that don't contain `CrateInfo`
75    if rust_common.crate_info in target:
76        return target[rust_common.crate_info]
77    elif rust_common.test_crate_info in target:
78        return target[rust_common.test_crate_info].crate
79    else:
80        return None
81
82def _clippy_aspect_impl(target, ctx):
83    crate_info = _get_clippy_ready_crate_info(target, ctx)
84    if not crate_info:
85        return [ClippyInfo(output = depset([]))]
86
87    toolchain = find_toolchain(ctx)
88    cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
89
90    dep_info, build_info, linkstamps = collect_deps(
91        deps = crate_info.deps,
92        proc_macro_deps = crate_info.proc_macro_deps,
93        aliases = crate_info.aliases,
94        # Clippy doesn't need to invoke transitive linking, therefore doesn't need linkstamps.
95        are_linkstamps_supported = False,
96    )
97
98    compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
99        ctx,
100        ctx.rule.file,
101        ctx.rule.files,
102        linkstamps,
103        toolchain,
104        cc_toolchain,
105        feature_configuration,
106        crate_info,
107        dep_info,
108        build_info,
109    )
110
111    args, env = construct_arguments(
112        ctx = ctx,
113        attr = ctx.rule.attr,
114        file = ctx.file,
115        toolchain = toolchain,
116        tool_path = toolchain.clippy_driver.path,
117        cc_toolchain = cc_toolchain,
118        feature_configuration = feature_configuration,
119        crate_info = crate_info,
120        dep_info = dep_info,
121        linkstamp_outs = linkstamp_outs,
122        ambiguous_libs = ambiguous_libs,
123        output_hash = determine_output_hash(crate_info.root, ctx.label),
124        rust_flags = [],
125        out_dir = out_dir,
126        build_env_files = build_env_files,
127        build_flags_files = build_flags_files,
128        emit = ["dep-info", "metadata"],
129        skip_expanding_rustc_env = True,
130    )
131
132    if crate_info.is_test:
133        args.rustc_flags.add("--test")
134
135    clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags
136
137    # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output
138    # or rustc may fail to create intermediate output files because the directory does not exist.
139    if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output:
140        clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output)
141        args.process_wrapper_flags.add("--stderr-file", clippy_out)
142
143        if clippy_flags:
144            fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported.
145See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""")
146
147        # If we are capturing the output, we want the build system to be able to keep going
148        # and consume the output. Some clippy lints are denials, so we treat them as warnings.
149        args.rustc_flags.add("-Wclippy::all")
150    else:
151        # A marker file indicating clippy has executed successfully.
152        # This file is necessary because "ctx.actions.run" mandates an output.
153        clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output)
154        args.process_wrapper_flags.add("--touch-file", clippy_out)
155
156        if clippy_flags:
157            args.rustc_flags.add_all(clippy_flags)
158        else:
159            # The user didn't provide any clippy flags explicitly so we apply conservative defaults.
160
161            # Turn any warnings from clippy or rustc into an error, as otherwise
162            # Bazel will consider the execution result of the aspect to be "success",
163            # and Clippy won't be re-triggered unless the source file is modified.
164            args.rustc_flags.add("-Dwarnings")
165
166    # Upstream clippy requires one of these two filenames or it silently uses
167    # the default config. Enforce the naming so users are not confused.
168    valid_config_file_names = [".clippy.toml", "clippy.toml"]
169    if ctx.file._config.basename not in valid_config_file_names:
170        fail("The clippy config file must be named one of: {}".format(valid_config_file_names))
171    env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname)
172    compile_inputs = depset([ctx.file._config], transitive = [compile_inputs])
173
174    ctx.actions.run(
175        executable = ctx.executable._process_wrapper,
176        inputs = compile_inputs,
177        outputs = [clippy_out],
178        env = env,
179        tools = [toolchain.clippy_driver],
180        arguments = args.all,
181        mnemonic = "Clippy",
182        toolchain = "@rules_rust//rust:toolchain_type",
183    )
184
185    return [
186        OutputGroupInfo(clippy_checks = depset([clippy_out])),
187        ClippyInfo(output = depset([clippy_out])),
188    ]
189
190# Example: Run the clippy checker on all targets in the codebase.
191#   bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \
192#               --output_groups=clippy_checks \
193#               //...
194rust_clippy_aspect = aspect(
195    fragments = ["cpp"],
196    attrs = {
197        "_capture_output": attr.label(
198            doc = "Value of the `capture_clippy_output` build setting",
199            default = Label("//:capture_clippy_output"),
200        ),
201        "_cc_toolchain": attr.label(
202            doc = (
203                "Required attribute to access the cc_toolchain. See [Accessing the C++ toolchain]" +
204                "(https://docs.bazel.build/versions/master/integrating-with-rules-cc.html#accessing-the-c-toolchain)"
205            ),
206            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
207        ),
208        "_clippy_flags": attr.label(
209            doc = "Arguments to pass to clippy",
210            default = Label("//:clippy_flags"),
211        ),
212        "_config": attr.label(
213            doc = "The `clippy.toml` file used for configuration",
214            allow_single_file = True,
215            default = Label("//:clippy.toml"),
216        ),
217        "_error_format": attr.label(
218            doc = "The desired `--error-format` flags for clippy",
219            default = "//:error_format",
220        ),
221        "_extra_rustc_flag": attr.label(
222            default = Label("//:extra_rustc_flag"),
223        ),
224        "_per_crate_rustc_flag": attr.label(
225            default = Label("//:experimental_per_crate_rustc_flag"),
226        ),
227        "_process_wrapper": attr.label(
228            doc = "A process wrapper for running clippy on all platforms",
229            default = Label("//util/process_wrapper"),
230            executable = True,
231            cfg = "exec",
232        ),
233    },
234    provides = [ClippyInfo],
235    required_providers = [
236        [rust_common.crate_info],
237        [rust_common.test_crate_info],
238    ],
239    toolchains = [
240        str(Label("//rust:toolchain_type")),
241        "@bazel_tools//tools/cpp:toolchain_type",
242    ],
243    implementation = _clippy_aspect_impl,
244    doc = """\
245Executes the clippy checker on specified targets.
246
247This aspect applies to existing rust_library, rust_test, and rust_binary rules.
248
249As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`:
250
251```python
252load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
253
254rust_library(
255    name = "hello_lib",
256    srcs = ["src/lib.rs"],
257)
258
259rust_test(
260    name = "greeting_test",
261    srcs = ["tests/greeting.rs"],
262    deps = [":hello_lib"],
263)
264```
265
266Then the targets can be analyzed with clippy using the following command:
267
268```output
269$ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \
270              --output_groups=clippy_checks //hello_lib:all
271```
272""",
273)
274
275def _rust_clippy_rule_impl(ctx):
276    clippy_ready_targets = [dep for dep in ctx.attr.deps if "clippy_checks" in dir(dep[OutputGroupInfo])]
277    files = depset([], transitive = [dep[OutputGroupInfo].clippy_checks for dep in clippy_ready_targets])
278    return [DefaultInfo(files = files)]
279
280rust_clippy = rule(
281    implementation = _rust_clippy_rule_impl,
282    attrs = {
283        "deps": attr.label_list(
284            doc = "Rust targets to run clippy on.",
285            providers = [
286                [rust_common.crate_info],
287                [rust_common.test_crate_info],
288            ],
289            aspects = [rust_clippy_aspect],
290        ),
291    },
292    doc = """\
293Executes the clippy checker on a specific target.
294
295Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \
296within the build system.
297
298For example, given the following example targets:
299
300```python
301load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
302
303rust_library(
304    name = "hello_lib",
305    srcs = ["src/lib.rs"],
306)
307
308rust_test(
309    name = "greeting_test",
310    srcs = ["tests/greeting.rs"],
311    deps = [":hello_lib"],
312)
313```
314
315Rust clippy can be set as a build target with the following:
316
317```python
318load("@rules_rust//rust:defs.bzl", "rust_clippy")
319
320rust_clippy(
321    name = "hello_library_clippy",
322    testonly = True,
323    deps = [
324        ":hello_lib",
325        ":greeting_test",
326    ],
327)
328```
329""",
330)
331
332def _capture_clippy_output_impl(ctx):
333    """Implementation of the `capture_clippy_output` rule
334
335    Args:
336        ctx (ctx): The rule's context object
337
338    Returns:
339        list: A list containing the CaptureClippyOutputInfo provider
340    """
341    return [CaptureClippyOutputInfo(capture_output = ctx.build_setting_value)]
342
343capture_clippy_output = rule(
344    doc = "Control whether to print clippy output or store it to a file, using the configured error_format.",
345    implementation = _capture_clippy_output_impl,
346    build_setting = config.bool(flag = True),
347)
348