Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] | |
Expand Down Expand Up @@ -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/
Expand Down
19 changes: 19 additions & 0 deletions example/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure if this okay, isn't cppcheckpremium a paid tool?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. There is also a free version, which can be installed with apt install cppcheck. I tried to use rules_distroless, but it does not work due to transitive dependencies. Do we need to model the tool in Bazel or can it be installed in the environment?

)

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"],
)
16 changes: 16 additions & 0 deletions example/WORKSPACE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
31 changes: 31 additions & 0 deletions example/tools/lint/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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__"])

Expand Down Expand Up @@ -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"],
}),
)
14 changes: 14 additions & 0 deletions example/tools/lint/cppcheck.BUILD
Original file line number Diff line number Diff line change
@@ -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(["**"]),
)
11 changes: 11 additions & 0 deletions example/tools/lint/cppcheck_wrapper.sh.tpl
Original file line number Diff line number Diff line change
@@ -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 \
"$@"
7 changes: 7 additions & 0 deletions example/tools/lint/linters.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions lint/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
202 changes: 202 additions & 0 deletions lint/cppcheck.bzl
Original file line number Diff line number Diff line change
@@ -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"],
)
Loading