Skip to content

Commit f5c4f4e

Browse files
committed
Fix #15047 (baseline: add script and documentation)
1 parent 3a9750e commit f5c4f4e

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

man/manual.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,120 @@ You can write comments about a suppression as follows:
777777
// cppcheck-suppress warningid ; some comment
778778
// cppcheck-suppress warningid // some comment
779779

780+
# Generating and using a baseline
781+
782+
When you first run Cppcheck on an existing codebase it's common to get a
783+
large number of warnings. A "baseline" lets you suppress all of today's
784+
warnings and see only *new* warnings introduced from now on.
785+
786+
This works by asking Cppcheck to include a content-based `hash` for every warning
787+
(computed from the surrounding code, not the line number), then converting today's
788+
warnings into an XML suppressions file keyed on `id` + `fileName` + `hash`. Because
789+
the hash is based on content rather than line number, warnings stay suppressed even
790+
after unrelated lines above them are added or removed. A warning only reappears if
791+
the code it actually points at changes, or a new warning shows up elsewhere.
792+
793+
## 1. Generate the baseline
794+
795+
Run Cppcheck with `--xml` and capture stderr (where Cppcheck writes its XML) to a
796+
file:
797+
798+
```sh
799+
cppcheck --enable=style --xml src 2> baseline-results.xml
800+
```
801+
802+
Use whatever combination of `--enable`/defines/include paths you normally
803+
analyze the project with — the suppressions you get out only cover the
804+
checks you ran.
805+
806+
## 2. Convert the results into a suppressions file
807+
808+
A [script](https://github.com/cppcheck-opensource/cppcheck/blob/main/tools/generate-baseline-suppressions.py) can be used to generate the baseline:
809+
```sh
810+
python3 generate-baseline-suppressions.py baseline-results.xml suppressions.xml
811+
```
812+
813+
This produces a `suppressions.xml` like:
814+
815+
```xml
816+
<?xml version="1.0"?>
817+
<suppressions>
818+
<suppress>
819+
<id>uninitvar</id>
820+
<fileName>src/file1.c</fileName>
821+
<hash>12345678</hash>
822+
</suppress>
823+
</suppressions>
824+
```
825+
826+
`suppressions.xml` needs to be shared by everyone who runs Cppcheck on this
827+
codebase, including CI.
828+
829+
Having a script instead of using some special cppcheck flags has the advantages:
830+
* it's flexible. You can tweak it if needed.
831+
* there is fewer flags for us to maintain and document, and for you to learn.
832+
833+
## 3. Use the baseline on future runs
834+
835+
```sh
836+
cppcheck --enable=style --xml --suppress-xml=suppressions.xml src
837+
```
838+
839+
Only warnings that aren't in the baseline are reported: new warnings in changed
840+
code, and warnings for checks/files that weren't covered when the baseline was
841+
generated.
842+
843+
## 4. Refreshing the baseline
844+
845+
Re-run steps 1-2 whenever you want to accept the current state as the new
846+
baseline (e.g. after cleaning up a batch of warnings, or deliberately accepting a
847+
new one). Regenerating overwrites `suppressions.xml` with an entry for every
848+
warning present at that time.
849+
850+
## The baseline doesn't need to be kept in sync
851+
852+
Once code that a baselined warning pointed at is fixed, refactored away, or
853+
deleted, its `suppress` entry in `suppressions.xml` becomes dead: nothing will
854+
ever match it again. You don't need to go find and remove it.
855+
856+
Cppcheck's `unmatchedSuppression` check (part of `--enable=all`) normally warns
857+
about suppressions that never matched anything, on the theory that a
858+
suppression nobody needs is probably a mistake. But it does *not* fire for
859+
`suppress` entries that carry a `<hash>` — which is every entry the baseline
860+
script generates. So a baseline file with plenty of dead entries produces no
861+
noise, and developers never need to prune it by hand — it only needs to be
862+
regenerated (step 4) when you deliberately want to reset what's accepted.
863+
864+
## Caveats
865+
866+
- A warning can only be suppressed this way if it carries a `hash` attribute in
867+
the XML output. Almost all checks compute one. Critical errors, i.e. syntax
868+
errors do not get hash and must be fixed. Certain information messages do not
869+
get hash neither.
870+
- Changed Cppcheck options might produce new warnings that are not suppressed
871+
by the baseline.
872+
- Cppcheck upgrades don't affect the hash directly (it isn't version-tagged),
873+
but if a new release for instance changes a check's message wording, the hash
874+
changes with it — so upgrading Cppcheck can resurrect baselined warnings for
875+
checks whose messages were reworded, even though nothing in the analyzed code
876+
changed.
877+
878+
## Strategies for gradually shrinking the baseline
879+
880+
A baseline makes it possible to adopt Cppcheck in CI immediately without
881+
fixing everything first, but nothing about it enforces that the accepted set
882+
of warnings actually shrinks over time.
883+
884+
Generic advice:
885+
886+
- **Prioritize by severity** Checks like `uninitvar` or
887+
`nullPointer` are more valuable to clear than `style` warnings; a baseline
888+
makes it possible to drive the highest-severity checks to zero first while
889+
deliberately leaving lower-risk ones suppressed longer.
890+
- **Be careful** Every fix is a code change, and every code change carries some
891+
risk of introducing a new bug. It can make sense to leave some things suppressed
892+
to minimize the risk that bugs are introduced in working code.
893+
780894
# XML output
781895

782896
Cppcheck can generate output in XML format. Use `--xml` to enable this format.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
"""Convert a cppcheck XML results file into a cppcheck XML suppressions file.
3+
4+
Usage: generate-baseline-suppressions.py <results.xml> <suppressions.xml>
5+
6+
Each <error> in the results file that has a non-zero "hash" attribute is
7+
converted into a <suppress> entry using the error's id, the file of its
8+
first <location> (the primary location used by cppcheck's suppression
9+
matching) and the hash.
10+
"""
11+
import sys
12+
import xml.etree.ElementTree as ET
13+
14+
15+
def convert(results_path, suppressions_path):
16+
tree = ET.parse(results_path)
17+
root = tree.getroot()
18+
errors_el = root.find('errors')
19+
20+
seen = set()
21+
suppressions = []
22+
for error in errors_el.findall('error'):
23+
error_id = error.get('id')
24+
error_hash = error.get('hash')
25+
location = error.find('location')
26+
if error_id is None or location is None:
27+
continue
28+
if not error_hash or error_hash == '0':
29+
continue
30+
file_name = location.get('file')
31+
key = (error_id, file_name, error_hash)
32+
if key in seen:
33+
continue
34+
seen.add(key)
35+
suppressions.append(key)
36+
37+
out_root = ET.Element('suppressions')
38+
for error_id, file_name, error_hash in suppressions:
39+
suppress = ET.SubElement(out_root, 'suppress')
40+
ET.SubElement(suppress, 'id').text = error_id
41+
ET.SubElement(suppress, 'fileName').text = file_name
42+
ET.SubElement(suppress, 'hash').text = error_hash
43+
44+
ET.indent(out_root, space=' ')
45+
out_tree = ET.ElementTree(out_root)
46+
out_tree.write(suppressions_path, encoding='UTF-8', xml_declaration=True)
47+
print(f'Wrote {len(suppressions)} suppression(s) to {suppressions_path}')
48+
49+
50+
if __name__ == '__main__':
51+
if len(sys.argv) != 3:
52+
print(f'Usage: {sys.argv[0]} <results.xml> <suppressions.xml>', file=sys.stderr)
53+
sys.exit(1)
54+
convert(sys.argv[1], sys.argv[2])

0 commit comments

Comments
 (0)