diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index ef22bae2..8c4cb7c5 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -52,4 +52,9 @@ stardoc_with_diff_test(
bzl_library_target = "//lint:ktlint",
)
+stardoc_with_diff_test(
+ name = "clang-tidy",
+ bzl_library_target = "//lint:clang_tidy",
+)
+
update_docs(name = "update")
diff --git a/docs/clang-tidy.md b/docs/clang-tidy.md
new file mode 100644
index 00000000..52c74ac8
--- /dev/null
+++ b/docs/clang-tidy.md
@@ -0,0 +1,136 @@
+
+
+API for calling declaring a clang-tidy lint aspect.
+
+Typical usage:
+
+First, install clang-tidy with llvm_toolchain or as a native binary (llvm_toolchain
+does not support Windows as of 06/2024, but providing a native clang-tidy.exe works)
+
+Next, declare a binary target for it, typically in `tools/lint/BUILD.bazel`:
+
+e.g. using llvm_toolchain:
+```starlark
+native_binary(
+ name = "clang_tidy",
+ src = "@llvm_toolchain_llvm//:bin/clang-tidy"
+ out = "clang_tidy",
+)
+```
+
+e.g as native binary:
+```starlark
+native_binary(
+ name = "clang_tidy",
+ src = "clang-tidy.exe"
+ out = "clang_tidy",
+)
+```
+
+Finally, create the linter aspect, typically in `tools/lint/linters.bzl`:
+
+```starlark
+load("@aspect_rules_lint//lint:clang_tidy.bzl", "clang_tidy_aspect")
+
+clang_tidy = clang_tidy_aspect(
+ binary = "@@//path/to:clang-tidy",
+ configs = "@@//path/to:.clang-tidy",
+)
+```
+
+
+
+
+## clang_tidy_action
+
+
+clang_tidy_action(ctx, compilation_context, executable, srcs, stdout, exit_code)
+
+
+Create a Bazel Action that spawns a clang-tidy process.
+
+Adapter for wrapping Bazel around
+https://clang.llvm.org/extra/clang-tidy/
+
+
+**PARAMETERS**
+
+
+| Name | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| ctx | an action context OR aspect context | none |
+| compilation_context | from target | none |
+| executable | struct with a clang-tidy field | none |
+| srcs | file objects to lint | none |
+| stdout | output file containing the stdout or --output-file of clang-tidy | none |
+| exit_code | output file containing the exit code of clang-tidy. If None, then fail the build when clang-tidy exits non-zero. | none |
+
+
+
+
+## clang_tidy_fix
+
+
+clang_tidy_fix(ctx, compilation_context, executable, srcs, patch, stdout, exit_code)
+
+
+Create a Bazel Action that spawns clang-tidy with --fix.
+
+**PARAMETERS**
+
+
+| Name | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| ctx | an action context OR aspect context | none |
+| compilation_context | from target | none |
+| executable | struct with a clang_tidy field | none |
+| srcs | list of file objects to lint | none |
+| patch | output file containing the applied fixes that can be applied with the patch(1) command. | none |
+| stdout | output file containing the stdout or --output-file of clang-tidy | none |
+| exit_code | output file containing the exit code of clang-tidy | none |
+
+
+
+
+## is_parent_in_list
+
+
+is_parent_in_list(dir, list)
+
+
+
+
+**PARAMETERS**
+
+
+| Name | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| dir | -
| none |
+| list | -
| none |
+
+
+
+
+## lint_clang_tidy_aspect
+
+
+lint_clang_tidy_aspect(binary, configs, global_config, header_filter, lint_target_headers,
+ angle_includes_are_system, verbose)
+
+
+A factory function to create a linter aspect.
+
+**PARAMETERS**
+
+
+| Name | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| binary | the clang-tidy binary, typically a rule like
starlark native_binary( name = "clang_tidy", src = "clang-tidy.exe" out = "clang_tidy", )
| none |
+| configs | labels of the .clang-tidy files to make available to clang-tidy's config search. These may be in subdirectories and clang-tidy will apply them if appropriate. This may also include .clang-format files which may be used for formatting fixes. | []
|
+| global_config | label of a single global .clang-tidy file to pass to clang-tidy on the command line. This will cause clang-tidy to ignore any other config files in the source directories. | []
|
+| header_filter | optional, set to a posix regex to supply to clang-tidy with the -header-filter option | ""
|
+| lint_target_headers | optional, set to True to pass a pattern that includes all headers with the target's directory prefix. This crude control may include headers from the linted target in the results. If supplied, overrides the header_filter option. | False
|
+| angle_includes_are_system | controls how angle includes are passed to clang-tidy. By default, Bazel passes these as -isystem. Change this to False to pass these as -I, which allows clang-tidy to regard them as regular header files. | True
|
+| verbose | print debug messages including clang-tidy command lines being invoked. | False
|
+
+
diff --git a/example/.bazelrc b/example/.bazelrc
index a3898fb8..1913390b 100644
--- a/example/.bazelrc
+++ b/example/.bazelrc
@@ -1,6 +1,22 @@
+# Automatically apply --config=linux, --config=windows etc
+common --enable_platform_specific_config
+
# Don't depend on a JAVA_HOME pointing at a system JDK
# see https://github.com/bazelbuild/rules_jvm_external/issues/445
build --repo_env=JAVA_HOME=../bazel_tools/jdk
common --incompatible_enable_proto_toolchain_resolution
common --@aspect_rules_ts//ts:skipLibCheck=always
+
+# enable symlinks on windows
+startup --windows_enable_symlinks
+
+# c++ options
+common:linux --action_env=BAZEL_CXXOPTS="-std=c++20"
+common:windows --action_env=BAZEL_CXXOPTS="/std:c++20"
+
+# clang-tidy needs the INCLUDE variable set to the system value.
+# Would prefer to do this inside the aspect, not sure if it is possible.
+common --action_env=INCLUDE
+# ensure that minimal other envvars are passed by clang-tidy run_shell
+common --incompatible_strict_action_env
diff --git a/example/.clang-tidy b/example/.clang-tidy
new file mode 100644
index 00000000..b2b110c0
--- /dev/null
+++ b/example/.clang-tidy
@@ -0,0 +1,19 @@
+# https://clang.llvm.org/extra/clang-tidy/
+# config-file to provoke some checks in example
+---
+Checks: "*,
+ -abseil-*,
+ -altera-*,
+ -android-*,
+ -fuchsia-*,
+ -google-*,
+ -llvm*,
+ -modernize-use-trailing-return-type,
+ -zircon-*,
+ -readability-else-after-return,
+ -readability-static-accessed-through-instance,
+ -readability-avoid-const-params-in-decls,
+ -cppcoreguidelines-non-private-member-variables-in-classes,
+ -misc-non-private-member-variables-in-classes,"
+# promote a random warning to error to test integration. Do not copy this line.
+WarningsAsErrors: "readability-inconsistent-declaration-parameter-name"
diff --git a/example/BUILD.bazel b/example/BUILD.bazel
index 1fd5ed82..46fcf6c7 100644
--- a/example/BUILD.bazel
+++ b/example/BUILD.bazel
@@ -22,6 +22,7 @@ exports_files(
".vale.ini",
".editorconfig",
"ktlint-baseline.xml",
+ ".clang-tidy",
],
visibility = ["//visibility:public"],
)
diff --git a/example/MODULE.bazel b/example/MODULE.bazel
index 9ca16283..1a6c9461 100644
--- a/example/MODULE.bazel
+++ b/example/MODULE.bazel
@@ -17,6 +17,7 @@ bazel_dep(name = "rules_rust", version = "0.45.1")
bazel_dep(name = "buildifier_prebuilt", version = "6.3.3")
bazel_dep(name = "platforms", version = "0.0.8")
bazel_dep(name = "rules_kotlin", version = "1.9.0")
+bazel_dep(name = "rules_cc", version = "0.0.9")
local_path_override(
module_name = "aspect_rules_lint",
diff --git a/example/lint.sh b/example/lint.sh
index ebbb4bbf..f4e6c199 100755
--- a/example/lint.sh
+++ b/example/lint.sh
@@ -22,9 +22,26 @@ fix=""
buildevents=$(mktemp)
filter='.namedSetOfFiles | values | .files[] | select(.name | endswith($ext)) | ((.pathPrefix | join("/")) + "/" + .name)'
+unameOut="$(uname -s)"
+case "${unameOut}" in
+ Linux*) machine=Linux;;
+ Darwin*) machine=Mac;;
+ CYGWIN*) machine=Windows;;
+ MINGW*) machine=Windows;;
+ MSYS_NT*) machine=Windows;;
+ *) machine="UNKNOWN:${unameOut}"
+esac
+
+args=()
+if [ $machine == "Windows" ]; then
+ # avoid missing linters on windows platform
+ args=("--aspects=$(echo //tools/lint:linters.bzl%{flake8,pmd,ruff,vale,clang_tidy} | tr ' ' ',')")
+else
+ args=("--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,ktlint,pmd,ruff,shellcheck,vale,clang_tidy} | tr ' ' ',')")
+fi
+
# NB: perhaps --remote_download_toplevel is needed as well with remote execution?
-args=(
- "--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,ktlint,pmd,ruff,shellcheck,vale} | tr ' ' ',')"
+args+=(
# Allow lints of code that fails some validation action
# See https://github.com/aspect-build/rules_ts/pull/574#issuecomment-2073632879
"--norun_validations"
@@ -59,7 +76,12 @@ fi
bazel build ${args[@]} $@
# TODO: Maybe this could be hermetic with bazel run @aspect_bazel_lib//tools:jq or sth
-valid_reports=$(jq --arg ext report --raw-output "$filter" "$buildevents")
+if [ $machine == "Windows" ]; then
+ # jq on windows outputs CRLF which breaks this script. https://github.com/jqlang/jq/issues/92
+ valid_reports=$(jq --arg ext report --raw-output "$filter" "$buildevents" | tr -d '\r')
+else
+ valid_reports=$(jq --arg ext report --raw-output "$filter" "$buildevents")
+fi
# Show the results.
while IFS= read -r report; do
@@ -77,7 +99,7 @@ done <<<"$valid_reports"
# [*] 1 fixable with the `--fix` option.
# so that the naive thing of pasting that flag to lint.sh will do what the user expects.
if [ -n "$fix" ]; then
- valid_patches=$(jq --arg ext patch --raw-output "$filter" "$buildevents")
+ valid_patches=$valid_reports
while IFS= read -r patch; do
# Exclude coverage reports, and check if the report is empty.
if [[ "$patch" == *coverage.dat ]] || [[ ! -s "$patch" ]]; then
diff --git a/example/src/cpp/lib/BUILD b/example/src/cpp/lib/BUILD
new file mode 100644
index 00000000..b49df3b1
--- /dev/null
+++ b/example/src/cpp/lib/BUILD
@@ -0,0 +1,18 @@
+load("@rules_cc//cc:defs.bzl", "cc_library")
+
+exports_files(["get/.clang-tidy"])
+
+cc_library(
+ name = "hello-time",
+ srcs = [
+ "get/get-time.cc",
+ "get/get-time.h",
+ "hello-time.cc",
+ ],
+ hdrs = [
+ "hello-time.h",
+ "xhello-time.h",
+ ],
+ includes = ["."],
+ visibility = ["//src/cpp/main:__pkg__"],
+)
diff --git a/example/src/cpp/lib/get/.clang-tidy b/example/src/cpp/lib/get/.clang-tidy
new file mode 100644
index 00000000..2826f037
--- /dev/null
+++ b/example/src/cpp/lib/get/.clang-tidy
@@ -0,0 +1,7 @@
+# https://clang.llvm.org/extra/clang-tidy/
+# this is an example of a .clang-tidy file in a subdirectory that overrides
+# the configuration for files in this directory and below. It must be made
+# visibile through an export_files declaration in this package and added
+# to the 'configs' attribute in clang_tidy aspect.
+---
+Checks: "-*, misc-const-correctness"
diff --git a/example/src/cpp/lib/get/get-time.cc b/example/src/cpp/lib/get/get-time.cc
new file mode 100644
index 00000000..864174fa
--- /dev/null
+++ b/example/src/cpp/lib/get/get-time.cc
@@ -0,0 +1,13 @@
+#include "get-time.h"
+#include
+#include
+
+// Some deliberately bad code
+const char* get_localtime_impl() {
+ // warning: variable 'result' of type 'std::time_t' (aka 'long long') can be declared 'const' [misc-const-correctness]
+ std::time_t result = std::time(nullptr);
+ // warning: function 'asctime' is not bounds-checking and non-reentrant; 'strftime' should be used instead [bugprone-unsafe-functions,cert-msc24-c,cert-msc33-c]
+ std::string result_str = std::asctime(std::localtime(&result));
+ // rules_lint author: note the memory leak is not detected
+ return strdup(result_str.c_str());
+}
diff --git a/example/src/cpp/lib/get/get-time.h b/example/src/cpp/lib/get/get-time.h
new file mode 100644
index 00000000..5408f708
--- /dev/null
+++ b/example/src/cpp/lib/get/get-time.h
@@ -0,0 +1,17 @@
+#ifndef LIB_GET_TIME_H_
+#define LIB_GET_TIME_H_
+
+#include
+
+const char* get_localtime_impl();
+
+// Deliberatly bad code
+inline char* get_localtime() {
+ const char* time = get_localtime_impl();
+ char timebuf[20];
+ strcpy(timebuf, time);
+ // warning: Address of stack memory associated with local variable 'timebuf' returned to caller [clang-analyzer-core.StackAddressEscape]
+ return timebuf;
+}
+
+#endif
diff --git a/example/src/cpp/lib/hello-time.cc b/example/src/cpp/lib/hello-time.cc
new file mode 100644
index 00000000..c029fdc0
--- /dev/null
+++ b/example/src/cpp/lib/hello-time.cc
@@ -0,0 +1,12 @@
+#include "hello-time.h"
+// warning: included header ctime is not used directly [misc-include-cleaner]
+#include
+#include
+#include
+
+// Deliberately bad code
+void print_localtime() {
+ char* localtime = get_localtime();
+ // warning: do not call c-style vararg functions [cppcoreguidelines-pro-type-vararg,hicpp-vararg]
+ printf("%s\n", localtime);
+}
diff --git a/example/src/cpp/lib/hello-time.h b/example/src/cpp/lib/hello-time.h
new file mode 100644
index 00000000..ee01af1e
--- /dev/null
+++ b/example/src/cpp/lib/hello-time.h
@@ -0,0 +1,16 @@
+#ifndef LIB_HELLO_TIME_H_
+#define LIB_HELLO_TIME_H_
+
+#include
+#include
+#include
+
+void print_localtime();
+
+inline void print_localtime2() {
+ std::string a = "time";
+ for (int i=0; i
+#include
+
+inline void print_localtime3() {
+ std::string a = "time";
+ for (int i=0; i to provoke error
+std::string get_greet(const std::string &thing);
+
+#endif
diff --git a/example/src/cpp/main/hello-greet.cc b/example/src/cpp/main/hello-greet.cc
new file mode 100644
index 00000000..10efa2b9
--- /dev/null
+++ b/example/src/cpp/main/hello-greet.cc
@@ -0,0 +1,6 @@
+#include "hello-greet.h"
+#include
+
+std::string get_greet(const std::string& who) {
+ return "Hello " + who;
+}
diff --git a/example/src/cpp/main/hello-greet.h b/example/src/cpp/main/hello-greet.h
new file mode 100644
index 00000000..2543b46b
--- /dev/null
+++ b/example/src/cpp/main/hello-greet.h
@@ -0,0 +1,8 @@
+#ifndef MAIN_HELLO_GREET_H_
+#define MAIN_HELLO_GREET_H_
+
+#include
+
+std::string get_greet(const std::string &thing);
+
+#endif
diff --git a/example/src/cpp/main/hello-world.cc b/example/src/cpp/main/hello-world.cc
new file mode 100644
index 00000000..b1d1e8ff
--- /dev/null
+++ b/example/src/cpp/main/hello-world.cc
@@ -0,0 +1,15 @@
+#include "hello-time.h"
+#include "hello-greet.h"
+#include
+#include
+
+int main(int argc, char** argv) {
+ std::string who = "world";
+ if (argc > 1) {
+ who = argv[1];
+ }
+ // warning: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl]
+ std::cout << get_greet(who) << std::endl;
+ print_localtime();
+ return 0;
+}
diff --git a/example/src/hello.cpp b/example/src/hello.cpp
index af2f6a9e..126b7f8d 100644
--- a/example/src/hello.cpp
+++ b/example/src/hello.cpp
@@ -1,3 +1,51 @@
#include
+#include
+#include
+#include
+#include
+#include
-int main() { printf("Hello, world!\n"); }
+// deliberately bad code to trigger clang-tidy warning
+int string_to_int(const char *num) {
+ return atoi(num);
+}
+
+// deliberately insecure code to trigger clang-tidy warning
+void ls() {
+ system("ls");
+}
+
+// Code with a fixable issue
+void remove_from_vector() {
+ std::vector xs = {1,2,3,4,5,6};
+ std::remove(xs.begin(), xs.end(), 4);
+}
+
+// Code with a fixable issue
+static auto stringCpy(const std::string &str) -> char * {
+ char *result = reinterpret_cast(malloc(str.size()));
+ strcpy(result, str.data());
+ return result;
+}
+
+class dummy {
+public:
+ dummy() {};
+private:
+ int x;
+};
+
+static int compare(int x, int y) {
+ if (x < y);
+ {
+ x++;
+ }
+ return x;
+}
+
+int main() {
+ printf("Hello, world!\n");
+ compare(3, 4);
+ char* a = NULL;
+ char* b = 0;
+}
diff --git a/example/src/subdir/gitlab-generated.js b/example/src/subdir/gitlab-generated.js
index 5dba6bb1..bdb0ac5a 100644
--- a/example/src/subdir/gitlab-generated.js
+++ b/example/src/subdir/gitlab-generated.js
@@ -1,3 +1,2 @@
// Ignored by https://github.com/aspect-build/rules_lint/blob/example/.gitattributes
- export var x = "white space issue and no semi colon"
-
\ No newline at end of file
+export var x = "white space issue and no semi colon";
diff --git a/example/src/subdir/linguist-generated.js b/example/src/subdir/linguist-generated.js
index 5dba6bb1..bdb0ac5a 100644
--- a/example/src/subdir/linguist-generated.js
+++ b/example/src/subdir/linguist-generated.js
@@ -1,3 +1,2 @@
// Ignored by https://github.com/aspect-build/rules_lint/blob/example/.gitattributes
- export var x = "white space issue and no semi colon"
-
\ No newline at end of file
+export var x = "white space issue and no semi colon";
diff --git a/example/src/subdir/rules-lint-ignored.js b/example/src/subdir/rules-lint-ignored.js
index 5dba6bb1..bdb0ac5a 100644
--- a/example/src/subdir/rules-lint-ignored.js
+++ b/example/src/subdir/rules-lint-ignored.js
@@ -1,3 +1,2 @@
// Ignored by https://github.com/aspect-build/rules_lint/blob/example/.gitattributes
- export var x = "white space issue and no semi colon"
-
\ No newline at end of file
+export var x = "white space issue and no semi colon";
diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel
index 655662ac..9d0aa219 100644
--- a/example/tools/lint/BUILD.bazel
+++ b/example/tools/lint/BUILD.bazel
@@ -20,6 +20,7 @@ alias(
"@bazel_tools//src/conditions:linux_aarch64": "@ruff_aarch64-unknown-linux-gnu//:ruff",
"@bazel_tools//src/conditions:darwin_arm64": "@ruff_aarch64-apple-darwin//:ruff",
"@bazel_tools//src/conditions:darwin_x86_64": "@ruff_x86_64-apple-darwin//:ruff",
+ "@bazel_tools//src/conditions:windows_x64": "@ruff_x86_64-pc-windows-msvc//:ruff.exe",
}),
)
@@ -51,6 +52,7 @@ native_binary(
"@bazel_tools//src/conditions:linux_aarch64": "@vale_Linux_arm64//:vale",
"@bazel_tools//src/conditions:darwin_x86_64": "@vale_macOS_64-bit//:vale",
"@bazel_tools//src/conditions:darwin_arm64": "@vale_macOS_arm64//:vale",
+ "@bazel_tools//src/conditions:windows_x64": "@vale_Windows_64-bit//:vale.exe",
},
),
out = "vale",
@@ -68,3 +70,20 @@ copy_to_directory(
],
include_external_repositories = ["vale_*"],
)
+
+native_binary(
+ name = "clang_tidy",
+ src = select(
+ {
+ "@bazel_tools//src/conditions:linux_x86_64": "@llvm_toolchain_llvm//:bin/clang-tidy",
+ "@bazel_tools//src/conditions:linux_aarch64": "@llvm_toolchain_llvm//:bin/clang-tidy",
+ "@bazel_tools//src/conditions:darwin_x86_64": "@llvm_toolchain_llvm//:bin/clang-tidy",
+ "@bazel_tools//src/conditions:darwin_arm64": "@llvm_toolchain_llvm//:bin/clang-tidy",
+ # llvm_toolchain doesn't support windows: https://github.com/bazel-contrib/toolchains_llvm/issues/4
+ # as a workaround, you can download exes from
+ # https://github.com/llvm/llvm-project/releases/tag/llvmorg-18.1.6 and make available locally.
+ "@bazel_tools//src/conditions:windows_x64": "clang-tidy.exe",
+ },
+ ),
+ out = "clang_tidy",
+)
diff --git a/example/tools/lint/linters.bzl b/example/tools/lint/linters.bzl
index 0c53222f..4af3c91d 100644
--- a/example/tools/lint/linters.bzl
+++ b/example/tools/lint/linters.bzl
@@ -1,6 +1,7 @@
"Define linter aspects"
load("@aspect_rules_lint//lint:buf.bzl", "lint_buf_aspect")
+load("@aspect_rules_lint//lint:clang_tidy.bzl", "lint_clang_tidy_aspect")
load("@aspect_rules_lint//lint:eslint.bzl", "lint_eslint_aspect")
load("@aspect_rules_lint//lint:flake8.bzl", "lint_flake8_aspect")
load("@aspect_rules_lint//lint:ktlint.bzl", "lint_ktlint_aspect")
@@ -71,3 +72,26 @@ ktlint = lint_ktlint_aspect(
)
ktlint_test = lint_test(aspect = ktlint)
+
+clang_tidy = lint_clang_tidy_aspect(
+ binary = "@@//tools/lint:clang_tidy",
+ configs = [
+ "@@//:.clang-tidy",
+ "@@//src/cpp/lib:get/.clang-tidy",
+ ],
+ lint_target_headers = True,
+ angle_includes_are_system = False,
+ verbose = False,
+)
+
+clang_tidy_test = lint_test(aspect = clang_tidy)
+
+# an example of setting up a different clang-tidy aspect with different
+# options. This one uses a single global clang-tidy file
+clang_tidy_global_config = lint_clang_tidy_aspect(
+ binary = "@@//tools/lint:clang_tidy",
+ global_config = "@@//:.clang-tidy",
+ lint_target_headers = True,
+ angle_includes_are_system = False,
+ verbose = False,
+)
diff --git a/format/test/format_test.bats b/format/test/format_test.bats
index e36c0a7b..7598e43e 100644
--- a/format/test/format_test.bats
+++ b/format/test/format_test.bats
@@ -116,7 +116,7 @@ bats_load_library "bats-assert"
run bazel run //format/test:format_C++_with_clang-format
assert_success
- assert_output --partial "+ clang-format -style=file --fallback-style=none -i example/src/hello.cpp"
+ assert_output --partial "+ clang-format -style=file --fallback-style=none -i example/src/cpp/lib/get/get-time.cc"
}
@test "should run clang-format on Cuda" {
diff --git a/lint/BUILD.bazel b/lint/BUILD.bazel
index e279c7be..16543295 100644
--- a/lint/BUILD.bazel
+++ b/lint/BUILD.bazel
@@ -197,3 +197,21 @@ bzl_library(
"@bazel_skylib//lib:dicts",
],
)
+
+bzl_library(
+ name = "clang_tidy",
+ srcs = ["clang_tidy.bzl"],
+ visibility = ["//visibility:public"],
+ deps = _BAZEL_TOOLS + [
+ "//lint/private:lint_aspect",
+ "@bazel_skylib//lib:dicts",
+ "@bazel_tools//tools/build_defs/cc:action_names.bzl",
+ "@bazel_tools//tools/cpp:toolchain_utils.bzl",
+ ],
+)
+
+sh_binary(
+ name = "clang_tidy_wrapper",
+ srcs = ["clang_tidy_wrapper.bash"],
+ visibility = ["//visibility:public"],
+)
diff --git a/lint/clang_tidy.bzl b/lint/clang_tidy.bzl
new file mode 100644
index 00000000..a885d69f
--- /dev/null
+++ b/lint/clang_tidy.bzl
@@ -0,0 +1,405 @@
+"""API for calling declaring a clang-tidy lint aspect.
+
+Typical usage:
+
+First, install clang-tidy with llvm_toolchain or as a native binary (llvm_toolchain
+does not support Windows as of 06/2024, but providing a native clang-tidy.exe works)
+
+Next, declare a binary target for it, typically in `tools/lint/BUILD.bazel`:
+
+e.g. using llvm_toolchain:
+```starlark
+native_binary(
+ name = "clang_tidy",
+ src = "@llvm_toolchain_llvm//:bin/clang-tidy"
+ out = "clang_tidy",
+)
+```
+
+e.g as native binary:
+```starlark
+native_binary(
+ name = "clang_tidy",
+ src = "clang-tidy.exe"
+ out = "clang_tidy",
+)
+```
+
+Finally, create the linter aspect, typically in `tools/lint/linters.bzl`:
+
+```starlark
+load("@aspect_rules_lint//lint:clang_tidy.bzl", "clang_tidy_aspect")
+
+clang_tidy = clang_tidy_aspect(
+ binary = "@@//path/to:clang-tidy",
+ configs = "@@//path/to:.clang-tidy",
+)
+```
+"""
+
+load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
+load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "dummy_successful_lint_action", "patch_and_report_files", "report_files")
+
+_MNEMONIC = "AspectRulesLintClangTidy"
+
+def _gather_inputs(ctx, compilation_context, srcs):
+ inputs = srcs + ctx.files._configs + compilation_context.headers.to_list()
+ if (any(ctx.files._global_config)):
+ inputs.append(ctx.files._global_config[0])
+ return inputs
+
+def _toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile):
+ cc_toolchain = find_cpp_toolchain(ctx)
+ feature_configuration = cc_common.configure_features(
+ ctx = ctx,
+ cc_toolchain = cc_toolchain,
+ )
+ compile_variables = cc_common.create_compile_variables(
+ feature_configuration = feature_configuration,
+ cc_toolchain = cc_toolchain,
+ user_compile_flags = ctx.fragments.cpp.cxxopts + ctx.fragments.cpp.copts,
+ )
+ flags = cc_common.get_memory_inefficient_command_line(
+ feature_configuration = feature_configuration,
+ action_name = action_name,
+ variables = compile_variables,
+ )
+ return flags
+
+def _update_flag(flag):
+ # update from MSVC C++ standard to clang C++ standard
+ unsupported_flags = [
+ "-fno-canonical-system-headers",
+ "-fstack-usage",
+ "/nologo",
+ "/COMPILER_MSVC",
+ "/showIncludes",
+ ]
+ if (flag in unsupported_flags):
+ return None
+
+ # omit warning flags
+ if (flag.startswith("/wd") or flag.startswith("-W")):
+ return None
+
+ # remap c++ standard to clang
+ if (flag.startswith("/std:")):
+ flag = "-std=" + flag.removeprefix("/std:")
+
+ # remap defines
+ if (flag.startswith("/D")):
+ flag = "-" + flag[1:]
+ if (flag.startswith("/FI")):
+ flag = "-include=" + flag.removeprefix("/FI")
+
+ # skip other msvc options
+ if (flag.startswith("/")):
+ return None
+ return flag
+
+def _safe_flags(ctx, flags):
+ # Some flags might be used by GCC/MSVC, but not understood by Clang.
+ # Remap or remove them here, to allow users to run clang-tidy, without having
+ # a clang toolchain configured (that would produce a good command line with --compiler clang)
+ flags = []
+ skipped_flags = []
+ for flag in flags:
+ flag = _update_flag(flag)
+ if (flag):
+ flags.append(flag)
+ elif (ctx.attr._verbose):
+ skipped_flags.append(flag)
+ if (ctx.attr._verbose and any(skipped_flags)):
+ # buildifier: disable=print
+ print("skipped flags: " + " ".join(skipped_flags))
+ return flags
+
+def _prefixed(list, prefix):
+ array = []
+ for arg in list:
+ array.append(prefix)
+ array.append(arg)
+ return array
+
+def _angle_includes_option(ctx):
+ if (ctx.attr._angle_includes_are_system):
+ return "-isystem"
+ return "-I"
+
+def _is_cxx(file):
+ if file.extension == "c":
+ return False
+ return True
+
+def _is_source(file):
+ permitted_source_types = [
+ "c",
+ "cc",
+ "cpp",
+ "cxx",
+ "c++",
+ "C",
+ ]
+ return (file.is_source and file.extension in permitted_source_types)
+
+# modification of filter_srcs in lint_aspect.bzl that filters out header files
+def _filter_srcs(rule):
+ if "lint-genfiles" in rule.attr.tags:
+ return rule.files.srcs
+ else:
+ return [s for s in rule.files.srcs if _is_source(s)]
+
+def is_parent_in_list(dir, list):
+ for item in list:
+ if (dir != item and dir.startswith(item)):
+ return True
+ return False
+
+def _common_prefixes(headers):
+ # crude code to work out a common directory prefix for all headers
+ # is there a canonical way to do this in starlark?
+ dirs = []
+ for h in headers:
+ dir = h.dirname
+ if dir not in dirs:
+ dirs.append(dir)
+ dirs2 = []
+ for dir in dirs:
+ if (not is_parent_in_list(dir, dirs)):
+ dirs2.append(dir)
+ return dirs2
+
+def _aggregate_regex(compilation_context):
+ dirs = _common_prefixes(compilation_context.direct_headers)
+ if not any(dirs):
+ regex = None
+ elif len(dirs) == 1:
+ regex = ".*" + dirs[0] + "/.*"
+ else:
+ regex = ".*"
+ return regex
+
+def _quoted_arg(arg):
+ return "\"" + arg + "\""
+
+def _get_args(ctx, compilation_context, srcs):
+ args = []
+ if (any(ctx.files._global_config)):
+ args.append("--config-file=" + ctx.files._global_config[0].short_path)
+ if (ctx.attr._lint_target_headers):
+ regex = _aggregate_regex(compilation_context)
+ if (regex):
+ args.append(_quoted_arg("-header-filter=" + regex))
+ elif (ctx.attr._header_filter):
+ regex = ctx.attr._header_filter
+ args.append(_quoted_arg("-header-filter=" + regex))
+ args.extend([src.short_path for src in srcs])
+
+ args.append("--")
+
+ # add args specified by the toolchain, on the command line and rule copts
+ rule_flags = ctx.rule.attr.copts if hasattr(ctx.rule.attr, "copts") else []
+ sources_are_cxx = _is_cxx(srcs[0])
+ if (sources_are_cxx):
+ args.extend(_safe_flags(ctx, _toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_flags) + ["-xc++"])
+ else:
+ args.extend(_safe_flags(ctx, _toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_flags) + ["-xc"])
+
+ # add defines
+ for define in compilation_context.defines.to_list():
+ args.append("-D" + define)
+ for define in compilation_context.local_defines.to_list():
+ args.append("-D" + define)
+
+ # add includes
+ args.extend(_prefixed(compilation_context.framework_includes.to_list(), "-F"))
+ args.extend(_prefixed(compilation_context.includes.to_list(), "-I"))
+ args.extend(_prefixed(compilation_context.quote_includes.to_list(), "-iquote"))
+ args.extend(_prefixed(compilation_context.system_includes.to_list(), _angle_includes_option(ctx)))
+ args.extend(_prefixed(compilation_context.external_includes.to_list(), "-isystem"))
+
+ return args
+
+def clang_tidy_action(ctx, compilation_context, executable, srcs, stdout, exit_code):
+ """Create a Bazel Action that spawns a clang-tidy process.
+
+ Adapter for wrapping Bazel around
+ https://clang.llvm.org/extra/clang-tidy/
+
+ Args:
+ ctx: an action context OR aspect context
+ compilation_context: from target
+ executable: struct with a clang-tidy field
+ srcs: file objects to lint
+ stdout: output file containing the stdout or --output-file of clang-tidy
+ exit_code: output file containing the exit code of clang-tidy.
+ If None, then fail the build when clang-tidy exits non-zero.
+ """
+
+ outputs = [stdout]
+ env = {}
+ env["CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE"] = stdout.path
+ if exit_code:
+ env["CLANG_TIDY__EXIT_CODE_OUTPUT_FILE"] = exit_code.path
+ outputs.append(exit_code)
+ if (ctx.attr._verbose):
+ env["CLANG_TIDY__VERBOSE"] = "1"
+
+ ctx.actions.run_shell(
+ inputs = _gather_inputs(ctx, compilation_context, srcs),
+ outputs = outputs,
+ tools = [executable._clang_tidy_wrapper, executable._clang_tidy],
+ command = executable._clang_tidy_wrapper.path + " $@",
+ arguments = [executable._clang_tidy.path] + _get_args(ctx, compilation_context, srcs),
+ use_default_shell_env = True,
+ env = env,
+ mnemonic = _MNEMONIC,
+ progress_message = "Linting %{label} with clang-tidy",
+ )
+
+def clang_tidy_fix(ctx, compilation_context, executable, srcs, patch, stdout, exit_code):
+ """Create a Bazel Action that spawns clang-tidy with --fix.
+
+ Args:
+ ctx: an action context OR aspect context
+ compilation_context: from target
+ executable: struct with a clang_tidy field
+ srcs: list of file objects to lint
+ patch: output file containing the applied fixes that can be applied with the patch(1) command.
+ stdout: output file containing the stdout or --output-file of clang-tidy
+ exit_code: output file containing the exit code of clang-tidy
+ """
+ patch_cfg = ctx.actions.declare_file("_{}.patch_cfg".format(ctx.label.name))
+
+ env = {}
+ if (ctx.attr._verbose):
+ env["CLANG_TIDY__VERBOSE"] = "1"
+
+ ctx.actions.write(
+ output = patch_cfg,
+ content = json.encode({
+ "linter": executable._clang_tidy_wrapper.path,
+ "args": [executable._clang_tidy.path, "--fix"] + _get_args(ctx, compilation_context, srcs),
+ "env": env,
+ "files_to_diff": [src.path for src in srcs],
+ "output": patch.path,
+ }),
+ )
+
+ ctx.actions.run(
+ inputs = _gather_inputs(ctx, compilation_context, srcs) + [patch_cfg],
+ outputs = [patch, stdout, exit_code],
+ executable = executable._patcher,
+ arguments = [patch_cfg.path],
+ env = {
+ "BAZEL_BINDIR": ".",
+ "JS_BINARY__EXIT_CODE_OUTPUT_FILE": exit_code.path,
+ "JS_BINARY__STDOUT_OUTPUT_FILE": stdout.path,
+ "JS_BINARY__SILENT_ON_SUCCESS": "1",
+ },
+ tools = [executable._clang_tidy_wrapper, executable._clang_tidy],
+ mnemonic = _MNEMONIC,
+ progress_message = "Linting %{label} with clang-tidy",
+ )
+
+# buildifier: disable=function-docstring
+def _clang_tidy_aspect_impl(target, ctx):
+ if not CcInfo in target:
+ return []
+
+ files_to_lint = _filter_srcs(ctx.rule)
+ compilation_context = target[CcInfo].compilation_context
+
+ if ctx.attr._options[LintOptionsInfo].fix:
+ patch, report, exit_code, info = patch_and_report_files(_MNEMONIC, target, ctx)
+ if len(files_to_lint) == 0:
+ dummy_successful_lint_action(ctx, report, exit_code, patch)
+ else:
+ clang_tidy_fix(ctx, compilation_context, ctx.executable, files_to_lint, patch, report, exit_code)
+ else:
+ report, exit_code, info = report_files(_MNEMONIC, target, ctx)
+ if len(files_to_lint) == 0:
+ dummy_successful_lint_action(ctx, report, exit_code)
+ else:
+ clang_tidy_action(ctx, compilation_context, ctx.executable, files_to_lint, report, exit_code)
+ return [info]
+
+def lint_clang_tidy_aspect(binary, configs = [], global_config = [], header_filter = "", lint_target_headers = False, angle_includes_are_system = True, verbose = False):
+ """A factory function to create a linter aspect.
+
+ Args:
+ binary: the clang-tidy binary, typically a rule like
+
+ ```starlark
+ native_binary(
+ name = "clang_tidy",
+ src = "clang-tidy.exe"
+ out = "clang_tidy",
+ )
+ ```
+ configs: labels of the .clang-tidy files to make available to clang-tidy's config search. These may be
+ in subdirectories and clang-tidy will apply them if appropriate. This may also include .clang-format
+ files which may be used for formatting fixes.
+ global_config: label of a single global .clang-tidy file to pass to clang-tidy on the command line. This
+ will cause clang-tidy to ignore any other config files in the source directories.
+ header_filter: optional, set to a posix regex to supply to clang-tidy with the -header-filter option
+ lint_target_headers: optional, set to True to pass a pattern that includes all headers with the target's
+ directory prefix. This crude control may include headers from the linted target in the results. If
+ supplied, overrides the header_filter option.
+ angle_includes_are_system: controls how angle includes are passed to clang-tidy. By default, Bazel
+ passes these as -isystem. Change this to False to pass these as -I, which allows clang-tidy to regard
+ them as regular header files.
+ verbose: print debug messages including clang-tidy command lines being invoked.
+ """
+
+ if type(global_config) == "string":
+ global_config = [global_config]
+
+ return aspect(
+ implementation = _clang_tidy_aspect_impl,
+ attrs = {
+ "_options": attr.label(
+ default = "//lint:options",
+ providers = [LintOptionsInfo],
+ ),
+ "_configs": attr.label_list(
+ default = configs,
+ allow_files = True,
+ ),
+ "_global_config": attr.label_list(
+ default = global_config,
+ allow_files = True,
+ ),
+ "_lint_target_headers": attr.bool(
+ default = lint_target_headers,
+ ),
+ "_header_filter": attr.string(
+ default = header_filter,
+ ),
+ "_angle_includes_are_system": attr.bool(
+ default = angle_includes_are_system,
+ ),
+ "_verbose": attr.bool(
+ default = verbose,
+ ),
+ "_clang_tidy": attr.label(
+ default = binary,
+ executable = True,
+ cfg = "exec",
+ ),
+ "_clang_tidy_wrapper": attr.label(
+ default = Label("@aspect_rules_lint//lint:clang_tidy_wrapper"),
+ executable = True,
+ cfg = "exec",
+ ),
+ "_patcher": attr.label(
+ default = "@aspect_rules_lint//lint/private:patcher",
+ executable = True,
+ cfg = "exec",
+ ),
+ "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
+ },
+ toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
+ fragments = ["cpp"],
+ )
diff --git a/lint/clang_tidy_wrapper.bash b/lint/clang_tidy_wrapper.bash
new file mode 100755
index 00000000..e94d28f5
--- /dev/null
+++ b/lint/clang_tidy_wrapper.bash
@@ -0,0 +1,84 @@
+#!/usr/bin/bash
+# This is a wrapper for clang-tidy which gives us control over error handling
+# Usage: clang_tidy_wrapper.bash ... --
+#
+# Controls:
+# - CLANG_TIDY__VERBOSE: If set, be verbose
+# - CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE: If set, write stdout and stderr to this file
+# - CLANG_TIDY__EXIT_CODE_OUTPUT_FILE: If set, write the highest exit code
+# to this file and return success
+
+# First arg is clang-tidy path
+clang_tidy=$1
+shift
+
+if [[ -n $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE ]]; then
+ # Create the file if it doesn't exist
+ touch $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE
+ # Clear the file if it does exist
+ > $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE
+ if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo "Output > ${CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE}"
+ fi
+fi
+if [[ -n $CLANG_TIDY__EXIT_CODE_OUTPUT_FILE ]]; then
+ if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo "Exit Code -> ${CLANG_TIDY__EXIT_CODE_OUTPUT_FILE}"
+ fi
+fi
+
+if [[ -n $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE ]]; then
+ out_file=$CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE
+else
+ out_file=$(mktemp)
+fi
+# include stderr in output file; it contains some of the diagnostics
+command="$clang_tidy $@ $file > $out_file 2>&1"
+if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo "$@"
+ echo "cwd: " `pwd`
+ echo $command
+fi
+eval $command
+exit_code=$?
+if [[ -z $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE ]]; then
+ cat $out_file
+fi
+# distinguish between compile (fatal) errors and warnings-as-errors errors
+fatal_error=0
+if [ $exit_code -ne 0 ] && [ -s $out_file ]; then
+ while read line
+ do
+ if [[ $line == *"clang-diagnostic-error"* ]]; then
+ fatal_error=1
+ break
+ fi
+ done < "$out_file"
+fi
+if [ $fatal_error -ne 0 ]; then
+ cat $out_file
+ rm $out_file
+ if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo "found clang-diagnostic-error (regarding as fatal)"
+ echo "exit $exit_code"
+ fi
+ exit $exit_code
+fi
+if [[ -z $CLANG_TIDY__STDOUT_STDERR_OUTPUT_FILE ]]; then
+ rm $out_file
+fi
+
+# if CLANG_TIDY__EXIT_CODE_FILE is set, write the max exit code to that file and return success
+if [[ -n $CLANG_TIDY__EXIT_CODE_OUTPUT_FILE ]]; then
+ if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo "echo $exit_code > $CLANG_TIDY__EXIT_CODE_OUTPUT_FILE"
+ echo "exit 0"
+ fi
+ echo $exit_code > $CLANG_TIDY__EXIT_CODE_OUTPUT_FILE
+ exit 0
+fi
+
+if [[ -n $CLANG_TIDY__VERBOSE ]]; then
+ echo exit $exit_code
+fi
+exit $exit_code