aboutsummaryrefslogtreecommitdiff
path: root/rules/cc/clang_tidy.bzl
blob: a3b8425a66ee46303aa098122688eb2790b7aa86 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load(
    "@bazel_tools//tools/build_defs/cc:action_names.bzl",
    "CPP_COMPILE_ACTION_NAME",
    "C_COMPILE_ACTION_NAME",
)
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("@soong_injection//cc_toolchain:config_constants.bzl", "constants")
load("@soong_injection//product_config:product_variables.bzl", "product_vars")
load("//build/bazel/rules:common.bzl", "get_dep_targets")
load(":cc_library_common.bzl", "get_compilation_args")

ClangTidyInfo = provider(
    "Info provided from clang-tidy actions",
    fields = {
        "tidy_files": "Outputs from the clang-tidy tool",
        "transitive_tidy_files": "Outputs from the clang-tidy tool for all transitive dependencies." +
                                 " Currently, these are needed so that mixed-build targets can also run clang-tidy for their dependencies.",
    },
)

_PRODUCT_VARIABLE_TIDY_CHECKS = product_vars["TidyChecks"].split(",") if "TidyChecks" in product_vars else []
_TIDY_GLOBAL_NO_CHECKS = constants.TidyGlobalNoChecks.split(",")
_TIDY_GLOBAL_NO_ERROR_CHECKS = constants.TidyGlobalNoErrorChecks.split(",")
_TIDY_DEFAULT_GLOBAL_CHECKS = constants.TidyDefaultGlobalChecks.split(",")
_TIDY_EXTERNAL_VENDOR_CHECKS = constants.TidyExternalVendorChecks.split(",")
_TIDY_DEFAULT_GLOBAL_CHECKS_NO_ANALYZER = constants.TidyDefaultGlobalChecks.split(",") + ["-clang-analyzer-*"]
_TIDY_EXTRA_ARG_FLAGS = constants.TidyExtraArgFlags

def _check_bad_tidy_flags(tidy_flags):
    """should be kept up to date with
    https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/check.go;l=128;drc=b45a2ea782074944f79fc388df20b06e01f265f7
    """
    for flag in tidy_flags:
        flag = flag.strip()
        if not flag.startswith("-"):
            fail("Flag `%s` must start with `-`" % flag)
        if flag.startswith("-fix"):
            fail("Flag `%s` is not allowed, since it could cause multiple writes to the same source file" % flag)
        if flag.startswith("-checks="):
            fail("Flag `%s` is not allowed, use `tidy_checks` property instead" % flag)
        if "-warnings-as-errors=" in flag:
            fail("Flag `%s` is not allowed, use `tidy_checks_as_errors` property instead" % flag)
        if " " in flag:
            fail("Bad flag: `%s` is not an allowed multi-word flag. Should it be split into multiple flags?" % flag)

def _check_bad_tidy_checks(tidy_checks):
    """should be kept up to date with
    https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/check.go;l=145;drc=b45a2ea782074944f79fc388df20b06e01f265f7
    """
    for check in tidy_checks:
        if " " in check:
            fail("Check `%s` invalid, cannot contain spaces" % check)
        if "," in check:
            fail("Check `%s` invalid, cannot contain commas. Split each entry into its own string instead" % check)

def _add_with_tidy_flags(ctx, tidy_flags):
    with_tidy_flags = ctx.attr._with_tidy_flags[BuildSettingInfo].value
    if with_tidy_flags:
        return tidy_flags + with_tidy_flags
    return tidy_flags

def _add_header_filter(ctx, tidy_flags):
    """If TidyFlags does not contain -header-filter, add default header filter.
    """
    for flag in tidy_flags:
        # Find the substring because the flag could also appear as --header-filter=...
        # and with or without single or double quotes.
        if "-header-filter=" in flag:
            return tidy_flags

    # Default header filter should include only the module directory,
    # not the out/soong/.../ModuleDir/...
    # Otherwise, there will be too many warnings from generated files in out/...
    # If a module wants to see warnings in the generated source files,
    # it should specify its own -header-filter flag.
    default_dirs = ctx.attr._default_tidy_header_dirs[BuildSettingInfo].value
    if default_dirs == "":
        header_filter = "-header-filter=^" + ctx.label.package + "/"
    else:
        header_filter = "-header-filter=\"(^%s/|%s)\"" % (ctx.label.package, default_dirs)
    return tidy_flags + [header_filter]

def _add_extra_arg_flags(tidy_flags):
    return tidy_flags + ["-extra-arg-before=" + f for f in _TIDY_EXTRA_ARG_FLAGS]

def _add_quiet_if_not_global_tidy(tidy_flags):
    if len(_PRODUCT_VARIABLE_TIDY_CHECKS) == 0:
        return tidy_flags + [
            "-quiet",
            "-extra-arg-before=-fno-caret-diagnostics",
        ]
    return tidy_flags

