Skip to content

Commit c0b11dd

Browse files
authored
feat: Add suggestions for no-prototype-builtins (#17677)
* feat: Add suggestion for no-prototype-builtins Suggest a fix e.g. a.hasOwnProperty(b) -> Object.prototype.hasOwnProperty.call(a, b). However, if the method call follows an optional chain, then make no suggestions. * Don't provide suggestion if Object is shadowed or not in globals * Add parentheses back for SequenceExpression * Give no suggestion if no-unsafe-optional-chaining
1 parent 4fc44c0 commit c0b11dd

File tree

2 files changed

+222
-6
lines changed

2 files changed

+222
-6
lines changed

lib/rules/no-prototype-builtins.js

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,37 @@
1010

1111
const astUtils = require("./utils/ast-utils");
1212

13+
//------------------------------------------------------------------------------
14+
// Helpers
15+
//------------------------------------------------------------------------------
16+
17+
/**
18+
* Returns true if the node or any of the objects
19+
* to the left of it in the member/call chain is optional.
20+
*
21+
* e.g. `a?.b`, `a?.b.c`, `a?.()`, `a()?.()`
22+
* @param {ASTNode} node The expression to check
23+
* @returns {boolean} `true` if there is a short-circuiting optional `?.`
24+
* in the same option chain to the left of this call or member expression,
25+
* or the node itself is an optional call or member `?.`.
26+
*/
27+
function isAfterOptional(node) {
28+
let leftNode;
29+
30+
if (node.type === "MemberExpression") {
31+
leftNode = node.object;
32+
} else if (node.type === "CallExpression") {
33+
leftNode = node.callee;
34+
} else {
35+
return false;
36+
}
37+
if (node.optional) {
38+
return true;
39+
}
40+
return isAfterOptional(leftNode);
41+
}
42+
43+
1344
//------------------------------------------------------------------------------
1445
// Rule Definition
1546
//------------------------------------------------------------------------------
@@ -25,10 +56,13 @@ module.exports = {
2556
url: "https://eslint.org/docs/latest/rules/no-prototype-builtins"
2657
},
2758

59+
hasSuggestions: true,
60+
2861
schema: [],
2962

3063
messages: {
31-
prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object."
64+
prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object.",
65+
callObjectPrototype: "Call Object.prototype.{{prop}} explicitly."
3266
}
3367
},
3468

@@ -59,7 +93,61 @@ module.exports = {
5993
messageId: "prototypeBuildIn",
6094
loc: callee.property.loc,
6195
data: { prop: propName },
62-
node
96+
node,
97+
suggest: [
98+
{
99+
messageId: "callObjectPrototype",
100+
data: { prop: propName },
101+
fix(fixer) {
102+
const sourceCode = context.sourceCode;
103+
104+
/*
105+
* A call after an optional chain (e.g. a?.b.hasOwnProperty(c))
106+
* must be fixed manually because the call can be short-circuited
107+
*/
108+
if (isAfterOptional(node)) {
109+
return null;
110+
}
111+
112+
/*
113+
* A call on a ChainExpression (e.g. (a?.hasOwnProperty)(c)) will trigger
114+
* no-unsafe-optional-chaining which should be fixed before this suggestion
115+
*/
116+
if (node.callee.type === "ChainExpression") {
117+
return null;
118+
}
119+
120+
const objectVariable = astUtils.getVariableByName(sourceCode.getScope(node), "Object");
121+
122+
/*
123+
* We can't use Object if the global Object was shadowed,
124+
* or Object does not exist in the global scope for some reason
125+
*/
126+
if (!objectVariable || objectVariable.scope.type !== "global" || objectVariable.defs.length > 0) {
127+
return null;
128+
}
129+
130+
let objectText = sourceCode.getText(callee.object);
131+
132+
if (astUtils.getPrecedence(callee.object) <= astUtils.getPrecedence({ type: "SequenceExpression" })) {
133+
objectText = `(${objectText})`;
134+
}
135+
136+
const openParenToken = sourceCode.getTokenAfter(
137+
node.callee,
138+
astUtils.isOpeningParenToken
139+
);
140+
const isEmptyParameters = node.arguments.length === 0;
141+
const delim = isEmptyParameters ? "" : ", ";
142+
const fixes = [
143+
fixer.replaceText(callee, `Object.prototype.${propName}.call`),
144+
fixer.insertTextAfter(openParenToken, objectText + delim)
145+
];
146+
147+
return fixes;
148+
}
149+
}
150+
]
63151
});
64152
}
65153
}

