123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- /**
- * @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"
- ];
- /**
- * Ensure that if the key is provided, it must be an array.
- * @param {Object} obj Object to check with `key`.
- * @param {string} key Object key to check on `obj`.
- * @param {*} fallback If obj[key] is not present, this will be returned.
- * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
- */
- function checkArray(obj, key, fallback) {
- /* istanbul ignore if */
- if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
- throw new TypeError(`${key}, if provided, must be an Array`);
- }
- return obj[key] || fallback;
- }
- /**
- * 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) {
- let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
- if (capIsNewExceptions !== CAPS_ALLOWED) {
- capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
- }
- return capIsNewExceptions.reduce(invert, {});
- }
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
- module.exports = {
- meta: {
- type: "suggestion",
- docs: {
- description: "require constructor names to begin with a capital letter",
- category: "Stylistic Issues",
- recommended: false,
- url: "https://eslint.org/docs/rules/new-cap"
- },
- schema: [
- {
- type: "object",
- properties: {
- newIsCap: {
- type: "boolean",
- default: true
- },
- capIsNew: {
- type: "boolean",
- default: true
- },
- newIsCapExceptions: {
- type: "array",
- items: {
- type: "string"
- }
- },
- newIsCapExceptionPattern: {
- type: "string"
- },
- capIsNewExceptions: {
- type: "array",
- items: {
- type: "string"
- }
- },
- capIsNewExceptionPattern: {
- type: "string"
- },
- properties: {
- type: "boolean",
- default: true
- }
- },
- additionalProperties: false
- }
- ],
- 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 = Object.assign({}, context.options[0]);
- config.newIsCap = config.newIsCap !== false;
- config.capIsNew = config.capIsNew !== false;
- const skipProperties = config.properties === false;
- const newIsCapExceptions = checkArray(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.getSourceCode();
- //--------------------------------------------------------------------------
- // 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;
- }
- };
|