Skip to content

Commit ab5bbd7

Browse files
authored
Add support for assignable variables to *-prototype-* rules (#219)
1 parent d16dff2 commit ab5bbd7

File tree

3 files changed

+134
-5
lines changed

3 files changed

+134
-5
lines changed

lib/util/type-checker/object-type-checker.js

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,22 +202,99 @@ function buildExpressionTypeProviderImpl(context) {
202202
// It has an initial value.
203203
def.node.init &&
204204
// It does not write new values.
205-
(def.parent.kind === "const" ||
206-
variable.references.every(
207-
(ref) =>
208-
ref.isReadOnly() || ref.identifier === def.name,
209-
))
205+
def.parent.kind === "const"
210206
) {
211207
// The type of the initial value is the type of the variable.
212208
const init = getTypeInfo(def.node.init)
213209
return init && getPatternTypeInfo(def.name, def.node.id, init)
214210
}
211+
if (def.parent.kind === "const") {
212+
return null
213+
}
214+
return getAssignableVariableTypeInfo(variable)
215215
} else if (def.type === "FunctionName") {
216216
return getTypeInfo(def.node)
217217
}
218218
return null
219219
}
220220

221+
/**
222+
* @param {import("eslint").Scope.Variable} variable
223+
* @returns {TypeInfo | null}
224+
*/
225+
function getAssignableVariableTypeInfo(variable) {
226+
/** @type {TypeInfo[]} */
227+
const typeInfos = []
228+
for (const reference of variable.references) {
229+
if (!reference.writeExpr) {
230+
continue
231+
}
232+
/** @type {import("estree").Node|null} */
233+
const parent = reference.writeExpr.parent
234+
if (!parent) {
235+
return null // unknown
236+
}
237+
if (parent.type === "VariableDeclarator") {
238+
if (reference.writeExpr !== parent.init) {
239+
return null // unknown
240+
}
241+
const init = getTypeInfo(reference.writeExpr)
242+
if (!init) {
243+
return null // unknown
244+
}
245+
const typeInfo = getPatternTypeInfo(
246+
reference.identifier,
247+
parent.id,
248+
init,
249+
)
250+
if (!typeInfo) {
251+
return null // unknown
252+
}
253+
typeInfos.push(typeInfo)
254+
} else if (parent.type === "AssignmentExpression") {
255+
if (reference.writeExpr !== parent.right) {
256+
return null // unknown
257+
}
258+
const right = getTypeInfo(reference.writeExpr)
259+
if (!right) {
260+
return null // unknown
261+
}
262+
const typeInfo = getPatternTypeInfo(
263+
reference.identifier,
264+
parent.left,
265+
right,
266+
)
267+
if (!typeInfo) {
268+
return null // unknown
269+
}
270+
typeInfos.push(typeInfo)
271+
}
272+
}
273+
const firstTypeInfo = typeInfos.shift()
274+
if (!firstTypeInfo) {
275+
return null
276+
}
277+
if (!typeInfos.length) {
278+
return firstTypeInfo
279+
}
280+
if (typeInfos.every((t) => t.type === firstTypeInfo.type)) {
281+
return {
282+
type: firstTypeInfo.type,
283+
get return() {
284+
if (
285+
typeInfos.every(
286+
(t) => t.return === firstTypeInfo.return,
287+
)
288+
) {
289+
return firstTypeInfo.return
290+
}
291+
return null
292+
},
293+
}
294+
}
295+
return null
296+
}
297+
221298
/**
222299
* @param {import("estree").BinaryOperator
223300
* | import("estree").LogicalOperator

tests/lib/rules/no-nonstandard-array-prototype-properties.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ new RuleTester().run(ruleId, rule, {
4343
"Non-standard 'Array.prototype.01' property is forbidden.",
4444
],
4545
},
46+
{
47+
code: `
48+
let array2 = [1, 2, 3];
49+
array2.unknown(3, 4);
50+
array2 = [1, 2, 3, 4];
51+
array2.unknown(3, 5);`,
52+
errors: [
53+
"Non-standard 'Array.prototype.unknown' property is forbidden.",
54+
"Non-standard 'Array.prototype.unknown' property is forbidden.",
55+
],
56+
},
4657
],
4758
})
4859

tests/lib/util/type-checker/object-type-checker.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,47 @@ describe("define-prototype-method-handler/object-type-checker", () => {
438438
`,
439439
result: ["String"],
440440
},
441+
{
442+
code: `
443+
let array = []
444+
array = []
445+
target(array);
446+
`,
447+
result: ["Array"],
448+
},
449+
{
450+
code: `
451+
let array = []
452+
array = ''
453+
target(array);
454+
`,
455+
result: [null],
456+
},
457+
{
458+
code: `
459+
let {EPSILON:a} = Number;
460+
a = 42;
461+
({MAX_SAFE_INTEGER:a} = Number);
462+
target(a);
463+
`,
464+
result: ["Number"],
465+
},
466+
{
467+
code: `
468+
let a = Number.isFinite;
469+
a = Number.isNaN;
470+
target(a(foo));
471+
`,
472+
result: ["Boolean"],
473+
},
474+
{
475+
code: `
476+
let a = Number.isFinite;
477+
a = Number;
478+
target(a(foo));
479+
`,
480+
result: [null],
481+
},
441482
]) {
442483
;(only ? it.only : it)(code, () => {
443484
deepStrictEqual(

0 commit comments

Comments
 (0)