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:
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
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
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.