igniteui-cli icon indicating copy to clipboard operation
igniteui-cli copied to clipboard

Implement an AST transformer

Open jackofdiamond5 opened this issue 1 year ago • 2 comments

Closes #30400.

This PR introduces a new AST transformer class which will be used as a base for the platform-specific TypeScript file updates.

The idea of the AST transformer is to be platform agnostic and to be only concerned with the modification of a ts.SourceFile. As such it expects a source file as an input and then exposes multiple methods that can modify this source file.

Currently it can:

  • add new members to object literals
  • modify existing members of object literals
  • create new object literal expressions
  • prepend/append members to array literals
  • create new array literal expressions
  • create new import declarations
  • add identifiers to existing import declarations
  • detect collisions between existing import declarations
  • locate variable declarations by given name and type
  • look up a node's ancestor and check it against a condition
  • look up a ts.PropertyAssignment in an object literal
  • look up an identifier/element in an array literal
  • create a call expression of the form x.call<T>(args)
    • where the type argument and the method arguments are optional
  • transform the AST into source code and apply formatting (if a formatting service is provided)

It uses a visitCondition predicate to locate the appropriate node that is to be modified and that responsibility is left to the developer. For example, if we look at the future AngularTypeSriptFileUpdate, and if we assume that it extends the TypeScriptASTTransformer, and we consider its function addRoute, it may look something like this:

const newObjectLiteral = this.createObjectLiteralExpression([
  { name: "path", value: ts.factory.createStringLiteral("some-new-path") },
  { name: "component", value: ts.factory.createIdentifier("MyComponent") },
]);

this.addMembersToArrayLiteral(
  // the visitCondition
  (node) =>
    ts.isArrayLiteralExpression(node) &&
    node.elements.some(
      (e) =>
        ts.isObjectLiteralExpression(e) &&
        e.properties.some(
          (p) => ts.isPropertyAssignment(p) && p.name.getText() === "path"
        )
    ),
  // the elements to be added to the array literal
  [newObjectLiteral]
);

This will add a new route entry { path: "some-new-path", component: MyComponent }. Also, keep in mind that this is a crude and simplified example as in reality we will have to do some more checks to make sure that the node that we're adding goes precisely where we want it.

Regarding any of the exposed utilities, the transformer will not attempt to fix potentially broken code as it is only concerned with the modification of the AST and not whether or not the resulting code is actually runnable. When it comes to creating import declarations, it expoes a method that will detect any existing declarations with the same identifier or alias and this should give the developer enough info. I am considering, however, to implement an additional utility that uses the ts.LanguageSeevice to detect any anomalies in the code and raise them during finalization to make sure that the developer is certain that the resulting code will not have any errors.

The transformer always keeps the ts.SourceFile up to date when adding new nodes to it. This is needed, since when creating new nodes with the ts.factory they will not be part of any AST and as such will not have pos and end members set (they will be equal to -1). This causes a bit of a problem when trying to retrieve the text of the source file or any node that contains a dynamically added one as its child. The ts.Printer can still get the source, yes, but the current approach is to use the printer to reload the source file after modifications, so that we always have an up-to-date AST and can easily get any of its existing or dynamically added nodes.

The PR also introduces a FormattingService utility which operates on a ts.SourceFile and reads formatting data from the .editorconfig (will include eslint in the future) and attempts to format the output code by invoking the ts.LanguageService and applying transformations to the file based on the loaded preferences. This utility is standalone and can be integrated anywhere that we want to apply formatting.

jackofdiamond5 avatar Apr 08 '24 14:04 jackofdiamond5

Coverage Status

coverage: 67.261% (+0.2%) from 67.062% when pulling 4df028f2f7817716b5706771d37bbc8818145411 on bpenkov/typescript-ast-transformer into 1f68a2580899ea309e753792356f02308c970dd8 on master.

coveralls avatar Apr 11 '24 06:04 coveralls

When you say the AngularTypeSriptFileUpdate "extends", by the current API exposed this is more geared toward composition, rather than inheritance btw. None of the AST-related API are especially friendly to expose externally.

damyanpetev avatar Apr 18 '24 15:04 damyanpetev