From 8dd097a91530e4b047c4b391f21047c7d29d310d Mon Sep 17 00:00:00 2001
From: Sean McGivern <sean@gitlab.com>
Date: Tue, 28 Feb 2017 10:41:59 +0000
Subject: Add RuboCop cop for custom error classes

From the Ruby style guide:

    # bad
    class FooError < StandardError
    end

    # okish
    class FooError < StandardError; end

    # good
    FooError = Class.new(StandardError)

This cop does that, but only for error classes (classes where the
superclass ends in 'Error'). We have empty controllers and models, which
are perfectly valid empty classes.
---
 rubocop/cop/custom_error_class.rb | 64 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 64 insertions(+)
 create mode 100644 rubocop/cop/custom_error_class.rb

(limited to 'rubocop')

diff --git a/rubocop/cop/custom_error_class.rb b/rubocop/cop/custom_error_class.rb
new file mode 100644
index 00000000000..38d93acfe88
--- /dev/null
+++ b/rubocop/cop/custom_error_class.rb
@@ -0,0 +1,64 @@
+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
-- 
cgit v1.2.1