我想编写一个可以用来解析 Ruby 代码并报告它是否检测到某些函数调用的存在的 gem。我的特殊用例是我想确保没有提交合并到包含sleep
调用的主代码库中。
这样做的蛮力方法只是解析纯文本文件并在未注释的行上寻找“睡眠”,但我可以想象这容易出错,而且有点难看。
有没有办法在 Ruby 代码中搜索某些函数调用,也许将代码“编译”成某种标记化的形式并解析它?
我想编写一个可以用来解析 Ruby 代码并报告它是否检测到某些函数调用的存在的 gem。我的特殊用例是我想确保没有提交合并到包含sleep
调用的主代码库中。
这样做的蛮力方法只是解析纯文本文件并在未注释的行上寻找“睡眠”,但我可以想象这容易出错,而且有点难看。
有没有办法在 Ruby 代码中搜索某些函数调用,也许将代码“编译”成某种标记化的形式并解析它?
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.