blob: a8111df96a3f09fb44ef18e95728f7cef97bd2a2 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Defines SHAC checks for C and C++ files."""
load(
"./common.star",
"FORMATTER_MSG",
"cipd_platform_name",
"get_build_dir",
"get_fuchsia_dir",
"os_exec",
)
# Paths excluded from C/C++ formatting and static analysis checks.
_IGNORED_GLOBS = [
"!/build/bazel/fuchsia_idk/validation_data/**",
"!/build/sdk/generate_prebuild_idk/validation_data/**",
"!/src/devices/tools/fidlgen_banjo/tests/**",
"!**/goldens/**",
"!**/third_party/**",
]
# Maximum number of affected C/C++ files to analyze with clang-tidy in a single run.
_MAX_CLANG_TIDY_FILES = 100
def _clang_format(ctx):
"""Formats C/C++/Proto code using clang-format."""
cpp_files = list(ctx.scm.affected_files(glob = [
"*.c",
"*.cc",
"*.cpp",
"*.h",
"*.hh",
"*.hpp",
"*.proto",
] + _IGNORED_GLOBS).keys())
if not cpp_files:
return
fuchsia_dir = get_fuchsia_dir(ctx)
platform = cipd_platform_name(ctx)
clang_format_bin = "%s/prebuilt/third_party/clang/%s/bin/clang-format" % (
fuchsia_dir,
platform,
)
base_cmd = [
clang_format_bin,
"-style=file",
"-fallback-style=Google",
"-sort-includes",
]
batch_size = 500
dry_run_procs = []
for i in range(0, len(cpp_files), batch_size):
batch = cpp_files[i:i + batch_size]
dry_run_procs.append(
os_exec(
ctx,
base_cmd + ["--dry-run", "--Werror", "--ferror-limit=1"] + batch,
ok_retcodes = [0, 1],
),
)
violation_suffix = ": error: code should be clang-formatted [-Wclang-format-violations]"
unformatted = []
for proc in dry_run_procs:
res = proc.wait()
batch_unformatted = []
for line in res.stderr.splitlines():
if line.endswith(violation_suffix):
filepath = line[:-len(violation_suffix)].rsplit(":", 2)[0]
batch_unformatted.append(filepath)
if res.retcode and not batch_unformatted:
fail("clang-format failed:\n%s" % res.stderr)
unformatted.extend(batch_unformatted)
procs = []
for filepath in unformatted:
procs.append((
filepath,
os_exec(ctx, base_cmd + [filepath]),
))
for filepath, proc in procs:
formatted = proc.wait().stdout
# Protobuf string fields in SARIF output (--json-output) require valid UTF-8.
# If a file contains non-UTF-8 bytes (e.g. Latin-1/Windows-1252 comments),
# omit replacements so shac does not crash marshaling sarif.ArtifactContent.text.
is_valid_utf8 = (formatted == "".join(formatted.codepoints()))
if is_valid_utf8:
msg = FORMATTER_MSG
replacements = [formatted]
else:
msg = (
"File not formatted and contains non-UTF-8 bytes. " +
"Convert the file to UTF-8 or add it to _IGNORED_GLOBS " +
"if it should not be formatted."
)
replacements = []
ctx.emit.finding(
# Switch to "error" once existing unformatted files in the tree are cleaned up.
level = "warning",
message = msg,
filepath = filepath,
replacements = replacements,
)
def _header_guards(ctx):
"""Checks and formats C/C++ header guards."""
headers = list(ctx.scm.affected_files(glob = [
"*.h",
] + _IGNORED_GLOBS).keys())
if not headers:
return
fuchsia_dir = get_fuchsia_dir(ctx)
platform = cipd_platform_name(ctx)
python_bin = "%s/prebuilt/third_party/python3/%s/bin/python3" % (
fuchsia_dir,
platform,
)
checker_script = "%s/scripts/shac/check_header_guards.py" % fuchsia_dir
procs = []
for h in headers:
procs.append((
h,
os_exec(
ctx,
[python_bin, checker_script, "--root", fuchsia_dir, "--emit", h],
ok_retcodes = (0, 1),
),
))
for h, proc in procs:
res = proc.wait()
if res.retcode != 0:
ctx.emit.finding(
level = "warning",
message = res.stderr.strip() or ("Header guard issue in %s." % h),
filepath = h,
)
else:
formatted = res.stdout
original = str(ctx.io.read_file(h))
if formatted and formatted != original:
ctx.emit.finding(
level = "warning",
message = FORMATTER_MSG,
filepath = h,
replacements = [formatted],
)
def _clang_tidy(ctx):
"""Runs clang-tidy on C/C++ source files."""
cpp_affected = ctx.scm.affected_files(glob = [
"*.c",
"*.cc",
"*.cpp",
] + _IGNORED_GLOBS)
header_affected = ctx.scm.affected_files(glob = [
"*.h",
"*.hh",
"*.hpp",
] + _IGNORED_GLOBS)
# SHAC's `ctx.scm.affected_files()` populates `meta.action` differently
# depending on how SHAC is invoked:
# 1. `shac check` (default git diff) or `shac check --all`: SHAC uses its
# `gitCheckout` backend, setting `meta.action` to the git diff-filter
# code (e.g. "M", "A") for modified/added files and `""` for untouched
# files. Filtering to non-empty `meta.action` narrows `--all` runs down
# to only the files actually modified in git.
# 2. `shac check <file1> ...` (e.g. `fx lint --files=...`): SHAC uses its
# `specifiedFiles` backend, which does not query git and sets
# `meta.action == ""` on all passed files (even if modified in git).
#
# Therefore, if any file has a non-empty `meta.action`, analyze only those
# modified/added files; otherwise fall back to all returned keys (which handles
# explicit file arguments, while `_MAX_CLANG_TIDY_FILES` below still skips
# `--all` runs when zero C/C++ files were modified in git).
cpp_modified = [f for f, m in cpp_affected.items() if m.action]
header_modified = [f for f, m in header_affected.items() if m.action]
if cpp_modified or header_modified:
cpp_files = cpp_modified
header_files = header_modified
else:
cpp_files = list(cpp_affected.keys())
header_files = list(header_affected.keys())
if not cpp_files and not header_files:
return
# Full AST compilation is computationally expensive (~1-5 seconds per file).
# Skip analysis when the number of target C/C++ files exceeds the threshold
# (such as on large C++ refactors or `--all` runs with no modified C/C++ files).
total_files = len(cpp_files) + len(header_files)
if total_files > _MAX_CLANG_TIDY_FILES:
return
# clang-tidy requires the build output directory to locate compile_commands.json
# and generated headers (such as FIDL bindings). If fuchsia_build_dir is not
# configured, we cannot resolve compilation flags and must exit early.
build_dir_var = ctx.vars.get("fuchsia_build_dir")
if not build_dir_var:
return
fuchsia_dir = get_fuchsia_dir(ctx)
platform = cipd_platform_name(ctx)
python_bin = "%s/prebuilt/third_party/python3/%s/bin/python3" % (
fuchsia_dir,
platform,
)
clang_tidy_bin = "%s/prebuilt/third_party/clang/%s/bin/clang-tidy" % (
fuchsia_dir,
platform,
)
driver_script = "%s/scripts/shac/clang_tidy.py" % fuchsia_dir
build_dir = get_build_dir(ctx)
res = os_exec(
ctx,
[
python_bin,
driver_script,
"--clang-tidy",
clang_tidy_bin,
"--build-dir",
build_dir,
"--root",
ctx.scm.root,
] + cpp_files + header_files,
).wait()
if res.stderr:
print(res.stderr.strip()) # allow-print
for finding in json.decode(res.stdout):
if "line" in finding:
ctx.emit.finding(
level = "warning",
message = finding["message"],
filepath = finding["filepath"],
line = finding["line"],
col = finding["col"],
)
else:
ctx.emit.finding(
level = "warning",
message = finding["message"],
filepath = finding["filepath"],
)
def register_cpp_checks():
shac.register_check(shac.check(_clang_format, formatter = True))
shac.register_check(shac.check(_clang_tidy))
shac.register_check(shac.check(_header_guards, formatter = True))