TypeScript Compiler API:构建自定义 TypeScript 工具

TypeScript Compiler API:解锁元编程的潘多拉魔盒,打造专属 TypeScript 军火库 🚀

大家好!我是你们的老朋友,代码界的段子手,bug 界的终结者。今天,咱们要聊点刺激的,聊聊 TypeScript 的幕后英雄——Compiler API!

你是不是曾经对 TypeScript 编译过程感到好奇?是不是幻想过自己能像上帝一样操控 TypeScript 的一切?如果是,那 Compiler API 就是你手里的权杖,能让你把 TypeScript 玩出花来!

别害怕! 听起来很高大上,其实 Compiler API 就像一个乐高积木,你只需要了解每个积木的形状和功能,就能拼出各种你想要的玩具,啊不,工具!

1. TypeScript 编译:一个华丽的变身过程 🦋

在深入 Compiler API 之前,我们先来回顾一下 TypeScript 的编译过程,这就像一个丑小鸭变成白天鹅的华丽变身:

  1. 解析 (Parsing): TypeScript 编译器读取你的 .ts 文件,将代码分解成一个个小的语法单元,比如变量、函数、类等等。 这就像拆解玩具,把它们变成零件。
  2. 语法分析 (Syntax Analysis): 编译器会检查你的代码是否符合 TypeScript 的语法规则。 如果你写错了,它会毫不留情地告诉你,哪里错了,错得多离谱。 这就像检查零件是否完好无损,缺胳膊少腿可不行。
  3. 语义分析 (Semantic Analysis): 编译器会理解你的代码的含义,比如变量的类型、函数的参数等等。 这就像理解零件的用途,知道它们是用来做什么的。
  4. 类型检查 (Type Checking): 编译器会根据你的类型注解来检查代码的类型是否匹配。 如果你把字符串赋值给数字类型的变量,它会毫不留情地报错。 这就像检查零件是否能正确组装,大小不合适可不行。
  5. 代码生成 (Code Generation): 最后,编译器会将 TypeScript 代码转换成 JavaScript 代码,让浏览器或者 Node.js 可以执行。 这就像把零件组装成最终的玩具,可以玩耍了。

而 Compiler API,就是让你能够深入到这个编译过程的每一个环节,甚至可以修改它! 🤯

2. Compiler API:上帝模式的入场券 🎫

Compiler API 是一组 TypeScript 编译器暴露出来的 API,它允许你:

  • 读取和分析 TypeScript 代码。 你可以像编译器一样,解析 TypeScript 代码,获取代码的各种信息,比如变量的类型、函数的参数等等。
  • 修改 TypeScript 代码。 你可以修改 TypeScript 代码的语法树,添加、删除、修改节点,实现代码的转换和优化。
  • 生成 TypeScript 代码。 你可以根据自己的需求,生成新的 TypeScript 代码。

简单来说,Compiler API 让你拥有了:

  • 读心术: 能读懂 TypeScript 代码的内心世界。
  • 变形术: 能随心所欲地改变 TypeScript 代码。
  • 创造术: 能凭空创造 TypeScript 代码。

是不是感觉自己瞬间拥有了超能力? 😎

3. Compiler API 的核心概念:语法树 (Syntax Tree) 🌲

要玩转 Compiler API,首先要理解一个核心概念:语法树 (Syntax Tree)

语法树是 TypeScript 编译器用来表示代码结构的一种数据结构,它就像一棵倒立的树,根节点是整个 TypeScript 代码,叶子节点是代码的最小单元,比如变量、函数、表达式等等。

举个例子,对于这段简单的 TypeScript 代码:

const message: string = "Hello, world!";
console.log(message);

它的语法树大概是这样的(简化版):

  SourceFile
  ├── VariableStatement
  │   └── VariableDeclarationList
  │       └── VariableDeclaration
  │           ├── Identifier (message)
  │           ├── TypeAnnotation (string)
  │           └── StringLiteral ("Hello, world!")
  └── ExpressionStatement
      └── CallExpression
          ├── PropertyAccessExpression
          │   ├── Identifier (console)
          │   └── Identifier (log)
          └── ArgumentList
              └── Identifier (message)

