tools: Adds clang-tidy static analyzer
diff --git a/.githooks/pre-push b/.githooks/pre-push index be5073a..4aea9e5 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push
@@ -4,8 +4,11 @@ echo "Test compiler against well known repos." TARGETS='@com_google_googletest//... @com_github_google_benchmark//... @com_google_absl//... @com_google_protobuf//:protoc' +STATIC_ANALYZER_TARGETS='@com_google_protobuf//:protoc' # Test compilation of well known repositories bazel test $TARGETS --test_tag_filters=-benchmark -bazel build $TARGETS \ No newline at end of file +bazel build $TARGETS + +bazel build $STATIC_ANALYZER_TARGETS --aspects //tools/clang_tidy:clang_tidy.bzl%clang_tidy_aspect --output_groups=report \ No newline at end of file
diff --git a/.github/workflows/blank.yml b/.github/workflows/blank.yml index 3306ffc..b0b167c 100644 --- a/.github/workflows/blank.yml +++ b/.github/workflows/blank.yml
@@ -34,6 +34,7 @@ # Runs a single command using the runners shell - name: Bazel build/test external test repos run: | - bazelisk build @com_google_googletest//... @com_github_google_benchmark//... @com_google_absl//... bazelisk test @com_google_googletest//... @com_github_google_benchmark//... @com_google_absl//... --test_tag_filters=-benchmark + bazelisk build @com_google_googletest//... @com_github_google_benchmark//... @com_google_absl//... + bazelisk build @com_google_googletest//... @com_github_google_benchmark//... @com_google_absl//... --aspects //tools/clang_tidy:clang_tidy.bzl%clang_tidy_aspect --output_groups=report
diff --git a/config/BUILD.bazel b/config/BUILD.bazel index 742b6eb..dd4e519 100644 --- a/config/BUILD.bazel +++ b/config/BUILD.bazel
@@ -56,6 +56,11 @@ }), ) +alias( + name = "clang_tidy_config_multiplexer", + actual = "//tools/clang_tidy:default", +) + # This is a placeholder to signify an empty dependency. cc_toolchain_import( name = "empty",
diff --git a/config/rules_cc_toolchain_config.BUILD b/config/rules_cc_toolchain_config.BUILD index 56773fb..e682fe8 100644 --- a/config/rules_cc_toolchain_config.BUILD +++ b/config/rules_cc_toolchain_config.BUILD
@@ -39,5 +39,10 @@ label_flag( name = "startup_libs", - build_setting_default = "@rules_cc_toolchain//config:startup_libs", + build_setting_default = "@rules_cc_toolchain//config:startup_libs", +) + +label_flag( + name = "clang_tidy_config", + build_setting_default = "@rules_cc_toolchain//config:clang_tidy_config_multiplexer", )
diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 5e9fdb1..05448f9 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel
@@ -15,3 +15,9 @@ name = "dummy_test2", srcs = ["dummy_test2.cc"], ) + +# This is an intentionally buggy binary to test static analysis. +cc_binary( + name = "dereferencing_null_pointer", + srcs = ["dereferencing_null_pointer.cc"], +)
diff --git a/tests/dereferencing_null_pointer.cc b/tests/dereferencing_null_pointer.cc new file mode 100644 index 0000000..59328a4 --- /dev/null +++ b/tests/dereferencing_null_pointer.cc
@@ -0,0 +1,8 @@ +#include <iostream> + +int main() { + int *p = nullptr; + // This is an intentional bug, to test static analysis. + std::cout << *p << std::endl; + return 1; +} \ No newline at end of file
diff --git a/tools/clang_tidy/.clang-tidy b/tools/clang_tidy/.clang-tidy new file mode 100644 index 0000000..aefc81c --- /dev/null +++ b/tools/clang_tidy/.clang-tidy
@@ -0,0 +1,43 @@ +Checks: 'clang-diagnostic-*,clang-analyzer-*' +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +AnalyzeTemporaryDtors: false +FormatStyle: Google +CheckOptions: + - key: llvm-else-after-return.WarnOnConditionVariables + value: '0' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: '0' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: '0' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: '1' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: '1' + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: llvm-qualified-auto.AddConstToQualified + value: '0' + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: llvm-else-after-return.WarnOnUnfixable + value: '0' + - key: google-readability-function-size.StatementThreshold + value: '800' +
diff --git a/tools/clang_tidy/BUILD.bazel b/tools/clang_tidy/BUILD.bazel new file mode 100644 index 0000000..4540fd9 --- /dev/null +++ b/tools/clang_tidy/BUILD.bazel
@@ -0,0 +1,7 @@ +load(":clang_tidy.bzl", "clang_tidy_config") + +clang_tidy_config( + name = "default", + config = ".clang-tidy", + visibility = ["//visibility:public"], +)
diff --git a/tools/clang_tidy/clang_tidy.bzl b/tools/clang_tidy/clang_tidy.bzl new file mode 100644 index 0000000..4969f3b --- /dev/null +++ b/tools/clang_tidy/clang_tidy.bzl
@@ -0,0 +1,224 @@ +load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") +load( + "@rules_cc//cc:action_names.bzl", + "CPP_COMPILE_ACTION_NAME", + "C_COMPILE_ACTION_NAME", +) + +ClangTidyConfigInfo = provider( + "The config information for clang-tidy.", + fields = ["config"], +) + +def _language(file): + """ Returns the language for the file + + Args: + file (File): The file to get the language for + """ + if file.extension == ".c": + return "c" + else: + return "cc" + +def _is_src(file): + """ Returns true if the file is a source file + + Bazel allows for headers in the srcs attributes, we need to filter them out. + + Args: + file (File): The file to check. +""" + if file.extension in ["c", "cc", "cpp", "cxx", "C", "c++", "C++"] and \ + file.is_source: + return True + return False + +def _run_clang_tidy( + target, + ctx, + file, + clang_tidy_config, + compilation_action_name, + user_compile_flags): + """ Runs clang-tidy on a given translation unit + + Args: + target (Target): The target currently being built. + ctx (Context): The current build context. + file (File): The source file to run clang-tidy on. + clang_tidy_config (File): The configuration to use. + compilation_action_name (str): The name of the compilation action. + user_compile_flags (List): The user-specified flags to use. + + Returns: + A report file for the given translation unit. + """ + cc_toolchain = find_cc_toolchain(ctx) + + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + compilation_context = target[CcInfo].compilation_context + cc_compile_variables = cc_common.create_compile_variables( + user_compile_flags = user_compile_flags, + source_file = file.path, + feature_configuration = feature_configuration, + cc_toolchain = cc_toolchain, + include_directories = compilation_context.includes, + quote_include_directories = compilation_context.quote_includes, + system_include_directories = compilation_context.system_includes, + framework_include_directories = compilation_context.framework_includes, + preprocessor_defines = compilation_context.defines, + ) + cc_compile_command_line = cc_common.get_memory_inefficient_command_line( + feature_configuration = feature_configuration, + action_name = compilation_action_name, + variables = cc_compile_variables, + ) + report = ctx.actions.declare_file(ctx.rule.attr.name + + file.short_path.replace(".", "_") + + ".clang_tidy_report") + + ctx.actions.run_shell( + outputs = [report], + inputs = depset( + [file, clang_tidy_config], + transitive = [cc_toolchain.all_files, compilation_context.headers], + ), + mnemonic = "ClangTidy", + progress_message = "Analysing {}.".format(file.path), + arguments = [file.path, "--export-fixes", report.path, "--"] + + cc_compile_command_line, + # Clang tidy does not output a fix if the file is already clean. + # Bazel requires and output file, so we touch the output first. + command = "cp {clang_tidy_config} .clang-tidy && \ + touch {report_path} && \ + {clang_tidy} $@".format( + clang_tidy_config = clang_tidy_config.path, + report_path = report.path, + clang_tidy = ctx.executable._clang_tidy.path, + ), + tools = [ctx.executable._clang_tidy], + ) + return report + +def _clang_tidy_aspect_impl(target, ctx): + # Ignore targets that are not C++ + if not CcInfo in target: + return [] + clang_tidy_config = ctx.actions.declare_file(".clang-tidy") + ctx.actions.symlink( + output = clang_tidy_config, + target_file = ctx.attr._config[ClangTidyConfigInfo].config, + ) + + if hasattr(ctx.rule.attr, "srcs"): + srcs = [src for src in ctx.rule.files.srcs if _is_src(src)] + else: + srcs = [] + + if hasattr(ctx.rule.attr, "copts"): + user_compile_flags = ctx.rule.attr.copts + else: + user_compile_flags = [] + + reports = [] + for src in [src for src in srcs if _language(src) == "cc"]: + reports.append(_run_clang_tidy( + target, + ctx, + src, + clang_tidy_config, + CPP_COMPILE_ACTION_NAME, + ctx.fragments.cpp.cxxopts + + ctx.fragments.cpp.copts + + user_compile_flags, + )) + for src in [src for src in srcs if _language(src) == "c"]: + reports.append(_run_clang_tidy( + target, + ctx, + src, + clang_tidy_config, + C_COMPILE_ACTION_NAME, + ctx.fragments.cpp.conlyopts + user_compile_flags, + )) + + return [OutputGroupInfo(report = depset(reports))] + +clang_tidy_aspect = aspect( + _clang_tidy_aspect_impl, + fragments = ["cpp"], + attrs = { + "_clang_tidy": attr.label( + allow_single_file = True, + default = "@clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04//:bin/clang-tidy", + cfg = "exec", + executable = True, + ), + "_config": attr.label( + default = "@rules_cc_toolchain_config//:clang_tidy_config", + providers = [ClangTidyConfigInfo], + ), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + }, + toolchains = ["@bazel_tools//tools/cpp:toolchain_type"], + doc = """ + Runs clang-tidy on the given C++ sources + + This aspect runs clang-tidy on the given set of c/c++ sources. You can use this aspect + by running; + ``` sh + bazel build //my:target \\ + --aspects build_bazel_rules_cc//cc:clang_tidy:clang_tidy.bzl%clang_tidy_aspect + ``` + + You can override the default configuration by using the clang_tidy_config rule. e.g. + ```py + # //BUILD.bazel + cc_toolchain_config( + name = "my_config", + config = ".clang-tidy", + ) + ``` + The passing in a command line flag to point the aspect at your new config rule e.g. + ``` sh + bazel build //my:target \\ + --aspects @build_bazel_rules_cc//cc:clang_tidy:clang_tidy.bzl%clang_tidy_aspect \\ + --@rules_cc_toolchain_config//:clang_tidy_config=//:my_config + ``` + + In most cases it is likely that you will want to shorten the command line flags using + your .bazelrc file. e.g. + ``` + # //.bazelrc + build:analyze --aspects @build_bazel_rules_cc//cc:clang_tidy:clang_tidy.bzl%clang_tidy_aspect + build:analyze --@rules_cc_toolchain_config//:clang_tidy_config=//:my_config + ``` + + You can then run the analysis using the following command; + ``` sh + bazel build //my:target --config analyze + ``` + + """, +) + +def _clang_tidy_config_impl(ctx): + return [ClangTidyConfigInfo(config = ctx.file.config)] + +clang_tidy_config = rule( + _clang_tidy_config_impl, + attrs = { + "config": attr.label( + allow_single_file = [".clang-tidy"], + ), + }, + provides = [ClangTidyConfigInfo], +)