如何在打字稿中的编译器转换器上将标识符与现有符号绑定?

Ped*_*osa 4 typescript

我正在尝试使用打字稿编译器 API 编写打字稿编译器转换。然而,在创建新的 Identifier 节点时,即使节点被发送到最终的 .js 文件,它们似乎缺少符号绑定信息,因此最终输出是不正确的。

假设我有以下程序:

A.ts

export class A {
    static myMethod() {
        return 'value';
    }
}
Run Code Online (Sandbox Code Playgroud)

索引.ts

import { A } from './A';

export function main() {
    const value1 = 'replaceMe';
    const value2 = A.myMethod();
    const equals = value1 == value2;
}
Run Code Online (Sandbox Code Playgroud)

假设我尝试使用以下转换器编译上述程序:

function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
    return (context: ts.TransformationContext) => (file: ts.SourceFile) => transformFile(program, context, file);
}

function transformFile(program: ts.Program, context: ts.TransformationContext, file: ts.SourceFile): ts.SourceFile {
    const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
    console.log(ts.createPrinter().printFile(transformedFile));
    return transformedFile;
}
function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
    if (ts.isStringLiteral(node) && node.text == 'replaceMe') {
        return ts.createCall(
            ts.createPropertyAccess(
                ts.createIdentifier('A'),
                'myMethod'),
            [],
            []);
    }
    return ts.visitEachChild(node, child => visit(child, context), context);
}
Run Code Online (Sandbox Code Playgroud)

中间 AST 在漂亮打印时实际上看起来是正确的:

import { A } from './A';

export function main() {
    const value1 = A.myMethod();
    const value2 = A.myMethod();
    const equals = value1 == value2;
}
Run Code Online (Sandbox Code Playgroud)

但输出 javascript 不正确:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var A_1 = require("./A");
function main() {
    var value1 = A.myMethod();
    var value2 = A_1.A.myMethod();
    var equals = value1 == value2;
}
exports.main = main;
Run Code Online (Sandbox Code Playgroud)

我了解这可能是因为通过使用 来创建新标识符ts.createIdentitier('A'),此新标识符不会绑定到与A同一文件中的其他标识符相同的符号。

有没有办法使用公共编译器 API 将新标识符绑定到现有符号?

Tit*_*mir 5

Typescript 编译分阶段进行(解析、绑定、类型检查、发出,这里有更多细节)。您可以使用来自前一阶段的信息,但通常无法更改它。您可以在发出阶段进行的转换旨在允许您将 AST 从 Typescript 带到 Javascript,而不是重构代码。

实现目标的一种方法是创建程序,应用转换,然后使用修改后的代码创建一个新程序,尽可能多地重用原始程序(SourceFile在未发生更改的地方重用相同的程序)

function transformFile(program: ts.Program, file: ts.SourceFile): ts.SourceFile {
    let empty = ()=> {};
    // Dummy transformation context
    let context: ts.TransformationContext = {
        startLexicalEnvironment: empty,
        suspendLexicalEnvironment: empty,
        resumeLexicalEnvironment: empty,
        endLexicalEnvironment: ()=> [],
        getCompilerOptions: ()=> program.getCompilerOptions(),
        hoistFunctionDeclaration: empty,
        hoistVariableDeclaration: empty,
        readEmitHelpers: ()=>undefined,
        requestEmitHelper: empty,
        enableEmitNotification: empty,
        enableSubstitution: empty,
        isEmitNotificationEnabled: ()=> false,
        isSubstitutionEnabled: ()=> false,
        onEmitNode: empty,
        onSubstituteNode: (hint, node)=>node,
    };
    const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
    return transformedFile;
}

function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
    if (ts.isStringLiteral(node) && node.text == 'replaceMe') {
        return ts.createCall(
            ts.createPropertyAccess(
                ts.createIdentifier('A'),
                'myMethod'),
            [],
            []);
    }
    return ts.visitEachChild(node, child => visit(child, context), context);
}

let host = ts.createCompilerHost({});
let program = ts.createProgram(["toTrans.ts"], {}, host)

let transformed = program.getSourceFiles()
    .map(f=> ({ original: f, transformed: transformFile(program, f) }))
    .reduce<{ [name: string] : {original: ts.SourceFile, transformed: ts.SourceFile }}>((r, f)=> { r[f.original.fileName] = f; return r; }, {});

let originalGetSourceFile = host.getSourceFile;
let printer = ts.createPrinter();

// Rig the host to return the new verisons of transformed files.
host.getSourceFile = function(fileName, languageVersion, onError, shouldCreateNewSourceFile){
    let file = transformed[fileName];
    if(file){
        if(file.original != file.transformed){
            // Since we need to return a SourceFile it is tempting to return the transformed source file and not parse it again
            // The compiler doe not support Synthesized nodes in the AST except during emit, and it will check node positions 
            // (which for Synthesized are -1) and fail. So we need to reparse
            return ts.createSourceFile(fileName, printer.printFile(file.transformed), languageVersion);
        } else {
            // For unchanged files it should be safe to reuse the source file
            return file.original;
        }
    }
    return  originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
}

// Recreate the program, we pass in the original to 
program = ts.createProgram(["toTrans.ts"], {}, host, program);

var result = program.emit();
Run Code Online (Sandbox Code Playgroud)

另一种方法是使用语言服务并通过语言服务应用这些更改,但老实说,我对编译器的那部分没有经验,而且它似乎比这种方法更复杂。