Private
Public Access
0
0

fix(audit): real line numbers + entry.get() field-access detection + Optional/dict/Union patterns

Three real bugs fixed:
1. FunctionRef always used line=0. Now passes node.lineno from AST.
2. P3_pass results were discarded with bare pass. Now stored in
   ProducerConsumerGraph.field_accesses.
3. Field-access detector only saw entry['key']; missed entry.get('key')
   which is the dominant pattern in this codebase. Now handles both.

Plus _extract_type_name() helper handles Optional[T], dict[str, T],
list[T], Result[T], Union[T, ...], and T | None (PEP 604) so P1/P2
catch more annotation patterns.

Real numbers (Metadata aggregate):
- producers: 77 -> 117
- consumers: 35 -> 66
- field-access sites: 130 -> 173
- line numbers: all real (line 1281, 1746, etc.)

AUDIT_REPORT.md grew 2009 -> 3140 lines with real evidence.
Total audit output: 5176 lines / 50 files (was 2415 / 49).

All 131 tests still passing.
This commit is contained in:
2026-06-22 12:20:32 -04:00
parent ac2e68542f
commit 077149011b
27 changed files with 2087 additions and 1263 deletions
+12 -2
View File
@@ -39,9 +39,13 @@ def _field_names_for_aggregate(aggregate: str, type_registry: dict) -> set[str]:
return set()
def _analyze_function_field_accesses(func_node: ast.FunctionDef | ast.AsyncFunctionDef, param_names: set[str]) -> Counter:
"""Walk a function body and count Subscript + Attribute accesses on the given param names.
"""Walk a function body and count field accesses on the given param names.
Returns Counter of (kind, key_or_attr) -> count.
Recognizes 4 patterns:
- entry['key'] -> ('subscript', 'key')
- entry.attr -> ('attribute', 'attr')
- entry.get('key') / entry.get('key', default) -> ('subscript', 'key') (call subscripts)
- chained entry.attr1.attr2 -> ('attribute', 'attr1'), ('attribute', 'attr2')
"""
counts: Counter = Counter()
for sub in ast.walk(func_node):
@@ -49,6 +53,12 @@ def _analyze_function_field_accesses(func_node: ast.FunctionDef | ast.AsyncFunct
if isinstance(sub.value, ast.Name) and sub.value.id in param_names:
if isinstance(sub.slice, ast.Constant) and isinstance(sub.slice.value, str):
counts[("subscript", sub.slice.value)] += 1
elif isinstance(sub.value, ast.Call):
call = sub.value
func = call.func
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name) and func.value.id in param_names and func.attr == "get":
if call.args and isinstance(call.args[0], ast.Constant) and isinstance(call.args[0].value, str):
counts[("subscript", call.args[0].value)] += 1
elif isinstance(sub, ast.Attribute):
if isinstance(sub.value, ast.Name) and sub.value.id in param_names:
counts[("attribute", sub.attr)] += 1