TypeScript scope analyser for ESLint that provides comprehensive scope analysis capabilities for JavaScript and TypeScript code
—
The reference tracking system provides comprehensive reference tracking that identifies all identifier occurrences and their usage patterns throughout the analyzed code.
The main Reference class that represents a single occurrence of an identifier in code.
/**
* A Reference represents a single occurrence of an identifier in code.
*/
class Reference {
/** A unique ID for this instance - primarily used to help debugging and testing */
readonly $id: number;
/** Reference to the enclosing Scope */
readonly from: Scope;
/** Identifier syntax node */
readonly identifier: TSESTree.Identifier | TSESTree.JSXIdentifier;
/** True if this reference is the initial declaration/definition */
readonly init: boolean;
/** Information about implicit global references */
readonly maybeImplicitGlobal: ReferenceImplicitGlobal | null;
/** The variable that this identifier references, or null if unresolved */
readonly resolved: Variable | null;
/** The expression being written to, if this is a write reference */
readonly writeExpr: TSESTree.Node | null;
/** True if this reference is in a type context */
get isTypeReference(): boolean;
/** True if this reference is in a value context */
get isValueReference(): boolean;
/** Check if this reference represents a write operation */
isWrite(): boolean;
/** Check if this reference represents a read operation */
isRead(): boolean;
/** Check if this reference is read-only */
isReadOnly(): boolean;
/** Check if this reference is write-only */
isWriteOnly(): boolean;
/** Check if this reference is both read and write */
isReadWrite(): boolean;
}Enumeration defining the read/write characteristics of references.
enum ReferenceFlag {
Read = 0x1, // Reference reads the variable
Write = 0x2, // Reference writes to the variable
ReadWrite = 0x3 // Reference both reads and writes (e.g., +=, ++, --)
}
enum ReferenceTypeFlag {
Value = 0x1, // Reference is in a value context
Type = 0x2 // Reference is in a type context
}Interface for tracking references that may create implicit global variables.
interface ReferenceImplicitGlobal {
/** The AST node where the implicit global is referenced */
node: TSESTree.Node;
/** The identifier pattern being referenced */
pattern: TSESTree.BindingName;
/** The reference object if created */
ref?: Reference;
}Usage Examples:
import { analyze } from "@typescript-eslint/scope-manager";
import { parse } from "@typescript-eslint/parser";
const code = `
let count = 0;
const message = "Hello";
function increment() {
count++; // Read-write reference to 'count'
return count; // Read reference to 'count'
}
function greet(name: string) {
const greeting = message + ", " + name; // Read reference to 'message'
return greeting;
}
// Type reference
interface User {
name: string;
}
function createUser(): User { // Type reference to 'User'
return { name: "Alice" };
}
`;
const ast = parse(code);
const scopeManager = analyze(ast, { sourceType: 'module' });
// Analyze all references across all scopes
console.log('=== Reference Analysis ===');
scopeManager.scopes.forEach(scope => {
console.log(`\nScope: ${scope.type}`);
console.log(`References: ${scope.references.length}`);
scope.references.forEach((ref, index) => {
console.log(` Reference ${index}: ${ref.identifier.name}`);
console.log(` Type: ${ref.isTypeReference ? 'Type' : 'Value'}`);
console.log(` Read: ${ref.isRead()}`);
console.log(` Write: ${ref.isWrite()}`);
console.log(` Resolved: ${ref.resolved ? ref.resolved.name : 'unresolved'}`);
if (ref.writeExpr) {
console.log(` Write expression: ${ref.writeExpr.type}`);
}
console.log(` From scope: ${ref.from.type}`);
});
});Analyze different types of reference patterns:
// Collect and categorize references
const allReferences: Reference[] = [];
scopeManager.scopes.forEach(scope => {
allReferences.push(...scope.references);
});
// Categorize by operation type
const readRefs = allReferences.filter(ref => ref.isReadOnly());
const writeRefs = allReferences.filter(ref => ref.isWriteOnly());
const readWriteRefs = allReferences.filter(ref => ref.isReadWrite());
console.log('Reference Operation Analysis:');
console.log(` Read-only: ${readRefs.length}`);
console.log(` Write-only: ${writeRefs.length}`);
console.log(` Read-write: ${readWriteRefs.length}`);
// Categorize by context
const typeRefs = allReferences.filter(ref => ref.isTypeReference);
const valueRefs = allReferences.filter(ref => ref.isValueReference);
console.log('\nReference Context Analysis:');
console.log(` Type context: ${typeRefs.length}`);
console.log(` Value context: ${valueRefs.length}`);
// Analyze read-write operations
console.log('\nRead-Write Operations:');
readWriteRefs.forEach(ref => {
console.log(` ${ref.identifier.name}: ${ref.writeExpr?.type}`);
});Track how references resolve to their variable definitions:
// Analyze reference resolution
console.log('=== Reference Resolution Analysis ===');
const resolvedRefs = allReferences.filter(ref => ref.resolved);
const unresolvedRefs = allReferences.filter(ref => !ref.resolved);
console.log(`Resolved references: ${resolvedRefs.length}`);
console.log(`Unresolved references: ${unresolvedRefs.length}`);
// Group resolved references by variable
const refsByVariable = new Map<Variable, Reference[]>();
resolvedRefs.forEach(ref => {
if (ref.resolved) {
if (!refsByVariable.has(ref.resolved)) {
refsByVariable.set(ref.resolved, []);
}
refsByVariable.get(ref.resolved)!.push(ref);
}
});
console.log('\nReferences per variable:');
refsByVariable.forEach((refs, variable) => {
console.log(` ${variable.name}: ${refs.length} references`);
const scopes = new Set(refs.map(ref => ref.from.type));
console.log(` Used in scopes: ${Array.from(scopes).join(', ')}`);
const operations = {
read: refs.filter(ref => ref.isRead()).length,
write: refs.filter(ref => ref.isWrite()).length
};
console.log(` Operations: ${operations.read} reads, ${operations.write} writes`);
});
// Analyze unresolved references (potential issues)
if (unresolvedRefs.length > 0) {
console.log('\n⚠️ Unresolved References:');
unresolvedRefs.forEach(ref => {
console.log(` ${ref.identifier.name} in ${ref.from.type} scope`);
if (ref.maybeImplicitGlobal) {
console.log(` May be implicit global`);
}
});
}Analyze how references cross scope boundaries:
// Analyze cross-scope references
console.log('=== Cross-Scope Reference Analysis ===');
scopeManager.scopes.forEach(scope => {
console.log(`\nScope: ${scope.type}`);
// References that resolve to variables in parent scopes
const crossScopeRefs = scope.references.filter(ref =>
ref.resolved && ref.resolved.scope !== scope
);
if (crossScopeRefs.length > 0) {
console.log(` Cross-scope references: ${crossScopeRefs.length}`);
crossScopeRefs.forEach(ref => {
console.log(` ${ref.identifier.name} -> ${ref.resolved?.scope.type} scope`);
});
}
// Through references (unresolved in this scope)
if (scope.through.length > 0) {
console.log(` Through references: ${scope.through.length}`);
scope.through.forEach(ref => {
console.log(` ${ref.identifier.name} (unresolved)`);
});
}
});Analyze TypeScript-specific type vs value contexts:
// TypeScript context analysis
const code = `
interface Point {
x: number;
y: number;
}
class Rectangle {
width: number;
height: number;
}
function createPoint(): Point { // Type reference
return { x: 0, y: 0 };
}
function createRect(): Rectangle { // Type reference
return new Rectangle(); // Value reference
}
const point: Point = createPoint(); // Type and value references
const rect = new Rectangle(); // Value reference only
`;
const ast = parse(code);
const scopeManager = analyze(ast, { sourceType: 'module' });
// Analyze context usage
console.log('=== Context Analysis ===');
const contextAnalysis = new Map<string, { type: number, value: number }>();
scopeManager.scopes.forEach(scope => {
scope.references.forEach(ref => {
const name = ref.identifier.name;
if (!contextAnalysis.has(name)) {
contextAnalysis.set(name, { type: 0, value: 0 });
}
const stats = contextAnalysis.get(name)!;
if (ref.isTypeReference) stats.type++;
if (ref.isValueReference) stats.value++;
});
});
contextAnalysis.forEach((stats, name) => {
console.log(`${name}:`);
console.log(` Type context: ${stats.type} references`);
console.log(` Value context: ${stats.value} references`);
if (stats.type > 0 && stats.value > 0) {
console.log(` ✓ Used in both contexts`);
} else if (stats.type > 0) {
console.log(` 📝 Type-only usage`);
} else {
console.log(` 💾 Value-only usage`);
}
});Analyze different types of write operations:
// Analyze write expressions
console.log('=== Write Expression Analysis ===');
const writeRefs = allReferences.filter(ref => ref.writeExpr);
const writeTypes = new Map<string, number>();
writeRefs.forEach(ref => {
const type = ref.writeExpr!.type;
writeTypes.set(type, (writeTypes.get(type) || 0) + 1);
});
console.log('Write expression types:');
writeTypes.forEach((count, type) => {
console.log(` ${type}: ${count}`);
});
// Examples of write expressions:
// - AssignmentExpression: x = 5
// - UpdateExpression: x++, --y
// - VariableDeclarator: let x = 5
// - FunctionDeclaration: function f() {}
// - Parameter: function f(x) {} (x is written to)Understanding the lifecycle of references:
// Track reference lifecycle
console.log('=== Reference Lifecycle Analysis ===');
resolvedRefs.forEach(ref => {
if (ref.resolved) {
const variable = ref.resolved;
console.log(`\nReference: ${ref.identifier.name}`);
console.log(` Variable defined in: ${variable.scope.type} scope`);
console.log(` Referenced from: ${ref.from.type} scope`);
console.log(` Definition count: ${variable.defs.length}`);
console.log(` Total references: ${variable.references.length}`);
// Find if this is the first reference
const refIndex = variable.references.indexOf(ref);
console.log(` Reference order: ${refIndex + 1} of ${variable.references.length}`);
if (ref.init) {
console.log(` ✓ This is a defining reference`);
}
}
});Install with Tessl CLI
npx tessl i tessl/npm-typescript-eslint--scope-manager