]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag adding properties to native object's prototypes. | |
3 | * @author David Nelson | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | const globals = require("globals"); | |
14 | ||
eb39fafa DC |
15 | //------------------------------------------------------------------------------ |
16 | // Rule Definition | |
17 | //------------------------------------------------------------------------------ | |
18 | ||
34eeec05 | 19 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
20 | module.exports = { |
21 | meta: { | |
22 | type: "suggestion", | |
23 | ||
24 | docs: { | |
8f9d1d4d | 25 | description: "Disallow extending native types", |
eb39fafa | 26 | recommended: false, |
f2a92ac6 | 27 | url: "https://eslint.org/docs/latest/rules/no-extend-native" |
eb39fafa DC |
28 | }, |
29 | ||
30 | schema: [ | |
31 | { | |
32 | type: "object", | |
33 | properties: { | |
34 | exceptions: { | |
35 | type: "array", | |
36 | items: { | |
37 | type: "string" | |
38 | }, | |
39 | uniqueItems: true | |
40 | } | |
41 | }, | |
42 | additionalProperties: false | |
43 | } | |
44 | ], | |
45 | ||
46 | messages: { | |
47 | unexpected: "{{builtin}} prototype is read only, properties should not be added." | |
48 | } | |
49 | }, | |
50 | ||
51 | create(context) { | |
52 | ||
53 | const config = context.options[0] || {}; | |
f2a92ac6 | 54 | const sourceCode = context.sourceCode; |
eb39fafa DC |
55 | const exceptions = new Set(config.exceptions || []); |
56 | const modifiedBuiltins = new Set( | |
57 | Object.keys(globals.builtin) | |
58 | .filter(builtin => builtin[0].toUpperCase() === builtin[0]) | |
59 | .filter(builtin => !exceptions.has(builtin)) | |
60 | ); | |
61 | ||
62 | /** | |
63 | * Reports a lint error for the given node. | |
64 | * @param {ASTNode} node The node to report. | |
65 | * @param {string} builtin The name of the native builtin being extended. | |
66 | * @returns {void} | |
67 | */ | |
68 | function reportNode(node, builtin) { | |
69 | context.report({ | |
70 | node, | |
71 | messageId: "unexpected", | |
72 | data: { | |
73 | builtin | |
74 | } | |
75 | }); | |
76 | } | |
77 | ||
78 | /** | |
79 | * Check to see if the `prototype` property of the given object | |
80 | * identifier node is being accessed. | |
81 | * @param {ASTNode} identifierNode The Identifier representing the object | |
82 | * to check. | |
83 | * @returns {boolean} True if the identifier is the object of a | |
84 | * MemberExpression and its `prototype` property is being accessed, | |
85 | * false otherwise. | |
86 | */ | |
87 | function isPrototypePropertyAccessed(identifierNode) { | |
88 | return Boolean( | |
89 | identifierNode && | |
90 | identifierNode.parent && | |
91 | identifierNode.parent.type === "MemberExpression" && | |
92 | identifierNode.parent.object === identifierNode && | |
93 | astUtils.getStaticPropertyName(identifierNode.parent) === "prototype" | |
94 | ); | |
95 | } | |
96 | ||
97 | /** | |
6f036462 TL |
98 | * Check if it's an assignment to the property of the given node. |
99 | * Example: `*.prop = 0` // the `*` is the given node. | |
100 | * @param {ASTNode} node The node to check. | |
101 | * @returns {boolean} True if an assignment to the property of the node. | |
eb39fafa | 102 | */ |
6f036462 TL |
103 | function isAssigningToPropertyOf(node) { |
104 | return ( | |
105 | node.parent.type === "MemberExpression" && | |
106 | node.parent.object === node && | |
107 | node.parent.parent.type === "AssignmentExpression" && | |
108 | node.parent.parent.left === node.parent | |
eb39fafa DC |
109 | ); |
110 | } | |
111 | ||
112 | /** | |
6f036462 TL |
113 | * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. |
114 | * @param {ASTNode} node The node to check. | |
115 | * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. | |
eb39fafa | 116 | */ |
6f036462 TL |
117 | function isInDefinePropertyCall(node) { |
118 | return ( | |
119 | node.parent.type === "CallExpression" && | |
120 | node.parent.arguments[0] === node && | |
121 | astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) | |
eb39fafa DC |
122 | ); |
123 | } | |
124 | ||
125 | /** | |
126 | * Check to see if object prototype access is part of a prototype | |
127 | * extension. There are three ways a prototype can be extended: | |
128 | * 1. Assignment to prototype property (Object.prototype.foo = 1) | |
129 | * 2. Object.defineProperty()/Object.defineProperties() on a prototype | |
130 | * If prototype extension is detected, report the AssignmentExpression | |
131 | * or CallExpression node. | |
132 | * @param {ASTNode} identifierNode The Identifier representing the object | |
133 | * which prototype is being accessed and possibly extended. | |
134 | * @returns {void} | |
135 | */ | |
136 | function checkAndReportPrototypeExtension(identifierNode) { | |
6f036462 TL |
137 | if (!isPrototypePropertyAccessed(identifierNode)) { |
138 | return; // This is not `*.prototype` access. | |
139 | } | |
140 | ||
141 | /* | |
456be15e | 142 | * `identifierNode.parent` is a MemberExpression `*.prototype`. |
6f036462 TL |
143 | * If it's an optional member access, it may be wrapped by a `ChainExpression` node. |
144 | */ | |
145 | const prototypeNode = | |
146 | identifierNode.parent.parent.type === "ChainExpression" | |
147 | ? identifierNode.parent.parent | |
148 | : identifierNode.parent; | |
149 | ||
150 | if (isAssigningToPropertyOf(prototypeNode)) { | |
eb39fafa | 151 | |
6f036462 TL |
152 | // `*.prototype` -> MemberExpression -> AssignmentExpression |
153 | reportNode(prototypeNode.parent.parent, identifierNode.name); | |
154 | } else if (isInDefinePropertyCall(prototypeNode)) { | |
eb39fafa | 155 | |
6f036462 TL |
156 | // `*.prototype` -> CallExpression |
157 | reportNode(prototypeNode.parent, identifierNode.name); | |
eb39fafa DC |
158 | } |
159 | } | |
160 | ||
161 | return { | |
162 | ||
f2a92ac6 DC |
163 | "Program:exit"(node) { |
164 | const globalScope = sourceCode.getScope(node); | |
eb39fafa DC |
165 | |
166 | modifiedBuiltins.forEach(builtin => { | |
167 | const builtinVar = globalScope.set.get(builtin); | |
168 | ||
169 | if (builtinVar && builtinVar.references) { | |
170 | builtinVar.references | |
171 | .map(ref => ref.identifier) | |
172 | .forEach(checkAndReportPrototypeExtension); | |
173 | } | |
174 | }); | |
175 | } | |
176 | }; | |
177 | ||
178 | } | |
179 | }; |