首页 > 解决方案 > 如何在使用 Compiler API 进行类型检查之前转换 TypeScript 代码

问题描述

意图

我想使用 TypeScript 的Compiler API来试验 TypeScript 代码中的运算符重载。具体来说,我想找到 的所有实例x + y并将它们转换为op_add(x, y). 但是,我希望语言服务(例如 VS Code 中的 IntelliSense)能够了解转换并显示正确的类型。

例如在这段代码中:

interface Vector2 { x: number, y: number }
declare function op_add(x: Vector2, y: Vector2): Vector2
declare let a: Vector2, b: Vector2

let c = a + b

我希望当我将鼠标悬停在上面时c,它会显示Vector2


计划

为了实现这一点,我必须:

  1. typescript创建一个程序,以同样的方式公开相同的 API ttypescript
  2. 使该程序在将源代码传递给之前对其进行修改typescript
  3. 让 VS Code(或任何编辑器)使用我的包而不是typescript

处决

我首先创建了一个名为的简短脚本compile.ts,该脚本使用 Compiler API 来解析一个名为ASTsample.ts的文件。然后它直接修改 AST 并更改为. 最后它将修改后的代码打印到控制台,然后尝试发出。仅此一项对于 IDE 集成来说是不够的,但它是一个好的开始。Binary(x, PlusToken, y)Call(op_add, x, y)

compile.ts

import * as ts from "typescript"
import { possibleChildProperties } from "./visit";

let program = ts.createProgram(['sample.ts'], { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS })
let inputFiles = program.getSourceFiles()
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })

let outputCode: string

for (let input of inputFiles) {
  if (input.fileName === 'sample.ts') {
    ts.visitNode(input, visitor) // modifies input's AST
    outputCode = printer.printNode(ts.EmitHint.Unspecified, input, input)
    break
  }
}

console.log(outputCode) // works
let emitResult = program.emit() // fails



function visitor(node: ts.Node): ts.Node {
  if (node.kind === ts.SyntaxKind.BinaryExpression) {
    let expr = node as ts.BinaryExpression

    if (expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
      return ts.createCall(ts.createIdentifier('op_add'), [], [expr.left, expr.right])
    }
  }

  return visitChildren(node, visitor)
}

function visitChildren(node: ts.Node, visitor: ts.Visitor) {
  for (const prop of possibleChildProperties) {
    if (node[prop] !== undefined) {
      if (Array.isArray(node[prop]))
        node[prop] = node[prop].map(visitor)
      else
        node[prop] = visitor(node[prop])
    }
  }

  return node
}

sample.ts

let a = { a: 4 }
let b = { b: 3 }
let c = a + b

console.log输出:

let a = { a: 4 };
let b = { b: 3 };
let c = op_add(a, b);

问题

虽然代码打印机工作正常并输出正确的代码,但调用program.emit()会导致未指定的内部错误。这可能意味着我正在以不受支持的方式修改 AST。

/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:100920
                throw e;
                ^

Error: start < 0
    at createTextSpan (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:10559:19)
    at Object.createTextSpanFromBounds (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:10568:16)
    at getErrorSpanForNode (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:13914:19)
    at createDiagnosticForNodeInSourceFile (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:13808:20)
    at Object.createDiagnosticForNode (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:13799:16)
    at error (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:35703:22)
    at resolveNameHelper (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:36602:29)
    at resolveName (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:36274:20)
    at getResolvedSymbol (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:52602:21)
    at checkIdentifier (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:54434:26)

问题

在运行类型检查器之前修改程序的 AST 的正确方法是什么?我知道 AST 最好是只读的,但该标准ts.visitEachChild只能类型检查后使用。并且我自己对节点进行深度克隆似乎也不是一个可行的选择,因为没有任何方法可以Program从代码生成的 AST 创建一个。


更新

编辑 1:正如@jdaz 所注意到的,我sample.ts缺少一个声明op_add,这可能会导致问题。我将此行添加到文件的顶部:

declare function op_add(x: {}, y: {}): string

现在有一个不同的错误——文件​​诊断的生成失败:

/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:100920
                throw e;
                ^

Error: Debug Failure. Expected -2 >= 0
    at Object.createFileDiagnostic (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:17868:18)
    at grammarErrorAtPos (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:69444:36)
    at checkGrammarForAtLeastOneTypeArgument (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:68771:24)
    at checkGrammarTypeArguments (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:68777:17)
    at checkCallExpression (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:59255:18)
    at checkExpressionWorker (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:61687:28)
    at checkExpression (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:61597:38)
    at checkExpressionCached (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:61275:38)
    at checkVariableLikeDeclaration (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:63983:69)
    at checkVariableDeclaration (/home/m93a/Dokumenty/tsc-experiments/node_modules/typescript/lib/typescript.js:64051:20)

标签: typescripttypescript-compiler-api

解决方案


你接近了你的代码。您似乎遇到的第一个问题是发生的源代码文件检查,基本上Debug Failure. Expected -2 >= 0错误是说当尝试将 AST 与源代码匹配时它失败了。

第二个问题是您需要在visitNode生成新的 AST 树的同时修改现有的 AST 树。这也必须尽早完成(在发出称为 AFAIK 之前),否则 TypeChecker 可能会使用原始 AST 而不是更新的 AST。

下面是一个可以解决这两个问题的访问者函数示例。请注意,这真的很hacky和脆弱,预计它会经常中断。

老的:

function visitor(node: ts.Node): ts.Node {
  if (node.kind === ts.SyntaxKind.BinaryExpression) {
    let expr = node as ts.BinaryExpression

    if (expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
      return ts.createCall(ts.createIdentifier('op_add'), [], [expr.left, expr.right])
    }
  }

  return visitChildren(node, visitor)
}

新的:

function visitor(node: ts.Node): ts.Node {
  if (node.kind === ts.SyntaxKind.BinaryExpression) {
    let expr = node as ts.BinaryExpression;

    if (expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
      const newIdentifierNode = ts.createIdentifier('op_add');
      const newCallNode = ts.createCall(newIdentifierNode, [], [expr.left, expr.right]);
      newCallNode.flags = node.flags;
      newCallNode.pos = node.pos;
      newCallNode.end = node.end;
      newCallNode.parent = node.parent;
      newCallNode.typeArguments = undefined;

      Object.getOwnPropertyNames(node).forEach((prop) => {
          delete node[prop];
      });
      Object.getOwnPropertyNames(newCallNode).forEach((prop) => {
          node[prop] = newCallNode[prop];
      });
      return node;
    }
  }

  return visitChildren(node, visitor);
}

推荐阅读