UNPKG

@mysticatea/eslint-plugin

Version:
293 lines (255 loc) 9.05 kB
/** * @author Toru Nagashima * @copyright 2015 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ "use strict" //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const scopeNodeType = /^(?:(?:Block|Switch|For(?:In|Of)?)Statement|CatchClause|Program)$/u const containerNodeType = /^(?:For(?:In|Of)?Statement|(?:Arrow)?Function(?:Declaration|Expression))$/u /** * Checks whether or not a given definition should be skipped. * @param {escope.Variable.DefEntry} def - A definition to check. * @param {escope.Variable.DefEntry[]} defs - A definition list which includes `def`. * @param {escope.Variable} variable - A variable which is defined by the definition. * @returns {boolean} Whether or not the definition should be skipped. */ function shouldSkip(def, defs, variable) { // To check re-declarations. if (defs.length >= 2 && def.type !== "TDZ") { return false } switch (def.type) { case "ClassName": case "FunctionName": return variable.scope.block === def.node case "Parameter": case "TDZ": return true case "Variable": return def.parent.kind !== "var" default: return false } } /** * Pseudo scope information for `var`. * Finds and creates information of a containing scope of a given declaration. */ class PseudoScope { /** * @param {escope.Variable.DefineEntry} def - A declaration. */ constructor(def) { let node = null if (def.type === "Parameter") { node = def.node } else { node = (def.parent || def.node).parent while (!scopeNodeType.test(node.type)) { node = node.parent } if ( node.parent != null && containerNodeType.test(node.parent.type) ) { node = node.parent } } /** * The `Identifier` node of the declaration. * @type {ASTNode} */ this.identifier = def.name /** * The start position of the scope. * @type {number} */ this.start = node.range[0] /** * The end position of the scope. * @type {number} */ this.end = node.range[1] /** * The `Identifier` nodes of re-declarations. * @type {ASTNode[]} */ this.redeclarations = [] /** * The `PseudoScope` instances which are nested. * @type {PseudoScope[]} */ this.children = [] /** * The flag of shadowing. * @type {boolean} */ this.shadowing = false /** * The flag of used. * @type {boolean} */ this.used = false } /** * Creates pseudo scopes of a given variable. * @param {escope.Variable} variable - A variable to create. * @returns {PseudoScope[]} the created scopes. */ static createScopesFrom(variable) { const defs = variable.defs const scopes = [] for (const def of defs) { if (!shouldSkip(def, defs, variable)) { PseudoScope.push(scopes, new PseudoScope(def)) } } return scopes } /** * Adds a given scope into a given scope list. * This considers re-declarations and shadowing. * @param {PseudoScope[]} scopes - Scopes to be added. * @param {PseudoScope} newScope - A scope to add. * @returns {void} */ static push(scopes, newScope) { for (const scope of scopes) { if (scope.start === newScope.start && scope.end === newScope.end) { scope.redeclarations.push(newScope.identifier) return } if (scope.start <= newScope.start && newScope.end <= scope.end) { newScope.markAsShadowing() PseudoScope.push(scope.children, newScope) return } } scopes.push(newScope) } /** * Finds a containing scope of a given reference. * @param {PseudoScope[]} scopes - Scopes to be domain. * @param {escope.Reference} reference - A reference to find. * @returns {PseudoScope|null} A containing scope of the reference. */ static findScope(scopes, reference) { const range = reference.identifier.range for (const scope of scopes) { if (scope.start <= range[0] && range[1] <= scope.end) { return PseudoScope.findScope(scope.children, reference) || scope } } return null } /** * Turns a shadowing flag on. * @returns {void} */ markAsShadowing() { this.shadowing = true } /** * Turns an used flag on. * @returns {void} */ markAsUsed() { this.used = true } } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "disallow illegal usage of variables as block-scoped.", category: "Possible Errors", recommended: false, url: "https://github.com/mysticatea/eslint-plugin/blob/v13.0.0/docs/rules/block-scoped-var.md", }, fixable: null, schema: [], type: "suggestion", }, create(context) { /** * Finds and reports references which are outside of valid scopes. * @param {ASTNode} node - A node to get variables. * @returns {void} */ function checkForVariables(node) { const variables = context.getDeclaredVariables(node) for (const variable of variables) { const defs = variable.defs const lastDef = defs[defs.length - 1] // Skip except the last declaration. // Because `node.parent` is possibly not defined. if ((lastDef.parent || lastDef.node) !== node) { continue } // Collect the containing scopes. const scopes = PseudoScope.createScopesFrom(variable) if (scopes.length === 0) { continue } // Check whether or not any reading reference exists. // And while it does, warn references which does not belong to any // scope. let hasReadRef = false for (const reference of variable.references) { const scope = PseudoScope.findScope(scopes, reference) if (reference.isRead()) { hasReadRef = true if (scope != null) { scope.markAsUsed() } } if (scope == null) { context.report({ node: reference.identifier, message: '"{{name}}" is not defined.', data: { name: reference.identifier.name }, }) } } // Warn re-declarations, shadowing, and unused. scopes.forEach(function walk(scope) { for (const identifier of scope.redeclarations) { context.report({ node: identifier, message: '"{{name}}" is already defined.', data: { name: identifier.name }, }) } if (scope.shadowing) { context.report({ node: scope.identifier, message: '"{{name}}" is already defined in the upper scope.', data: { name: scope.identifier.name }, }) } if (hasReadRef && !scope.used) { context.report({ node: scope.identifier, message: '"{{name}}" is defined but never used.', data: { name: scope.identifier.name }, }) } scope.children.forEach(walk) }) } } return { VariableDeclaration: checkForVariables, FunctionDeclaration: checkForVariables, ClassDeclaration: checkForVariables, ImportDeclaration: checkForVariables, } }, }