UNPKG

7.63 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag use of constructors without capital letters
3 * @author Nicholas C. Zakas
4 * @copyright 2014 Jordan Harband. All rights reserved.
5 * @copyright 2013-2014 Nicholas C. Zakas. All rights reserved.
6 * @copyright 2015 Mathieu M-Gosselin. All rights reserved.
7 */
8
9"use strict";
10
11var CAPS_ALLOWED = [
12 "Array",
13 "Boolean",
14 "Date",
15 "Error",
16 "Function",
17 "Number",
18 "Object",
19 "RegExp",
20 "String",
21 "Symbol"
22];
23
24/**
25 * Ensure that if the key is provided, it must be an array.
26 * @param {Object} obj Object to check with `key`.
27 * @param {string} key Object key to check on `obj`.
28 * @param {*} fallback If obj[key] is not present, this will be returned.
29 * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
30 */
31function checkArray(obj, key, fallback) {
32 /* istanbul ignore if */
33 if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
34 throw new TypeError(key + ", if provided, must be an Array");
35 }
36 return obj[key] || fallback;
37}
38
39/**
40 * A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
41 * @param {Object} map Accumulator object for the reduce.
42 * @param {string} key Object key to set to `true`.
43 * @returns {Object} Returns the updated Object for further reduction.
44 */
45function invert(map, key) {
46 map[key] = true;
47 return map;
48}
49
50/**
51 * Creates an object with the cap is new exceptions as its keys and true as their values.
52 * @param {Object} config Rule configuration
53 * @returns {Object} Object with cap is new exceptions.
54 */
55function calculateCapIsNewExceptions(config) {
56 var capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
57
58 if (capIsNewExceptions !== CAPS_ALLOWED) {
59 capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
60 }
61
62 return capIsNewExceptions.reduce(invert, {});
63}
64
65//------------------------------------------------------------------------------
66// Rule Definition
67//------------------------------------------------------------------------------
68
69module.exports = function(context) {
70
71 var config = context.options[0] || {};
72 var NEW_IS_CAP = config.newIsCap !== false;
73 var CAP_IS_NEW = config.capIsNew !== false;
74
75 var newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
76
77 var capIsNewExceptions = calculateCapIsNewExceptions(config);
78
79 var listeners = {};
80
81 //--------------------------------------------------------------------------
82 // Helpers
83 //--------------------------------------------------------------------------
84
85 /**
86 * Get exact callee name from expression
87 * @param {ASTNode} node CallExpression or NewExpression node
88 * @returns {string} name
89 */
90 function extractNameFromExpression(node) {
91
92 var name = "",
93 property;
94
95 if (node.callee.type === "MemberExpression") {
96 property = node.callee.property;
97
98 if (property.type === "Literal" && (typeof property.value === "string")) {
99 name = property.value;
100 } else if (property.type === "Identifier" && !node.callee.computed) {
101 name = property.name;
102 }
103 } else {
104 name = node.callee.name;
105 }
106 return name;
107 }
108
109 /**
110 * Returns the capitalization state of the string -
111 * Whether the first character is uppercase, lowercase, or non-alphabetic
112 * @param {string} str String
113 * @returns {string} capitalization state: "non-alpha", "lower", or "upper"
114 */
115 function getCap(str) {
116 var firstChar = str.charAt(0);
117
118 var firstCharLower = firstChar.toLowerCase();
119 var firstCharUpper = firstChar.toUpperCase();
120
121 if (firstCharLower === firstCharUpper) {
122 // char has no uppercase variant, so it's non-alphabetic
123 return "non-alpha";
124 } else if (firstChar === firstCharLower) {
125 return "lower";
126 } else {
127 return "upper";
128 }
129 }
130
131 /**
132 * Returns whether a node is under a decorator or not.
133 * @param {ASTNode} node CallExpression node
134 * @returns {Boolean} Returns true if the node is under a decorator.
135 */
136 function isDecorator(node) {
137 return node.parent.type === "Decorator";
138 }
139
140 /**
141 * Check if capitalization is allowed for a CallExpression
142 * @param {Object} allowedMap Object mapping calleeName to a Boolean
143 * @param {ASTNode} node CallExpression node
144 * @param {string} calleeName Capitalized callee name from a CallExpression
145 * @returns {Boolean} Returns true if the callee may be capitalized
146 */
147 function isCapAllowed(allowedMap, node, calleeName) {
148 if (allowedMap[calleeName] || allowedMap[context.getSource(node.callee)]) {
149 return true;
150 }
151 if (calleeName === "UTC" && node.callee.type === "MemberExpression") {
152 // allow if callee is Date.UTC
153 return node.callee.object.type === "Identifier" &&
154 node.callee.object.name === "Date";
155 }
156 return false;
157 }
158
159 /**
160 * Reports the given message for the given node. The location will be the start of the property or the callee.
161 * @param {ASTNode} node CallExpression or NewExpression node.
162 * @param {string} message The message to report.
163 * @returns {void}
164 */
165 function report(node, message) {
166 var callee = node.callee;
167
168 if (callee.type === "MemberExpression") {
169 callee = callee.property;
170 }
171
172 context.report(node, callee.loc.start, message);
173 }
174
175 //--------------------------------------------------------------------------
176 // Public
177 //--------------------------------------------------------------------------
178
179 if (NEW_IS_CAP) {
180 listeners.NewExpression = function(node) {
181
182 var constructorName = extractNameFromExpression(node);
183 if (constructorName) {
184 var capitalization = getCap(constructorName);
185 var isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName);
186 if (!isAllowed) {
187 report(node, "A constructor name should not start with a lowercase letter.");
188 }
189 }
190 };
191 }
192
193 if (CAP_IS_NEW) {
194 listeners.CallExpression = function(node) {
195
196 var calleeName = extractNameFromExpression(node);
197 if (calleeName) {
198 var capitalization = getCap(calleeName);
199 var isAllowed = capitalization !== "upper" || isDecorator(node) || isCapAllowed(capIsNewExceptions, node, calleeName);
200 if (!isAllowed) {
201 report(node, "A function with a name starting with an uppercase letter should only be used as a constructor.");
202 }
203 }
204 };
205 }
206
207 return listeners;
208};
209
210module.exports.schema = [
211 {
212 "type": "object",
213 "properties": {
214 "newIsCap": {
215 "type": "boolean"
216 },
217 "capIsNew": {
218 "type": "boolean"
219 },
220 "newIsCapExceptions": {
221 "type": "array",
222 "items": {
223 "type": "string"
224 }
225 },
226 "capIsNewExceptions": {
227 "type": "array",
228 "items": {
229 "type": "string"
230 }
231 }
232 },
233 "additionalProperties": false
234 }
235];