3

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.

1 Answer 1

3

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"
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, I set the conditions like you said and it worked perfectly, but now I'm a little confused, in various parts of my code I use getText() like so ts.ImportDeclaration.importClause? .GetText() and` ts.ImportDeclaration.moduleSpecifier.getText() and also ts.CallExpression.arguments.map (token => token.getText()) `, as you mentioned, getText() is not recommended for use with transformed nodes, so in those cases what should I use instead?
You can search for the specific node you're looking for and then they should each have a property on them with the text you need. If you put the code in ts-ast-viewer.com then you can look at the tree and see which properties on the node have the information you need. For example, a module specifier is a string literal and string literals have a .text property. Doing this will also make your code work better if another transformer has run on the AST before yours.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.