module Gitlab
  module Styles
    module Rubocop
      module Cop
        # This cop makes sure that custom error classes, when empty, are declared
        # with Class.new.
        #
        # @example
        #   # bad
        #   class FooError < StandardError
        #   end
        #
        #   # okish
        #   class FooError < StandardError; end
        #
        #   # good
        #   FooError = Class.new(StandardError)
        class CustomErrorClass < RuboCop::Cop::Cop
          MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze

          def on_class(node)
            _klass, parent, body = node.children

            return if body

            parent_klass = class_name_from_node(parent)

            return unless parent_klass && parent_klass.to_s.end_with?('Error')

            add_offense(node, :expression)
          end

          def autocorrect(node)
            klass, parent, _body = node.children
            replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})"

            lambda do |corrector|
              corrector.replace(node.source_range, replacement)
            end
          end

          private

          # The nested constant `Foo::Bar::Baz` looks like:
          #
          #   s(:const,
          #     s(:const,
          #       s(:const, nil, :Foo), :Bar), :Baz)
          #
          # So recurse through that to get the name as written in the source.
          #
          def class_name_from_node(node, suffix = nil)
            return unless node&.type == :const

            name = node.children[1].to_s
            name = "#{name}::#{suffix}" if suffix

            if node.children[0]
              class_name_from_node(node.children[0], name)
            else
              name
            end
          end
        end
      end
    end
  end
end
