arbore sintactic abstract - Cum să combini două AST-uri Typescript


Încerc să combin două AST-uri TypeScript create din șiruri de caractere.

Eu creez ambele cu această metodă

const sourceFile = ts.createSourceFile(
    "file.ts",                  // filePath
    "function myFunction() {}", // fileText
    ts.ScriptTarget.Latest,     // scriptTarget
    true                        // setParentNodes -- sets the `parent` property
);

Totuși, acest lucru va crea un nod de tip ts.SourceFile, și dacă încerc să combin aceste două AST-uri primesc o eroare atunci când încerc să tipăresc sourceFile-ul final: "Unhandled SyntaxKind: SourceFile."

Cum ar trebui să îmbin corect două AST-uri?

Mulțumesc

EDIT: Iată un exemplu complet de cod funcțional. Când rulez asta, mă duce direct în debugger (în browser) cu eroarea menționată anterior.

Am încercat, de asemenea, să extrag în mod deliberat nodul ArrowFunction (în ast2 este înfășurat în SourceFile și ExpressionStatement), apoi programul rulează fără eroare, dar modifică complet șablonul literal intern într-un cod invalid.

const createAST = (str: string) =>
    ts.createSourceFile(
      "file.ts", // filePath
      str, // fileText
      ts.ScriptTarget.Latest, // scriptTarget
      true // setParentNodes -- sets the `parent` property
    )

  const template1 = `const arr = fields.map(() => {})`

  const template2 = `(field) => <Toolbar id={\`\${field.id}.\${field.name}\`} />`

  const ast1 = createAST(template1)
  const ast2 = createAST(template2)

  const replaceTransformer = <T extends ts.Node>(
    newNode: T
  ): ts.TransformerFactory<T> => {
    return context => {
      const visit: ts.Visitor = node => {
        if (ts.isArrowFunction(node)) return newNode

        return ts.visitEachChild(node, child => visit(child), context)
      }

      return node => ts.visitNode(node, visit)
    }
  }

  if (!ast1 || !ast2) return console.log("something went wrong")

  const result = ts.transform(ast1, [replaceTransformer(ast2)])

  const res = result.transformed[0]
  const printer = ts.createPrinter()
  const string = printer.printFile(res)
  console.log(string)

Răspuns #1

Prima problemă este că codul conține JSX într-un fișier .ts (aruncă o privire la rezultatul lui ast2). Pentru a rezolva acest lucru, schimbă extensia fișierului în .tsx astfel încât să fie analizat ca JSX.

A doua problemă este că plasarea unui fișier sursă într-un fișier sursă va duce la acea eroare. În schimb, este mai bine să înlocuiți o expresie cu o altă expresie, astfel încât în loc să furnizați fișierul sursă—ast2—să furnizați (ast2.statements[0] as ts.ExpressionStatement).expression.

A treia problemă este că TypeScript nu este conceput pentru a prelua nodurile dintr-un fișier sursă și a le folosi într-un alt fișier sursă. De exemplu, chiar și cu corecțiile de mai sus, veți obține în continuare următorul rezultat:

const arr = fields.map((field) => <Toolbar id={) =field.idfield.name}/>);

...motivul pentru aceasta este că imprimanta folosește pozițiile din ast2 pentru a indexa în textul fișierului sursă al ast1, așa că iese un rezultat inutil. Ce am recomandat să se facă în trecut este să setezi pos și end pentru toate nodurile din expresie din ast2 la -1 (implicitul folosit pentru nodurile create prin factory). Totuși, este un fel de soluție provizorie neobișnuită.

Exemplu complet

const createAST = (str: string) =>
  ts.createSourceFile(
    "file.tsx", // filePath
    str, // fileText
    ts.ScriptTarget.Latest, // scriptTarget
    true, // setParentNodes -- sets the `parent` property
  );

const template1 = `const arr = fields.map(() => {})`;
const template2 = "(field) => <Toolbar id={`${field.id}.${field.name}`} />";

const ast1 = createAST(template1);
const ast2 = createAST(template2);

const replaceTransformer = (
  newNode: ts.Node,
): ts.TransformerFactory<ts.SourceFile> => {
  return (context) => {
    const visit: ts.Visitor = (node) => {
      if (ts.isArrowFunction(node)) {
        stripRanges(newNode);
        return newNode;
      }

      return ts.visitEachChild(node, (child) => visit(child), context);
    };

    return (node) => ts.visitNode(node, visit);
  };
};

const newNode = (ast2.statements[0] as ts.ExpressionStatement).expression;
stripRanges(newNode);
const result = ts.transform(ast1, [
  replaceTransformer(newNode),
]);

const res = result.transformed[0];
const printer = ts.createPrinter();
console.log(printer.printFile(res));

function stripRanges(node: ts.Node) {
  (node as any).pos = -1;
  (node as any).end = -1;

  ts.forEachChild(node, stripRanges);
}

Alternativ, în loc să eliminăm intervalele, simpla ne-setare a nodurilor părinte pare să funcționeze de asemenea (furnizând false pentru acel argument), deoarece imprimanta nu pare să consulte textul fișierului sursă în acel caz… din nou, o altă soluție improvizată, având în vedere implementarea actuală a imprimantei.