TypeScript scope analyser for ESLint that provides comprehensive scope analysis capabilities for JavaScript and TypeScript code
—
The variable tracking system provides TypeScript-aware type context analysis and comprehensive definition tracking for all identifiers in analyzed code.
The main Variable class that represents variables with TypeScript type context awareness.
/**
* A Variable represents a locally scoped identifier. These include arguments to functions.
*/
class Variable extends VariableBase {
/** Unique identifier for this variable instance */
readonly $id: number;
/** The variable name, as given in the source code */
readonly name: string;
/** The array of the definitions of this variable */
readonly defs: Definition[];
/** The array of Identifier nodes which define this variable */
readonly identifiers: TSESTree.Identifier[];
/** List of References of this variable in its defining scope and all nested scopes */
readonly references: Reference[];
/** The scope where this variable is defined */
readonly scope: Scope;
/** True if the variable is considered used for the purposes of no-unused-vars */
eslintUsed: boolean;
/** True if the variable is valid in a type context, false otherwise */
get isTypeVariable(): boolean;
/** True if the variable is valid in a value context, false otherwise */
get isValueVariable(): boolean;
}ESLint-compatible variable implementation for seamless integration with existing ESLint rules.
/**
* ESLint-compatible variable that maintains compatibility with standard ESLint scope analysis
*/
class ESLintScopeVariable extends VariableBase {
readonly $id: number;
readonly name: string;
readonly defs: Definition[];
readonly identifiers: TSESTree.Identifier[];
readonly references: Reference[];
readonly scope: Scope;
eslintUsed: boolean;
/** If this key exists, this variable is a global variable added by ESLint */
writeable?: boolean;
/** Indicates if there are globals comment directives */
eslintExplicitGlobal?: boolean;
/** The configured value in config files */
eslintImplicitGlobalSetting?: 'readonly' | 'writable';
/** Global variable comments from source code */
eslintExplicitGlobalComments?: TSESTree.Comment[];
}Variables automatically defined by TypeScript library definitions.
/**
* Variable representing TypeScript library definitions (e.g., console, window, etc.)
*/
class ImplicitLibVariable extends VariableBase {
readonly $id: number;
readonly name: string;
readonly defs: LibDefinition[];
readonly identifiers: TSESTree.Identifier[];
readonly references: Reference[];
readonly scope: Scope;
eslintUsed: boolean;
constructor(scope: Scope, name: string, options: ImplicitLibVariableOptions);
}
interface ImplicitLibVariableOptions {
readonly eslintImplicitGlobalSetting?: 'readonly' | 'writable';
readonly isTypeVariable?: boolean;
readonly isValueVariable?: boolean;
readonly writeable?: boolean;
}
interface LibDefinition {
libs: readonly LibDefinition[];
variables: readonly [string, ImplicitLibVariableOptions][];
}Union type representing all possible variable types in the system.
type ScopeVariable = ESLintScopeVariable | Variable;Usage Examples:
import { analyze } from "@typescript-eslint/scope-manager";
import { parse } from "@typescript-eslint/parser";
const code = `
interface User {
name: string;
age: number;
}
function createUser(name: string, age: number): User {
const user: User = { name, age };
return user;
}
enum Status {
Active,
Inactive
}
`;
const ast = parse(code);
const scopeManager = analyze(ast, {
sourceType: 'module',
lib: ['es2020', 'dom']
});
// Analyze all variables
scopeManager.variables.forEach(variable => {
console.log(`Variable: ${variable.name}`);
console.log(` Type context: ${variable.isTypeVariable}`);
console.log(` Value context: ${variable.isValueVariable}`);
console.log(` Scope: ${variable.scope.type}`);
console.log(` Definitions: ${variable.defs.length}`);
console.log(` References: ${variable.references.length}`);
console.log(` ESLint used: ${variable.eslintUsed}`);
});
// Check type vs value variables
const typeOnlyVars = scopeManager.variables.filter(v =>
v.isTypeVariable && !v.isValueVariable
);
console.log('Type-only variables:', typeOnlyVars.map(v => v.name));
const valueOnlyVars = scopeManager.variables.filter(v =>
v.isValueVariable && !v.isTypeVariable
);
console.log('Value-only variables:', valueOnlyVars.map(v => v.name));
const dualContextVars = scopeManager.variables.filter(v =>
v.isTypeVariable && v.isValueVariable
);
console.log('Dual-context variables:', dualContextVars.map(v => v.name));TypeScript variables can exist in different contexts:
// Examples of different variable contexts
// Type-only context
interface Point { x: number; y: number } // 'Point' is type-only
type StringOrNumber = string | number; // 'StringOrNumber' is type-only
// Value-only context
const message = "Hello"; // 'message' is value-only
function greet() { } // 'greet' is value-only
// Dual context (both type and value)
class Rectangle { // 'Rectangle' is both type and value
width: number;
height: number;
}
enum Color { // 'Color' is both type and value
Red, Green, Blue
}
namespace Utils { // 'Utils' is both type and value
export const helper = () => {};
}
// Usage analysis
const scopeManager = analyze(ast);
scopeManager.variables.forEach(variable => {
if (variable.isTypeVariable && variable.isValueVariable) {
console.log(`${variable.name} can be used as both type and value`);
} else if (variable.isTypeVariable) {
console.log(`${variable.name} can only be used as a type`);
} else if (variable.isValueVariable) {
console.log(`${variable.name} can only be used as a value`);
}
});Variables can have multiple definitions from various sources:
// Analyze variable definitions
scopeManager.variables.forEach(variable => {
console.log(`\nVariable: ${variable.name}`);
variable.defs.forEach((def, index) => {
console.log(` Definition ${index}:`);
console.log(` Type: ${def.type}`);
console.log(` Node: ${def.node.type}`);
console.log(` Is type definition: ${def.isTypeDefinition}`);
console.log(` Is variable definition: ${def.isVariableDefinition}`);
});
// Check if variable is redeclared
if (variable.defs.length > 1) {
console.log(` ⚠️ Variable ${variable.name} is redeclared`);
}
// Analyze identifiers
console.log(` Identifier nodes: ${variable.identifiers.length}`);
variable.identifiers.forEach((id, index) => {
console.log(` ${index}: ${id.name} at line ${id.loc?.start.line}`);
});
});Track how variables are used throughout the code:
// Analyze variable references
scopeManager.variables.forEach(variable => {
console.log(`\nVariable: ${variable.name}`);
console.log(`References: ${variable.references.length}`);
variable.references.forEach((ref, index) => {
console.log(` Reference ${index}:`);
console.log(` From scope: ${ref.from.type}`);
console.log(` Is read: ${ref.isRead()}`);
console.log(` Is write: ${ref.isWrite()}`);
console.log(` Resolved: ${ref.resolved ? 'Yes' : 'No'}`);
if (ref.writeExpr) {
console.log(` Write expression: ${ref.writeExpr.type}`);
}
});
// Analyze usage patterns
const reads = variable.references.filter(ref => ref.isRead());
const writes = variable.references.filter(ref => ref.isWrite());
console.log(` Read operations: ${reads.length}`);
console.log(` Write operations: ${writes.length}`);
if (writes.length === 0 && variable.defs.length > 0) {
console.log(` ⚠️ Variable ${variable.name} is never modified`);
}
if (reads.length === 0) {
console.log(` ⚠️ Variable ${variable.name} is never read`);
}
});TypeScript library definitions create implicit variables:
// Analyze implicit library variables
const globalScope = scopeManager.globalScope;
if (globalScope) {
const implicitVars = globalScope.variables.filter(v =>
v instanceof ImplicitLibVariable
);
console.log('Implicit library variables:');
implicitVars.forEach(variable => {
const implicitVar = variable as ImplicitLibVariable;
console.log(` ${implicitVar.name}`);
console.log(` Type variable: ${implicitVar.isTypeVariable}`);
console.log(` Value variable: ${implicitVar.isValueVariable}`);
console.log(` Writable: ${implicitVar.writeable}`);
});
}
// Common implicit variables from TypeScript libs:
// - console (from lib.dom.d.ts or lib.node.d.ts)
// - window, document (from lib.dom.d.ts)
// - process, Buffer (from @types/node)
// - Promise, Array, Object (from lib.es*.d.ts)Understanding how variables relate to their scopes:
// Analyze variable-scope relationships
scopeManager.scopes.forEach(scope => {
console.log(`\nScope: ${scope.type}`);
console.log(`Variables defined in this scope: ${scope.set.size}`);
// Variables defined directly in this scope
scope.set.forEach((variable, name) => {
console.log(` Owns: ${name}`);
});
// All variables accessible in this scope
console.log(`Total accessible variables: ${scope.variables.length}`);
// Check variable scope chain
scope.variables.forEach(variable => {
if (variable.scope === scope) {
console.log(` Local: ${variable.name}`);
} else {
console.log(` Inherited: ${variable.name} (from ${variable.scope.type})`);
}
});
});Install with Tessl CLI
npx tessl i tessl/npm-typescript-eslint--scope-manager