diff --git a/analyzer/codechecker_analyzer/analyzer.py b/analyzer/codechecker_analyzer/analyzer.py index b3030dbe53..ecda285de0 100644 --- a/analyzer/codechecker_analyzer/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzer.py @@ -226,7 +226,7 @@ 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_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..2919666fa5 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_base.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_base.py @@ -56,9 +56,18 @@ def resolve_missing_binary(cls, configured_binary, environ): """ raise NotImplementedError("Subclasses should implement this!") + @abstractmethod + def get_binary_version(self, 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]: + def is_binary_version_incompatible(cls, environ) -> Optional[str]: """ CodeChecker can only execute certain versions of analyzers. Returns a error object (an optional string). If the return value is diff --git a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py index 39323a167d..8882e38c9f 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_types.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_types.py @@ -186,8 +186,7 @@ def check_supported_analyzers(analyzers): # Check version compatibility of the analyzer binary. if analyzer_bin: analyzer = supported_analyzers[analyzer_name] - error = analyzer.is_binary_version_incompatible(analyzer_bin, - check_env) + error = analyzer.is_binary_version_incompatible(check_env) if error: failed_analyzers.add((analyzer_name, f"Incompatible version: {error}")) @@ -221,8 +220,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..02b32580fa 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py @@ -171,16 +171,18 @@ 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, environ, details=False) -> str: + if details: + version = [self.analyzer_binary(), '--version'] + else: + version = [self.analyzer_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)) @@ -578,7 +580,7 @@ def resolve_missing_binary(cls, configured_binary, environ): return clang @classmethod - def is_binary_version_incompatible(cls, configured_binary, environ): + def is_binary_version_incompatible(cls, environ): """ We support pretty much every ClangSA version. """ diff --git a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py index 6547a0184d..5d012750b5 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,17 @@ 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, environ, details=False) -> str: + version = [self.analyzer_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)) @@ -533,7 +544,7 @@ def resolve_missing_binary(cls, configured_binary, environ): return clangtidy @classmethod - def is_binary_version_incompatible(cls, configured_binary, environ): + def is_binary_version_incompatible(cls, environ): """ We support pretty much every Clang-Tidy version. """ diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index 4d891142fc..c8cfbc535f 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,18 @@ def analyzer_binary(cls): .analyzer_binaries[cls.ANALYZER_NAME] @classmethod - def get_version(cls, env=None): + def get_binary_version(self, environ, details=False) -> str: """ Get analyzer version information. """ - version = [cls.analyzer_binary(), '--version'] + version = [self.analyzer_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)) @@ -334,33 +336,15 @@ def resolve_missing_binary(cls, configured_binary, env): 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): + def is_binary_version_incompatible(cls, environ): """ Check the version compatibility of the given analyzer binary. """ - analyzer_version = \ - cls.__get_analyzer_version(configured_binary, environ) + analyzer_version = cls.get_binary_version(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..39c3c0ead3 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,20 @@ def resolve_missing_binary(cls, configured_binary, env): pass @classmethod - def __get_analyzer_version(cls, analyzer_binary, env): + def get_binary_version(self, environ, 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 = [self.analyzer_binary(), '--version'] + else: + version = [self.analyzer_binary(), '-dumpfullversion'] try: output = subprocess.check_output(version, - env=env, + env=environ, 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)) @@ -195,12 +191,11 @@ def __get_analyzer_version(cls, analyzer_binary, env): return None @classmethod - def is_binary_version_incompatible(cls, configured_binary, environ): + def is_binary_version_incompatible(cls, environ): """ Check the version compatibility of the given analyzer binary. """ - analyzer_version = \ - cls.__get_analyzer_version(configured_binary, environ) + analyzer_version = cls.get_binary_version(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..e677631852 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] + check_env = context.analyzer_env + version = analyzer_class.get_binary_version(check_env) + if not version: + version = 'ERROR' + + binary = context.analyzer_binaries.get(analyzer_name) + 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}")