rubocop-highlands 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE.md +9 -0
  5. data/README.md +68 -0
  6. data/config/default.yml +39 -0
  7. data/config/rubocop-bundler.yml +8 -0
  8. data/config/rubocop-gemspec.yml +9 -0
  9. data/config/rubocop-highlands.yml +100 -0
  10. data/config/rubocop-layout.yml +560 -0
  11. data/config/rubocop-lint.yml +301 -0
  12. data/config/rubocop-metrics.yml +47 -0
  13. data/config/rubocop-naming.yml +85 -0
  14. data/config/rubocop-performance.yml +141 -0
  15. data/config/rubocop-rails.yml +208 -0
  16. data/config/rubocop-rspec.yml +331 -0
  17. data/config/rubocop-security.yml +17 -0
  18. data/config/rubocop-style.yml +983 -0
  19. data/lib/rubocop-highlands.rb +11 -0
  20. data/lib/rubocop/cop/highlands/class_name.rb +47 -0
  21. data/lib/rubocop/cop/highlands/class_or_module_declared_in_wrong_file.rb +138 -0
  22. data/lib/rubocop/cop/highlands/const_assigned_in_wrong_file.rb +74 -0
  23. data/lib/rubocop/cop/highlands/continuation_slash.rb +25 -0
  24. data/lib/rubocop/cop/highlands/default_scope.rb +20 -0
  25. data/lib/rubocop/cop/highlands/factory_attr_references_class.rb +74 -0
  26. data/lib/rubocop/cop/highlands/factory_class_use_string.rb +39 -0
  27. data/lib/rubocop/cop/highlands/mass_assignment_accessible_modifier.rb +18 -0
  28. data/lib/rubocop/cop/highlands/module_method_in_wrong_file.rb +104 -0
  29. data/lib/rubocop/cop/highlands/no_timeout.rb +19 -0
  30. data/lib/rubocop/cop/highlands/opt_arg_parameters.rb +38 -0
  31. data/lib/rubocop/cop/highlands/phrase_bundle_keys.rb +67 -0
  32. data/lib/rubocop/cop/highlands/risky_activerecord_invocation.rb +63 -0
  33. data/lib/rubocop/cop/highlands/rspec_describe_or_context_under_namespace.rb +114 -0
  34. data/lib/rubocop/cop/highlands/rspec_environment_modification.rb +58 -0
  35. data/lib/rubocop/cop/highlands/simple_modifier_conditional.rb +23 -0
  36. data/lib/rubocop/cop/highlands/simple_unless.rb +19 -0
  37. data/lib/rubocop/cop/highlands/spec_constant_assignment.rb +55 -0
  38. data/lib/rubocop/cop/highlands/unsafe_yaml_marshal.rb +47 -0
  39. data/lib/rubocop/highlands.rb +16 -0
  40. data/lib/rubocop/highlands/inflections.rb +14 -0
  41. data/lib/rubocop/highlands/inject.rb +20 -0
  42. data/lib/rubocop/highlands/rails_autoloading.rb +55 -0
  43. data/lib/rubocop/highlands/version.rb +8 -0
  44. data/rubocop-highlands.gemspec +31 -0
  45. data/spec/rubocop/cop/highlands/class_name_spec.rb +78 -0
  46. data/spec/rubocop/cop/highlands/class_or_module_declared_in_wrong_file_spec.rb +174 -0
  47. data/spec/rubocop/cop/highlands/const_assigned_in_wrong_file_spec.rb +178 -0
  48. data/spec/rubocop/cop/highlands/continuation_slash_spec.rb +162 -0
  49. data/spec/rubocop/cop/highlands/default_scope_spec.rb +38 -0
  50. data/spec/rubocop/cop/highlands/factory_attr_references_class_spec.rb +160 -0
  51. data/spec/rubocop/cop/highlands/factory_class_use_string_spec.rb +26 -0
  52. data/spec/rubocop/cop/highlands/mass_assignment_accessible_modifier_spec.rb +28 -0
  53. data/spec/rubocop/cop/highlands/module_method_in_wrong_file_spec.rb +181 -0
  54. data/spec/rubocop/cop/highlands/no_timeout_spec.rb +30 -0
  55. data/spec/rubocop/cop/highlands/opt_arg_parameter_spec.rb +103 -0
  56. data/spec/rubocop/cop/highlands/phrase_bundle_keys_spec.rb +74 -0
  57. data/spec/rubocop/cop/highlands/risky_activerecord_invocation_spec.rb +54 -0
  58. data/spec/rubocop/cop/highlands/rspec_describe_or_context_under_namespace_spec.rb +284 -0
  59. data/spec/rubocop/cop/highlands/rspec_environment_modification_spec.rb +64 -0
  60. data/spec/rubocop/cop/highlands/simple_modifier_conditional_spec.rb +122 -0
  61. data/spec/rubocop/cop/highlands/simple_unless_spec.rb +36 -0
  62. data/spec/rubocop/cop/highlands/spec_constant_assignment_spec.rb +80 -0
  63. data/spec/rubocop/cop/highlands/unsafe_yaml_marshal_spec.rb +50 -0
  64. data/spec/spec_helper.rb +35 -0
  65. metadata +151 -0
