abstrakt syntakstre - Hvordan slå sammen to Typescript AST-er


Jeg prøver å slå sammen to TypeScript AST-er laget fra strenger.

Jeg lager begge med denne metoden

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

Men dette vil opprette en node av typen ts.SourceFile, og hvis jeg prøver å slå sammen disse to AST-ene får jeg en feil når jeg prøver å skrive ut den endelige sourceFile: "Ubehandlet SyntaxKind: SourceFile."

Hvordan bør jeg riktig slå sammen to AST-er?

Takk

REDIGER: Her er et fullstendig fungerende kodeeksempel. Når jeg kjører dette, kaster det meg direkte inn i feilsøker (i nettleseren) med nevnte feil.

Jeg prøvde også bevisst å trekke ut ArrowFunction-noden (i ast2 er den pakket inn i SourceFile og ExpressionStatement), deretter kjører programmet uten feil, men endrer helt den indre template-literal til ugyldig kode.

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)

Svar #1

Det første problemet er at koden har JSX i en .ts-fil (ta en titt på resultatet av ast2). For å fikse dette, endre filnavnutvidelsen til .tsx slik at den vil bli tolket som JSX.

Det andre problemet er at det å sette en kildefil innenfor en kildefil vil føre til den feilen. I stedet er det best å erstatte et uttrykk med et annet uttrykk, så i stedet for å gi kildefilen—ast2—gi (ast2.statements[0] as ts.ExpressionStatement).expression.

Det tredje problemet er at TypeScript ikke er designet for å ta nodene fra én kildefil og bruke dem i en annen kildefil. For eksempel, selv med korrigeringene ovenfor, vil du fortsatt få følgende utdata:

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

...årsakene til dette er fordi skriveren bruker posisjonene fra ast2 for å indeksere inn i kildefilteksten til ast1, så det spyr ut søppel. Det jeg har anbefalt å gjøre tidligere er å sette pos og end for alle nodene i uttrykket fra ast2 til -1 (standardverdien som brukes for fabrikkskapte noder). Det er dog en litt hackete løsning.

Fullt eksempel

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);
}

Alternativt i stedet for å fjerne områdene, ser det ut til å fungere bare å ikke sette foreldrenodene (ved å gi false for det argumentet), siden skriveren ikke ser ut til å konsultere tekstfilen i det tilfellet… igjen, en annen midlertidig løsning gitt den nåværende implementeringen av skriveren.