TypeScript compiler API: problem with ts.TransformerFactory replacing nodes

I'm creating a transformer that needs to replace each Call Expression placeholder with the code __REPLACED__, so I wrote this code:

compiler.ts

import * as ts from "typescript"

const filePath = "source.ts"

const programOptions = {
    rootNames: [filePath],
    options: {
        target: ts.ScriptTarget.ES2020,
        outDir: "outdir"
    }
}

const transformerFactory: ts.TransformerFactory<ts.SourceFile> = context => {
    return node => {
        const visitor: ts.Visitor = rootNode => {
            const node = ts.visitEachChild(rootNode, visitor, context)
            if (ts.isCallExpression(node)) {
                if (node.expression.getText() !== "placeholder") {
                    return node
                }
                const subsNode = ts.factory.createIdentifier("__REPLACED__")
                return subsNode
            }
            return node
        }
        return ts.visitNode(node, visitor)
    }
}

const program = ts.createIncrementalProgram(programOptions)
const entrySourceFile = program.getSourceFile(filePath)
const customTransformers: ts.CustomTransformers = { before: [transformerFactory] }
const emitResults = program.emit(entrySourceFile, undefined, undefined, undefined, customTransformers)

It works well if the source.ts file doesn't have any placeholder inside a anonymous function call, the code below everything works right:

source.ts

placeholder("param") 
void function() {
    placeholder("param")
}

Using thats source.ts described above, TypeScript Compiler API emits what I expected:

outdir\source.js

__REPLACED__;
void async function () {
    __REPLACED__;
};

But if I call this anonymous function right after creating it, I get an error if I use the following code as source.ts:

source.ts

placeholder("param") 
void function() {
    placeholder("param")
}() // <---- HERE'S THE PROBLEM

Here's the trace error:

(node:9204) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'text' of undefined
    at NodeObject.getText (project_dir\node_modules\typescript\lib\typescript.js:156127:31)
    at visitor (project_dir\typescriptCompiler.ts:22:41)
    at visitNode (project_dir\node_modules\typescript\lib\typescript.js:85158:23)
    at Object.visitEachChild (project_dir\node_modules\typescript\lib\typescript.js:85565:59)
    at visitor (project_dir\typescriptCompiler.ts:20:33)
    at visitNode (project_dir\node_modules\typescript\lib\typescript.js:85158:23)
    at Object.visitEachChild (project_dir\node_modules\typescript\lib\typescript.js:85622:64)
    at visitor (project_dir\typescriptCompiler.ts:20:33)
    at visitNodes (project_dir\node_modules\typescript\lib\typescript.js:85211:48)
    at visitLexicalEnvironment (project_dir\node_modules\typescript\lib\typescript.js:85251:22)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:9204) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:9204) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I suspect the problem is with recursion, i think somehow I'm removing the anonymous function node before accessing the placehorlders.

in advance thank you for your help.

Thanks.

Upvotes: 3

Views: 3279

Answers (1)

David Sherret
David Sherret

Reputation: 106580

Using the getText() method in a transformer is not reliable and I would recommend never using it or other methods that consult the source file text in a transformer.

The getText() method goes up the tree via the parents to get the root source file node. Once there, it then looks at the source file's text property then gets the string between the node's start and end positions.

In the case of any nodes created while transforming, they will not have a parent set and its pos and end will be -1 and -1:

> ts.factory.createIdentifier("__REPLACED__")
Identifier {
  pos: -1,
  end: -1,
  parent: undefined,
  // etc...
}

So the error occurs because parent is undefined.

It is true, that you could bypass the error you are seeing by providing the source file to getText by doing node.expression.getText(sourceFile); however, I still wouldn't recommend this because it won't work on transformed nodes due to their range being [-1, -1].

Solution: Stay in the AST

Instead, I would suggest to check if the expression is an identifier and look at its escapedText property. Roughly something along these lines (untested and you will need to adapt this for your code):

ts.isIdentifier(node.expression) && node.expression.escapedText === "placeholder"

Upvotes: 3

Related Questions