Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace quotemarks with TypeScript compiler API transformers

I have a TypeScript code generation scenario, where I construct an AST and then print it and save to a file. By default, the printed string literals are wrapped in double quotes, I would like an option to have single quotes. As mentioned here, I should be able to walk the tree and replace the string literals, I am unsure how, though.

export const quotemarkTransformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
    function visit(node: ts.Node): ts.Node {
        node = ts.visitEachChild(node, visit, context);
        if (node.kind === ts.SyntaxKind.StringLiteral) {
            const stringLiteral = node as ts.StringLiteral;
            // change raw node text?
            return ts.createLiteral(stringLiteral.text);
        }
        return node;
    }
    return ts.visitNode(rootNode, visit);
}

Creating string literals with TS factory functions ts.createLiteral(stringLiteral.text) will always use double quotes. Any way to access and change emitted text directly?

like image 968
DoubleZ Avatar asked Apr 16 '26 05:04

DoubleZ


1 Answers

There is an internal property you can set on the StringLiteral to do this:

if (node.kind === ts.SyntaxKind.StringLiteral)
    (node as any).singleQuote = true;

See here and here.

It's very important to note that this would be depending on a property that's not present in the public API so it might stop working one day. If you're uncomfortable doing this then follow the instructions below.


Given the emitted text:

  1. Parse or reuse an AST that matches the emitted text.
  2. Traverse the AST and for every string literal that has the searching for quote character, store the start position of both quote characters.
  3. With the emitted source file text and the quote character start positions, replace the text at every position with the new quote character.

Here's some code that shows an example:

// setup
const emittedFilePath = "/file.js";
const emittedText = `'this'; 'is a'; "test";`;
const emittedSourceFile = ts.createSourceFile(
    emittedFilePath,
    emittedText,
    ts.ScriptTarget.Latest,
    false);

// replace all ' with "
console.log(replaceQuoteChars(emittedSourceFile, `'`, `"`));

// main code...
type QuoteChar = "'" | "\"";

function replaceQuoteChars<OldChar extends QuoteChar>(
    sourceFile: ts.SourceFile,
    oldChar: OldChar,
    newChar: Exclude<QuoteChar, OldChar>
) {
    return getNewText(
        getQuoteCharPositions(emittedSourceFile, oldChar)
    );

    function getNewText(quoteCharPositions: number[]) {
        const fileText = sourceFile.getFullText();
        let result = "";
        let lastPos = 0;

        for (const pos of quoteCharPositions) {
            result += fileText.substring(lastPos, pos) + newChar;
            lastPos = pos + 1;
        }

        result += fileText.substring(lastPos);
        return result;
    }
}

function getQuoteCharPositions(
    sourceFile: ts.SourceFile,
    searchingChar: QuoteChar
) {
    const sourceFileText = sourceFile.getFullText();
    const result: number[] = [];
    visitNode(sourceFile);
    return result;

    function visitNode(node: ts.Node) {
        if (ts.isStringLiteral(node))
            handleStringLiteral(node);
        else
            ts.forEachChild(node, visitNode);
    }

    function handleStringLiteral(node: ts.StringLiteral) {
        const start = node.getStart(sourceFile);
        const quoteChar = sourceFileText[start];

        if (quoteChar === searchingChar) {
            result.push(start);
            result.push(node.end - 1);
        }
    }
}
like image 153
David Sherret Avatar answered Apr 23 '26 01:04

David Sherret