UNPKG

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