ค้นหาเว็บไซต์

วิธีการผูกตัวระบุเข้ากับสัญลักษณ์ที่มีอยู่บนตัวแปลงคอมไพเลอร์ใน TypeScript?


ฉันกำลังพยายามเขียนตัวแปลงของตัวคอมไพเลอร์ TypeScript โดยใช้ API ของตัวคอมไพเลอร์ TypeScript อย่างไรก็ตาม เมื่อสร้างโหนด Identifier ใหม่ ถึงแม้ว่าโหนดเหล่านี้จะถูกสร้างออกมาในไฟล์ .js สุดท้าย แต่ดูเหมือนว่าพวกมันจะขาดข้อมูลการผูกสัญลักษณ์ ทำให้ผลลัพธ์สุดท้ายไม่ถูกต้อง

สมมติว่าฉันมีโปรแกรมดังต่อไปนี้:

A.ts

export class A {
    static myMethod() {
        return 'value';
    }
}

Index.ts

import { A } from './A';

export function main() {
    const value1 = 'replaceMe';
    const value2 = A.myMethod();
    const equals = value1 == value2;
}

สมมติว่าฉันพยายามคอมไพล์โปรแกรมด้านบนด้วยทรานส์ฟอร์มเมอร์ดังต่อไปนี้:

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);
}

AST ขั้นกลางจริงๆ ดูถูกต้องเมื่อพิมพ์ให้อ่านง่าย:

import { A } from './A';

export function main() {
    const value1 = A.myMethod();
    const value2 = A.myMethod();
    const equals = value1 == value2;
}

แต่โค้ดจาวาสคริปต์ที่ได้ออกมาไม่ถูกต้อง:

"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;

ฉันเข้าใจว่านี่อาจเป็นเพราะว่าเมื่อสร้างตัวระบุใหม่ด้วย ts.createIdentitier('A') ตัวระบุใหม่นี้จะไม่ได้เชื่อมโยงกับสัญลักษณ์เดียวกันกับตัวระบุ A ตัวอื่นในไฟล์เดียวกัน

มีวิธีใดที่จะเชื่อมโยงตัวระบุใหม่กับสัญลักษณ์ที่มีอยู่โดยใช้ API ของคอมไพลเลอร์สาธารณะหรือไม่?

คำตอบ #1

การคอมไพล์ 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();

อีกวิธีหนึ่งคือการใช้บริการภาษาและใช้การเปลี่ยนแปลงเหล่านี้ผ่านบริการภาษา แต่พูดตามตรง ฉันไม่มีประสบการณ์กับส่วนของคอมไพเลอร์นั้น และมันดูซับซ้อนกว่าวิธีนี้

บทความที่เกี่ยวข้อง