From 274dc42fd46d672bbf450807ae08c07a36ea22f3 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 26 Aug 2026 05:47:45 +0200 Subject: [PATCH 1/5] Add parse step to per-file Move logging into common.py Fix lint issues Update expected action number (since we added an extra parse action) Undo changes to codechecker script --- src/per_file.bzl | 47 +++++++++++++++++++++++++++++++++++++++++ src/per_file_script.py | 47 +++++++++++++++++++++++++++++------------ test/unit/caching/BUILD | 4 ++-- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/per_file.bzl b/src/per_file.bzl index b1c0db57..7e522eeb 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -207,6 +207,7 @@ def _per_file_impl(ctx): info = ctx.attr.toolchain[platform_common.ToolchainInfo].codecheckerinfo else: info = ctx.toolchains["//:toolchain_type"].codecheckerinfo + for target in ctx.attr.targets: if not CcInfo in target: continue @@ -235,6 +236,52 @@ def _per_file_impl(ctx): sources_and_headers, ) all_files += outputs + + # Parse action: collect all plists into a directory and run + # CodeChecker parse to produce result.txt, result.json, HTML report + codechecker_files = ctx.actions.declare_directory( + ctx.label.name + "/parse", + ) + codechecker_parse_log = ctx.actions.declare_file( + ctx.label.name + "/codechecker_parse.log", + ) + + # Build arguments for the parse action + # The data dir is where the per-file analyze actions put their plists + # All plists are in /data/, derive path from first plist + #data_dir_path = plist_and_metadata_files[0].dirname if plist_and_metadata_files else "" + + ctx.actions.run( + inputs = all_files + [config_file], + outputs = [codechecker_files, codechecker_parse_log], + executable = per_file_script, + tools = [ + info.runfiles, + ctx.attr._per_file_script[DefaultInfo].files_to_run, + ], + arguments = [ + "--mode", + "Parse", + "--codechecker", + info.codechecker.path, + "--data_dir", + codechecker_files.path, + "--log", + codechecker_parse_log.path, + "--config", + config_file.path, + "--clang", + info.clangsa.path, + "--clang_tidy", + info.clang_tidy.path, + ], + mnemonic = "CodeCheckerParse", + use_default_shell_env = True, + progress_message = "CodeChecker parse %s" % str(ctx.label), + ) + + all_files += [codechecker_files, codechecker_parse_log] + ctx.actions.write( output = ctx.outputs.test_script, is_executable = True, diff --git a/src/per_file_script.py b/src/per_file_script.py index adcf316d..237ae5a0 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -17,12 +17,16 @@ """ import argparse -from dataclasses import dataclass import os import re import shutil import subprocess -from common import fail, setup_logging, build_env +from dataclasses import dataclass +# pylint outside bazel cannot follow the dependency graph +# This should be removed when pylint is integrated into bazel +from common import ( # pylint: disable=no-name-in-module + fail, parse, setup_logging, build_env +) @dataclass @@ -52,30 +56,30 @@ def parse_args(argv=None): ) parser.add_argument("--mode", required=True, help="Execution mode") parser.add_argument( - "--codechecker", required=True, help="Path to CodeChecker binary" + "--codechecker", required=False, help="Path to CodeChecker binary" ) parser.add_argument("--verbosity", default="INFO", help="Log level") parser.add_argument( - "--commands", required=True, help="Path to compile_commands.json" + "--commands", required=False, help="Path to compile_commands.json" ) parser.add_argument( "--analyze", default="", help="CodeChecker analyze arguments" ) - parser.add_argument("--config", required=True, help="Path to config file") + parser.add_argument("--config", required=False, help="Path to config file") parser.add_argument( "--data_dir", required=True, help="Output directory for CodeChecker" ) parser.add_argument( - "--file", required=True, help="Path to the file to be analyzed" + "--file", required=False, help="Path to the file to be analyzed" ) - parser.add_argument("--log", required=True, help="Path to the log file") - parser.add_argument("--skip", required=True, help="Path to the skip file") + parser.add_argument("--log", required=False, help="Path to the log file") + parser.add_argument("--skip", required=False, help="Path to the skip file") parser.add_argument( - "--metadata", required=True, help="Path to the metadata file" + "--metadata", required=False, help="Path to the metadata file" ) parser.add_argument( "--analyzer_plists", - required=True, + required=False, help="Semicolon-separated list of analyzer,plist_path pairs", ) parser.add_argument( @@ -89,13 +93,15 @@ def parse_args(argv=None): args = parser.parse_args(argv) - analyzer_plist_paths = [ - item.split(",") for item in args.analyzer_plists.split(";") - ] + analyzer_plist_paths = [] + if args.analyzer_plists: + analyzer_plist_paths = [ + item.split(",") for item in args.analyzer_plists.split(";") + ] return Config( execution_mode=args.mode, - codechecker_bin=os.path.realpath(args.codechecker), + codechecker_bin=os.path.realpath(args.codechecker or "/"), compile_commands=args.commands, codechecker_args=args.analyze, config_file=args.config, @@ -288,6 +294,19 @@ def main(): _create_compile_commands_json_with_absolute_paths(cfg) _run_codechecker(cfg) _move_output_files(cfg) + elif cfg.execution_mode == "Parse": + with open(cfg.log_file, "a", encoding="utf-8"): + pass + parse( + input_dir=cfg.data_dir + "/..", + output_dir=cfg.data_dir, + codechecker=cfg.codechecker_bin, + config=cfg.config_file, + env="", + log=cfg.log_file, + clang=cfg.clang, + clang_tidy=cfg.clang_tidy, + ) else: fail( cfg.log_file, diff --git a/test/unit/caching/BUILD b/test/unit/caching/BUILD index c173b56b..79ce3a17 100644 --- a/test/unit/caching/BUILD +++ b/test/unit/caching/BUILD @@ -33,14 +33,14 @@ caching_test( caching_test( name = "caching_per_file_test", - expected_action_count = 1, + expected_action_count = 2, file_to_modify = "secondary.cc", target_name = "per_file_caching", ) caching_test( name = "caching_per_file_ctu_test", - expected_action_count = 2, + expected_action_count = 3, file_to_modify = "secondary.cc", target_name = "per_file_caching_ctu", ) From eb86db2d1e2008f664cb1a2db1c03fc30fe3cd03 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Fri, 9 Oct 2026 07:01:07 +0200 Subject: [PATCH 2/5] Change how parsing is stored in per-file --- src/codechecker_script.py | 1 - src/common.py | 9 ++++----- src/per_file.bzl | 17 +++++++---------- src/per_file_script.py | 3 +-- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/codechecker_script.py b/src/codechecker_script.py index 95154335..e37603df 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -246,7 +246,6 @@ def run(args): analyze(args) parse( input_dir=args.output, - output_dir=args.output, codechecker=args.codechecker, config=args.config, env=args.env, diff --git a/src/common.py b/src/common.py index 25bf028f..76977378 100644 --- a/src/common.py +++ b/src/common.py @@ -136,14 +136,13 @@ def stage(title, method="info"): # pylint: disable=too-many-arguments,too-many-positional-arguments def parse( - input_dir, output_dir, codechecker, config, env, log, clang, clang_tidy + input_dir, codechecker, config, env, log, clang, clang_tidy ): """ Run CodeChecker parse commands Args: input_dir (str): Path to the input directory - output_dir (str): Path to the output directory codechecker (str): Path to the CodeChecker binary config (str): Path to the CodeChecker configuration file env (list): Environment variables @@ -160,18 +159,18 @@ def parse( ) # Save results to JSON file command = ( - f"{codechecker_parse} --export=json > " f"{output_dir}/result.json" + f"{codechecker_parse} --export=json > " f"{input_dir}/result.json" ) execute(log, command, env=env, codes=[0, 2]) # Save results as HTML report logging.info("CodeChecker parse -e html") command = ( - codechecker_parse + " --export=html --output=" + output_dir + "/report" + codechecker_parse + " --export=html --output=" + input_dir + "/report" ) execute(log, command, env=env, codes=[0, 2]) # Save results to text file logging.info("CodeChecker parse to text result") - result_file = output_dir + "/result.txt" + result_file = input_dir + "/result.txt" command = codechecker_parse + " > " + result_file execute(log, command, env=env, codes=[0, 2]) logging.info("Result:\n\n%s\n", read_file(log, result_file)) diff --git a/src/per_file.bzl b/src/per_file.bzl index 7e522eeb..0400b2c4 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -239,21 +239,18 @@ def _per_file_impl(ctx): # Parse action: collect all plists into a directory and run # CodeChecker parse to produce result.txt, result.json, HTML report - codechecker_files = ctx.actions.declare_directory( - ctx.label.name + "/parse", + html_parse_dir = ctx.actions.declare_directory( + ctx.label.name + "/report", ) + json_parse = ctx.actions.declare_file(ctx.label.name + "/result.json") + txt_parse = ctx.actions.declare_file(ctx.label.name + "/result.txt") codechecker_parse_log = ctx.actions.declare_file( ctx.label.name + "/codechecker_parse.log", ) - # Build arguments for the parse action - # The data dir is where the per-file analyze actions put their plists - # All plists are in /data/, derive path from first plist - #data_dir_path = plist_and_metadata_files[0].dirname if plist_and_metadata_files else "" - ctx.actions.run( inputs = all_files + [config_file], - outputs = [codechecker_files, codechecker_parse_log], + outputs = [html_parse_dir, codechecker_parse_log, json_parse, txt_parse], executable = per_file_script, tools = [ info.runfiles, @@ -265,7 +262,7 @@ def _per_file_impl(ctx): "--codechecker", info.codechecker.path, "--data_dir", - codechecker_files.path, + html_parse_dir.path + "/..", "--log", codechecker_parse_log.path, "--config", @@ -280,7 +277,7 @@ def _per_file_impl(ctx): progress_message = "CodeChecker parse %s" % str(ctx.label), ) - all_files += [codechecker_files, codechecker_parse_log] + all_files += [html_parse_dir, codechecker_parse_log] ctx.actions.write( output = ctx.outputs.test_script, diff --git a/src/per_file_script.py b/src/per_file_script.py index 237ae5a0..636bafc7 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -298,8 +298,7 @@ def main(): with open(cfg.log_file, "a", encoding="utf-8"): pass parse( - input_dir=cfg.data_dir + "/..", - output_dir=cfg.data_dir, + input_dir=cfg.data_dir, codechecker=cfg.codechecker_bin, config=cfg.config_file, env="", From 35d740fda1361e450a5e50ba36da8179ab497e37 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Fri, 9 Oct 2026 07:39:30 +0200 Subject: [PATCH 3/5] Update docs --- docs/codechecker.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/codechecker.md b/docs/codechecker.md index 7f4edaa4..5b6ce3f5 100644 --- a/docs/codechecker.md +++ b/docs/codechecker.md @@ -77,15 +77,19 @@ bazel test ... ### Analysis results -You can find the analysis results in the `bazel-bin/` folder, on which you -can run [`CodeChecker store`](https://github.com/Ericsson/codechecker/blob/master/docs/web/user_guide.md#store) -or [`CodeChecker parse`](https://github.com/Ericsson/codechecker/blob/master/docs/analyzer/user_guide.md#parse). -The precise output path to the directory can vary, -but you should look for `your_codechecker_rule_name/codechecker-files/data`. -In simpler cases, something like the following: +Analysis results are written to the bazel-bin/ directory. +The exact output path may vary, but you should find them under: +`bazel-bin/.../your_codechecker_rule_name/codechecker-files/`. +This directory contains: +- result.txt — human-readable results +- result.json — results in structured JSON format +- report/ — results in HTML format +- data/ — the directory containing the raw results + +You can store the results with [`CodeChecker store`](https://github.com/Ericsson/codechecker/blob/master/docs/web/user_guide.md#store). +In simpler cases, with something like the following: ```bash -CodeChecker parse bazel-bin/your_codechecker_rule_name/codechecker-files/data CodeChecker store bazel-bin/your_codechecker_rule_name/codechecker-files/data -n "Run name" ``` From 77a285d6112b20c955303a719288bd7ad0aaa0cd Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Fri, 9 Oct 2026 08:38:35 +0200 Subject: [PATCH 4/5] Fix minor issues --- src/per_file.bzl | 2 +- src/per_file_script.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/per_file.bzl b/src/per_file.bzl index 0400b2c4..d8af6a1b 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -277,7 +277,7 @@ def _per_file_impl(ctx): progress_message = "CodeChecker parse %s" % str(ctx.label), ) - all_files += [html_parse_dir, codechecker_parse_log] + all_files += [html_parse_dir, codechecker_parse_log, json_parse, txt_parse] ctx.actions.write( output = ctx.outputs.test_script, diff --git a/src/per_file_script.py b/src/per_file_script.py index 636bafc7..9776bb95 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -22,9 +22,7 @@ import shutil import subprocess from dataclasses import dataclass -# pylint outside bazel cannot follow the dependency graph -# This should be removed when pylint is integrated into bazel -from common import ( # pylint: disable=no-name-in-module +from common import ( fail, parse, setup_logging, build_env ) From c7c3127c24b2099e7cbbfadd0de2b2432169108f Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 26 Aug 2026 05:47:45 +0200 Subject: [PATCH 5/5] Add severities to per_file rule Move logging into common.py Fix lint issues Update expected action number (since we added an extra parse action) Undo changes to codechecker script --- src/codechecker.bzl | 1 + src/per_file.bzl | 47 +++++++++++++++++++++++++++--------------- src/per_file_script.py | 9 +++++++- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/codechecker.bzl b/src/codechecker.bzl index e4fd8909..3ea88a40 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -372,6 +372,7 @@ def codechecker_test( options = analyze, skip = skip, config = config, + severities = severities, toolchain = toolchain, tags = codechecker_tags, **kwargs diff --git a/src/per_file.bzl b/src/per_file.bzl index d8af6a1b..36da36d2 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -279,29 +279,39 @@ def _per_file_impl(ctx): all_files += [html_parse_dir, codechecker_parse_log, json_parse, txt_parse] + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") ctx.actions.write( - output = ctx.outputs.test_script, + output = launcher, + content = """#!/bin/bash + exec {tool} --mode=Test \ + --data_dir '{codechecker_files}' --severities '{severities}' \ + --clang '{clang}' --clang_tidy '{clang_tidy}' + """.format( + tool = per_file_script.executable.short_path, + codechecker_files = html_parse_dir.short_path + "/..", + severities = " ".join(ctx.attr.severities), + clang = info.clangsa.short_path, + clang_tidy = info.clang_tidy.short_path, + ), is_executable = True, - content = """ - DATA_DIR=$(dirname {dirname}) - # ls -la $DATA_DIR/data - # find $DATA_DIR/data -name *.plist -exec sed -i -e "s|.*execroot/codechecker_bazel/||g" {{}} \\; - # cat $DATA_DIR/data/test-src-lib.cc_clangsa.plist - echo "Running: CodeChecker parse $DATA_DIR/data" - $(realpath {codechecker}) parse $DATA_DIR/data - """.format(dirname = ctx.outputs.test_script.short_path, codechecker = info.codechecker.short_path), - ) - files = depset( - direct = all_files, ) + + # Return test script and all required files run_files = [ - ctx.outputs.test_script, + launcher, ] + info.runfiles.to_list() + all_files + all_runfiles = ctx.runfiles(files = run_files) + + # Add runfiles from the py_binary target (for common.py etc.) + all_runfiles = all_runfiles.merge( + ctx.attr._per_file_script[DefaultInfo].default_runfiles, + ) + return [ DefaultInfo( - files = files, - runfiles = ctx.runfiles(files = run_files), - executable = ctx.outputs.test_script, + files = depset(all_files), + runfiles = all_runfiles, + executable = launcher, ), ] @@ -328,6 +338,10 @@ per_file_test = rule( default = "", #"@platforms//os:linux", doc = "Platform to build for", ), + "severities": attr.string_list( + default = ["HIGH"], + doc = "List of defect severities: HIGH, MEDIUM, LOW, STYLE etc", + ), "skip": attr.string_list( default = [], doc = "List of skip/ignore file rules. " + @@ -354,7 +368,6 @@ per_file_test = rule( } | version_specific_attributes(), outputs = { "compile_commands": "%{name}/compile_commands.json", - "test_script": "%{name}/test_script.sh", }, test = True, toolchains = ["//:toolchain_type"], diff --git a/src/per_file_script.py b/src/per_file_script.py index 9776bb95..8bdd98e0 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -23,7 +23,7 @@ import subprocess from dataclasses import dataclass from common import ( - fail, parse, setup_logging, build_env + check_results, fail, parse, setup_logging, build_env ) @@ -32,6 +32,7 @@ class Config: # pylint: disable=too-many-instance-attributes """Configuration parsed from command-line arguments.""" execution_mode: str + severities: str codechecker_bin: str compile_commands: str codechecker_args: str @@ -70,6 +71,9 @@ def parse_args(argv=None): parser.add_argument( "--file", required=False, help="Path to the file to be analyzed" ) + parser.add_argument( + "--severities", required=False, help="Severities to check" + ) parser.add_argument("--log", required=False, help="Path to the log file") parser.add_argument("--skip", required=False, help="Path to the skip file") parser.add_argument( @@ -99,6 +103,7 @@ def parse_args(argv=None): return Config( execution_mode=args.mode, + severities=args.severities, codechecker_bin=os.path.realpath(args.codechecker or "/"), compile_commands=args.commands, codechecker_args=args.analyze, @@ -304,6 +309,8 @@ def main(): clang=cfg.clang, clang_tidy=cfg.clang_tidy, ) + elif cfg.execution_mode == "Test": + check_results(cfg.data_dir, cfg.log_file, cfg.severities) else: fail( cfg.log_file,