From 4315b5a49c6e6900805ab5dbc7be953a7fdd254e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20Cserv=C3=A1k?= Date: Mon, 28 Aug 2023 15:31:50 +0200 Subject: [PATCH] Configurability of analyzer versions The modification can be used to validate the version of each analyzer. The user can specify all desired analyzers after the --analyzers flag, as was possible before. Also, the exact version number can be typed for each analyzer, separated by an '==' char. For example: 'CodeChecker analyze compile_commands.json -o reports --analyzers clangsa==14.0.0 cppcheck==2.7'. If the version number was not the same as in the current environment, the analyzer would be disabled. The user can enumerate the analyzers with or without versions, for example: 'CodeChecker analyze compile_commands.json -o reports --analyzers clangsa==14.0.0 cppcheck'. --- analyzer/codechecker_analyzer/analyzer.py | 6 +-- .../analyzers/analyzer_base.py | 27 +++++++++++ .../analyzers/analyzer_types.py | 25 ++++++++-- .../analyzers/clangsa/analyzer.py | 13 +++-- .../analyzers/clangsa/version.py | 5 ++ .../analyzers/clangtidy/analyzer.py | 27 +++++++++-- .../analyzers/cppcheck/analyzer.py | 47 ++++++++----------- analyzer/codechecker_analyzer/cmd/analyze.py | 1 - analyzer/codechecker_analyzer/cmd/check.py | 1 - docs/analyzer/user_guide.md | 6 ++- 10 files changed, 111 insertions(+), 47 deletions(-) diff --git a/analyzer/codechecker_analyzer/analyzer.py b/analyzer/codechecker_analyzer/analyzer.py index d9fb71009c..88687392f3 100644 --- a/analyzer/codechecker_analyzer/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzer.py @@ -131,7 +131,6 @@ def perform_analysis(args, skip_handlers, actions, metadata_tool, in the given analysis context for the supplied build actions. Additionally, insert statistical information into the metadata dict. """ - context = analyzer_context.get_context() ctu_reanalyze_on_failure = 'ctu_reanalyze_on_failure' in args and \ @@ -141,7 +140,6 @@ def perform_analysis(args, skip_handlers, actions, metadata_tool, else analyzer_types.supported_analyzers analyzers, errored = analyzer_types.check_supported_analyzers(analyzers) analyzer_types.check_available_analyzers(analyzers, errored) - ctu_collect = False ctu_analyze = False ctu_dir = '' @@ -229,9 +227,7 @@ def perform_analysis(args, skip_handlers, actions, metadata_tool, if state == CheckerState.enabled: enabled_checkers[analyzer].append(check) - # TODO: cppcheck may require a different environment than clang. - version = analyzer_types.supported_analyzers[analyzer] \ - .get_version(context.analyzer_env) + version = analyzer_types.supported_analyzers[analyzer].get_version() metadata_info['analyzer_statistics']['version'] = version metadata_tool['analyzers'][analyzer] = metadata_info diff --git a/analyzer/codechecker_analyzer/analyzers/analyzer_base.py b/analyzer/codechecker_analyzer/analyzers/analyzer_base.py index 639e6eb0b9..1ffb5404ae 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_base.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_base.py @@ -46,6 +46,24 @@ def config_handler(self): def construct_analyzer_cmd(self, result_handler): raise NotImplementedError("Subclasses should implement this!") + @classmethod + @abstractmethod + def analyzer_binary(cls): + """ + A subclass should have a analyzer_binary method + to return the bin of analyzer. + """ + pass + + @classmethod + @abstractmethod + def analyzer_env(cls): + """ + A subclass should have a analyzer_env method + to return the env for analyzer. + """ + pass + @classmethod def resolve_missing_binary(cls, configured_binary, environ): """ @@ -63,6 +81,15 @@ def version_compatible(cls, configured_binary, environ): """ raise NotImplementedError("Subclasses should implement this!") + @classmethod + @abstractmethod + def version_info(cls): + """ + A subclass should have a version_info method + to return with the analyzer's version number. + """ + pass + @classmethod def construct_config_handler(cls, args): """ Should return a subclass of AnalyzerConfigHandler.""" diff --git a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py index b1400b2258..15954c1647 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py @@ -25,6 +25,8 @@ from .clangsa.analyzer import ClangSA from .cppcheck.analyzer import Cppcheck +from distutils.version import StrictVersion + LOG = get_logger('analyzer') supported_analyzers = {ClangSA.ANALYZER_NAME: ClangSA, @@ -113,8 +115,9 @@ def print_unsupported_analyzers(errored): for analyzer_binary, reason in errored: LOG.warning("Analyzer '%s' is enabled but CodeChecker is failed to " "execute analysis with it: '%s'. Please check your " - "'PATH' environment variable and the " - "'config/package_layout.json' file!", + "'PATH' environment variable, the " + "'config/package_layout.json' file " + "and the --analyzers flag!", analyzer_binary, reason) @@ -148,8 +151,11 @@ def check_supported_analyzers(analyzers): enabled_analyzers = set() failed_analyzers = set() - for analyzer_name in analyzers: + analyzer_name, requested_version = analyzer_name.split('==', 1) \ + if len(analyzer_name.split('==', 1)) == 2 \ + else [analyzer_name, None] + if analyzer_name not in supported_analyzers: failed_analyzers.add((analyzer_name, "Analyzer unsupported by CodeChecker!")) @@ -184,7 +190,18 @@ def check_supported_analyzers(analyzers): # Check version compatibility of the analyzer binary. if analyzer_bin: analyzer = supported_analyzers[analyzer_name] - if not analyzer.version_compatible(analyzer_bin, check_env): + if requested_version: + bin_version = StrictVersion(str(analyzer.version_info())) + requested_version = StrictVersion(requested_version) + if requested_version != bin_version: + LOG.warning( + f"Given version: {requested_version}, found version " + f"for {analyzer_name} analyzer: {bin_version}" + ) + failed_analyzers.add((analyzer_name, + "Wrong version given.")) + available_analyzer = False + if not analyzer.version_compatible(): failed_analyzers.add((analyzer_name, "Incompatible version.")) available_analyzer = False diff --git a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py index 8fc231e9e8..4806d6118a 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py @@ -115,6 +115,13 @@ def analyzer_binary(cls): return analyzer_context.get_context() \ .analyzer_binaries[cls.ANALYZER_NAME] + @classmethod + def analyzer_env(cls): + """ + Return the env for analyzer. + """ + return analyzer_context.get_context().analyzer_env + @classmethod def analyzer_plugins(cls) -> List[str]: """ @@ -152,12 +159,12 @@ def __add_plugin_load_flags(cls, analyzer_cmd: List[str]): analyzer_cmd.extend(["-load", plugin]) @classmethod - def get_version(cls, env=None): + def get_version(cls): """ Get analyzer version information. """ version = [cls.analyzer_binary(), '--version'] try: output = subprocess.check_output(version, - env=env, + env=cls.analyzer_env(), universal_newlines=True, encoding="utf-8", errors="ignore") @@ -526,7 +533,7 @@ def resolve_missing_binary(cls, configured_binary, environ): return clang @classmethod - def version_compatible(cls, configured_binary, environ): + def version_compatible(cls): """ Check the version compatibility of the given analyzer binary. """ diff --git a/analyzer/codechecker_analyzer/analyzers/clangsa/version.py b/analyzer/codechecker_analyzer/analyzers/clangsa/version.py index 3749766b93..104fc74c50 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/version.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/version.py @@ -31,6 +31,11 @@ def __init__(self, self.installed_dir = str(installed_dir) self.vendor = str(vendor) + def __str__(self): + return f"{self.major_version}." \ + f"{self.minor_version}." \ + f"{self.patch_version}" + class ClangVersionInfoParser: """ diff --git a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py index 7bc5d92b95..007484ebe0 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py @@ -18,6 +18,8 @@ import subprocess from typing import List, Tuple +from distutils.version import StrictVersion + import yaml from codechecker_common.logger import get_logger @@ -178,12 +180,16 @@ def analyzer_binary(cls): .analyzer_binaries[cls.ANALYZER_NAME] @classmethod - def get_version(cls, env=None): + def analyzer_env(cls): + return analyzer_context.get_context().analyzer_env + + @classmethod + def get_version(cls): """ Get analyzer version information. """ version = [cls.analyzer_binary(), '--version'] try: output = subprocess.check_output(version, - env=env, + env=cls.analyzer_env(), universal_newlines=True, encoding="utf-8", errors="ignore") @@ -195,6 +201,21 @@ def get_version(cls, env=None): return None + @classmethod + def version_info(cls): + """ + Run the Clang-tidy version command + parse the output + and return the analyzer's version. + """ + version_output = cls.get_version() + version_re = re.compile(r'version\s+(?P[\d\.]+)') + match = version_re.search(version_output) + if match: + return StrictVersion(match.group('version')) + else: + return None + def add_checker_config(self, checker_cfg): LOG.error("Not implemented yet") @@ -466,7 +487,7 @@ def resolve_missing_binary(cls, configured_binary, environ): return clangtidy @classmethod - def version_compatible(cls, configured_binary, environ): + def version_compatible(cls): """ Check the version compatibility of the given analyzer binary. """ diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index 8d4559bb81..a027b0ff8d 100644 --- a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py @@ -59,16 +59,6 @@ def parse_checkers(cppcheck_output): return checkers -def parse_version(cppcheck_output): - """ - Parse cppcheck version output and return the version number. - """ - version_re = re.compile(r'^Cppcheck (?P[\d\.]+)') - match = version_re.match(cppcheck_output) - if match: - return StrictVersion(match.group('version')) - - class Cppcheck(analyzer_base.SourceAnalyzer): """ Constructs the Cppcheck analyzer commands. @@ -82,12 +72,16 @@ def analyzer_binary(cls): .analyzer_binaries[cls.ANALYZER_NAME] @classmethod - def get_version(cls, env=None): + def analyzer_env(cls): + return os.environ + + @classmethod + def get_version(cls): """ Get analyzer version information. """ version = [cls.analyzer_binary(), '--version'] try: output = subprocess.check_output(version, - env=env, + env=cls.analyzer_env(), universal_newlines=True, encoding="utf-8", errors="ignore") @@ -331,29 +325,26 @@ def resolve_missing_binary(cls, configured_binary, env): return cppcheck @classmethod - def __get_analyzer_version(cls, analyzer_binary, env): + def version_info(cls): """ - Return the analyzer version. + Run the Cppcheck version command + parse the output + and return the analyzer's version. """ - command = [analyzer_binary, "--version"] - - try: - result = subprocess.check_output( - command, - env=env, - encoding="utf-8", - errors="ignore") - return parse_version(result) - except (subprocess.CalledProcessError, OSError): - return [] + version_output = cls.get_version() + version_re = re.compile(r'^Cppcheck (?P[\d\.]+)') + match = version_re.match(version_output) + if match: + return StrictVersion(match.group('version')) + else: + return None @classmethod - def version_compatible(cls, configured_binary, environ): + def version_compatible(cls): """ Check the version compatibility of the given analyzer binary. """ - analyzer_version = \ - cls.__get_analyzer_version(configured_binary, environ) + analyzer_version = cls.version_info() # The analyzer version should be above 1.80 because '--plist-output' # argument was introduced in this release. diff --git a/analyzer/codechecker_analyzer/cmd/analyze.py b/analyzer/codechecker_analyzer/cmd/analyze.py index 5daf73858a..b2a6894ad4 100644 --- a/analyzer/codechecker_analyzer/cmd/analyze.py +++ b/analyzer/codechecker_analyzer/cmd/analyze.py @@ -347,7 +347,6 @@ def add_arguments_to_parser(parser): dest='analyzers', metavar='ANALYZER', required=False, - choices=analyzer_types.supported_analyzers, default=argparse.SUPPRESS, help="Run analysis only with the analyzers " "specified. Currently supported analyzers " diff --git a/analyzer/codechecker_analyzer/cmd/check.py b/analyzer/codechecker_analyzer/cmd/check.py index 4293715147..b03114a3a1 100644 --- a/analyzer/codechecker_analyzer/cmd/check.py +++ b/analyzer/codechecker_analyzer/cmd/check.py @@ -292,7 +292,6 @@ def add_arguments_to_parser(parser): dest='analyzers', metavar='ANALYZER', required=False, - choices=analyzer_types.supported_analyzers, default=argparse.SUPPRESS, help="Run analysis only with the analyzers " "specified. Currently supported analyzers " diff --git a/docs/analyzer/user_guide.md b/docs/analyzer/user_guide.md index 7878ffaefc..aa0991c86b 100644 --- a/docs/analyzer/user_guide.md +++ b/docs/analyzer/user_guide.md @@ -254,7 +254,8 @@ analyzer arguments: --analyzers ANALYZER [ANALYZER ...] Run analysis only with the analyzers specified. Currently supported analyzers are: clangsa, clang- - tidy. + tidy, cppcheck. Version number can be also specified + for analyzers to verify them. --capture-analysis-output Store standard output and standard error of successful analyzer invocations into the '/success' @@ -1098,7 +1099,8 @@ analyzer arguments: --analyzers ANALYZER [ANALYZER ...] Run analysis only with the analyzers specified. Currently supported analyzers are: clangsa, clang- - tidy. + tidy, cppcheck. Version number can be also specified + for analyzers to verify them. --capture-analysis-output Store standard output and standard error of successful analyzer invocations into the '/success'