mirror of
https://github.com/SCST-project/scst.git
synced 2026-10-22 06:02:33 +00:00
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.
210 lines
9.4 KiB
Python
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())
|