Skip to content

Commit e770500

Browse files
committed
Add cppcheck lint aspect
1 parent 78eebc1 commit e770500

File tree

7 files changed

+303
-1
lines changed

7 files changed

+303
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Linters which are not language-specific:
3131

3232
| Language | Formatter | Linter(s) |
3333
| ---------------------- | ------------------------- | -------------------------------- |
34-
| C / C++ | [clang-format] | [clang-tidy] |
34+
| C / C++ | [clang-format] | [clang-tidy] or [cppcheck] |
3535
| Cuda | [clang-format] | |
3636
| CSS, Less, Sass | [Prettier] | [Stylelint] |
3737
| Go | [gofmt] or [gofumpt] | |
@@ -92,6 +92,7 @@ Linters which are not language-specific:
9292
[taplo]: https://taplo.tamasfe.dev/
9393
[clang-format]: https://clang.llvm.org/docs/ClangFormat.html
9494
[clang-tidy]: https://clang.llvm.org/extra/clang-tidy/
95+
[cppcheck]: https://www.cppcheck.com/
9596
[vale]: https://vale.sh/
9697
[yamlfmt]: https://github.com/google/yamlfmt
9798
[yamllint]: https://yamllint.readthedocs.io/en/stable/

example/tools/lint/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,8 @@ alias(
119119
name = "rubocop",
120120
actual = "@bundle//bin:rubocop",
121121
)
122+
123+
sh_binary(
124+
name = "cppcheck",
125+
srcs = ["cppcheck_wrapper.sh"],
126+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
~/.local/bin/cppcheckpremium/cppcheck \
2+
--check-level=exhaustive \
3+
--enable=warning,style,performance,portability,information \
4+
"$@"

example/tools/lint/linters.bzl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
load("@aspect_rules_lint//lint:buf.bzl", "lint_buf_aspect")
44
load("@aspect_rules_lint//lint:checkstyle.bzl", "lint_checkstyle_aspect")
55
load("@aspect_rules_lint//lint:clang_tidy.bzl", "lint_clang_tidy_aspect")
6+
load("@aspect_rules_lint//lint:cppcheck.bzl", "lint_cppcheck_aspect")
67
load("@aspect_rules_lint//lint:eslint.bzl", "lint_eslint_aspect")
78
load("@aspect_rules_lint//lint:flake8.bzl", "lint_flake8_aspect")
89
load("@aspect_rules_lint//lint:keep_sorted.bzl", "lint_keep_sorted_aspect")
@@ -121,6 +122,12 @@ clang_tidy = lint_clang_tidy_aspect(
121122

122123
clang_tidy_test = lint_test(aspect = clang_tidy)
123124

125+
cppcheck = lint_cppcheck_aspect(
126+
binary = Label("//tools/lint:cppcheck"),
127+
verbose = True,
128+
)
129+
cppcheck_test = lint_test(aspect = cppcheck)
130+
124131
# an example of setting up a different clang-tidy aspect with different
125132
# options. This one uses a single global clang-tidy file
126133
clang_tidy_global_config = lint_clang_tidy_aspect(

lint/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,19 @@ bzl_library(
287287
name = "vale_versions",
288288
srcs = ["vale_versions.bzl"],
289289
)
290+
291+
bzl_library(
292+
name = "cppcheck",
293+
srcs = ["cppcheck.bzl"],
294+
deps = _BAZEL_TOOLS + [
295+
"//lint/private:lint_aspect",
296+
"@bazel_skylib//lib:dicts",
297+
"@bazel_tools//tools/build_defs/cc:action_names.bzl",
298+
"@bazel_tools//tools/cpp:toolchain_utils.bzl",
299+
],
300+
)
301+
302+
sh_binary(
303+
name = "cppcheck_wrapper",
304+
srcs = ["cppcheck_wrapper.bash"],
305+
)

lint/cppcheck.bzl

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""API for calling declaring a cppcheck lint aspect.
2+
"""
3+
4+
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
5+
load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "OPTIONAL_SARIF_PARSER_TOOLCHAIN", "OUTFILE_FORMAT", "noop_lint_action", "output_files", "parse_to_sarif_action")
6+
7+
_MNEMONIC = "AspectRulesLintCppCheck"
8+
9+
def _gather_inputs(compilation_context, srcs):
10+
inputs = srcs
11+
return depset(inputs, transitive = [compilation_context.headers])
12+
13+
# taken over from clang_tidy.bzl
14+
def _is_source(file):
15+
permitted_source_types = [
16+
"c",
17+
"cc",
18+
"cpp",
19+
"cxx",
20+
"c++",
21+
"C",
22+
]
23+
return (file.is_source and file.extension in permitted_source_types)
24+
25+
# taken over from clang_tidy.bzl
26+
# modification of filter_srcs in lint_aspect.bzl that filters out header files
27+
def _filter_srcs(rule):
28+
# some rules can return a CcInfo without having a srcs attribute
29+
if not hasattr(rule.attr, "srcs"):
30+
return []
31+
if "lint-genfiles" in rule.attr.tags:
32+
return rule.files.srcs
33+
else:
34+
return [s for s in rule.files.srcs if _is_source(s)]
35+
36+
def _prefixed(list, prefix):
37+
array = []
38+
for arg in list:
39+
array.append("{} {}".format(prefix, arg))
40+
return array
41+
42+
def _get_compiler_args(compilation_context):
43+
# add includes
44+
args = []
45+
args.extend(_prefixed(compilation_context.framework_includes.to_list(), "-I"))
46+
args.extend(_prefixed(compilation_context.includes.to_list(), "-I"))
47+
args.extend(_prefixed(compilation_context.quote_includes.to_list(), "-I"))
48+
args.extend(_prefixed(compilation_context.system_includes.to_list(), "-I"))
49+
args.extend(_prefixed(compilation_context.external_includes.to_list(), "-I"))
50+
return args
51+
52+
def cppcheck_action(ctx, compilation_context, executable, srcs, stdout, exit_code, do_xml = False):
53+
"""Create a Bazel Action that spawns a cppcheck process.
54+
55+
Args:
56+
ctx: an action context OR aspect context
57+
compilation_context: from target
58+
executable: struct with a cppcheck field
59+
srcs: file objects to lint
60+
stdout: output file containing the stdout or --output-file of cppcheck
61+
exit_code: output file containing the exit code of cppcheck.
62+
If None, then fail the build when cppcheck exits non-zero.
63+
do_xml: If true, xml output is generated
64+
"""
65+
66+
outputs = [stdout]
67+
env = {}
68+
env["CPPCHECK__STDOUT_STDERR_OUTPUT_FILE"] = stdout.path
69+
70+
if exit_code:
71+
env["CPPCHECK__EXIT_CODE_OUTPUT_FILE"] = exit_code.path
72+
outputs.append(exit_code)
73+
74+
env["CPPCHECK__VERBOSE"] = "1" if ctx.attr._verbose else ""
75+
76+
cppcheck_args = []
77+
78+
# cppcheck shall fail with exit code != 0 if issues found
79+
cppcheck_args.append("--error-exitcode=31")
80+
81+
# add include paths
82+
cppcheck_args.extend(_get_compiler_args(compilation_context))
83+
84+
if do_xml:
85+
cppcheck_args.append("--xml-version=3")
86+
87+
for f in srcs:
88+
cppcheck_args.append(f.short_path)
89+
90+
ctx.actions.run_shell(
91+
inputs = _gather_inputs(compilation_context, srcs),
92+
outputs = outputs,
93+
tools = [executable._cppcheck_wrapper, executable._cppcheck, find_cpp_toolchain(ctx).all_files],
94+
command = executable._cppcheck_wrapper.path + " $@",
95+
arguments = [executable._cppcheck.path] + cppcheck_args,
96+
env = env,
97+
mnemonic = _MNEMONIC,
98+
progress_message = "Linting %{label} with cppcheck",
99+
)
100+
101+
def _cppcheck_aspect_impl(target, ctx):
102+
if not CcInfo in target:
103+
return []
104+
105+
files_to_lint = _filter_srcs(ctx.rule)
106+
compilation_context = target[CcInfo].compilation_context
107+
if hasattr(ctx.rule.attr, "implementation_deps"):
108+
compilation_context = cc_common.merge_compilation_contexts(
109+
compilation_contexts = [compilation_context] +
110+
[implementation_dep[CcInfo].compilation_context for implementation_dep in ctx.rule.attr.implementation_deps],
111+
)
112+
113+
outputs, info = output_files(_MNEMONIC, target, ctx)
114+
115+
if len(files_to_lint) == 0:
116+
noop_lint_action(ctx, outputs)
117+
return [info]
118+
119+
cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, outputs.human.out, outputs.human.exit_code)
120+
121+
# report:
122+
raw_machine_report = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "raw_machine_report"))
123+
cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, raw_machine_report, outputs.machine.exit_code)
124+
parse_to_sarif_action(ctx, _MNEMONIC, raw_machine_report, outputs.machine.out)
125+
126+
xml_output = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "xml"))
127+
xml_exit_code = ctx.actions.declare_file(OUTFILE_FORMAT.format(label = target.label.name, mnemonic = _MNEMONIC, suffix = "xml_exit_code"))
128+
cppcheck_action(ctx, compilation_context, ctx.executable, files_to_lint, xml_output, xml_exit_code, do_xml = True)
129+
130+
# Create new OutputGroupInfo with xml_output added to machine outputs
131+
info = OutputGroupInfo(
132+
rules_lint_human = info.rules_lint_human,
133+
rules_lint_machine = info.rules_lint_machine,
134+
rules_lint_report = info.rules_lint_report,
135+
rules_lint_xml = depset([xml_output]),
136+
_validation = info._validation,
137+
)
138+
return [info]
139+
140+
def lint_cppcheck_aspect(binary, verbose = False):
141+
"""A factory function to create a linter aspect.
142+
143+
Args:
144+
binary: the cppcheck binary, typically a rule like
145+
146+
```starlark
147+
sh_binary(
148+
name = "cppcheck",
149+
srcs = [":cppcheck_wrapper.sh"],
150+
)
151+
```
152+
As cppcheck does not support any configuration files so far, all arguments
153+
shall be directly implemented in the wrapper script. This file can also directly
154+
pass the license file to cppcheck, if needed.
155+
156+
An example wrapper script could look like this:
157+
158+
```bash
159+
#!/bin/bash
160+
161+
~/.local/bin/cppcheckpremium/cppcheck \
162+
--check-level=exhaustive \
163+
--enable=warning,style,performance,portability,information \
164+
"$@"
165+
```
166+
167+
verbose: print debug messages including cppcheck command lines being invoked.
168+
"""
169+
170+
return aspect(
171+
implementation = _cppcheck_aspect_impl,
172+
attrs = {
173+
"_options": attr.label(
174+
default = "//lint:options",
175+
providers = [LintOptionsInfo],
176+
),
177+
"_verbose": attr.bool(
178+
default = verbose,
179+
),
180+
"_cppcheck": attr.label(
181+
default = binary,
182+
executable = True,
183+
cfg = "exec",
184+
),
185+
"_cppcheck_wrapper": attr.label(
186+
default = Label("@aspect_rules_lint//lint:cppcheck_wrapper"),
187+
executable = True,
188+
cfg = "exec",
189+
),
190+
"_patcher": attr.label(
191+
default = "@aspect_rules_lint//lint/private:patcher",
192+
executable = True,
193+
cfg = "exec",
194+
),
195+
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
196+
},
197+
toolchains = [
198+
OPTIONAL_SARIF_PARSER_TOOLCHAIN,
199+
"@bazel_tools//tools/cpp:toolchain_type",
200+
],
201+
fragments = ["cpp"],
202+
)

lint/cppcheck_wrapper.bash

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# This is a wrapper for cppcheck which gives us control over error handling
3+
# Usage: cppcheck_wrapper.bash <cppcheck-path> <args>
4+
#
5+
# Controls:
6+
# - CPPCHECK__VERBOSE: If set, be verbose
7+
# - CPPCHECK__STDOUT_STDERR_OUTPUT_FILE: If set, write stdout and stderr to this file
8+
# - CPPCHECK__EXIT_CODE_OUTPUT_FILE: If set, write the highest exit code
9+
# to this file and return success
10+
11+
# First arg is cppcheck path
12+
cppcheck=$1
13+
shift
14+
15+
if [[ -n $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then
16+
# Create the file if it doesn't exist
17+
touch $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE
18+
# Clear the file if it does exist
19+
> $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE
20+
if [[ -n $CPPCHECK__VERBOSE ]]; then
21+
echo "Output > ${CPPCHECK__STDOUT_STDERR_OUTPUT_FILE}"
22+
fi
23+
fi
24+
if [[ -n $CPPCHECK__EXIT_CODE_OUTPUT_FILE ]]; then
25+
if [[ -n $CPPCHECK__VERBOSE ]]; then
26+
echo "Exit Code -> ${CPPCHECK__EXIT_CODE_OUTPUT_FILE}"
27+
fi
28+
fi
29+
30+
if [[ -n $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then
31+
out_file=$CPPCHECK__STDOUT_STDERR_OUTPUT_FILE
32+
else
33+
out_file=$(mktemp)
34+
fi
35+
# include stderr in output file; it contains some of the diagnostics
36+
command="$cppcheck $@ $file > $out_file 2>&1"
37+
if [[ -n $CPPCHECK__VERBOSE ]]; then
38+
echo "$@"
39+
echo "cwd: " `pwd`
40+
echo $command
41+
fi
42+
eval $command
43+
exit_code=$?
44+
if [ $exit_code -eq 1 ] && [ -s $out_file ]; then
45+
echo "Error: " $exit_code
46+
echo "Something went wrong when running cppcheck. Maybe license file missing?"
47+
fi
48+
cat $out_file
49+
50+
if [[ -z $CPPCHECK__STDOUT_STDERR_OUTPUT_FILE ]]; then
51+
rm $out_file
52+
fi
53+
# if CPPCHECK__EXIT_CODE_FILE is set, write the max exit code to that file and return success
54+
if [[ -n $CPPCHECK__EXIT_CODE_OUTPUT_FILE ]]; then
55+
if [[ -n $CPPCHECK__VERBOSE ]]; then
56+
echo "echo $exit_code > $CPPCHECK__EXIT_CODE_OUTPUT_FILE"
57+
echo "exit 0"
58+
fi
59+
echo $exit_code > $CPPCHECK__EXIT_CODE_OUTPUT_FILE
60+
exit 0
61+
fi
62+
63+
if [[ -n $CPPCHECK__VERBOSE ]]; then
64+
echo exit $exit_code
65+
fi
66+
67+
exit $exit_code

0 commit comments

Comments
 (0)