Files
ai-exchange/scripts/run_script_static_checks.py
2026-05-18 22:30:34 +08:00

152 lines
5.1 KiB
Python

from pathlib import Path
import ast
import json
import os
import sys
if __package__ in {None, ""}:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.run_secret_static_checks import run_secret_static_checks
ROOT = Path(__file__).resolve().parents[1]
SCRIPT_DIR = ROOT / "scripts"
FORBIDDEN_PATTERNS = (
"env " + "= env or " + "os.environ",
"env or " + "os.environ",
)
def run_script_static_checks(env=None, script_dir: Path = SCRIPT_DIR) -> dict:
env = os.environ if env is None else env
findings = []
for path in sorted(script_dir.glob("*.py")):
findings.extend(check_script_file(path))
findings.extend(run_secret_static_checks(env=env)["findings"])
return {
"passed": len(findings) == 0,
"script_count": len(list(script_dir.glob("*.py"))),
"findings": findings,
}
def check_script_file(path: Path) -> list[dict]:
findings = []
source = path.read_text(encoding="utf-8")
tree = None
try:
tree = ast.parse(source, filename=str(path))
except SyntaxError as exc:
findings.append(
{
"file": display_path(path),
"line": exc.lineno or 1,
"pattern": "python_syntax",
"reason": "script_python_syntax_error",
}
)
if tree is not None:
findings.extend(find_python39_incompatible_annotations(tree, path))
for line_number, line in enumerate(source.splitlines(), start=1):
for pattern in FORBIDDEN_PATTERNS:
if pattern in line:
findings.append(
{
"file": display_path(path),
"line": line_number,
"pattern": pattern,
"reason": forbidden_pattern_reason(pattern),
}
)
if tree is not None and has_main_function(tree) and "raise SystemExit(main(" not in source:
findings.append(
{
"file": display_path(path),
"line": 1,
"pattern": "raise SystemExit(main())",
"reason": "automation_entrypoint_must_preserve_exit_code",
}
)
return findings
def forbidden_pattern_reason(pattern: str) -> str:
return "empty_env_must_not_fall_back_to_ambient_environment"
def has_main_function(tree: ast.AST) -> bool:
return any(isinstance(node, ast.FunctionDef) and node.name == "main" for node in ast.walk(tree))
def find_python39_incompatible_annotations(tree: ast.AST, path: Path) -> list[dict]:
findings = []
def inspect_annotation(annotation) -> None:
if annotation is not None and has_pep604_optional_annotation(annotation):
findings.append(
{
"file": display_path(path),
"line": getattr(annotation, "lineno", 1),
"pattern": "PEP604 Optional",
"reason": "python39_incompatible_pep604_optional_annotation",
}
)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
inspect_annotation(node.returns)
for arg in list(node.args.args) + list(node.args.kwonlyargs) + list(node.args.posonlyargs):
inspect_annotation(arg.annotation)
if node.args.vararg:
inspect_annotation(node.args.vararg.annotation)
if node.args.kwarg:
inspect_annotation(node.args.kwarg.annotation)
elif isinstance(node, ast.AnnAssign):
inspect_annotation(node.annotation)
return findings
def has_pep604_optional_annotation(annotation) -> bool:
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
return is_none_annotation(annotation.left) or is_none_annotation(annotation.right)
return any(has_pep604_optional_annotation(child) for child in ast.iter_child_nodes(annotation))
def is_none_annotation(node) -> bool:
return isinstance(node, ast.Constant) and node.value is None
def display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return path.name
def main(env=None) -> int:
env = os.environ if env is None else env
result = run_script_static_checks(env=env)
if _get_bool(env, "SCRIPT_CHECKS_OUTPUT_JSON", False):
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
else:
print(f"Script static checks passed: {result['passed']}")
print(f"Script files checked: {result['script_count']}")
for finding in result["findings"]:
print(
f"{finding['file']}:{finding['line']} "
f"{finding['reason']} pattern={finding['pattern']}"
)
return 0 if result["passed"] else 2
def _get_bool(env, key: str, default: bool) -> bool:
value = env.get(key)
if value is None or value == "":
return default
return str(value).strip().lower() in {"1", "true", "yes", "on"}
if __name__ == "__main__":
raise SystemExit(main())