From c1fd05305fd4aaf4fe8cf13bacc70bbd20fc1119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Tue, 22 Feb 2022 12:01:46 +0100 Subject: [PATCH 1/2] [analyzer] Allow --file and skipfile option to be given together The CodeChecker VSCodePlugin uses the `--file` parameter to analyze single files. Large projects load in their configuration using the `--config` parameter and if there is a `-i skipfile` given in the config, `CodeChecker analyze` call drops an error. This patch will allow `-i skipfile` and `--file` to be given together. --- .../codechecker_analyzer/analysis_manager.py | 34 ++-- analyzer/codechecker_analyzer/analyzer.py | 12 +- .../analyzers/clangsa/result_handler.py | 6 +- .../analyzers/clangtidy/result_handler.py | 6 +- .../analyzers/result_handler_base.py | 4 +- .../buildlog/log_parser.py | 19 +- analyzer/codechecker_analyzer/cmd/analyze.py | 45 +++-- analyzer/codechecker_analyzer/cmd/parse.py | 20 +- analyzer/codechecker_analyzer/makefile.py | 6 +- .../pre_analysis_manager.py | 8 +- analyzer/tests/functional/skip/test_skip.py | 176 ++++++++++++++++-- analyzer/tests/unit/test_log_parser.py | 43 ++--- .../unit/test_remove_report_from_plist.py | 25 +-- codechecker_common/skiplist_handler.py | 17 ++ .../report/__init__.py | 8 +- .../report/reports.py | 6 +- 16 files changed, 292 insertions(+), 143 deletions(-) diff --git a/analyzer/codechecker_analyzer/analysis_manager.py b/analyzer/codechecker_analyzer/analysis_manager.py index 40dbb26812..8f02162122 100644 --- a/analyzer/codechecker_analyzer/analysis_manager.py +++ b/analyzer/codechecker_analyzer/analysis_manager.py @@ -169,7 +169,7 @@ def is_ctu_active(source_analyzer): def prepare_check(action, analyzer_config, output_dir, checker_labels, - skip_handler, statistics_data, disable_ctu=False): + skip_handlers, statistics_data, disable_ctu=False): """ Construct the source analyzer and result handler. """ # Create a source analyzer. source_analyzer = \ @@ -209,7 +209,7 @@ def prepare_check(action, analyzer_config, output_dir, checker_labels, rh = source_analyzer.construct_result_handler(action, output_dir, checker_labels, - skip_handler) + skip_handlers) # NOTICE! # The currently analyzed source file needs to be set before the @@ -220,7 +220,7 @@ def prepare_check(action, analyzer_config, output_dir, checker_labels, return source_analyzer, rh -def handle_success(rh, result_file, result_base, skip_handler, +def handle_success(rh, result_file, result_base, skip_handlers, capture_analysis_output, success_dir): """ Result postprocessing is required if the analysis was @@ -232,7 +232,7 @@ def handle_success(rh, result_file, result_base, skip_handler, save_output(os.path.join(success_dir, result_base), rh.analyzer_stdout, rh.analyzer_stderr) - rh.postprocess_result(skip_handler) + rh.postprocess_result(skip_handlers) # Generated reports will be handled separately at store. @@ -319,7 +319,7 @@ def handle_reproducer(source_analyzer, rh, zip_file, actions_map): def handle_failure( - source_analyzer, rh, zip_file, result_base, actions_map, skip_handler + source_analyzer, rh, zip_file, result_base, actions_map, skip_handlers ): """ If the analysis fails a debug zip is packed together which contains @@ -334,7 +334,7 @@ def handle_failure( checks = source_analyzer.config_handler.checks() state = checks.get('clang-diagnostic-error', (CheckerState.default, ''))[0] if state != CheckerState.disabled: - rh.postprocess_result(skip_handler) + rh.postprocess_result(skip_handlers) # Remove files that successfully analyzed earlier on. plist_file = result_base + ".plist" @@ -488,7 +488,7 @@ def check(check_data): skiplist handler is None if no skip file was configured. """ actions_map, action, context, analyzer_config, \ - output_dir, skip_handler, quiet_output_on_stdout, \ + output_dir, skip_handlers, quiet_output_on_stdout, \ capture_analysis_output, generate_reproducer, analysis_timeout, \ analyzer_environment, ctu_reanalyze_on_failure, \ output_dirs, statistics_data = check_data @@ -509,7 +509,7 @@ def check(check_data): source_analyzer, rh = prepare_check(action, analyzer_config, output_dir, context.checker_labels, - skip_handler, statistics_data) + skip_handlers, statistics_data) reanalyzed = os.path.exists(rh.analyzer_result_file) @@ -605,12 +605,12 @@ def handle_analysis_result(success, zip_file=zip_file): if success: handle_success(rh, result_file, result_base, - skip_handler, capture_analysis_output, + skip_handlers, capture_analysis_output, success_dir) elif not generate_reproducer: handle_failure(source_analyzer, rh, os.path.join(failed_dir, zip_file), - result_base, actions_map, skip_handler) + result_base, actions_map, skip_handlers) if rh.analyzer_returncode == 0: handle_analysis_result(success=True) @@ -639,7 +639,7 @@ def handle_analysis_result(success, zip_file=zip_file): source_analyzer, rh = \ prepare_check(action, analyzer_config, output_dir, context.checker_labels, - skip_handler, statistics_data, + skip_handlers, statistics_data, True) reanalyzed = os.path.exists(rh.analyzer_result_file) @@ -698,20 +698,20 @@ def handle_analysis_result(success, zip_file=zip_file): action.source -def skip_cpp(compile_actions, skip_handler): +def skip_cpp(compile_actions, skip_handlers): """If there is no skiplist handler there was no skip list file in the command line. C++ file skipping is handled here. """ - if not skip_handler: + if not skip_handlers: return compile_actions, [] analyze = [] skip = [] for compile_action in compile_actions: - if skip_handler and skip_handler.should_skip(compile_action.source): + if skip_handlers and skip_handlers.should_skip(compile_action.source): skip.append(compile_action) else: analyze.append(compile_action) @@ -720,7 +720,7 @@ def skip_cpp(compile_actions, skip_handler): def start_workers(actions_map, actions, context, analyzer_config_map, - jobs, output_path, skip_handler, metadata_tool, + jobs, output_path, skip_handlers, metadata_tool, quiet_analyze, capture_analysis_output, generate_reproducer, timeout, ctu_reanalyze_on_failure, statistics_data, manager, compile_cmd_count): @@ -738,7 +738,7 @@ def signal_handler(signum, frame): sys.exit(128 + signum) signal.signal(signal.SIGINT, signal_handler) - actions, skipped_actions = skip_cpp(actions, skip_handler) + actions, skipped_actions = skip_cpp(actions, skip_handlers) # Start checking parallel. checked_var = multiprocessing.Value('i', 1) actions_num = multiprocessing.Value('i', len(actions)) @@ -781,7 +781,7 @@ def signal_handler(signum, frame): context, analyzer_config_map.get(build_action.analyzer_type), output_path, - skip_handler, + skip_handlers, quiet_analyze, capture_analysis_output, generate_reproducer, diff --git a/analyzer/codechecker_analyzer/analyzer.py b/analyzer/codechecker_analyzer/analyzer.py index 767938e2eb..eeac095295 100644 --- a/analyzer/codechecker_analyzer/analyzer.py +++ b/analyzer/codechecker_analyzer/analyzer.py @@ -159,7 +159,7 @@ def __get_ctu_data(config_map, ctu_dir): 'ctu_temp_fnmap_folder': 'tmpExternalFnMaps'} -def perform_analysis(args, skip_handler, context, actions, metadata_tool, +def perform_analysis(args, skip_handlers, context, actions, metadata_tool, compile_cmd_count): """ Perform static analysis via the given (or if not, all) analyzers, @@ -280,7 +280,7 @@ def perform_analysis(args, skip_handler, context, actions, metadata_tool, ctu_data = __get_ctu_data(config_map, ctu_dir) makefile_creator = MakeFileCreator(analyzers, args.output_path, - config_map, context, skip_handler, + config_map, context, skip_handlers, ctu_collect, statistics_data, ctu_data) makefile_creator.create(actions) @@ -314,13 +314,13 @@ def perform_analysis(args, skip_handler, context, actions, metadata_tool, pre_analyze = [a for a in actions if a.analyzer_type == ClangSA.ANALYZER_NAME] - pre_anal_skip_handler = None + pre_anal_skip_handlers = None # Skip list is applied only in pre-analysis # if --ctu-collect or --stats-collect was called explicitly if ((ctu_collect and not ctu_analyze) or ("stats_output" in args and args.stats_output)): - pre_anal_skip_handler = skip_handler + pre_anal_skip_handlers = skip_handlers clangsa_config = config_map.get(ClangSA.ANALYZER_NAME) @@ -329,7 +329,7 @@ def perform_analysis(args, skip_handler, context, actions, metadata_tool, context, clangsa_config, args.jobs, - pre_anal_skip_handler, + pre_anal_skip_handlers, ctu_data, statistics_data, manager) @@ -349,7 +349,7 @@ def perform_analysis(args, skip_handler, context, actions, metadata_tool, analysis_manager.start_workers(actions_map, actions, context, config_map, args.jobs, args.output_path, - skip_handler, + skip_handlers, metadata_tool, 'quiet' in args, 'capture_analysis_output' in args, diff --git a/analyzer/codechecker_analyzer/analyzers/clangsa/result_handler.py b/analyzer/codechecker_analyzer/analyzers/clangsa/result_handler.py index 0b232c5e33..fc0ca5e0c2 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangsa/result_handler.py +++ b/analyzer/codechecker_analyzer/analyzers/clangsa/result_handler.py @@ -17,7 +17,7 @@ from codechecker_report_converter.report import report_file from codechecker_report_converter.report.hash import get_report_hash, HashType from codechecker_common.logger import get_logger -from codechecker_common.skiplist_handler import SkipListHandler +from codechecker_common.skiplist_handler import SkipListHandlers from ..result_handler_base import ResultHandler @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs): super(ClangSAResultHandler, self).__init__(*args, **kwargs) - def postprocess_result(self, skip_handler: Optional[SkipListHandler]): + def postprocess_result(self, skip_handlers: Optional[SkipListHandlers]): """ Generate analyzer result output file which can be parsed and stored into the database. @@ -43,7 +43,7 @@ def postprocess_result(self, skip_handler: Optional[SkipListHandler]): reports = report_file.get_reports( self.analyzer_result_file, self.checker_labels, source_dir_path=self.source_dir_path) - reports = [r for r in reports if not r.skip(skip_handler)] + reports = [r for r in reports if not r.skip(skip_handlers)] hash_type = None if self.report_hash_type in ['context-free', 'context-free-v2']: diff --git a/analyzer/codechecker_analyzer/analyzers/clangtidy/result_handler.py b/analyzer/codechecker_analyzer/analyzers/clangtidy/result_handler.py index 03b5a34b56..0c9e097a88 100644 --- a/analyzer/codechecker_analyzer/analyzers/clangtidy/result_handler.py +++ b/analyzer/codechecker_analyzer/analyzers/clangtidy/result_handler.py @@ -19,7 +19,7 @@ from codechecker_report_converter.report.hash import get_report_hash, HashType from codechecker_common.logger import get_logger -from codechecker_common.skiplist_handler import SkipListHandler +from codechecker_common.skiplist_handler import SkipListHandlers from ..result_handler_base import ResultHandler @@ -36,7 +36,7 @@ def __init__(self, *args, **kwargs): super(ClangTidyResultHandler, self).__init__(*args, **kwargs) - def postprocess_result(self, skip_handler: Optional[SkipListHandler]): + def postprocess_result(self, skip_handlers: Optional[SkipListHandlers]): """ Generate analyzer result output file which can be parsed and stored into the database. @@ -45,7 +45,7 @@ def postprocess_result(self, skip_handler: Optional[SkipListHandler]): tidy_stdout = self.analyzer_stdout.splitlines() reports = Parser().get_reports_from_iter(tidy_stdout) - reports = [r for r in reports if not r.skip(skip_handler)] + reports = [r for r in reports if not r.skip(skip_handlers)] # In the earlier versions of CodeChecker Clang Tidy never used context # free hash even if we enabled it with '--report-hash context-free' diff --git a/analyzer/codechecker_analyzer/analyzers/result_handler_base.py b/analyzer/codechecker_analyzer/analyzers/result_handler_base.py index 4ae2b0014e..c0447238d5 100644 --- a/analyzer/codechecker_analyzer/analyzers/result_handler_base.py +++ b/analyzer/codechecker_analyzer/analyzers/result_handler_base.py @@ -17,7 +17,7 @@ from typing import Optional from codechecker_common.logger import get_logger -from codechecker_common.skiplist_handler import SkipListHandler +from codechecker_common.skiplist_handler import SkipListHandlers LOG = get_logger('analyzer') @@ -176,7 +176,7 @@ def clean_results(self): # There might be no result file if analysis failed. LOG.debug(oserr) - def postprocess_result(self, skip_handler: Optional[SkipListHandler]): + def postprocess_result(self, skip_handlers: Optional[SkipListHandlers]): """ Postprocess result if needed. Should be called after the analyses finished. diff --git a/analyzer/codechecker_analyzer/buildlog/log_parser.py b/analyzer/codechecker_analyzer/buildlog/log_parser.py index fc243e14ea..756b2c13cf 100644 --- a/analyzer/codechecker_analyzer/buildlog/log_parser.py +++ b/analyzer/codechecker_analyzer/buildlog/log_parser.py @@ -1193,8 +1193,8 @@ def parse_unique_log(compilation_database, compiler_info_file=None, keep_gcc_include_fixed=False, keep_gcc_intrin=False, - analysis_skip_handler=None, - pre_analysis_skip_handler=None, + analysis_skip_handlers=None, + pre_analysis_skip_handlers=None, ctu_or_stats_enabled=False, env=None, analyzer_clang_version=None): @@ -1240,10 +1240,10 @@ def parse_unique_log(compilation_database, pre analysis step nothing should be skipped to collect the required information for the analysis step where not all the files are analyzed. - analysis_skip_handler -- skip handler for files which should be skipped + analysis_skip_handlers -- skip handlers for files which should be skipped during analysis - pre_analysis_skip_handler -- skip handler for files wich should be skipped - during pre analysis + pre_analysis_skip_handlers -- skip handlers for files wich should be + skipped during pre analysis ctu_or_stats_enabled -- ctu or statistics based analysis was enabled influences the behavior which files are skipped. env -- Is the environment where a subprocess call should be executed. @@ -1274,10 +1274,11 @@ def parse_unique_log(compilation_database, # at both analysis phases (pre analysis and analysis). # Skipping of the compile commands is done differently if no # CTU or statistics related feature was enabled. - if analysis_skip_handler \ - and analysis_skip_handler.should_skip(entry['file']) \ - and (not ctu_or_stats_enabled or pre_analysis_skip_handler - and pre_analysis_skip_handler.should_skip(entry['file'])): + if analysis_skip_handlers \ + and analysis_skip_handlers.should_skip(entry['file']) \ + and (not ctu_or_stats_enabled or pre_analysis_skip_handlers + and pre_analysis_skip_handlers.should_skip( + entry['file'])): skipped_cmp_cmd_count += 1 continue diff --git a/analyzer/codechecker_analyzer/cmd/analyze.py b/analyzer/codechecker_analyzer/cmd/analyze.py index 4802b7fe93..5f8ca9559f 100644 --- a/analyzer/codechecker_analyzer/cmd/analyze.py +++ b/analyzer/codechecker_analyzer/cmd/analyze.py @@ -26,7 +26,9 @@ from codechecker_analyzer.arg import OrderedCheckersAction from codechecker_analyzer.buildlog import log_parser -from codechecker_common import arg, logger, skiplist_handler, cmd_config +from codechecker_common import arg, logger, cmd_config +from codechecker_common.skiplist_handler import SkipListHandler, \ + SkipListHandlers LOG = logger.get_logger('system') @@ -159,7 +161,7 @@ def add_arguments_to_parser(parser): "threads mean faster analysis at the cost of " "using more memory.") - skip_mode = parser.add_mutually_exclusive_group() + skip_mode = parser.add_argument_group("file filter arguments") skip_mode.add_argument('-i', '--ignore', '--skip', dest="skipfile", required=False, @@ -723,29 +725,24 @@ def add_arguments_to_parser(parser): func=main, func_process_config_file=cmd_config.process_config_file) -def __get_skip_handler(args) -> skiplist_handler.SkipListHandler: +def __get_skip_handlers(args) -> SkipListHandlers: """ - Initialize and return a skiplist handler if + Initialize and return a list of skiplist handlers if there is a skip list file in the arguments or files options is provided. """ - skip_file_content = "" - + skip_handlers = SkipListHandlers() if 'files' in args: # Creates a skip file where all source files will be skipped except # the given source files and all the header files. skip_files = ['+{0}'.format(f) for f in args.files] skip_files.extend(['+/*.h', '+/*.H', '+/*.tcc']) skip_files.append('-*') + skip_handlers.append(SkipListHandler("\n".join(skip_files))) + if 'skipfile' in args: + with open(args.skipfile, encoding="utf-8", errors="ignore") as f: + skip_handlers.append(SkipListHandler(f.read())) - skip_file_content = "\n".join(skip_files) - elif 'skipfile' in args: - with open(args.skipfile, - encoding="utf-8", errors="ignore") as skip_file: - skip_file_content = skip_file.read() - - if skip_file_content: - LOG.debug_analyzer("Creating skiplist handler.") - return skiplist_handler.SkipListHandler(skip_file_content) + return skip_handlers def __update_skip_file(args): @@ -858,9 +855,6 @@ def main(args): LOG.error("Checker option in wrong format: %s", config) sys.exit(1) - # Process the skip list if present. - skip_handler = __get_skip_handler(args) - # Enable alpha uniqueing by default if ctu analysis is used. if 'none' in args.compile_uniqueing and 'ctu_phases' in args: args.compile_uniqueing = "alpha" @@ -874,15 +868,18 @@ def main(args): sys.exit(1) compiler_info_file = args.compiler_info_file + # Process the skip list if present. + skip_handlers = __get_skip_handlers(args) + ctu_or_stats_enabled = False # Skip list is applied only in pre-analysis # if --ctu-collect was called explicitly. - pre_analysis_skip_handler = None + pre_analysis_skip_handlers = None if 'ctu_phases' in args: ctu_collect = args.ctu_phases[0] ctu_analyze = args.ctu_phases[1] if ctu_collect and not ctu_analyze: - pre_analysis_skip_handler = skip_handler + pre_analysis_skip_handlers = skip_handlers if ctu_collect or ctu_analyze: ctu_or_stats_enabled = True @@ -890,7 +887,7 @@ def main(args): # Skip list is applied only in pre-analysis # if --stats-collect was called explicitly. if 'stats_output' in args and args.stats_output: - pre_analysis_skip_handler = skip_handler + pre_analysis_skip_handlers = skip_handlers ctu_or_stats_enabled = True if 'stats_enabled' in args and args.stats_enabled: @@ -948,8 +945,8 @@ def main(args): compiler_info_file, args.keep_gcc_include_fixed, args.keep_gcc_intrin, - skip_handler, - pre_analysis_skip_handler, + skip_handlers, + pre_analysis_skip_handlers, ctu_or_stats_enabled, analyzer_env, analyzer_clang_version) @@ -1018,7 +1015,7 @@ def main(args): LOG.debug_analyzer("Compile commands forwarded for analysis: %d", compile_cmd_count.analyze) - analyzer.perform_analysis(args, skip_handler, context, actions, + analyzer.perform_analysis(args, skip_handlers, context, actions, metadata_tool, compile_cmd_count) diff --git a/analyzer/codechecker_analyzer/cmd/parse.py b/analyzer/codechecker_analyzer/cmd/parse.py index 9e7ecdbddf..2108d4d157 100644 --- a/analyzer/codechecker_analyzer/cmd/parse.py +++ b/analyzer/codechecker_analyzer/cmd/parse.py @@ -32,7 +32,8 @@ from codechecker_analyzer import analyzer_context, suppress_handler from codechecker_common import arg, logger, cmd_config -from codechecker_common.skiplist_handler import SkipListHandler +from codechecker_common.skiplist_handler import SkipListHandler, \ + SkipListHandlers LOG = logger.get_logger('system') @@ -202,7 +203,6 @@ def add_arguments_to_parser(parser): ', '.join(REVIEW_STATUS_VALUES))) group = parser.add_argument_group("file filter arguments") - group = group.add_mutually_exclusive_group() group.add_argument('-i', '--ignore', '--skip', dest="skipfile", @@ -359,18 +359,14 @@ def get_output_file_path(default_file_name: str) -> Optional[str]: if output_dir_path: return os.path.join(output_dir_path, default_file_name) - skip_file_content = "" + skip_handlers = SkipListHandlers() if 'files' in args: items = [f"+{file_path}" for file_path in args.files] items.append("-*") - - skip_file_content = "\n".join(items) - elif 'skipfile' in args: - with open(args.skipfile, 'r', - encoding='utf-8', errors='ignore') as skip_file: - skip_file_content = skip_file.read() - - skip_handler = SkipListHandler(skip_file_content) + skip_handlers.append(SkipListHandler("\n".join(items))) + if 'skipfile' in args: + with open(args.skipfile, 'r', encoding='utf-8', errors='ignore') as f: + skip_handlers.append(SkipListHandler(f.read())) trim_path_prefixes = args.trim_path_prefix if \ 'trim_path_prefix' in args else None @@ -396,7 +392,7 @@ def get_output_file_path(default_file_name: str) -> Optional[str]: file_path, context.checker_labels, file_cache) reports = reports_helper.skip( - reports, processed_path_hashes, skip_handler, suppr_handler, + reports, processed_path_hashes, skip_handlers, suppr_handler, src_comment_status_filter) statistics.num_of_analyzer_result_files += 1 diff --git a/analyzer/codechecker_analyzer/makefile.py b/analyzer/codechecker_analyzer/makefile.py index cf64330d77..ab41a591ed 100644 --- a/analyzer/codechecker_analyzer/makefile.py +++ b/analyzer/codechecker_analyzer/makefile.py @@ -33,12 +33,12 @@ class MakeFileCreator: """ Creates a Makefile from analyzer actions. """ def __init__(self, analyzers, output_path, config_map, context, - skip_handler, pre_analysis, statistics_data, ctu_data): + skip_handlers, pre_analysis, statistics_data, ctu_data): self.__analyzers = analyzers self.__output_path = output_path self.__config_map = config_map self.__context = context - self.__skip_handler = skip_handler + self.__skip_handlers = skip_handlers self.__pre_analysis = pre_analysis self.__log_info = "[`date +'%Y-%m-%d %H:%M:%S'`] -" @@ -235,7 +235,7 @@ def __write_analysis_targets(self, mfile, action, post_pre_all_target): source_analyzer, rh = analysis_manager.prepare_check( action, self.__config_map.get(action.analyzer_type), self.__output_path, self.__context.checker_labels, - self.__skip_handler, self.__statistics_data) + self.__skip_handlers, self.__statistics_data) if self.__statistics_data and post_pre_all_target: stats_cfg = SpecialReturnValueCollector.checker_analyze_cfg( diff --git a/analyzer/codechecker_analyzer/pre_analysis_manager.py b/analyzer/codechecker_analyzer/pre_analysis_manager.py index c324fa8395..0c38c721c1 100644 --- a/analyzer/codechecker_analyzer/pre_analysis_manager.py +++ b/analyzer/codechecker_analyzer/pre_analysis_manager.py @@ -83,7 +83,7 @@ def init_worker(checked_num, action_num): def pre_analyze(params): - action, context, clangsa_config, skip_handler, \ + action, context, clangsa_config, skip_handlers, \ ctu_data, statistics_data = params analyzer_environment = env.extend(context.path_env_extra, @@ -91,7 +91,7 @@ def pre_analyze(params): progress_checked_num.value += 1 - if skip_handler and skip_handler.should_skip(action.source): + if skip_handlers and skip_handlers.should_skip(action.source): return if action.analyzer_type != ClangSA.ANALYZER_NAME: return @@ -154,7 +154,7 @@ def pre_analyze(params): def run_pre_analysis(actions, context, clangsa_config, - jobs, skip_handler, ctu_data, statistics_data, manager): + jobs, skip_handlers, ctu_data, statistics_data, manager): """ Run multiple pre analysis jobs before the actual analysis. """ @@ -196,7 +196,7 @@ def signal_handler(signum, frame): collect_actions = [(build_action, context, clangsa_config, - skip_handler, + skip_handlers, ctu_data, statistics_data) for build_action in actions] diff --git a/analyzer/tests/functional/skip/test_skip.py b/analyzer/tests/functional/skip/test_skip.py index daa5d8ed3d..40a797f977 100644 --- a/analyzer/tests/functional/skip/test_skip.py +++ b/analyzer/tests/functional/skip/test_skip.py @@ -12,10 +12,12 @@ of skipped reports from the report files. """ +import glob import json import os import plistlib import subprocess +import tempfile import unittest from libtest import env @@ -38,8 +40,8 @@ def setUp(self): self.report_dir = os.path.join(self.test_workspace, "reports") self.test_dir = os.path.join(os.path.dirname(__file__), 'test_files') - def test_skip(self): - """Analyze a project with a skip file.""" + def __log_and_analyze_simple(self, analyzer_extra_options=None): + """ Log and analyze the 'simple' project. """ test_dir = os.path.join(self.test_dir, "simple") build_json = os.path.join(self.test_workspace, "build.json") @@ -57,9 +59,12 @@ def test_skip(self): encoding="utf-8", errors="ignore") print(out) # Create and run analyze command. - analyze_cmd = [self._codechecker_cmd, "analyze", "-c", build_json, - "--analyzers", "clangsa", "--verbose", "debug", - "--ignore", "skipfile", "-o", self.report_dir] + analyze_cmd = [ + self._codechecker_cmd, "analyze", "-c", build_json, + "--analyzers", "clangsa", "-o", self.report_dir] + + if analyzer_extra_options: + analyze_cmd.extend(analyzer_extra_options) process = subprocess.Popen( analyze_cmd, @@ -75,6 +80,29 @@ def test_skip(self): errcode = process.returncode self.assertEqual(errcode, 0) + def __run_parse(self, extra_options=None): + """ Run parse command with the given extra options. """ + cmd = [ + self._codechecker_cmd, "parse", self.report_dir, + "--export", "json"] + + if extra_options: + cmd.extend(extra_options) + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + errors="ignore") + out, err = process.communicate() + + return out, err, process.returncode + + def test_skip(self): + """Analyze a project with a skip file.""" + self.__log_and_analyze_simple(["--ignore", "skipfile"]) + # Check if file is skipped. report_dir_files = os.listdir(self.report_dir) for f in report_dir_files: @@ -176,22 +204,130 @@ def test_analyze_only_header(self): for f in report_dir_files])) # Get reports only from the header file. - cmd = [ - self._codechecker_cmd, "parse", self.report_dir, - "--file", "*/lib.h", - "--export", "json"] + out, _, _ = self.__run_parse(["--file", "*/lib.h"]) - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=test_dir, - encoding="utf-8", - errors="ignore") - out, err = process.communicate() - - errcode = process.returncode - self.assertEqual(errcode, 2) + data = json.loads(out) + self.assertTrue(len(data['reports'])) + def test_analyze_skip_everything(self): + """ + Test analyze command when everything is skipped by a skipfile. + """ + with tempfile.NamedTemporaryFile( + mode='w', + suffix="skipfile", + encoding='utf-8') as skip_file: + # Skip everything by the skip file. + skip_file.write('-*') + skip_file.flush() + + self.__log_and_analyze_simple([ + "--ignore", skip_file.name]) + self.assertFalse( + glob.glob(os.path.join(self.report_dir, '*.plist'))) + + def test_analyze_file_option_skip_everything(self): + """ + Test analyze command --file option when everything is skipped by a + skipfile. + """ + with tempfile.NamedTemporaryFile( + mode='w', + suffix="skipfile", + encoding='utf-8') as skip_file: + # Skip everything by the skip file. + skip_file.write('-*') + skip_file.flush() + + self.__log_and_analyze_simple([ + "--ignore", skip_file.name, + "--file", "*/file_to_be_skipped.cpp"]) + self.assertFalse( + glob.glob(os.path.join(self.report_dir, '*.plist'))) + + def test_analyze_file_option(self): + """ + Test analyze command --file option when everything is skipped except + a single file. + """ + with tempfile.NamedTemporaryFile( + mode='w', + suffix="skipfile", + encoding='utf-8') as skip_file: + # Skip everything except a single source file which is marked as + # included in the skip file and will be filtered by the --file + # option. + skip_file.write('\n'.join([ + '+*skip_header.cpp', + '-*' + ])) + skip_file.flush() + + self.__log_and_analyze_simple([ + "--ignore", skip_file.name, + "--file", "*/skip_header.cpp"]) + print(glob.glob( + os.path.join(self.report_dir, '*.plist'))) + self.assertFalse( + any('skip_header.cpp' not in f for f in glob.glob( + os.path.join(self.report_dir, '*.plist')))) + + # Skip every source files except a single one which will be + # filtered by the --file option. + skip_file.write('\n'.join([ + '-*file_to_be_skipped.cpp', + '-*skip.h' + ])) + skip_file.flush() + + self.__log_and_analyze_simple([ + "--ignore", skip_file.name, + "--file", "*/skip_header.cpp"]) + print(glob.glob( + os.path.join(self.report_dir, '*.plist'))) + self.assertFalse( + any('skip_header.cpp' not in f for f in glob.glob( + os.path.join(self.report_dir, '*.plist')))) + + def test_analyze_only_file_option(self): + """ + Test analyze command --file option without a skip file. + """ + self.__log_and_analyze_simple([ + "--file", "*/skip_header.cpp"]) + print(glob.glob( + os.path.join(self.report_dir, '*.plist'))) + self.assertFalse( + any('skip_header.cpp' not in f for f in glob.glob( + os.path.join(self.report_dir, '*.plist')))) + + def test_parse_file_option(self): + """ Test parse command --file option. """ + skipfile = os.path.join(self.test_dir, "simple", "skipfile") + + self.__log_and_analyze_simple() + + # Only reports from the given files are returned. + out, _, returncode = self.__run_parse( + ["--file", "*/skip_header.cpp"]) + self.assertEqual(returncode, 2) + data = json.loads(out) + self.assertTrue(len(data['reports'])) + self.assertTrue(all( + r['file']['original_path'].endswith('/skip_header.cpp') + for r in data['reports'])) + + # The given file is skipped by the skipfile. + _, _, returncode = self.__run_parse( + ["--file", "*/file_to_be_skipped.cpp", "--ignore", skipfile]) + self.assertEqual(returncode, 0) + + # The given file is not skipped by the skip file. + out, _, returncode = self.__run_parse( + ["--file", "*/skip_header.cpp", "--ignore", skipfile]) + self.assertEqual(returncode, 2) data = json.loads(out) self.assertTrue(len(data['reports'])) + self.assertTrue(all( + r['file']['original_path'].endswith('/skip_header.cpp') + for r in data['reports'])) diff --git a/analyzer/tests/unit/test_log_parser.py b/analyzer/tests/unit/test_log_parser.py index 980c1bec86..afc04824be 100644 --- a/analyzer/tests/unit/test_log_parser.py +++ b/analyzer/tests/unit/test_log_parser.py @@ -18,7 +18,8 @@ from codechecker_report_converter.util import load_json_or_empty from codechecker_analyzer.buildlog import log_parser -from codechecker_common import skiplist_handler +from codechecker_common.skiplist_handler import SkipListHandler, \ + SkipListHandlers class LogParserTest(unittest.TestCase): @@ -324,13 +325,13 @@ def test_skip_everything_from_parse(self): -*/lib1/* -*/lib2/* """ - analysis_skip = skiplist_handler.SkipListHandler(skip_list) - pre_analysis_skip = skiplist_handler.SkipListHandler(skip_list) + analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) + pre_analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) build_actions, _ = log_parser.\ parse_unique_log(cmp_cmd_json, self.__this_dir, - analysis_skip_handler=analysis_skip, - pre_analysis_skip_handler=pre_analysis_skip) + analysis_skip_handlers=analysis_skip, + pre_analysis_skip_handlers=pre_analysis_skip) self.assertEqual(len(build_actions), 0) @@ -359,13 +360,13 @@ def test_skip_everything_from_parse_relative_path(self): -*/lib1/a.cpp -/tmp/lib2/a.cpp """ - analysis_skip = skiplist_handler.SkipListHandler(skip_list) - pre_analysis_skip = skiplist_handler.SkipListHandler(skip_list) + analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) + pre_analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) build_actions, _ = log_parser.\ parse_unique_log(cmp_cmd_json, self.__this_dir, - analysis_skip_handler=analysis_skip, - pre_analysis_skip_handler=pre_analysis_skip) + analysis_skip_handlers=analysis_skip, + pre_analysis_skip_handlers=pre_analysis_skip) self.assertEqual(len(build_actions), 1) self.assertEqual(build_actions[0].source, '/tmp/lib1/d.cpp') @@ -391,13 +392,13 @@ def test_skip_all_in_pre_from_parse(self): pre_skip_list = """ -* """ - analysis_skip = skiplist_handler.SkipListHandler(skip_list) - pre_analysis_skip = skiplist_handler.SkipListHandler(pre_skip_list) + analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) + pre_analysis_skip = SkipListHandlers([SkipListHandler(pre_skip_list)]) build_actions, _ = log_parser.\ parse_unique_log(cmp_cmd_json, self.__this_dir, - analysis_skip_handler=analysis_skip, - pre_analysis_skip_handler=pre_analysis_skip) + analysis_skip_handlers=analysis_skip, + pre_analysis_skip_handlers=pre_analysis_skip) self.assertEqual(len(build_actions), 1) @@ -421,14 +422,14 @@ def test_skip_no_pre_from_parse(self): skip_list = """ -*/lib1/* """ - analysis_skip = skiplist_handler.SkipListHandler(skip_list) - pre_analysis_skip = skiplist_handler.SkipListHandler("") + analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) + pre_analysis_skip = SkipListHandlers([SkipListHandler("")]) build_actions, _ = log_parser.\ parse_unique_log(cmp_cmd_json, self.__this_dir, - analysis_skip_handler=analysis_skip, + analysis_skip_handlers=analysis_skip, ctu_or_stats_enabled=True, - pre_analysis_skip_handler=pre_analysis_skip) + pre_analysis_skip_handlers=pre_analysis_skip) self.assertEqual(len(build_actions), 3) @@ -448,13 +449,13 @@ def test_no_skip_from_parse(self): skip_list = """ -*/lib1/* """ - analysis_skip = skiplist_handler.SkipListHandler("") - pre_analysis_skip = skiplist_handler.SkipListHandler(skip_list) + analysis_skip = SkipListHandlers([SkipListHandler("")]) + pre_analysis_skip = SkipListHandlers([SkipListHandler(skip_list)]) build_actions, _ = log_parser.\ parse_unique_log(cmp_cmd_json, self.__this_dir, - analysis_skip_handler=analysis_skip, - pre_analysis_skip_handler=pre_analysis_skip) + analysis_skip_handlers=analysis_skip, + pre_analysis_skip_handlers=pre_analysis_skip) self.assertEqual(len(build_actions), 3) diff --git a/analyzer/tests/unit/test_remove_report_from_plist.py b/analyzer/tests/unit/test_remove_report_from_plist.py index 7a22dede0f..01d3ce0a54 100644 --- a/analyzer/tests/unit/test_remove_report_from_plist.py +++ b/analyzer/tests/unit/test_remove_report_from_plist.py @@ -14,7 +14,8 @@ from codechecker_report_converter.report import report_file, \ reports as reports_handler -from codechecker_common.skiplist_handler import SkipListHandler +from codechecker_common.skiplist_handler import SkipListHandler, \ + SkipListHandlers OLD_PWD = None @@ -41,11 +42,11 @@ def __test_skip_reports( self, plist_file_path: str, expected_plist_file_path: str, - skip_handler: SkipListHandler + skip_handlers: SkipListHandlers ): """ Test skipping reports from a plist file. """ reports = report_file.get_reports(plist_file_path) - reports = reports_handler.skip(reports, skip_handler=skip_handler) + reports = reports_handler.skip(reports, skip_handlers=skip_handlers) expected_reports = report_file.get_reports(expected_plist_file_path) @@ -54,26 +55,26 @@ def __test_skip_reports( def test_skip_x_header(self): """ Test skipping a header file. """ with open('skip_x_header.txt', - encoding="utf-8", errors="ignore") as skip_file: - skip_handler = SkipListHandler(skip_file.read()) + encoding="utf-8", errors="ignore") as f: + skip_handlers = SkipListHandlers([SkipListHandler(f.read())]) self.__test_skip_reports( - 'x.plist', 'skip_x_header.expected.plist', skip_handler) + 'x.plist', 'skip_x_header.expected.plist', skip_handlers) def test_skip_all_header(self): """ Test skipping all header files. """ with open('skip_all_header.txt', - encoding="utf-8", errors="ignore") as skip_file: - skip_handler = SkipListHandler(skip_file.read()) + encoding="utf-8", errors="ignore") as f: + skip_handlers = SkipListHandlers([SkipListHandler(f.read())]) self.__test_skip_reports( - 'x.plist', 'skip_all_header.expected.plist', skip_handler) + 'x.plist', 'skip_all_header.expected.plist', skip_handlers) def test_keep_only_empty(self): """ Test skipping all files except empty. """ with open('keep_only_empty.txt', - encoding="utf-8", errors="ignore") as skip_file: - skip_handler = SkipListHandler(skip_file.read()) + encoding="utf-8", errors="ignore") as f: + skip_handlers = SkipListHandlers([SkipListHandler(f.read())]) self.__test_skip_reports( - 'x.plist', 'keep_only_empty.expected.plist', skip_handler) + 'x.plist', 'keep_only_empty.expected.plist', skip_handlers) diff --git a/codechecker_common/skiplist_handler.py b/codechecker_common/skiplist_handler.py index 57bd9a7247..d32af787c2 100644 --- a/codechecker_common/skiplist_handler.py +++ b/codechecker_common/skiplist_handler.py @@ -107,3 +107,20 @@ def __call__(self, source_file_path: str) -> bool: Check if the given source should be skipped. """ return self.should_skip(source_file_path) + + +class SkipListHandlers(list): + def should_skip(self, file_path: str): + """ + True if the given source should be skipped by any of the skip list + handler. + """ + return any(handler.should_skip(file_path) for handler in self) + + # FIXME: eliminate this function and use should_skip instead of this. + # Do the same in the SkipListHandler class above. + def __call__(self, file_path: str) -> bool: + """ + Check if the given source should be skipped. + """ + return self.should_skip(file_path) diff --git a/tools/report-converter/codechecker_report_converter/report/__init__.py b/tools/report-converter/codechecker_report_converter/report/__init__.py index 9e58eadd87..a426eb2d11 100644 --- a/tools/report-converter/codechecker_report_converter/report/__init__.py +++ b/tools/report-converter/codechecker_report_converter/report/__init__.py @@ -22,7 +22,7 @@ LOG = logging.getLogger('report-converter') -SkipListHandler = Callable[[str], bool] +SkipListHandlers = Callable[[str], bool] InvalidFileContentMsg: str = \ @@ -517,12 +517,12 @@ def review_status(self) -> str: return 'unreviewed' - def skip(self, skip_handler: Optional[SkipListHandler]) -> bool: + def skip(self, skip_handlers: Optional[SkipListHandlers]) -> bool: """ True if the report should be skipped. """ - if not skip_handler: + if not skip_handlers: return False - return skip_handler(self.file.original_path) + return skip_handlers(self.file.original_path) def to_json(self) -> Dict: """ Creates a JSON dictionary. """ diff --git a/tools/report-converter/codechecker_report_converter/report/reports.py b/tools/report-converter/codechecker_report_converter/report/reports.py index 90e64cd59e..6db7e99112 100644 --- a/tools/report-converter/codechecker_report_converter/report/reports.py +++ b/tools/report-converter/codechecker_report_converter/report/reports.py @@ -11,7 +11,7 @@ from typing import Any, Callable, Iterable, List, Optional, Set -from codechecker_report_converter.report import Report, SkipListHandler +from codechecker_report_converter.report import Report, SkipListHandlers from codechecker_report_converter.report.hash import get_report_path_hash LOG = logging.getLogger('report-converter') @@ -56,14 +56,14 @@ def dump_changed_files(changed_files: Set[str]): def skip( reports: List[Report], processed_path_hashes: Optional[Set[str]] = None, - skip_handler: Optional[SkipListHandler] = None, + skip_handlers: Optional[SkipListHandlers] = None, suppr_handler: Optional[GenericSuppressHandler] = None, src_comment_status_filter: Optional[Iterable[str]] = None ) -> List[Report]: """ Skip reports. """ kept_reports = [] for report in reports: - if skip_handler and report.skip(skip_handler): + if skip_handlers and report.skip(skip_handlers): LOG.debug("Skip report because file path (%s) is on the skip " "list.", report.file.path) continue From 9d6988c12c69371048bc1dcc0cdcb6767baa4a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Wed, 2 Mar 2022 09:21:50 +0100 Subject: [PATCH 2/2] [analyze] Analyze header file with --file option --- analyzer/codechecker_analyzer/cmd/analyze.py | 60 +++++++++++++++++--- analyzer/tests/functional/skip/test_skip.py | 16 +++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/analyzer/codechecker_analyzer/cmd/analyze.py b/analyzer/codechecker_analyzer/cmd/analyze.py index 5f8ca9559f..f01303b03b 100644 --- a/analyzer/codechecker_analyzer/cmd/analyze.py +++ b/analyzer/codechecker_analyzer/cmd/analyze.py @@ -19,7 +19,10 @@ import shutil import sys +from typing import List + from codechecker_report_converter.util import load_json_or_empty +from tu_collector import tu_collector from codechecker_analyzer import analyzer, analyzer_context, env from codechecker_analyzer.analyzers import analyzer_types, clangsa @@ -33,6 +36,9 @@ LOG = logger.get_logger('system') +header_file_extensions = ( + '.h', '.hh', '.H', '.hp', '.hxx', '.hpp', '.HPP', '.h++', '.tcc') + epilog_env_var = f""" CC_ANALYZERS_FROM_PATH Set to `yes` or `1` to enforce taking the analyzers from the `PATH` instead of the given binaries. @@ -725,22 +731,58 @@ def add_arguments_to_parser(parser): func=main, func_process_config_file=cmd_config.process_config_file) -def __get_skip_handlers(args) -> SkipListHandlers: +def get_affected_file_paths( + file_filters: List[str], + compile_commands: tu_collector.CompilationDB +) -> List[str]: + """ + Returns a list of source files for existing header file otherwise returns + with the same file path expression. + """ + file_paths = [] # Use list to keep the order of the file paths. + for file_filter in file_filters: + file_paths.append(file_filter) + + if os.path.exists(file_filter) and \ + file_filter.endswith(header_file_extensions): + LOG.info("Get dependent source files for '%s'...", file_filter) + dependent_sources = tu_collector.get_dependent_sources( + compile_commands, file_filter) + + LOG.info("Get dependent source files for '%s' done.", file_filter) + LOG.debug("Dependent source files: %s", + ', '.join(dependent_sources)) + + file_paths.extend(dependent_sources) + + return file_paths + + +def __get_skip_handlers(args, compile_commands) -> SkipListHandlers: """ Initialize and return a list of skiplist handlers if there is a skip list file in the arguments or files options is provided. """ skip_handlers = SkipListHandlers() if 'files' in args: + source_file_paths = get_affected_file_paths( + args.files, compile_commands) + # Creates a skip file where all source files will be skipped except # the given source files and all the header files. - skip_files = ['+{0}'.format(f) for f in args.files] + skip_files = ['+{0}'.format(f) for f in source_file_paths] skip_files.extend(['+/*.h', '+/*.H', '+/*.tcc']) skip_files.append('-*') - skip_handlers.append(SkipListHandler("\n".join(skip_files))) + content = "\n".join(skip_files) + skip_handlers.append(SkipListHandler(content)) + LOG.debug("Skip handler is created for the '--file' option with the " + "following filters:\n%s", content) if 'skipfile' in args: with open(args.skipfile, encoding="utf-8", errors="ignore") as f: - skip_handlers.append(SkipListHandler(f.read())) + content = f.read() + skip_handlers.append(SkipListHandler(content)) + LOG.debug("Skip handler is created for the '--ignore' option with " + "the following filters:\n%s", content) return skip_handlers @@ -868,8 +910,12 @@ def main(args): sys.exit(1) compiler_info_file = args.compiler_info_file + compile_commands = load_json_or_empty(args.logfile) + if compile_commands is None: + sys.exit(1) + # Process the skip list if present. - skip_handlers = __get_skip_handlers(args) + skip_handlers = __get_skip_handlers(args, compile_commands) ctu_or_stats_enabled = False # Skip list is applied only in pre-analysis @@ -897,10 +943,6 @@ def main(args): analyzer_env = env.extend(context.path_env_extra, context.ld_lib_path_extra) - compile_commands = load_json_or_empty(args.logfile) - if compile_commands is None: - sys.exit(1) - # Number of all the compilation commands in the parsed log files, # logged by the logger. all_cmp_cmd_count = len(compile_commands) diff --git a/analyzer/tests/functional/skip/test_skip.py b/analyzer/tests/functional/skip/test_skip.py index 40a797f977..4107dc1a7b 100644 --- a/analyzer/tests/functional/skip/test_skip.py +++ b/analyzer/tests/functional/skip/test_skip.py @@ -79,6 +79,7 @@ def __log_and_analyze_simple(self, analyzer_extra_options=None): print(err) errcode = process.returncode self.assertEqual(errcode, 0) + return out, err def __run_parse(self, extra_options=None): """ Run parse command with the given extra options. """ @@ -226,6 +227,19 @@ def test_analyze_skip_everything(self): self.assertFalse( glob.glob(os.path.join(self.report_dir, '*.plist'))) + def test_analyze_header_with_file_option(self): + """ Analyze a header file with the --file option. """ + header_file = os.path.join(self.test_dir, "simple", "skip.h") + out, _ = self.__log_and_analyze_simple(["--file", header_file]) + self.assertIn( + f"Get dependent source files for '{header_file}'...", out) + self.assertIn( + f"Get dependent source files for '{header_file}' done.", out) + + plist_files = glob.glob(os.path.join(self.report_dir, '*.plist')) + self.assertTrue(plist_files) + self.assertTrue(all('skip_header.cpp' in f for f in plist_files)) + def test_analyze_file_option_skip_everything(self): """ Test analyze command --file option when everything is skipped by a @@ -295,8 +309,6 @@ def test_analyze_only_file_option(self): """ self.__log_and_analyze_simple([ "--file", "*/skip_header.cpp"]) - print(glob.glob( - os.path.join(self.report_dir, '*.plist'))) self.assertFalse( any('skip_header.cpp' not in f for f in glob.glob( os.path.join(self.report_dir, '*.plist'))))