Skip to content

Commit 99537f8

Browse files
authored
Add RuboCop cop to enforce no strict loading model or association config (#69)
### Summary - [x] [Fix GitHub Action to quote ruby 3.0 version so it's not evaluated as 3](98706ef) - [x] [Fix rubocop/cop/betterment require list to be sorted](e2784ab) - [x] [Add RuboCop::Cop::Betterment::UseGlobalStrictLoading cops](ce87479) - [x] [Upgrade betterlint to 1.17.0](ffeb167) ### Description This branch adds two cops to ensure models and associations don't have strict loading configuration.
1 parent 7605ff2 commit 99537f8

File tree

10 files changed

+305
-17
lines changed

10 files changed

+305
-17
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
ruby: [3.0, 3.1, 3.2]
11+
ruby: ['3.0', 3.1, 3.2]
1212
gemfile:
1313
- Gemfile
1414

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.17.0] - 2025-01-24
8+
### Added
9+
- Betterment/UseGlobalStrictLoading/ByDefaultForModels cop
10+
- Betterment/UseGlobalStrictLoading/ForAssociations cop
11+
712
## [1.1.0] - 2022-02-16
813
### Added
914
- Betterment/NonStandardActions cop

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
betterlint (1.16.0)
4+
betterlint (1.17.0)
55
rubocop (~> 1.62.0)
66
rubocop-graphql (~> 1.5.0)
77
rubocop-performance (~> 1.21.0)

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,20 @@ actions often indicates error handling (e.g. `422 Unprocessable Entity`).
280280
This cop requires you to explicitly provide an HTTP status code when rendering a response in the
281281
create, update, and destroy actions. When autocorrecting, this will automatically add
282282
`status: :unprocessable_entity` or `status: :ok` depending on what you're rendering.
283+
284+
### Betterment/UseGlobalStrictLoading/ByDefaultForModels
285+
286+
This cop identifies models where `self.strict_loading_by_default` is assigned to explicitly, and prefers that it be removed in favor of using the global strict loading settings.
287+
288+
We use this to create a `.rubocop_todo.yml` file showing all the models that have been explicitly configured, so that we can "burn down" and lean more heavily on using global settings. We prefer to enable strict loading by default globally, but transioning a large Rails application to work in that state is difficult and must be broken down into smaller steps. Our workflow is to change all models to turn off strict loading, regenerate our rubocop todo file, then enable the flag globally. Then on a model-by-model basis we'll enable them one at a time, and fix all spec failures per model.
289+
290+
This allows us two benefits:
291+
292+
1. We can gradually change code used by older models to use strict loading without having to fix the world all at once.
293+
2. All new models will strict load by default, forcing us to specify the objcts to load up-front in our controllers and other call sites.
294+
295+
### Betterment/UseGlobalStrictLoading/ForAssociations
296+
297+
This cop identifies associations where `:strict_loading` is set explicitly, and prefers that it be removed in favor of using the global strict loading settings.
298+
299+
This is related to the [Betterment/UseGlobalStrictLoading/ByDefaultForModels](#bettermentuseglobalstrictloadingbydefaultformodels) cop, but allows for more granular enablement and disablement of associations within a model. The intention is similar, in that we are using this cop to help "burn down" code to strict load, but it allows us to focus on the per-association level. Some models may have quite a lot of usage, so enabling it for a model might cause thousands of failures in the specs. In those cases we will disable all the associations, and then work through them one at a time until all code that uses the model strict loads.

betterlint.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
55

66
Gem::Specification.new do |s|
77
s.name = "betterlint"
8-
s.version = "1.16.0"
8+
s.version = "1.17.0"
99
s.authors = ["Development"]
1010
s.email = ["[email protected]"]
1111
s.summary = "Betterment rubocop configuration"

config/default.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ Betterment/UnsafeJob:
102102
Betterment/UnscopedFind:
103103
StyleGuide: '#bettermentunscopedfind'
104104

105+
Betterment/UseGlobalStrictLoading/ByDefaultForModels:
106+
Enabled: true
107+
SafeAutoCorrect: false
108+
109+
Betterment/UseGlobalStrictLoading/ForAssociations:
110+
Enabled: true
111+
SafeAutoCorrect: false
112+
105113
FactoryBot/AssociationStyle:
106114
Enabled: false
107115

lib/rubocop/cop/betterment.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
# frozen_string_literal: true
22

33
require 'rubocop'
4-
require 'rubocop/cop/betterment/utils/parser'
5-
require 'rubocop/cop/betterment/utils/method_return_table'
6-
require 'rubocop/cop/betterment/utils/hardcoded_attribute'
4+
require 'rubocop/cop/betterment/active_job_performable'
5+
require 'rubocop/cop/betterment/allowlist_blocklist'
76
require 'rubocop/cop/betterment/authorization_in_controller'
87
require 'rubocop/cop/betterment/direct_delayed_enqueue'
98
require 'rubocop/cop/betterment/dynamic_params'
10-
require 'rubocop/cop/betterment/unscoped_find'
11-
require 'rubocop/cop/betterment/unsafe_job'
12-
require 'rubocop/cop/betterment/timeout'
9+
require 'rubocop/cop/betterment/fetch_boolean'
10+
require 'rubocop/cop/betterment/hardcoded_id'
11+
require 'rubocop/cop/betterment/implicit_redirect_type'
1312
require 'rubocop/cop/betterment/internals_protection'
1413
require 'rubocop/cop/betterment/memoization_with_arguments'
1514
require 'rubocop/cop/betterment/non_standard_actions'
1615
require 'rubocop/cop/betterment/non_standard_controller'
16+
require 'rubocop/cop/betterment/redirect_status'
17+
require 'rubocop/cop/betterment/render_status'
18+
require 'rubocop/cop/betterment/server_error_assertion'
1719
require 'rubocop/cop/betterment/site_prism_loaded'
1820
require 'rubocop/cop/betterment/spec_helper_required_outside_spec_dir'
19-
require 'rubocop/cop/betterment/implicit_redirect_type'
20-
require 'rubocop/cop/betterment/active_job_performable'
21-
require 'rubocop/cop/betterment/allowlist_blocklist'
22-
require 'rubocop/cop/betterment/server_error_assertion'
23-
require 'rubocop/cop/betterment/hardcoded_id'
21+
require 'rubocop/cop/betterment/timeout'
22+
require 'rubocop/cop/betterment/unsafe_job'
23+
require 'rubocop/cop/betterment/unscoped_find'
24+
require 'rubocop/cop/betterment/use_global_strict_loading'
25+
require 'rubocop/cop/betterment/utils/hardcoded_attribute'
26+
require 'rubocop/cop/betterment/utils/method_return_table'
27+
require 'rubocop/cop/betterment/utils/parser'
2428
require 'rubocop/cop/betterment/vague_serialize'
25-
require 'rubocop/cop/betterment/fetch_boolean'
26-
require 'rubocop/cop/betterment/render_status'
27-
require 'rubocop/cop/betterment/redirect_status'
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Betterment
6+
module UseGlobalStrictLoading
7+
# This cop ensures that `self.strict_loading_by_default = <any value>` is not set in ActiveRecord models.
8+
class ByDefaultForModels < Base
9+
extend AutoCorrector
10+
include RangeHelp
11+
12+
MSG = 'Do not set `self.strict_loading_by_default` in ActiveRecord models.'
13+
14+
# @!method strict_loading_by_default_set?(node)
15+
def_node_matcher :strict_loading_by_default_set?, <<~PATTERN
16+
$(send self :strict_loading_by_default= _)
17+
PATTERN
18+
19+
def on_send(node)
20+
strict_loading_by_default_set?(node) do |method_call|
21+
add_offense(method_call) do |corrector|
22+
corrector.remove(method_call)
23+
end
24+
end
25+
end
26+
end
27+
28+
# This cop ensures that `strict_loading: <any value>` is not set in ActiveRecord associations.
29+
class ForAssociations < Base
30+
extend AutoCorrector
31+
include RangeHelp
32+
33+
MSG = 'Do not set `:strict_loading` in ActiveRecord associations.'
34+
35+
ASSOCIATION_METHODS = %i(belongs_to has_and_belongs_to_many has_many has_one).freeze
36+
37+
# @!method association_with_strict_loading?(node)
38+
def_node_matcher :association_with_strict_loading?, <<~PATTERN
39+
(send nil? {#{ASSOCIATION_METHODS.map(&:inspect).join(' ')}} ... (hash <$(pair (sym :strict_loading) ...) ...>))
40+
PATTERN
41+
42+
def on_send(node)
43+
association_with_strict_loading?(node) do |pair|
44+
add_offense(node) do |corrector|
45+
corrector.remove(range_with_surrounding_comma(range_with_surrounding_space(range: pair.source_range, side: :left), :left))
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
52+
end
53+
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RuboCop::Cop::Betterment::UseGlobalStrictLoading::ByDefaultForModels, :config do
6+
context 'when `self.strict_loading_by_default` is set' do
7+
context 'when the class has no code before or after self.strict_loading_by_default' do
8+
it 'registers an offense' do
9+
expect_offense(<<~RUBY)
10+
class MyModel < ApplicationRecord
11+
self.strict_loading_by_default = true
12+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `self.strict_loading_by_default` in ActiveRecord models.
13+
end
14+
RUBY
15+
16+
# Add explicit spaces to prevent editor from stripping them on-save
17+
expect_correction(<<~RUBY)
18+
class MyModel < ApplicationRecord
19+
#{' '}
20+
end
21+
RUBY
22+
end
23+
end
24+
25+
context 'when the class has a comment after self.strict_loading_by_default' do
26+
it 'registers an offense' do
27+
expect_offense(<<~RUBY)
28+
class MyModel < ApplicationRecord
29+
self.strict_loading_by_default = true # Set to true to enable strict_loading_by_default
30+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `self.strict_loading_by_default` in ActiveRecord models.
31+
end
32+
RUBY
33+
34+
expect_correction(<<~RUBY)
35+
class MyModel < ApplicationRecord
36+
# Set to true to enable strict_loading_by_default
37+
end
38+
RUBY
39+
end
40+
end
41+
42+
context 'when the class has code before self.strict_loading_by_default' do
43+
it 'registers an offense' do
44+
expect_offense(<<~RUBY)
45+
class MyModel < ApplicationRecord
46+
self.table_name = :my_models
47+
self.strict_loading_by_default = true
48+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `self.strict_loading_by_default` in ActiveRecord models.
49+
end
50+
RUBY
51+
52+
# Add explicit spaces to prevent editor from stripping them on-save
53+
expect_correction(<<~RUBY)
54+
class MyModel < ApplicationRecord
55+
self.table_name = :my_models
56+
#{' '}
57+
end
58+
RUBY
59+
end
60+
end
61+
62+
context 'when the class has code after self.strict_loading_by_default' do
63+
it 'registers an offense' do
64+
expect_offense(<<~RUBY)
65+
class MyModel < ApplicationRecord
66+
self.strict_loading_by_default = true
67+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `self.strict_loading_by_default` in ActiveRecord models.
68+
69+
belongs_to :user
70+
end
71+
RUBY
72+
73+
# Add explicit spaces to prevent editor from stripping them on-save
74+
expect_correction(<<~RUBY)
75+
class MyModel < ApplicationRecord
76+
#{' '}
77+
78+
belongs_to :user
79+
end
80+
RUBY
81+
end
82+
end
83+
end
84+
85+
it 'does not register an offense when `self.strict_loading_by_default` is not set' do
86+
expect_no_offenses(<<~RUBY)
87+
class MyModel < ApplicationRecord
88+
end
89+
RUBY
90+
end
91+
end
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RuboCop::Cop::Betterment::UseGlobalStrictLoading::ForAssociations, :config do
6+
context 'when `strict_loading` is set in an association' do
7+
it 'registers an offense for belongs_to' do
8+
expect_offense(<<~RUBY)
9+
class MyModel < ApplicationRecord
10+
belongs_to :user, strict_loading: false # preserve this comment
11+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
12+
end
13+
RUBY
14+
15+
expect_correction(<<~RUBY)
16+
class MyModel < ApplicationRecord
17+
belongs_to :user # preserve this comment
18+
end
19+
RUBY
20+
end
21+
22+
it 'registers an offense for has_and_belongs_to_many' do
23+
expect_offense(<<~RUBY)
24+
class MyModel < ApplicationRecord
25+
has_and_belongs_to_many :tags, strict_loading: true # preserve this comment
26+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
27+
end
28+
RUBY
29+
30+
expect_correction(<<~RUBY)
31+
class MyModel < ApplicationRecord
32+
has_and_belongs_to_many :tags # preserve this comment
33+
end
34+
RUBY
35+
end
36+
37+
it 'registers an offense for has_many' do
38+
expect_offense(<<~RUBY)
39+
class MyModel < ApplicationRecord
40+
has_many :posts, strict_loading: true # preserve this comment
41+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
42+
end
43+
RUBY
44+
45+
expect_correction(<<~RUBY)
46+
class MyModel < ApplicationRecord
47+
has_many :posts # preserve this comment
48+
end
49+
RUBY
50+
end
51+
52+
it 'registers an offense for has_one' do
53+
expect_offense(<<~RUBY)
54+
class MyModel < ApplicationRecord
55+
has_one :account, strict_loading: true # preserve this comment
56+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
57+
end
58+
RUBY
59+
60+
expect_correction(<<~RUBY)
61+
class MyModel < ApplicationRecord
62+
has_one :account # preserve this comment
63+
end
64+
RUBY
65+
end
66+
67+
it 'preserves other options' do
68+
expect_offense(<<~RUBY)
69+
class MyModel < ApplicationRecord
70+
has_many :posts, strict_loading: true, dependent: :destroy # preserve this comment
71+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
72+
end
73+
RUBY
74+
75+
expect_correction(<<~RUBY)
76+
class MyModel < ApplicationRecord
77+
has_many :posts, dependent: :destroy # preserve this comment
78+
end
79+
RUBY
80+
end
81+
82+
it 'preserves multiline options' do
83+
expect_offense(<<~RUBY)
84+
class MyModel < ApplicationRecord
85+
has_many :posts, # preserve this comment
86+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not set `:strict_loading` in ActiveRecord associations.
87+
strict_loading: true, # preserve this comment
88+
dependent: :destroy # preserve this comment
89+
end
90+
RUBY
91+
92+
expect_correction(<<~RUBY)
93+
class MyModel < ApplicationRecord
94+
has_many :posts, # preserve this comment, # preserve this comment
95+
dependent: :destroy # preserve this comment
96+
end
97+
RUBY
98+
end
99+
end
100+
101+
context 'when `strict_loading` is not set in an association' do
102+
it 'does not register an offense' do
103+
expect_no_offenses(<<~RUBY)
104+
class MyModel < ApplicationRecord
105+
belongs_to :user # comment
106+
has_many :posts # comment
107+
has_and_belongs_to_many :tags # comment
108+
has_one :account # comment
109+
end
110+
RUBY
111+
end
112+
end
113+
end

0 commit comments

Comments
 (0)