Files
scst/scripts/analyzer_policy.py
Gleb Chesnokov 74a2e4bac3 scripts: Add the SCST code style linter
Document SCS.1 and expose make scs_lint for the common C source/header
scope, including userspace and KUnit. Adapt engine's OCS.7.5 check to
SCST_KUNIT_VISIBLE, SCST_KUNIT_VISIBLE_INLINE and VISIBLE_IF_KUNIT.
Require each annotation to occupy its own line; leave other function
attributes and source warnings unchanged.

Use Tree-sitter to distinguish code from comments, literals and
preprocessor directive contents while checking every conditional
branch. Pin the parser dependencies and run them through uv.

Support whole-tree, DIFFBR and explicit-file selection without new
exclusions or diagnostic exceptions. Retain findings and checked-file
records even when a later input fails. Add isolated rule, diff-mode,
coverage and read-failure fixtures with a separate test target.
2026-10-20 14:16:08 +03:00

210 lines
9.4 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", "checkpatch", "scs_lint")
BASIC_POLICIES = ("coccicheck", "cppcheck", "checkpatch", "scs_lint")
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())