diff --git a/README.md b/README.md index 4d9c46d2..5553fa40 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] | | @@ -90,6 +90,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/MODULE.bazel b/example/MODULE.bazel index 85022bc8..5589e10c 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -128,3 +128,22 @@ rust.toolchain( edition = "2021", versions = ["1.75.0"], ) + +# Download cppcheck premium tar files for different platforms +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "cppcheck_premium_linux", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-kacV5Qvy8pHqYoejdDCId4yS3VJDaqTqSJ9JgEIfQd0=", + strip_prefix = "cppcheckpremium-25.8.4", + urls = ["https://files.cppchecksolutions.com/25.8.4/ubuntu-22.04/cppcheckpremium-25.8.4-amd64.tar.gz"], +) + +http_archive( + name = "cppcheck_premium_macos", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-bsxXTXw2YPcClRfTXzsLtgDNTYDmHpNapu12la93tAs=", + strip_prefix = "cppcheckpremium", + urls = ["https://files.cppchecksolutions.com/25.8.4/cppcheckpremium-25.8.4-macos-15.tar.gz"], +) diff --git a/example/WORKSPACE.bazel b/example/WORKSPACE.bazel index e7ef39c2..ea9378ec 100644 --- a/example/WORKSPACE.bazel +++ b/example/WORKSPACE.bazel @@ -385,3 +385,19 @@ multitool( "@aspect_rules_lint//lint:multitool.lock.json", ], ) + +http_archive( + name = "cppcheck_premium_linux", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-kacV5Qvy8pHqYoejdDCId4yS3VJDaqTqSJ9JgEIfQd0=", + strip_prefix = "cppcheckpremium-25.8.4", + urls = ["https://files.cppchecksolutions.com/25.8.4/ubuntu-22.04/cppcheckpremium-25.8.4-amd64.tar.gz"], +) + +http_archive( + name = "cppcheck_premium_macos", + build_file = "//tools/lint:cppcheck.BUILD", + integrity = "sha256-bsxXTXw2YPcClRfTXzsLtgDNTYDmHpNapu12la93tAs=", + strip_prefix = "cppcheckpremium", + urls = ["https://files.cppchecksolutions.com/25.8.4/cppcheckpremium-25.8.4-macos-15.tar.gz"], +) diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel index 77741c68..f94076bc 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__"]) @@ -114,3 +115,33 @@ native_binary( ), out = "clang_tidy", ) + +genrule( + name = "cppcheck_wrapper", + srcs = ["cppcheck_wrapper.sh.tpl"] + select({ + "@platforms//os:linux": [ + "@cppcheck_premium_linux//:cppcheck_binary", + ], + "@platforms//os:macos": [ + "@cppcheck_premium_macos//:cppcheck_binary", + ], + }), + outs = ["cppcheck_wrapper.sh"], + cmd = select({ + "@platforms//os:linux": """ + sed 's|@@CPPCHECK_BINARY@@|$(rlocationpath @cppcheck_premium_linux//:cppcheck_binary)|g' $(location cppcheck_wrapper.sh.tpl) > $@ + """, + "@platforms//os:macos": """ + sed 's|@@CPPCHECK_BINARY@@|$(rlocationpath @cppcheck_premium_macos//:cppcheck_binary)|g' $(location cppcheck_wrapper.sh.tpl) > $@ + """, + }), +) + +sh_binary( + name = "cppcheck", + srcs = [":cppcheck_wrapper"], + data = select({ + "@platforms//os:linux": ["@cppcheck_premium_linux//:runtime_files"], + "@platforms//os:macos": ["@cppcheck_premium_macos//:runtime_files"], + }), +) diff --git a/example/tools/lint/cppcheck.BUILD b/example/tools/lint/cppcheck.BUILD new file mode 100644 index 00000000..203b7583 --- /dev/null +++ b/example/tools/lint/cppcheck.BUILD @@ -0,0 +1,14 @@ +# BUILD file for cppcheck premium external repository + +package(default_visibility = ["//visibility:public"]) + +# Main cppcheck binary as a specific target +filegroup( + name = "cppcheck_binary", + srcs = ["cppcheck"], +) + +filegroup( + name = "runtime_files", + srcs = glob(["**"]), +) diff --git a/example/tools/lint/cppcheck_wrapper.sh.tpl b/example/tools/lint/cppcheck_wrapper.sh.tpl new file mode 100644 index 00000000..889e7670 --- /dev/null +++ b/example/tools/lint/cppcheck_wrapper.sh.tpl @@ -0,0 +1,11 @@ +#!/bin/bash + +SCRIPT_DIR="$(dirname "$0")" +CPPCHECK_BINARY="$SCRIPT_DIR/cppcheck.runfiles/@@CPPCHECK_BINARY@@" + +# 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 8620c1f8..2f870342 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") @@ -120,6 +121,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 bcb82731..216866be 100644 --- a/lint/BUILD.bazel +++ b/lint/BUILD.bazel @@ -281,3 +281,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..80f6ba12 --- /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/cppcheckpremium/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