Skip to content

Commit 47f06fa

Browse files
committed
Add cppcheck lint aspect
1 parent 4ba6e3f commit 47f06fa

File tree

9 files changed

+359
-1
lines changed

9 files changed

+359
-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] | |
@@ -87,6 +87,7 @@ Linters which are not language-specific:
8787
[taplo] : https://taplo.tamasfe.dev/
8888
[clang-format]: https://clang.llvm.org/docs/ClangFormat.html
8989
[clang-tidy]: https://clang.llvm.org/extra/clang-tidy/
90+
[cppcheck]: https://www.cppcheck.com/
9091
[vale]: https://vale.sh/
9192
[yamlfmt]: https://github.com/google/yamlfmt
9293
[rustfmt]: https://rust-lang.github.io/rustfmt

docs/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,9 @@ stardoc_with_diff_test(
7777
bzl_library_target = "@aspect_rules_lint//lint:clang_tidy",
7878
)
7979

80+
stardoc_with_diff_test(
81+
name = "cppcheck",
82+
bzl_library_target = "@aspect_rules_lint//lint:cppcheck",
83+
)
84+
8085
update_docs(name = "update")

docs/cppcheck.md

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/tools/lint/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,8 @@ native_binary(
104104
),
105105
out = "clang_tidy",
106106
)
107+
108+
sh_binary(
109+
name = "cppcheck",
110+
srcs = ["cppcheck_wrapper.sh"],
111+
)
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")
@@ -102,6 +103,12 @@ clang_tidy = lint_clang_tidy_aspect(
102103

103104
clang_tidy_test = lint_test(aspect = clang_tidy)
104105

106+
cppcheck = lint_cppcheck_aspect(
107+
binary = Label("//tools/lint:cppcheck"),
108+
verbose = True,
109+
)
110+
cppcheck_test = lint_test(aspect = cppcheck)
111+
105112
# an example of setting up a different clang-tidy aspect with different
106113
# options. This one uses a single global clang-tidy file
107114
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
@@ -269,3 +269,19 @@ sh_binary(
269269
name = "clang_tidy_wrapper",
270270
srcs = ["clang_tidy_wrapper.bash"],
271271
)
272+
273+
bzl_library(
274+
name = "cppcheck",
275+
srcs = ["cppcheck.bzl"],
276+
deps = _BAZEL_TOOLS + [
277+
"//lint/private:lint_aspect",
278+
"@bazel_skylib//lib:dicts",
279+
"@bazel_tools//tools/build_defs/cc:action_names.bzl",
280+
"@bazel_tools//tools/cpp:toolchain_utils.bzl",
281+
],
282+
)
283+
284+
sh_binary(
285+
name = "cppcheck_wrapper",
286+
srcs = ["cppcheck_wrapper.bash"],
287+
)

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+
)

0 commit comments

Comments
 (0)