You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							283 lines
						
					
					
						
							9.4 KiB
						
					
					
				
			
		
		
	
	
							283 lines
						
					
					
						
							9.4 KiB
						
					
					
				/** | 
						|
 * @fileoverview Rule to flag use of constructors without capital letters | 
						|
 * @author Nicholas C. Zakas | 
						|
 */ | 
						|
 | 
						|
"use strict"; | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Requirements | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// 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) { | 
						|
 | 
						|
            let name = ""; | 
						|
 | 
						|
            if (node.callee.type === "MemberExpression") { | 
						|
                const property = node.callee.property; | 
						|
 | 
						|
                if (property.type === "Literal" && (typeof property.value === "string")) { | 
						|
                    name = property.value; | 
						|
                } else if (property.type === "Identifier" && !node.callee.computed) { | 
						|
                    name = property.name; | 
						|
                } | 
						|
            } else { | 
						|
                name = node.callee.name; | 
						|
            } | 
						|
            return name; | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * 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; | 
						|
            } | 
						|
 | 
						|
            if (calleeName === "UTC" && node.callee.type === "MemberExpression") { | 
						|
 | 
						|
                // allow if callee is Date.UTC | 
						|
                return node.callee.object.type === "Identifier" && | 
						|
                    node.callee.object.name === "Date"; | 
						|
            } | 
						|
 | 
						|
            return skipProperties && node.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 = node.callee; | 
						|
 | 
						|
            if (callee.type === "MemberExpression") { | 
						|
                callee = callee.property; | 
						|
            } | 
						|
 | 
						|
            context.report({ node, loc: callee.loc.start, 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; | 
						|
    } | 
						|
};
 | 
						|
 |