每一个节点都代表了代码的一部分,比如 VariableStatement 代表变量声明语句,Identifier 代表标识符,StringLiteral 代表字符串字面量等等。

理解语法树非常重要! 因为 Compiler API 的核心操作就是对语法树进行遍历、分析和修改。

4. Compiler API 的常用接口:解锁技能点 🔓

Compiler API 提供了大量的接口,让我们来解锁几个常用的技能点:

  • ts.createProgram: 创建一个 TypeScript 编译器程序,它是 Compiler API 的入口。 你需要告诉它你要编译哪些文件,以及编译器的配置选项。

    const program = ts.createProgram({
        rootNames: ['src/index.ts'], // 要编译的文件
        options: {
            target: ts.ScriptTarget.ES2020, // 编译目标版本
            module: ts.ModuleKind.CommonJS, // 模块化方式
        },
    });
  • program.getSourceFile: 获取指定文件的语法树。 拿到语法树,你才能开始分析和修改代码。

    const sourceFile = program.getSourceFile('src/index.ts');
    if (sourceFile) {
        // 对 sourceFile 进行操作
    }
  • ts.forEachChild: 遍历语法树的子节点。 这是最常用的遍历语法树的方式,你可以递归地遍历整个语法树。

    function visit(node: ts.Node) {
        console.log(node.kind, ts.SyntaxKind[node.kind]); // 打印节点类型
        ts.forEachChild(node, visit); // 递归遍历子节点
    }
    
    if (sourceFile) {
        visit(sourceFile);
    }
  • ts.SyntaxKind: 枚举了所有可能的语法节点的类型。 你可以使用它来判断节点的类型,比如 ts.SyntaxKind.VariableDeclaration 代表变量声明节点,ts.SyntaxKind.Identifier 代表标识符节点等等。

  • *`ts.create系列函数:** 用于创建各种语法节点。 比如ts.createVariableDeclaration创建变量声明节点,ts.createIdentifier` 创建标识符节点等等。 这些函数让你能够动态地创建新的 TypeScript 代码。

  • *`ts.update系列函数:** 用于更新现有的语法节点。 比如ts.updateVariableDeclaration` 更新变量声明节点。 这些函数让你能够修改现有的 TypeScript 代码。

  • ts.transformNodes: 用于转换语法树的节点。 这是一个非常强大的函数,你可以使用它来对语法树进行各种复杂的转换。

掌握了这些技能点,你就可以开始用 Compiler API 做一些有趣的事情了! 🎉

5. 实战演练:打造你的专属 TypeScript 工具箱 🛠️

理论讲多了容易犯困,咱们来点实际的,用 Compiler API 打造几个小工具:

5.1 自动添加版权声明 📝

假设你希望在每个 TypeScript 文件的开头自动添加版权声明,你可以这样做:

import * as ts from 'typescript';
import * as fs from 'fs';

const banner = `
/**
 * Copyright (c) ${new Date().getFullYear()} Your Company
 * All rights reserved.
 */
`;

function addBanner(fileName: string) {
    const program = ts.createProgram({
        rootNames: [fileName],
        options: {
            target: ts.ScriptTarget.ES2020,
            module: ts.ModuleKind.CommonJS,
        },
    });

    const sourceFile = program.getSourceFile(fileName);
    if (!sourceFile) {
        console.error(`File not found: ${fileName}`);
        return;
    }

    const bannerComment = ts.factory.createJSDocComment(banner); // 创建 JSDoc 注释节点
    const updatedSourceFile = ts.factory.updateSourceFile(
        sourceFile,
        [bannerComment, ...sourceFile.statements], // 将注释添加到文件的开头
        sourceFile.isDeclarationFile,
        sourceFile.referencedFiles,
        sourceFile.typeReferenceDirectives,
        sourceFile.hasNoDefaultLib,
        sourceFile.libReferenceDirectives
    );

    const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
    const result = printer.printFile(updatedSourceFile);

    fs.writeFileSync(fileName, result);
    console.log(`Added banner to ${fileName}`);
}

// 使用示例
addBanner('src/index.ts');

