typescript - 如何在使用 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
。
计划
为了实现这一点,我必须:
typescript
创建一个程序,以同样的方式公开相同的 APIttypescript
。- 使该程序在将源代码传递给之前对其进行修改
typescript
- 让 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)
解决方案
你接近了你的代码。您似乎遇到的第一个问题是发生的源代码文件检查,基本上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);
}
推荐阅读
- c++ - 如何使用 CMake 将文件放在目标文件位置旁边?
- php - Jquery无限循环没有关注正确的领域
- java - Java swing 应用程序在线程“AWT-EventQueue-0”中抛出 NullPointerException,原因不明
- javascript - 函数内的 Javascript 暂停
- android - 您通常如何为仪器单元测试执行模拟?
- python - DynamoDB:使用 Python 导入大数据
- c# - SQL 查询的结果永远不会返回 null;
- typescript - TypeScript 导入语句在 Expo 项目中找不到 Firebase 模块
- flutter - Flutter AudioService 每次获取 Fetch Url
- python - Flask 知道主应用程序中的类,但不知道 html 模板中的类