]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag use of constructors without capital letters | |
3 | * @author Nicholas C. Zakas | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | const CAPS_ALLOWED = [ | |
19 | "Array", | |
20 | "Boolean", | |
21 | "Date", | |
22 | "Error", | |
23 | "Function", | |
24 | "Number", | |
25 | "Object", | |
26 | "RegExp", | |
27 | "String", | |
28 | "Symbol", | |
29 | "BigInt" | |
30 | ]; | |
31 | ||
32 | /** | |
33 | * Ensure that if the key is provided, it must be an array. | |
34 | * @param {Object} obj Object to check with `key`. | |
35 | * @param {string} key Object key to check on `obj`. | |
609c276f TL |
36 | * @param {any} fallback If obj[key] is not present, this will be returned. |
37 | * @throws {TypeError} If key is not an own array type property of `obj`. | |
eb39fafa DC |
38 | * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` |
39 | */ | |
40 | function checkArray(obj, key, fallback) { | |
41 | ||
42 | /* istanbul ignore if */ | |
43 | if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { | |
44 | throw new TypeError(`${key}, if provided, must be an Array`); | |
45 | } | |
46 | return obj[key] || fallback; | |
47 | } | |
48 | ||
49 | /** | |
50 | * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. | |
51 | * @param {Object} map Accumulator object for the reduce. | |
52 | * @param {string} key Object key to set to `true`. | |
53 | * @returns {Object} Returns the updated Object for further reduction. | |
54 | */ | |
55 | function invert(map, key) { | |
56 | map[key] = true; | |
57 | return map; | |
58 | } | |
59 | ||
60 | /** | |
61 | * Creates an object with the cap is new exceptions as its keys and true as their values. | |
62 | * @param {Object} config Rule configuration | |
63 | * @returns {Object} Object with cap is new exceptions. | |
64 | */ | |
65 | function calculateCapIsNewExceptions(config) { | |
66 | let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); | |
67 | ||
68 | if (capIsNewExceptions !== CAPS_ALLOWED) { | |
69 | capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); | |
70 | } | |
71 | ||
72 | return capIsNewExceptions.reduce(invert, {}); | |
73 | } | |
74 | ||
75 | //------------------------------------------------------------------------------ | |
76 | // Rule Definition | |
77 | //------------------------------------------------------------------------------ | |
78 | ||
34eeec05 | 79 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
80 | module.exports = { |
81 | meta: { | |
82 | type: "suggestion", | |
83 | ||
84 | docs: { | |
85 | description: "require constructor names to begin with a capital letter", | |
eb39fafa DC |
86 | recommended: false, |
87 | url: "https://eslint.org/docs/rules/new-cap" | |
88 | }, | |
89 | ||
90 | schema: [ | |
91 | { | |
92 | type: "object", | |
93 | properties: { | |
94 | newIsCap: { | |
95 | type: "boolean", | |
96 | default: true | |
97 | }, | |
98 | capIsNew: { | |
99 | type: "boolean", | |
100 | default: true | |
101 | }, | |
102 | newIsCapExceptions: { | |
103 | type: "array", | |
104 | items: { | |
105 | type: "string" | |
106 | } | |
107 | }, | |
108 | newIsCapExceptionPattern: { | |
109 | type: "string" | |
110 | }, | |
111 | capIsNewExceptions: { | |
112 | type: "array", | |
113 | items: { | |
114 | type: "string" | |
115 | } | |
116 | }, | |
117 | capIsNewExceptionPattern: { | |
118 | type: "string" | |
119 | }, | |
120 | properties: { | |
121 | type: "boolean", | |
122 | default: true | |
123 | } | |
124 | }, | |
125 | additionalProperties: false | |
126 | } | |
127 | ], | |
128 | messages: { | |
129 | upper: "A function with a name starting with an uppercase letter should only be used as a constructor.", | |
130 | lower: "A constructor name should not start with a lowercase letter." | |
131 | } | |
132 | }, | |
133 | ||
134 | create(context) { | |
135 | ||
136 | const config = Object.assign({}, context.options[0]); | |
137 | ||
138 | config.newIsCap = config.newIsCap !== false; | |
139 | config.capIsNew = config.capIsNew !== false; | |
140 | const skipProperties = config.properties === false; | |
141 | ||
142 | const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); | |
143 | const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null; | |
144 | ||
145 | const capIsNewExceptions = calculateCapIsNewExceptions(config); | |
146 | const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null; | |
147 | ||
148 | const listeners = {}; | |
149 | ||
150 | const sourceCode = context.getSourceCode(); | |
151 | ||
152 | //-------------------------------------------------------------------------- | |
153 | // Helpers | |
154 | //-------------------------------------------------------------------------- | |
155 | ||
156 | /** | |
157 | * Get exact callee name from expression | |
158 | * @param {ASTNode} node CallExpression or NewExpression node | |
159 | * @returns {string} name | |
160 | */ | |
161 | function extractNameFromExpression(node) { | |
6f036462 TL |
162 | return node.callee.type === "Identifier" |
163 | ? node.callee.name | |
164 | : astUtils.getStaticPropertyName(node.callee) || ""; | |
eb39fafa DC |
165 | } |
166 | ||
167 | /** | |
168 | * Returns the capitalization state of the string - | |
169 | * Whether the first character is uppercase, lowercase, or non-alphabetic | |
170 | * @param {string} str String | |
171 | * @returns {string} capitalization state: "non-alpha", "lower", or "upper" | |
172 | */ | |
173 | function getCap(str) { | |
174 | const firstChar = str.charAt(0); | |
175 | ||
176 | const firstCharLower = firstChar.toLowerCase(); | |
177 | const firstCharUpper = firstChar.toUpperCase(); | |
178 | ||
179 | if (firstCharLower === firstCharUpper) { | |
180 | ||
181 | // char has no uppercase variant, so it's non-alphabetic | |
182 | return "non-alpha"; | |
183 | } | |
184 | if (firstChar === firstCharLower) { | |
185 | return "lower"; | |
186 | } | |
187 | return "upper"; | |
188 | ||
189 | } | |
190 | ||
191 | /** | |
192 | * Check if capitalization is allowed for a CallExpression | |
193 | * @param {Object} allowedMap Object mapping calleeName to a Boolean | |
194 | * @param {ASTNode} node CallExpression node | |
195 | * @param {string} calleeName Capitalized callee name from a CallExpression | |
196 | * @param {Object} pattern RegExp object from options pattern | |
197 | * @returns {boolean} Returns true if the callee may be capitalized | |
198 | */ | |
199 | function isCapAllowed(allowedMap, node, calleeName, pattern) { | |
200 | const sourceText = sourceCode.getText(node.callee); | |
201 | ||
202 | if (allowedMap[calleeName] || allowedMap[sourceText]) { | |
203 | return true; | |
204 | } | |
205 | ||
206 | if (pattern && pattern.test(sourceText)) { | |
207 | return true; | |
208 | } | |
209 | ||
6f036462 TL |
210 | const callee = astUtils.skipChainExpression(node.callee); |
211 | ||
212 | if (calleeName === "UTC" && callee.type === "MemberExpression") { | |
eb39fafa DC |
213 | |
214 | // allow if callee is Date.UTC | |
6f036462 TL |
215 | return callee.object.type === "Identifier" && |
216 | callee.object.name === "Date"; | |
eb39fafa DC |
217 | } |
218 | ||
6f036462 | 219 | return skipProperties && callee.type === "MemberExpression"; |
eb39fafa DC |
220 | } |
221 | ||
222 | /** | |
223 | * Reports the given messageId for the given node. The location will be the start of the property or the callee. | |
224 | * @param {ASTNode} node CallExpression or NewExpression node. | |
225 | * @param {string} messageId The messageId to report. | |
226 | * @returns {void} | |
227 | */ | |
228 | function report(node, messageId) { | |
6f036462 | 229 | let callee = astUtils.skipChainExpression(node.callee); |
eb39fafa DC |
230 | |
231 | if (callee.type === "MemberExpression") { | |
232 | callee = callee.property; | |
233 | } | |
234 | ||
56c4a2cb | 235 | context.report({ node, loc: callee.loc, messageId }); |
eb39fafa DC |
236 | } |
237 | ||
238 | //-------------------------------------------------------------------------- | |
239 | // Public | |
240 | //-------------------------------------------------------------------------- | |
241 | ||
242 | if (config.newIsCap) { | |
243 | listeners.NewExpression = function(node) { | |
244 | ||
245 | const constructorName = extractNameFromExpression(node); | |
246 | ||
247 | if (constructorName) { | |
248 | const capitalization = getCap(constructorName); | |
249 | const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern); | |
250 | ||
251 | if (!isAllowed) { | |
252 | report(node, "lower"); | |
253 | } | |
254 | } | |
255 | }; | |
256 | } | |
257 | ||
258 | if (config.capIsNew) { | |
259 | listeners.CallExpression = function(node) { | |
260 | ||
261 | const calleeName = extractNameFromExpression(node); | |
262 | ||
263 | if (calleeName) { | |
264 | const capitalization = getCap(calleeName); | |
265 | const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern); | |
266 | ||
267 | if (!isAllowed) { | |
268 | report(node, "upper"); | |
269 | } | |
270 | } | |
271 | }; | |
272 | } | |
273 | ||
274 | return listeners; | |
275 | } | |
276 | }; |