From 893ffd38fd0bc1e20828f20f067732795962c4d9 Mon Sep 17 00:00:00 2001 From: bruntib Date: Thu, 10 Oct 2024 13:25:24 +0200 Subject: [PATCH 1/2] [fix] Cppcheck unknown C/C++ standard Cppcheck tool is failing from version 2.15 if "--std=" flag is given an unknown value. For example, "--std=c++0x" is not known, but "--std=c++11" is. Before cppcheck-2.15 the latest known standard version was silently used. In this patch the unknown standard versions are mapped to the known ones. --- .../analyzers/cppcheck/analyzer.py | 29 +++++++++++++++++++ .../tests/functional/analyze/test_analyze.py | 22 ++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index 4ad42a296e..ed44223560 100644 --- a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py @@ -138,6 +138,34 @@ def parse_analyzer_config(self): # * -std c99 # * -stdlib=libc++ std_regex = re.compile("-(-std$|-?std=.*)") + + # Mapping is needed, because, if a standard version not known by + # cppcheck is used, then it will assume the latest available version + # before cppcheck-2.15 or fail the analysis from cppcheck-2.15. + # https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html#index-std-1 + standard_mapping = { + "c90": "c89", + "c18": "c17", + "iso9899:2017": "c17", + "iso9899:2018": "c17", + "iso9899:1990": "c89", + "iso9899:199409": "c89", # Good enough + "c9x": "c99", + "iso9899:1999": "c99", + "iso9899:199x": "c99", + "c1x": "c11", + "iso9899:2011": "c11", + "c2x": "c23", + "iso9899:2024": "c23", + "c++98": "c++03", + "c++0x": "c++11", + "c++1y": "c++14", + "c++1z": "c++17", + "c++2a": "c++20", + "c++2b": "c++23", + "c++2c": "c++26" + } + for i, analyzer_option in enumerate(self.buildaction.analyzer_options): if interesting_option.match(analyzer_option): params.extend([analyzer_option]) @@ -161,6 +189,7 @@ def parse_analyzer_config(self): else: standard = self.buildaction.analyzer_options[i+1] standard = standard.lower().replace("gnu", "c") + standard = standard_mapping.get(standard, standard) params.extend(["--std=" + standard]) return params diff --git a/analyzer/tests/functional/analyze/test_analyze.py b/analyzer/tests/functional/analyze/test_analyze.py index 88e46ad47c..3b47fa7d93 100644 --- a/analyzer/tests/functional/analyze/test_analyze.py +++ b/analyzer/tests/functional/analyze/test_analyze.py @@ -1083,7 +1083,7 @@ def test_cppcheck_standard(self): stdout=subprocess.PIPE).stdout.decode() # Test correct handover. - self.assertTrue("--std=c99" in out) + self.assertIn("--std=c99", out) # Cppcheck does not support gnu variants of the standards, # These are transformed into their respective c and c++ @@ -1104,7 +1104,25 @@ def test_cppcheck_standard(self): stdout=subprocess.PIPE).stdout.decode() # Test if the standard is correctly transformed - self.assertTrue("--std=c99" in out) + self.assertIn("--std=c99", out) + + build_log = [{"directory": self.test_workspace, + "command": "gcc -c -std=iso9899:2017 " + source_file, + "file": source_file + }] + + with open(build_json, 'w', + encoding="utf-8", errors="ignore") as outfile: + json.dump(build_log, outfile) + + out = subprocess.run(analyze_cmd, + cwd=self.test_dir, + # env=self.env, + check=False, + stdout=subprocess.PIPE).stdout.decode() + + self.assertNotIn("iso9899:2017", out) + self.assertIn("--std=c17", out) def test_makefile_generation(self): """ Test makefile generation. """ From ed42e731f99fff62c1296a06b689e258e42053e9 Mon Sep 17 00:00:00 2001 From: bruntib Date: Thu, 10 Oct 2024 17:23:32 +0200 Subject: [PATCH 2/2] [fix] Arguments in --cppcheckargs are stronger When a C/C++ standard version is provided both in the original build command and the --cppcheckargs file, then only the one in --cppcheckargs should be used in the analyzer command. --- .../analyzers/cppcheck/analyzer.py | 13 +++++++++- .../tests/functional/analyze/test_analyze.py | 26 +++++++++++++++++++ codechecker_common/util.py | 11 ++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index ed44223560..e5d03251a2 100644 --- a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py @@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET from codechecker_common.logger import get_logger +from codechecker_common import util from codechecker_analyzer import analyzer_context, env from codechecker_analyzer.env import get_binary_in_path @@ -223,7 +224,17 @@ def construct_analyzer_cmd(self, result_handler): analyzer_cmd.extend(config.analyzer_extra_arguments) # Pass whitelisted parameters - analyzer_cmd.extend(self.parse_analyzer_config()) + params = self.parse_analyzer_config() + + def is_std(arg): + return arg.startswith("--std=") + + if util.index_of(config.analyzer_extra_arguments, is_std) >= 0: + std_idx = util.index_of(params, is_std) + if std_idx >= 0: + del params[std_idx] + + analyzer_cmd.extend(params) # TODO fix this in a follow up patch, because it is failing # the macos pypy test. diff --git a/analyzer/tests/functional/analyze/test_analyze.py b/analyzer/tests/functional/analyze/test_analyze.py index 3b47fa7d93..db89acb7bf 100644 --- a/analyzer/tests/functional/analyze/test_analyze.py +++ b/analyzer/tests/functional/analyze/test_analyze.py @@ -18,6 +18,7 @@ import shutil import subprocess import shlex +import tempfile import unittest import zipfile @@ -1124,6 +1125,31 @@ def test_cppcheck_standard(self): self.assertNotIn("iso9899:2017", out) self.assertIn("--std=c17", out) + # Test if standard version in --cppcheckargs is stronger. + with tempfile.NamedTemporaryFile(mode='w', + encoding='utf-8') as cppcheck_args: + with open(build_json, 'w', + encoding="utf-8", errors="ignore") as outfile: + build_log = [{ + "directory": self.test_workspace, + "command": "gcc -c -std=c++0x " + source_file, + "file": source_file}] + json.dump(build_log, outfile) + + cppcheck_args.write("--std=c++11") + cppcheck_args.close() + + analyze_cmd.extend(['--cppcheckargs', cppcheck_args.name]) + + out = subprocess.run(analyze_cmd, + cwd=self.test_dir, + # env=self.env, + check=False, + stdout=subprocess.PIPE).stdout.decode() + + self.assertIn("--std=c++11", out) + self.assertNotIn("--std=c++0x", out) + def test_makefile_generation(self): """ Test makefile generation. """ build_json = os.path.join(self.test_workspace, "build_extra_args.json") diff --git a/codechecker_common/util.py b/codechecker_common/util.py index e389b8d1a0..b71953c753 100644 --- a/codechecker_common/util.py +++ b/codechecker_common/util.py @@ -112,3 +112,14 @@ def path_for_fake_root(full_path: str, root_path: str = '/') -> str: def strtobool(value: str) -> bool: """Parse a string value to a boolean.""" return value.lower() in ('y', 'yes', 't', 'true', 'on', '1') + + +def index_of(iterable, lambda_func) -> int: + """Return the index of the first element in iterable for which + lambda_func returns True. + """ + for i, item in enumerate(iterable): + if lambda_func(item): + return i + + return -1