I previously wrote a post about search in ruby ast and I was showing the RuboCop node pattern in action.

Well, I take it as a personal challenge to write a small compiler and isolate the node pattern language to learn about compilers and scratch my own itch.

RuboCop seems too much to what I’d like to do and I start discovering how to build these functions along the way.

I started working on fast: https://github.com/jonatas/fast/.

Instead of keep piling tutorials and blog posts on my own domain, I decided to create one dedicated documentation to the library, and here we are:

https://jonatas.github.io/fast/

I created a few tutorials showing how to identify code similarity and create dynamic experiments that can replace code and keep running the test suite.

If the test passes, it tries a new refactor and keep refactoring.

I’m thrilled with all the work I dedicated here and during this week I implemented a new feature that I’d like to show here.

Basically, the current function is able to use custom ruby methods to leverage complexity in AST search.

With the new syntax, it’s possible to use embedded methods in the middle of the expressions that also matches nodes or inner elements.

Let’s take a look in some code example:

class Example
  def first_method
  end
  def second_method
  end
  def first_method # repeated
  end
end

How can we build a search to find the repeated method?

With a simple search of def we have the following results:

fast 'def ' example.rb                                                                                                                                                                                                                                                                                                                                                                                                                                    21:30:40
# example.rb:2
def first_method
  end
# example.rb:4
def second_method
  end
# example.rb:6
def first_method # repeated
  end

If I know the method name I can simply say it:

fast '(def first_method)' example.rb                                                                                                                                                                                                                                                                                                                                                                                                                       21:30:56
# example.rb:2
def first_method
  end
# example.rb:6
def first_method # repeated
  end

But how can I collect the method names to discover if they’re repeated?

Using Fast.capture It can be easily found. Let’s see how I could do it in the old way?

Fast.capture(Fast.ast_from_file("example.rb"), "(def $_)")
# => [:first_method, :second_method, :first_method]

Great! But I’d like to pick only the third element that is the first duplicated.

How can I make it happen? How to ignore the second_method and the first first_method?

We need to build a small method that can record the method name and collect unique methods. When it founds some method that is already registered. It can target this as a “match”. Let’s implement the method and use it inside our expression:

def duplicated(method_name)
  @methods ||= []
  already_exists = @methods.include?(method_name)
  @methods << method_name
  already_exists
end

The method simply receives a symbol and check if it was previously included in the array. Now, we can use the method:

duplicated :a
# => false
duplicated :a
# => true
duplicated :a
# => true
duplicated :b
# => false
duplicated :b
# => true
puts Fast.search_file( '(def #duplicated)', 'example.rb')
# (def :first_method
#  (args) nil)

Keep in mind that if you rerun the same search, it will not work because we need to reset the @methods variable.

The MethodCall will simply take the argument, no matter if it’s a node or some inner element. It will depend on where the function is placed in the DSL.

If we want to match with the node, it needs to be written and validating the def internally. Example:

def duplicated_def node
  return false unless node.type == :def
  method_name, = node.children
  @methods ||= []
  already_exists = @methods.include?(method_name)
  @methods << method_name
  already_exists
end

We created a guard clause to avoid match other node types. As the method call will receive the node inline with the expression, we can even remove the parens from the expression:

puts Fast.search_file( '#duplicated_def', 'example.rb')
(def :first_method
  (args) nil)

That’s all I have for today! I’m a bit bored with my tool, and I’m also working to extract the node pattern from RuboCop to a separated library.


Share → Twitter Facebook Linkedin


Hello there, my name is Jônatas Davi Paganini and this is my personal blog.
I'm developer advocate at Timescale and I also have a few open source projects on github.

Check my talks or connect with me via linkedin / twitter / github / instagram / facebook / strava / meetup.