1

I want to write a gem that can be used to parse Ruby code and report on whether or not it detects the presence of certain function calls. My particular use case is that I want to ensure that no commits are merged into the main codebase that contain sleep calls.

The brute force way of doing this would simply be to parse the plain text files and look for "sleep" on uncommented lines, but I can image that being error-prone, as well as somewhat ugly.

Is there a way to search for certain function calls in Ruby code, perhaps 'compiling' the code into some sort of tokenised form and parsing that?

1
  • I don't think that makes much sense. Someone can alias sleep to a different method and then call that method. You don't need to detect that?
    – sawa
    Commented Jun 4, 2013 at 13:11

1 Answer 1

2

I'm guessing this is just for debugging purposes, eg you are putting sleep statements in for testing, and don't want them in when you commit. If that is the case, the below code does what you want:

require 'ripper'
class MethodParser
    def initialize(source)
        @ast = Ripper.sexp(source)
    end
    def is_method_called?(method_name)
        search_ast_for_method(@ast, method_name)
    end
    private

    def is_top_level_method_call(ast, method_name)
        # firstly check if possible command block
        unless ast.is_a?(Array) && ast.length > 1 && ast[1].is_a?(Array)
            return false
        end
        # now check if it is a function call or command, and check the method name
        if [:command, :fcall].include? ast[0]
            ast[1].include?(method_name.to_s)
        else
            false
        end
    end

    def search_ast_for_method(ast, method_name)
        return true if is_top_level_method_call(ast, method_name)
        return false unless ast.is_a? Array
        ast.any? { |e| search_ast_for_method(e, method_name) }
    end
end

Example usage:

>> m = MethodParser.new <<EOF
class TestClass
  def method
    puts "hello"
    sleep(42)
  end
end
EOF
=> #<MethodParser:0x007f9df3a493c0 @ast=[:program, [[:class, [:const_ref, [:@const, "TestClass", [1, 6]]], nil, [:bodystmt, [[:def, [:@ident, "method", [2, 6]], [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:command, [:@ident, "puts", [3, 4]], [:args_add_block, [[:string_literal, [:string_content, [:@tstring_content, "hello", [3, 10]]]]], false]], [:method_add_arg, [:fcall, [:@ident, "sleep", [4, 4]]], [:arg_paren, [:args_add_block, [[:@int, "42", [4, 10]]], false]]]], nil, nil, nil]]], nil, nil, nil]]]]>
>> m.is_method_called? :sleep
=> true
>> m.is_method_called? :puts
=> true
>> m.is_method_called? :hello
=> false
>> m.is_method_called? "hello"
=> false

Note that any dynamic method invocation, or just method aliasing will bypass this, eg eval("sl" + "eep 4"), or send(:sleep, 4). If it is just sanity testing committed code though it should suffice.

Finally it doesn't detect the sleep in Kernel.sleep 4, although it wouldn't be hard to fix that if need that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.