2 * @fileoverview Rule to flag use of constructors without capital letters
3 * @author Nicholas C. Zakas
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const CAPS_ALLOWED = [
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`
39 function checkArray(obj, key, fallback) {
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`);
45 return obj[key] || fallback;
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.
54 function invert(map, key) {
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.
64 function calculateCapIsNewExceptions(config) {
65 let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
67 if (capIsNewExceptions !== CAPS_ALLOWED) {
68 capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
71 return capIsNewExceptions.reduce(invert, {});
74 //------------------------------------------------------------------------------
76 //------------------------------------------------------------------------------
83 description: "require constructor names to begin with a capital letter",
84 category: "Stylistic Issues",
86 url: "https://eslint.org/docs/rules/new-cap"
101 newIsCapExceptions: {
107 newIsCapExceptionPattern: {
110 capIsNewExceptions: {
116 capIsNewExceptionPattern: {
124 additionalProperties: false
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."
135 const config = Object.assign({}, context.options[0]);
137 config.newIsCap = config.newIsCap !== false;
138 config.capIsNew = config.capIsNew !== false;
139 const skipProperties = config.properties === false;
141 const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
142 const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null;
144 const capIsNewExceptions = calculateCapIsNewExceptions(config);
145 const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null;
147 const listeners = {};
149 const sourceCode = context.getSourceCode();
151 //--------------------------------------------------------------------------
153 //--------------------------------------------------------------------------
156 * Get exact callee name from expression
157 * @param {ASTNode} node CallExpression or NewExpression node
158 * @returns {string} name
160 function extractNameFromExpression(node) {
161 return node.callee.type === "Identifier"
163 : astUtils.getStaticPropertyName(node.callee) || "";
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"
172 function getCap(str) {
173 const firstChar = str.charAt(0);
175 const firstCharLower = firstChar.toLowerCase();
176 const firstCharUpper = firstChar.toUpperCase();
178 if (firstCharLower === firstCharUpper) {
180 // char has no uppercase variant, so it's non-alphabetic
183 if (firstChar === firstCharLower) {
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
198 function isCapAllowed(allowedMap, node, calleeName, pattern) {
199 const sourceText = sourceCode.getText(node.callee);
201 if (allowedMap[calleeName] || allowedMap[sourceText]) {
205 if (pattern && pattern.test(sourceText)) {
209 const callee = astUtils.skipChainExpression(node.callee);
211 if (calleeName === "UTC" && callee.type === "MemberExpression") {
213 // allow if callee is Date.UTC
214 return callee.object.type === "Identifier" &&
215 callee.object.name === "Date";
218 return skipProperties && callee.type === "MemberExpression";
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.
227 function report(node, messageId) {
228 let callee = astUtils.skipChainExpression(node.callee);
230 if (callee.type === "MemberExpression") {
231 callee = callee.property;
234 context.report({ node, loc: callee.loc, messageId });
237 //--------------------------------------------------------------------------
239 //--------------------------------------------------------------------------
241 if (config.newIsCap) {
242 listeners.NewExpression = function(node) {
244 const constructorName = extractNameFromExpression(node);
246 if (constructorName) {
247 const capitalization = getCap(constructorName);
248 const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern);
251 report(node, "lower");
257 if (config.capIsNew) {
258 listeners.CallExpression = function(node) {
260 const calleeName = extractNameFromExpression(node);
263 const capitalization = getCap(calleeName);
264 const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern);
267 report(node, "upper");