Skip to content

Latest commit

 

History

History
279 lines (220 loc) · 8.07 KB

manipulation.md

File metadata and controls

279 lines (220 loc) · 8.07 KB

Manipulation

See section in file //doc/tan-liu-article.md describing my experience reproducing Tan Li Hau lessons in youtube video "Manipulating AST with JavaScript".

Many of the examples here are explained in the video Q&A Is there other ways to manipulate ast nodes besides replaceWith? by Tan Li Hau (陈立豪) (2021). Do not miss it.

Replacing a node

➜ babel-learning git:(main) ✗ cat src/manipulation/replacewith-plugin.cjs

module.exports = function (babel) {
  const { types: t } = babel;

  return {
    name: "ast-transform", // not required
    visitor: {
      BinaryExpression(path) {
        const { node } = path;
        if (node.operator == "*" && node.left.name == node.right.name) {
          path.replaceWith(t.binaryExpression("**", path.node.left, t.numericLiteral(2)));
          path.stop;
        }
      }
    }
  };
}

Here is the execution:

babel-learning git:(main) ✗ cat src/manipulation/square.js

let square = n => n * n

➜ babel-learning git:(main) ✗ npx babel src/manipulation/square.js --plugins=./src/manipulation/replacewith-plugin.cjs

"use strict";

let square = n => n ** 2;

See section /src/manipulating-ast-with-js/README.md for a complete example.

Returned value

When you call path.replaceWith, you replace the node at the current path with a new node. This method returns an array of NodePath objects representing the paths of the newly inserted nodes.

Replacing a node with multiple nodes

The following plugin replaces the return statement with two statements: a let statement and a return statement.

➜ babel-learning git:(main) cat src/manipulation/replacewithmultiple-plugin.cjs

module.exports = function (babel) {
  return {
    name: "ast-transform2", // not required
    visitor: {
      ReturnStatement(path) {
        if (path.node.argument.type == "BinaryExpression" && path.node.argument.left.name  == "n") {
          path.replaceWithMultiple([
            babel.template(`let a = N`)({N: path.node.argument.left }),
            babel.template(`return 4*a`)()
          ])
        }
      }
    }
  };
}

When executed with input:

  babel-learning git:(main) cat src/manipulation/square2.js
let square = n => { return n * n; }

We get:

➜ babel-learning git:(main) npx babel src/manipulation/square2.js --plugins=./src/manipulation/replacewithmultiple-plugin.cjs

"use strict";

let square = n => {
  let a = n;
  return 4 * a;
};

Note: When replacing an expression with multiple nodes, they must be statements. This is because Babel uses heuristics extensively when replacing nodes which means that you can do some pretty crazy transformations that would be extremely verbose otherwise.

See section replacewithmultipleat file /src/manipulating-ast-with-js/README.md

Replacing a node with a source string

FunctionDeclaration(path) {
  path.replaceWithSourceString(`function add(a, b) {
    return a + b;
  }`);
}
- function square(n) {
-   return n * n;
+ function add(a, b) {
+   return a + b;
  }

Note: It's not recommended to use this API unless you're dealing with dynamic source strings, otherwise it's more efficient to parse the code outside of the visitor.

Inserting a sibling node

FunctionDeclaration(path) {
  path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
  path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
+ "Because I'm easy come, easy go.";
  function square(n) {
    return n * n;
  }
+ "A little high, little low.";

Note: This should always be a statement or an array of statements. This uses the same heuristics mentioned in Replacing a node with multiple nodes.

Inserting into a container

If you want to insert into an AST node that is an array like body. Similar to insertBefore/insertAfter, except that you have to specify the listKey, which is usually body.

ClassMethod(path) {
  path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));
  path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}
 class A {
  constructor() {
+   "before"
    var a = 'middle';
+   "after"
  }
 }

Removing a node

FunctionDeclaration(path) {
  path.remove();
}
- function square(n) {
-   return n * n;
- }

Replacing a parent

Just call replaceWith with the parentPath: path.parentPath

BinaryExpression(path) {
  path.parentPath.replaceWith(
    t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
  );
}
  function square(n) {
-   return n * n;
+   "Anyway the wind blows, doesn't really matter to me, to me.";
  }

Removing a parent

BinaryExpression(path) {
  path.parentPath.remove();
}
  function square(n) {
-   return n * n;
  }

Manipulating AST with JavaScript series by Tan Liu Hau

What are the differences between function declaration and function expressions?

t.toExpression

In the Babel AST (Abstract Syntax Tree), expressions are nodes that represent values and can appear on the right-hand side of an assignment, as arguments to functions, and in various other places.

The toExpression method in the @babel/types package is used to convert a given AST node into an expression if it is not already one. This can be particularly useful when working with Babel transformations where you need to ensure that a node conforms to the syntax rules that expect expressions. There are entities such as functions and classes that can appear in both statements and expressions.

Here is an example of how to use the toExpression method in a Babel plugin:

➜ manipulation git:(main) ✗ cat fun-declaration-to-expression-plugin.cjs

module.exports = function(babel) {
  const { types: t } = babel;

  return {
    name: "learning-toExpression", // nombre del plugin
    visitor: {
      FunctionDeclaration(path) {
        let node = path.node;
        let name = node.id.name;
        node.id = null;
        let r = path.replaceWith(
          t.assignmentExpression(
            "=",
            t.identifier(name),
            t.toExpression(node)
          ))
          idPath = null;
        //console.log(r[0].node.left.name); 
      } 
    }
  }
};

Given the input program:

manipulation git:(main)  cat a-function-declaration.js 
function foo() {}

When we use the former plugin we get:

➜ manipulation git:(main) ✗ npx babel a-function-declaration.js --plugins=./fun-declaration-to-expression-plugin.cjs

foo = function () {};

See