• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const 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`.
36 * @param {*} fallback If obj[key] is not present, this will be returned.
37 * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
38 */
39function checkArray(obj, key, fallback) {
40
41    /* istanbul ignore if */
42    if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
43        throw new TypeError(`${key}, if provided, must be an Array`);
44    }
45    return obj[key] || fallback;
46}
47
48/**
49 * A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
50 * @param {Object} map Accumulator object for the reduce.
51 * @param {string} key Object key to set to `true`.
52 * @returns {Object} Returns the updated Object for further reduction.
53 */
54function invert(map, key) {
55    map[key] = true;
56    return map;
57}
58
59/**
60 * Creates an object with the cap is new exceptions as its keys and true as their values.
61 * @param {Object} config Rule configuration
62 * @returns {Object} Object with cap is new exceptions.
63 */
64function calculateCapIsNewExceptions(config) {
65    let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
66
67    if (capIsNewExceptions !== CAPS_ALLOWED) {
68        capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
69    }
70
71    return capIsNewExceptions.reduce(invert, {});
72}
73
74//------------------------------------------------------------------------------
75// Rule Definition
76//------------------------------------------------------------------------------
77
78module.exports = {
79    meta: {
80        type: "suggestion",
81
82        docs: {
83            description: "require constructor names to begin with a capital letter",
84            category: "Stylistic Issues",
85            recommended: false,
86            url: "https://eslint.org/docs/rules/new-cap"
87        },
88
89        schema: [
90            {
91                type: "object",
92                properties: {
93                    newIsCap: {
94                        type: "boolean",
95                        default: true
96                    },
97                    capIsNew: {
98                        type: "boolean",
99                        default: true
100                    },
101                    newIsCapExceptions: {
102                        type: "array",
103                        items: {
104                            type: "string"
105                        }
106                    },
107                    newIsCapExceptionPattern: {
108                        type: "string"
109                    },
110                    capIsNewExceptions: {
111                        type: "array",
112                        items: {
113                            type: "string"
114                        }
115                    },
116                    capIsNewExceptionPattern: {
117                        type: "string"
118                    },
119                    properties: {
120                        type: "boolean",
121                        default: true
122                    }
123                },
124                additionalProperties: false
125            }
126        ],
127        messages: {
128            upper: "A function with a name starting with an uppercase letter should only be used as a constructor.",
129            lower: "A constructor name should not start with a lowercase letter."
130        }
131    },
132
133    create(context) {
134
135        const config = Object.assign({}, context.options[0]);
136
137        config.newIsCap = config.newIsCap !== false;
138        config.capIsNew = config.capIsNew !== false;
139        const skipProperties = config.properties === false;
140
141        const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
142        const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null;
143
144        const capIsNewExceptions = calculateCapIsNewExceptions(config);
145        const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null;
146
147        const listeners = {};
148
149        const sourceCode = context.getSourceCode();
150
151        //--------------------------------------------------------------------------
152        // Helpers
153        //--------------------------------------------------------------------------
154
155        /**
156         * Get exact callee name from expression
157         * @param {ASTNode} node CallExpression or NewExpression node
158         * @returns {string} name
159         */
160        function extractNameFromExpression(node) {
161            return node.callee.type === "Identifier"
162                ? node.callee.name
163                : astUtils.getStaticPropertyName(node.callee) || "";
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            const callee = astUtils.skipChainExpression(node.callee);
210
211            if (calleeName === "UTC" && callee.type === "MemberExpression") {
212
213                // allow if callee is Date.UTC
214                return callee.object.type === "Identifier" &&
215                    callee.object.name === "Date";
216            }
217
218            return skipProperties && callee.type === "MemberExpression";
219        }
220
221        /**
222         * Reports the given messageId for the given node. The location will be the start of the property or the callee.
223         * @param {ASTNode} node CallExpression or NewExpression node.
224         * @param {string} messageId The messageId to report.
225         * @returns {void}
226         */
227        function report(node, messageId) {
228            let callee = astUtils.skipChainExpression(node.callee);
229
230            if (callee.type === "MemberExpression") {
231                callee = callee.property;
232            }
233
234            context.report({ node, loc: callee.loc, messageId });
235        }
236
237        //--------------------------------------------------------------------------
238        // Public
239        //--------------------------------------------------------------------------
240
241        if (config.newIsCap) {
242            listeners.NewExpression = function(node) {
243
244                const constructorName = extractNameFromExpression(node);
245
246                if (constructorName) {
247                    const capitalization = getCap(constructorName);
248                    const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern);
249
250                    if (!isAllowed) {
251                        report(node, "lower");
252                    }
253                }
254            };
255        }
256
257        if (config.capIsNew) {
258            listeners.CallExpression = function(node) {
259
260                const calleeName = extractNameFromExpression(node);
261
262                if (calleeName) {
263                    const capitalization = getCap(calleeName);
264                    const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern);
265
266                    if (!isAllowed) {
267                        report(node, "upper");
268                    }
269                }
270            };
271        }
272
273        return listeners;
274    }
275};
276