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.
This commit is contained in:
parent
6b4d490782
commit
8dd097a915
2 changed files with 175 additions and 0 deletions
64
rubocop/cop/custom_error_class.rb
Normal file
64
rubocop/cop/custom_error_class.rb
Normal file
|
@ -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
|
111
spec/rubocop/cop/custom_error_class_spec.rb
Normal file
111
spec/rubocop/cop/custom_error_class_spec.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
require 'spec_helper'
|
||||
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
|
||||
require_relative '../../../rubocop/cop/custom_error_class'
|
||||
|
||||
describe RuboCop::Cop::CustomErrorClass do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'when a class has a body' do
|
||||
it 'does nothing' do
|
||||
inspect_source(cop, 'class CustomError < StandardError; def foo; end; end')
|
||||
|
||||
expect(cop.offenses).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a class has no explicit superclass' do
|
||||
it 'does nothing' do
|
||||
inspect_source(cop, 'class CustomError; end')
|
||||
|
||||
expect(cop.offenses).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a class has a superclass that does not end in Error' do
|
||||
it 'does nothing' do
|
||||
inspect_source(cop, 'class CustomError < BasicObject; end')
|
||||
|
||||
expect(cop.offenses).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a class is empty and inherits from a class ending in Error' do
|
||||
context 'when the class is on a single line' do
|
||||
let(:source) do
|
||||
<<-SOURCE
|
||||
module Foo
|
||||
class CustomError < Bar::Baz::BaseError; end
|
||||
end
|
||||
SOURCE
|
||||
end
|
||||
|
||||
let(:expected) do
|
||||
<<-EXPECTED
|
||||
module Foo
|
||||
CustomError = Class.new(Bar::Baz::BaseError)
|
||||
end
|
||||
EXPECTED
|
||||
end
|
||||
|
||||
it 'registers an offense' do
|
||||
expected_highlights = source.split("\n")[1].strip
|
||||
|
||||
inspect_source(cop, source)
|
||||
|
||||
aggregate_failures do
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect(cop.offenses.map(&:line)).to eq([2])
|
||||
expect(cop.highlights).to contain_exactly(expected_highlights)
|
||||
end
|
||||
end
|
||||
|
||||
it 'autocorrects to the right version' do
|
||||
autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
|
||||
|
||||
expect(autocorrected).to eq(expected)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the class is on multiple lines' do
|
||||
let(:source) do
|
||||
<<-SOURCE
|
||||
module Foo
|
||||
class CustomError < Bar::Baz::BaseError
|
||||
end
|
||||
end
|
||||
SOURCE
|
||||
end
|
||||
|
||||
let(:expected) do
|
||||
<<-EXPECTED
|
||||
module Foo
|
||||
CustomError = Class.new(Bar::Baz::BaseError)
|
||||
end
|
||||
EXPECTED
|
||||
end
|
||||
|
||||
it 'registers an offense' do
|
||||
expected_highlights = source.split("\n")[1..2].join("\n").strip
|
||||
|
||||
inspect_source(cop, source)
|
||||
|
||||
aggregate_failures do
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect(cop.offenses.map(&:line)).to eq([2])
|
||||
expect(cop.highlights).to contain_exactly(expected_highlights)
|
||||
end
|
||||
end
|
||||
|
||||
it 'autocorrects to the right version' do
|
||||
autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
|
||||
|
||||
expect(autocorrected).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue