literal() in tests/test_fleet_page.py walks the tree for Assign, AugAssign and AnnAssign, and separately flags subscript assignment and mutating method calls. That kills the five shapes in #99. These still survive, each leaving the test reading the original literal while the module uses something else:
| mutant |
why it survives |
worth closing |
VOC, _X = [...], 1 |
tuple-unpack target is a Tuple, not a Name, so getattr(target, "id") is None |
yes — VOC, PREF = load() is a natural refactor |
del VOC[0] / del VOC[:] |
ast.Delete is not examined at all |
yes — cheap to add |
VOC[0] += "X" |
the AugAssign branch tests node.target.id, so a Subscript target is skipped. Asymmetric: plain VOC[0] = is caught |
moderate |
VOC[0], _x = "BOGUS", 1 |
subscript store nested inside a Tuple target |
low |
print((VOC := [...])) |
ast.NamedExpr not handled |
low |
for VOC in [...], with ... as VOC at module level |
neither is Assign |
low |
globals()["VOC"] = ..., exec(...), list.append(VOC, x) |
dynamic |
no — accept |
Suggested fix
Collect stores rather than top-level target ids, which covers the first five in one pass:
for node in ast.walk(tree):
targets = (node.targets if isinstance(node, (ast.Assign, ast.Delete))
else [node.target] if isinstance(node, (ast.AugAssign, ast.AnnAssign, ast.NamedExpr))
else [])
for target in targets:
for sub in ast.walk(target):
if isinstance(sub, ast.Name) and sub.id == name and isinstance(sub.ctx, (ast.Store, ast.Del)):
...
elif isinstance(sub, ast.Subscript) and getattr(sub.value, "id", None) == name:
mutations.append("subscript assignment")
The ctx guard matters: in VOC[0] = x the Name node is a Load, so without it the subscript store would be miscounted as a rebinding.
Also worth a line each
assertGreater(len(value), 10) does not check the value is a list. VOC = "abcdefghijklmnop" — a 16-character string — survives, as does a duplicated entry inside the literal. assertIsInstance(value, list) plus a duplicate check is cheap.
- A bare
VOC: list with no value makes ast.literal_eval(None) raise ValueError: malformed node or string rather than a clean assertion. It still fails, just untidily.
None of these shapes exists in either script today. The real fix remains #97 — make the modules importable so the tests read the actual objects instead of parsing text for them; every entry above then becomes moot.
Found by an adversarial review of #100.
literal()intests/test_fleet_page.pywalks the tree forAssign,AugAssignandAnnAssign, and separately flags subscript assignment and mutating method calls. That kills the five shapes in #99. These still survive, each leaving the test reading the original literal while the module uses something else:VOC, _X = [...], 1Tuple, not aName, sogetattr(target, "id")is NoneVOC, PREF = load()is a natural refactordel VOC[0]/del VOC[:]ast.Deleteis not examined at allVOC[0] += "X"AugAssignbranch testsnode.target.id, so aSubscripttarget is skipped. Asymmetric: plainVOC[0] =is caughtVOC[0], _x = "BOGUS", 1Tupletargetprint((VOC := [...]))ast.NamedExprnot handledfor VOC in [...],with ... as VOCat module levelAssignglobals()["VOC"] = ...,exec(...),list.append(VOC, x)Suggested fix
Collect stores rather than top-level target ids, which covers the first five in one pass:
The
ctxguard matters: inVOC[0] = xtheNamenode is a Load, so without it the subscript store would be miscounted as a rebinding.Also worth a line each
assertGreater(len(value), 10)does not check the value is a list.VOC = "abcdefghijklmnop"— a 16-character string — survives, as does a duplicated entry inside the literal.assertIsInstance(value, list)plus a duplicate check is cheap.VOC: listwith no value makesast.literal_eval(None)raiseValueError: malformed node or stringrather than a clean assertion. It still fails, just untidily.None of these shapes exists in either script today. The real fix remains #97 — make the modules importable so the tests read the actual objects instead of parsing text for them; every entry above then becomes moot.
Found by an adversarial review of #100.