ESLint Scope is an ECMAScript scope analyzer that identifies variable scopes, references, and declarations in JavaScript AST nodes. It analyzes the lexical scope structure of JavaScript code to understand variable binding, usage patterns, and scope relationships for static analysis tools.
import { analyze } from "eslint-scope";For CommonJS:
const { analyze } = require("eslint-scope");Named imports (ESM):
import {
analyze,
ScopeManager,
Scope,
Reference,
Variable,
Definition
} from "eslint-scope";import * as espree from "espree";
import { analyze } from "eslint-scope";
// Parse JavaScript code
const ast = espree.parse(`
let globalVar = 1;
function outer(param) {
let localVar = 2;
function inner() {
return globalVar + localVar + param;
}
return inner;
}
`, { ecmaVersion: 2022, sourceType: "module" });
// Analyze scope structure
const scopeManager = analyze(ast, {
ecmaVersion: 2022,
sourceType: "module"
});
// Access scopes and references
console.log(scopeManager.scopes.length); // 4 (global, outer function, inner function, module)
console.log(scopeManager.globalScope.variables.map(v => v.name)); // ["globalVar", "outer"]Core function that analyzes AST and returns comprehensive scope information.
/**
* Analyzes AST and returns scope information
* @param {espree.Tree} tree - AST from espree.parse()
* @param {AnalysisOptions} options - Analysis configuration options
* @returns {ScopeManager} Scope manager with all scopes and references
*/
function analyze(tree, options);Manages all scopes in the analyzed code and provides methods to query scope information.
class ScopeManager {
/** Array of all scopes in the program */
scopes: Scope[];
/** The global scope */
globalScope: Scope;
/**
* Get the scope that contains the given AST node
* @param {espree.Node} node - AST node
* @param {boolean} inner - If true, get inner-most scope
* @returns {Scope | null} Containing scope or null
*/
acquire(node, inner);
/**
* Get variables declared by the given AST node
* @param {espree.Node} node - AST node (usually a declaration)
* @returns {Variable[]} Array of declared variables
*/
getDeclaredVariables(node);
/** Check if this is a global return (CommonJS-style) */
isGlobalReturn();
/** Check if this is ES module mode */
isModule();
/** Check if strict mode is implied */
isImpliedStrict();
/** Check if strict mode is supported */
isStrictModeSupported();
}Represents an individual lexical scope with its variables, references, and scope chain.
class Scope {
/** Scope type: "global", "module", "function", "block", "switch", "catch", "with", "for", "class" */
type: string;
/** Whether this scope is in strict mode */
isStrict: boolean;
/** Variables declared in this scope */
variables: Variable[];
/** References to variables from this scope */
references: Reference[];
/** The scope that defines variables (for block scopes, this points to function scope) */
variableScope: Scope;
/** Whether this is a function expression scope */
functionExpressionScope: boolean;
/** Whether this scope contains a direct call to eval() */
directCallToEvalScope: boolean;
/** Whether 'this' is referenced in this scope */
thisFound: boolean;
/** Map of variable names to Variable objects */
set: Map<string, Variable>;
/** Map tracking tainted variables */
taints: Map<string, boolean>;
/** Whether this scope is dynamic (contains with or eval) */
dynamic: boolean;
/** AST node that defines this scope */
block: espree.Node;
/** References that could not be resolved in this scope */
through: Reference[];
/** Parent scope in the scope chain */
upper: Scope;
/** Check if this is a static scope (not dynamic) */
isStatic();
/**
* Resolve a reference to a variable
* @param {espree.Identifier} ident - Identifier to resolve
* @returns {Reference} Reference object
*/
resolve(ident);
/** Check if the arguments object is materialized */
isArgumentsMaterialized();
/** Check if 'this' is materialized */
isThisMaterialized();
/**
* Check if a name is used in this scope
* @param {string} name - Variable name
* @returns {boolean} True if name is used
*/
isUsedName(name);
}Represents a reference to a variable, including information about how it's used.
class Reference {
/** The identifier AST node */
identifier: espree.Identifier;
/** The scope this reference comes from */
from: Scope;
/** Whether this reference is tainted by dynamic scope */
tainted: boolean;
/** The variable this reference resolves to (null if unresolved) */
resolved: Variable | null;
/** Reference flags: READ (1), WRITE (2), RW (3) */
flag: number;
/** Expression being written to this reference (for write references) */
writeExpr?: espree.Node;
/** Whether this is a partial write (destructuring) */
partial?: boolean;
/** Whether this is an initialization write */
init?: boolean;
/** Check if this is a static reference (not dynamic) */
isStatic();
/** Check if this reference reads the variable */
isRead();
/** Check if this reference writes to the variable */
isWrite();
/** Check if this reference only reads the variable */
isReadOnly();
/** Check if this reference only writes to the variable */
isWriteOnly();
/** Check if this reference both reads and writes the variable */
isReadWrite();
}Represents a variable declaration with all its references and definition sites.
class Variable {
/** Variable name */
name: string;
/** Identifier nodes where this variable is declared */
identifiers: espree.Identifier[];
/** All references to this variable */
references: Reference[];
/** All definition sites of this variable */
defs: Definition[];
/** Whether this variable is tainted by dynamic scope */
tainted: boolean;
/** Whether this variable needs to be stored on the stack */
stack: boolean;
/** The scope this variable belongs to */
scope: Scope;
}Represents a definition site of a variable (declaration, parameter, etc.).
class Definition {
/** Definition type: "Variable", "Parameter", "FunctionName", "ClassName", "CatchClause", "ImportBinding", "ImplicitGlobalVariable" */
type: string;
/** Variable name being defined */
name: espree.Identifier;
/** AST node containing the definition */
node: espree.Node;
/** Parent AST node of the definition */
parent?: espree.Node;
/** Index of parameter (for Parameter definitions) */
index?: number;
/** Whether this is a rest parameter */
rest?: boolean;
}interface AnalysisOptions {
/** Assume code is optimistic (treat unknown references as global) */
optimistic?: boolean; // Default: false
/** Enable Node.js scope analysis */
nodejsScope?: boolean; // Default: false
/** Enable implied strict mode */
impliedStrict?: boolean; // Default: false
/** Source type: "script", "module", or "commonjs" */
sourceType?: "script" | "module" | "commonjs"; // Default: "script"
/** ECMAScript version (3, 5, 6-17, 2015-2026, or "latest") */
ecmaVersion?: number | "latest"; // Default: 5
/** Enable JSX parsing */
jsx?: boolean; // Default: false
/** Additional visitor keys for custom AST nodes */
childVisitorKeys?: VisitorKeys;
/** Fallback strategy for unknown nodes: "iteration" */
fallback?: string; // Default: "iteration"
/** Ignore eval() calls in scope analysis */
ignoreEval?: boolean; // Default: false
}Scope hierarchy analysis:
import * as espree from "espree";
import { analyze } from "eslint-scope";
const ast = espree.parse(`
let global = 1;
function outer() {
let local = 2;
function inner() {
return global + local;
}
}
`, { ecmaVersion: 2022 });
const scopeManager = analyze(ast, { ecmaVersion: 2022 });
// Walk through scope hierarchy
scopeManager.scopes.forEach((scope, index) => {
console.log(`Scope ${index}: ${scope.type}`);
console.log(`Variables: ${scope.variables.map(v => v.name).join(", ")}`);
console.log(`References: ${scope.references.length}`);
});Variable usage tracking:
const ast = espree.parse(`
let x = 1;
x = x + 2;
console.log(x);
`, { ecmaVersion: 2022 });
const scopeManager = analyze(ast, { ecmaVersion: 2022 });
const globalScope = scopeManager.globalScope;
const xVariable = globalScope.set.get("x");
console.log(`Variable 'x' has ${xVariable.references.length} references:`);
xVariable.references.forEach((ref, index) => {
console.log(` ${index + 1}. ${ref.isRead() ? "Read" : ""}${ref.isWrite() ? "Write" : ""} at line ${ref.identifier.loc.start.line}`);
});Unresolved references:
const ast = espree.parse(`
function test() {
return undeclaredVar; // This will be unresolved
}
`, { ecmaVersion: 2022 });
const scopeManager = analyze(ast, {
ecmaVersion: 2022,
optimistic: false // Don't assume unknown references are global
});
scopeManager.scopes.forEach(scope => {
scope.through.forEach(ref => {
console.log(`Unresolved reference: ${ref.identifier.name} at line ${ref.identifier.loc.start.line}`);
});
});Module vs Script analysis:
// Module analysis
const moduleAst = espree.parse(`
import { foo } from "bar";
export let x = foo();
`, { ecmaVersion: 2022, sourceType: "module" });
const moduleScopeManager = analyze(moduleAst, {
ecmaVersion: 2022,
sourceType: "module"
});
console.log(`Module has ${moduleScopeManager.scopes.length} scopes`);
console.log(`Is module: ${moduleScopeManager.isModule()}`);
// Script analysis
const scriptAst = espree.parse(`
var x = require("bar");
`, { ecmaVersion: 2022, sourceType: "script" });
const scriptScopeManager = analyze(scriptAst, {
ecmaVersion: 2022,
sourceType: "script"
});
console.log(`Script has ${scriptScopeManager.scopes.length} scopes`);
console.log(`Is module: ${scriptScopeManager.isModule()}`);eslint-scope identifies the following scope types:
References are categorized by their usage pattern:
x += 1)eslint-scope is designed to work seamlessly with ESLint rules:
// Example ESLint rule using eslint-scope
module.exports = {
create(context) {
return {
Program() {
const scopeManager = context.getSourceCode().scopeManager;
// Access scope information
scopeManager.scopes.forEach(scope => {
scope.variables.forEach(variable => {
if (variable.references.length === 0) {
context.report({
node: variable.identifiers[0],
message: `'${variable.name}' is defined but never used.`
});
}
});
});
}
};
}
};