/** * @fileoverview Rule to flag use of variables before they are defined * @author Ilya Volodin * @copyright 2013 Ilya Volodin. All rights reserved. */ "use strict"; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ var SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/; /** * Parses a given value as options. * * @param {any} options - A value to parse. * @returns {object} The parsed options. */ function parseOptions(options) { var functions = true; var classes = true; if (typeof options === "string") { functions = (options !== "nofunc"); } else if (typeof options === "object" && options !== null) { functions = options.functions !== false; classes = options.classes !== false; } return {functions: functions, classes: classes}; } /** * @returns {boolean} `false`. */ function alwaysFalse() { return false; } /** * Checks whether or not a given variable is a function declaration. * * @param {escope.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 {escope.Variable} variable - A variable to check. * @param {escope.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 function declaration or a class declaration in an upper function scope. * * @param {escope.Variable} variable - A variable to check. * @param {escope.Reference} reference - A reference to check. * @returns {boolean} `true` if the variable is a function declaration or a class declaration. */ function isFunctionOrOuterClass(variable, reference) { return isFunction(variable, reference) || isOuterClass(variable, reference); } /** * 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. * * @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; } var node = variable.identifiers[0].parent; var location = reference.identifier.range[1]; while (node) { if (node.type === "VariableDeclarator") { if (isInRange(node.init, 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 = function(context) { var options = parseOptions(context.options[0]); // Defines a function which checks whether or not a reference is allowed according to the option. var isAllowed; if (options.functions && options.classes) { isAllowed = alwaysFalse; } else if (options.functions) { isAllowed = isOuterClass; } else if (options.classes) { isAllowed = isFunction; } else { isAllowed = isFunctionOrOuterClass; } /** * Finds and validates all variables in a given scope. * @param {Scope} scope The scope object. * @returns {void} * @private */ function findVariablesInScope(scope) { scope.references.forEach(function(reference) { var 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)) || isAllowed(variable, reference) ) { return; } // Reports. context.report({ node: reference.identifier, message: "'{{name}}' was used before it was defined", data: reference.identifier }); }); } /** * Validates variables inside of a node's scope. * @param {ASTNode} node The node to check. * @returns {void} * @private */ function findVariables() { var scope = context.getScope(); findVariablesInScope(scope); } var ruleDefinition = { "Program:exit": function(node) { var scope = context.getScope(), ecmaFeatures = context.parserOptions.ecmaFeatures || {}; findVariablesInScope(scope); // both Node.js and Modules have an extra scope if (ecmaFeatures.globalReturn || node.sourceType === "module") { findVariablesInScope(scope.childScopes[0]); } } }; if (context.parserOptions.ecmaVersion >= 6) { ruleDefinition["BlockStatement:exit"] = ruleDefinition["SwitchStatement:exit"] = findVariables; ruleDefinition["ArrowFunctionExpression:exit"] = function(node) { if (node.body.type !== "BlockStatement") { findVariables(node); } }; } else { ruleDefinition["FunctionExpression:exit"] = ruleDefinition["FunctionDeclaration:exit"] = ruleDefinition["ArrowFunctionExpression:exit"] = findVariables; } return ruleDefinition; }; module.exports.schema = [ { "oneOf": [ { "enum": ["nofunc"] }, { "type": "object", "properties": { "functions": {"type": "boolean"}, "classes": {"type": "boolean"} }, "additionalProperties": false } ] } ];