0
# AST Analysis
1
2
Tools for analyzing Abstract Syntax Trees (ASTs), tracking variable scopes, and extracting assignment patterns. These utilities are essential for plugins that need to understand code structure and variable usage.
3
4
## Capabilities
5
6
### attachScopes
7
8
Attaches scope objects to AST nodes for tracking variable declarations and scope chains. Each scope provides methods to check if variables are defined in the current or parent scopes.
9
10
```typescript { .api }
11
/**
12
* Attaches Scope objects to the relevant nodes of an AST
13
* Each Scope object has a scope.contains(name) method that returns true
14
* if a given name is defined in the current scope or a parent scope
15
* @param ast - The AST to attach scopes to
16
* @param propertyName - Property name to attach scope to (defaults to 'scope')
17
* @returns The root scope object
18
*/
19
function attachScopes(ast: BaseNode, propertyName?: string): AttachedScope;
20
21
interface AttachedScope {
22
parent?: AttachedScope;
23
isBlockScope: boolean;
24
declarations: { [key: string]: boolean };
25
addDeclaration(node: BaseNode, isBlockDeclaration: boolean, isVar: boolean): void;
26
contains(name: string): boolean;
27
}
28
```
29
30
**Parameters:**
31
32
- `ast` (BaseNode): The AST root node to attach scopes to
33
- `propertyName` (string, optional): The property name to use for attaching scopes to nodes. Defaults to `'scope'`
34
35
**Returns:** The root `AttachedScope` object
36
37
**AttachedScope Methods:**
38
39
- `contains(name: string): boolean` - Returns true if the variable name is defined in this scope or any parent scope
40
- `addDeclaration(node: BaseNode, isBlockDeclaration: boolean, isVar: boolean): void` - Adds a variable declaration to this scope
41
42
**Usage Examples:**
43
44
```typescript
45
import { attachScopes } from "@rollup/pluginutils";
46
import { walk } from "estree-walker";
47
48
export default function myPlugin(options = {}) {
49
return {
50
transform(code, id) {
51
const ast = this.parse(code);
52
53
// Attach scopes to AST nodes
54
let scope = attachScopes(ast, 'scope');
55
56
walk(ast, {
57
enter(node) {
58
// Update current scope when entering scoped nodes
59
if (node.scope) scope = node.scope;
60
61
// Check if variables are defined in current scope
62
if (node.type === 'Identifier') {
63
if (!scope.contains(node.name)) {
64
// Variable is not defined in any scope - likely global
65
console.log(`Global variable detected: ${node.name}`);
66
}
67
}
68
},
69
70
leave(node) {
71
// Return to parent scope when leaving scoped nodes
72
if (node.scope) scope = scope.parent;
73
}
74
});
75
76
return { code };
77
}
78
};
79
}
80
81
// Advanced usage with variable injection
82
export default function injectPlugin(options = {}) {
83
const { injections = {} } = options;
84
85
return {
86
transform(code, id) {
87
const ast = this.parse(code);
88
let scope = attachScopes(ast);
89
90
let hasInjections = false;
91
92
walk(ast, {
93
enter(node) {
94
if (node.scope) scope = node.scope;
95
96
if (node.type === 'Identifier' && injections[node.name]) {
97
// Only inject if variable is not already defined
98
if (!scope.contains(node.name)) {
99
// Inject the variable
100
hasInjections = true;
101
}
102
}
103
},
104
leave(node) {
105
if (node.scope) scope = scope.parent;
106
}
107
});
108
109
if (hasInjections) {
110
// Add imports for injected variables
111
const imports = Object.keys(injections)
112
.map(name => `import ${name} from '${injections[name]}';`)
113
.join('\n');
114
115
return { code: imports + '\n' + code };
116
}
117
118
return { code };
119
}
120
};
121
}
122
```
123
124
### extractAssignedNames
125
126
Extracts variable names from destructuring patterns and assignment targets. Handles complex nested destructuring patterns including objects, arrays, and rest patterns.
127
128
```typescript { .api }
129
/**
130
* Extracts the names of all assignment targets based upon specified patterns
131
* Handles destructuring patterns including objects, arrays, and rest patterns
132
* @param param - An AST node representing an assignment pattern
133
* @returns Array of extracted variable names
134
*/
135
function extractAssignedNames(param: BaseNode): string[];
136
```
137
138
**Parameters:**
139
140
- `param` (BaseNode): An AST node representing an assignment pattern (typically from variable declarations or function parameters)
141
142
**Returns:** Array of strings representing all variable names that would be assigned
143
144
**Supported Patterns:**
145
146
- Simple identifiers: `x` → `['x']`
147
- Object destructuring: `{a, b: c}` → `['a', 'c']`
148
- Array destructuring: `[x, y]` → `['x', 'y']`
149
- Rest patterns: `{...rest}` → `['rest']`
150
- Assignment patterns: `{x = 5}` → `['x']`
151
- Nested destructuring: `{a: {b, c}}` → `['b', 'c']`
152
153
**Usage Examples:**
154
155
```typescript
156
import { extractAssignedNames } from "@rollup/pluginutils";
157
import { walk } from "estree-walker";
158
159
export default function myPlugin(options = {}) {
160
return {
161
transform(code, id) {
162
const ast = this.parse(code);
163
const declaredNames = new Set();
164
165
walk(ast, {
166
enter(node) {
167
// Extract names from variable declarations
168
if (node.type === 'VariableDeclarator') {
169
const names = extractAssignedNames(node.id);
170
names.forEach(name => declaredNames.add(name));
171
console.log(`Declared variables: ${names.join(', ')}`);
172
}
173
174
// Extract names from function parameters
175
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
176
node.params.forEach(param => {
177
const names = extractAssignedNames(param);
178
names.forEach(name => declaredNames.add(name));
179
console.log(`Function parameter names: ${names.join(', ')}`);
180
});
181
}
182
183
// Extract names from catch clauses
184
if (node.type === 'CatchClause' && node.param) {
185
const names = extractAssignedNames(node.param);
186
names.forEach(name => declaredNames.add(name));
187
console.log(`Catch parameter names: ${names.join(', ')}`);
188
}
189
}
190
});
191
192
console.log(`All declared names: ${Array.from(declaredNames).join(', ')}`);
193
return { code };
194
}
195
};
196
}
197
198
// Usage with variable tracking
199
export default function trackVariablesPlugin() {
200
return {
201
transform(code, id) {
202
const ast = this.parse(code);
203
const variableUsage = new Map();
204
205
walk(ast, {
206
enter(node) {
207
if (node.type === 'VariableDeclarator') {
208
const names = extractAssignedNames(node.id);
209
210
// Track different destructuring patterns
211
names.forEach(name => {
212
if (!variableUsage.has(name)) {
213
variableUsage.set(name, {
214
declared: true,
215
used: false,
216
line: node.loc?.start.line
217
});
218
}
219
});
220
221
// Example patterns and their extracted names:
222
// const x = 1; → ['x']
223
// const {a, b: c} = obj; → ['a', 'c']
224
// const [x, y, ...rest] = arr; → ['x', 'y', 'rest']
225
// const {a: {b, c}} = nested; → ['b', 'c']
226
// const {x = 5} = obj; → ['x']
227
}
228
229
if (node.type === 'Identifier') {
230
const usage = variableUsage.get(node.name);
231
if (usage) {
232
usage.used = true;
233
}
234
}
235
}
236
});
237
238
// Report unused variables
239
for (const [name, info] of variableUsage) {
240
if (info.declared && !info.used) {
241
console.warn(`Unused variable '${name}' at line ${info.line}`);
242
}
243
}
244
245
return { code };
246
}
247
};
248
}
249
```
250
251
## Common Patterns
252
253
### Variable Injection with Scope Analysis
254
255
```typescript
256
import { attachScopes, extractAssignedNames } from "@rollup/pluginutils";
257
import { walk } from "estree-walker";
258
259
export default function smartInjectPlugin(options = {}) {
260
const { injections = {}, globals = [] } = options;
261
262
return {
263
transform(code, id) {
264
const ast = this.parse(code);
265
let scope = attachScopes(ast);
266
267
const toInject = new Set();
268
const declared = new Set();
269
270
// First pass: collect all declared variables
271
walk(ast, {
272
enter(node) {
273
if (node.scope) scope = node.scope;
274
275
// Extract declared names from various declaration types
276
if (node.type === 'VariableDeclarator') {
277
extractAssignedNames(node.id).forEach(name => declared.add(name));
278
}
279
280
if (node.type === 'FunctionDeclaration' && node.id) {
281
declared.add(node.id.name);
282
}
283
284
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
285
node.params.forEach(param => {
286
extractAssignedNames(param).forEach(name => declared.add(name));
287
});
288
}
289
},
290
leave(node) {
291
if (node.scope) scope = scope.parent;
292
}
293
});
294
295
// Second pass: find variables to inject
296
scope = attachScopes(ast);
297
walk(ast, {
298
enter(node) {
299
if (node.scope) scope = node.scope;
300
301
if (node.type === 'Identifier' && injections[node.name]) {
302
// Only inject if not declared in any scope and not a global
303
if (!scope.contains(node.name) && !globals.includes(node.name)) {
304
toInject.add(node.name);
305
}
306
}
307
},
308
leave(node) {
309
if (node.scope) scope = scope.parent;
310
}
311
});
312
313
if (toInject.size > 0) {
314
const imports = Array.from(toInject)
315
.map(name => `import ${name} from '${injections[name]}';`)
316
.join('\n');
317
318
return { code: imports + '\n' + code };
319
}
320
321
return { code };
322
}
323
};
324
}
325
```
326
327
### Dead Code Detection
328
329
```typescript
330
import { attachScopes, extractAssignedNames } from "@rollup/pluginutils";
331
import { walk } from "estree-walker";
332
333
export default function deadCodePlugin() {
334
return {
335
transform(code, id) {
336
const ast = this.parse(code);
337
let scope = attachScopes(ast);
338
339
const variables = new Map();
340
341
// Track variable declarations and usage
342
walk(ast, {
343
enter(node) {
344
if (node.scope) scope = node.scope;
345
346
// Record declarations
347
if (node.type === 'VariableDeclarator') {
348
const names = extractAssignedNames(node.id);
349
names.forEach(name => {
350
variables.set(name, {
351
declared: node,
352
used: false,
353
scope: scope
354
});
355
});
356
}
357
358
// Record usage
359
if (node.type === 'Identifier') {
360
const variable = variables.get(node.name);
361
if (variable) {
362
variable.used = true;
363
}
364
}
365
},
366
leave(node) {
367
if (node.scope) scope = scope.parent;
368
}
369
});
370
371
// Report unused variables
372
for (const [name, info] of variables) {
373
if (!info.used) {
374
console.warn(`Dead code: unused variable '${name}'`);
375
}
376
}
377
378
return { code };
379
}
380
};
381
}
382
```