Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to bind an identifier with an existing symbol on a compiler transformer in typescript?

Tags:

typescript

I am trying to write a typescript compiler transform using the typescript compiler API. However, when creating new Identifier nodes, even though the nodes get emitted to the final .js file, they seem to lack symbol binding information so the final output is incorrect.

Suppose I have the following program:

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

Suppose I try to compile the above program with the following transformer:

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

The intermediate AST actually looks correct when pretty-printed:

import { A } from './A';

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

But the output javascript is incorrect:

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

I understand this may be because by creating a new identifier with ts.createIdentitier('A'), this new identifier isn't bound to the same symbol as the other A identifier in the same file.

Is there a way to bind a new identifier to an existing symbol using the public compiler API?

like image 740
Pedro Pedrosa Avatar asked Mar 06 '18 20:03

Pedro Pedrosa


People also ask

What is TypeScript AST?

The AST is a data structure to represent the structure of your source file in a format readable by machines. Indeed, if I throw the above example in the TypeScript AST Viewer I get immediate access to the AST.

What is TSC compiler?

The TypeScript Compiler − The TypeScript compiler (tsc) converts the instructions written in TypeScript to its JavaScript equivalent. The TypeScript Language Service − The "Language Service" exposes an additional layer around the core compiler pipeline that are editor-like applications.

Is TypeScript from Microsoft?

TypeScript is a free and open source programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript and adds optional static typing to the language. It is designed for the development of large applications and transpiles to JavaScript.


1 Answers

Typescript compilation happens in phases (parsing, binding, type checking, emitting, a bit more detail here). You can use information form a previous phase but you can't typically change it. The transformations you can do during the emit phase are intended to allow you to bring the AST from Typescript to Javascript, not to refactor the code.

One way to accomplish your goal, would be to create the program, apply the transformations, and then create a new program with the modified code, reusing as much of the original program as possible (reuse the same SourceFile where no changes occurred)

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

Another way would be to use the language service and apply these changes through the language service, but honestly I have no experience with that part of the compiler and it seemed more complicated than this approach.

like image 200
Titian Cernicova-Dragomir Avatar answered Sep 20 '22 23:09

Titian Cernicova-Dragomir