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.
		
		
		
		
		
			
		
			
				
					
					
						
							301 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
	
	
							301 lines
						
					
					
						
							11 KiB
						
					
					
				/** | 
						|
 * @fileoverview Rule to flag non-quoted property names in object literals. | 
						|
 * @author Mathias Bynens <http://mathiasbynens.be/> | 
						|
 */ | 
						|
"use strict"; | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Requirements | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
const espree = require("espree"), | 
						|
    keywords = require("./utils/keywords"); | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Rule Definition | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
module.exports = { | 
						|
    meta: { | 
						|
        type: "suggestion", | 
						|
 | 
						|
        docs: { | 
						|
            description: "require quotes around object literal property names", | 
						|
            category: "Stylistic Issues", | 
						|
            recommended: false, | 
						|
            url: "https://eslint.org/docs/rules/quote-props" | 
						|
        }, | 
						|
 | 
						|
        schema: { | 
						|
            anyOf: [ | 
						|
                { | 
						|
                    type: "array", | 
						|
                    items: [ | 
						|
                        { | 
						|
                            enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | 
						|
                        } | 
						|
                    ], | 
						|
                    minItems: 0, | 
						|
                    maxItems: 1 | 
						|
                }, | 
						|
                { | 
						|
                    type: "array", | 
						|
                    items: [ | 
						|
                        { | 
						|
                            enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | 
						|
                        }, | 
						|
                        { | 
						|
                            type: "object", | 
						|
                            properties: { | 
						|
                                keywords: { | 
						|
                                    type: "boolean" | 
						|
                                }, | 
						|
                                unnecessary: { | 
						|
                                    type: "boolean" | 
						|
                                }, | 
						|
                                numbers: { | 
						|
                                    type: "boolean" | 
						|
                                } | 
						|
                            }, | 
						|
                            additionalProperties: false | 
						|
                        } | 
						|
                    ], | 
						|
                    minItems: 0, | 
						|
                    maxItems: 2 | 
						|
                } | 
						|
            ] | 
						|
        }, | 
						|
 | 
						|
        fixable: "code" | 
						|
    }, | 
						|
 | 
						|
    create(context) { | 
						|
 | 
						|
        const MODE = context.options[0], | 
						|
            KEYWORDS = context.options[1] && context.options[1].keywords, | 
						|
            CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false, | 
						|
            NUMBERS = context.options[1] && context.options[1].numbers, | 
						|
 | 
						|
            MESSAGE_UNNECESSARY = "Unnecessarily quoted property '{{property}}' found.", | 
						|
            MESSAGE_UNQUOTED = "Unquoted property '{{property}}' found.", | 
						|
            MESSAGE_NUMERIC = "Unquoted number literal '{{property}}' used as key.", | 
						|
            MESSAGE_RESERVED = "Unquoted reserved word '{{property}}' used as key.", | 
						|
            sourceCode = context.getSourceCode(); | 
						|
 | 
						|
 | 
						|
        /** | 
						|
         * Checks whether a certain string constitutes an ES3 token | 
						|
         * @param   {string} tokenStr The string to be checked. | 
						|
         * @returns {boolean} `true` if it is an ES3 token. | 
						|
         */ | 
						|
        function isKeyword(tokenStr) { | 
						|
            return keywords.indexOf(tokenStr) >= 0; | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary) | 
						|
         * @param   {string} rawKey The raw key value from the source | 
						|
         * @param   {espreeTokens} tokens The espree-tokenized node key | 
						|
         * @param   {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked | 
						|
         * @returns {boolean} Whether or not a key has redundant quotes. | 
						|
         * @private | 
						|
         */ | 
						|
        function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) { | 
						|
            return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length && | 
						|
                (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || | 
						|
                (tokens[0].type === "Numeric" && !skipNumberLiterals && String(+tokens[0].value) === tokens[0].value)); | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Returns a string representation of a property node with quotes removed | 
						|
         * @param {ASTNode} key Key AST Node, which may or may not be quoted | 
						|
         * @returns {string} A replacement string for this property | 
						|
         */ | 
						|
        function getUnquotedKey(key) { | 
						|
            return key.type === "Identifier" ? key.name : key.value; | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Returns a string representation of a property node with quotes added | 
						|
         * @param {ASTNode} key Key AST Node, which may or may not be quoted | 
						|
         * @returns {string} A replacement string for this property | 
						|
         */ | 
						|
        function getQuotedKey(key) { | 
						|
            if (key.type === "Literal" && typeof key.value === "string") { | 
						|
 | 
						|
                // If the key is already a string literal, don't replace the quotes with double quotes. | 
						|
                return sourceCode.getText(key); | 
						|
            } | 
						|
 | 
						|
            // Otherwise, the key is either an identifier or a number literal. | 
						|
            return `"${key.type === "Identifier" ? key.name : key.value}"`; | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Ensures that a property's key is quoted only when necessary | 
						|
         * @param   {ASTNode} node Property AST node | 
						|
         * @returns {void} | 
						|
         */ | 
						|
        function checkUnnecessaryQuotes(node) { | 
						|
            const key = node.key; | 
						|
 | 
						|
            if (node.method || node.computed || node.shorthand) { | 
						|
                return; | 
						|
            } | 
						|
 | 
						|
            if (key.type === "Literal" && typeof key.value === "string") { | 
						|
                let tokens; | 
						|
 | 
						|
                try { | 
						|
                    tokens = espree.tokenize(key.value); | 
						|
                } catch (e) { | 
						|
                    return; | 
						|
                } | 
						|
 | 
						|
                if (tokens.length !== 1) { | 
						|
                    return; | 
						|
                } | 
						|
 | 
						|
                const isKeywordToken = isKeyword(tokens[0].value); | 
						|
 | 
						|
                if (isKeywordToken && KEYWORDS) { | 
						|
                    return; | 
						|
                } | 
						|
 | 
						|
                if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { | 
						|
                    context.report({ | 
						|
                        node, | 
						|
                        message: MESSAGE_UNNECESSARY, | 
						|
                        data: { property: key.value }, | 
						|
                        fix: fixer => fixer.replaceText(key, getUnquotedKey(key)) | 
						|
                    }); | 
						|
                } | 
						|
            } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { | 
						|
                context.report({ | 
						|
                    node, | 
						|
                    message: MESSAGE_RESERVED, | 
						|
                    data: { property: key.name }, | 
						|
                    fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | 
						|
                }); | 
						|
            } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") { | 
						|
                context.report({ | 
						|
                    node, | 
						|
                    message: MESSAGE_NUMERIC, | 
						|
                    data: { property: key.value }, | 
						|
                    fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | 
						|
                }); | 
						|
            } | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Ensures that a property's key is quoted | 
						|
         * @param   {ASTNode} node Property AST node | 
						|
         * @returns {void} | 
						|
         */ | 
						|
        function checkOmittedQuotes(node) { | 
						|
            const key = node.key; | 
						|
 | 
						|
            if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { | 
						|
                context.report({ | 
						|
                    node, | 
						|
                    message: MESSAGE_UNQUOTED, | 
						|
                    data: { property: key.name || key.value }, | 
						|
                    fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | 
						|
                }); | 
						|
            } | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes | 
						|
         * @param   {ASTNode} node Property AST node | 
						|
         * @param   {boolean} checkQuotesRedundancy Whether to check quotes' redundancy | 
						|
         * @returns {void} | 
						|
         */ | 
						|
        function checkConsistency(node, checkQuotesRedundancy) { | 
						|
            const quotedProps = [], | 
						|
                unquotedProps = []; | 
						|
            let keywordKeyName = null, | 
						|
                necessaryQuotes = false; | 
						|
 | 
						|
            node.properties.forEach(property => { | 
						|
                const key = property.key; | 
						|
 | 
						|
                if (!key || property.method || property.computed || property.shorthand) { | 
						|
                    return; | 
						|
                } | 
						|
 | 
						|
                if (key.type === "Literal" && typeof key.value === "string") { | 
						|
 | 
						|
                    quotedProps.push(property); | 
						|
 | 
						|
                    if (checkQuotesRedundancy) { | 
						|
                        let tokens; | 
						|
 | 
						|
                        try { | 
						|
                            tokens = espree.tokenize(key.value); | 
						|
                        } catch (e) { | 
						|
                            necessaryQuotes = true; | 
						|
                            return; | 
						|
                        } | 
						|
 | 
						|
                        necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); | 
						|
                    } | 
						|
                } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { | 
						|
                    unquotedProps.push(property); | 
						|
                    necessaryQuotes = true; | 
						|
                    keywordKeyName = key.name; | 
						|
                } else { | 
						|
                    unquotedProps.push(property); | 
						|
                } | 
						|
            }); | 
						|
 | 
						|
            if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) { | 
						|
                quotedProps.forEach(property => { | 
						|
                    context.report({ | 
						|
                        node: property, | 
						|
                        message: "Properties shouldn't be quoted as all quotes are redundant.", | 
						|
                        fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key)) | 
						|
                    }); | 
						|
                }); | 
						|
            } else if (unquotedProps.length && keywordKeyName) { | 
						|
                unquotedProps.forEach(property => { | 
						|
                    context.report({ | 
						|
                        node: property, | 
						|
                        message: "Properties should be quoted as '{{property}}' is a reserved word.", | 
						|
                        data: { property: keywordKeyName }, | 
						|
                        fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | 
						|
                    }); | 
						|
                }); | 
						|
            } else if (quotedProps.length && unquotedProps.length) { | 
						|
                unquotedProps.forEach(property => { | 
						|
                    context.report({ | 
						|
                        node: property, | 
						|
                        message: "Inconsistently quoted property '{{key}}' found.", | 
						|
                        data: { key: property.key.name || property.key.value }, | 
						|
                        fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | 
						|
                    }); | 
						|
                }); | 
						|
            } | 
						|
        } | 
						|
 | 
						|
        return { | 
						|
            Property(node) { | 
						|
                if (MODE === "always" || !MODE) { | 
						|
                    checkOmittedQuotes(node); | 
						|
                } | 
						|
                if (MODE === "as-needed") { | 
						|
                    checkUnnecessaryQuotes(node); | 
						|
                } | 
						|
            }, | 
						|
            ObjectExpression(node) { | 
						|
                if (MODE === "consistent") { | 
						|
                    checkConsistency(node, false); | 
						|
                } | 
						|
                if (MODE === "consistent-as-needed") { | 
						|
                    checkConsistency(node, true); | 
						|
                } | 
						|
            } | 
						|
        }; | 
						|
 | 
						|
    } | 
						|
};
 | 
						|
 |