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..9f08480224 100644 --- a/analyzer/codechecker_analyzer/analyzers/analyzer_base.py +++ b/analyzer/codechecker_analyzer/analyzers/analyzer_base.py @@ -25,6 +25,15 @@ LOG = get_logger('analyzer') +def handle_analyzer_executable_from_config(analyzer_name, path): + context = analyzer_context.get_context() + if not os.path.isfile(path): + LOG.error(f"'{path}' is not a path to an analyzer binary " + f"given to --analyzer-config={analyzer_name}:executable!") + sys.exit(1) + context.analyzer_binaries[analyzer_name] = path + + class SourceAnalyzer(metaclass=ABCMeta): """ Base class for different source analyzers. @@ -56,6 +65,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..05a175042f 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py @@ -41,7 +41,8 @@ def parse_clang_help_page( command: List[str], - start_label: str + start_label: str, + starting_value: (str, str) = None ) -> List[str]: """ Parse the clang help page starting from a specific label. @@ -75,6 +76,8 @@ def parse_clang_help_page( re.compile(r"^\s{3,}(?P[^\n]+)$") res = [] + if starting_value: + res.append(starting_value) flag = None desc = [] @@ -171,16 +174,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)) @@ -305,7 +311,11 @@ def get_analyzer_config(cls) -> List[str]: command.append("-analyzer-config-help") - return parse_clang_help_page(command, 'OPTIONS:') + return parse_clang_help_page( + command, 'OPTIONS:', + ("executable", "Use the specified analyzer binary. This " + "supersedes any other method CodeChecker might use " + "to get hold of one.")) def post_analyze(self, result_handler): """ @@ -700,6 +710,11 @@ def construct_config_handler(cls, args): isinstance(args.analyzer_config, list): for cfg in args.analyzer_config: if cfg.analyzer == cls.ANALYZER_NAME: + if cfg.option == 'executable': + analyzer_base.handle_analyzer_executable_from_config( + cfg.analyzer, cfg.value) + LOG.info(f"Using clangsa binary '{cfg.value}'") + continue handler.checker_config.append(f"{cfg.option}={cfg.value}") return handler diff --git a/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py b/analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py index 6547a0184d..3d3530cfc4 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)) @@ -289,6 +301,11 @@ def get_analyzer_config(cls): """ Return the analyzer configuration with all checkers enabled. """ + + tidy_configs = [ + ("executable", "Use the specified analyzer binary. This " + "supersedes any other method CodeChecker might use " + "to get hold of one.")] try: result = subprocess.check_output( [cls.analyzer_binary(), "-dump-config", "-checks=*"], @@ -296,9 +313,10 @@ def get_analyzer_config(cls): universal_newlines=True, encoding="utf-8", errors="ignore") - return parse_analyzer_config(result) + tidy_configs.extend(parse_analyzer_config(result)) except (subprocess.CalledProcessError, OSError): - return [] + pass + return tidy_configs def get_checker_list(self, config) -> Tuple[List[str], List[str]]: """ @@ -583,6 +601,11 @@ def construct_config_handler(cls, args): isinstance(args.analyzer_config, list): for cfg in args.analyzer_config: if cfg.analyzer == cls.ANALYZER_NAME: + if cfg.option == 'executable': + analyzer_base.handle_analyzer_executable_from_config( + cfg.analyzer, cfg.value) + LOG.info(f"Using clang-tidy binary '{cfg.value}'") + continue analyzer_config[cfg.option] = cfg.value # Reports in headers are hidden by default in clang-tidy. Re-enable it diff --git a/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py b/analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py index 4d891142fc..6a33f9987a 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)) @@ -257,11 +260,13 @@ def get_analyzer_config(cls): """ Config options for cppcheck. """ - return [("addons", "A list of cppcheck addon files."), + return [("executable", "Use the specified analyzer binary. This " + "supersedes any other method CodeChecker might " + "use to get hold of one."), + ("addons", "A list of cppcheck addon files."), ("libraries", "A list of cppcheck library definiton files."), ("platform", "The platform configuration .xml file."), - ("inconclusive", "Enable inconclusive reports.") - ] + ("inconclusive", "Enable inconclusive reports.")] @classmethod def get_checker_config(cls): @@ -333,34 +338,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 " \ @@ -389,6 +377,11 @@ def construct_config_handler(cls, args): isinstance(args.analyzer_config, list): for cfg in args.analyzer_config: if cfg.analyzer == cls.ANALYZER_NAME: + if cfg.option == 'executable': + analyzer_base.handle_analyzer_executable_from_config( + cfg.analyzer, cfg.value) + LOG.info(f"Using cppcheck binary '{cfg.value}'") + continue analyzer_config[cfg.option].append(cfg.value) handler.analyzer_config = analyzer_config diff --git a/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py b/analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py index 9c5633ae05..05843dd5e5 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 @@ -132,7 +127,11 @@ def get_analyzer_config(cls): Config options for gcc. """ # TODO - return [] + gcc_configs = [ + ("executable", "Use the specified analyzer binary. This " + "supersedes any other method CodeChecker might use " + "to get hold of one.")] + return gcc_configs @classmethod def get_checker_config(cls): @@ -174,19 +173,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 +201,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}")