From e489457354a29344e8733db5dd9198917a824a0a Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 31 Mar 2026 11:40:39 +0200 Subject: [PATCH 1/6] Add skip option to per_file --- src/codechecker.bzl | 1 + src/per_file.bzl | 6 ++++++ src/per_file_script.py | 19 ++++++++++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/codechecker.bzl b/src/codechecker.bzl index 505532d0..bb428ff3 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -341,6 +341,7 @@ def codechecker_test( name = name, targets = targets, options = analyze, + skip = skip, config = config, tags = tags, **kwargs diff --git a/src/per_file.bzl b/src/per_file.bzl index 27e87654..1178842c 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -132,6 +132,7 @@ def _create_wrapper_script(ctx, options, compile_commands_json, config_file): "{codechecker_args}": options_str, "{compile_commands_json}": compile_commands_json.path, "{config_file}": config_file.path, + "[\"{skip_list}\"]": str(ctx.attr.skip) }, ) @@ -223,6 +224,11 @@ per_file_test = rule( ], doc = "List of compilable targets which should be checked.", ), + "skip": attr.string_list( + default = [], + doc = "List of skip/ignore file rules. " + + "See https://codechecker.readthedocs.io/en/latest/analyzer/user_guide/#skip-file", + ), "_per_file_script_template": attr.label( default = ":per_file_script.py", allow_single_file = True, diff --git a/src/per_file_script.py b/src/per_file_script.py index d64b7a6c..934d1536 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -19,6 +19,7 @@ """ import os +from pathlib import Path import re import shutil import subprocess @@ -28,20 +29,28 @@ # The output directory for CodeChecker DATA_DIR: Optional[str] = None # The file to be analyzed -FILE_PATH: Optional[str] = None +FILE_PATH: str = None # pyright: ignore # List of pairs of analyzers and their plist files -ANALYZER_PLIST_PATHS: Optional[list[list[str]]] = None +ANALYZER_PLIST_PATHS: list[list[str]] = None # pyright: ignore LOG_FILE: Optional[str] = None COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" CODECHECKER_ARGS: str = "{codechecker_args}" CONFIG_FILE: str = "{config_file}" +SKIP_LIST: list[str] = ["{skip_list}"] DATA_DIR = sys.argv[1] FILE_PATH = sys.argv[2] LOG_FILE = sys.argv[3] ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")] +def skipped(): + for pattern in SKIP_LIST: + if re.search(pattern, FILE_PATH): + return True + return False + + def log(msg: str) -> None: """ Append message to the log file @@ -151,7 +160,11 @@ def main(): print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths() - _run_codechecker() + if skipped(): + for analyzer_list in ANALYZER_PLIST_PATHS: + Path(analyzer_list[1]).touch() + else: + _run_codechecker() _move_plist_files() From 6ded9001275e863b82d81511ca28c1becb34c83e Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 31 Mar 2026 13:57:39 +0200 Subject: [PATCH 2/6] Fix test and skip option --- src/per_file_script.py | 37 ++++++++++++++++++++++++++++++++----- test/unit/skip/BUILD | 12 ++++++++++++ test/unit/skip/test_skip.py | 36 ++++++++++++++++++++++++++---------- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/per_file_script.py b/src/per_file_script.py index 934d1536..e38eb1c1 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -18,6 +18,7 @@ Codechecker wrapper script for per-file analysis """ +import fnmatch import os from pathlib import Path import re @@ -32,7 +33,7 @@ FILE_PATH: str = None # pyright: ignore # List of pairs of analyzers and their plist files ANALYZER_PLIST_PATHS: list[list[str]] = None # pyright: ignore -LOG_FILE: Optional[str] = None +LOG_FILE: str = None # pyright: ignore COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" CODECHECKER_ARGS: str = "{codechecker_args}" @@ -43,12 +44,36 @@ LOG_FILE = sys.argv[3] ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")] +EMPTY_PLIST = """ + + + + diagnostics + + files + + + +""" def skipped(): + skip_this = False + + positive_patterns = [] + negative_patterns = [] + for pattern in SKIP_LIST: + if pattern[0] == '+': + positive_patterns.append(fnmatch.translate(pattern[1::])) + else: + negative_patterns.append(fnmatch.translate(pattern[1::])) + for pattern in negative_patterns: + if re.search(pattern, FILE_PATH): + skip_this = True + for pattern in positive_patterns: if re.search(pattern, FILE_PATH): - return True - return False + skip_this = False + return skip_this def log(msg: str) -> None: @@ -160,12 +185,14 @@ def main(): print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths() + Path(LOG_FILE).touch() if skipped(): for analyzer_list in ANALYZER_PLIST_PATHS: - Path(analyzer_list[1]).touch() + with open(analyzer_list[1], "w", encoding="utf-8") as file: + file.write(EMPTY_PLIST) else: _run_codechecker() - _move_plist_files() + _move_plist_files() if __name__ == "__main__": diff --git a/test/unit/skip/BUILD b/test/unit/skip/BUILD index 5d9e0737..b55d07fe 100644 --- a/test/unit/skip/BUILD +++ b/test/unit/skip/BUILD @@ -76,6 +76,18 @@ codechecker_test( skip = [ "-*test/unit/skip/skip*", ], + targets = [ + "simple_target", + ], +) + +codechecker_test( + name = "per_file_skipfile_both_files_except_one", + per_file = True, + skip = [ + "-*test/unit/skip/skip*", + "+*test/unit/skip/skip.cc" + ], tags = ["manual"], targets = [ "simple_target", diff --git a/test/unit/skip/test_skip.py b/test/unit/skip/test_skip.py index 807230e0..66a49899 100644 --- a/test/unit/skip/test_skip.py +++ b/test/unit/skip/test_skip.py @@ -41,7 +41,7 @@ def test_codechecker_skipfile(self): ) self.assertEqual(ret, 0, stderr) - def test_per_file_skipfile_full_path(self): + def test_per_file_skipfile_exact_file_path(self): """ Test: bazel test //test/unit/skip:per_file_skipfile_exact_file_path """ @@ -53,8 +53,7 @@ def test_per_file_skipfile_full_path(self): f"{self.BAZEL_TESTLOGS_DIR}/" "per_file_skipfile_exact_file_path/test.log" ) - # FIXME: change to assertFalse, this file should be skipped - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) self.assertTrue( @@ -73,8 +72,7 @@ def test_per_file_skipfile_folder_skip_path(self): f"{self.BAZEL_TESTLOGS_DIR}/" "per_file_skipfile_folder_skip_path/test.log" ) - # FIXME: change to assertFalse, this file should be skipped - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) # This is correct. @@ -89,17 +87,35 @@ def test_per_file_skipfile_both_files(self): ret, _, stderr = self.run_command( "bazel test //test/unit/skip:per_file_skipfile_both_files" ) - # FIXME: The return code here should be 0, both files should be skipped - self.assertEqual(ret, 3, stderr) + self.assertEqual(ret, 0, stderr) log_file = ( f"{self.BAZEL_TESTLOGS_DIR}/per_file_skipfile_both_files/test.log" ) - # FIXME: Change to assertFalse after fix, should have been skipped. - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) - # FIXME: Change to assertFalse after fix, should have been skipped. + self.assertFalse( + self.contains_regex_in_file(log_file, r"defect\(s\) in skip2.cc") + ) + + def test_per_file_skipfile_both_files_except_one(self): + """ + Test: bazel test + //test/unit/skip:per_file_skipfile_both_files_except_one + """ + ret, _, stderr = self.run_command( + "bazel test //test/unit/skip:" + "per_file_skipfile_both_files_except_one" + ) + self.assertEqual(ret, 3, stderr) + log_file = ( + f"{self.BAZEL_TESTLOGS_DIR}/" + "per_file_skipfile_both_files_except_one/test.log" + ) self.assertTrue( + self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") + ) + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip2.cc") ) From 216cc7b759d9f53a6e1f07f0ba5d22a7ca27f763 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 31 Mar 2026 14:01:21 +0200 Subject: [PATCH 3/6] Add docstring --- src/per_file_script.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/per_file_script.py b/src/per_file_script.py index e38eb1c1..1423b981 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -56,23 +56,27 @@ """ + def skipped(): + """ + Checks if this file should be skipped. + Return a boolean value. + """ skip_this = False - positive_patterns = [] negative_patterns = [] - + for pattern in SKIP_LIST: - if pattern[0] == '+': + if pattern[0] == "+": positive_patterns.append(fnmatch.translate(pattern[1::])) else: negative_patterns.append(fnmatch.translate(pattern[1::])) for pattern in negative_patterns: if re.search(pattern, FILE_PATH): - skip_this = True + skip_this = True for pattern in positive_patterns: if re.search(pattern, FILE_PATH): - skip_this = False + skip_this = False return skip_this @@ -169,12 +173,11 @@ def _move_plist_files(): rf"_{analyzer_info[0]}_.*\.plist$", file ) and os.path.isfile( os.path.join(DATA_DIR, file) # type: ignore - ): shutil.move( - os.path.join(DATA_DIR, file), # type: ignore + os.path.join(DATA_DIR, file), # type: ignore analyzer_info[1], - ) + ) def main(): From 1bb58dbc14a6e0d89517a5e1f630b8316b2fe8c0 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 7 Apr 2026 14:53:02 +0200 Subject: [PATCH 4/6] Rewrite per_file to use CodeChecker skipfiles --- src/per_file.bzl | 34 ++++++++++++---- src/per_file_script.py | 90 ++++++++++++++---------------------------- test/unit/skip/BUILD | 2 +- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/src/per_file.bzl b/src/per_file.bzl index 1178842c..57d55e56 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -50,12 +50,30 @@ def _run_code_checker( clangsa_plist = ctx.actions.declare_file(clangsa_plist_file_name) codechecker_log = ctx.actions.declare_file(codechecker_log_file_name) + # Create skipfile + config = ctx.actions.declare_file( + "{}/{}_skipfile".format(*file_name_params), + ) + ctx.actions.write( + output = config, + content = "\n".join(ctx.attr.skip), + ) + if "--ctu" in options: - inputs = [compile_commands_json, config_file] + sources_and_headers + inputs = [ + compile_commands_json, + config_file, + config, + ] + sources_and_headers else: # NOTE: we collect only headers, so CTU may not work! headers = depset(transitive = target[SourceFilesInfo].headers.to_list()) - inputs = depset([compile_commands_json, config_file, src], transitive = [headers]) + inputs = depset([ + compile_commands_json, + config_file, + src, + config, + ], transitive = [headers]) outputs = [clang_tidy_plist, clangsa_plist, codechecker_log] @@ -71,6 +89,7 @@ def _run_code_checker( data_dir, src.path, codechecker_log.path, + config.path, analyzer_output_paths, ], mnemonic = "CodeChecker", @@ -132,7 +151,6 @@ def _create_wrapper_script(ctx, options, compile_commands_json, config_file): "{codechecker_args}": options_str, "{compile_commands_json}": compile_commands_json.path, "{config_file}": config_file.path, - "[\"{skip_list}\"]": str(ctx.attr.skip) }, ) @@ -218,17 +236,17 @@ per_file_test = rule( default = [], doc = "List of CodeChecker options, e.g.: --ctu", ), + "skip": attr.string_list( + default = [], + doc = "List of skip/ignore file rules. " + + "See https://codechecker.readthedocs.io/en/latest/analyzer/user_guide/#skip-file", + ), "targets": attr.label_list( aspects = [ compile_commands_aspect, ], doc = "List of compilable targets which should be checked.", ), - "skip": attr.string_list( - default = [], - doc = "List of skip/ignore file rules. " + - "See https://codechecker.readthedocs.io/en/latest/analyzer/user_guide/#skip-file", - ), "_per_file_script_template": attr.label( default = ":per_file_script.py", allow_single_file = True, diff --git a/src/per_file_script.py b/src/per_file_script.py index 1423b981..302e6e55 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -18,31 +18,24 @@ Codechecker wrapper script for per-file analysis """ -import fnmatch import os -from pathlib import Path import re import shutil import subprocess import sys -from typing import Optional -# The output directory for CodeChecker -DATA_DIR: Optional[str] = None -# The file to be analyzed -FILE_PATH: str = None # pyright: ignore -# List of pairs of analyzers and their plist files -ANALYZER_PLIST_PATHS: list[list[str]] = None # pyright: ignore -LOG_FILE: str = None # pyright: ignore COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" CODECHECKER_ARGS: str = "{codechecker_args}" CONFIG_FILE: str = "{config_file}" -SKIP_LIST: list[str] = ["{skip_list}"] +SKIP_FILE: str = sys.argv[4] +# The output directory for CodeChecker DATA_DIR = sys.argv[1] +# The file to be analyzed FILE_PATH = sys.argv[2] LOG_FILE = sys.argv[3] -ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")] +# List of pairs of analyzers and their plist files +ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[5].split(";")] EMPTY_PLIST = """ @@ -57,34 +50,11 @@ """ -def skipped(): - """ - Checks if this file should be skipped. - Return a boolean value. - """ - skip_this = False - positive_patterns = [] - negative_patterns = [] - - for pattern in SKIP_LIST: - if pattern[0] == "+": - positive_patterns.append(fnmatch.translate(pattern[1::])) - else: - negative_patterns.append(fnmatch.translate(pattern[1::])) - for pattern in negative_patterns: - if re.search(pattern, FILE_PATH): - skip_this = True - for pattern in positive_patterns: - if re.search(pattern, FILE_PATH): - skip_this = False - return skip_this - - def log(msg: str) -> None: """ Append message to the log file """ - with open(LOG_FILE, "a", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "a", encoding="utf-8") as log_file: log_file.write(msg) @@ -114,8 +84,9 @@ def _run_codechecker() -> None: codechecker_cmd: list[str] = ( ["CodeChecker", "analyze"] + CODECHECKER_ARGS.split() - + ["--output=" + DATA_DIR] # type: ignore - + ["--file=*/" + FILE_PATH] # type: ignore + + ["--output=" + DATA_DIR] + + ["--file=*/" + FILE_PATH] + + ["--skip", SKIP_FILE] + ["--config", CONFIG_FILE] + [COMPILE_COMMANDS_ABSOLUTE] ) @@ -135,7 +106,7 @@ def _run_codechecker() -> None: log(result.stdout) try: - with open(LOG_FILE, "a", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "a", encoding="utf-8") as log_file: subprocess.run( codechecker_cmd, env=os.environ, @@ -156,7 +127,7 @@ def _display_error(ret_code: int) -> None: # Log and exit on error print("===-----------------------------------------------------===") print(f"[ERROR]: CodeChecker returned with {ret_code}!") - with open(LOG_FILE, "r", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "r", encoding="utf-8") as log_file: print(log_file.read()) sys.exit(1) @@ -164,38 +135,37 @@ def _display_error(ret_code: int) -> None: def _move_plist_files(): """ Move the plist files from the temporary directory to their final destination + If the files doesn't exists, write an empty plist file to the target. """ # NOTE: the following we do to get rid of md5 hash in plist file names # Copy the plist files to the specified destinations - for file in os.listdir(DATA_DIR): - for analyzer_info in ANALYZER_PLIST_PATHS: # type: ignore - if re.search( - rf"_{analyzer_info[0]}_.*\.plist$", file - ) and os.path.isfile( - os.path.join(DATA_DIR, file) # type: ignore - ): - shutil.move( - os.path.join(DATA_DIR, file), # type: ignore - analyzer_info[1], - ) + compiled_analyzers = [ + (re.compile(rf"_{analyzer[0]}_.*\.plist$"), analyzer[1]) + for analyzer in ANALYZER_PLIST_PATHS + ] + + for regex, target_file in compiled_analyzers: + for file_path in os.listdir(DATA_DIR): + if not os.path.isfile(os.path.join(DATA_DIR, file_path)): + continue + if regex.search(file_path): + shutil.move(os.path.join(DATA_DIR, file_path), target_file) + break + else: + with open(target_file, "w", encoding="utf-8") as file: + file.write(EMPTY_PLIST) def main(): """ Main function of CodeChecker wrapper """ - if len(sys.argv) != 5: + if len(sys.argv) != 6: print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths() - Path(LOG_FILE).touch() - if skipped(): - for analyzer_list in ANALYZER_PLIST_PATHS: - with open(analyzer_list[1], "w", encoding="utf-8") as file: - file.write(EMPTY_PLIST) - else: - _run_codechecker() - _move_plist_files() + _run_codechecker() + _move_plist_files() if __name__ == "__main__": diff --git a/test/unit/skip/BUILD b/test/unit/skip/BUILD index b55d07fe..b45ec426 100644 --- a/test/unit/skip/BUILD +++ b/test/unit/skip/BUILD @@ -85,8 +85,8 @@ codechecker_test( name = "per_file_skipfile_both_files_except_one", per_file = True, skip = [ + "+*test/unit/skip/skip.cc", "-*test/unit/skip/skip*", - "+*test/unit/skip/skip.cc" ], tags = ["manual"], targets = [ From 1defb2f2e045b0f2d96cc2c5b4b1a6f18dea1369 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Mon, 4 May 2026 14:42:11 +0200 Subject: [PATCH 5/6] Make variable names more expressive --- src/per_file_script.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/per_file_script.py b/src/per_file_script.py index 302e6e55..f32ee842 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -136,23 +136,32 @@ def _move_plist_files(): """ Move the plist files from the temporary directory to their final destination If the files doesn't exists, write an empty plist file to the target. + This can happen when an analysis was skipped + because of a CodeChecker skipfile. + For each analysis action we must have an output file, even if its skipped, + so we substitute it with an empty one. """ # NOTE: the following we do to get rid of md5 hash in plist file names # Copy the plist files to the specified destinations - compiled_analyzers = [ - (re.compile(rf"_{analyzer[0]}_.*\.plist$"), analyzer[1]) + plist_search_mappings = [ + (analyzer[1], re.compile(rf"_{analyzer[0]}_.*\.plist$")) for analyzer in ANALYZER_PLIST_PATHS ] - for regex, target_file in compiled_analyzers: + for ( + destination_plist_path, + source_plist_search_pattern, + ) in plist_search_mappings: for file_path in os.listdir(DATA_DIR): if not os.path.isfile(os.path.join(DATA_DIR, file_path)): continue - if regex.search(file_path): - shutil.move(os.path.join(DATA_DIR, file_path), target_file) + if source_plist_search_pattern.search(file_path): + shutil.move( + os.path.join(DATA_DIR, file_path), destination_plist_path + ) break else: - with open(target_file, "w", encoding="utf-8") as file: + with open(destination_plist_path, "w", encoding="utf-8") as file: file.write(EMPTY_PLIST) From 63fed80d910d2005697ed8976cf38ea6fb221791 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Mon, 4 May 2026 14:44:59 +0200 Subject: [PATCH 6/6] Match metadata with codechecker's output --- src/per_file_script.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/per_file_script.py b/src/per_file_script.py index f32ee842..ab9901dc 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -41,10 +41,14 @@ - diagnostics - - files - + metadata + + generated_by + + name + CodeChecker + + """ @@ -143,7 +147,7 @@ def _move_plist_files(): """ # NOTE: the following we do to get rid of md5 hash in plist file names # Copy the plist files to the specified destinations - plist_search_mappings = [ + destination_and_source_pattern_pairs = [ (analyzer[1], re.compile(rf"_{analyzer[0]}_.*\.plist$")) for analyzer in ANALYZER_PLIST_PATHS ] @@ -151,7 +155,7 @@ def _move_plist_files(): for ( destination_plist_path, source_plist_search_pattern, - ) in plist_search_mappings: + ) in destination_and_source_pattern_pairs: for file_path in os.listdir(DATA_DIR): if not os.path.isfile(os.path.join(DATA_DIR, file_path)): continue