Files
scst/scripts/lint_scope.py
T
Gleb Chesnokov c1e2f41a49 scripts: Store shared analyzer scope as reviewed policy
Move the source roots, file types and input exclusions from lint_scope.py
into policy/common.json. Keep the existing scope, including KUnit sources,
the newer QLogic target and the exclusions for the legacy QLogic tree,
upstream QLogic sources, compiler probes and generated module metadata.

Adapt engine's reviewed-data and strict-loader approach to SCST. Require
reasons for input exclusions and reject malformed, missing or ambiguous
policy before selecting files. Do not import engine's exclusions or add
environment-based filtering or diagnostic suppression.

Use this policy for full, diff and explicit-file selection. Preserve both
lexical and resolved path checks, single-commit diff bases and index-based
selection. Treat policy paths literally in Git pathspecs as well as Python.
Add isolated fixtures for schema failures and policy-driven selection.
2026-10-19 21:38:56 +03:00

76 lines
3.2 KiB
Python

"""Shared, tracked C source scope for SCST analyzers."""
import os
import subprocess
from pathlib import Path
from analyzer_policy import load_policy
def in_scope(path: Path, root: Path) -> bool:
policy = load_policy()
try:
# Neither an alias nor its destination may widen the source scope.
names = (Path(os.path.abspath(path)).relative_to(Path(os.path.abspath(root))).as_posix(),
path.resolve().relative_to(root.resolve()).as_posix())
except ValueError:
return False
return all(name.startswith(tuple(directory + "/" for directory in policy["source_roots"]))
and Path(name).suffix in policy["source_suffixes"]
and not any(name.endswith(rule["suffix"]) for rule in policy["exclude_suffixes"])
and not any(name.startswith(rule["path"] + "/")
for rule in policy["exclude_directories"])
for name in names)
def glob_literal(value: str) -> str:
"""Quote literal policy paths for Git's glob pathspecs."""
return "".join({"[": "[[]", "*": "[*]", "?": "[?]"}.get(char, char) for char in value)
def git_pathspecs() -> list[str]:
policy = load_policy()
return [*(f":(top,glob){glob_literal(directory)}/**/*{suffix}"
for directory in policy["source_roots"] for suffix in policy["source_suffixes"]),
*(f":(top,exclude,glob)**/*{rule['suffix']}" for rule in policy["exclude_suffixes"]),
*(f":(top,exclude,glob){glob_literal(rule['path'])}/**"
for rule in policy["exclude_directories"])]
def git_names(root: Path, *arguments: str) -> list[str]:
output = subprocess.check_output(["git", "-C", str(root), *arguments])
return [os.fsdecode(name) for name in output.split(b"\0") if name]
def tracked_sources(root: Path) -> list[Path]:
roots = [f":(top,literal){directory}" for directory in load_policy()["source_roots"]]
names = git_names(root, "ls-files", "--cached", "-z", "--", *roots)
return sorted({root / name for name in names
if in_scope(root / name, root) and (root / name).is_file()})
def resolve_base(root: Path, base: str) -> str:
"""Require one commit before passing a user-supplied base to Git diff."""
return subprocess.check_output(
["git", "-C", str(root), "rev-parse", "--verify", "--end-of-options",
f"{base}^{{commit}}"], text=True).strip()
def changed_sources(root: Path, base: str) -> list[Path]:
revision = resolve_base(root, base)
names = git_names(root, "diff", "--no-renames", "--name-only", "-z",
"--diff-filter=d", revision, "--", *git_pathspecs())
return sorted({root / name for name in names
if in_scope(root / name, root) and (root / name).is_file()})
def select_sources(root: Path, files: list[Path], base: str | None) -> list[Path]:
if files and base:
raise ValueError("select files or a Git base, not both")
if files:
for path in files:
if not path.is_file() or not in_scope(path, root):
raise ValueError(f"not a project C source/header: {path}")
return sorted({path.resolve() for path in files})
return changed_sources(root, base) if base else tracked_sources(root)