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:
ed
2026-06-22 12:20:32 -04:00
parent ac2e68542f
commit 077149011b
27 changed files with 2087 additions and 1263 deletions
+7 -7
View File
@@ -341,7 +341,7 @@ def test_p1_pass_finds_producer_of_T() -> None:
''')
tree = ast.parse(source)
producers = P1_pass(tree, file="synthetic.py")
assert ("send_result", "Metadata", "producer", "high") in producers
assert ("send_result", "Metadata", "producer", "high", 2) in producers
def test_p1_pass_finds_producer_of_Result_T() -> None:
"""P1 detects a function whose return annotation is Result[T] (producer of T)."""
@@ -351,7 +351,7 @@ def test_p1_pass_finds_producer_of_Result_T() -> None:
''')
tree = ast.parse(source)
producers = P1_pass(tree, file="synthetic.py")
assert ("fetch", "FileItems", "producer", "high") in producers
assert ("fetch", "FileItems", "producer", "high", 2) in producers
def test_p1_pass_skips_non_annotated_return() -> None:
"""P1 returns [] for functions without return annotations."""
@@ -371,7 +371,7 @@ def test_p2_pass_finds_consumer_of_T() -> None:
''')
tree = ast.parse(source)
consumers = P2_pass(tree, file="synthetic.py")
assert ("process", "Metadata", "consumer", "high") in consumers
assert ("process", "Metadata", "consumer", "high", 2) in consumers
def test_p2_pass_finds_consumer_of_list_T() -> None:
"""P2 detects a function whose parameter is list[T] (consumer of T)."""
@@ -381,7 +381,7 @@ def test_p2_pass_finds_consumer_of_list_T() -> None:
''')
tree = ast.parse(source)
consumers = P2_pass(tree, file="synthetic.py")
assert ("aggregate", "FileItems", "consumer", "high") in consumers
assert ("aggregate", "FileItems", "consumer", "high", 2) in consumers
def test_p2_pass_skips_untyped_parameter() -> None:
"""P2 returns [] for parameters without type annotations."""
@@ -401,7 +401,7 @@ def test_p3_pass_finds_consumer_via_subscript() -> None:
''')
tree = ast.parse(source)
accesses = P3_pass(tree, file="synthetic.py", type_registry={})
assert ("process", "path", "subscript", 1) in accesses
assert ("process", "path", "subscript", 1, 2) in accesses
def test_p3_pass_finds_consumer_via_attribute() -> None:
"""P3 detects a function that reads entry.attr; returns (function, attr, kind, count)."""
@@ -411,7 +411,7 @@ def test_p3_pass_finds_consumer_via_attribute() -> None:
''')
tree = ast.parse(source)
accesses = P3_pass(tree, file="synthetic.py", type_registry={})
assert ("process", "path", "attribute", 1) in accesses
assert ("process", "path", "attribute", 1, 2) in accesses
def test_p3_pass_counts_multiple_accesses() -> None:
"""P3 counts multiple accesses to the same key within a single function."""
@@ -423,7 +423,7 @@ def test_p3_pass_counts_multiple_accesses() -> None:
''')
tree = ast.parse(source)
accesses = P3_pass(tree, file="synthetic.py", type_registry={})
path_count = sum(c for fn, k, kind, c in accesses if fn == "process" and k == "path")
path_count = sum(c for fn, k, kind, c, line in accesses if fn == "process" and k == "path")
assert path_count == 2
def test_build_pcg_returns_result() -> None: