or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ast-analysis.mdcode-generation.mdfile-processing.mdindex.mdregex-utilities.md

ast-analysis.mddocs/

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

```