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.
		
		
		
		
			
				
					230 lines
				
				7.4 KiB
			
		
		
			
		
	
	
					230 lines
				
				7.4 KiB
			| 
								 
											4 years ago
										 
									 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @fileoverview Rule to flag use of variables before they are defined
							 | 
						||
| 
								 | 
							
								 * @author Ilya Volodin
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								"use strict";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								//------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								// Helpers
							 | 
						||
| 
								 | 
							
								//------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
							 | 
						||
| 
								 | 
							
								const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Parses a given value as options.
							 | 
						||
| 
								 | 
							
								 * @param {any} options A value to parse.
							 | 
						||
| 
								 | 
							
								 * @returns {Object} The parsed options.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function parseOptions(options) {
							 | 
						||
| 
								 | 
							
								    let functions = true;
							 | 
						||
| 
								 | 
							
								    let classes = true;
							 | 
						||
| 
								 | 
							
								    let variables = true;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (typeof options === "string") {
							 | 
						||
| 
								 | 
							
								        functions = (options !== "nofunc");
							 | 
						||
| 
								 | 
							
								    } else if (typeof options === "object" && options !== null) {
							 | 
						||
| 
								 | 
							
								        functions = options.functions !== false;
							 | 
						||
| 
								 | 
							
								        classes = options.classes !== false;
							 | 
						||
| 
								 | 
							
								        variables = options.variables !== false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return { functions, classes, variables };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks whether or not a given variable is a function declaration.
							 | 
						||
| 
								 | 
							
								 * @param {eslint-scope.Variable} variable A variable to check.
							 | 
						||
| 
								 | 
							
								 * @returns {boolean} `true` if the variable is a function declaration.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function isFunction(variable) {
							 | 
						||
| 
								 | 
							
								    return variable.defs[0].type === "FunctionName";
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks whether or not a given variable is a class declaration in an upper function scope.
							 | 
						||
| 
								 | 
							
								 * @param {eslint-scope.Variable} variable A variable to check.
							 | 
						||
| 
								 | 
							
								 * @param {eslint-scope.Reference} reference A reference to check.
							 | 
						||
| 
								 | 
							
								 * @returns {boolean} `true` if the variable is a class declaration.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function isOuterClass(variable, reference) {
							 | 
						||
| 
								 | 
							
								    return (
							 | 
						||
| 
								 | 
							
								        variable.defs[0].type === "ClassName" &&
							 | 
						||
| 
								 | 
							
								        variable.scope.variableScope !== reference.from.variableScope
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks whether or not a given variable is a variable declaration in an upper function scope.
							 | 
						||
| 
								 | 
							
								 * @param {eslint-scope.Variable} variable A variable to check.
							 | 
						||
| 
								 | 
							
								 * @param {eslint-scope.Reference} reference A reference to check.
							 | 
						||
| 
								 | 
							
								 * @returns {boolean} `true` if the variable is a variable declaration.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function isOuterVariable(variable, reference) {
							 | 
						||
| 
								 | 
							
								    return (
							 | 
						||
| 
								 | 
							
								        variable.defs[0].type === "Variable" &&
							 | 
						||
| 
								 | 
							
								        variable.scope.variableScope !== reference.from.variableScope
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks whether or not a given location is inside of the range of a given node.
							 | 
						||
| 
								 | 
							
								 * @param {ASTNode} node An node to check.
							 | 
						||
| 
								 | 
							
								 * @param {number} location A location to check.
							 | 
						||
| 
								 | 
							
								 * @returns {boolean} `true` if the location is inside of the range of the node.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function isInRange(node, location) {
							 | 
						||
| 
								 | 
							
								    return node && node.range[0] <= location && location <= node.range[1];
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks whether or not a given reference is inside of the initializers of a given variable.
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * This returns `true` in the following cases:
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 *     var a = a
							 | 
						||
| 
								 | 
							
								 *     var [a = a] = list
							 | 
						||
| 
								 | 
							
								 *     var {a = a} = obj
							 | 
						||
| 
								 | 
							
								 *     for (var a in a) {}
							 | 
						||
| 
								 | 
							
								 *     for (var a of a) {}
							 | 
						||
| 
								 | 
							
								 * @param {Variable} variable A variable to check.
							 | 
						||
| 
								 | 
							
								 * @param {Reference} reference A reference to check.
							 | 
						||
| 
								 | 
							
								 * @returns {boolean} `true` if the reference is inside of the initializers.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								function isInInitializer(variable, reference) {
							 | 
						||
| 
								 | 
							
								    if (variable.scope !== reference.from) {
							 | 
						||
| 
								 | 
							
								        return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let node = variable.identifiers[0].parent;
							 | 
						||
| 
								 | 
							
								    const location = reference.identifier.range[1];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    while (node) {
							 | 
						||
| 
								 | 
							
								        if (node.type === "VariableDeclarator") {
							 | 
						||
| 
								 | 
							
								            if (isInRange(node.init, location)) {
							 | 
						||
| 
								 | 
							
								                return true;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (FOR_IN_OF_TYPE.test(node.parent.parent.type) &&
							 | 
						||
| 
								 | 
							
								                isInRange(node.parent.parent.right, location)
							 | 
						||
| 
								 | 
							
								            ) {
							 | 
						||
| 
								 | 
							
								                return true;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								        } else if (node.type === "AssignmentPattern") {
							 | 
						||
| 
								 | 
							
								            if (isInRange(node.right, location)) {
							 | 
						||
| 
								 | 
							
								                return true;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        } else if (SENTINEL_TYPE.test(node.type)) {
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        node = node.parent;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return false;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								//------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								// Rule Definition
							 | 
						||
| 
								 | 
							
								//------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = {
							 | 
						||
| 
								 | 
							
								    meta: {
							 | 
						||
| 
								 | 
							
								        type: "problem",
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        docs: {
							 | 
						||
| 
								 | 
							
								            description: "disallow the use of variables before they are defined",
							 | 
						||
| 
								 | 
							
								            category: "Variables",
							 | 
						||
| 
								 | 
							
								            recommended: false,
							 | 
						||
| 
								 | 
							
								            url: "https://eslint.org/docs/rules/no-use-before-define"
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        schema: [
							 | 
						||
| 
								 | 
							
								            {
							 | 
						||
| 
								 | 
							
								                oneOf: [
							 | 
						||
| 
								 | 
							
								                    {
							 | 
						||
| 
								 | 
							
								                        enum: ["nofunc"]
							 | 
						||
| 
								 | 
							
								                    },
							 | 
						||
| 
								 | 
							
								                    {
							 | 
						||
| 
								 | 
							
								                        type: "object",
							 | 
						||
| 
								 | 
							
								                        properties: {
							 | 
						||
| 
								 | 
							
								                            functions: { type: "boolean" },
							 | 
						||
| 
								 | 
							
								                            classes: { type: "boolean" },
							 | 
						||
| 
								 | 
							
								                            variables: { type: "boolean" }
							 | 
						||
| 
								 | 
							
								                        },
							 | 
						||
| 
								 | 
							
								                        additionalProperties: false
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                ]
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    create(context) {
							 | 
						||
| 
								 | 
							
								        const options = parseOptions(context.options[0]);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        /**
							 | 
						||
| 
								 | 
							
								         * Determines whether a given use-before-define case should be reported according to the options.
							 | 
						||
| 
								 | 
							
								         * @param {eslint-scope.Variable} variable The variable that gets used before being defined
							 | 
						||
| 
								 | 
							
								         * @param {eslint-scope.Reference} reference The reference to the variable
							 | 
						||
| 
								 | 
							
								         * @returns {boolean} `true` if the usage should be reported
							 | 
						||
| 
								 | 
							
								         */
							 | 
						||
| 
								 | 
							
								        function isForbidden(variable, reference) {
							 | 
						||
| 
								 | 
							
								            if (isFunction(variable)) {
							 | 
						||
| 
								 | 
							
								                return options.functions;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (isOuterClass(variable, reference)) {
							 | 
						||
| 
								 | 
							
								                return options.classes;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (isOuterVariable(variable, reference)) {
							 | 
						||
| 
								 | 
							
								                return options.variables;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return true;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        /**
							 | 
						||
| 
								 | 
							
								         * Finds and validates all variables in a given scope.
							 | 
						||
| 
								 | 
							
								         * @param {Scope} scope The scope object.
							 | 
						||
| 
								 | 
							
								         * @returns {void}
							 | 
						||
| 
								 | 
							
								         * @private
							 | 
						||
| 
								 | 
							
								         */
							 | 
						||
| 
								 | 
							
								        function findVariablesInScope(scope) {
							 | 
						||
| 
								 | 
							
								            scope.references.forEach(reference => {
							 | 
						||
| 
								 | 
							
								                const variable = reference.resolved;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                /*
							 | 
						||
| 
								 | 
							
								                 * Skips when the reference is:
							 | 
						||
| 
								 | 
							
								                 * - initialization's.
							 | 
						||
| 
								 | 
							
								                 * - referring to an undefined variable.
							 | 
						||
| 
								 | 
							
								                 * - referring to a global environment variable (there're no identifiers).
							 | 
						||
| 
								 | 
							
								                 * - located preceded by the variable (except in initializers).
							 | 
						||
| 
								 | 
							
								                 * - allowed by options.
							 | 
						||
| 
								 | 
							
								                 */
							 | 
						||
| 
								 | 
							
								                if (reference.init ||
							 | 
						||
| 
								 | 
							
								                    !variable ||
							 | 
						||
| 
								 | 
							
								                    variable.identifiers.length === 0 ||
							 | 
						||
| 
								 | 
							
								                    (variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) ||
							 | 
						||
| 
								 | 
							
								                    !isForbidden(variable, reference)
							 | 
						||
| 
								 | 
							
								                ) {
							 | 
						||
| 
								 | 
							
								                    return;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                // Reports.
							 | 
						||
| 
								 | 
							
								                context.report({
							 | 
						||
| 
								 | 
							
								                    node: reference.identifier,
							 | 
						||
| 
								 | 
							
								                    message: "'{{name}}' was used before it was defined.",
							 | 
						||
| 
								 | 
							
								                    data: reference.identifier
							 | 
						||
| 
								 | 
							
								                });
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            scope.childScopes.forEach(findVariablesInScope);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return {
							 | 
						||
| 
								 | 
							
								            Program() {
							 | 
						||
| 
								 | 
							
								                findVariablesInScope(context.getScope());
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        };
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								};
							 |