bbatsov/rubocop

View on GitHub
lib/rubocop/cop/style/module_function.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Checks for use of `extend self` or `module_function` in a module.
      #
      # Supported styles are: `module_function` (default), `extend_self` and `forbidden`.
      #
      # A couple of things to keep in mind:
      #
      # - `forbidden` style prohibits the usage of both styles
      # - in default mode (`module_function`), the cop won't be activated when the module
      #   contains any private methods
      #
      # @safety
      #   Autocorrection is unsafe (and is disabled by default) because `extend self`
      #   and `module_function` do not behave exactly the same.
      #
      # @example EnforcedStyle: module_function (default)
      #   # bad
      #   module Test
      #     extend self
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     module_function
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     extend self
      #     # ...
      #     private
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     class << self
      #       # ...
      #     end
      #   end
      #
      # @example EnforcedStyle: extend_self
      #   # bad
      #   module Test
      #     module_function
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     extend self
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     class << self
      #       # ...
      #     end
      #   end
      #
      # @example EnforcedStyle: forbidden
      #   # bad
      #   module Test
      #     module_function
      #     # ...
      #   end
      #
      #   # bad
      #   module Test
      #     extend self
      #     # ...
      #   end
      #
      #   # bad
      #   module Test
      #     extend self
      #     # ...
      #     private
      #     # ...
      #   end
      #
      #   # good
      #   module Test
      #     class << self
      #       # ...
      #     end
      #   end
      class ModuleFunction < Base
        include ConfigurableEnforcedStyle
        extend AutoCorrector

        MODULE_FUNCTION_MSG = 'Use `module_function` instead of `extend self`.'
        EXTEND_SELF_MSG = 'Use `extend self` instead of `module_function`.'
        FORBIDDEN_MSG = 'Do not use `module_function` or `extend self`.'

        # @!method module_function_node?(node)
        def_node_matcher :module_function_node?, '(send nil? :module_function)'

        # @!method extend_self_node?(node)
        def_node_matcher :extend_self_node?, '(send nil? :extend self)'

        # @!method private_directive?(node)
        def_node_matcher :private_directive?, '(send nil? :private ...)'

        def on_module(node)
          return unless node.body&.begin_type?

          each_wrong_style(node.body.children) do |child_node|
            add_offense(child_node) do |corrector|
              next if style == :forbidden

              if extend_self_node?(child_node)
                corrector.replace(child_node, 'module_function')
              else
                corrector.replace(child_node, 'extend self')
              end
            end
          end
        end

        private

        def each_wrong_style(nodes, &block)
          case style
          when :module_function
            check_module_function(nodes, &block)
          when :extend_self
            check_extend_self(nodes, &block)
          when :forbidden
            check_forbidden(nodes, &block)
          end
        end

        def check_module_function(nodes)
          return if nodes.any? { |node| private_directive?(node) }

          nodes.each do |node|
            yield node if extend_self_node?(node)
          end
        end

        def check_extend_self(nodes)
          nodes.each do |node|
            yield node if module_function_node?(node)
          end
        end

        def check_forbidden(nodes)
          nodes.each do |node|
            yield node if extend_self_node?(node)
            yield node if module_function_node?(node)
          end
        end

        def message(_range)
          return FORBIDDEN_MSG if style == :forbidden

          style == :module_function ? MODULE_FUNCTION_MSG : EXTEND_SELF_MSG
        end
      end
    end
  end
end