Group multiple actions

Synvert provides the ability to write code snippets that can automatically rewrite your source code. This video demonstrates how to use group to apply multiple actions at once.

In the episode upgrade rails 4.2 to 5.0 part 3, we convert the usage of after_commit with on: :create to after_create_commit using the following Synvert snippet:

Synvert::Rewriter.new 'rails', 'use_after_commit_alias' do
  if_gem 'activerecord', '>= 5.0'
  within_files 'app/models/**/*.rb' do
    with_node node_type: 'send', receiver: nil, message: 'after_commit',
              arguments: { size: 2, '1': { node_type: 'hash', on_value: { in: %i[create update destroy] } } } do
      delete 'arguments.1.on_pair', and_comma: true
      replace :message, with: 'after_{{arguments.1.on_value}}_commit'
    end
  end
end

When you search in the VSCode Synvert extension, you will see two actions.

The first action is to replace after_commit with after_create_commit, and the second action is to delete on: :create. Performing these actions one after the other can be cumbersome and may lead to mistakes.

To simplify this process, we introduced a new group DSL. This group groups multiple actions into a single group, allowing you to execute them in one click. Here’s how the modified snippet looks:

Synvert::Rewriter.new 'rails', 'use_after_commit_alias' do
  if_gem 'activerecord', '>= 5.0'
  within_files 'app/models/**/*.rb' do
    with_node node_type: 'send', receiver: nil, message: 'after_commit',
              arguments: { size: 2, '1': { node_type: 'hash', on_value: { in: %i[create update destroy] } } } do
      group do
        delete 'arguments.1.on_pair', and_comma: true
        replace :message, with: 'after_{{arguments.1.on_value}}_commit'
      end
    end
  end
end

With this modification, you can now apply both the replace and delete actions with a single click, making the process more efficient.

The grouping functionality also applies to JavaScript snippets. For instance, consider the following snippet that converts the usage of <div className="container"></div> to <Container></Container>:

new Synvert.Rewriter("group", "name", () => {
  configure({ parser: Synvert.Parser.TYPESCRIPT });
  withinFiles("**/*.jsx", () => {
    withNode({ nodeType: "JsxElement", openingElement: { nodeType: "JsxOpeningElement", tagName: "div", attributes: { nodeType: "JsxAttributes", properties: { 0: { nodeType: "JsxAttribute", name: "className", initializer: { nodeType: "StringLiteral", text: "container" } }, length: 1 } } }, closingElement: { nodeType: "JsxClosingElement", tagName: "div" } }, () => {
      replace("closingElement", { with: "</Container>" });
      replace("openingElement", { with: "<Container>" });
    });
  });
});

The search results include two actions: one for replacing closing element </div> with </Container> and the other for replacing opening element <div className="container"> with <Container>.

To simplify and group these two actions, you can use the group function:

new Synvert.Rewriter("group", "name", () => {
  configure({ parser: Synvert.Parser.TYPESCRIPT });
  withinFiles("**/*.jsx", () => {
    withNode({ nodeType: "JsxElement", openingElement: { nodeType: "JsxOpeningElement", tagName: "div", attributes: { nodeType: "JsxAttributes", properties: { 0: { nodeType: "JsxAttribute", name: "className", initializer: { nodeType: "StringLiteral", text: "container" } }, length: 1 } } }, closingElement: { nodeType: "JsxClosingElement", tagName: "div" } }, () => {
      group(() => {
        replace("closingElement", { with: "</Container>" });
        replace("openingElement", { with: "<Container>" });
      });
    });
  });
});

With this modification, you can now apply both the replace actions with a single click.

0 Comments
Synvert's Substack
Synvert's Substack
Authors
Richard Huang