0
# Usage Analysis
1
2
Usage Analysis is an advanced capability of ts-api-utils that provides comprehensive tools for analyzing variable and symbol usage patterns within TypeScript source files. This module enables sophisticated code analysis by tracking how identifiers are declared and referenced across different scopes and domains, making it essential for building linters, refactoring tools, and other advanced TypeScript tooling.
3
4
## Overview
5
6
TypeScript code analysis often requires understanding not just what variables and types are declared, but how they are used throughout a codebase. The Usage Analysis module provides a systematic approach to:
7
8
1. **Track variable declarations** across different syntactic contexts
9
2. **Monitor usage patterns** for identifiers in type and value spaces
10
3. **Analyze scope boundaries** and their effects on identifier visibility
11
4. **Distinguish between domains** where identifiers exist (namespace, type, value spaces)
12
13
This capability is particularly valuable for tools that need to understand identifier lifecycles, detect unused variables, analyze dependencies, or perform safe refactoring operations.
14
15
## Core Concepts
16
17
### Domains
18
19
TypeScript operates with multiple "domains" or "spaces" where identifiers can exist:
20
21
- **Value Space**: Runtime values, variables, functions
22
- **Type Space**: Types, interfaces, type aliases
23
- **Namespace Space**: Module namespaces and namespace declarations
24
- **Import Domain**: Special handling for imported identifiers
25
26
Understanding these domains is crucial because the same identifier name can exist in different domains simultaneously without conflict.
27
28
### Usage Patterns
29
30
The system tracks two primary aspects of identifier usage:
31
32
1. **Declarations**: Where and how identifiers are introduced into scope
33
2. **References**: Where identifiers are accessed or used
34
35
### Scope Analysis
36
37
Variable usage is analyzed within the context of TypeScript's scoping rules, including:
38
39
- **Function scope boundaries**
40
- **Block scope boundaries**
41
- **Type parameter scopes**
42
- **Conditional type scopes**
43
44
## Usage Collection
45
46
The main entry point for variable usage analysis.
47
48
### collectVariableUsage
49
50
Creates a comprehensive mapping of each declared identifier to its usage information within a source file.
51
52
```typescript
53
function collectVariableUsage(sourceFile: ts.SourceFile): Map<ts.Identifier, UsageInfo> { .api }
54
```
55
56
**Parameters:**
57
- `sourceFile: ts.SourceFile` - The TypeScript source file to analyze
58
59
**Returns:** A Map where keys are identifier nodes and values contain complete usage information including declarations, references, domains, and scope details.
60
61
**Example:**
62
```typescript
63
import { collectVariableUsage } from 'ts-api-utils';
64
65
declare const sourceFile: ts.SourceFile;
66
67
const usage = collectVariableUsage(sourceFile);
68
69
for (const [identifier, information] of usage) {
70
console.log(`${identifier.getText()} is used ${information.uses.length} time(s).`);
71
console.log(` Declared in domain: ${information.domain}`);
72
console.log(` Exported: ${information.exported}`);
73
console.log(` Global scope: ${information.inGlobalScope}`);
74
}
75
```
76
77
## Usage Information Types
78
79
Data structures that represent how variables and types are used throughout code.
80
81
### UsageInfo
82
83
Contains comprehensive information about how an identifier was declared and referenced.
84
85
```typescript
86
interface UsageInfo {
87
declarations: ts.Identifier[];
88
domain: DeclarationDomain;
89
exported: boolean;
90
inGlobalScope: boolean;
91
uses: Usage[];
92
} { .api }
93
```
94
95
**Properties:**
96
- `declarations: ts.Identifier[]` - All locations where the identifier was declared
97
- `domain: DeclarationDomain` - Which domain(s) the identifier exists in
98
- `exported: boolean` - Whether the identifier was exported from its module/namespace scope
99
- `inGlobalScope: boolean` - Whether any declaration was in the global scope
100
- `uses: Usage[]` - All references to the identifier throughout the file
101
102
### Usage
103
104
Represents a single instance of an identifier being referenced.
105
106
```typescript
107
interface Usage {
108
domain: UsageDomain;
109
location: ts.Identifier;
110
} { .api }
111
```
112
113
**Properties:**
114
- `domain: UsageDomain` - Which domain(s) the usage occurs in
115
- `location: ts.Identifier` - The AST node representing the usage location
116
117
### DeclarationDomain
118
119
Enumeration defining the different domains where identifiers can be declared.
120
121
```typescript
122
enum DeclarationDomain {
123
Namespace = 1,
124
Type = 2,
125
Value = 4,
126
Any = 7, // Namespace | Type | Value
127
Import = 8
128
} { .api }
129
```
130
131
**Values:**
132
- `Namespace` - Declared in namespace space (modules, namespace declarations)
133
- `Type` - Declared in type space (interfaces, type aliases, type parameters)
134
- `Value` - Declared in value space (variables, functions, classes)
135
- `Any` - Exists in all three primary domains (enums, classes)
136
- `Import` - Special flag for imported identifiers
137
138
### UsageDomain
139
140
Enumeration defining the different domains where identifier usage can occur.
141
142
```typescript
143
enum UsageDomain {
144
Namespace = 1,
145
Type = 2,
146
Value = 4,
147
Any = 7, // Namespace | Type | Value
148
TypeQuery = 8, // typeof usage
149
ValueOrNamespace = 5 // Value | Namespace
150
} { .api }
151
```
152
153
**Values:**
154
- `Namespace` - Used as a namespace reference
155
- `Type` - Used in a type position
156
- `Value` - Used as a runtime value
157
- `Any` - Could be used in any domain
158
- `TypeQuery` - Used in `typeof` expressions
159
- `ValueOrNamespace` - Ambiguous value or namespace usage
160
161
**Example:**
162
```typescript
163
import { collectVariableUsage, DeclarationDomain, UsageDomain } from 'ts-api-utils';
164
165
function analyzeIdentifierUsage(sourceFile: ts.SourceFile) {
166
const usage = collectVariableUsage(sourceFile);
167
168
for (const [identifier, info] of usage) {
169
const name = identifier.getText();
170
171
// Check what domains the identifier is declared in
172
if (info.domain & DeclarationDomain.Type) {
173
console.log(`${name} is declared as a type`);
174
}
175
if (info.domain & DeclarationDomain.Value) {
176
console.log(`${name} is declared as a value`);
177
}
178
179
// Analyze usage patterns
180
const typeUsages = info.uses.filter(use => use.domain & UsageDomain.Type);
181
const valueUsages = info.uses.filter(use => use.domain & UsageDomain.Value);
182
183
console.log(`${name} used ${typeUsages.length} time(s) in type positions`);
184
console.log(`${name} used ${valueUsages.length} time(s) in value positions`);
185
}
186
}
187
```
188
189
## Domain Analysis
190
191
Functions for understanding the context and domain of identifier usage.
192
193
### getUsageDomain
194
195
Determines which domain(s) an identifier usage occurs within based on its syntactic context.
196
197
```typescript
198
function getUsageDomain(node: ts.Identifier): UsageDomain | undefined { .api }
199
```
200
201
**Parameters:**
202
- `node: ts.Identifier` - The identifier node to analyze
203
204
**Returns:** The usage domain(s) for the identifier, or `undefined` if the context doesn't represent a usage
205
206
**Example:**
207
```typescript
208
import { getUsageDomain, UsageDomain } from 'ts-api-utils';
209
210
function analyzeIdentifierContext(identifier: ts.Identifier) {
211
const domain = getUsageDomain(identifier);
212
213
if (!domain) {
214
console.log('Identifier is not a usage (e.g., in declaration position)');
215
return;
216
}
217
218
if (domain & UsageDomain.Type) {
219
console.log('Used in type context');
220
}
221
222
if (domain & UsageDomain.Value) {
223
console.log('Used in value context');
224
}
225
226
if (domain === UsageDomain.TypeQuery) {
227
console.log('Used in typeof expression');
228
}
229
}
230
```
231
232
### getDeclarationDomain
233
234
Determines which domain(s) an identifier declaration exists within based on its declaration context.
235
236
```typescript
237
function getDeclarationDomain(node: ts.Identifier): DeclarationDomain | undefined { .api }
238
```
239
240
**Parameters:**
241
- `node: ts.Identifier` - The identifier node in a declaration position
242
243
**Returns:** The declaration domain(s) for the identifier, or `undefined` if not a declaration
244
245
**Example:**
246
```typescript
247
import { getDeclarationDomain, DeclarationDomain } from 'ts-api-utils';
248
249
function analyzeDeclarationContext(identifier: ts.Identifier) {
250
const domain = getDeclarationDomain(identifier);
251
252
if (!domain) {
253
console.log('Identifier is not in declaration position');
254
return;
255
}
256
257
if (domain & DeclarationDomain.Type && domain & DeclarationDomain.Value) {
258
console.log('Declares both type and value (e.g., class or enum)');
259
} else if (domain & DeclarationDomain.Type) {
260
console.log('Type declaration (interface, type alias)');
261
} else if (domain & DeclarationDomain.Value) {
262
console.log('Value declaration (variable, function)');
263
}
264
265
if (domain & DeclarationDomain.Import) {
266
console.log('Import declaration');
267
}
268
}
269
```
270
271
## Scope Analysis
272
273
Understanding and working with TypeScript's scoping rules for accurate usage analysis.
274
275
### Scope Interface
276
277
The core interface for representing scopes in the usage analysis system.
278
279
```typescript
280
interface Scope {
281
addUse(use: Usage, scope?: Scope): void;
282
addVariable(
283
identifier: string,
284
name: ts.PropertyName,
285
selector: ScopeBoundarySelector,
286
exported: boolean,
287
domain: DeclarationDomain
288
): void;
289
createOrReuseEnumScope(name: string, exported: boolean): EnumScope;
290
createOrReuseNamespaceScope(
291
name: string,
292
exported: boolean,
293
ambient: boolean,
294
hasExportStatement: boolean
295
): NamespaceScope;
296
end(cb: UsageInfoCallback): void;
297
getDestinationScope(selector: ScopeBoundarySelector): Scope;
298
getFunctionScope(): Scope;
299
getVariables(): Map<string, InternalUsageInfo>;
300
markExported(name: ts.ModuleExportName, as?: ts.ModuleExportName): void;
301
} { .api }
302
```
303
304
### ScopeBoundary
305
306
Enumeration defining different types of scope boundaries in TypeScript.
307
308
```typescript
309
enum ScopeBoundary {
310
None = 0,
311
Function = 1,
312
Block = 2,
313
Type = 4,
314
ConditionalType = 8
315
} { .api }
316
```
317
318
**Values:**
319
- `None` - No scope boundary
320
- `Function` - Function scope boundary (functions, methods, constructors)
321
- `Block` - Block scope boundary (blocks, loops, conditionals)
322
- `Type` - Type parameter scope boundary
323
- `ConditionalType` - Conditional type scope boundary (`T extends U ? X : Y`)
324
325
### ScopeBoundarySelector
326
327
Enumeration for selecting which scope boundaries to consider during analysis.
328
329
```typescript
330
enum ScopeBoundarySelector {
331
Function = 1, // ScopeBoundary.Function
332
Block = 3, // Function | Block
333
InferType = 8, // ScopeBoundary.ConditionalType
334
Type = 7 // Block | Type
335
} { .api }
336
```
337
338
### isBlockScopeBoundary
339
340
Determines if a node represents a block scope boundary.
341
342
```typescript
343
function isBlockScopeBoundary(node: ts.Node): ScopeBoundary { .api }
344
```
345
346
**Parameters:**
347
- `node: ts.Node` - The AST node to check
348
349
**Returns:** The type of scope boundary represented by the node
350
351
**Example:**
352
```typescript
353
import { isBlockScopeBoundary, ScopeBoundary } from 'ts-api-utils';
354
355
function analyzeScope(node: ts.Node) {
356
const boundary = isBlockScopeBoundary(node);
357
358
if (boundary & ScopeBoundary.Function) {
359
console.log('Function scope boundary');
360
}
361
362
if (boundary & ScopeBoundary.Block) {
363
console.log('Block scope boundary');
364
}
365
366
if (boundary & ScopeBoundary.Type) {
367
console.log('Type parameter scope boundary');
368
}
369
}
370
```
371
372
## Advanced Usage Patterns
373
374
### Detecting Unused Variables
375
376
```typescript
377
import { collectVariableUsage } from 'ts-api-utils';
378
379
function findUnusedVariables(sourceFile: ts.SourceFile): ts.Identifier[] {
380
const usage = collectVariableUsage(sourceFile);
381
const unused: ts.Identifier[] = [];
382
383
for (const [identifier, info] of usage) {
384
// Skip exported variables - they might be used externally
385
if (info.exported) continue;
386
387
// Skip global declarations - they might be used by other files
388
if (info.inGlobalScope) continue;
389
390
// Variable is unused if it has no usage references
391
if (info.uses.length === 0) {
392
unused.push(identifier);
393
}
394
}
395
396
return unused;
397
}
398
```
399
400
### Analyzing Cross-Domain Usage
401
402
```typescript
403
import {
404
collectVariableUsage,
405
DeclarationDomain,
406
UsageDomain
407
} from 'ts-api-utils';
408
409
function analyzeCrossDomainUsage(sourceFile: ts.SourceFile) {
410
const usage = collectVariableUsage(sourceFile);
411
412
for (const [identifier, info] of usage) {
413
const name = identifier.getText();
414
415
// Find identifiers that exist in multiple domains
416
if (info.domain === DeclarationDomain.Any) {
417
console.log(`${name} exists in all domains (likely enum or class)`);
418
419
// Analyze how it's actually used
420
const typeUses = info.uses.filter(use => use.domain & UsageDomain.Type);
421
const valueUses = info.uses.filter(use => use.domain & UsageDomain.Value);
422
423
if (typeUses.length > 0 && valueUses.length > 0) {
424
console.log(` Used in both type and value contexts`);
425
} else if (typeUses.length > 0) {
426
console.log(` Only used in type contexts`);
427
} else if (valueUses.length > 0) {
428
console.log(` Only used in value contexts`);
429
}
430
}
431
}
432
}
433
```
434
435
### Scope-Aware Refactoring
436
437
```typescript
438
import { collectVariableUsage, DeclarationDomain } from 'ts-api-utils';
439
440
function canSafelyRename(
441
sourceFile: ts.SourceFile,
442
targetIdentifier: ts.Identifier,
443
newName: string
444
): boolean {
445
const usage = collectVariableUsage(sourceFile);
446
const targetInfo = usage.get(targetIdentifier);
447
448
if (!targetInfo) {
449
return false; // Identifier not found
450
}
451
452
// Check if new name conflicts with existing declarations
453
for (const [identifier, info] of usage) {
454
if (identifier.getText() === newName) {
455
// Check for domain conflicts
456
if (info.domain & targetInfo.domain) {
457
// Names would conflict in overlapping domains
458
return false;
459
}
460
}
461
}
462
463
return true;
464
}
465
```
466
467
### Type-Only Import Detection
468
469
```typescript
470
import {
471
collectVariableUsage,
472
DeclarationDomain,
473
UsageDomain
474
} from 'ts-api-utils';
475
476
function findTypeOnlyImports(sourceFile: ts.SourceFile): string[] {
477
const usage = collectVariableUsage(sourceFile);
478
const typeOnlyImports: string[] = [];
479
480
for (const [identifier, info] of usage) {
481
// Check if it's an import
482
if (!(info.domain & DeclarationDomain.Import)) continue;
483
484
// Check if all uses are in type contexts
485
const hasValueUsage = info.uses.some(use =>
486
use.domain & (UsageDomain.Value | UsageDomain.ValueOrNamespace)
487
);
488
489
if (!hasValueUsage && info.uses.length > 0) {
490
typeOnlyImports.push(identifier.getText());
491
}
492
}
493
494
return typeOnlyImports;
495
}
496
```
497
498
## Utility Functions
499
500
### identifierToKeywordKind
501
502
TypeScript version compatibility utility for working with identifier keyword kinds.
503
504
```typescript { .api }
505
function identifierToKeywordKind(node: ts.Identifier): ts.SyntaxKind | undefined;
506
```
507
508
**Parameters:**
509
- `node: ts.Identifier` - The identifier node to check
510
511
**Returns:** The syntax kind if the identifier represents a keyword, `undefined` otherwise
512
513
**Description:** This function provides compatibility support for TypeScript versions before 5.0 that don't have the built-in `identifierToKeywordKind` function.
514
515
**Example:**
516
```typescript
517
import { identifierToKeywordKind } from 'ts-api-utils';
518
519
function isKeywordIdentifier(identifier: ts.Identifier): boolean {
520
return identifierToKeywordKind(identifier) !== undefined;
521
}
522
```
523
524
## Integration with Other Modules
525
526
Usage Analysis works seamlessly with other ts-api-utils modules for comprehensive code analysis.
527
528
### With Node Analysis
529
530
```typescript
531
import {
532
collectVariableUsage,
533
isVariableDeclaration,
534
hasModifiers
535
} from 'ts-api-utils';
536
537
function analyzeVariableModifiers(sourceFile: ts.SourceFile) {
538
const usage = collectVariableUsage(sourceFile);
539
540
for (const [identifier, info] of usage) {
541
// Check each declaration location
542
for (const declaration of info.declarations) {
543
const parent = declaration.parent;
544
545
if (isVariableDeclaration(parent)) {
546
const variableStatement = parent.parent?.parent;
547
if (hasModifiers(variableStatement)) {
548
console.log(`${identifier.getText()} has modifiers`);
549
}
550
}
551
}
552
}
553
}
554
```
555
556
### With Type System Analysis
557
558
```typescript
559
import {
560
collectVariableUsage,
561
getCallSignaturesOfType
562
} from 'ts-api-utils';
563
564
function analyzeCallableVariables(
565
sourceFile: ts.SourceFile,
566
typeChecker: ts.TypeChecker
567
) {
568
const usage = collectVariableUsage(sourceFile);
569
570
for (const [identifier, info] of usage) {
571
// Only analyze value declarations
572
if (!(info.domain & DeclarationDomain.Value)) continue;
573
574
const type = typeChecker.getTypeAtLocation(identifier);
575
const signatures = getCallSignaturesOfType(type);
576
577
if (signatures.length > 0) {
578
console.log(`${identifier.getText()} is callable with ${signatures.length} signature(s)`);
579
}
580
}
581
}
582
```
583
584
## Best Practices
585
586
### Performance Considerations
587
588
1. **Cache results**: Usage analysis is expensive - cache results when analyzing multiple files
589
2. **Scope appropriately**: Only analyze files that need usage information
590
3. **Filter early**: Use domain filters to focus on relevant identifiers
591
592
### Accuracy Guidelines
593
594
1. **Consider all domains**: Remember that identifiers can exist in multiple domains simultaneously
595
2. **Handle imports specially**: Import declarations have special domain semantics
596
3. **Respect scope boundaries**: Usage analysis respects TypeScript's scoping rules
597
598
### Common Patterns
599
600
```typescript
601
// Safe usage information access
602
function getUsageInfo(
603
usage: Map<ts.Identifier, UsageInfo>,
604
identifier: ts.Identifier
605
): UsageInfo | undefined {
606
return usage.get(identifier);
607
}
608
609
// Domain-specific usage filtering
610
function getTypeUsages(info: UsageInfo): Usage[] {
611
return info.uses.filter(use => use.domain & UsageDomain.Type);
612
}
613
614
// Exported identifier detection
615
function isExported(info: UsageInfo): boolean {
616
return info.exported || info.inGlobalScope;
617
}
618
```
619
620
The Usage Analysis module provides the foundation for sophisticated TypeScript code analysis, enabling tools to understand identifier lifecycles, detect patterns, and perform safe transformations while respecting TypeScript's complex scoping and domain rules.