Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a SourceFile from an array of Statement-nodes instead of a string

It's easy to create a SoureFile object from a plain string:

ts.createSourceFile(fileName, sourceText, languageVersion, setParentNodes, scriptKind)

However, I don't see a way to create one from an array of Statement-nodes (created by the various factory functions).

I tried to bodge a solution like this:

const source = ts.createSourceFile(fileName, '', languageVersion);
source.statements = myNodeArray;

But this (perhaps unsurprisingly) doesn't work. I also tried (ab)using the transformer API like this:

function createSourcefile(filename: string, ast: ts.Node[], languageVersion: ts.ScriptTarget): ts.SourceFile {
    const dummy = ts.createSourceFile(filename, 'dummy', languageVersion); // need at least 1 node

    return ts.transform(
        dummy,
        [ transformContext => sourceFile => ts.visitEachChild(sourceFile, node => ast, transformContext) ]
    ).transformed[0];
}

But this doesn't appear to work either.

With both methods I get get following error during the emit process:

Error: start < 0
  at createTextSpan (node_modules\typescript\lib\typescript.js:10263:19)
  at Object.createTextSpanFromBounds (node_modules\typescript\lib\typescript.js:10272:16)
  at getErrorSpanForNode (node_modules\typescript\lib\typescript.js:13544:19)
  at createDiagnosticForNodeInSourceFile (node_modules\typescript\lib\typescript.js:13449:20)
  at Object.createDiagnosticForNode (node_modules\typescript\lib\typescript.js:13440:16)
  at lookupOrIssueError (node_modules\typescript\lib\typescript.js:34976:22)
  at addDuplicateDeclarationError (node_modules\typescript\lib\typescript.js:35177:23)
  at \node_modules\typescript\lib\typescript.js:35173:17
  at Object.forEach (node_modules\typescript\lib\typescript.js:317:30)
  at addDuplicateDeclarationErrorsForSymbols (node_modules\typescript\lib\typescript.js:35171:16)
  at mergeSymbol (node_modules\typescript\lib\typescript.js:35158:21)
  at \node_modules\typescript\lib\typescript.js:35200:47
  at Map.forEach (<anonymous>)
  at mergeSymbolTable (node_modules\typescript\lib\typescript.js:35198:20)
  at initializeTypeChecker (node_modules\typescript\lib\typescript.js:66463:21)
  at Object.createTypeChecker (node_modules\typescript\lib\typescript.js:34935:9)
  at getDiagnosticsProducingTypeChecker (node_modules\typescript\lib\typescript.js:98560:93)
  at emitWorker (node_modules\typescript\lib\typescript.js:98588:32)
  at \node_modules\typescript\lib\typescript.js:98569:66
  at runWithCancellationToken (node_modules\typescript\lib\typescript.js:98665:24)
  at Object.emit (node_modules\typescript\lib\typescript.js:98569:20)
  *snip*

Is there a way to get this to work?

I guess I could in theory use a printer to convert the AST into a string, but this would obviously be a massive waste.


I've made a gist with a self-contained example using a 'virtual compiler host' and David Sherret's range stripping suggestion.

Strangely enough, I discovered that this error doesn't happen with all node types. In my (limited) testing I only encountered it when the AST contained an ImportDeclaration node.

like image 861
superbadcodemonkey Avatar asked Oct 26 '25 23:10

superbadcodemonkey


1 Answers

Thanks for the reproducible example! I've completely changed my answer so see the history for my past answer.

I looked into this and what's occurring is the following:

  1. The type checker goes to check the file.
  2. It encounters the diagnostic "Cannot find module 'bar'." (Note: This is after fixing ts.createImportDeclaration to provide a string literal and not an identifier for the module specifier)
  3. It goes to create a range for this diagnostic and the createTextSpan function throws because the synthetic nodes have a position less than zero. This is regardless of my previous stripRanges suggestion because ts.createImportDeclaration creates a node with negative positions.

So after doing this investigation I've been reminded that the type checker often makes the assumption that the nodes will refer to positions in the source file text. This is because the transformation phase of compiling happens after type checking.

If you want type checking on your created source file, I think you'll need to print it to a string and reparse to get a new source file containing descendant nodes with correct positions, then use that.

If you don't care about type checking then unfortunately at the moment I don't think there's a way to transform the code without type checking using the current API. It might be possible to hack something into custom transformers when emitting though... perhaps have an empty file, then add the statements in during custom transformation.

like image 159
David Sherret Avatar answered Oct 30 '25 12:10

David Sherret



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!