วิธีการผูกตัวระบุเข้ากับสัญลักษณ์ที่มีอยู่บนตัวแปลงคอมไพเลอร์ใน 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();
อีกวิธีหนึ่งคือการใช้บริการภาษาและใช้การเปลี่ยนแปลงเหล่านี้ผ่านบริการภาษา แต่พูดตามตรง ฉันไม่มีประสบการณ์กับส่วนของคอมไพเลอร์นั้น และมันดูซับซ้อนกว่าวิธีนี้
