Skip to content

Commit 896956d

Browse files
Your Nameclaude
andcommitted
fix(unvalidated_lookup_chain): suppress dict write FP
Dict writes (collection[var] = value, ctx=Store) never raise KeyError — only reads can. Also treat a write as an implicit guard so subsequent reads of collection[var] on the next lines are not flagged. Eliminates ~17 false positives in tadata-org/fastapi_mcp where properties[param_name] = copy() was flagged then properties[param_name]["title"] = ... was also flagged as unvalidated even though the key was just created. Adds test: test_unvalidated_lookup_chain_dict_write_not_flagged (170 tests total). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8b472e6 commit 896956d

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

failure_mode.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,13 @@ def visit_Subscript(self, node: pyast.Subscript) -> None:
13411341
# defaultdict never raises KeyError on missing keys
13421342
self.generic_visit(node)
13431343
return
1344+
if isinstance(node.ctx, pyast.Store):
1345+
# Dict write: collection[var] = ... — never raises KeyError.
1346+
# Also serves as an implicit guard: var is now in collection for
1347+
# subsequent reads (e.g. collection[var]["sub"] = ... on next line).
1348+
self._guarded.setdefault(var, set()).add(collection)
1349+
self.generic_visit(node)
1350+
return
13441351
guarded_against = self._guarded.get(var, set())
13451352
if collection not in guarded_against:
13461353
results.append(

test_checker.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,37 @@ def aggregate(records):
14401440
assert not v, "annotated defaultdict subscript must not be flagged"
14411441

14421442

1443+
def test_unvalidated_lookup_chain_dict_write_not_flagged(tmp_path):
1444+
"""Dict write d[k] = v where k came from .get() must NOT be flagged.
1445+
Corpus: tadata-org/fastapi_mcp — operation_map[operation_id] = {...}
1446+
where operation_id = operation.get('operationId'). Dict writes never raise
1447+
KeyError; also serves as implicit guard for subsequent reads."""
1448+
_write_src(
1449+
tmp_path,
1450+
"convert.py",
1451+
"""
1452+
def build_map(operations):
1453+
operation_map = {}
1454+
properties = {}
1455+
for operation in operations:
1456+
operation_id = operation.get('operationId')
1457+
if not operation_id:
1458+
continue
1459+
# Write — must NOT flag (no KeyError possible on dict write)
1460+
operation_map[operation_id] = {'method': 'get'}
1461+
param_schema = operation.get('schema', {})
1462+
param_name = operation.get('name')
1463+
if param_name:
1464+
properties[param_name] = param_schema.copy()
1465+
# Subsequent read after write above — still NOT a bug
1466+
properties[param_name]['title'] = param_name
1467+
""",
1468+
)
1469+
violations = check_codebase(tmp_path)
1470+
v = [v for v in violations if v.context == "unvalidated_lookup_chain"]
1471+
assert not v, f"dict write d[k]=v must not be flagged as unvalidated_lookup_chain: {v}"
1472+
1473+
14431474
def test_optional_dereference_get_with_default_not_flagged(tmp_path):
14441475
""".get(key, default) with a non-None default must not be flagged."""
14451476
_write_src(

0 commit comments

Comments
 (0)