tests/lib/rules/no-prototype-builtins.js

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ ruleTester.run("no-prototype-builtins", rule, {
6161
endColumn: 19,
6262
messageId: "prototypeBuildIn",
6363
data: { prop: "hasOwnProperty" },
64+
suggestions: [
65+
{
66+
messageId: "callObjectPrototype",
67+
output: "Object.prototype.hasOwnProperty.call(foo, 'bar')"
68+
}
69+
],
6470
type: "CallExpression"
6571
}]
6672
},
@@ -73,6 +79,12 @@ ruleTester.run("no-prototype-builtins", rule, {
7379
endColumn: 18,
7480
messageId: "prototypeBuildIn",
7581
data: { prop: "isPrototypeOf" },
82+
suggestions: [
83+
{
84+
messageId: "callObjectPrototype",
85+
output: "Object.prototype.isPrototypeOf.call(foo, 'bar')"
86+
}
87+
],
7688
type: "CallExpression"
7789
}]
7890
},
@@ -84,6 +96,12 @@ ruleTester.run("no-prototype-builtins", rule, {
8496
endLine: 1,
8597
endColumn: 25,
8698
messageId: "prototypeBuildIn",
99+
suggestions: [
100+
{
101+
messageId: "callObjectPrototype",
102+
output: "Object.prototype.propertyIsEnumerable.call(foo, 'bar')"
103+
}
104+
],
87105
data: { prop: "propertyIsEnumerable" }
88106
}]
89107
},
@@ -96,6 +114,12 @@ ruleTester.run("no-prototype-builtins", rule, {
96114
endColumn: 23,
97115
messageId: "prototypeBuildIn",
98116
data: { prop: "hasOwnProperty" },
117+
suggestions: [
118+
{
119+
messageId: "callObjectPrototype",
120+
output: "Object.prototype.hasOwnProperty.call(foo.bar, 'bar')"
121+
}
122+
],
99123
type: "CallExpression"
100124
}]
101125
},
@@ -108,6 +132,12 @@ ruleTester.run("no-prototype-builtins", rule, {
108132
endColumn: 26,
109133
messageId: "prototypeBuildIn",
110134
data: { prop: "isPrototypeOf" },
135+
suggestions: [
136+
{
137+
messageId: "callObjectPrototype",
138+
output: "Object.prototype.isPrototypeOf.call(foo.bar.baz, 'bar')"
139+
}
140+
],
111141
type: "CallExpression"
112142
}]
113143
},
@@ -120,6 +150,12 @@ ruleTester.run("no-prototype-builtins", rule, {
120150
endColumn: 21,
121151
messageId: "prototypeBuildIn",
122152
data: { prop: "hasOwnProperty" },
153+
suggestions: [
154+
{
155+
messageId: "callObjectPrototype",
156+
output: "Object.prototype.hasOwnProperty.call(foo, 'bar')"
157+
}
158+
],
123159
type: "CallExpression"
124160
}]
125161
},
@@ -133,6 +169,12 @@ ruleTester.run("no-prototype-builtins", rule, {
133169
endColumn: 20,
134170
messageId: "prototypeBuildIn",
135171
data: { prop: "isPrototypeOf" },
172+
suggestions: [
173+
{
174+
messageId: "callObjectPrototype",
175+
output: "Object.prototype.isPrototypeOf.call(foo, 'bar').baz"
176+
}
177+
],
136178
type: "CallExpression"
137179
}]
138180
},
@@ -145,30 +187,116 @@ ruleTester.run("no-prototype-builtins", rule, {
145187
endColumn: 31,
146188
messageId: "prototypeBuildIn",
147189
data: { prop: "propertyIsEnumerable" },
190+
suggestions: [
191+
{
192+
messageId: "callObjectPrototype",
193+
output: String.raw`Object.prototype.propertyIsEnumerable.call(foo.bar, 'baz')`
194+
}
195+
],
148196
type: "CallExpression"
149197
}]
150198
},
199+
{
200+
201+
// Can't suggest Object.prototype when Object is shadowed
202+
code: "(function(Object) {return foo.hasOwnProperty('bar');})",
203+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
204+
},
205+
{
206+
code: "foo.hasOwnProperty('bar')",
207+
globals: {
208+
Object: "off"
209+
},
210+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }],
211+
name: "Can't suggest Object.prototype when there is no Object global variable"
212+
},
151213

