Search code examples
rubyrubocopruby-parser

create a rubocop cop that triggers when method is invoked without a block


I'm trying to develop a new cop (based on these guidelines) and am banging my head trying to get the correct node pattern.

I want the cop to register an offense when X.some_method is called without providing a block.
i.e X.some_method is an offense, but X.some_method { blah } is not.

I got the correct pattern for identifying X.some_method, which is '(send (const nil? :X) :some_method ...'.
But not sure how to create a pattern for "is not given a block"?


Solution

  • Apparently, in the parsed AST, when a node is given a block, that node is then represented as the first child of that block.
    i.e

    [92] pry(RuboCop)> node # `X.some_method(something) { do_something }`
    => s(:block,
      s(:send,
        s(:const, nil, :X), :some_method,
        s(:send, nil, :something)),
      s(:args),
      s(:send, nil, :do_something))
    
    

    And we can check that using the Rubocop::AST instances.
    Here is the full implementation (includes an option for multiple method names):

      MSG = 'Avoid using `X.%<method>s` without providing a block.'
    
      def_node_matcher :x_method, '(send (const nil? :X) ${:some_method :another_method} ...)'
    
      def on_send(node)
          x_method(node) do |method_name|
            return if !method_name || first_child_of_block?(node)
            add_offense(node, location: :selector, message: format(MSG, method: method_name))
          end
    
        end
    
      private
    
        # checks if the given node's parent is a block, and the given node is its first child,
        # which would mean that the block is supplied to the given node (i.e `node { block }`)
        def first_child_of_block?(node)
          return false unless (parent = node.parent)
          return false unless parent.type == :block
          parent.children.first == node
        end