@@ -0,0 +1,23 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to tackle prevent more complicated modifier if/unless statements
5
+ # https://github.com/highlands/ruby#only-simple-if-unless
6
+ class SimpleModifierConditional < Cop
7
+ MSG = 'Modifier if/unless usage is okay when the body is simple, ' \
8
+ 'the condition is simple, and the whole thing fits on one line. ' \
9
+ 'Otherwise, avoid modifier if/unless.'.freeze
10
+
11
+ def_node_matcher :multiple_conditionals?, '(if ({and or :^} ...) ...)'
12
+
13
+ def on_if(node)
14
+ return unless node.modifier_form?
15
+
16
+ if multiple_conditionals?(node) || node.multiline?
17
+ add_offense(node)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to tackle prevent unless statements with multiple conditions
5
+ # https://github.com/highlands/ruby#unless-with-multiple-conditions
6
+ class SimpleUnless < Cop
7
+ MSG = 'Unless usage is okay when there is only one conditional'.freeze
8
+
9
+ def_node_matcher :multiple_conditionals?, '(if ({and or :^} ...) ...)'
10
+
11
+ def on_if(node)
12
+ return unless node.unless?
13
+
14
+ add_offense(node) if multiple_conditionals?(node)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ require 'rubocop-rspec'
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Highlands
6
+ # This cop checks for constant assignment inside of specs
7
+ #
8
+ # @example
9
+ # # bad
10
+ # describe Something do
11
+ # PAYLOAD = [1, 2, 3]
12
+ # end
13
+ #
14
+ # # good
15
+ # describe Something do
16
+ # let(:payload) { [1, 2, 3] }
17
+ # end
18
+ #
19
+ # # bad
20
+ # describe Something do
21
+ # MyClass::PAYLOAD = [1, 2, 3]
22
+ # end
23
+ #
24
+ # # good
25
+ # describe Something do
26
+ # before { stub_const('MyClass::PAYLOAD', [1, 2, 3])
27
+ # end
28
+ class SpecConstantAssignment < Cop
29
+ include RuboCop::RSpec::TopLevelDescribe
30
+ MESSAGE = "Defining constants inside of specs can cause spurious behavior. " \
31
+ "It is almost always preferable to use `let` statements, "\
32
+ "anonymous class/module definitions, or stub_const".freeze
33
+
34
+ def on_casgn(node)
35
+ return unless in_spec_file?(node)
36
+ parent_module_name = node.parent_module_name
37
+ if node.parent_module_name && parent_module_name != 'Object'
38
+ return
39
+ end
40
+ add_offense(node, message: MESSAGE)
41
+ end
42
+
43
+ private
44
+
45
+ def in_spec_file?(node)
46
+ filename = node.location.expression.source_buffer.name
47
+
48
+ # For tests, the input is a string
49
+ return true if filename == "(string)"
50
+ filename.include?("/spec/")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,47 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Disallow use of YAML/Marshal methods that can trigger RCE on untrusted input
5
+ class UnsafeYamlMarshal < Cop
6
+ MSG = 'Using unsafe YAML parsing methods on untrusted input can lead ' \
7
+ 'to remote code execution. Use `safe_load`, `parse`, `parse_file`, or ' \
8
+ '`parse_stream` instead'.freeze
9
+
10
+ def on_send(node)
11
+ receiver, method_name, *_args = *node
12
+
13
+ return if receiver.nil?
14
+ return unless receiver.const_type?
15
+
16
+ check_yaml(node, receiver, method_name, *_args)
17
+ check_marshal(node, receiver, method_name, *_args)
18
+ rescue => e
19
+ puts e
20
+ puts e.backtrace
21
+ raise
22
+ end
23
+
24
+ def check_yaml(node, receiver, method_name, *_args)
25
+ return unless ['YAML', 'Psych'].include?(receiver.const_name)
26
+ return unless [:load, :load_documents, :load_file, :load_stream].include?(method_name)
27
+
28
+ message = "Using `#{receiver.const_name}.#{method_name}` on untrusted input can lead " \
29
+ "to remote code execution. Use `safe_load`, `parse`, `parse_file`, or " \
30
+ "`parse_stream` instead"
31
+
32
+ add_offense(node, message: message)
33
+ end
34
+
35
+ def check_marshal(node, receiver, method_name, *_args)
36
+ return unless receiver.const_name == 'Marshal'
37
+ return unless method_name == :load
38
+
39
+ message = 'Using `Marshal.load` on untrusted input can lead to remote code execution. ' \
40
+ 'Restructure your code to not use Marshal'
41
+
42
+ add_offense(node, message: message)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ require 'pathname'
2
+ require 'psych'
3
+
4
+ Dir.glob(File.expand_path('cop/**/*.rb', File.dirname(__FILE__))).map(&method(:require))
5
+
6
+ module RuboCop
7
+ # RuboCop Highlands project namespace
8
+ module Highlands
9
+ PROJECT_ROOT =
10
+ Pathname.new(__FILE__).parent.parent.parent.expand_path.freeze
11
+ CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
12
+ CONFIG = Psych.safe_load(CONFIG_DEFAULT.read).freeze
13
+
14
+ private_constant(*constants(false))
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # String inflections copied over from ActiveSupport
2
+ module Inflections
3
+ # Convert Foo::BarBaz to foo/bar_baz.
4
+ # Copied from ActiveSupport.
5
+ def underscore(camel_cased_word)
6
+ word = camel_cased_word.to_s.dup
7
+ word.gsub!(/::/, '/')
8
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
9
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
10
+ word.tr!("-", "_")
11
+ word.downcase!
12
+ word
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # Straight up ripped from the custom Rspec rubocop
2
+ # https://github.com/nevir/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
3
+ require 'yaml'
4
+
5
+ module RuboCop
6
+ module Highlands
7
+ # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
+ # bit of our configuration.
9
+ module Inject
10
+ def self.defaults!
11
+ path = CONFIG_DEFAULT.to_s
12
+ hash = ConfigLoader.load_file(path).to_hash
13
+ config = Config.new(hash, path)
14
+ puts "configuration from #{path}" if ConfigLoader.debug?
15
+ config = ConfigLoader.merge_with_default(config, path)
16
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ # These methods are useful for Rubocop rules related to Rails autoloading.
2
+ module RailsAutoloading
3
+ def run_rails_autoloading_cops?(path)
4
+ return false unless config["Rails".freeze]
5
+ return false unless config["Rails".freeze]["Enabled".freeze]
6
+
7
+ # Ignore rake tasks
8
+ return false unless path.end_with?(".rb")
9
+
10
+ true
11
+ end
12
+
13
+ # Given "foo/bar/baz", return:
14
+ # [
15
+ # %r{/foo.rb$},
16
+ # %r{/foo/bar.rb$},
17
+ # %r{/foo/bar/baz.rb$},
18
+ # %r{/foo/bar/baz/}, # <= only if allow_dir = true
19
+ # ]
20
+ def allowable_paths_for(expected_dir, options = {})
21
+ options = { allow_dir: false }.merge(options)
22
+ allowable_paths = []
23
+ next_slash = expected_dir.index("/")
24
+ while next_slash
25
+ allowable_paths << %r{/#{expected_dir[0...next_slash]}.rb$}
26
+ next_slash = expected_dir.index("/", next_slash + 1)
27
+ end
28
+ allowable_paths << %r{#{expected_dir}.rb$}
29
+ allowable_paths << %r{/#{expected_dir}/} if options[:allow_dir]
30
+ allowable_paths
31
+ end
32
+
33
+ def normalize_module_name(module_name)
34
+ return '' if module_name.nil?
35
+ normalized_name = module_name.gsub(/#<Class:|>/, "")
36
+ normalized_name = "" if normalized_name == "Object"
37
+ normalized_name
38
+ end
39
+
40
+ # module_name looks like one of these:
41
+ # Foo::Bar for an instance method
42
+ # #<Class:Foo::Bar> for a class method.
43
+ # For either case we return ["Foo", "Bar"]
44
+ def split_modules(module_name)
45
+ normalize_module_name(module_name).split("::")
46
+ end
47
+
48
+ def full_const_name(parent_module_name, const_name)
49
+ if parent_module_name == "".freeze
50
+ "#{const_name}"
51
+ else
52
+ "#{parent_module_name}::#{const_name}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Highlands
5
+ # Version information for the the Highlands RuboCop plugin.
6
+ VERSION = '1.0.0'.freeze
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'rubocop/highlands/version'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'rubocop-highlands'
6
+ spec.summary = 'Custom code style checking for Church of the Highlands.'
7
+ spec.description = <<-EOF
8
+ A plugin for RuboCop code style enforcing & linting tool. It includes Rubocop configuration
9
+ used at Church of the Highlands and a few custom rules that have caused internal issues at
10
+ Church of the Highlands but are not supported by core Rubocop.
11
+ EOF
12
+ spec.authors = ['Church of the Highlands - Digital Team']
13
+ spec.email = ['info@churchofthehighlands.com']
14
+ spec.homepage = 'https://github.com/highlands/ruby'
15
+ spec.license = 'MIT'
16
+ spec.version = RuboCop::Highlands::VERSION
17
+ spec.platform = Gem::Platform::RUBY
18
+ spec.required_ruby_version = '>= 2.1'
19
+
20
+ spec.require_paths = ['lib']
21
+ spec.files = Dir[
22
+ '{config,lib,spec}/**/*',
23
+ '*.md',
24
+ '*.gemspec',
25
+ 'Gemfile',
26
+ ]
27
+
28
+ spec.add_dependency('rubocop', '~> 0.58.0')
29
+ spec.add_dependency('rubocop-rspec', '~> 1.30.0')
30
+ spec.add_development_dependency('rspec', '~> 3.5')
31
+ end
@@ -0,0 +1,78 @@
1
+ describe RuboCop::Cop::Highlands::ClassName do
2
+ subject(:cop) { described_class.new }
3
+
4
+ describe "belongs_to" do
5
+ it 'rejects with Model.name' do
6
+ source = [
7
+ 'class Coupon',
8
+ ' belongs_to :user, :class_name => User.name',
9
+ 'end',
10
+ ].join("\n")
11
+ inspect_source(source)
12
+
13
+ expect(cop.offenses.size).to eq(1)
14
+ expect(cop.offenses.map(&:line).sort).to eq([2])
15
+ end
16
+
17
+ it 'passes with "Model"' do
18
+ source = [
19
+ 'class Coupon',
20
+ ' belongs_to :user, :class_name => "User"',
21
+ 'end',
22
+ ].join("\n")
23
+ inspect_source(source)
24
+
25
+ expect(cop.offenses).to be_empty
26
+ end
27
+ end
28
+
29
+ describe "has_many" do
30
+ it 'rejects with Model.name' do
31
+ source = [
32
+ 'class Coupon',
33
+ ' has_many :reservations, :class_name => Reservation2.name',
34
+ 'end',
35
+ ].join("\n")
36
+ inspect_source(source)
37
+
38
+ expect(cop.offenses.size).to eq(1)
39
+ expect(cop.offenses.map(&:line)).to eq([2])
40
+ end
41
+
42
+ it 'passes with "Model"' do
43
+ source = [
44
+ 'class Coupon',
45
+ ' has_many :reservations, :class_name => "Reservation2"',
46
+ 'end',
47
+ ].join("\n")
48
+ inspect_source(source)
49
+
50
+ expect(cop.offenses).to be_empty
51
+ end
52
+ end
53
+
54
+ describe "has_one" do
55
+ it 'rejects with Model.name' do
56
+ source = [
57
+ 'class Coupon',
58
+ ' has_one :loss, :class_name => Payments::Loss.name',
59
+ 'end',
60
+ ].join("\n")
61
+ inspect_source(source)
62
+
63
+ expect(cop.offenses.size).to eq(1)
64
+ expect(cop.offenses.map(&:line)).to eq([2])
65
+ end
66
+
67
+ it 'passes with "Model"' do
68
+ source = [
69
+ 'class Coupon',
70
+ ' has_one :loss, :class_name => "Payments::Loss"',
71
+ 'end',
72
+ ].join("\n")
73
+ inspect_source(source)
74
+
75
+ expect(cop.offenses).to be_empty
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,174 @@
1
+ describe RuboCop::Cop::Highlands::ClassOrModuleDeclaredInWrongFile do
2
+ subject(:cop) { described_class.new(config) }
3
+
4
+ let(:config) do
5
+ RuboCop::Config.new(
6
+ {
7
+ "Rails" => {
8
+ "Enabled" => true,
9
+ },
10
+ }
11
+ )
12
+ end
13
+
14
+ # Put source in a directory under /tmp because this cop cares about the filename
15
+ # but not the parent dir name.
16
+ let(:tmpdir) { Dir.mktmpdir }
17
+ let(:models_dir) do
18
+ gemfile = File.new("#{tmpdir}/Gemfile", "w")
19
+ gemfile.close
20
+ FileUtils.mkdir_p("#{tmpdir}/app/models").first
21
+ end
22
+
23
+ after do
24
+ FileUtils.rm_rf tmpdir
25
+ end
26
+
27
+ it 'rejects if class declaration is in a file with non-matching name' do
28
+ source = [
29
+ 'module Foo',
30
+ ' module Bar',
31
+ ' class Baz',
32
+ ' end',
33
+ ' end',
34
+ 'end',
35
+ ].join("\n")
36
+
37
+ File.open "#{models_dir}/qux.rb", "w" do |file|
38
+ inspect_source(source, file)
39
+ end
40
+
41
+ expect(cop.offenses.size).to eq(3)
42
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2, 3])
43
+ expect(cop.offenses.first.message).to include('Module Foo should be defined in foo.rb.')
44
+ end
45
+
46
+ it 'rejects if class declaration is in a file with matching name but wrong parent dir' do
47
+ source = [
48
+ 'module Foo',
49
+ ' module Bar',
50
+ ' class Baz',
51
+ ' end',
52
+ ' end',
53
+ 'end',
54
+ ].join("\n")
55
+
56
+ File.open "#{models_dir}/baz.rb", "w" do |file|
57
+ inspect_source(source, file)
58
+ end
59
+
60
+ expect(cop.offenses.size).to eq(3)
61
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2, 3])
62
+ expect(cop.offenses.last.message).to include('Class Baz should be defined in foo/bar/baz.rb.')
63
+ end
64
+
65
+ it 'accepts if class declaration is in a file with matching name and right parent dir' do
66
+ source = [
67
+ 'module Foo',
68
+ ' module Bar',
69
+ ' class Baz',
70
+ ' end',
71
+ ' end',
72
+ 'end',
73
+ ].join("\n")
74
+
75
+ FileUtils.mkdir_p "#{models_dir}/foo/bar"
76
+ File.open "#{models_dir}/foo/bar/baz.rb", "w" do |file|
77
+ inspect_source(source, file)
78
+ end
79
+
80
+ expect(cop.offenses).to be_empty
81
+ end
82
+
83
+ it 'rejects if class declaration is in wrong dir and parent module uses ::' do
84
+ source = [
85
+ 'module Foo::Bar',
86
+ ' class Baz',
87
+ ' end',
88
+ 'end',
89
+ ].join("\n")
90
+
91
+ FileUtils.mkdir_p "#{models_dir}/bar"
92
+ File.open "#{models_dir}/bar/baz.rb", "w" do |file|
93
+ inspect_source(source, file)
94
+ end
95
+
96
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2])
97
+ expect(cop.offenses.last.message).to include('Class Baz should be defined in foo/bar/baz.rb.')
98
+ end
99
+
100
+ it 'accepts if class declaration is in a file with matching name and parent module uses ::' do
101
+ source = [
102
+ 'module Foo::Bar',
103
+ ' class Baz',
104
+ ' end',
105
+ 'end',
106
+ ].join("\n")
107
+
108
+ FileUtils.mkdir_p "#{models_dir}/foo/bar"
109
+ File.open "#{models_dir}/foo/bar/baz.rb", "w" do |file|
110
+ inspect_source(source, file)
111
+ end
112
+
113
+ expect(cop.offenses).to be_empty
114
+ end
115
+
116
+ it 'accepts class declaration where the containing class uses an acronym' do
117
+ source = [
118
+ 'module CSVFoo',
119
+ ' class Baz',
120
+ ' end',
121
+ 'end',
122
+ ].join("\n")
123
+
124
+ FileUtils.mkdir_p "#{models_dir}/csv_foo"
125
+ File.open "#{models_dir}/csv_foo/baz.rb", "w" do |file|
126
+ inspect_source(source, file)
127
+ end
128
+
129
+ expect(cop.offenses).to be_empty
130
+ end
131
+
132
+ it 'ignores class/module declaration in a rake task' do
133
+ source = [
134
+ 'class Baz',
135
+ 'end',
136
+ ].join("\n")
137
+
138
+ File.open "#{models_dir}/foo.rake", "w" do |file|
139
+ inspect_source(source, file)
140
+ end
141
+
142
+ expect(cop.offenses).to be_empty
143
+ end
144
+
145
+ it 'suggests moving error classes into the file that defines the owning scope' do
146
+ source = [
147
+ 'module Foo',
148
+ ' class BarError < StandardError; end',
149
+ 'end',
150
+ ].join("\n")
151
+
152
+ File.open "#{models_dir}/bar.rb", "w" do |file|
153
+ inspect_source(source, file)
154
+ end
155
+
156
+ expect(cop.offenses.map(&:line)).to include(2)
157
+ expect(cop.offenses.map(&:message)).to include(%r{Class BarError should be defined in foo\.rb.})
158
+ end
159
+
160
+ it 'recognizes error class based on the superclass name' do
161
+ source = [
162
+ 'module Foo',
163
+ ' class Bar < StandardError; end',
164
+ 'end',
165
+ ].join("\n")
166
+
167
+ File.open "#{models_dir}/bar.rb", "w" do |file|
168
+ inspect_source(source, file)
169
+ end
170
+
171
+ expect(cop.offenses.map(&:line)).to include(2)
172
+ expect(cop.offenses.map(&:message)).to include(%r{Class Bar should be defined in foo\.rb.})
173
+ end
174
+ end