From e6774db59dfd8bbf6eaa59006c36a6bf8bd16f2c Mon Sep 17 00:00:00 2001 From: Guillaume Chauvel Date: Fri, 16 Oct 2026 11:32:19 +0200 Subject: [PATCH 1/2] Fix #13417 (addons: Line suppressions acts as global suppressions) Suppression.isMatch() accepted any suppression without a type in its last branch, ignoring lineNumber, so --suppress=id:file:line hid the id on every line of the file when an addon runs outside cppcheck. --- addons/cppcheckdata.py | 1 + test/addon/test_cppcheckdata.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/cppcheckdata.py b/addons/cppcheckdata.py index 4271bd81376..a6127fed5df 100755 --- a/addons/cppcheckdata.py +++ b/addons/cppcheckdata.py @@ -1052,6 +1052,7 @@ def isMatch(self, file, line, message, errorId): # Other Suppression (Globally set via suppression file or cli command) if ((self.fileName is None or fnmatch(file, self.fileName)) and (self.suppressionType is None) + and (self.lineNumber is None) and (self.symbolName is None or fnmatch(message, '*'+self.symbolName+'*')) and fnmatch(errorId, self.errorId)): return True diff --git a/test/addon/test_cppcheckdata.py b/test/addon/test_cppcheckdata.py index fc5d553e78b..2711d4541b0 100644 --- a/test/addon/test_cppcheckdata.py +++ b/test/addon/test_cppcheckdata.py @@ -771,9 +771,6 @@ def test_line_suppression(self): assert supp.isMatch('a.c', '5', 'msg', 'zerodiv') assert not supp.isMatch('a.c', '5', 'msg', 'nullPointer') - @pytest.mark.xfail(strict=False, - reason='the "other suppression" fallback in Suppression.isMatch() does not check ' - 'that lineNumber is unset, so a line suppression matches every line') def test_line_suppression_other_line(self): supp = self.make_suppression(errorId='zerodiv', lineNumber='5') assert not supp.isMatch('a.c', '6', 'msg', 'zerodiv') @@ -821,7 +818,8 @@ def test_dumpfile_suppressions(self, dump_factory): location = cppcheckdata.Location({'file': supp.fileName, 'line': '5', 'column': '5'}) assert cppcheckdata.is_suppressed(location, 'msg', 'uninitvar') assert not cppcheckdata.is_suppressed(location, 'msg', 'nullPointer') - # NOTE: a non-matching line is not checked here - see test_line_suppression_other_line + location = cppcheckdata.Location({'file': supp.fileName, 'line': '6', 'column': '5'}) + assert not cppcheckdata.is_suppressed(location, 'msg', 'uninitvar') class TestPureHelpers: From 5ad5b72d2541c2b401b31f80a6d4781b9cbeecba Mon Sep 17 00:00:00 2001 From: Guillaume Chauvel Date: Sat, 14 Nov 2026 15:25:09 +0100 Subject: [PATCH 2/2] Add thisAndNextLine to the dumped suppressions cppcheck applies "{ // cppcheck-suppress id" to the comment line and to the next line. The dump only had lineNumber, so an addon run on the dump reported the id on the next line. Suppression.isMatch() accepts that line when thisAndNextLine is set. --- addons/cppcheckdata.py | 7 +++++-- lib/suppressions.cpp | 2 ++ test/addon/test_cppcheckdata.py | 7 +++++++ test/cli/dumpfile_test.py | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/addons/cppcheckdata.py b/addons/cppcheckdata.py index a6127fed5df..96e08471b42 100755 --- a/addons/cppcheckdata.py +++ b/addons/cppcheckdata.py @@ -997,6 +997,7 @@ class Suppression: errorId The id string of the error to suppress, can be a wildcard fileName The name of the file to suppress warnings for, can include wildcards lineNumber The number of the line to suppress warnings from, can be 0 to represent any line + thisAndNextLine Set if the line after lineNumber is suppressed too symbolName The name of the symbol to match warnings for, can include wildcards lineBegin The first line to suppress warnings from lineEnd The last line to suppress warnings from @@ -1006,6 +1007,7 @@ class Suppression: errorId = None fileName = None lineNumber = None + thisAndNextLine = None symbolName = None lineBegin = None lineEnd = None @@ -1015,13 +1017,14 @@ def __init__(self, element): self.errorId = element.get('errorId') self.fileName = element.get('fileName') self.lineNumber = element.get('lineNumber') + self.thisAndNextLine = element.get('thisAndNextLine') self.symbolName = element.get('symbolName') self.lineBegin = element.get('lineBegin') self.lineEnd = element.get('lineEnd') self.suppressionType = element.get('type') def __repr__(self): - attrs = ["errorId", "fileName", "lineNumber", "symbolName", "lineBegin", "lineEnd","suppressionType"] + attrs = ["errorId", "fileName", "lineNumber", "thisAndNextLine", "symbolName", "lineBegin", "lineEnd","suppressionType"] return "{}({})".format( "Suppression", ", ".join(("{}={}".format(a, repr(getattr(self, a))) for a in attrs)) @@ -1031,7 +1034,7 @@ def isMatch(self, file, line, message, errorId): # Line Suppression if ((self.fileName is None or fnmatch(file, self.fileName)) and (self.suppressionType is None) # Verify use of default suppression type (None = unique) - and (self.lineNumber is not None and int(line) == int(self.lineNumber)) + and (self.lineNumber is not None and (int(line) == int(self.lineNumber) or (self.thisAndNextLine and int(line) == int(self.lineNumber) + 1))) and (self.symbolName is None or fnmatch(message, '*'+self.symbolName+'*')) and fnmatch(errorId, self.errorId)): return True diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 0089510c86f..7f8d05eb0fa 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -609,6 +609,8 @@ void SuppressionList::dump(std::ostream & out, const std::string& filePath) cons out << " fileName=\"" << ErrorLogger::toxml(suppression.fileName) << '"'; if (suppression.lineNumber != Suppression::NO_LINE) out << " lineNumber=\"" << suppression.lineNumber << '"'; + if (suppression.thisAndNextLine) + out << " thisAndNextLine=\"true\""; if (!suppression.symbolName.empty()) out << " symbolName=\"" << ErrorLogger::toxml(suppression.symbolName) << '\"'; if (suppression.hash > 0) diff --git a/test/addon/test_cppcheckdata.py b/test/addon/test_cppcheckdata.py index 2711d4541b0..cd86ba8f450 100644 --- a/test/addon/test_cppcheckdata.py +++ b/test/addon/test_cppcheckdata.py @@ -775,6 +775,13 @@ def test_line_suppression_other_line(self): supp = self.make_suppression(errorId='zerodiv', lineNumber='5') assert not supp.isMatch('a.c', '6', 'msg', 'zerodiv') + def test_line_suppression_this_and_next_line(self): + supp = self.make_suppression(errorId='zerodiv', lineNumber='5', thisAndNextLine='true') + assert supp.isMatch('a.c', '5', 'msg', 'zerodiv') + assert supp.isMatch('a.c', '6', 'msg', 'zerodiv') + assert not supp.isMatch('a.c', '4', 'msg', 'zerodiv') + assert not supp.isMatch('a.c', '7', 'msg', 'zerodiv') + def test_wildcard_errorId(self): supp = self.make_suppression(errorId='*', lineNumber='5') assert supp.isMatch('a.c', '5', 'msg', 'anything') diff --git a/test/cli/dumpfile_test.py b/test/cli/dumpfile_test.py index 5dbb29f9e38..b4af9f74549 100644 --- a/test/cli/dumpfile_test.py +++ b/test/cli/dumpfile_test.py @@ -146,3 +146,22 @@ def test_line_macro(tmpdir): with open(dumpfile, 'rt') as f: dump = f.read() assert 'type="number" isInt="true" macroName="__LINE__"' in dump + + +def test_suppression_this_and_next_line(tmpdir): + test_file = str(tmpdir / 'test.c') + with open(test_file, 'wt') as f: + f.write('void f(void)\n') + f.write('{ // cppcheck-suppress id1\n') + f.write(' // cppcheck-suppress id2\n') + f.write(' x = 0;\n') + f.write('}\n') + + exitcode, _, stderr = cppcheck(['--dump', '--inline-suppr', test_file]) + assert exitcode == 0, stderr + + dumpfile = test_file + '.dump' + with open(dumpfile, 'rt') as f: + dump = f.read() + assert 'lineNumber="2" thisAndNextLine="true" inline="true"' in dump + assert 'lineNumber="4" inline="true"' in dump