Skip to content

Commit b62814d

Browse files
Bart de Waterbdewater
authored andcommitted
Add new Rails/ActiveSupportOnLoad cop.
This cop is extracted from Shopify's internal Rubocop repository. Many thanks to the original authors for their work. Julian Nadeau <[email protected]> Rafael Mendonça França <[email protected]> Francois Chagnon <[email protected]> Jean Boussier <[email protected]>
1 parent 931e9b2 commit b62814d

File tree

5 files changed

+170
-0
lines changed

5 files changed

+170
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Change log
22

33
## master (unreleased)
4+
* [#x](https://github.com/rubocop/rubocop-rails/issues/x): Add new `Rails/ActiveSupportOnLoadac` cop. ([@bdewater][])
45

56
## 2.15.2 (2022-07-07)
67

@@ -644,3 +645,4 @@
644645
[@kkitadate]: https://github.com/kkitadate
645646
[@Darhazer]: https://github.com/Darhazer
646647
[@kazarin]: https://github.com/kazarin
648+
[@bdewater]: https://github.com/bdewater

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ Rails/ActiveSupportAliases:
129129
Enabled: true
130130
VersionAdded: '0.48'
131131

132+
Rails/ActiveSupportOnLoad:
133+
Description: 'Use `ActiveSupport.on_load(...)` to patch Rails core classes.'
134+
StyleGuide: 'https://rails.rubystyle.guide/#hash-conditions'
135+
Enabled: 'pending'
136+
VersionAdded: '<<next>>'
137+
132138
Rails/AddColumnIndex:
133139
Description: >-
134140
Rails migrations don't make use of a given `index` key, but also
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Use Active Support lazy load hooks to patch Rails core classes, so they are not forcible loaded early.
7+
#
8+
# @example
9+
#
10+
# # bad
11+
# ActiveRecord::Base.include(MyClass)
12+
#
13+
# # good
14+
# ActiveSupport.on_load(:active_record) { include MyClass }
15+
class ActiveSupportOnLoad < Base
16+
extend AutoCorrector
17+
18+
AUTOCORRECTABLE_CLASSES = {
19+
'ActiveRecord::Base' => 'active_record',
20+
'ActionController::Base' => 'action_controller',
21+
'ActiveJob::Base' => 'active_job',
22+
'ActionView::Base' => 'action_view',
23+
'ActionMailer::Base' => 'action_mailer',
24+
'ActionController::TestCase' => 'action_controller_test_case',
25+
'ActiveSupport::TestCase' => 'active_support_test_case',
26+
'ActiveJob::TestCase' => 'active_job_test_case',
27+
'ActionDispatch::IntegrationTest' => 'action_dispatch_integration_test',
28+
'ActionMailer::TestCase' => 'action_mailer_test_case'
29+
}.freeze
30+
RESTRICT_ON_SEND = %i[prepend include extend].freeze
31+
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
32+
33+
def on_send(node)
34+
receiver, method, arguments = *node # rubocop:disable InternalAffairs/NodeDestructuring
35+
return unless receiver && AUTOCORRECTABLE_CLASSES.key?(receiver.const_name)
36+
37+
hook_name = AUTOCORRECTABLE_CLASSES[receiver.const_name]
38+
preferred = "ActiveSupport.on_load(:#{hook_name}) { #{method} #{arguments.source} }"
39+
40+
add_offense(node, message: format(MSG, prefer: preferred, current: node.source)) do |corrector|
41+
corrector.replace(node, preferred)
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
require_relative 'rails/active_record_callbacks_order'
1616
require_relative 'rails/active_record_override'
1717
require_relative 'rails/active_support_aliases'
18+
require_relative 'rails/active_support_on_load'
1819
require_relative 'rails/add_column_index'
1920
require_relative 'rails/after_commit_override'
2021
require_relative 'rails/application_controller'
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe(RuboCop::Cop::Rails::ActiveSupportOnLoad, :config) do
4+
it 'adds offense when trying to extend a framework class with include' do
5+
expect_offense(<<~RUBY)
6+
ActiveRecord::Base.include(MyClass)
7+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `ActiveSupport.on_load(:active_record) { include MyClass }` instead of `ActiveRecord::Base.include(MyClass)`.
8+
RUBY
9+
end
10+
11+
it 'adds offense when trying to extend a framework class with prepend' do
12+
expect_offense(<<~RUBY)
13+
ActiveRecord::Base.prepend(MyClass)
14+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `ActiveSupport.on_load(:active_record) { prepend MyClass }` instead of `ActiveRecord::Base.prepend(MyClass)`.
15+
RUBY
16+
end
17+
18+
it 'adds offense when trying to extend a framework class with extend' do
19+
expect_offense(<<~RUBY)
20+
ActiveRecord::Base.extend(MyClass)
21+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `ActiveSupport.on_load(:active_record) { extend MyClass }` instead of `ActiveRecord::Base.extend(MyClass)`.
22+
RUBY
23+
end
24+
25+
it 'adds offense when trying to extend a framework class with absolute name' do
26+
expect_offense(<<~RUBY)
27+
::ActiveRecord::Base.extend(MyClass)
28+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `ActiveSupport.on_load(:active_record) { extend MyClass }` instead of `::ActiveRecord::Base.extend(MyClass)`.
29+
RUBY
30+
end
31+
32+
it 'adds offense when trying to extend a framework class with a variable' do
33+
expect_offense(<<~RUBY)
34+
ActiveRecord::Base.extend(my_class)
35+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `ActiveSupport.on_load(:active_record) { extend my_class }` instead of `ActiveRecord::Base.extend(my_class)`.
36+
RUBY
37+
end
38+
39+
it 'does not add offense when extending a variable' do
40+
expect_no_offenses(<<~RUBY)
41+
foo.extend(MyClass)
42+
RUBY
43+
end
44+
45+
it 'does not add offense when extending the framework using on_load and include' do
46+
expect_no_offenses(<<~RUBY)
47+
ActiveSupport.on_load(:active_record) { include MyClass }
48+
RUBY
49+
end
50+
51+
it 'does not add offense when extending the framework using on_load and include in a multi-line block' do
52+
expect_no_offenses(<<~RUBY)
53+
ActiveSupport.on_load(:active_record) do
54+
include MyClass
55+
end
56+
RUBY
57+
end
58+
59+
it 'does not add offense when not including a class' do
60+
expect_no_offenses(<<~RUBY)
61+
ActiveRecord::Base.include_root_in_json = false
62+
RUBY
63+
end
64+
65+
it 'does not add offense when using include?' do
66+
expect_no_offenses(<<~RUBY)
67+
name.include?('bob')
68+
RUBY
69+
end
70+
71+
context 'autocorrect' do
72+
it 'autocorrects extension on supported classes' do
73+
source = <<~RUBY
74+
ActiveRecord::Base.prepend(MyClass)
75+
RUBY
76+
77+
corrected_source = <<~RUBY
78+
ActiveSupport.on_load(:active_record) { prepend MyClass }
79+
RUBY
80+
81+
corrected = autocorrect_source(source)
82+
83+
expect(corrected).to(eq(corrected_source))
84+
end
85+
86+
it 'does not autocorrect extension on unsupported classes' do
87+
source = <<~RUBY
88+
MyClass1.prepend(MyClass)
89+
RUBY
90+
91+
corrected = autocorrect_source(source)
92+
93+
expect(corrected).to(eq(source))
94+
95+
source = <<~RUBY
96+
MyClass1.include(MyClass)
97+
RUBY
98+
99+
corrected = autocorrect_source(source)
100+
101+
expect(corrected).to(eq(source))
102+
end
103+
104+
it 'does not autocorrect when there is no extension on the supported classes' do
105+
source = <<~RUBY
106+
ActiveRecord::Base.include_root_in_json = false
107+
RUBY
108+
109+
corrected = autocorrect_source(source)
110+
111+
expect(corrected).to(eq(source))
112+
end
113+
end
114+
end

0 commit comments

Comments
 (0)