Una guida completa all'API del compilatore TypeScript, che copre Abstract Syntax Trees (AST), analisi del codice, trasformazione e generazione per sviluppatori internazionali.
API del compilatore TypeScript: padroneggiare la manipolazione dell'AST e la trasformazione del codice
L'API del compilatore TypeScript fornisce un'interfaccia potente per analizzare, manipolare e generare codice TypeScript e JavaScript. Al suo cuore risiede l'Abstract Syntax Tree (AST), una rappresentazione strutturata del codice sorgente. Comprendere come lavorare con l'AST sblocca le capacità per la creazione di strumenti avanzati, come linters, formattatori di codice, analizzatori statici e generatori di codice personalizzati.
Cos'è l'API del compilatore TypeScript?
L'API del compilatore TypeScript è un insieme di interfacce e funzioni TypeScript che espongono il funzionamento interno del compilatore TypeScript. Consente agli sviluppatori di interagire a livello di codice con il processo di compilazione, andando oltre la semplice compilazione del codice. Puoi usarlo per:
- Analizzare il codice: ispezionare la struttura del codice, identificare potenziali problemi ed estrarre informazioni semantiche.
- Trasformare il codice: modificare il codice esistente, aggiungere nuove funzionalità o refactorare il codice automaticamente.
- Generare codice: creare nuovo codice da zero in base a modelli o altri input.
Questa API è essenziale per la creazione di strumenti di sviluppo sofisticati che migliorano la qualità del codice, automatizzano le attività ripetitive e migliorano la produttività degli sviluppatori.
Comprendere l'Abstract Syntax Tree (AST)
L'AST è una rappresentazione ad albero della struttura del tuo codice. Ogni nodo nell'albero rappresenta una costruzione sintattica, come una dichiarazione di variabile, una chiamata di funzione o un'istruzione di controllo del flusso. L'API del compilatore TypeScript fornisce strumenti per attraversare l'AST, ispezionare i suoi nodi e modificarli.
Considera questo semplice codice TypeScript:
function greet(name: string): string {
return `Ciao, ${name}!`;
}
console.log(greet("Mondo"));
L'AST per questo codice rappresenterebbe la dichiarazione della funzione, l'istruzione return, il template literal, la chiamata a console.log e altri elementi del codice. La visualizzazione dell'AST può essere impegnativa, ma strumenti come AST explorer (astexplorer.net) possono aiutare. Questi strumenti consentono di inserire codice e visualizzare il suo AST corrispondente in un formato intuitivo. L'uso di AST Explorer ti aiuterà a capire il tipo di struttura del codice che manipolerai.
Tipi di nodo AST chiave
L'API del compilatore TypeScript definisce vari tipi di nodo AST, ognuno dei quali rappresenta una diversa costruzione sintattica. Ecco alcuni tipi di nodo comuni:
- SourceFile: rappresenta un intero file TypeScript.
- FunctionDeclaration: rappresenta una definizione di funzione.
- VariableDeclaration: rappresenta una dichiarazione di variabile.
- Identifier: rappresenta un identificatore (ad esempio, nome variabile, nome funzione).
- StringLiteral: rappresenta un letterale stringa.
- CallExpression: rappresenta una chiamata di funzione.
- ReturnStatement: rappresenta un'istruzione return.
Ogni tipo di nodo ha proprietà che forniscono informazioni sull'elemento di codice corrispondente. Ad esempio, un nodo `FunctionDeclaration` potrebbe avere proprietà per il suo nome, i parametri, il tipo di ritorno e il corpo.
Inizia a utilizzare l'API del compilatore
Per iniziare a utilizzare l'API del compilatore, dovrai installare TypeScript e avere una conoscenza di base della sintassi TypeScript. Ecco un semplice esempio che dimostra come leggere un file TypeScript e stampare il suo AST:
import * as ts from "typescript";
import * as fs from "fs";
const fileName = "example.ts";
const sourceCode = fs.readFileSync(fileName, "utf8");
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.ES2015, // Versione ECMAScript di destinazione
true // SetParentNodes: true per mantenere i riferimenti al padre nell'AST
);
function printAST(node: ts.Node, indent = 0) {
const indentStr = " ".repeat(indent);
console.log(`${indentStr}${ts.SyntaxKind[node.kind]}`);
node.forEachChild(child => printAST(child, indent + 1));
}
printAST(sourceFile);
Spiegazione:
- Importa moduli: importa il modulo `typescript` e il modulo `fs` per le operazioni del file system.
- Leggi il file sorgente: legge il contenuto di un file TypeScript denominato `example.ts`. Dovrai creare un file `example.ts` affinché funzioni.
- Crea SourceFile: crea un oggetto `SourceFile`, che rappresenta la radice dell'AST. La funzione `ts.createSourceFile` analizza il codice sorgente e genera l'AST.
- Stampa AST: definisce una funzione ricorsiva `printAST` che attraversa l'AST e stampa il tipo di ogni nodo.
- Chiama printAST: chiama `printAST` per iniziare a stampare l'AST dal nodo radice `SourceFile`.
Per eseguire questo codice, salvatelo come file `.ts` (ad esempio, `ast-example.ts`), create un file `example.ts` con del codice TypeScript, quindi compilate ed eseguite il codice:
tsc ast-example.ts
node ast-example.js
Questo stamperà l'AST del tuo file `example.ts` sulla console. L'output mostrerà la gerarchia dei nodi e i loro tipi. Ad esempio, potrebbe mostrare `FunctionDeclaration`, `Identifier`, `Block` e altri tipi di nodo.
Attraversamento dell'AST
L'API del compilatore fornisce diversi modi per attraversare l'AST. Il più semplice è l'uso del metodo `forEachChild`, come mostrato nell'esempio precedente. Questo metodo visita ogni nodo figlio di un dato nodo.
Per scenari di attraversamento più complessi, è possibile utilizzare un pattern `Visitor`. Un visitor è un oggetto che definisce i metodi da chiamare per specifici tipi di nodo. Questo ti consente di personalizzare il processo di attraversamento ed eseguire azioni in base al tipo di nodo.
import * as ts from "typescript";
import * as fs from "fs";
const fileName = "example.ts";
const sourceCode = fs.readFileSync(fileName, "utf8");
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.ES2015,
true
);
class IdentifierVisitor {
visit(node: ts.Node) {
if (ts.isIdentifier(node)) {
console.log(`Identificatore trovato: ${node.text}`);
}
ts.forEachChild(node, n => this.visit(n));
}
}
const visitor = new IdentifierVisitor();
visitor.visit(sourceFile);
Spiegazione:
- Classe IdentifierVisitor: definisce una classe `IdentifierVisitor` con un metodo `visit`.
- Metodo Visit: il metodo `visit` verifica se il nodo corrente è un `Identifier`. In tal caso, stampa il testo dell'identificatore. Quindi chiama ricorsivamente `ts.forEachChild` per visitare i nodi figli.
- Crea Visitor: crea un'istanza di `IdentifierVisitor`.
- Avvia l'attraversamento: chiama il metodo `visit` su `SourceFile` per avviare l'attraversamento.
Questo esempio dimostra come trovare tutti gli identificatori nell'AST. Puoi adattare questo pattern per trovare altri tipi di nodo ed eseguire azioni diverse.
Trasformazione dell'AST
Il vero potere dell'API del compilatore risiede nella sua capacità di trasformare l'AST. È possibile modificare l'AST per modificare la struttura e il comportamento del codice. Questa è la base per gli strumenti di refactoring del codice, i generatori di codice e altri strumenti avanzati.
Per trasformare l'AST, dovrai usare la funzione `ts.transform`. Questa funzione accetta un `SourceFile` e un elenco di funzioni `TransformerFactory`. Un `TransformerFactory` è una funzione che accetta un `TransformationContext` e restituisce una funzione `Transformer`. La funzione `Transformer` è responsabile della visita e della trasformazione dei nodi nell'AST.
Ecco un semplice esempio che dimostra come aggiungere un commento all'inizio di un file TypeScript:
import * as ts from "typescript";
import * as fs from "fs";
const fileName = "example.ts";
const sourceCode = fs.readFileSync(fileName, "utf8");
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.ES2015,
true
);
const transformerFactory: ts.TransformerFactory = context => {
return transformer => {
return node => {
if (ts.isSourceFile(node)) {
// Crea un commento principale
const comment = ts.addSyntheticLeadingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
" Questo file è stato trasformato automaticamente ",
true // hasTrailingNewLine
);
return node;
}
return node;
};
};
};
const { transformed } = ts.transform(sourceFile, [transformerFactory]);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed
});
const result = printer.printFile(transformed[0]);
fs.writeFileSync("example.transformed.ts", result);
Spiegazione:
- TransformerFactory: definisce una funzione `TransformerFactory` che restituisce una funzione `Transformer`.
- Transformer: la funzione `Transformer` verifica se il nodo corrente è un `SourceFile`. In tal caso, aggiunge un commento principale al nodo usando `ts.addSyntheticLeadingComment`.
- ts.transform: chiama `ts.transform` per applicare la trasformazione al `SourceFile`.
- Stampante: crea un oggetto `Printer` per generare codice dall'AST trasformato.
- Stampa e scrittura: stampa il codice trasformato e lo scrive in un nuovo file denominato `example.transformed.ts`.
Questo esempio dimostra una semplice trasformazione, ma puoi usare lo stesso pattern per eseguire trasformazioni più complesse, come il refactoring del codice, l'aggiunta di istruzioni di registrazione o la generazione di documentazione.
Tecniche di trasformazione avanzate
Ecco alcune tecniche di trasformazione avanzate che puoi utilizzare con l'API del compilatore:
- Creazione di nuovi nodi: usa le funzioni `ts.createXXX` per creare nuovi nodi AST. Ad esempio, `ts.createVariableDeclaration` crea un nuovo nodo di dichiarazione di variabile.
- Sostituzione di nodi: sostituisci i nodi esistenti con nuovi nodi usando la funzione `ts.visitEachChild`.
- Aggiunta di nodi: aggiungi nuovi nodi all'AST usando le funzioni `ts.updateXXX`. Ad esempio, `ts.updateBlock` aggiorna un'istruzione block con nuove istruzioni.
- Rimozione di nodi: rimuovi i nodi dall'AST restituendo `undefined` dalla funzione transformer.
Generazione del codice
Dopo aver trasformato l'AST, dovrai generare il codice da esso. L'API del compilatore fornisce un oggetto `Printer` a questo scopo. Il `Printer` prende un AST e genera una rappresentazione stringa del codice.
La funzione `ts.createPrinter` crea un oggetto `Printer`. È possibile configurare la stampante con varie opzioni, come il carattere di nuova riga da utilizzare e se emettere commenti.
Il metodo `printer.printFile` accetta un `SourceFile` e restituisce una rappresentazione stringa del codice. È quindi possibile scrivere questa stringa in un file.
Applicazioni pratiche dell'API del compilatore
L'API del compilatore TypeScript ha numerose applicazioni pratiche nello sviluppo software. Ecco alcuni esempi:
- Linters: crea linters personalizzati per applicare gli standard di codifica e identificare potenziali problemi nel codice.
- Formattatori di codice: crea formattatori di codice per formattare automaticamente il codice in base a una guida di stile specifica.
- Analizzatori statici: sviluppa analizzatori statici per rilevare bug, vulnerabilità di sicurezza e colli di bottiglia delle prestazioni nel codice.
- Generatori di codice: genera codice da modelli o altri input, automatizzando le attività ripetitive e riducendo il codice boilerplate. Ad esempio, la generazione di client API o schemi di database da un file di descrizione.
- Strumenti di refactoring: crea strumenti di refactoring per rinominare automaticamente le variabili, estrarre funzioni o spostare codice tra file.
- Automazione dell'internazionalizzazione (i18n): estrae automaticamente le stringhe traducibili dal codice TypeScript e genera file di localizzazione per lingue diverse. Ad esempio, uno strumento potrebbe scansionare il codice alla ricerca di stringhe passate a una funzione `translate()` e aggiungerle automaticamente a un file di risorse di traduzione.
Esempio: creazione di un semplice linter
Creiamo un semplice linter che controlla le variabili inutilizzate nel codice TypeScript. Questo linter identificherà le variabili dichiarate ma mai utilizzate.
import * as ts from "typescript";
import * as fs from "fs";
const fileName = "example.ts";
const sourceCode = fs.readFileSync(fileName, "utf8");
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.ES2015,
true
);
function findUnusedVariables(sourceFile: ts.SourceFile) {
const usedVariables = new Set();
function visit(node: ts.Node) {
if (ts.isIdentifier(node)) {
usedVariables.add(node.text);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
const unusedVariables: string[] = [];
function checkVariableDeclaration(node: ts.Node) {
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
const variableName = node.name.text;
if (!usedVariables.has(variableName)) {
unusedVariables.push(variableName);
}
}
ts.forEachChild(node, checkVariableDeclaration);
}
checkVariableDeclaration(sourceFile);
return unusedVariables;
}
const unusedVariables = findUnusedVariables(sourceFile);
if (unusedVariables.length > 0) {
console.log("Variabili inutilizzate:");
unusedVariables.forEach(variable => console.log(`- ${variable}`));
} else {
console.log("Nessuna variabile inutilizzata trovata.");
}
Spiegazione:
- Funzione findUnusedVariables: definisce una funzione `findUnusedVariables` che accetta un `SourceFile` come input.
- usedVariables Set: crea un `Set` per memorizzare i nomi delle variabili utilizzate.
- Funzione visit: definisce una funzione ricorsiva `visit` che attraversa l'AST e aggiunge i nomi di tutti gli identificatori al set `usedVariables`.
- Funzione checkVariableDeclaration: definisce una funzione ricorsiva `checkVariableDeclaration` che verifica se una dichiarazione di variabile non è usata. In tal caso, aggiunge il nome della variabile all'array `unusedVariables`.
- Return unusedVariables: restituisce un array contenente i nomi di eventuali variabili inutilizzate.
- Output: stampa le variabili inutilizzate sulla console.
Questo esempio dimostra un semplice linter. Puoi estenderlo per verificare altri standard di codifica e identificare altri potenziali problemi nel tuo codice. Ad esempio, potresti controllare importazioni inutilizzate, funzioni eccessivamente complesse o potenziali vulnerabilità di sicurezza. La chiave è capire come attraversare l'AST e identificare i tipi di nodo specifici che ti interessano.
Best practice e considerazioni
- Comprendi l'AST: investi tempo per comprendere la struttura dell'AST. Usa strumenti come AST explorer per visualizzare l'AST del tuo codice.
- Usa type guard: usa type guard (`ts.isXXX`) per assicurarti di lavorare con i tipi di nodo corretti.
- Considera le prestazioni: le trasformazioni AST possono essere costose dal punto di vista computazionale. Ottimizza il tuo codice per ridurre al minimo il numero di nodi che visiti e trasformi.
- Gestisci gli errori: gestisci gli errori in modo appropriato. L'API del compilatore può generare eccezioni se provi a eseguire operazioni non valide sull'AST.
- Test completo: testa a fondo le tue trasformazioni per assicurarti che producano i risultati desiderati e non introducano nuovi bug.
- Usa le librerie esistenti: considera l'utilizzo di librerie esistenti che forniscono astrazioni di livello superiore sull'API del compilatore. Queste librerie possono semplificare le attività comuni e ridurre la quantità di codice che devi scrivere. Esempi includono `ts-morph` e `typescript-eslint`.
Conclusione
L'API del compilatore TypeScript è uno strumento potente per la creazione di strumenti di sviluppo avanzati. Comprendendo come lavorare con l'AST, puoi creare linters, formattatori di codice, analizzatori statici e altri strumenti che migliorano la qualità del codice, automatizzano le attività ripetitive e migliorano la produttività degli sviluppatori. Sebbene l'API possa essere complessa, i vantaggi di padroneggiarla sono significativi. Questa guida completa fornisce le basi per esplorare e utilizzare l'API del compilatore in modo efficace nei tuoi progetti. Ricorda di sfruttare strumenti come AST Explorer, di gestire con cura i tipi di nodo e di testare a fondo le tue trasformazioni. Con pratica e dedizione, puoi sbloccare tutto il potenziale dell'API del compilatore TypeScript e creare soluzioni innovative per il panorama dello sviluppo software.
Ulteriori approfondimenti:
- Documentazione dell'API del compilatore TypeScript: [https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
- AST Explorer: [https://astexplorer.net/](https://astexplorer.net/)
- Libreria ts-morph: [https://ts-morph.com/](https://ts-morph.com/)
- typescript-eslint: [https://typescript-eslint.io/](https://typescript-eslint.io/)