def _clang_rewrite_tidy_checks(tidy_checks):
    # List of tidy checks that should be disabled globally. When the compiler is
    # updated, some checks enabled by this module may be disabled if they have
    # become more strict, or if they are a new match for a wildcard group like
    # `modernize-*`.
    clang_tidy_disable_checks = [
        "misc-no-recursion",
        "readability-function-cognitive-complexity",  # http://b/175055536
    ]

    tidy_checks = tidy_checks + ["-" + c for c in clang_tidy_disable_checks]

    # clang-tidy does not allow later arguments to override earlier arguments,
    # so if we just disabled an argument that was explicitly enabled we must
    # remove the enabling argument from the list.
    return [t for t in tidy_checks if t not in clang_tidy_disable_checks]

def _add_checks_for_dir(directory):
    """should be kept up to date with
    https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/config/tidy.go;l=170;drc=b45a2ea782074944f79fc388df20b06e01f265f7
    """

    # This is a map of local path prefixes to the set of default clang-tidy checks
    # to be used.  This is like android.IsThirdPartyPath, but with more patterns.
    # The last matched local_path_prefix should be the most specific to be used.
    directory_checks = [
        ("external/", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("frameworks/compile/mclinker/", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("hardware/", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("hardware/google/", _TIDY_DEFAULT_GLOBAL_CHECKS),
        ("hardware/interfaces/", _TIDY_DEFAULT_GLOBAL_CHECKS),
        ("hardware/ril/", _TIDY_DEFAULT_GLOBAL_CHECKS),
        ("hardware/libhardware", _TIDY_DEFAULT_GLOBAL_CHECKS),  # all 'hardware/libhardware*'
        ("vendor/", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("vendor/google", _TIDY_DEFAULT_GLOBAL_CHECKS),  # all 'vendor/google*'
        ("vendor/google/external/", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("vendor/google_arc/libs/org.chromium.arc.mojom", _TIDY_EXTERNAL_VENDOR_CHECKS),
        ("vendor/google_devices/", _TIDY_EXTERNAL_VENDOR_CHECKS),  # many have vendor code
    ]

    for d, checks in reversed(directory_checks):
        if directory.startswith(d):
            return checks

    return _TIDY_DEFAULT_GLOBAL_CHECKS

def _add_global_tidy_checks(ctx, local_checks, input_file):
    global_tidy_checks = []
    if product_vars["TidyChecks"]:
        global_tidy_checks = _PRODUCT_VARIABLE_TIDY_CHECKS
    elif not input_file.is_source:
        # don't run clang-tidy for generated files
        global_tidy_checks = _TIDY_DEFAULT_GLOBAL_CHECKS_NO_ANALYZER
    else:
        global_tidy_checks = _add_checks_for_dir(ctx.label.package)

    # If Tidy_checks contains "-*", ignore all checks before "-*".
    for i, check in enumerate(local_checks):
        if check == "-*":
            global_tidy_checks = []
            local_checks = local_checks[i:]

    tidy_checks = global_tidy_checks + _clang_rewrite_tidy_checks(local_checks)
    tidy_checks.extend(_TIDY_GLOBAL_NO_CHECKS)

    #TODO(b/255747672) disable cert check on windows only
    return tidy_checks

def _add_global_tidy_checks_as_errors(tidy_checks_as_errors):
    return tidy_checks_as_errors + _TIDY_GLOBAL_NO_ERROR_CHECKS

def _create_clang_tidy_action(
        ctx,
        clang_tool,
        input_file,
        tidy_checks,
        tidy_checks_as_errors,
        tidy_flags,
        clang_flags,
        headers,
        tidy_timeout):
    tidy_flags = _add_with_tidy_flags(ctx, tidy_flags)
    tidy_flags = _add_header_filter(ctx, tidy_flags)
    tidy_flags = _add_extra_arg_flags(tidy_flags)
    tidy_flags = _add_quiet_if_not_global_tidy(tidy_flags)
    tidy_checks = _add_global_tidy_checks(ctx, tidy_checks, input_file)
    tidy_checks_as_errors = _add_global_tidy_checks_as_errors(tidy_checks_as_errors)

    _check_bad_tidy_checks(tidy_checks)
    _check_bad_tidy_flags(tidy_flags)

    args = ctx.actions.args()
    args.add(input_file)
    if tidy_checks:
        args.add("-checks=" + ",".join(tidy_checks))
    if tidy_checks_as_errors:
        args.add("-warnings-as-errors=" + ",".join(tidy_checks_as_errors))
    if tidy_flags:
        args.add_all(tidy_flags)
    args.add("--")
    args.add_all(clang_flags)

    tidy_file = ctx.actions.declare_file(paths.join(ctx.label.name, input_file.short_path + ".tidy"))
    env = {
        "CLANG_CMD": clang_tool,
        "TIDY_FILE": tidy_file.path,
    }
    if tidy_timeout:
        env["TIDY_TIMEOUT"] = tidy_timeout

    ctx.actions.run(
        inputs = [input_file] + headers,
        outputs = [tidy_file],
        arguments = [args],
        env = env,
        progress_message = "Running clang-tidy on {}".format(input_file.short_path),
        tools = [
            ctx.executable._clang_tidy,
            ctx.executable._clang_tidy_real,
        ],
        executable = ctx.executable._clang_tidy_sh,
        execution_requirements = {
            "no-sandbox": "1",
        },
        mnemonic = "ClangTidy",
    )

    return tidy_file

def generate_clang_tidy_actions(
        ctx,
        flags,
        deps,
        srcs,
        hdrs,
        language,
        tidy_flags,
        tidy_checks,
        tidy_checks_as_errors,
        tidy_timeout):
    """Generates actions for clang tidy

    Args:
        ctx (Context): rule context that is expected to contain
            - ctx.executable._clang_tidy
            - ctx.executable._clang_tidy_sh
            - ctx.executable._clang_tidy_real
            - ctx.label._with_tidy_flags
        flags (list[str]): list of target-specific (non-toolchain) flags passed
            to clang compile action
        deps (list[Target]): list of Targets which provide headers to
            compilation context
        srcs (list[File]): list of srcs to which clang-tidy will be applied
        hdrs (list[File]): list of headers used by srcs. This is used to provide
            explicit inputs to the action
        language (str): must be one of ["c++", "c"]. This is used to decide what
            toolchain arguments are passed to the clang compile action
        tidy_flags (list[str]): additional flags to pass to the clang-tidy tool
        tidy_checks (list[str]): list of checks for clang-tidy to perform
        tidy_checks_as_errors (list[str]): list of checks to pass as
            "-warnings-as-errors" to clang-tidy
        tidy_checks_as_errors (str): timeout to pass to clang-tidy tool
        tidy_timeout (str): timeout in seconds after which to stop a clang-tidy
            invocation
    Returns:
        tidy_file_outputs: (list[File]): list of .tidy files output by the
            clang-tidy.sh tool
    """
    toolchain = find_cpp_toolchain(ctx)
    feature_config = cc_common.configure_features(
        ctx = ctx,
        cc_toolchain = toolchain,
        language = "c++",
        requested_features = ctx.features,
        unsupported_features = ctx.disabled_features,
    )

    language = language
    action_name = ""
    if language == "c++":
        action_name = CPP_COMPILE_ACTION_NAME
    elif language == "c":
        action_name = C_COMPILE_ACTION_NAME
    else:
        fail("invalid language:", language)

    dep_info = cc_common.merge_cc_infos(direct_cc_infos = [d[CcInfo] for d in deps])
    compilation_ctx = dep_info.compilation_context
    args = get_compilation_args(
        toolchain = toolchain,
        feature_config = feature_config,
        flags = flags,
        compilation_ctx = compilation_ctx,
        action_name = action_name,
    )

    clang_tool = cc_common.get_tool_for_action(
        feature_configuration = feature_config,
        action_name = action_name,
    )

    header_inputs = (
        hdrs +
        compilation_ctx.headers.to_list() +
        compilation_ctx.direct_headers +
        compilation_ctx.direct_private_headers +
        compilation_ctx.direct_public_headers +
        compilation_ctx.direct_textual_headers
    )

    tidy_file_outputs = []
    for src in srcs:
        tidy_file = _create_clang_tidy_action(
            ctx = ctx,
            input_file = src,
            headers = header_inputs,
            clang_tool = paths.basename(clang_tool),
            tidy_checks = tidy_checks,
            tidy_checks_as_errors = tidy_checks_as_errors,
            tidy_flags = tidy_flags,
            clang_flags = args,
            tidy_timeout = tidy_timeout,
        )
        tidy_file_outputs.append(tidy_file)

    return tidy_file_outputs

def collect_deps_clang_tidy_info(ctx):
    transitive_clang_tidy_files = []
    for attr_deps in get_dep_targets(ctx.attr, predicate = lambda target: ClangTidyInfo in target).values():
        for dep in attr_deps:
            transitive_clang_tidy_files.append(dep[ClangTidyInfo].transitive_tidy_files)
    return ClangTidyInfo(
        tidy_files = depset(),
        transitive_tidy_files = depset(transitive = transitive_clang_tidy_files),
    )

def _never_tidy_for_dir(directory):
    # should stay up to date with https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/config/tidy.go;l=227;drc=f5864ba3633fdbadfb434483848887438fc11f59
    return directory.startswith("external/grpc-grpc")

def clang_tidy_for_dir(allow_external_vendor, directory):
    return not _never_tidy_for_dir(directory) and (
        allow_external_vendor or _add_checks_for_dir(directory) != _TIDY_EXTERNAL_VENDOR_CHECKS
    )