Skip to content

Commit 3a76ae7

Browse files
authored
fix: updates ast (#402)
* fix: updates ast * fix: updates ast helper * chore: improves regex for array prototype updates
1 parent dba981a commit 3a76ae7

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

lib/constants/es-features/13.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const ES13_FEATURES = {
4848
property: "at",
4949
excludeCallerTypes: ["ObjectExpression"],
5050
requireNumericArg: true,
51+
requireArrayLikeCall: true,
5152
},
5253
},
5354
ObjectHasOwn: {

lib/helpers/ast.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,117 @@ function hasNumericArgument(node) {
3232
return isNumericLiteral || isNegativeNumeric;
3333
}
3434

35+
const ARRAY_LIKE_NAMES = new Set([
36+
"arr",
37+
"array",
38+
"list",
39+
"items",
40+
"values",
41+
"arguments",
42+
]);
43+
44+
const ARRAY_CONSTRUCTOR_FUNCTIONS = new Set([
45+
"Array",
46+
"from",
47+
"of",
48+
"slice",
49+
"filter",
50+
"map",
51+
"concat",
52+
]);
53+
54+
const ARRAY_METHODS = new Set([
55+
"slice",
56+
"filter",
57+
"map",
58+
"concat",
59+
"from",
60+
"of",
61+
]);
62+
63+
const COMMON_NON_ARRAY_NAMES = new Set([
64+
"obj",
65+
"object",
66+
"foo",
67+
"bar",
68+
"baz",
69+
"data",
70+
"config",
71+
"options",
72+
"params",
73+
"props",
74+
"state",
75+
"item",
76+
"element",
77+
"node",
78+
"target",
79+
"source",
80+
"result",
81+
"response",
82+
"request",
83+
"payload",
84+
"model",
85+
"view",
86+
"controller",
87+
"service",
88+
"utils",
89+
"helpers",
90+
"custom",
91+
]);
92+
function isArrayExpression(calleeObject) {
93+
return calleeObject.type === "ArrayExpression";
94+
}
95+
96+
function isArrayLikeIdentifier(calleeObject) {
97+
const isIdentifier = calleeObject.type === "Identifier";
98+
if (!isIdentifier) return false;
99+
100+
const objectName = calleeObject.name.toLowerCase();
101+
102+
const isCommonNonArrayName = COMMON_NON_ARRAY_NAMES.has(objectName);
103+
if (isCommonNonArrayName) return false;
104+
105+
return ARRAY_LIKE_NAMES.has(objectName);
106+
}
107+
108+
function isArrayConstructorCall(calleeObject) {
109+
const isCallExpression = calleeObject.type === "CallExpression";
110+
if (!isCallExpression) return false;
111+
112+
const innerCallee = calleeObject.callee;
113+
const isIdentifierCallee = innerCallee?.type === "Identifier";
114+
const isMemberExpressionCallee = innerCallee?.type === "MemberExpression";
115+
116+
if (isIdentifierCallee) {
117+
const functionName = innerCallee.name;
118+
return ARRAY_CONSTRUCTOR_FUNCTIONS.has(functionName);
119+
}
120+
121+
if (isMemberExpressionCallee) {
122+
const innerProperty = innerCallee.property;
123+
const hasPropertyName = innerProperty?.name;
124+
const isArrayMethod = hasPropertyName && ARRAY_METHODS.has(innerProperty.name);
125+
return isArrayMethod;
126+
}
127+
128+
return false;
129+
}
130+
131+
function isArrayLikeCall(node) {
132+
const callee = node.callee;
133+
const calleeObject = callee?.object;
134+
const hasCalleeObject = calleeObject != null;
135+
136+
if (!hasCalleeObject) return false;
137+
138+
const isArrayExpr = isArrayExpression(calleeObject);
139+
const isArrayIdent = isArrayLikeIdentifier(calleeObject);
140+
const isArrayConstr = isArrayConstructorCall(calleeObject);
141+
const isArray = isArrayExpr || isArrayIdent || isArrayConstr;
142+
143+
return isArray;
144+
}
145+
35146
function checkMap(node, astInfo) {
36147
const callee = node.callee;
37148
const calleeObject = callee?.object;
@@ -82,6 +193,9 @@ function checkMap(node, astInfo) {
82193
const hasNumericArgMismatch =
83194
astInfo.requireNumericArg && !hasNumericArgument(node);
84195

196+
const hasArrayLikeCallMismatch =
197+
astInfo.requireArrayLikeCall && !isArrayLikeCall(node);
198+
85199
const hasMismatch =
86200
hasKindMismatch ||
87201
hasOperatorMismatch ||
@@ -93,7 +207,8 @@ function checkMap(node, astInfo) {
93207
hasObjectAndPropertyMismatch ||
94208
hasOptionsCauseMismatch ||
95209
hasExcludedCallerType ||
96-
hasNumericArgMismatch;
210+
hasNumericArgMismatch ||
211+
hasArrayLikeCallMismatch;
97212

98213
if (hasMismatch) return false;
99214

tests/unit/helpers/ast.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,77 @@ describe("helpers/ast.js", () => {
162162
const result = checkMap(node, astInfo);
163163
assert.strictEqual(result, true);
164164
});
165+
166+
it("should require array-like call for ArrayPrototypeAt", () => {
167+
const node = {
168+
callee: {
169+
object: { type: "ArrayExpression" },
170+
property: { name: "at" },
171+
},
172+
arguments: [{ type: "Literal", value: -1 }],
173+
};
174+
const astInfo = {
175+
property: "at",
176+
requireNumericArg: true,
177+
requireArrayLikeCall: true,
178+
};
179+
180+
const result = checkMap(node, astInfo);
181+
assert.strictEqual(result, true);
182+
});
183+
184+
it("should reject custom object at() calls", () => {
185+
const node = {
186+
callee: {
187+
object: { type: "Identifier", name: "foo" },
188+
property: { name: "at" },
189+
},
190+
arguments: [{ type: "Literal", value: -1 }],
191+
};
192+
const astInfo = {
193+
property: "at",
194+
requireNumericArg: true,
195+
requireArrayLikeCall: true,
196+
};
197+
198+
const result = checkMap(node, astInfo);
199+
assert.strictEqual(result, false);
200+
});
201+
202+
it("should accept array-like variable names", () => {
203+
const node = {
204+
callee: {
205+
object: { type: "Identifier", name: "arr" },
206+
property: { name: "at" },
207+
},
208+
arguments: [{ type: "Literal", value: -1 }],
209+
};
210+
const astInfo = {
211+
property: "at",
212+
requireNumericArg: true,
213+
requireArrayLikeCall: true,
214+
};
215+
216+
const result = checkMap(node, astInfo);
217+
assert.strictEqual(result, true);
218+
});
219+
220+
it("should accept arguments object", () => {
221+
const node = {
222+
callee: {
223+
object: { type: "Identifier", name: "arguments" },
224+
property: { name: "at" },
225+
},
226+
arguments: [{ type: "Literal", value: 0 }],
227+
};
228+
const astInfo = {
229+
property: "at",
230+
requireNumericArg: true,
231+
requireArrayLikeCall: true,
232+
};
233+
234+
const result = checkMap(node, astInfo);
235+
assert.strictEqual(result, true);
236+
});
165237
});
166238
});

tests/unit/helpers/astDetector.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,12 @@ describe("helpers/astDetector.js", () => {
418418
assert.strictEqual(result.ArrayPrototypeAt, true);
419419
});
420420

421+
it("should not detect ArrayPrototypeAt for custom object .at() with numeric arg (#387)", () => {
422+
const ast = parse("const foo = { at() {} }; foo.at(-1);");
423+
const result = detectFeaturesFromAST(ast);
424+
assert.strictEqual(result.ArrayPrototypeAt, false);
425+
});
426+
421427
it("should not detect OptionalCatchBinding for catch with param (ES5-valid)", () => {
422428
const ast = parse("try { fn(); } catch (error) { fallback(); }");
423429
const result = detectFeaturesFromAST(ast);

0 commit comments

Comments
 (0)