这个工具很简单,它首先创建一个 TypeScript 编译器程序,然后获取指定文件的语法树,接着创建一个 JSDoc 注释节点,并将它添加到文件的开头。最后,它将修改后的语法树转换成 TypeScript 代码,并写入到文件中。

5.2 自动生成 API 文档 📚

假设你想根据 TypeScript 代码中的注释自动生成 API 文档,你可以这样做:

import * as ts from 'typescript';
import * as fs from 'fs';

interface ApiDoc {
    name: string;
    description: string;
    parameters: { name: string; type: string; description: string }[];
    returnType: string;
}

function generateApiDocs(fileName: string): ApiDoc[] {
    const program = ts.createProgram({
        rootNames: [fileName],
        options: {
            target: ts.ScriptTarget.ES2020,
            module: ts.ModuleKind.CommonJS,
            declaration: true, // 开启声明文件生成
        },
    });

    const sourceFile = program.getSourceFile(fileName);
    if (!sourceFile) {
        console.error(`File not found: ${fileName}`);
        return [];
    }

    const checker = program.getTypeChecker(); // 获取类型检查器

    const apiDocs: ApiDoc[] = [];

    function visit(node: ts.Node) {
        if (ts.isFunctionDeclaration(node) && node.name) {
            const symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                const jsDocTags = symbol.getJsDocTags(); // 获取 JSDoc 标签
                const description = ts.displayPartsToString(symbol.getDocumentationComment(checker));

                const parameters = node.parameters.map(param => {
                    const paramSymbol = checker.getSymbolAtLocation(param.name);
                    return {
                        name: param.name.getText(),
                        type: checker.typeToString(checker.getTypeAtLocation(param)),
                        description: paramSymbol ? ts.displayPartsToString(paramSymbol.getDocumentationComment(checker)) : '',
                    };
                });

                const returnType = checker.typeToString(checker.getReturnTypeOfSignature(checker.getSignatureFromDeclaration(node)!));

                apiDocs.push({
                    name: node.name.getText(),
                    description,
                    parameters,
                    returnType,
                });
            }
        }
        ts.forEachChild(node, visit);
    }

    visit(sourceFile);
    return apiDocs;
}

// 使用示例
const apiDocs = generateApiDocs('src/index.ts');
console.log(JSON.stringify(apiDocs, null, 2));

这个工具会遍历 TypeScript 代码,找到所有的函数声明,然后提取函数的名称、描述、参数和返回值等信息,并将这些信息转换成 API 文档的 JSON 格式。

5.3 代码风格检查 (Linter) 👮

你可以使用 Compiler API 来创建一个自定义的代码风格检查工具,检查代码是否符合你的规范。 比如,你可以检查代码中是否使用了 console.log,或者是否使用了特定的命名规范。

import * as ts from 'typescript';

function checkConsoleLog(fileName: string) {
    const program = ts.createProgram({
        rootNames: [fileName],
        options: {
            target: ts.ScriptTarget.ES2020,
            module: ts.ModuleKind.CommonJS,
        },
    });

    const sourceFile = program.getSourceFile(fileName);
    if (!sourceFile) {
        console.error(`File not found: ${fileName}`);
        return;
    }

    function visit(node: ts.Node) {
        if (ts.isCallExpression(node) && node.expression) {
            if (ts.isPropertyAccessExpression(node.expression) &&
                node.expression.expression.kind === ts.SyntaxKind.Identifier &&
                (node.expression.expression as ts.Identifier).text === 'console' &&
                node.expression.name.text === 'log') {
                console.warn(`[WARN] Found console.log in ${fileName} at line ${sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1}`);
            }
        }
        ts.forEachChild(node, visit);
    }

    visit(sourceFile);
}

// 使用示例
checkConsoleLog('src/index.ts');

这个工具会遍历 TypeScript 代码,找到所有的 console.log 调用,并发出警告。

注意: 这只是一个简单的示例,你可以根据自己的需求,扩展这个工具,添加更多的代码风格检查规则。

6. Compiler API 的进阶之路:Transformer 变形金刚 🤖

ts.transformNodes 是 Compiler API 中最强大的 API 之一,它允许你对语法树进行各种复杂的转换。 我们可以使用 Transformer 来实现代码的优化、代码的增强等等。

