dsherret / ts-morph

TypeScript Compiler API wrapper for static analysis and programmatic code changes.
https://ts-morph.com
MIT License
4.87k stars 194 forks source link

transform doesn't handle recursively transformed nodes (fix suggested) #1471

Open sparecycles opened 9 months ago

sparecycles commented 9 months ago

So, I had this hair-brained idea to use ts-morph to wrap await around sub-expressions.

E.g. fetch('http://example.com').json().data.x -> (await (await fetch('http://example.com')).json()).data.x

Describe the bug

Version: 20.0.0

I get the following error attempting this

  filePath: '/__temp___.ts',
  oldText: "export default fetch('http://example.com').json().data.x",
  newText: "export default (await fetch('http://example.com'))fetch('http://example.com')(await (await fetch('http://example.com')).json()).data.x"

To Reproduce

import { Project, ScriptTarget, ModuleKind, ts } from "ts-morph";

const project = new Project({
  compilerOptions: {
    allowJs: true,
    target: ScriptTarget.ES2022,
    module: ModuleKind.ES2022,
  },
  useInMemoryFileSystem: true,
});

const expr = `fetch('http://example.com').json().data.x`;

console.log(
  project
    .createSourceFile("__temp___.ts", `export default ${expr}`)
    .getDefaultExportSymbol()
    .getValueDeclaration()
    .getChildAtIndex(2) // (BTW is there a cleaner way to make a file -> get an expression? I'm all ears.)
    .transform(({ currentNode, factory, visitChildren }) => {
      if (ts.isCallExpression(currentNode)) {
        return factory.createParenthesizedExpression(factory.createAwaitExpression(visitChildren()));
      }
      return visitChildren();
    })
    .getFullText(),
);

Expected behavior

% node example.js
 (await (await fetch('http://example.com')).json()).data.x

... as was produced after applying the following fix ... 😉

diff --git a/node_modules/ts-morph/dist/ts-morph.js b/node_modules/ts-morph/dist/ts-morph.js
index df176bd..918503f 100644
--- a/node_modules/ts-morph/dist/ts-morph.js
+++ b/node_modules/ts-morph/dist/ts-morph.js
@@ -3746,7 +3746,7 @@ class Node {
             const start = oldNode.getStart(compilerSourceFile, true);
             const end = oldNode.end;
             let lastTransformation;
-            while ((lastTransformation = transformations[transformations.length - 1]) && lastTransformation.start > start)
+            while ((lastTransformation = transformations[transformations.length - 1]) && lastTransformation.start >= start)
                 transformations.pop();
             const wrappedNode = compilerFactory.getExistingNodeFromCompilerNode(oldNode);
             transformations.push({

See also Issue #852 / PR #853

sparecycles commented 4 months ago

Thanks for looking at this, Nell!

My current use of this, if it helps to drive testing, is

fetch('http://example.com').await.json().await.data.x;
// --> (await (await fetch('http://example.com')).json()).data.x

via .transform(transformDotAwait)

export function transformDotAwait({
  factory,
  visitChildren,
}: TransformTraversalControl): ts.Node {
  const result = visitChildren();

  if (
    ts.isPropertyAccessExpression(result) &&
    result.name.getText() === "await"
  ) {
    return factory.createParenthesizedExpression(
      factory.createAwaitExpression(result.expression),
    );
  }

  return result;
}