From 7af389ab8904ba5e64ec2deebe4a505931f76110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Umann?= Date: Thu, 19 Oct 2023 14:42:17 +0200 Subject: [PATCH] [analyzers] Deprecate --all and --details for analyzers * Deprecate both `--all` and `--details`, because they were more confusing than useful * We list all supported (not available) analyzers by default, but print a warning message when its not available. * We used to query the version of each analyzer binary in a uniform way, but clang, cppcheck, gcc prints their version using different flags, and in different formats. I changed this to call the analyzer plugins' get_binary_version method (which I also implemented in this patch). --- analyzer/codechecker_analyzer/analyzer.py | 3 +- .../analyzers/analyzer_base.py | 11 +++ .../analyzers/analyzer_types.py | 3 +- .../analyzers/clangsa/analyzer.py | 13 +-- .../analyzers/clangtidy/analyzer.py | 22 +++-- .../analyzers/cppcheck/analyzer.py | 34 +++----- .../analyzers/gcc/analyzer.py | 19 ++-- .../codechecker_analyzer/cmd/analyzers.py | 87 ++++++++----------- 8 files changed, 91 insertions(+), 101 deletions(-) diff --git a/analyzer/codechecker_analyzer/analyzer.py b/analyzer/codechecker_analyzer/analyzer.py index b3030dbe53..74de35dd2d 100644 --- a/analyzer/codechecker_analyzer/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzer.py @@ -226,7 +226,8 @@ def perform_analysis(args, skip_handlers, actions, metadata_tool, # TODO: cppcheck may require a different environment than clang. version = analyzer_types.supported_analyzers[analyzer] \ - .get_version(context.analyzer_env) + .get_binary_version(context.analyzer_binaries[analyzer], + context.analyzer_env) 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 a95059a946..f53bb546e0 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_base.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_base.py @@ -56,6 +56,17 @@ def resolve_missing_binary(cls, configured_binary, environ): """ raise NotImplementedError("Subclasses should implement this!") + @abstractmethod + def get_binary_version(self, configured_binary, environ, details=False) \ + -> str: + """ + Return the version number of the binary that CodeChecker found, even + if its incompatible. If details is true, additional version information + is provided. If details is false, the return value should be + convertible to a distutils.version.StrictVersion type. + """ + raise NotImplementedError("Subclasses should implement this!") + @classmethod def is_binary_version_incompatible(cls, configured_binary, environ) \ -> Optional[str]: diff --git a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py index 39323a167d..30fa93ad5b 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py @@ -221,8 +221,7 @@ def construct_analyzer(buildaction, LOG.error('Unsupported analyzer type: %s', analyzer_type) return analyzer - except Exception as ex: - LOG.debug_analyzer(ex) + except Exception: # We should've detected well before this point that something is off # with the analyzer. We can't recover here. raise diff --git a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py index 1a483a095d..0ec0356600 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py @@ -171,16 +171,19 @@ def __add_plugin_load_flags(cls, analyzer_cmd: List[str]): analyzer_cmd.extend(["-load", plugin]) @classmethod - def get_version(cls, env=None): - """ Get analyzer version information. """ - version = [cls.analyzer_binary(), '--version'] + def get_binary_version(self, configured_binary, environ, details=False) \ + -> str: + if details: + version = [configured_binary, '--version'] + else: + version = [configured_binary, '-dumpversion'] try: output = subprocess.check_output(version, - env=env, + env=environ, universal_newlines=True, encoding="utf-8", errors="ignore") - return output + return output.strip() except (subprocess.CalledProcessError, OSError) as oerr: LOG.warning("Failed to get analyzer version: %s", ' '.join(version)) diff --git a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py index 6547a0184d..40c8d8929e 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py @@ -204,6 +204,16 @@ def need_asterisk(checker: str) -> bool: return result +def parse_version(tidy_output): + """ + Parse clang-tidy version output and return the version number. + """ + version_re = re.compile(r'.*version (?P[\d\.]+)', re.S) + match = version_re.match(tidy_output) + if match: + return match.group('version') + + class ClangTidy(analyzer_base.SourceAnalyzer): """ Constructs the clang tidy analyzer commands. @@ -220,16 +230,18 @@ def analyzer_binary(cls): .analyzer_binaries[cls.ANALYZER_NAME] @classmethod - def get_version(cls, env=None): - """ Get analyzer version information. """ - version = [cls.analyzer_binary(), '--version'] + def get_binary_version(self, configured_binary, environ, details=False) \ + -> str: + version = [configured_binary, '--version'] try: output = subprocess.check_output(version, - env=env, + env=environ, universal_newlines=True, encoding="utf-8", errors="ignore") - return output + if details: + return output.strip() + return parse_version(output) except (subprocess.CalledProcessError, OSError) as oerr: LOG.warning("Failed to get analyzer version: %s", ' '.join(version)) diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index 4d891142fc..338dbc62b4 100644 --- a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py @@ -67,7 +67,7 @@ def parse_version(cppcheck_output): version_re = re.compile(r'^Cppcheck (?P[\d\.]+)') match = version_re.match(cppcheck_output) if match: - return StrictVersion(match.group('version')) + return match.group('version') class Cppcheck(analyzer_base.SourceAnalyzer): @@ -83,16 +83,19 @@ def analyzer_binary(cls): .analyzer_binaries[cls.ANALYZER_NAME] @classmethod - def get_version(cls, env=None): + def get_binary_version(self, configured_binary, environ, details=False) \ + -> str: """ Get analyzer version information. """ - version = [cls.analyzer_binary(), '--version'] + version = [configured_binary, '--version'] try: output = subprocess.check_output(version, - env=env, + env=environ, universal_newlines=True, encoding="utf-8", errors="ignore") - return output + if details: + return output.strip() + return parse_version(output) except (subprocess.CalledProcessError, OSError) as oerr: LOG.warning("Failed to get analyzer version: %s", ' '.join(version)) @@ -333,34 +336,17 @@ def resolve_missing_binary(cls, configured_binary, env): LOG.debug("Using '%s' for Cppcheck!", cppcheck) return cppcheck - @classmethod - def __get_analyzer_version(cls, analyzer_binary, env): - """ - Return the analyzer 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 [] - @classmethod def is_binary_version_incompatible(cls, configured_binary, environ): """ Check the version compatibility of the given analyzer binary. """ analyzer_version = \ - cls.__get_analyzer_version(configured_binary, environ) + cls.get_binary_version(configured_binary, environ) # The analyzer version should be above 1.80 because '--plist-output' # argument was introduced in this release. - if analyzer_version >= StrictVersion("1.80"): + if StrictVersion(analyzer_version) >= StrictVersion("1.80"): return None return "CppCheck binary found is too old at " \ diff --git a/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py b/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py index 9c5633ae05..8a9fb8b63a 100644 --- a/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py @@ -42,11 +42,6 @@ def analyzer_binary(cls): return analyzer_context.get_context() \ .analyzer_binaries[cls.ANALYZER_NAME] - @classmethod - def get_version(cls, env=None): - """ Get analyzer version information. """ - return cls.__get_analyzer_version(cls.analyzer_binary(), env) - def add_checker_config(self, checker_cfg): # TODO pass @@ -174,19 +169,21 @@ def resolve_missing_binary(cls, configured_binary, env): pass @classmethod - def __get_analyzer_version(cls, analyzer_binary, env): + def get_binary_version(self, configured_binary, env, details=False) \ + -> str: """ Return the analyzer version. """ - # --version outputs a lot of garbage as well (like copyright info), - # this only contains the version info. - version = [analyzer_binary, '-dumpfullversion'] + if details: + version = [configured_binary, '--version'] + else: + version = [configured_binary, '-dumpfullversion'] try: output = subprocess.check_output(version, env=env, encoding="utf-8", errors="ignore") - return output + return output.strip() except (subprocess.CalledProcessError, OSError) as oerr: LOG.warning("Failed to get analyzer version: %s", ' '.join(version)) @@ -200,7 +197,7 @@ def is_binary_version_incompatible(cls, configured_binary, environ): Check the version compatibility of the given analyzer binary. """ analyzer_version = \ - cls.__get_analyzer_version(configured_binary, environ) + cls.get_binary_version(configured_binary, environ) # The analyzer version should be above 13.0.0 because the # '-fdiagnostics-format=sarif-file' argument was introduced in this diff --git a/analyzer/codechecker_analyzer/cmd/analyzers.py b/analyzer/codechecker_analyzer/cmd/analyzers.py index d599695895..861fd7b95c 100644 --- a/analyzer/codechecker_analyzer/cmd/analyzers.py +++ b/analyzer/codechecker_analyzer/cmd/analyzers.py @@ -13,7 +13,6 @@ import argparse import subprocess -import sys from codechecker_report_converter import twodim @@ -51,29 +50,24 @@ def add_arguments_to_parser(parser): Add the subcommand's arguments to the given argparse.ArgumentParser. """ - working_analyzers, _ = analyzer_types.check_supported_analyzers( - analyzer_types.supported_analyzers) - parser.add_argument('--all', dest="all", action='store_true', default=argparse.SUPPRESS, required=False, - help="Show all supported analyzers, not just the " - "available ones.") + help="DEPRECATED.") parser.add_argument('--details', dest="details", action='store_true', default=argparse.SUPPRESS, required=False, - help="Show details about the analyzers, not just " - "their names.") + help="DEPRECATED.") parser.add_argument('--dump-config', dest='dump_config', required=False, - choices=working_analyzers, + choices=analyzer_types.supported_analyzers, help="Dump the available checker options for the " "given analyzer to the standard output. " "Currently only clang-tidy supports this option. " @@ -89,7 +83,7 @@ def add_arguments_to_parser(parser): dest='analyzer_config', required=False, default=argparse.SUPPRESS, - choices=working_analyzers, + choices=analyzer_types.supported_analyzers, help="Show analyzer configuration options. These can " "be given to 'CodeChecker analyze " "--analyzer-config'.") @@ -118,7 +112,7 @@ def main(args): logger.setup_logger(args.verbose if 'verbose' in args else None, stream) context = analyzer_context.get_context() - working_analyzers, errored = \ + _, errored = \ analyzer_types.check_supported_analyzers( analyzer_types.supported_analyzers) @@ -158,63 +152,50 @@ def uglify(text): return text.lower().replace(' ', '_') if 'analyzer_config' in args: - if 'details' in args: - header = ['Option', 'Description'] - else: - header = ['Option'] + header = ['Option', 'Description'] if args.output_format in ['csv', 'json']: header = list(map(uglify, header)) - analyzer = args.analyzer_config - analyzer_class = analyzer_types.supported_analyzers[analyzer] + analyzer_name = args.analyzer_config + analyzer_class = analyzer_types.supported_analyzers[analyzer_name] configs = analyzer_class.get_analyzer_config() if not configs: - LOG.error("Failed to get analyzer configuration options for '%s' " - "analyzer! Please try to upgrade your analyzer version " - "to use this feature.", analyzer) - sys.exit(1) + LOG.warning("No analyzer configurations found for " + f"'{analyzer_name}'. If you suspsect this shouldn't " + "be the case, try to update your analyzer or check " + "whether CodeChecker found the intended binary.") - rows = [(':'.join((analyzer, c[0])), c[1]) if 'details' in args - else (':'.join((analyzer, c[0])),) for c in configs] + rows = [(':'.join((analyzer_name, c[0])), c[1]) for c in configs] print(twodim.to_str(args.output_format, header, rows)) + for err_analyzer_name, err_reason in errored: + if analyzer_name == err_analyzer_name: + LOG.warning( + f"Can't analyze with '{analyzer_name}': {err_reason}") + return - if 'details' in args: - header = ['Name', 'Path', 'Version'] - else: - header = ['Name'] + header = ['Name', 'Path', 'Version'] if args.output_format in ['csv', 'json']: header = list(map(uglify, header)) rows = [] - for analyzer in working_analyzers: - if 'details' not in args: - rows.append([analyzer]) - else: - binary = context.analyzer_binaries.get(analyzer) - try: - version = subprocess.check_output( - [binary, '--version'], encoding="utf-8", errors="ignore") - except (subprocess.CalledProcessError, OSError): - version = 'ERROR' - - rows.append([analyzer, - binary, - version]) - - if 'all' in args: - for analyzer, err_reason in errored: - if 'details' not in args: - rows.append([analyzer]) - else: - rows.append([analyzer, - context.analyzer_binaries.get(analyzer), - err_reason]) - - if rows: - print(twodim.to_str(args.output_format, header, rows)) + for analyzer_name in analyzer_types.supported_analyzers: + analyzer_class = analyzer_types.supported_analyzers[analyzer_name] + binary = context.analyzer_binaries.get(analyzer_name) + check_env = context.analyzer_env + version = analyzer_class.get_binary_version(binary, check_env) + if not version: + version = 'ERROR' + + rows.append([analyzer_name, binary, version]) + + assert rows + print(twodim.to_str(args.output_format, header, rows)) + + for analyzer_name, err_reason in errored: + LOG.warning(f"Can't analyze with '{analyzer_name}': {err_reason}")