2 * @fileoverview Rule to flag use of constructors without capital letters
3 * @author Nicholas C. Zakas
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 //------------------------------------------------------------------------------
14 //------------------------------------------------------------------------------
16 const CAPS_ALLOWED = [
31 * Ensure that if the key is provided, it must be an array.
32 * @param {Object} obj Object to check with `key`.
33 * @param {string} key Object key to check on `obj`.
34 * @param {*} fallback If obj[key] is not present, this will be returned.
35 * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
37 function checkArray(obj, key, fallback) {
39 /* istanbul ignore if */
40 if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
41 throw new TypeError(`${key}, if provided, must be an Array`);
43 return obj[key] || fallback;
47 * A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
48 * @param {Object} map Accumulator object for the reduce.
49 * @param {string} key Object key to set to `true`.
50 * @returns {Object} Returns the updated Object for further reduction.
52 function invert(map, key) {
58 * Creates an object with the cap is new exceptions as its keys and true as their values.
59 * @param {Object} config Rule configuration
60 * @returns {Object} Object with cap is new exceptions.
62 function calculateCapIsNewExceptions(config) {
63 let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
65 if (capIsNewExceptions !== CAPS_ALLOWED) {
66 capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
69 return capIsNewExceptions.reduce(invert, {});
72 //------------------------------------------------------------------------------
74 //------------------------------------------------------------------------------
81 description: "require constructor names to begin with a capital letter",
82 category: "Stylistic Issues",
84 url: "https://eslint.org/docs/rules/new-cap"
105 newIsCapExceptionPattern: {
108 capIsNewExceptions: {
114 capIsNewExceptionPattern: {
122 additionalProperties: false
126 upper: "A function with a name starting with an uppercase letter should only be used as a constructor.",
127 lower: "A constructor name should not start with a lowercase letter."
133 const config = Object.assign({}, context.options[0]);
135 config.newIsCap = config.newIsCap !== false;
136 config.capIsNew = config.capIsNew !== false;
137 const skipProperties = config.properties === false;
139 const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
140 const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null;
142 const capIsNewExceptions = calculateCapIsNewExceptions(config);
143 const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null;
145 const listeners = {};
147 const sourceCode = context.getSourceCode();
149 //--------------------------------------------------------------------------
151 //--------------------------------------------------------------------------
154 * Get exact callee name from expression
155 * @param {ASTNode} node CallExpression or NewExpression node
156 * @returns {string} name
158 function extractNameFromExpression(node) {
162 if (node.callee.type === "MemberExpression") {
163 const property = node.callee.property;
165 if (property.type === "Literal" && (typeof property.value === "string")) {
166 name = property.value;
167 } else if (property.type === "Identifier" && !node.callee.computed) {
168 name = property.name;
171 name = node.callee.name;
177 * Returns the capitalization state of the string -
178 * Whether the first character is uppercase, lowercase, or non-alphabetic
179 * @param {string} str String
180 * @returns {string} capitalization state: "non-alpha", "lower", or "upper"
182 function getCap(str) {
183 const firstChar = str.charAt(0);
185 const firstCharLower = firstChar.toLowerCase();
186 const firstCharUpper = firstChar.toUpperCase();
188 if (firstCharLower === firstCharUpper) {
190 // char has no uppercase variant, so it's non-alphabetic
193 if (firstChar === firstCharLower) {
201 * Check if capitalization is allowed for a CallExpression
202 * @param {Object} allowedMap Object mapping calleeName to a Boolean
203 * @param {ASTNode} node CallExpression node
204 * @param {string} calleeName Capitalized callee name from a CallExpression
205 * @param {Object} pattern RegExp object from options pattern
206 * @returns {boolean} Returns true if the callee may be capitalized
208 function isCapAllowed(allowedMap, node, calleeName, pattern) {
209 const sourceText = sourceCode.getText(node.callee);
211 if (allowedMap[calleeName] || allowedMap[sourceText]) {
215 if (pattern && pattern.test(sourceText)) {
219 if (calleeName === "UTC" && node.callee.type === "MemberExpression") {
221 // allow if callee is Date.UTC
222 return node.callee.object.type === "Identifier" &&
223 node.callee.object.name === "Date";
226 return skipProperties && node.callee.type === "MemberExpression";
230 * Reports the given messageId for the given node. The location will be the start of the property or the callee.
231 * @param {ASTNode} node CallExpression or NewExpression node.
232 * @param {string} messageId The messageId to report.
235 function report(node, messageId) {
236 let callee = node.callee;
238 if (callee.type === "MemberExpression") {
239 callee = callee.property;
242 context.report({ node, loc: callee.loc.start, messageId });
245 //--------------------------------------------------------------------------
247 //--------------------------------------------------------------------------
249 if (config.newIsCap) {
250 listeners.NewExpression = function(node) {
252 const constructorName = extractNameFromExpression(node);
254 if (constructorName) {
255 const capitalization = getCap(constructorName);
256 const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern);
259 report(node, "lower");
265 if (config.capIsNew) {
266 listeners.CallExpression = function(node) {
268 const calleeName = extractNameFromExpression(node);
271 const capitalization = getCap(calleeName);
272 const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern);
275 report(node, "upper");