152214
// Optional chaining
153215
{
154216
code: "foo?.hasOwnProperty('bar')",
155217
parserOptions: { ecmaVersion: 2020 },
156-
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }]
218+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
219+
},
220+
{
221+
code: "foo?.bar.hasOwnProperty('baz')",
222+
parserOptions: { ecmaVersion: 2020 },
223+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
224+
},
225+
{
226+
code: "foo.hasOwnProperty?.('bar')",
227+
parserOptions: { ecmaVersion: 2020 },
228+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
229+
},
230+
{
231+
232+
/*
233+
* If hasOwnProperty is part of a ChainExpresion
234+
* and the optional part is before it, then don't suggest the fix
235+
*/
236+
code: "foo?.hasOwnProperty('bar').baz",
237+
parserOptions: { ecmaVersion: 2020 },
238+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
157239
},
158240
{
241+
242+
/*
243+
* If hasOwnProperty is part of a ChainExpresion
244+
* but the optional part is after it, then the fix is safe
245+
*/
246+
code: "foo.hasOwnProperty('bar')?.baz",
247+
parserOptions: { ecmaVersion: 2020 },
248+
errors: [{
249+
messageId: "prototypeBuildIn",
250+
data: { prop: "hasOwnProperty" },
251+
suggestions: [
252+
{
253+
messageId: "callObjectPrototype",
254+
output: "Object.prototype.hasOwnProperty.call(foo, 'bar')?.baz"
255+
}
256+
]
257+
}]
258+
},
259+
{
260+
261+
code: "(a,b).hasOwnProperty('bar')",
262+
parserOptions: { ecmaVersion: 2020 },
263+
errors: [{
264+
messageId: "prototypeBuildIn",
265+
data: { prop: "hasOwnProperty" },
266+
suggestions: [
267+
268+
// Make sure the SequenceExpression has parentheses before other arguments
269+
{
270+
messageId: "callObjectPrototype",
271+
output: "Object.prototype.hasOwnProperty.call((a,b), 'bar')"
272+
}
273+
]
274+
}]
275+
},
276+
{
277+
278+
// No suggestion where no-unsafe-optional-chaining is reported on the call
159279
code: "(foo?.hasOwnProperty)('bar')",
160280
parserOptions: { ecmaVersion: 2020 },
161-
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }]
281+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
282+
283+
},
284+
{
285+
code: "(foo?.hasOwnProperty)?.('bar')",
286+
parserOptions: { ecmaVersion: 2020 },
287+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
162288
},
163289
{
164290
code: "foo?.['hasOwnProperty']('bar')",
165291
parserOptions: { ecmaVersion: 2020 },
166-
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }]
292+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
167293
},
168294
{
295+
296+
// No suggestion where no-unsafe-optional-chaining is reported on the call
169297
code: "(foo?.[`hasOwnProperty`])('bar')",
170298
parserOptions: { ecmaVersion: 2020 },
171-
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }]
299+
errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }]
172300
}
173301
]
174302
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy