From 5d11241571f87f9303496a082ef469379ee9b433 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Wed, 26 Nov 2025 16:26:07 -0500 Subject: [PATCH] build: add clang-tidy integration - Add meson infrastructure for clang-tidy in nix-meson-build-support/common/clang-tidy/ with compile_commands.json cleaning and parallel runner - Create custom clang-tidy plugin scaffolding in src/clang-tidy-plugin/ - Add enableClangTidyLayer in packaging/components.nix that runs clang-tidy on all C++ components via postBuild hook - Add clangTidy Hydra job in packaging/hydra.nix - Add clang-tidy CI job in .github/workflows/ci.yml to enforce on PRs - Document usage in doc/manual/source/development/static-analysis.md Uses debug build mode and skips tests for faster CI. Warnings are treated as errors to catch issues early. --- .clang-tidy | 4 +- .github/workflows/ci.yml | 17 +++ doc/manual/source/SUMMARY.md.in | 1 + .../source/development/static-analysis.md | 85 +++++++++++ flake.nix | 4 + meson.build | 3 + .../common/clang-tidy/.clang-tidy | 76 ++++++++++ .../clang-tidy/build_required_targets.py | 63 +++++++++ .../common/clang-tidy/clang-tidy-runner.py | 132 ++++++++++++++++++ .../common/clang-tidy/clean_compdb.py | 80 +++++++++++ .../common/clang-tidy/meson.build | 58 ++++++++ nix-meson-build-support/common/meson.build | 1 + packaging/components.nix | 50 +++++++ packaging/hydra.nix | 15 ++ src/clang-tidy-plugin/.version | 1 + src/clang-tidy-plugin/NixClangTidyChecks.cc | 35 +++++ src/clang-tidy-plugin/meson.build | 31 ++++ src/clang-tidy-plugin/nix-meson-build-support | 1 + src/clang-tidy-plugin/package.nix | 48 +++++++ 19 files changed, 702 insertions(+), 3 deletions(-) mode change 100644 => 120000 .clang-tidy create mode 100644 doc/manual/source/development/static-analysis.md create mode 100644 nix-meson-build-support/common/clang-tidy/.clang-tidy create mode 100755 nix-meson-build-support/common/clang-tidy/build_required_targets.py create mode 100755 nix-meson-build-support/common/clang-tidy/clang-tidy-runner.py create mode 100755 nix-meson-build-support/common/clang-tidy/clean_compdb.py create mode 100755 nix-meson-build-support/common/clang-tidy/meson.build create mode 120000 src/clang-tidy-plugin/.version create mode 100755 src/clang-tidy-plugin/NixClangTidyChecks.cc create mode 100644 src/clang-tidy-plugin/meson.build create mode 120000 src/clang-tidy-plugin/nix-meson-build-support create mode 100644 src/clang-tidy-plugin/package.nix diff --git a/.clang-tidy b/.clang-tidy deleted file mode 100644 index 0887b867087..00000000000 --- a/.clang-tidy +++ /dev/null @@ -1,3 +0,0 @@ -# We use pointers to aggregates in a couple of places, intentionally. -# void * would look weird. -Checks: '-bugprone-sizeof-expression' diff --git a/.clang-tidy b/.clang-tidy new file mode 120000 index 00000000000..871c4b83d57 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1 @@ +nix-meson-build-support/common/clang-tidy/.clang-tidy \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe9d94248d1..004ee9cba06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -271,6 +271,23 @@ jobs: docker tag nix:$NIX_VERSION $IMAGE_ID:master docker push $IMAGE_ID:master + clang-tidy: + needs: basic-checks + runs-on: ubuntu-24.04 + name: clang-tidy + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: ./.github/actions/install-nix-action + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + dogfood: ${{ github.event_name == 'workflow_dispatch' && inputs.dogfood || github.event_name != 'workflow_dispatch' }} + extra_nix_config: "sandbox = true" + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Run clang-tidy + run: nix build .#hydraJobs.clangTidy.x86_64-linux -L + flake_regressions: needs: tests runs-on: ubuntu-24.04 diff --git a/doc/manual/source/SUMMARY.md.in b/doc/manual/source/SUMMARY.md.in index a8e286314e2..15d631e5412 100644 --- a/doc/manual/source/SUMMARY.md.in +++ b/doc/manual/source/SUMMARY.md.in @@ -147,6 +147,7 @@ - [CLI guideline](development/cli-guideline.md) - [JSON guideline](development/json-guideline.md) - [C++ style guide](development/cxx.md) + - [Static Analysis](development/static-analysis.md) - [Experimental Features](development/experimental-features.md) - [Contributing](development/contributing.md) - [Releases](release-notes/index.md) diff --git a/doc/manual/source/development/static-analysis.md b/doc/manual/source/development/static-analysis.md new file mode 100644 index 00000000000..d594fc58fd1 --- /dev/null +++ b/doc/manual/source/development/static-analysis.md @@ -0,0 +1,85 @@ +# Static Analysis + +Nix uses [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) for static analysis of C++ code. +This helps catch bugs, enforce coding standards, and maintain code quality. + +## Running clang-tidy locally + +To run clang-tidy on the entire codebase in the development shell: + +```console +$ nix develop .#native-clangStdenv +$ configurePhase +$ meson compile -C build clang-tidy +``` + +This will analyze all C++ source files and report any warnings. + +To automatically apply fixes for certain warnings: + +```console +$ meson compile -C build clang-tidy-fix +``` + +> **Warning**: Review the changes before committing, as automatic fixes may not always be correct. + +## CI integration + +clang-tidy runs automatically on every pull request via GitHub Actions. +The CI job builds `.#hydraJobs.clangTidy.x86_64-linux` which: + +1. Builds all components with debug mode (for faster compilation) +2. Runs clang-tidy on each component +3. Fails if any warnings are found (warnings are treated as errors) + +## Configuration + +The clang-tidy configuration is in `.clang-tidy` at the repository root (symlinked from `nix-meson-build-support/common/clang-tidy/.clang-tidy`). + +### Suppressing warnings + +If a warning is a false positive, you can suppress it in several ways: + +1. **Inline suppression** (preferred for specific cases): + ```cpp + // NOLINTBEGIN(bugprone-some-check) + ... code ... + // NOLINTEND(bugprone-some-check) + ``` + Or for a single line: + ```cpp + int x = something(); // NOLINT(bugprone-some-check) + ``` + +2. **Configuration file** (for project-wide suppression): + Add the check to the disabled list in `.clang-tidy`: + ```yaml + Checks: + - -bugprone-some-check # Reason for disabling + ``` + +3. **Check options** (for configuring check behavior): + ```yaml + CheckOptions: + bugprone-reserved-identifier.AllowedIdentifiers: '__some_identifier' + ``` + +### Adding new checks + +To enable additional checks: + +1. Edit `nix-meson-build-support/common/clang-tidy/.clang-tidy` +2. Add the check to the `Checks` list +3. Run clang-tidy locally to see the impact +4. Fix any new warnings or disable specific sub-checks if needed + +## Custom clang-tidy plugin + +The Nix project includes infrastructure for custom clang-tidy checks in `src/clang-tidy-plugin/`. +These checks can enforce Nix-specific coding patterns that aren't covered by standard clang-tidy checks. + +To add a new custom check: + +1. Add the check implementation in `src/clang-tidy-plugin/` +2. Register it in `NixClangTidyChecks.cc` +3. Enable it in `.clang-tidy` with the `nix-` prefix diff --git a/flake.nix b/flake.nix index 08f5189832c..15e9c131059 100644 --- a/flake.nix +++ b/flake.nix @@ -424,6 +424,10 @@ "nix-perl-bindings" = { supportsCross = false; }; + + "nix-clang-tidy-plugin" = { + supportsCross = false; + }; } ( pkgName: diff --git a/meson.build b/meson.build index c072a482163..a2bfb238104 100644 --- a/meson.build +++ b/meson.build @@ -66,3 +66,6 @@ endif if get_option('kaitai-struct-checks') subproject('kaitai-struct-checks') endif + +# Static Analysis +subdir('nix-meson-build-support/common/clang-tidy') diff --git a/nix-meson-build-support/common/clang-tidy/.clang-tidy b/nix-meson-build-support/common/clang-tidy/.clang-tidy new file mode 100644 index 00000000000..ffea27bf936 --- /dev/null +++ b/nix-meson-build-support/common/clang-tidy/.clang-tidy @@ -0,0 +1,76 @@ +UseColor: true +Checks: + - -* + - bugprone-* + # Too many warnings + - -bugprone-assignment-in-if-condition + # Too many warnings + - -bugprone-narrowing-conversions + # Kind of nonsense + - -bugprone-easily-swappable-parameters + # Too many warnings for now + - -bugprone-implicit-widening-of-multiplication-result + # Exception handling patterns in Nix + - -bugprone-empty-catch + # Many warnings + - -bugprone-unchecked-optional-access + # Many warnings, questionable lint + - -bugprone-branch-clone + # Extremely noisy before clang 19: https://github.com/llvm/llvm-project/issues/93959 + - -bugprone-multi-level-implicit-pointer-conversion + # We don't compile out our asserts + - -bugprone-assert-side-effect + # TODO: figure out if this warning is useful + - -bugprone-exception-escape + # We use pointers to aggregates intentionally; void * would look weird + - -bugprone-sizeof-expression + # + # Checks disabled to pass on current codebase (can be progressively enabled): + # + # 11 warnings - optional value conversions in various places + - -bugprone-optional-value-conversion + # 4 warnings - switches without default cases + - -bugprone-switch-missing-default-case + # 4 warnings - string_view::data() usage patterns + - -bugprone-suspicious-stringview-data-usage + # 4 warnings - .cc files included in other files (intentional pattern) + - -bugprone-suspicious-include + # 2 warnings - unused return values (AllowCastToVoid helps but some remain) + - -bugprone-unused-return-value + # 2 warnings - unused local RAII-style variables + - -bugprone-unused-local-non-trivial-variable + # 2 warnings - returning const& from parameter + - -bugprone-return-const-ref-from-parameter + # 1 warning - unsafe C functions (e.g., getenv) + - -bugprone-unsafe-functions + # 1 warning - signed char misuse + - -bugprone-signed-char-misuse + # 1 warning - calling parent virtual instead of override + - -bugprone-parent-virtual-call + # 1 warning - null termination issues + - -bugprone-not-null-terminated-result + # 1 warning - macro parentheses + - -bugprone-macro-parentheses + # 1 warning - increment/decrement in conditions + - -bugprone-inc-dec-in-conditions + # + # Non-bugprone checks (also disabled to pass on current codebase): + # + # 4 warnings - exceptions not derived from std::exception + # All thrown exceptions must derive from std::exception + # - hicpp-exception-baseclass + # 88 warnings - C-style casts should be explicit about intent + # - cppcoreguidelines-pro-type-cstyle-cast + # Capturing async lambdas are dangerous (no warnings, kept enabled) + - cppcoreguidelines-avoid-capturing-lambda-coroutines + # Custom nix checks (when added) + - nix-* + +CheckOptions: + # __asan_default_options: ASAN runtime configuration function (see nix-meson-build-support/common/asan-options/) + # __wrap___assert_fail: Linker-wrapped assert handler for better stack traces (see nix-meson-build-support/common/assert-fail/) + # _SingleDerivedPathRaw, _DerivedPathRaw: Internal type aliases in derived-path.hh (leading underscore pattern) + # _SingleBuiltPathRaw, _BuiltPathRaw: Internal type aliases in built-path.hh (leading underscore pattern) + bugprone-reserved-identifier.AllowedIdentifiers: '__asan_default_options;__wrap___assert_fail;_SingleDerivedPathRaw;_DerivedPathRaw;_SingleBuiltPathRaw;_BuiltPathRaw' + # Allow explicitly discarding return values with (void) cast + bugprone-unused-return-value.AllowCastToVoid: true diff --git a/nix-meson-build-support/common/clang-tidy/build_required_targets.py b/nix-meson-build-support/common/clang-tidy/build_required_targets.py new file mode 100755 index 00000000000..da7213a60ed --- /dev/null +++ b/nix-meson-build-support/common/clang-tidy/build_required_targets.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Builds all generated files before running clang-tidy. + +clang-tidy needs all source files and headers to exist before it can +analyze the code. This script queries Ninja for all custom command +targets that generate files needed for analysis and builds them. + +Generated files include: +- .gen.hh: Embedded file headers (SQL schemas, Nix expressions, etc.) +- .gen.inc: Generated include files +- lexer-tab.cc, parser-tab.cc: Flex/Bison generated parsers +- Perl XS bindings, Kaitai parsers, and other generated sources + +See: https://github.com/mesonbuild/meson/issues/12817 +""" + +import subprocess + + +def get_targets_of_rule(build_root: str, rule_name: str) -> list[str]: + return ( + subprocess.check_output( + ["ninja", "-C", build_root, "-t", "targets", "rule", rule_name] + ) + .decode() + .strip() + .splitlines() + ) + + +def ninja_build(build_root: str, targets: list[str]): + if targets: + subprocess.check_call(["ninja", "-C", build_root, "--", *targets]) + + +def main(): + import argparse + + ap = argparse.ArgumentParser(description="Build required targets for clang-tidy") + ap.add_argument("build_root", help="Ninja build root", type=str) + + args = ap.parse_args() + + custom_commands = get_targets_of_rule(args.build_root, "CUSTOM_COMMAND") + + targets = ( + # Generated headers from embedded files + [t for t in custom_commands if t.endswith(".gen.hh")] + # Generated include files + + [t for t in custom_commands if t.endswith(".gen.inc")] + # Flex/Bison generated parsers + + [t for t in custom_commands if t.endswith("-tab.cc")] + # Kaitai Struct generated parsers + + [t for t in custom_commands if t.endswith(".cpp") and "kaitai" in t] + # Perl XS generated bindings + + [t for t in custom_commands if t.endswith(".cc") and "perl" in t.lower()] + ) + ninja_build(args.build_root, targets) + + +if __name__ == "__main__": + main() diff --git a/nix-meson-build-support/common/clang-tidy/clang-tidy-runner.py b/nix-meson-build-support/common/clang-tidy/clang-tidy-runner.py new file mode 100755 index 00000000000..3f29e814a0d --- /dev/null +++ b/nix-meson-build-support/common/clang-tidy/clang-tidy-runner.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +Wrapper around run-clang-tidy with better UX. + +This script handles: +- Loading the custom nix-clang-tidy plugin +- Setting up header filters to only check project headers +- Managing parallelism based on available CPUs +- Working around nixpkgs Python interpreter patching issues +""" + +import multiprocessing +import os +import sys +from pathlib import Path + + +def default_concurrency(): + return min( + multiprocessing.cpu_count(), int(os.environ.get("NIX_BUILD_CORES", "16")) + ) + + +def go( + exe: str, + plugin_path: Path | None, + config_file: Path | None, + compile_commands_json_dir: Path, + jobs: int, + paths: list[Path], + werror: bool, + fix: bool, +): + args = [ + # Explicitly invoke with python because of a nixpkgs bug where + # clang-unwrapped does not patch interpreters in run-clang-tidy. + sys.executable, + exe, + "-quiet", + ] + + if plugin_path is not None: + args += ["-load", str(plugin_path)] + + if config_file is not None: + args += ["-config-file", str(config_file)] + + args += [ + "-p", + str(compile_commands_json_dir), + "-j", + str(jobs), + # Only check headers in the nix include directories + "-header-filter", + r"nix/[^/]+/.*\.hh", + ] + + if werror: + args += ["-warnings-as-errors", "*"] + if fix: + args += ["-fix"] + + args += ["--"] + args += [str(p) for p in paths] + + os.execvp(sys.executable, args) + + +def main(): + import argparse + + ap = argparse.ArgumentParser(description="Run clang-tidy on the Nix codebase") + ap.add_argument( + "--jobs", + "-j", + type=int, + default=default_concurrency(), + help="Parallel linting jobs to run", + ) + ap.add_argument( + "--plugin-path", + type=Path, + default=None, + help="Path to the nix-clang-tidy plugin", + ) + ap.add_argument( + "--config-file", + type=Path, + default=None, + help="Path to the .clang-tidy config file", + ) + ap.add_argument( + "--compdb-path", + type=Path, + help="Path to the directory containing the cleaned compilation database", + ) + ap.add_argument( + "--werror", + action="store_true", + help="Treat warnings as errors", + ) + ap.add_argument( + "--fix", + action="store_true", + help="Apply fixes for warnings", + ) + ap.add_argument( + "--run-clang-tidy-path", + default="run-clang-tidy", + help="Path to run-clang-tidy", + ) + ap.add_argument( + "paths", + nargs="*", + help="Source paths to check", + ) + args = ap.parse_args() + + go( + args.run_clang_tidy_path, + args.plugin_path, + args.config_file, + args.compdb_path, + args.jobs, + args.paths, + args.werror, + args.fix, + ) + + +if __name__ == "__main__": + main() diff --git a/nix-meson-build-support/common/clang-tidy/clean_compdb.py b/nix-meson-build-support/common/clang-tidy/clean_compdb.py new file mode 100755 index 00000000000..1caeef92d7f --- /dev/null +++ b/nix-meson-build-support/common/clang-tidy/clean_compdb.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Strips PCH (precompiled header) arguments from a compilation database. + +This is needed because clang-tidy cannot process PCH flags when using +nixpkgs' cc-wrapper. The cc-wrapper makes assumptions that don't hold +when the compiler is being used indirectly by clang-tidy. + +See: https://github.com/mesonbuild/meson/issues/13499 +""" + +import json +import shlex + + +def process_compdb(compdb: list[dict]) -> list[dict]: + def munch_command(args: list[str]) -> list[str]: + out = [] + eat_next = False + for i, arg in enumerate(args): + if arg in ["-fpch-preprocess", "-fpch-instantiate-templates"]: + # -fpch-preprocess as used with gcc + # -fpch-instantiate-templates as used by clang + continue + elif arg == "-include-pch" or ( + arg == "-include" + and i + 1 < len(args) + and args[i + 1] == "precompiled-headers.hh" + ): + # -include-pch some-pch (clang), or -include some-pch (gcc) + eat_next = True + continue + if not eat_next: + out.append(arg) + eat_next = False + return out + + def chomp(item: dict) -> dict: + item = item.copy() + item["command"] = shlex.join(munch_command(shlex.split(item["command"]))) + return item + + def cmdfilter(item: dict) -> bool: + file = item["file"] + # Filter out precompiled header files + if file.endswith("precompiled-headers.hh"): + return False + # Filter out Rust files + if file.endswith(".rs"): + return False + # Filter out Kaitai Struct generated code (uses reserved identifiers) + if "kaitai" in file: + return False + # Filter out Flex/Bison generated parsers (generated code) + if file.endswith("-tab.cc"): + return False + # Filter out Perl XS generated bindings (generated code) + if "/perl/" in file and file.endswith(".cc"): + return False + return True + + return [chomp(x) for x in compdb if cmdfilter(x)] + + +def main(): + import argparse + + ap = argparse.ArgumentParser( + description="Strip PCH arguments from compilation database" + ) + ap.add_argument("input", type=argparse.FileType("r"), help="Input json file") + ap.add_argument("output", type=argparse.FileType("w"), help="Output json file") + args = ap.parse_args() + + input_json = json.load(args.input) + json.dump(process_compdb(input_json), args.output, indent=2) + + +if __name__ == "__main__": + main() diff --git a/nix-meson-build-support/common/clang-tidy/meson.build b/nix-meson-build-support/common/clang-tidy/meson.build new file mode 100755 index 00000000000..9fba4bd0a12 --- /dev/null +++ b/nix-meson-build-support/common/clang-tidy/meson.build @@ -0,0 +1,58 @@ +# clang-tidy integration for the Nix project +# +# This provides the 'clang-tidy' run_target for per-component builds. +# The custom plugin (nix-clang-tidy) is built as a separate component +# and loaded automatically when available. + +python = find_program('python3') +run_clang_tidy = find_program('run-clang-tidy', required : false) + +# Find the custom clang-tidy plugin if available +nix_clang_tidy_plugin = cxx.find_library('nix-clang-tidy', required : false) + +# Clean the compilation database by stripping PCH arguments. +# This is needed because clang-tidy cannot process PCH flags from cc-wrapper. +# See: https://github.com/mesonbuild/meson/issues/13499 +meson.add_postconf_script( + python, + meson.current_source_dir() / 'clean_compdb.py', + meson.global_build_root() / 'compile_commands.json', + meson.current_build_dir() / 'compile_commands.json', +) + +if run_clang_tidy.found() + run_clang_tidy_args = [ + meson.current_source_dir() / 'clang-tidy-runner.py', + '--run-clang-tidy-path', + run_clang_tidy, + '--compdb-path', + meson.current_build_dir(), + '--config-file', + meson.current_source_dir() / '.clang-tidy', + ] + + if nix_clang_tidy_plugin.found() + run_clang_tidy_args += [ + '--plugin-path', + nix_clang_tidy_plugin.full_path(), + ] + endif + + run_target( + 'clang-tidy', + command : [ + python, + run_clang_tidy_args, + '--werror', + ], + ) + + run_target( + 'clang-tidy-fix', + command : [ + python, + run_clang_tidy_args, + '--fix', + ], + ) +endif diff --git a/nix-meson-build-support/common/meson.build b/nix-meson-build-support/common/meson.build index 5fcf557e70b..7f3907c7feb 100644 --- a/nix-meson-build-support/common/meson.build +++ b/nix-meson-build-support/common/meson.build @@ -67,3 +67,4 @@ nix_soversion = meson.project_version().split('+')[0].split('pre')[0] subdir('assert-fail') subdir('asan-options') +subdir('clang-tidy') diff --git a/packaging/components.nix b/packaging/components.nix index bbd6208b90d..8671c366b1f 100644 --- a/packaging/components.nix +++ b/packaging/components.nix @@ -223,6 +223,44 @@ let ); }; + enableClangTidyLayer = + finalAttrs: prevAttrs: + let + # Components without clang-tidy infrastructure (non-C++ or special builds) + excludedComponents = [ + "nix-perl" + "nix-manual" + "nix-internal-api-docs" + "nix-external-api-docs" + "nix-json-schema-checks" + "nix-kaitai-struct-checks" + "nix-functional-tests" + ]; + shouldRunClangTidy = !builtins.elem (prevAttrs.pname or "") excludedComponents; + in + lib.optionalAttrs (scope.withClangTidy && shouldRunClangTidy) { + nativeBuildInputs = + (prevAttrs.nativeBuildInputs or [ ]) + ++ [ + pkgs.buildPackages.llvmPackages.clang-tools # provides run-clang-tidy + ] + ++ lib.optionals (prevAttrs.pname or "" != "nix-clang-tidy-plugin") [ + scope.nix-clang-tidy-plugin + ]; + + # Use debug build for faster compilation (no optimizations) + mesonBuildType = "debug"; + + # Skip tests - we only care about clang-tidy results + doCheck = false; + + # Run clang-tidy after the normal build + postBuild = (prevAttrs.postBuild or "") + '' + echo "Running clang-tidy on ${finalAttrs.pname}..." + ninja clang-tidy + ''; + }; + nixDefaultsLayer = finalAttrs: prevAttrs: { strictDeps = prevAttrs.strictDeps or true; enableParallelBuilding = true; @@ -275,6 +313,11 @@ in */ withUBSan = false; + /** + Whether meson components are checked with [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). + */ + withClangTidy = false; + /** A user-provided extension function to apply to each component derivation. */ @@ -362,6 +405,7 @@ in mesonLayer fixupStaticLayer enableSanitizersLayer + enableClangTidyLayer scope.mesonComponentOverrides ]; mkMesonExecutable = mkPackageBuilder [ @@ -373,6 +417,7 @@ in mesonBuildLayer fixupStaticLayer enableSanitizersLayer + enableClangTidyLayer scope.mesonComponentOverrides ]; mkMesonLibrary = mkPackageBuilder [ @@ -385,6 +430,7 @@ in mesonLibraryLayer fixupStaticLayer enableSanitizersLayer + enableClangTidyLayer scope.mesonComponentOverrides ]; @@ -450,6 +496,10 @@ in nix-perl-bindings = callPackage ../src/perl/package.nix { }; + nix-clang-tidy-plugin = callPackage ../src/clang-tidy-plugin/package.nix { + llvmPackages = pkgs.buildPackages.llvmPackages; + }; + /** Combined package that has the CLI, libraries, and (assuming non-cross, no overrides) it requires that all tests succeed. */ diff --git a/packaging/hydra.nix b/packaging/hydra.nix index 67e2c0dfd65..fa4cea03c1c 100644 --- a/packaging/hydra.nix +++ b/packaging/hydra.nix @@ -64,6 +64,7 @@ let "nix-functional-tests" "nix-json-schema-checks" "nix-kaitai-struct-checks" + "nix-clang-tidy-plugin" ] ++ lib.optionals enableBindings [ "nix-perl-bindings" @@ -172,6 +173,20 @@ rec { in forAllPackages (pkgName: forAllSystems (system: components.${system}.${pkgName})); + # Static analysis with clang-tidy + clangTidy = lib.genAttrs linux64BitSystems ( + system: + let + pkgs = nixpkgsFor.${system}.nativeForStdenv.clangStdenv; + tidyScope = pkgs.nixComponents2.overrideScope ( + self: super: { + withClangTidy = true; + } + ); + in + tidyScope.nix-everything + ); + buildNoTests = forAllSystems (system: nixpkgsFor.${system}.native.nixComponents2.nix-cli); # Toggles some settings for better coverage. Windows needs these diff --git a/src/clang-tidy-plugin/.version b/src/clang-tidy-plugin/.version new file mode 120000 index 00000000000..b7badcd0cc8 --- /dev/null +++ b/src/clang-tidy-plugin/.version @@ -0,0 +1 @@ +../../.version \ No newline at end of file diff --git a/src/clang-tidy-plugin/NixClangTidyChecks.cc b/src/clang-tidy-plugin/NixClangTidyChecks.cc new file mode 100755 index 00000000000..a6d503e8319 --- /dev/null +++ b/src/clang-tidy-plugin/NixClangTidyChecks.cc @@ -0,0 +1,35 @@ +/** + * @file NixClangTidyChecks.cc + * @brief Custom clang-tidy checks for the Nix project. + * + * This module registers custom clang-tidy checks specific to the Nix codebase. + * To add a new check: + * 1. Create CheckName.hh and CheckName.cc in this directory + * 2. Include the header here + * 3. Register the check in addCheckFactories() + * 4. Add the source file to meson.build + * 5. Enable the check in .clang-tidy (e.g., nix-checkname) + */ + +#include +#include + +namespace nix::clang_tidy { + +using namespace clang; +using namespace clang::tidy; + +class NixClangTidyChecks : public ClangTidyModule +{ +public: + void addCheckFactories([[maybe_unused]] ClangTidyCheckFactories & CheckFactories) override + { + // Custom checks will be registered here. + // Example: + // CheckFactories.registerCheck("nix-my-custom-check"); + } +}; + +static ClangTidyModuleRegistry::Add X("nix-module", "Adds Nix-specific checks"); + +} // namespace nix::clang_tidy diff --git a/src/clang-tidy-plugin/meson.build b/src/clang-tidy-plugin/meson.build new file mode 100644 index 00000000000..39d9426d934 --- /dev/null +++ b/src/clang-tidy-plugin/meson.build @@ -0,0 +1,31 @@ +project( + 'nix-clang-tidy-plugin', + 'cpp', + version : files('.version'), + default_options : [ + 'cpp_std=c++23', + 'warning_level=2', + ], + meson_version : '>= 1.1', + license : 'LGPL-2.1-or-later', +) + +cxx = meson.get_compiler('cpp') + +subdir('nix-meson-build-support/deps-lists') +subdir('nix-meson-build-support/common') + +# clang-tidy plugins require LLVM (clang headers provided via buildInputs) +llvm_dep = dependency('LLVM', version : '>= 16', required : true) + +sources = files( + 'NixClangTidyChecks.cc', +) + +# Build as a shared module (plugin) that can be loaded by clang-tidy --load +nix_clang_tidy_plugin = shared_module( + 'nix-clang-tidy', + sources, + dependencies : [ llvm_dep ], + install : true, +) diff --git a/src/clang-tidy-plugin/nix-meson-build-support b/src/clang-tidy-plugin/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/clang-tidy-plugin/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/clang-tidy-plugin/package.nix b/src/clang-tidy-plugin/package.nix new file mode 100644 index 00000000000..d2bced848c2 --- /dev/null +++ b/src/clang-tidy-plugin/package.nix @@ -0,0 +1,48 @@ +{ + lib, + stdenv, + mkMesonDerivation, + + pkg-config, + llvmPackages, + + # Configuration Options + + version, +}: + +let + inherit (lib) fileset; +in + +mkMesonDerivation (finalAttrs: { + pname = "nix-clang-tidy-plugin"; + inherit version; + + workDir = ./.; + fileset = fileset.unions [ + ../../nix-meson-build-support + ./nix-meson-build-support + ../../.version + ./.version + ./meson.build + (fileset.fileFilter (file: file.hasExt "cc") ./.) + (fileset.fileFilter (file: file.hasExt "hh") ./.) + ]; + + nativeBuildInputs = [ + pkg-config + llvmPackages.llvm.dev + ]; + + buildInputs = [ + llvmPackages.libclang + llvmPackages.clang-unwrapped.dev # for ClangTidyModule.h + ]; + + meta = { + description = "Custom clang-tidy checks for the Nix codebase"; + platforms = lib.platforms.unix; + broken = !stdenv.cc.isClang; + }; +})