Files
scst/scripts/analyzer_policy.py
T
Gleb Chesnokov 6c3405f5fe scripts: Add Cppcheck with native build contexts
Expose make cppcheck using freshly captured kernel CHECK commands and
real userspace compiler invocations. Keep their include paths and
definitions unchanged and analyze kernel and userspace contexts
separately.

Use the shared source scope without additional input exclusions, project
models or diagnostic suppressions. Relevant diffs trigger full
configured analysis; KUnit coverage follows the actual build
configuration.

Retain build logs, compilation databases, native XML and checker
coverage. Separate source findings from parser failures, missing inputs
and incomplete builds, with isolated report and capture fixtures.
2026-10-19 21:38:56 +03:00

210 lines
9.3 KiB
Python

"""Inspect the shared analyzer input policy without invoking an analyzer."""
import argparse
import json
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
POLICY_PATH = Path(__file__).resolve().parent / "policy/common.json"
SCHEMA = {
"version": int,
"source_roots": [str],
"source_suffixes": [str],
"exclude_directories": [{"path": str, "reason": str}],
"exclude_suffixes": [{"suffix": str, "reason": str}],
"notes": [str],
}
KBUILD_SCHEMA = {
"version": int,
"exclude_directories": [{"path": str, "reason": str}],
"source_exceptions": [{"file": str, "function": str, "message": str, "reason": str}],
"kernel_exceptions": [{"header": str, "message": str, "reason": str}],
}
BASIC_SCHEMA = {"version": int, "notes": [str]}
POLICIES = ("common", "sparse", "smatch", "coccicheck", "cppcheck")
BASIC_POLICIES = ("coccicheck", "cppcheck")
def relative_path(value: str, location: str):
if (value.startswith("/") or "\\" in value or
any(part in {"", ".", ".."} for part in value.split("/")) or
any(ord(char) < 32 or ord(char) == 127 for char in value)):
raise ValueError(f"{location}: expected a normalized repository-relative path: {value!r}")
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate policy key: {key}")
result[key] = value
return result
def validate(value, schema, location):
"""Reject unknown, missing and mistyped fields, including nested rules."""
if isinstance(schema, dict):
if not isinstance(value, dict) or value.keys() != schema.keys():
raise ValueError(f"{location}: expected fields {', '.join(schema)}")
for key, child in schema.items():
validate(value[key], child, f"{location}.{key}")
elif isinstance(schema, list):
if not isinstance(value, list):
raise ValueError(f"{location}: expected a list")
for index, item in enumerate(value):
validate(item, schema[0], f"{location}[{index}]")
elif type(value) is not schema:
raise ValueError(f"{location}: expected {schema.__name__}")
elif schema is str and not value.strip():
raise ValueError(f"{location}: empty value")
def read_policy(path: Path, name="common") -> dict:
if name not in POLICIES:
raise ValueError(f"unknown policy: {name}")
policy = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_object)
schema = BASIC_SCHEMA if name in BASIC_POLICIES else (
SCHEMA if name == "common" else KBUILD_SCHEMA)
validate(policy, schema, str(path))
if policy["version"] != 1:
raise ValueError(f"{path}: unsupported policy version")
if name in BASIC_POLICIES:
return policy
if name != "common":
for key, fields in (("exclude_directories", ("path",)),
("source_exceptions", ("file", "function", "message")),
("kernel_exceptions", ("header", "message"))):
entries = [tuple(rule[field] for field in fields) for rule in policy[key]]
if len(entries) != len(set(entries)):
raise ValueError(f"{path}: duplicate {key} entry")
for rule in policy[key]:
relative_path(rule[fields[0]], str(path))
if name == "sparse" and policy["source_exceptions"]:
raise ValueError(f"{path}: function-level exceptions are supported only for Smatch")
for rule in policy["kernel_exceptions"]:
if not rule["header"].startswith("include/"):
raise ValueError(f"{path}: kernel header must be beneath include/")
return policy
for key in ("source_roots", "source_suffixes"):
if not policy[key] or len(policy[key]) != len(set(policy[key])):
raise ValueError(f"{path}: {key} must be nonempty and unique")
if set(policy["source_suffixes"]) - {".c", ".h"}:
raise ValueError(f"{path}: only C sources and headers are supported")
for key, field in (("exclude_directories", "path"), ("exclude_suffixes", "suffix")):
entries = [rule[field] for rule in policy[key]]
if len(entries) != len(set(entries)):
raise ValueError(f"{path}: duplicate {key} entry")
directories = policy["source_roots"] + [r["path"] for r in policy["exclude_directories"]]
for directory in directories:
relative_path(directory, str(path))
for rule in policy["exclude_suffixes"]:
suffix = rule["suffix"]
if not suffix.startswith(".") or any(char not in ".abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
for char in suffix):
raise ValueError(f"{path}: expected a literal filename suffix: {suffix!r}")
return policy
@lru_cache(maxsize=None)
def load_policy(name="common") -> dict:
"""Read once per process; invalid or missing policy must stop selection."""
if name not in POLICIES:
raise ValueError(f"unknown policy: {name}")
return read_policy(POLICY_PATH if name == "common" else POLICY_PATH.with_name(name + ".json"),
name)
def source_policy(name="common") -> dict:
common = load_policy()
if name in BASIC_POLICIES:
load_policy(name)
return common
if name == "common":
return common
return {**common, "exclude_directories": common["exclude_directories"] +
load_policy(name)["exclude_directories"]}
def text_report(result: dict, action: str) -> str:
lines = [f"Policy: {result['policy']} ({result['policy_file']})"]
if action == "validate":
return "\n".join([*lines, "Policy is valid."])
lines.append("Coverage: shared input selection only; no analyzer has run.")
if action == "files":
lines.append(f"Eligible tracked files: {result['count']}")
lines.extend(result["selected_tracked_files"])
if not result["count"]:
lines.append("No eligible tracked inputs.")
return "\n".join(lines)
policy = result["effective_source_policy"]
lines += [f"Included roots: {', '.join(policy['source_roots'])}",
f"File types: {', '.join(policy['source_suffixes'])}",
"Excluded directories:"]
lines += [f" {rule['path']}: {rule['reason']}" for rule in policy["exclude_directories"]]
if not policy["exclude_directories"]:
lines.append(" none")
lines.append("Excluded filename suffixes:")
lines += [f" {rule['suffix']}: {rule['reason']}" for rule in policy["exclude_suffixes"]]
if not policy["exclude_suffixes"]:
lines.append(" none")
lines.append("Environment overrides for input exclusions: none.")
lines.extend(policy["notes"])
if result["policy"] in BASIC_POLICIES:
lines.append("Additional input exclusions and diagnostic exceptions: none.")
lines.extend(result["configured_policy"]["notes"])
elif result["policy"] != "common":
native = result["configured_policy"]
lines.append("Source warning exceptions (exact file, function and message):")
lines += [f" {r['file']} {r['function']}(): {r['message']} — {r['reason']}"
for r in native["source_exceptions"]] or [" none"]
lines.append("External kernel warning exceptions (resolved from the build context):")
lines += [f" {r['header']}: {r['message']} — {r['reason']}"
for r in native["kernel_exceptions"]] or [" none"]
lines.append("Kbuild C=2; unlimited warnings; full module build and all its findings.")
if result["policy"] == "smatch":
lines.append("Native kernel model (-p=kernel); installed data is used unchanged.")
else:
lines.append("Diagnostic filtering is not defined by the common input policy.")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("show", "files", "validate"))
parser.add_argument("policy", choices=POLICIES, nargs="?", default="common")
parser.add_argument("--format", choices=("text", "json"), default="text")
args = parser.parse_args()
try:
policy = load_policy(args.policy)
scope = source_policy(args.policy)
result = {"policy": args.policy, "policy_file": f"scripts/policy/{args.policy}.json"}
if args.action == "validate":
result["valid"] = True
else:
result["coverage"] = "Shared input selection only; no analyzer has run."
if args.action == "show":
result["configured_policy"] = policy
result["effective_source_policy"] = scope
else:
from lint_scope import tracked_sources
root = Path(__file__).resolve().parent.parent
files = [path.relative_to(root).as_posix()
for path in tracked_sources(root, args.policy)]
result.update(selected_tracked_files=files, count=len(files))
print(json.dumps(result, indent=2) if args.format == "json"
else text_report(result, args.action))
except (OSError, ValueError, subprocess.CalledProcessError) as error:
print(f"analyzer policy: {error}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
sys.dont_write_bytecode = True
sys.exit(main())