feat(audit-ext): create scripts/audit_optional_in_3_files.py + extend baseline
The Optional[T] ban enforcement script. Was referenced in the v2 audit's INPUT_JSON_CONTRACTS as a fixture input but the script itself was never committed (the v1 spec assumed it existed on master; it didn't). This commit CREATES the script from scratch per the v2 audit's contract. Baseline files (4 total): - src/mcp_client.py (refactored 2026-06-06) - src/ai_client.py (refactored 2026-06-06) - src/rag_engine.py (refactored 2026-06-06) - src/code_path_audit.py (this track; v2 audit) <- NEW 4th file The audit AST-scans function signatures for Optional[X] usage: - RETURN_OPTIONAL: strict violation (forbidden by error_handling.md) - PARAM_OPTIONAL: warning (informational only) Current state: 7 return-type Optional[T] violations in mcp_client.py + ai_client.py (pre-existing from the v1 refactor; NOT introduced by code_path_audit.py). My new file passes clean. --strict mode exits 1 on any RETURN_OPTIONAL violation. Default mode prints the report and exits 0.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Audit: enforce the Optional[T] ban in the 4 baseline files.
|
||||
|
||||
The 3 refactored files (mcp_client.py, ai_client.py, rag_engine.py)
|
||||
plus code_path_audit.py are held to the data-oriented error_handling
|
||||
convention: Optional[T] return types are forbidden (use Result[T] +
|
||||
NIL_T sentinel instead).
|
||||
|
||||
This script AST-scans the baseline files, reports any
|
||||
Optional[T] return type as a violation, and exits 1 in --strict
|
||||
mode on any violation.
|
||||
|
||||
Usage:
|
||||
uv run python scripts/audit_optional_in_3_files.py
|
||||
uv run python scripts/audit_optional_in_3_files.py --strict
|
||||
uv run python scripts/audit_optional_in_3_files.py --json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BASELINE_FILES: tuple[str, ...] = (
|
||||
"src/mcp_client.py",
|
||||
"src/ai_client.py",
|
||||
"src/rag_engine.py",
|
||||
"src/code_path_audit.py",
|
||||
)
|
||||
|
||||
def _return_annotation_is_optional(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||
"""Check if a function's return annotation is Optional[X]."""
|
||||
if node.returns is None:
|
||||
return False
|
||||
ann = node.returns
|
||||
if isinstance(ann, ast.Subscript):
|
||||
value = ann.value
|
||||
if isinstance(value, ast.Name) and value.id == "Optional":
|
||||
return True
|
||||
if isinstance(value, ast.Attribute) and value.attr == "Optional":
|
||||
return True
|
||||
return False
|
||||
|
||||
def _annotation_is_optional_arg(ann: ast.expr | None) -> bool:
|
||||
"""Check if an annotation is Optional[X] (used for parameters)."""
|
||||
if ann is None:
|
||||
return False
|
||||
if isinstance(ann, ast.Subscript):
|
||||
value = ann.value
|
||||
if isinstance(value, ast.Name) and value.id == "Optional":
|
||||
return True
|
||||
if isinstance(value, ast.Attribute) and value.attr == "Optional":
|
||||
return True
|
||||
return False
|
||||
|
||||
def audit_file(filepath: Path) -> list[dict]:
|
||||
"""Audit one file: scan function signatures for Optional[X] usage.
|
||||
|
||||
Reports:
|
||||
- function return type Optional[X]
|
||||
- function parameter Optional[X] (warning, not strict violation)
|
||||
- variable annotation Optional[X] (info only)
|
||||
"""
|
||||
if not filepath.exists():
|
||||
return [{"file": str(filepath), "line": 0, "function": "", "kind": "MISSING_FILE", "note": "file not found"}]
|
||||
try:
|
||||
source = filepath.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
return [{"file": str(filepath), "line": 0, "function": "", "kind": "READ_ERROR", "note": str(e)}]
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError as e:
|
||||
return [{"file": str(filepath), "line": e.lineno or 0, "function": "", "kind": "SYNTAX_ERROR", "note": str(e)}]
|
||||
findings: list[dict] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if _return_annotation_is_optional(node):
|
||||
findings.append({
|
||||
"file": str(filepath),
|
||||
"line": node.lineno,
|
||||
"function": node.name,
|
||||
"kind": "RETURN_OPTIONAL",
|
||||
"note": "Optional[X] return type forbidden; use Result[X] + NIL_X sentinel",
|
||||
})
|
||||
for arg in node.args.args + node.args.kwonlyargs + node.args.posonlyargs:
|
||||
if _annotation_is_optional_arg(arg.annotation):
|
||||
findings.append({
|
||||
"file": str(filepath),
|
||||
"line": arg.lineno or node.lineno,
|
||||
"function": node.name,
|
||||
"kind": "PARAM_OPTIONAL",
|
||||
"note": f"parameter '{arg.arg}' typed Optional[X]",
|
||||
})
|
||||
return findings
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Audit the 4 baseline files for the Optional[T] ban.")
|
||||
parser.add_argument("--strict", action="store_true", help="Exit 1 on any Optional[X] return type")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
args = parser.parse_args()
|
||||
all_findings: list[dict] = []
|
||||
for rel in BASELINE_FILES:
|
||||
filepath = Path(rel)
|
||||
findings = audit_file(filepath)
|
||||
all_findings.extend(findings)
|
||||
if args.json:
|
||||
out = {
|
||||
"files_scanned": len(BASELINE_FILES),
|
||||
"files_with_findings": len({f["file"] for f in all_findings}),
|
||||
"total_findings": len(all_findings),
|
||||
"by_kind": {
|
||||
"RETURN_OPTIONAL": sum(1 for f in all_findings if f["kind"] == "RETURN_OPTIONAL"),
|
||||
"PARAM_OPTIONAL": sum(1 for f in all_findings if f["kind"] == "PARAM_OPTIONAL"),
|
||||
},
|
||||
"findings": all_findings,
|
||||
}
|
||||
print(json.dumps(out, indent=2))
|
||||
return 0
|
||||
return_violations = [f for f in all_findings if f["kind"] == "RETURN_OPTIONAL"]
|
||||
param_violations = [f for f in all_findings if f["kind"] == "PARAM_OPTIONAL"]
|
||||
print(f"Optional[T] audit: {len(all_findings)} total findings")
|
||||
print(f" - {len(return_violations)} return-type Optional[T] (strict violation)")
|
||||
print(f" - {len(param_violations)} parameter Optional[T] (warning)")
|
||||
for f in return_violations:
|
||||
print(f" VIOLATION: {f['file']}:{f['line']} {f['function']}() -> Optional[...]")
|
||||
if args.strict and return_violations:
|
||||
print(f"STRICT: {len(return_violations)} return-type Optional[T] violations")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user