]>
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 | ||
19 | module.exports = { | |
20 | meta: { | |
21 | type: "suggestion", | |
22 | ||
23 | docs: { | |
24 | description: "disallow extending native types", | |
25 | category: "Best Practices", | |
26 | recommended: false, | |
27 | url: "https://eslint.org/docs/rules/no-extend-native" | |
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] || {}; | |
54 | const exceptions = new Set(config.exceptions || []); | |
55 | const modifiedBuiltins = new Set( | |
56 | Object.keys(globals.builtin) | |
57 | .filter(builtin => builtin[0].toUpperCase() === builtin[0]) | |
58 | .filter(builtin => !exceptions.has(builtin)) | |
59 | ); | |
60 | ||
61 | /** | |
62 | * Reports a lint error for the given node. | |
63 | * @param {ASTNode} node The node to report. | |
64 | * @param {string} builtin The name of the native builtin being extended. | |
65 | * @returns {void} | |
66 | */ | |
67 | function reportNode(node, builtin) { | |
68 | context.report({ | |
69 | node, | |
70 | messageId: "unexpected", | |
71 | data: { | |
72 | builtin | |
73 | } | |
74 | }); | |
75 | } | |
76 | ||
77 | /** | |
78 | * Check to see if the `prototype` property of the given object | |
79 | * identifier node is being accessed. | |
80 | * @param {ASTNode} identifierNode The Identifier representing the object | |
81 | * to check. | |
82 | * @returns {boolean} True if the identifier is the object of a | |
83 | * MemberExpression and its `prototype` property is being accessed, | |
84 | * false otherwise. | |
85 | */ | |
86 | function isPrototypePropertyAccessed(identifierNode) { | |
87 | return Boolean( | |
88 | identifierNode && | |
89 | identifierNode.parent && | |
90 | identifierNode.parent.type === "MemberExpression" && | |
91 | identifierNode.parent.object === identifierNode && | |
92 | astUtils.getStaticPropertyName(identifierNode.parent) === "prototype" | |
93 | ); | |
94 | } | |
95 | ||
96 | /** | |
6f036462 TL |
97 | * Check if it's an assignment to the property of the given node. |
98 | * Example: `*.prop = 0` // the `*` is the given node. | |
99 | * @param {ASTNode} node The node to check. | |
100 | * @returns {boolean} True if an assignment to the property of the node. | |
eb39fafa | 101 | */ |
6f036462 TL |
102 | function isAssigningToPropertyOf(node) { |
103 | return ( | |
104 | node.parent.type === "MemberExpression" && | |
105 | node.parent.object === node && | |
106 | node.parent.parent.type === "AssignmentExpression" && | |
107 | node.parent.parent.left === node.parent | |
eb39fafa DC |
108 | ); |
109 | } | |
110 | ||
111 | /** | |
6f036462 TL |
112 | * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. |
113 | * @param {ASTNode} node The node to check. | |
114 | * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. | |
eb39fafa | 115 | */ |
6f036462 TL |
116 | function isInDefinePropertyCall(node) { |
117 | return ( | |
118 | node.parent.type === "CallExpression" && | |
119 | node.parent.arguments[0] === node && | |
120 | astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) | |
eb39fafa DC |
121 | ); |
122 | } | |
123 | ||
124 | /** | |
125 | * Check to see if object prototype access is part of a prototype | |
126 | * extension. There are three ways a prototype can be extended: | |
127 | * 1. Assignment to prototype property (Object.prototype.foo = 1) | |
128 | * 2. Object.defineProperty()/Object.defineProperties() on a prototype | |
129 | * If prototype extension is detected, report the AssignmentExpression | |
130 | * or CallExpression node. | |
131 | * @param {ASTNode} identifierNode The Identifier representing the object | |
132 | * which prototype is being accessed and possibly extended. | |
133 | * @returns {void} | |
134 | */ | |
135 | function checkAndReportPrototypeExtension(identifierNode) { | |
6f036462 TL |
136 | if (!isPrototypePropertyAccessed(identifierNode)) { |
137 | return; // This is not `*.prototype` access. | |
138 | } | |
139 | ||
140 | /* | |
456be15e | 141 | * `identifierNode.parent` is a MemberExpression `*.prototype`. |
6f036462 TL |
142 | * If it's an optional member access, it may be wrapped by a `ChainExpression` node. |
143 | */ | |
144 | const prototypeNode = | |
145 | identifierNode.parent.parent.type === "ChainExpression" | |
146 | ? identifierNode.parent.parent | |
147 | : identifierNode.parent; | |
148 | ||
149 | if (isAssigningToPropertyOf(prototypeNode)) { | |
eb39fafa | 150 | |
6f036462 TL |
151 | // `*.prototype` -> MemberExpression -> AssignmentExpression |
152 | reportNode(prototypeNode.parent.parent, identifierNode.name); | |
153 | } else if (isInDefinePropertyCall(prototypeNode)) { | |
eb39fafa | 154 | |
6f036462 TL |
155 | // `*.prototype` -> CallExpression |
156 | reportNode(prototypeNode.parent, identifierNode.name); | |
eb39fafa DC |
157 | } |
158 | } | |
159 | ||
160 | return { | |
161 | ||
162 | "Program:exit"() { | |
163 | const globalScope = context.getScope(); | |
164 | ||
165 | modifiedBuiltins.forEach(builtin => { | |
166 | const builtinVar = globalScope.set.get(builtin); | |
167 | ||
168 | if (builtinVar && builtinVar.references) { | |
169 | builtinVar.references | |
170 | .map(ref => ref.identifier) | |
171 | .forEach(checkAndReportPrototypeExtension); | |
172 | } | |
173 | }); | |
174 | } | |
175 | }; | |
176 | ||
177 | } | |
178 | }; |