Análise estática avançada com ts-morph
1. Introdução ao ts-morph e seus fundamentos
O ts-morph é uma biblioteca que fornece uma API wrapper sobre o compilador TypeScript, simplificando drasticamente a análise e manipulação de código fonte. Enquanto o módulo ts nativo do TypeScript oferece acesso direto ao compilador, sua API é extremamente verbosa e de baixo nível. O ts-morph abstrai essa complexidade, oferecendo uma interface mais expressiva e orientada a objetos.
Diferenças fundamentais:
| Aspecto | ts nativo |
ts-morph |
|---|---|---|
| Criação de AST | Requer múltiplas chamadas | Project.createSourceFile() |
| Navegação | ts.forEachChild() |
node.getChildren() |
| Manipulação | Complexa, com factories | Métodos intuitivos como replaceWithText() |
Instalação e configuração básica:
import { Project, SourceFile } from "ts-morph";
const project = new Project({
tsConfigFilePath: "tsconfig.json",
addFilesFromTsConfig: true,
});
// Ou criar manualmente um arquivo fonte
const sourceFile = project.createSourceFile(
"exemplo.ts",
`
function saudacao(nome: string): string {
return "Olá, " + nome;
}
`
);
console.log(sourceFile.getText());
O objeto Project gerencia todo o contexto de análise, incluindo arquivos, dependências e configurações do TypeScript.
2. Navegando na AST com ts-morph
A Árvore Sintática Abstrata (AST) é a estrutura central do ts-morph. Cada elemento do código é um Node, classificado por SyntaxKind.
// Navegação básica
const functionDecl = sourceFile.getFunction("saudacao");
if (functionDecl) {
console.log("Nome da função:", functionDecl.getName());
console.log("Parâmetros:", functionDecl.getParameters().map(p => p.getName()));
// Navegação hierárquica
const body = functionDecl.getBody();
const returnStatement = body?.getDescendantsOfKind(
SyntaxKind.ReturnStatement
);
// Filtragem com predicados customizados
const strings = body?.getDescendants().filter(node =>
node.getKind() === SyntaxKind.StringLiteral
);
}
Métodos essenciais de navegação:
- getChildren() — retorna nós filhos diretos
- getParent() — sobe um nível na hierarquia
- getDescendants() — todos os descendentes recursivamente
- getDescendantsOfKind(kind) — filtragem por tipo específico
3. Manipulação e transformação de código fonte
Uma das capacidades mais poderosas do ts-morph é a manipulação programática do código.
// Adicionando imports
const arquivo = project.getSourceFileOrThrow("src/app.ts");
arquivo.addImportDeclaration({
moduleSpecifier: "./utils",
namedImports: ["formatDate", "parseNumber"],
});
// Inserindo statements
const funcao = arquivo.getFunction("processData");
funcao?.insertStatements(0, [
"if (!data) throw new Error('Dados inválidos');",
"const validated = validateInput(data);",
]);
// Substituição de nós
const oldVar = arquivo.getVariableDeclaration("oldName");
oldVar?.replaceWithText("const newName = 42");
// Renomeação automatizada
const symbols = arquivo.getExportedDeclarations();
symbols.forEach((declarations, name) => {
if (name.startsWith("deprecated_")) {
declarations.forEach(decl => decl.rename(name.replace("deprecated_", "")));
}
});
4. Análise de tipos e inferência
O ts-morph expõe o sistema de tipos do TypeScript de forma acessível.
const sourceFile = project.createSourceFile(
"tipos.ts",
`
interface Usuario {
id: number;
nome: string;
email: string;
}
function buscarUsuario(id: number): Usuario {
// implementação
}
const usuario: Usuario = { id: 1, nome: "João", email: "joao@email.com" };
`
);
// Obtendo tipos de variáveis
const usuarioVar = sourceFile.getVariableDeclaration("usuario");
const tipoUsuario = usuarioVar?.getType();
console.log("Propriedades do tipo:", tipoUsuario?.getProperties().map(p => p.getName()));
// Análise de funções
const funcao = sourceFile.getFunction("buscarUsuario");
const tipoRetorno = funcao?.getReturnType();
console.log("Tipo de retorno:", tipoRetorno?.getText());
// Verificação de compatibilidade
const tipoNumber = project.getTypeChecker().getTypeAtLocation(
sourceFile.getFirstDescendantByKind(SyntaxKind.NumberKeyword)!
);
console.log("É string?", tipoNumber.isString()); // false
console.log("É número?", tipoNumber.isNumber()); // true
5. Busca e consulta de código em larga escala
Para projetos reais, o ts-morph permite análise em múltiplos arquivos.
// Projeto completo
const project = new Project({
tsConfigFilePath: "tsconfig.json",
addFilesFromTsConfig: true,
});
// Consultas em todo o projeto
const allFunctions = project.getSourceFiles()
.flatMap(sf => sf.getFunctions())
.filter(fn => fn.getParameters().length > 5);
// Encontrando referências
const minhaClasse = project.getSourceFile("minhaClasse.ts")
?.getClass("MinhaClasse");
if (minhaClasse) {
const referencias = minhaClasse.findReferencesAsNodes();
console.log(`Referências encontradas: ${referencias.length}`);
// Definições
const definicoes = minhaClasse.getDefinitions();
definicoes.forEach(def => {
console.log(`Definido em: ${def.getSourceFile().getFilePath()}`);
});
}
// Exportações
const exports = project.getSourceFileOrThrow("index.ts")
.getExportedDeclarations();
exports.forEach((decls, name) => {
console.log(`Exportado: ${name} - ${decls.length} declarações`);
});
6. Criação de ferramentas de análise personalizadas
O ts-morph é ideal para construir ferramentas de análise estática sob medida.
// Lint customizado: proibir console.log
function lintConsoleLogs(project: Project): string[] {
const erros: string[] = [];
project.getSourceFiles().forEach(sf => {
const consoleCalls = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
.filter(call => {
const expr = call.getExpression();
return expr.getText() === "console.log";
});
consoleCalls.forEach(call => {
erros.push(
`${sf.getFilePath()}:${call.getStartLineNumber()} - console.log não permitido`
);
});
});
return erros;
}
// Geração de métricas
interface Metricas {
arquivo: string;
funcoes: number;
complexidadeMedia: number;
coberturaTipos: number;
}
function gerarMetricas(project: Project): Metricas[] {
return project.getSourceFiles().map(sf => {
const funcoes = sf.getFunctions();
const totalParams = funcoes.reduce((acc, fn) =>
acc + fn.getParameters().length, 0
);
const paramsTipados = funcoes.reduce((acc, fn) =>
acc + fn.getParameters().filter(p => p.getType().getText() !== "any").length, 0
);
return {
arquivo: sf.getFilePath(),
funcoes: funcoes.length,
complexidadeMedia: funcoes.length > 0 ? totalParams / funcoes.length : 0,
coberturaTipos: totalParams > 0 ? (paramsTipados / totalParams) * 100 : 100,
};
});
}
// Integração CLI
const erros = lintConsoleLogs(project);
const metricas = gerarMetricas(project);
console.log("Erros encontrados:", erros.length);
console.log("Métricas:", JSON.stringify(metricas, null, 2));
7. Boas práticas e armadilhas comuns
Performance:
// Evite processamento redundante
const sourceFile = project.getSourceFileOrThrow("grande.ts");
// Ruim: percorre várias vezes
const vars = sourceFile.getVariableDeclarations();
const funcs = sourceFile.getFunctions();
// Bom: cache de resultados
const allDeclarations = sourceFile.getDescendants();
const vars2 = allDeclarations.filter(n => n.getKind() === SyntaxKind.VariableDeclaration);
const funcs2 = allDeclarations.filter(n => n.getKind() === SyntaxKind.FunctionDeclaration);
// Lazy loading com Project
const project = new Project({
useInMemoryFileSystem: true,
skipLoadingLibFiles: true, // Para projetos grandes
});
Lidando com erros:
try {
const sourceFile = project.getSourceFileOrThrow("inexistente.ts");
} catch (error) {
console.error("Arquivo não encontrado no projeto");
}
// Código inválido
const sourceFile = project.createSourceFile("invalido.ts", "const x = ;");
const diagnostics = project.getPreEmitDiagnostics();
diagnostics.forEach(diag => {
console.log(`Erro: ${diag.getMessageText()}`);
});
Limitações importantes:
- ts-morph não executa TypeScript, apenas analisa sintaticamente
- Para análise semântica completa, ainda depende do compilador TypeScript
- Projetos muito grandes podem exigir otimizações de memória
- Algumas transformações complexas podem ser mais fáceis com o
tsnativo
Referências
- Documentação oficial do ts-morph — Guia completo com exemplos de uso, API reference e tutoriais introdutórios
- TypeScript Compiler API — Documentação oficial da Microsoft sobre a API do compilador TypeScript que fundamenta o ts-morph
- AST Explorer — Ferramenta interativa para visualizar ASTs de TypeScript, útil para entender a estrutura que o ts-morph navega
- ts-morph: A Wrapper Around the TypeScript Compiler API — Artigo técnico detalhado no LogRocket Blog sobre casos de uso avançados
- Building Custom Linters with ts-morph — Tutorial prático no DEV Community sobre criação de ferramentas de análise personalizadas
- TypeScript Deep Dive: Compiler Internals — Capítulo do livro "TypeScript Deep Dive" sobre os internos do compilador que o ts-morph abstrai