UNPKG

9.38 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 },
82
83 schema: [
84 {
85 type: "object",
86 properties: {
87 newIsCap: {
88 type: "boolean"
89 },
90 capIsNew: {
91 type: "boolean"
92 },
93 newIsCapExceptions: {
94 type: "array",
95 items: {
96 type: "string"
97 }
98 },
99 newIsCapExceptionPattern: {
100 type: "string"
101 },
102 capIsNewExceptions: {
103 type: "array",
104 items: {
105 type: "string"
106 }
107 },
108 capIsNewExceptionPattern: {
109 type: "string"
110 },
111 properties: {
112 type: "boolean"
113 }
114 },
115 additionalProperties: false
116 }
117 ]
118 },
119
120 create(context) {
121
122 const config = context.options[0] ? Object.assign({}, context.options[0]) : {};
123
124 config.newIsCap = config.newIsCap !== false;
125 config.capIsNew = config.capIsNew !== false;
126 const skipProperties = config.properties === false;
127
128 const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
129 const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern) : null;
130
131 const capIsNewExceptions = calculateCapIsNewExceptions(config);
132 const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern) : null;
133
134 const listeners = {};
135
136 const sourceCode = context.getSourceCode();
137
138 //--------------------------------------------------------------------------
139 // Helpers
140 //--------------------------------------------------------------------------
141
142 /**
143 * Get exact callee name from expression
144 * @param {ASTNode} node CallExpression or NewExpression node
145 * @returns {string} name
146 */
147 function extractNameFromExpression(node) {
148
149 let name = "";
150
151 if (node.callee.type === "MemberExpression") {
152 const property = node.callee.property;
153
154 if (property.type === "Literal" && (typeof property.value === "string")) {
155 name = property.value;
156 } else if (property.type === "Identifier" && !node.callee.computed) {
157 name = property.name;
158 }
159 } else {
160 name = node.callee.name;
161 }
162 return name;
163 }
164
165 /**
166 * Returns the capitalization state of the string -
167 * Whether the first character is uppercase, lowercase, or non-alphabetic
168 * @param {string} str String
169 * @returns {string} capitalization state: "non-alpha", "lower", or "upper"
170 */
171 function getCap(str) {
172 const firstChar = str.charAt(0);
173
174 const firstCharLower = firstChar.toLowerCase();
175 const firstCharUpper = firstChar.toUpperCase();
176
177 if (firstCharLower === firstCharUpper) {
178
179 // char has no uppercase variant, so it's non-alphabetic
180 return "non-alpha";
181 } else if (firstChar === firstCharLower) {
182 return "lower";
183 } else {
184 return "upper";
185 }
186 }
187
188 /**
189 * Check if capitalization is allowed for a CallExpression
190 * @param {Object} allowedMap Object mapping calleeName to a Boolean
191 * @param {ASTNode} node CallExpression node
192 * @param {string} calleeName Capitalized callee name from a CallExpression
193 * @param {Object} pattern RegExp object from options pattern
194 * @returns {boolean} Returns true if the callee may be capitalized
195 */
196 function isCapAllowed(allowedMap, node, calleeName, pattern) {
197 const sourceText = sourceCode.getText(node.callee);
198
199 if (allowedMap[calleeName] || allowedMap[sourceText]) {
200 return true;
201 }
202
203 if (pattern && pattern.test(sourceText)) {
204 return true;
205 }
206
207 if (calleeName === "UTC" && node.callee.type === "MemberExpression") {
208
209 // allow if callee is Date.UTC
210 return node.callee.object.type === "Identifier" &&
211 node.callee.object.name === "Date";
212 }
213
214 return skipProperties && node.callee.type === "MemberExpression";
215 }
216
217 /**
218 * Reports the given message for the given node. The location will be the start of the property or the callee.
219 * @param {ASTNode} node CallExpression or NewExpression node.
220 * @param {string} message The message to report.
221 * @returns {void}
222 */
223 function report(node, message) {
224 let callee = node.callee;
225
226 if (callee.type === "MemberExpression") {
227 callee = callee.property;
228 }
229
230 context.report(node, callee.loc.start, message);
231 }
232
233 //--------------------------------------------------------------------------
234 // Public
235 //--------------------------------------------------------------------------
236
237 if (config.newIsCap) {
238 listeners.NewExpression = function(node) {
239
240 const constructorName = extractNameFromExpression(node);
241
242 if (constructorName) {
243 const capitalization = getCap(constructorName);
244 const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern);
245
246 if (!isAllowed) {
247 report(node, "A constructor name should not start with a lowercase letter.");
248 }
249 }
250 };
251 }
252
253 if (config.capIsNew) {
254 listeners.CallExpression = function(node) {
255
256 const calleeName = extractNameFromExpression(node);
257
258 if (calleeName) {
259 const capitalization = getCap(calleeName);
260 const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern);
261
262 if (!isAllowed) {
263 report(node, "A function with a name starting with an uppercase letter should only be used as a constructor.");
264 }
265 }
266 };
267 }
268
269 return listeners;
270 }
271};