4

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.

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

3 Comments

This feels a lot less 'hacky' than my transformer approach (which turned out to not work after all), but I still get the same error. Could it be that the printer just doesn't support any form of "synthesized" source files?
That's unfortunate. Perhaps try stripping the ranges of all the nodes in the tree? I added some code to my answer that does that. If that doesn't work, would you be able add an example to the question that has some reproduction steps? I'll look into it more after that.
Unfortunately that didn't work either. I have added a self-contained example to my question, thank you so much for your willingness to help!

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.