2025-04-19 23:12:19 -04:00

279 lines
7.7 KiB
JavaScript

/**
* @fileoverview Rule to flag use of constructors without capital letters
* @author Nicholas C. Zakas
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require('./utils/ast-utils');
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const CAPS_ALLOWED = [
'Array',
'Boolean',
'Date',
'Error',
'Function',
'Number',
'Object',
'RegExp',
'String',
'Symbol',
'BigInt',
];
/**
* A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
* @param {Object} map Accumulator object for the reduce.
* @param {string} key Object key to set to `true`.
* @returns {Object} Returns the updated Object for further reduction.
*/
function invert(map, key) {
map[key] = true;
return map;
}
/**
* Creates an object with the cap is new exceptions as its keys and true as their values.
* @param {Object} config Rule configuration
* @returns {Object} Object with cap is new exceptions.
*/
function calculateCapIsNewExceptions(config) {
const capIsNewExceptions = Array.from(
new Set([...config.capIsNewExceptions, ...CAPS_ALLOWED])
);
return capIsNewExceptions.reduce(invert, {});
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Require constructor names to begin with a capital letter',
recommended: false,
url: 'https://eslint.org/docs/latest/rules/new-cap',
},
schema: [
{
type: 'object',
properties: {
newIsCap: {
type: 'boolean',
},
capIsNew: {
type: 'boolean',
},
newIsCapExceptions: {
type: 'array',
items: {
type: 'string',
},
},
newIsCapExceptionPattern: {
type: 'string',
},
capIsNewExceptions: {
type: 'array',
items: {
type: 'string',
},
},
capIsNewExceptionPattern: {
type: 'string',
},
properties: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
capIsNew: true,
capIsNewExceptions: CAPS_ALLOWED,
newIsCap: true,
newIsCapExceptions: [],
properties: true,
},
],
messages: {
upper:
'A function with a name starting with an uppercase letter should only be used as a constructor.',
lower: 'A constructor name should not start with a lowercase letter.',
},
},
create(context) {
const [config] = context.options;
const skipProperties = !config.properties;
const newIsCapExceptions = config.newIsCapExceptions.reduce(invert, {});
const newIsCapExceptionPattern =
config.newIsCapExceptionPattern ?
new RegExp(config.newIsCapExceptionPattern, 'u')
: null;
const capIsNewExceptions = calculateCapIsNewExceptions(config);
const capIsNewExceptionPattern =
config.capIsNewExceptionPattern ?
new RegExp(config.capIsNewExceptionPattern, 'u')
: null;
const listeners = {};
const sourceCode = context.sourceCode;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Get exact callee name from expression
* @param {ASTNode} node CallExpression or NewExpression node
* @returns {string} name
*/
function extractNameFromExpression(node) {
return node.callee.type === 'Identifier' ?
node.callee.name
: astUtils.getStaticPropertyName(node.callee) || '';
}
/**
* Returns the capitalization state of the string -
* Whether the first character is uppercase, lowercase, or non-alphabetic
* @param {string} str String
* @returns {string} capitalization state: "non-alpha", "lower", or "upper"
*/
function getCap(str) {
const firstChar = str.charAt(0);
const firstCharLower = firstChar.toLowerCase();
const firstCharUpper = firstChar.toUpperCase();
if (firstCharLower === firstCharUpper) {
// char has no uppercase variant, so it's non-alphabetic
return 'non-alpha';
}
if (firstChar === firstCharLower) {
return 'lower';
}
return 'upper';
}
/**
* Check if capitalization is allowed for a CallExpression
* @param {Object} allowedMap Object mapping calleeName to a Boolean
* @param {ASTNode} node CallExpression node
* @param {string} calleeName Capitalized callee name from a CallExpression
* @param {Object} pattern RegExp object from options pattern
* @returns {boolean} Returns true if the callee may be capitalized
*/
function isCapAllowed(allowedMap, node, calleeName, pattern) {
const sourceText = sourceCode.getText(node.callee);
if (allowedMap[calleeName] || allowedMap[sourceText]) {
return true;
}
if (pattern && pattern.test(sourceText)) {
return true;
}
const callee = astUtils.skipChainExpression(node.callee);
if (calleeName === 'UTC' && callee.type === 'MemberExpression') {
// allow if callee is Date.UTC
return (
callee.object.type === 'Identifier' && callee.object.name === 'Date'
);
}
return skipProperties && callee.type === 'MemberExpression';
}
/**
* Reports the given messageId for the given node. The location will be the start of the property or the callee.
* @param {ASTNode} node CallExpression or NewExpression node.
* @param {string} messageId The messageId to report.
* @returns {void}
*/
function report(node, messageId) {
let callee = astUtils.skipChainExpression(node.callee);
if (callee.type === 'MemberExpression') {
callee = callee.property;
}
context.report({ node, loc: callee.loc, messageId });
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
if (config.newIsCap) {
listeners.NewExpression = function (node) {
const constructorName = extractNameFromExpression(node);
if (constructorName) {
const capitalization = getCap(constructorName);
const isAllowed =
capitalization !== 'lower' ||
isCapAllowed(
newIsCapExceptions,
node,
constructorName,
newIsCapExceptionPattern
);
if (!isAllowed) {
report(node, 'lower');
}
}
};
}
if (config.capIsNew) {
listeners.CallExpression = function (node) {
const calleeName = extractNameFromExpression(node);
if (calleeName) {
const capitalization = getCap(calleeName);
const isAllowed =
capitalization !== 'upper' ||
isCapAllowed(
capIsNewExceptions,
node,
calleeName,
capIsNewExceptionPattern
);
if (!isAllowed) {
report(node, 'upper');
}
}
};
}
return listeners;
},
};