Transformer 就像一个变形金刚,它可以将一种语法树转换成另一种语法树。

一个 Transformer 接收一个语法树节点作为输入,并返回一个新的语法树节点。 你可以使用 Transformer 来:

  • 添加新的代码。 比如,你可以使用 Transformer 在每个函数调用前后添加日志。
  • 删除现有的代码。 比如,你可以使用 Transformer 删除代码中的 console.log
  • 修改现有的代码。 比如,你可以使用 Transformer 将 const 变量转换成 let 变量。

使用 Transformer 的基本步骤如下:

  1. 创建一个 Transformer 工厂函数。 这个函数接收一个 ts.TransformationContext 对象作为参数,并返回一个 ts.Transformer 对象。
  2. 创建一个 ts.Transformer 对象。 这个对象包含一个 visit 方法,用于遍历语法树的节点。
  3. visit 方法中,对每个节点进行转换。 如果你需要修改节点,你需要返回一个新的节点。 如果你不需要修改节点,你可以直接返回原始节点。
  4. 使用 ts.transformNodes 函数,将 Transformer 应用到语法树上。

示例:删除代码中的 console.log

import * as ts from 'typescript';

function removeConsoleLogTransformerFactory(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
    return (context: ts.TransformationContext) => {
        return (sourceFile: ts.SourceFile) => {
            function visitor(node: ts.Node): ts.Node {
                if (ts.isCallExpression(node) && node.expression) {
                    if (ts.isPropertyAccessExpression(node.expression) &&
                        node.expression.expression.kind === ts.SyntaxKind.Identifier &&
                        (node.expression.expression as ts.Identifier).text === 'console' &&
                        node.expression.name.text === 'log') {
                        return undefined; // 删除节点
                    }
                }
                return ts.visitEachChild(node, visitor, context);
            }
            return ts.visitNode(sourceFile, visitor);
        };
    };
}

function transformCode(fileName: string) {
    const program = ts.createProgram({
        rootNames: [fileName],
        options: {
            target: ts.ScriptTarget.ES2020,
            module: ts.ModuleKind.CommonJS,
        },
    });

    const sourceFile = program.getSourceFile(fileName);
    if (!sourceFile) {
        console.error(`File not found: ${fileName}`);
        return;
    }

    const { transformed } = ts.transformNodes(
        sourceFile,
        {
            transformations: [removeConsoleLogTransformerFactory(program)],
            program,
        }
    );

    const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
    const result = printer.printFile(transformed[0]);

    console.log(result); // 打印转换后的代码
}

// 使用示例
transformCode('src/index.ts');

这个 Transformer 会删除代码中所有的 console.log 调用。

Transformer 是 Compiler API 的高级用法,它可以让你对 TypeScript 代码进行各种复杂的转换和优化。 掌握 Transformer,你就能成为真正的 TypeScript 大师! 🧙

7. 总结:开启你的 TypeScript 元编程之旅 🚀

Compiler API 是 TypeScript 提供的一组强大的 API,它允许你深入到 TypeScript 编译过程的每一个环节,甚至可以修改它。

通过 Compiler API,你可以:

  • 读取和分析 TypeScript 代码。
  • 修改 TypeScript 代码。
  • 生成 TypeScript 代码。

掌握 Compiler API,你就能打造各种各样的 TypeScript 工具,比如:

  • 代码风格检查器 (Linter)。
  • 代码自动格式化工具 (Formatter)。
  • API 文档生成器。
  • 代码优化器。
  • 代码转换器。

Compiler API 就像一个潘多拉魔盒,它蕴藏着无限的可能性。 只要你有足够的想象力,你就能用它创造出各种奇妙的工具!

所以,不要犹豫,赶快拿起你的键盘,开始你的 TypeScript 元编程之旅吧! 祝你玩得开心! 🎉

最后的温馨提示: Compiler API 比较复杂,需要一定的 TypeScript 基础。 如果你对 TypeScript 还不太熟悉,建议先学习 TypeScript 的基础知识,然后再来学习 Compiler API。

希望这篇文章能够帮助你理解 Compiler API,并激发你对 TypeScript 元编程的兴趣! 谢谢大家! 👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注