Skip to content

Commit f886106

Browse files
authored
Make no-json-stringify-set-or-map binding-aware to avoid cross-scope name collisions (#54301)
1 parent 3818270 commit f886106

2 files changed

Lines changed: 83 additions & 21 deletions

File tree

eslint-factory/src/rules/no-json-stringify-set-or-map.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ describe("no-json-stringify-set-or-map", () => {
5050
});
5151
});
5252

53+
it("valid: same-name non-Set binding is not flagged when another scope has a Set", () => {
54+
cjsRuleTester.run("no-json-stringify-set-or-map", noJsonStringifySetOrMapRule, {
55+
valid: [
56+
`
57+
function tracksSetWithoutStringify() {
58+
const seen = new Set([1, 2]);
59+
return seen.size;
60+
}
61+
function stringifyObject() {
62+
const seen = { other: true };
63+
JSON.stringify(seen);
64+
}
65+
`,
66+
],
67+
invalid: [],
68+
});
69+
});
70+
5371
it("invalid: JSON.stringify on a const Set binding", () => {
5472
cjsRuleTester.run("no-json-stringify-set-or-map", noJsonStringifySetOrMapRule, {
5573
valid: [],
@@ -89,4 +107,38 @@ describe("no-json-stringify-set-or-map", () => {
89107
],
90108
});
91109
});
110+
111+
it("invalid: same-name bindings in different scopes only flag the Set/Map binding", () => {
112+
cjsRuleTester.run("no-json-stringify-set-or-map", noJsonStringifySetOrMapRule, {
113+
valid: [],
114+
invalid: [
115+
{
116+
code: `
117+
function withSet() {
118+
const seen = new Set([1, 2]);
119+
JSON.stringify(seen);
120+
}
121+
function withObject() {
122+
const seen = { other: true };
123+
JSON.stringify(seen);
124+
}
125+
`,
126+
errors: [{ messageId: "jsonStringifySetOrMap" }],
127+
},
128+
{
129+
code: `
130+
function withObject() {
131+
const seen = { other: true };
132+
JSON.stringify(seen);
133+
}
134+
function withSet() {
135+
const seen = new Set([1, 2]);
136+
JSON.stringify(seen);
137+
}
138+
`,
139+
errors: [{ messageId: "jsonStringifySetOrMap" }],
140+
},
141+
],
142+
});
143+
});
92144
});

eslint-factory/src/rules/no-json-stringify-set-or-map.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ function isSetOrMapConstruction(sourceCode: TSESLint.SourceCode, expr: TSESTree.
3131
return name;
3232
}
3333

34+
function getConstSetOrMapBindingKind(sourceCode: TSESLint.SourceCode, identifier: TSESTree.Identifier): "Set" | "Map" | null {
35+
let scope: SourceCodeScope | null = sourceCode.getScope(identifier);
36+
37+
while (scope) {
38+
const variable = scope.set.get(identifier.name);
39+
if (variable && variable.defs.length > 0) {
40+
for (const def of variable.defs) {
41+
if (def.type !== "Variable") continue;
42+
43+
const declarator = def.node as TSESTree.VariableDeclarator;
44+
if (declarator.id.type !== AST_NODE_TYPES.Identifier || declarator.id.name !== identifier.name) continue;
45+
if (!declarator.init) continue;
46+
47+
const declaration = declarator.parent;
48+
if (declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") continue;
49+
50+
const kind = isSetOrMapConstruction(sourceCode, declarator.init);
51+
if (kind) return kind;
52+
}
53+
54+
return null;
55+
}
56+
57+
scope = scope.upper;
58+
}
59+
60+
return null;
61+
}
62+
3463
export const noJsonStringifySetOrMapRule = createRule({
3564
name: "no-json-stringify-set-or-map",
3665
meta: {
@@ -49,30 +78,11 @@ export const noJsonStringifySetOrMapRule = createRule({
4978
create(context) {
5079
const sourceCode = context.sourceCode;
5180

52-
// Track variable names in scope whose declared initializer is `new Set(...)` / `new Map(...)`,
53-
// mapped to which kind they are. Reassignment to something else removes the binding from tracking
54-
// (handled conservatively: only VariableDeclarator initializers are tracked, not `let` reassignment).
55-
const trackedVars = new Map<string, "Set" | "Map">();
56-
5781
function suggestionFor(kind: "Set" | "Map", varName: string): string {
5882
return kind === "Set" ? `Array.from(${varName})` : `Object.fromEntries(${varName})`;
5983
}
6084

6185
return {
62-
VariableDeclarator(node: TSESTree.VariableDeclarator) {
63-
if (node.id.type !== AST_NODE_TYPES.Identifier) return;
64-
if (!node.init) return;
65-
const kind = isSetOrMapConstruction(sourceCode, node.init);
66-
if (!kind) return;
67-
68-
// Only track when declared with `const` — `let`/`var` bindings could be reassigned
69-
// to a non-Set/Map value later, which this rule cannot statically follow.
70-
const declaration = node.parent;
71-
if (declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") return;
72-
73-
trackedVars.set(node.id.name, kind);
74-
},
75-
7686
CallExpression(node: TSESTree.CallExpression) {
7787
const callee = node.callee;
7888
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return;
@@ -97,9 +107,9 @@ export const noJsonStringifySetOrMapRule = createRule({
97107
return;
98108
}
99109

100-
// Reference to a tracked `const x = new Set(...)/new Map(...)` binding.
110+
// Reference to a `const x = new Set(...)/new Map(...)` binding.
101111
if (firstArg.type !== AST_NODE_TYPES.Identifier) return;
102-
const kind = trackedVars.get(firstArg.name);
112+
const kind = getConstSetOrMapBindingKind(sourceCode, firstArg);
103113
if (!kind) return;
104114

105115
context.report({

0 commit comments

Comments
 (0)