Skip to content

Morph

WARNING: Experimental feature

Morph is an AST-based helper for idempotent code merging. It understands Ruby code and merges applied changes intelligently, grouping similar macros together, placing code in the most appropriate location, and enabling simple templating without the need to define complex matchers. It matches code by its structure rather than by string comparison.

It is not designed for complex code transformations, but rather for extending the existing structure. It works best when adding macros, method definitions, and extending DSLs.

Because morph is idempotent and places code intelligently, generators can be run multiple times with different parameters against the same file - even after the user has modified it. Each run adds only what is missing, leaving existing code untouched.

Basic Usage

before

ruby
class User
  has_one :address

  def full_name
    "#{first_name} #{last_name}"
  end
end

patch

ruby
morph "app/models/user.rb", <<~RUBY
  # frozen_string_literal: true

  class User
    has_many :posts

    private

    def foo = puts "bar"
  end
RUBY

after

ruby
# frozen_string_literal: true

class User
  has_one :address
  has_many :posts

  def full_name
    "#{first_name} #{last_name}"
  end

  private

  def foo = puts "bar"
end

The __add__ Marker

Mark container lines with # __add__ to indicate they were introduced by the generator. In reverse mode, morph removes the container itself once it becomes empty.

ruby
morph "app/models/user.rb", <<~RUBY
  class User < ApplicationRecord
    module Searchable # __add__
      def self.search(query)
        where("name LIKE ?", "%#{query}%")
      end
    end
  end
RUBY

Running --reverse removes the search method. Since Searchable was marked with __add__ and is now empty, it is also removed.

Without __add__, empty containers are left in place.

The __merge__ Marker

Tag a call node with # __merge__ to merge its arguments.

before

ruby
class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:email, :password)
  end
end

patch

ruby
morph "app/controllers/users_controller.rb", <<~RUBY
  class UsersController < ApplicationController
    def user_params
      params.require(:user).permit(:name, :avatar) # __merge__
    end
  end
RUBY

after

ruby
class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:email, :password, :name, :avatar) 
  end
end

Without __merge__, Morph would detect the user_params method as already defined and skip modification.