AST do TypeScript: lendo e transformando código
1. Introdução à AST (Árvore Sintática Abstrata)
A Árvore Sintática Abstrata (AST) é uma representação hierárquica da estrutura gramatical do código fonte, abstraindo detalhes sintáticos como espaçamento, pontuação e comentários. Diferente da CST (Árvore Sintática Concreta), que preserva cada caractere, a AST foca apenas na estrutura essencial do programa.
O pipeline de processamento segue: código fonte → tokenização (lexer) → parsing → AST. Cada nó da AST representa um constructo da linguagem, como funções, variáveis ou expressões.
2. O Compilador TypeScript e sua API de AST
O compilador TypeScript expõe uma API robusta para trabalhar com AST. O ponto de partida é ts.createSourceFile:
import * as ts from 'typescript';
const code = `
function greet(name: string): string {
return "Hello, " + name;
}
`;
const sourceFile = ts.createSourceFile(
'example.ts',
code,
ts.ScriptTarget.Latest,
true
);
console.log(sourceFile.statements.length); // 1
Os tipos fundamentais de nós incluem:
- SourceFile: raiz da AST
- Statement: declarações como FunctionDeclaration, VariableStatement
- Expression: expressões como BinaryExpression, CallExpression
- Identifier: nomes de variáveis, funções
- Literal: valores literais (string, number, boolean)
Para percorrer a AST manualmente:
function visitNode(node: ts.Node, depth: number = 0) {
console.log(' '.repeat(depth) + ts.SyntaxKind[node.kind]);
ts.forEachChild(node, child => visitNode(child, depth + 1));
}
visitNode(sourceFile);
3. Lendo e Navegando na AST
O ts.SyntaxKind é um enum que identifica cada tipo de nó. Funções auxiliares como ts.isFunctionDeclaration() facilitam a navegação:
function extractFunctionDeclarations(sourceFile: ts.SourceFile): ts.FunctionDeclaration[] {
const functions: ts.FunctionDeclaration[] = [];
function visit(node: ts.Node) {
if (ts.isFunctionDeclaration(node)) {
functions.push(node);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return functions;
}
const functions = extractFunctionDeclarations(sourceFile);
functions.forEach(fn => {
if (fn.name) {
console.log(`Function: ${fn.name.text}`);
}
});
Para encontrar variáveis não utilizadas:
function findUnusedVariables(sourceFile: ts.SourceFile): string[] {
const declared = new Set<string>();
const used = new Set<string>();
function visit(node: ts.Node) {
if (ts.isVariableDeclaration(node) && node.name) {
declared.add(node.name.getText());
}
if (ts.isIdentifier(node) && node.parent && !ts.isVariableDeclaration(node.parent)) {
used.add(node.text);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return [...declared].filter(v => !used.has(v));
}
4. Transformando a AST com Visitantes
O padrão Visitante permite modificar a AST de forma controlada. Usamos ts.visitNode e ts.visitEachChild:
function addLoggingToFunctions(sourceFile: ts.SourceFile): ts.SourceFile {
function visitor(node: ts.Node): ts.Node {
if (ts.isFunctionDeclaration(node) && node.body) {
const logStatement = ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createIdentifier('console.log'),
undefined,
[ts.factory.createStringLiteral(`Entering function: ${node.name?.text || 'anonymous'}`)]
)
);
const updatedBody = ts.factory.createBlock([
logStatement,
...node.body.statements
]);
return ts.factory.updateFunctionDeclaration(
node,
node.modifiers,
node.asteriskToken,
node.name,
node.typeParameters,
node.parameters,
node.type,
updatedBody
);
}
return ts.visitEachChild(node, visitor, undefined);
}
return ts.visitNode(sourceFile, visitor) as ts.SourceFile;
}
Para usar transformações completas:
const result = ts.transform(sourceFile, [context => {
return sourceFile => {
function visitor(node: ts.Node): ts.Node {
// transformação aqui
return ts.visitEachChild(node, visitor, context);
}
return ts.visitNode(sourceFile, visitor);
};
}]);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const transformedCode = printer.printFile(result.transformed[0] as ts.SourceFile);
5. Análise Estática com a AST
Para análise mais profunda, usamos o TypeChecker:
function getTypeInfo(sourceFile: ts.SourceFile): void {
const program = ts.createProgram({
rootNames: [sourceFile.fileName],
options: {}
});
const checker = program.getTypeChecker();
function visit(node: ts.Node) {
if (ts.isVariableDeclaration(node) && node.initializer) {
const type = checker.getTypeAtLocation(node);
console.log(`Variable ${node.name.getText()}: ${checker.typeToString(type)}`);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
}
Para gerar documentação automática a partir de JSDoc:
function extractJSDoc(sourceFile: ts.SourceFile): Array<{ name: string; docs: string }> {
const docs: Array<{ name: string; docs: string }> = [];
function visit(node: ts.Node) {
const jsDoc = ts.getJSDocTags(node);
if (jsDoc.length > 0 && (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node))) {
docs.push({
name: node.name?.text || 'unknown',
docs: jsDoc.map(tag => `${tag.tagName.text}: ${tag.comment}`).join('\n')
});
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return docs;
}
6. Ferramentas Práticas e Integração
Usando ts-morph para simplificar manipulações:
import { Project, SyntaxKind } from 'ts-morph';
const project = new Project();
const sourceFile = project.createSourceFile('temp.ts', `
function oldName() {
return 42;
}
`);
// Renomear função
sourceFile.getFunction('oldName')?.rename('newName');
// Adicionar import
sourceFile.addImportDeclaration({
moduleSpecifier: './utils',
namedImports: ['helper']
});
console.log(sourceFile.getText());
Para criar regras ESLint personalizadas:
// Regra ESLint que proíbe console.log
export const rule = {
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log') {
context.report({
node,
message: 'console.log não é permitido'
});
}
}
};
}
};
7. Boas Práticas e Armadilhas Comuns
Performance: Evite percorrer toda a AST quando possível. Use ts.forEachChild com early exit:
function findFirstFunction(node: ts.Node): ts.FunctionDeclaration | undefined {
let found: ts.FunctionDeclaration | undefined;
ts.forEachChild(node, child => {
if (found) return; // early exit
if (ts.isFunctionDeclaration(child)) {
found = child;
} else {
found = findFirstFunction(child);
}
});
return found;
}
Preservação de formatação: Use ts.createPrinter com ts.PrinterOptions para manter espaçamento:
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: false
});
Limitações: A AST não resolve tipos ou escopos. Para isso, use TypeChecker. Lembre-se que modificações na AST podem quebrar referências.
Debugging: Visualize a AST com ts.SyntaxKind e use ts.createPrinter para ver o resultado das transformações.
function debugAST(node: ts.Node, indent: string = '') {
console.log(`${indent}${ts.SyntaxKind[node.kind]} [${node.getStart()}-${node.getEnd()}]`);
ts.forEachChild(node, child => debugAST(child, indent + ' '));
}
Referências
- TypeScript Compiler API Documentation — Documentação oficial da API do compilador TypeScript, incluindo manipulação de AST
- AST Explorer for TypeScript — Ferramenta interativa para visualizar AST de código TypeScript em tempo real
- ts-morph Documentation — Biblioteca wrapper que simplifica a manipulação de AST do TypeScript
- TypeScript Deep Dive: Compiler — Guia aprofundado sobre o funcionamento interno do compilador TypeScript
- ESLint Custom Rules with TypeScript — Tutorial oficial para criar regras ESLint que operam na AST
- TypeScript AST Viewer — Visualizador online de AST TypeScript com suporte a SyntaxKind e posições
- Writing TypeScript Transformers — Artigo técnico sobre criação de transformadores personalizados para TypeScript