diff --git a/README.md b/README.md index d5754471..ffb9d108 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Linters which are not language-specific: | Language | Formatter | Linter(s) | | ---------------------- | ------------------------- | -------------------------------- | -| C / C++ | [clang-format] | [clang-tidy] | +| C / C++ | [clang-format] | [clang-tidy] or [cppcheck] | | Cuda | [clang-format] | | | CSS, Less, Sass | [Prettier] | [Stylelint] | | Go | [gofmt] or [gofumpt] | | @@ -93,6 +93,7 @@ Linters which are not language-specific: [taplo]: https://taplo.tamasfe.dev/ [clang-format]: https://clang.llvm.org/docs/ClangFormat.html [clang-tidy]: https://clang.llvm.org/extra/clang-tidy/ +[cppcheck]: https://www.cppcheck.com/ [vale]: https://vale.sh/ [yamlfmt]: https://github.com/google/yamlfmt [yamllint]: https://yamllint.readthedocs.io/en/stable/ diff --git a/example/.aspect/cli/config.yaml b/example/.aspect/cli/config.yaml index 91352919..bd2c99ae 100644 --- a/example/.aspect/cli/config.yaml +++ b/example/.aspect/cli/config.yaml @@ -10,5 +10,6 @@ lint: - //tools/lint:linters.bzl%vale - //tools/lint:linters.bzl%checkstyle - //tools/lint:linters.bzl%clang_tidy + - //tools/lint:linters.bzl%cppcheck - //tools/lint:linters.bzl%spotbugs - //tools/lint:linters.bzl%keep_sorted diff --git a/example/MODULE.bazel b/example/MODULE.bazel index 131b7c70..e6c215c3 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -177,3 +177,24 @@ ruby.bundle_fetch( use_repo(ruby, "bundle", "ruby", "ruby_toolchains") register_toolchains("@ruby_toolchains//:all") + +# Download cppcheck premium tar files for different platforms +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Even though cppcheckpremium is downloaded, it behaves like the free version, if we do not +# provide a cppcheck.cfg file (see cppcheck.BUILD file) and license key. +http_archive( + name = "cppcheck_linux", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-IqQ3Iofw6LoHh4YcdbN0m3tjg6utCiey7nGaOaPMv/I=", + strip_prefix = "cppcheckpremium-25.8.3", + urls = ["https://files.cppchecksolutions.com/25.8.3/ubuntu-22.04/cppcheckpremium-25.8.3-amd64.tar.gz"], +) + +http_archive( + name = "cppcheck_macos", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-PEtm/DxKNZNJJuZE+56AZ80R22sZjZoziekAmR7FhNk=", + strip_prefix = "cppcheckpremium", + urls = ["https://files.cppchecksolutions.com/25.8.3/cppcheckpremium-25.8.3-macos-15.tar.gz"], +) diff --git a/example/WORKSPACE.bazel b/example/WORKSPACE.bazel index 03435982..212a4029 100644 --- a/example/WORKSPACE.bazel +++ b/example/WORKSPACE.bazel @@ -427,3 +427,19 @@ multitool( "@aspect_rules_lint//lint:multitool.lock.json", ], ) + +http_archive( + name = "cppcheck_linux", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-IqQ3Iofw6LoHh4YcdbN0m3tjg6utCiey7nGaOaPMv/I=", + strip_prefix = "cppcheckpremium-25.8.3", + urls = ["https://files.cppchecksolutions.com/25.8.3/ubuntu-22.04/cppcheckpremium-25.8.3-amd64.tar.gz"], +) + +http_archive( + name = "cppcheck_macos", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-PEtm/DxKNZNJJuZE+56AZ80R22sZjZoziekAmR7FhNk=", + strip_prefix = "cppcheckpremium", + urls = ["https://files.cppchecksolutions.com/25.8.3/cppcheckpremium-25.8.3-macos-15.tar.gz"], +) diff --git a/example/lint.sh b/example/lint.sh index 5deca387..af55cff7 100755 --- a/example/lint.sh +++ b/example/lint.sh @@ -37,7 +37,7 @@ if [ $machine == "Windows" ]; then # avoid missing linters on windows platform args=("--aspects=$(echo //tools/lint:linters.bzl%{flake8,pylint,pmd,ruff,vale,yamllint,clang_tidy} | tr ' ' ',')") else - args=("--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,keep_sorted,ktlint,pmd,pylint,ruff,shellcheck,stylelint,vale,yamllint,clang_tidy,spotbugs} | tr ' ' ',')") + args=("--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,keep_sorted,ktlint,pmd,pylint,ruff,shellcheck,stylelint,vale,yamllint,clang_tidy,cppcheck,spotbugs} | tr ' ' ',')") fi # NB: perhaps --remote_download_toplevel is needed as well with remote execution? diff --git a/example/test/lint_test.bats b/example/test/lint_test.bats index d367d930..3c9950b1 100755 --- a/example/test/lint_test.bats +++ b/example/test/lint_test.bats @@ -29,7 +29,7 @@ EOF # PMD echo <<"EOF" | assert_output --partial * file: src/Foo.java - src: Foo.java:9:9 + src: Foo.jav fa:9:9 rule: FinalizeOverloaded msg: Finalize methods should not be overloaded code: protected void finalize(int a) {} diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel index 2d78c480..ca547904 100644 --- a/example/tools/lint/BUILD.bazel +++ b/example/tools/lint/BUILD.bazel @@ -11,6 +11,7 @@ load("@npm//:eslint/package_json.bzl", eslint_bin = "bin") load("@npm//:stylelint/package_json.bzl", stylelint_bin = "bin") load("@rules_java//java:defs.bzl", "java_binary") load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") package(default_visibility = ["//:__subpackages__"]) @@ -124,3 +125,20 @@ alias( name = "standardrb", actual = "@bundle//bin:standardrb", ) + +# cppcheck +genrule( + name = "cppcheck_wrapper", + srcs = ["cppcheck_wrapper.sh.tpl"], + outs = ["cppcheck_wrapper.sh"], + cmd = "cp $(location cppcheck_wrapper.sh.tpl) $@", +) + +sh_binary( + name = "cppcheck", + srcs = [":cppcheck_wrapper"], + data = select({ + "@platforms//os:linux": ["@cppcheck_linux//:files"], + "@platforms//os:macos": ["@cppcheck_macos//:files"], + }), +) diff --git a/example/tools/lint/cppcheck.BUILD b/example/tools/lint/cppcheck.BUILD new file mode 100644 index 00000000..913636dc --- /dev/null +++ b/example/tools/lint/cppcheck.BUILD @@ -0,0 +1,11 @@ +# BUILD file for cppcheck external repository + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "files", + srcs = glob( + ["**"], + exclude = ["cppcheck.cfg"], + ), +) diff --git a/example/tools/lint/cppcheck_wrapper.sh.tpl b/example/tools/lint/cppcheck_wrapper.sh.tpl new file mode 100644 index 00000000..bd17fba0 --- /dev/null +++ b/example/tools/lint/cppcheck_wrapper.sh.tpl @@ -0,0 +1,23 @@ +#!/bin/bash + +set -o errexit -o nounset -o pipefail + +SCRIPT_DIR="$(dirname "$0")" +RUNFILES_DIR="$SCRIPT_DIR/cppcheck.runfiles" + +# Find the cppcheck binary in the runfiles directory +# It will be under one of the cppcheck_* directories +# Note: cppcheck may be a symlink, so don't use -type f +CPPCHECK_BINARY=$(find "$RUNFILES_DIR" -name "cppcheck" \( -type f -o -type l \) 2>/dev/null | head -n1) + +if [[ -z "$CPPCHECK_BINARY" ]]; then + echo "Error: Could not find cppcheck binary in runfiles" >&2 + exit 1 +fi + +# cppcheck does not support config files. +# Instead options like --check-level can be added here: +"$CPPCHECK_BINARY" \ + --check-level=exhaustive \ + --enable=warning,style,performance,portability,information \ + "$@" diff --git a/example/tools/lint/linters.bzl b/example/tools/lint/linters.bzl index ae24ed6a..dbcff5fe 100644 --- a/example/tools/lint/linters.bzl +++ b/example/tools/lint/linters.bzl @@ -3,6 +3,7 @@ load("@aspect_rules_lint//lint:buf.bzl", "lint_buf_aspect") load("@aspect_rules_lint//lint:checkstyle.bzl", "lint_checkstyle_aspect") load("@aspect_rules_lint//lint:clang_tidy.bzl", "lint_clang_tidy_aspect") +load("@aspect_rules_lint//lint:cppcheck.bzl", "lint_cppcheck_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:keep_sorted.bzl", "lint_keep_sorted_aspect") @@ -122,6 +123,12 @@ clang_tidy = lint_clang_tidy_aspect( clang_tidy_test = lint_test(aspect = clang_tidy) +cppcheck = lint_cppcheck_aspect( + binary = Label("//tools/lint:cppcheck"), + verbose = True, +) +cppcheck_test = lint_test(aspect = cppcheck) + # 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( diff --git a/lint/BUILD.bazel b/lint/BUILD.bazel index 71ed80d4..321d5b94 100644 --- a/lint/BUILD.bazel +++ b/lint/BUILD.bazel @@ -293,3 +293,19 @@ bzl_library( name = "vale_versions", srcs = ["vale_versions.bzl"], ) + +bzl_library( + name = "cppcheck", + srcs = ["cppcheck.bzl"], + 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 = "cppcheck_wrapper", + srcs = ["cppcheck_wrapper.bash"], +) diff --git a/lint/cppcheck.bzl b/lint/cppcheck.bzl new file mode 100644 index 00000000..0d8ffd8f --- /dev/null +++ b/lint/cppcheck.bzl @@ -0,0 +1,202 @@ +"""API for calling declaring a cppcheck lint aspect. +""" + +load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") +load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "OPTIONAL_SARIF_PARSER_TOOLCHAIN", "OUTFILE_FORMAT", "noop_lint_action", "output_files", "parse_to_sarif_action") + +_MNEMONIC = "AspectRulesLintCppCheck" + +def _gather_inputs(compilation_context, srcs): + inputs = srcs + return depset(inputs, transitive = [compilation_context.headers]) + +# taken over from clang_tidy.bzl +def _is_source(file): + permitted_source_types = [ + "c", + "cc", + "cpp", + "cxx", + "c++", + "C", + ] + return (file.is_source and file.extension in permitted_source_types) + +# taken over from clang_tidy.bzl +# modification of filter_srcs in lint_aspect.bzl that filters out header files +def _filter_srcs(rule): + # some rules can return a CcInfo without having a srcs attribute + if not hasattr(rule.attr, "srcs"): + return [] + 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 _prefixed(list, prefix): + array = [] + for arg in list: + array.append("{} {}".format(prefix, arg)) + return array + +def _get_compiler_args(compilation_context): + # add includes + args = [] + args.extend(_prefixed(compilation_context.framework_includes.to_list(), "-I")) + args.extend(_prefixed(compilation_context.includes.to_list(), "-I")) + args.extend(_prefixed(compilation_context.quote_includes.to_list(), "-I")) + args.extend(_prefixed(compilation_context.system_includes.to_list(), "-I")) + args.extend(_prefixed(compilation_context.external_includes.to_list(), "-I")) + return args + +def cppcheck_action(ctx, compilation_context, executable, srcs, stdout, exit_code, do_xml = False): + """Create a Bazel Action that spawns a cppcheck process. + + Args: + ctx: an action context OR aspect context + compilation_context: from target + executable: struct with a cppcheck field + srcs: file objects to lint + stdout: output file containing the stdout or --output-file of cppcheck + exit_code: output file containing the exit code of cppcheck. + If None, then fail the build when cppcheck exits non-zero. + do_xml: If true, xml output is generated + """ + + outputs = [stdout] + env = {} + env["CPPCHECK__STDOUT_STDERR_OUTPUT_FILE"] = stdout.path + + if exit_code: + env["CPPCHECK__EXIT_CODE_OUTPUT_FILE"] = exit_code.path + outputs.append(exit_code) + + env["CPPCHECK__VERBOSE"] = "1" if ctx.attr._verbose else "" + + cppcheck_args = [] + + # cppcheck shall fail with exit code != 0 if issues found + cppcheck_args.append("--error-exitcode=31") + + # add include paths + cppcheck_args.extend(_get_compiler_args(compilation_context)) + + if do_xml: + cppcheck_args.append("--xml-version=3") + + for f in srcs: + cppcheck_args.append(f.short_path) + + ctx.actions.run_shell( + inputs = _gather_inputs(compilation_context, srcs), + outputs = outputs, + tools = [executable._cppcheck_wrapper, executable._cppcheck, find_cpp_toolchain(ctx).all_files], + command = executable._cppcheck_wrapper.path + " $@", + arguments = [executable._cppcheck.path] + cppcheck_args, + env = env, + mnemonic = _MNEMONIC, + progress_message = "Linting %{label} with cppcheck", + ) + +def _cppcheck_aspect_impl(target, ctx): + if not CcInfo in target: + return [] + + files_to_lint = _filter_srcs(ctx.rule) + compilation_context = target[CcInfo].compilation_context + if hasattr(ctx.rule.attr, "implementation_deps"): + compilation_context = cc_common.merge_compilation_contexts( + compilation_contexts = [compilation_context] + + [implementation_dep[CcInfo].compilation_context for implementation_dep in ctx.rule.attr.implementation_deps], + ) + + outputs, info = output_files(_MNEMONIC, target, ctx) + + if len(files_to_lint) == 0: + noop_lint_action(ctx, outputs) + return [info] + + cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, outputs.human.out, outputs.human.exit_code) + + # report: + raw_machine_report = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "raw_machine_report")) + cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, raw_machine_report, outputs.machine.exit_code) + parse_to_sarif_action(ctx, _MNEMONIC, raw_machine_report, outputs.machine.out) + + xml_output = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "xml")) + xml_exit_code = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "xml_exit_code")) + cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, xml_output, xml_exit_code, do_xml = True) + + # Create new OutputGroupInfo with xml_output added to machine outputs + info = OutputGroupInfo( + rules_lint_human = info.rules_lint_human, + rules_lint_machine = info.rules_lint_machine, + rules_lint_report = info.rules_lint_report, + rules_lint_xml = depset([xml_output]), + _validation = info._validation, + ) + return [info] + +def lint_cppcheck_aspect(binary, verbose = False): + """A factory function to create a linter aspect. + + Args: + binary: the cppcheck binary, typically a rule like + + ```starlark + sh_binary( + name = "cppcheck", + srcs = [":cppcheck_wrapper.sh"], + ) + ``` + As cppcheck does not support any configuration files so far, all arguments + shall be directly implemented in the wrapper script. This file can also directly + pass the license file to cppcheck, if needed. + + An example wrapper script could look like this: + + ```bash + #!/bin/bash + + ~/.local/bin/cppcheck/cppcheck \ + --check-level=exhaustive \ + --enable=warning,style,performance,portability,information \ + "$@" + ``` + + verbose: print debug messages including cppcheck command lines being invoked. + """ + + return aspect( + implementation = _cppcheck_aspect_impl, + attrs = { + "_options": attr.label( + default = "//lint:options", + providers = [LintOptionsInfo], + ), + "_verbose": attr.bool( + default = verbose, + ), + "_cppcheck": attr.label( + default = binary, + executable = True, + cfg = "exec", + ), + "_cppcheck_wrapper": attr.label( + default = Label("@aspect_rules_lint//lint:cppcheck_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 = [ + OPTIONAL_SARIF_PARSER_TOOLCHAIN, + "@bazel_tools//tools/cpp:toolchain_type", + ], + fragments = ["cpp"], + ) diff --git a/lint/cppcheck_wrapper.bash b/lint/cppcheck_wrapper.bash new file mode 100755 index 00000000..71841c21 --- /dev/null +++ b/lint/cppcheck_wrapper.bash @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# This is a wrapper for cppcheck which gives us control over error handling +# Usage: cppcheck_wrapper.bash +# +# Controls: +# - CPPCHECK__VERBOSE: If set, be verbose +# - CPPCHECK__STDOUT_STDERR_OUTPUT_FILE: If set, write stdout and stderr to this file +# - CPPCHECK__EXIT_CODE_OUTPUT_FILE: If set, write the highest exit code +# to this file and return success + +# First arg is cppcheck path +cppcheck=$1 +shift + +if [[ -n $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then + # Create the file if it doesn't exist + touch $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE + # Clear the file if it does exist + > $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE + if [[ -n $CPPCHECK__VERBOSE ]]; then + echo "Output > ${CPPCHECK__STDOUT_STDERR_OUTPUT_FILE}" + fi +fi +if [[ -n $CPPCHECK__EXIT_CODE_OUTPUT_FILE ]]; then + if [[ -n $CPPCHECK__VERBOSE ]]; then + echo "Exit Code -> ${CPPCHECK__EXIT_CODE_OUTPUT_FILE}" + fi +fi + +if [[ -n $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then + out_file=$CPPCHECK__STDOUT_STDERR_OUTPUT_FILE +else + out_file=$(mktemp) +fi +# include stderr in output file; it contains some of the diagnostics +command="$cppcheck $@ $file > $out_file 2>&1" +if [[ -n $CPPCHECK__VERBOSE ]]; then + echo "$@" + echo "cwd: " `pwd` + echo $command +fi +eval $command +exit_code=$? +if [ $exit_code -eq 1 ] && [ -s $out_file ]; then + echo "Error: " $exit_code + echo "Something went wrong when running cppcheck. Maybe license file missing?" +fi +cat $out_file + +if [[ -z $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then + rm $out_file +fi +# if CPPCHECK__EXIT_CODE_FILE is set, write the max exit code to that file and return success +if [[ -n $CPPCHECK__EXIT_CODE_OUTPUT_FILE ]]; then + if [[ -n $CPPCHECK__VERBOSE ]]; then + echo "echo $exit_code > $CPPCHECK__EXIT_CODE_OUTPUT_FILE" + echo "exit 0" + fi + echo $exit_code > $CPPCHECK__EXIT_CODE_OUTPUT_FILE + exit 0 +fi + +if [[ -n $CPPCHECK__VERBOSE ]]; then + echo exit $exit_code +fi + +exit $exit_code