【问题标题】:How to transform TypeScript code before type-checking using Compiler API如何在使用 Compiler API 进行类型检查之前转换 TypeScript 代码
【发布时间】:2020-11-16 05:35:16
【问题描述】:

意图

我想使用 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 的短脚本,它使用编译器API 将一个名为sample.ts 的文件解析为AST。然后直接修改AST,将Binary(x, PlusToken, y)改为Call(op_add, x, y)。最后它将修改后的代码打印到控制台,然后尝试发出。仅此一项对于 IDE 集成来说是不够的,但它是一个好的开始。

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 只能使用 类型检查之后。而且我自己对节点进行深度克隆似乎也不是一个可行的选择,因为没有任何方法可以从代码生成的 AST 创建Program


更新

编辑 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)

【问题讨论】:

  • 错误可能只是因为您没有在任何一个文件中定义op_add 吗?所以在typescript.js 中,当它调用node.getStart 时,它找不到任何东西并返回-1。
  • @jdaz 很棒的观察!但遗憾的是,一个错误消失了,另一个错误又出现了。我编辑了问题。
  • 嗯,你需要实际定义整个函数而不是只声明它吗?
  • 也试过了,和只做delcaring完全一样。

标签: typescript typescript-compiler-api


【解决方案1】:

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

第二个问题是您需要修改现有的 AST 树,而 visitNode 正在生成新的 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);
}

【讨论】:

  • 谢谢,这太棒了!实际上newCallNode.typeArguments = undefined; 部分也很重要。我不知道你必须为非泛型函数传递 undefined。
【解决方案2】:

这可能是一种 hacky 方式,但既然您已经拥有修改后的源代码,为什么不从中构建一个新的 AST?例如:

const newSource = ts.createSourceFile(
        'newSource.ts',
        outputCode,
        ts.ScriptTarget.ES5,
        true
)
const newProgram = ts.createProgram(['newSource.ts'], { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS })
let emitResult = newProgram.emit()

这避免了对原始 AST 的更改,并且运行时不会出错。

【讨论】:

  • 您好,感谢您的回复!这很好,但是如果我必须将文件路径/名称翻译成不同的路径/名称,那么连接语言服务会困难得多。按照 Stefan Charsley 的建议直接修改 AST 将使我的工作变得轻松 :)
【解决方案3】:

继续你的selected answer 和你显然没有克服的问题:

替换一个AST节点,或者创建一个新节点后,可以同步新的虚拟文本和每个节点

    ts.setSourceMapRange(newnode, ts.getSourceMapRange(node));
    ts.setCommentRange(newnode, ts.getCommentRange(node));

这将从访问者内部调用。

但是,出现的任何错误将(可能)与原始文本不同步 - 与您不接受建议创建新的非虚拟真实中间文本文件的答案中的问题相同。

这是一种可能的解决方法。我在部分相关的问题中使用了部分相似的方法,并且我已经将其调整为我认为应该在您的问题中起作用的方法。

编写一个转换器tf-overload,执行您指定的指定词法转换。

  • 通过 1
    • 编译:原始源的 AST 并报告错误。 过滤掉有关“+”参数不兼容的错误。 (不要认为这是一个好方法,......但是)。显示其他错误。
    • 不要排放。
  • 通过 2(可选)
    • 使用 tf-overload 将原始 AST 转换为新 AST。
    • 编译:新的AST,如果有任何错误,报告,但位置可能是关闭的。
  • 通过 3
    • 使用before 插槽中的转换器tf-overload 调用emit
    • 当从emit 中调用它时,将为您执行源映射转换。因此,您的调试器将显示相对于原始源文件的正确位置。发射不会进行符号检查,但发射阶段的任何警告/错误都会在编辑器中显示正确的位置。

您不会获得希望将鼠标悬停在“c”上的 UI 特殊信息。为此,您需要与ttypescriptts-patch 或其他等效项(或您自己的)集成,并确保您的编辑器指向正确修改的打字稿库。此外,为了对其他人有用,他们也将拥有正确的设置。不小的壮举。

但是,这取决于您的计划的细节和实际应用。为每次添加调用 vector2 可能会导致大量开销。您可能希望在编译 typeChecker 后使用符号信息,以确保仅在数组/数字元组上调用 vector2。然后你不需要过滤错误,而是创建你自己的。这是更多的工作。您可以在运行时检查数组长度。

【讨论】:

    猜你喜欢
    • 2019-11-20
    • 2012-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-02
    • 1970-01-01
    • 1970-01-01
    • 2021-05-12
    相关资源
    最近更新 更多