Summary
no-json-stringify-set-or-map flags JSON.stringify(x) when x was previously seen as const x = new Set(...)/new Map(...). The tracking is a single Map<string, "Set"|"Map"> created once per create(context) call (i.e. once per linted file) and keyed purely by variable name string, with no scope resolution at all.
Where
eslint-factory/src/rules/no-json-stringify-set-or-map.ts:
trackedVars declared once inside create(context) (file-wide, not per-block/per-function).
VariableDeclarator visitor: trackedVars.set(node.id.name, kind) — name-only key, no scope/binding captured.
CallExpression visitor for JSON.stringify(identifier): trackedVars.get(firstArg.name) — a pure name lookup, no check that the specific binding at the call site is the same binding that was tracked.
Impact
If a single file declares two different const-bound variables with the same name in different lexical scopes (e.g. two different functions each with their own const seen = new Set() / const cache = new Map()), the flat map only ever holds whichever same-named entry was most recently visited in traversal order. Depending on traversal/declaration order this produces either:
- a false positive: an unrelated non-Set/Map variable gets flagged because a same-named Set/Map from a different scope overwrote the tracking entry, or
- a false negative: a real
JSON.stringify(realSetVar) call is missed because a different, same-named, non-Set/Map variable overwrote the entry first.
Grounding
This is a soundness gap in the tracking design, not (yet) confirmed against a live same-file/same-name/different-scope collision — a targeted grep across actions/setup/js/**/*.cjs for repeated Set/Map variable names (seen, cache, map, ids, groups, etc.) combined with JSON.stringify(<same name>) calls found no live occurrence today. The codebase's current Set/Map variable names happen not to collide within any single file. The bug is real in the rule's logic regardless — it is a latent regression risk, and the fix is cheap (track bindings/scope, e.g. via context.sourceCode.getScope(node) + variable resolution, matching the scope-aware binding-resolution pattern already used elsewhere in this rule family, e.g. try-catch-rule-utils.ts's scope-walking resolvers) rather than name-string matching.
Ask
- Replace the name-keyed
trackedVars map with scope-aware binding resolution (resolve the Identifier at the JSON.stringify(x) call site to its actual variable/definition via ESLint scope analysis, then check whether that specific binding's initializer was new Set(...)/new Map(...)), instead of a flat per-file name map.
- Add a regression test: two functions in the same file, each declaring
const seen = new Set() / const seen = { other: true } (or similar) with the same name in different scopes, asserting only the actual Set/Map reference is flagged.
Acceptance criteria
Generated by 🤖 ESLint Refiner · agent · 280.9 AIC · ⌖ 9.73 AIC · ⊞ 5.3K · ◷
Summary
no-json-stringify-set-or-mapflagsJSON.stringify(x)whenxwas previously seen asconst x = new Set(...)/new Map(...). The tracking is a singleMap<string, "Set"|"Map">created once percreate(context)call (i.e. once per linted file) and keyed purely by variable name string, with no scope resolution at all.Where
eslint-factory/src/rules/no-json-stringify-set-or-map.ts:trackedVarsdeclared once insidecreate(context)(file-wide, not per-block/per-function).VariableDeclaratorvisitor:trackedVars.set(node.id.name, kind)— name-only key, no scope/binding captured.CallExpressionvisitor forJSON.stringify(identifier):trackedVars.get(firstArg.name)— a pure name lookup, no check that the specific binding at the call site is the same binding that was tracked.Impact
If a single file declares two different
const-bound variables with the same name in different lexical scopes (e.g. two different functions each with their ownconst seen = new Set()/const cache = new Map()), the flat map only ever holds whichever same-named entry was most recently visited in traversal order. Depending on traversal/declaration order this produces either:JSON.stringify(realSetVar)call is missed because a different, same-named, non-Set/Map variable overwrote the entry first.Grounding
This is a soundness gap in the tracking design, not (yet) confirmed against a live same-file/same-name/different-scope collision — a targeted grep across
actions/setup/js/**/*.cjsfor repeated Set/Map variable names (seen,cache,map,ids,groups, etc.) combined withJSON.stringify(<same name>)calls found no live occurrence today. The codebase's current Set/Map variable names happen not to collide within any single file. The bug is real in the rule's logic regardless — it is a latent regression risk, and the fix is cheap (track bindings/scope, e.g. viacontext.sourceCode.getScope(node)+ variable resolution, matching the scope-aware binding-resolution pattern already used elsewhere in this rule family, e.g.try-catch-rule-utils.ts's scope-walking resolvers) rather than name-string matching.Ask
trackedVarsmap with scope-aware binding resolution (resolve theIdentifierat theJSON.stringify(x)call site to its actual variable/definition via ESLint scope analysis, then check whether that specific binding's initializer wasnew Set(...)/new Map(...)), instead of a flat per-file name map.const seen = new Set()/const seen = { other: true }(or similar) with the same name in different scopes, asserting only the actual Set/Map reference is flagged.Acceptance criteria
Tracking is scope/binding-aware, not name-string-only.
New test covers a same-name, different-scope, mixed Set/Map-and-non-Set/Map case in one file.
Existing tests for the single-scope tracked-variable case still pass unchanged.
This issue will auto-expire if not addressed within 90 days.