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