rubocop-dev_doc 0.1.0 → 0.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +318 -33
  3. data/lib/dev_doc/test/best_practice_lints.rb +31 -0
  4. data/lib/dev_doc/test/lints/cron_schedule.rb +345 -0
  5. data/lib/dev_doc/test/lints/duplicate_snapshot.rb +197 -0
  6. data/lib/dev_doc/test/lints/no_file_excludes.rb +128 -0
  7. data/lib/rubocop/cop/dev_doc/auth/current_user_branching.rb +203 -0
  8. data/lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb +230 -0
  9. data/lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb +92 -0
  10. data/lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb +86 -0
  11. data/lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb +68 -13
  12. data/lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb +89 -0
  13. data/lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb +18 -3
  14. data/lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb +121 -0
  15. data/lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb +53 -0
  16. data/lib/rubocop/cop/dev_doc/migration/require_primary_key.rb +55 -0
  17. data/lib/rubocop/cop/dev_doc/migration/require_timestamps.rb +4 -13
  18. data/lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb +56 -0
  19. data/lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb +135 -0
  20. data/lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb +127 -0
  21. data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +99 -0
  22. data/lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb +83 -0
  23. data/lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb +236 -0
  24. data/lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb +22 -5
  25. data/lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb +137 -0
  26. data/lib/rubocop/cop/dev_doc/route/no_custom_actions.rb +171 -0
  27. data/lib/rubocop/cop/dev_doc/route/resource_name_number.rb +77 -0
  28. data/lib/rubocop/cop/dev_doc/route/resources_require_only.rb +29 -15
  29. data/lib/rubocop/cop/dev_doc/style/avoid_head_response.rb +56 -22
  30. data/lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb +102 -0
  31. data/lib/rubocop/cop/dev_doc/style/avoid_send.rb +42 -10
  32. data/lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb +158 -0
  33. data/lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb +129 -0
  34. data/lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb +150 -0
  35. data/lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb +118 -0
  36. data/lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb +91 -0
  37. data/lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb +53 -0
  38. data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +66 -0
  39. data/lib/rubocop/cop/dev_doc/test/response_assert_equal.rb +179 -0
  40. data/lib/rubocop/dev_doc/version.rb +1 -1
  41. data/lib/rubocop-dev_doc.rb +1 -0
  42. metadata +73 -10
  43. data/lib/rubocop/cop/dev_doc/migration/avoid_update_column.rb +0 -53
@@ -43,18 +43,18 @@ module RuboCop
43
43
  # end
44
44
  # end
45
45
  #
46
- # ## Exception
46
+ # ## Exception (auto-detected)
47
47
  # For performance reasons (large tables with millions of records) or when
48
48
  # using `null: false`, you may temporarily set a default and then
49
- # immediately remove it in the same migration:
49
+ # immediately remove it in the same migration. The cop suppresses the
50
+ # offense when it detects a matching `change_column_default ..., to: nil`
51
+ # for the same table and column anywhere in the same method body (`def
52
+ # change` / `def up`), including inside `reversible do |dir| dir.up`.
50
53
  #
51
- # ✔
54
+ # ✔ (no offense — two-step pattern auto-detected)
52
55
  # add_column :users, :profile_completion_rate, :float, default: 0.0
53
56
  # change_column_default :users, :profile_completion_rate, from: 0.0, to: nil
54
57
  #
55
- # NOTE: This cop currently flags the first line of the exception pattern.
56
- # The follow-up `change_column_default ..., to: nil` is not auto-detected.
57
- #
58
58
  # @example
59
59
  # # bad
60
60
  # add_column :users, :score, :integer, default: 0
@@ -65,7 +65,7 @@ module RuboCop
65
65
  # # good
66
66
  # add_column :users, :score, :integer
67
67
  #
68
- # # good (temporary default immediately removed)
68
+ # # good (temporary default immediately removed — two-step pattern)
69
69
  # add_column :users, :score, :integer, default: 0
70
70
  # change_column_default :users, :score, from: 0, to: nil
71
71
  class AvoidColumnDefault < Base
@@ -80,19 +80,74 @@ module RuboCop
80
80
  (hash <(pair (sym :default) _) ...>)
81
81
  PATTERN
82
82
 
83
+ def_node_matcher :change_column_default_to_nil?, <<~PATTERN
84
+ (send _ :change_column_default (sym $_) (sym $_) (hash <(pair (sym :to) (nil)) ...>))
85
+ PATTERN
86
+
83
87
  def on_send(node)
84
- return unless node.method?(:add_column) || COLUMN_METHODS.include?(node.method_name)
88
+ return unless column_method?(node)
89
+
90
+ options = node.arguments.find(&:hash_type?)
91
+ return unless options && has_default_option?(options)
92
+ return if two_step_pattern?(node)
85
93
 
86
- check_options(node.arguments.find(&:hash_type?))
94
+ add_offense(find_default_pair(options))
87
95
  end
88
96
 
89
97
  private
90
98
 
91
- def check_options(options)
92
- return unless options && has_default_option?(options)
99
+ def column_method?(node)
100
+ node.method?(:add_column) || COLUMN_METHODS.include?(node.method_name)
101
+ end
102
+
103
+ def find_default_pair(options)
104
+ options.pairs.find { |p| p.key.sym_type? && p.key.value == :default }
105
+ end
106
+
107
+ def two_step_pattern?(node)
108
+ table_name, col_name = extract_table_and_column(node)
109
+ return false unless table_name && col_name
110
+
111
+ enclosing_def = node.each_ancestor(:def).first
112
+ return false unless enclosing_def
113
+
114
+ enclosing_def.each_descendant(:send).any? do |sibling|
115
+ cancels_default?(sibling, table_name, col_name)
116
+ end
117
+ end
118
+
119
+ def extract_table_and_column(node)
120
+ node.method?(:add_column) ? add_column_pair(node) : column_def_pair(node)
121
+ end
122
+
123
+ def add_column_pair(node)
124
+ t = node.arguments[0]
125
+ c = node.arguments[1]
126
+ [t.value, c.value] if t&.sym_type? && c&.sym_type?
127
+ end
128
+
129
+ def column_def_pair(node)
130
+ c = node.arguments[0]
131
+ return unless c&.sym_type?
132
+
133
+ t = enclosing_table_sym(node)
134
+ [t.value, c.value] if t
135
+ end
136
+
137
+ def enclosing_table_sym(node)
138
+ table_block = node.each_ancestor(:block).find do |b|
139
+ %i[create_table change_table].include?(b.method_name)
140
+ end
141
+ return unless table_block
142
+
143
+ arg = table_block.send_node.arguments[0]
144
+ arg if arg&.sym_type?
145
+ end
93
146
 
94
- default_pair = options.pairs.find { |p| p.key.sym_type? && p.key.value == :default }
95
- add_offense(default_pair)
147
+ def cancels_default?(node, table_name, col_name)
148
+ change_column_default_to_nil?(node) do |t, c|
149
+ t == table_name && c == col_name
150
+ end
96
151
  end
97
152
  end
98
153
  end
@@ -0,0 +1,89 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Migration
5
+ # Flag conditional schema-change helpers (`add_column_if_not_exists`,
6
+ # `column_exists?`, etc.) inside migration files.
7
+ #
8
+ # ## Rationale
9
+ # Migrations are deterministic state transitions: "DB was at state N;
10
+ # after this migration it is at state N+1." Conditional schema helpers
11
+ # imply "I don't know what state the DB is in," which contradicts the
12
+ # migration model and hides schema drift.
13
+ #
14
+ # If a column "might already exist," that is a symptom — investigate
15
+ # why before papering over it with a defensive guard.
16
+ #
17
+ # The escape hatch is a per-line `rubocop:disable` comment with a
18
+ # rationale explaining the known-drift repair.
19
+ #
20
+ # ❌ hides drift; state is unknown
21
+ # add_column_if_not_exists :users, :something, :string
22
+ # add_column :users, :bar, :string unless column_exists?(:users, :bar)
23
+ #
24
+ # ✔️ declarative state transition
25
+ # add_column :users, :something, :string
26
+ #
27
+ # ✔️ documented one-shot drift repair (escape hatch)
28
+ # # rubocop:disable DevDoc/Migration/AvoidConditionalSchemaChanges
29
+ # add_column_if_not_exists :users, :something, :string
30
+ # # rubocop:enable DevDoc/Migration/AvoidConditionalSchemaChanges
31
+ #
32
+ # @example
33
+ # # bad
34
+ # add_column_if_not_exists :users, :something, :string
35
+ #
36
+ # # bad
37
+ # add_index_if_not_exists :users, :email
38
+ #
39
+ # # bad
40
+ # remove_column_if_exists :users, :legacy
41
+ #
42
+ # # bad (predicate guard shape)
43
+ # add_column :users, :foo, :string unless column_exists?(:users, :foo)
44
+ #
45
+ # # good
46
+ # add_column :users, :something, :string
47
+ class AvoidConditionalSchemaChanges < Base
48
+ IF_NOT_EXISTS_METHODS = %i[
49
+ add_column_if_not_exists
50
+ add_index_if_not_exists
51
+ add_foreign_key_if_not_exists
52
+ add_reference_if_not_exists
53
+ remove_column_if_exists
54
+ remove_index_if_exists
55
+ remove_foreign_key_if_exists
56
+ remove_reference_if_exists
57
+ ].freeze
58
+
59
+ EXISTENCE_PREDICATES = %i[
60
+ column_exists?
61
+ table_exists?
62
+ index_exists?
63
+ foreign_key_exists?
64
+ ].freeze
65
+
66
+ MSG_IF_NOT_EXISTS =
67
+ '`%<method>s` hides schema drift. Use the non-conditional form and investigate ' \
68
+ 'why states diverge. Suppress with a `rubocop:disable` comment only for documented ' \
69
+ 'one-shot drift repairs.'.freeze
70
+
71
+ MSG_PREDICATE =
72
+ '`%<method>s` guard hides schema drift. Use unconditional schema operations and ' \
73
+ 'investigate why states diverge. Suppress with a `rubocop:disable` comment only for ' \
74
+ 'documented one-shot drift repairs.'.freeze
75
+
76
+ def on_send(node)
77
+ if IF_NOT_EXISTS_METHODS.include?(node.method_name)
78
+ add_offense(node.loc.selector,
79
+ message: format(MSG_IF_NOT_EXISTS, method: node.method_name))
80
+ elsif EXISTENCE_PREDICATES.include?(node.method_name)
81
+ add_offense(node.loc.selector,
82
+ message: format(MSG_PREDICATE, method: node.method_name))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -12,27 +12,42 @@ module RuboCop
12
12
  #
13
13
  # ❌
14
14
  # t.json :metadata
15
+ # add_column :users, :metadata, :json
15
16
  #
16
17
  # ✔️
17
18
  # t.jsonb :metadata
19
+ # add_column :users, :metadata, :jsonb
18
20
  #
19
21
  # @example
20
22
  # # bad
21
23
  # t.json :metadata
24
+ # add_column :users, :metadata, :json
22
25
  #
23
26
  # # good
24
27
  # t.jsonb :metadata
28
+ # add_column :users, :metadata, :jsonb
25
29
  class AvoidJsonColumn < Base
26
30
  extend AutoCorrector
27
31
 
28
32
  MSG = 'Use `jsonb` instead of `json`. `jsonb` supports indexing and is faster to read.'.freeze
29
- RESTRICT_ON_SEND = %i[json].freeze
33
+ RESTRICT_ON_SEND = %i[json add_column].freeze
30
34
 
31
35
  def on_send(node)
32
- add_offense(node.loc.selector) do |corrector|
33
- corrector.replace(node.loc.selector, 'jsonb')
36
+ if node.method_name == :add_column
37
+ check_add_column(node)
38
+ else
39
+ add_offense(node.loc.selector) { |c| c.replace(node.loc.selector, 'jsonb') }
34
40
  end
35
41
  end
42
+
43
+ private
44
+
45
+ def check_add_column(node)
46
+ type_arg = node.arguments[2]
47
+ return unless type_arg&.sym_type? && type_arg.value == :json
48
+
49
+ add_offense(type_arg) { |c| c.replace(type_arg, ':jsonb') }
50
+ end
36
51
  end
37
52
  end
38
53
  end
@@ -0,0 +1,121 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Migration
5
+ # Avoid `null: false` on regular columns.
6
+ #
7
+ # ## Rationale
8
+ # `null: false` on a regular column bakes a business rule (presence) into
9
+ # the schema. Presence belongs in the application layer (model
10
+ # validations), where it is easy to change.
11
+ #
12
+ # The test for whether `null: false` is justified is "what would NULL
13
+ # mean for this column?":
14
+ #
15
+ # - If NULL is — or could become — a meaningful business state, presence
16
+ # is a business decision: keep it in the model. `email` NULL = a
17
+ # phone-only user; `organization_id` NULL = an unowned template.
18
+ # - If NULL is never a meaningful state by the nature of the data, it is
19
+ # a data-integrity concern and belongs in the schema (see Exception).
20
+ #
21
+ # The line is drawn for standardization and non-subjectivity. Whether a
22
+ # regular column is "required" is subjective and invites per-column
23
+ # debate (`email` looks required until phone signup makes it optional),
24
+ # so the schema should not bake in that debatable call.
25
+ #
26
+ # ❌ Regular column
27
+ # add_column :users, :profile_completion_rate, :float, null: false
28
+ #
29
+ # ✔️ Regular column
30
+ # add_column :users, :profile_completion_rate, :float
31
+ #
32
+ # ## Exception
33
+ # `null: false` IS the right choice where NULL is never a meaningful
34
+ # state:
35
+ #
36
+ # - **Required foreign keys** — NOT flagged: this cop never looks at
37
+ # `belongs_to`, `references`, or `add_reference`. A required FK bundles
38
+ # two things: `foreign_key: true` is pure referential integrity (never
39
+ # a business decision), while `null: false` on the FK is a
40
+ # *mandatory-ness* decision that can flip (a `document` may later be an
41
+ # unowned template). Both are allowed in the schema pragmatically — the
42
+ # referential-integrity guarantee carries the mandatory-ness with it.
43
+ # - **Enum columns** — NULL is outside the enum's domain (a type
44
+ # violation), so `null: false` is required, and enforced from the model
45
+ # side by `DevDoc/Rails/EnumColumnNotNull`. But an enum is a plain
46
+ # `integer` column, statically indistinguishable from any other
47
+ # integer, so THIS cop cannot detect it and WILL flag it. Disable it on
48
+ # the line with a brief reason — `-- enum` — so the migration is
49
+ # self-documenting: a reader sees at a glance that the column is an enum.
50
+ #
51
+ # ✔️ Required foreign key (never flagged)
52
+ # t.belongs_to :user, null: false, foreign_key: true
53
+ #
54
+ # ✔️ Enum (flagged here — disable with a brief `-- enum` reason)
55
+ # # rubocop:disable DevDoc/Migration/AvoidNonNull -- enum
56
+ # add_column :orders, :status, :integer, null: false
57
+ # # rubocop:enable DevDoc/Migration/AvoidNonNull
58
+ #
59
+ # NOTE: This cop is deliberately NOT enum-aware. It could read the
60
+ # model's `enum` declarations and skip those columns, but requiring an
61
+ # explicit per-line disable is intentional: it forces the developer to
62
+ # signal that the column is an enum, which documents the migration. A
63
+ # silent skip would hide that intent.
64
+ #
65
+ # NOTE: This cop only flags `null: false`. It does not flag `null: true`
66
+ # (redundant but harmless), and it does not require foreign keys to carry
67
+ # `null: false` — adding it to an FK is encouraged but not enforced here.
68
+ #
69
+ # @example
70
+ # # bad
71
+ # add_column :users, :name, :string, null: false
72
+ #
73
+ # # bad (enum without a disable — the cop flags it; disable with `-- enum`)
74
+ # t.integer :processing_status, null: false
75
+ #
76
+ # # good
77
+ # add_column :users, :name, :string
78
+ #
79
+ # # good (required foreign key — never flagged)
80
+ # t.belongs_to :user, null: false, foreign_key: true
81
+ class AvoidNonNull < Base
82
+ MSG = 'Avoid `null: false` on regular columns; enforce presence in the model layer. ' \
83
+ 'If this is an enum column, disable this cop on the line with a brief reason, e.g. `-- enum`.'.freeze
84
+
85
+ # Column-definition helpers that take a `null:` option. Deliberately
86
+ # EXCLUDES `references` / `belongs_to` (and the separate `add_reference`
87
+ # method): a required foreign key SHOULD carry `null: false`, so those
88
+ # are never flagged.
89
+ COLUMN_METHODS = %i[
90
+ string integer float boolean datetime date text binary decimal
91
+ json jsonb bigint
92
+ ].freeze
93
+
94
+ RESTRICT_ON_SEND = (COLUMN_METHODS + %i[add_column]).freeze
95
+
96
+ def_node_matcher :null_false_pair, <<~PATTERN
97
+ (hash <$(pair (sym :null) (false)) ...>)
98
+ PATTERN
99
+
100
+ def on_send(node)
101
+ return unless column_method?(node)
102
+
103
+ options = node.arguments.find(&:hash_type?)
104
+ return unless options
105
+
106
+ pair = null_false_pair(options)
107
+ return unless pair
108
+
109
+ add_offense(pair)
110
+ end
111
+
112
+ private
113
+
114
+ def column_method?(node)
115
+ node.method?(:add_column) || COLUMN_METHODS.include?(node.method_name)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,53 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Migration
5
+ # Avoid `create_join_table` — define an explicit join model instead.
6
+ #
7
+ # ## Rationale
8
+ # `create_join_table` produces a PK-less table tied to
9
+ # `has_and_belongs_to_many`, which has well-known limitations: no
10
+ # timestamps, no per-row callbacks, and no room for extra columns
11
+ # without migration gymnastics.
12
+ #
13
+ # The preferred Rails idiom is `has_many :through` with an explicit
14
+ # join model, which is just a normal table with a PK and timestamps.
15
+ #
16
+ # ❌
17
+ # create_join_table :products, :categories
18
+ #
19
+ # ✔️ Define an explicit join model
20
+ # create_table :product_categories do |t|
21
+ # t.belongs_to :product, null: false, foreign_key: true
22
+ # t.belongs_to :category, null: false, foreign_key: true
23
+ # t.timestamps
24
+ # end
25
+ #
26
+ # # In models:
27
+ # class Product < ApplicationRecord
28
+ # has_many :product_categories
29
+ # has_many :categories, through: :product_categories
30
+ # end
31
+ #
32
+ # @example
33
+ # # bad
34
+ # create_join_table :products, :categories
35
+ #
36
+ # # good
37
+ # create_table :product_categories do |t|
38
+ # t.belongs_to :product, null: false, foreign_key: true
39
+ # t.belongs_to :category, null: false, foreign_key: true
40
+ # t.timestamps
41
+ # end
42
+ class NoCreateJoinTable < Base
43
+ MSG = 'Avoid `create_join_table` — define an explicit join model with `has_many :through` instead.'.freeze
44
+ RESTRICT_ON_SEND = %i[create_join_table].freeze
45
+
46
+ def on_send(node)
47
+ add_offense(node.loc.selector)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Migration
5
+ # Every `create_table` migration must have a primary key.
6
+ #
7
+ # ## Rationale
8
+ # A table without a primary key is hard to reference, hard to debug,
9
+ # and hard to migrate later. `id: false` is rarely the right answer;
10
+ # `has_many :through` with an explicit join model is preferred for
11
+ # many-to-many relationships, and that join model has its own `id`.
12
+ #
13
+ # ❌
14
+ # create_table :products_categories, id: false do |t|
15
+ # t.belongs_to :product
16
+ # t.belongs_to :category
17
+ # end
18
+ #
19
+ # ✔️ Use an explicit join model with has_many :through instead
20
+ # create_table :product_categories do |t|
21
+ # t.belongs_to :product, null: false, foreign_key: true
22
+ # t.belongs_to :category, null: false, foreign_key: true
23
+ # t.timestamps
24
+ # end
25
+ #
26
+ # @example
27
+ # # bad
28
+ # create_table :products_categories, id: false do |t|
29
+ # t.belongs_to :product
30
+ # end
31
+ #
32
+ # # good
33
+ # create_table :product_categories do |t|
34
+ # t.belongs_to :product, null: false, foreign_key: true
35
+ # end
36
+ class RequirePrimaryKey < Base
37
+ MSG = 'Every table must have a primary key. Avoid `id: false` — ' \
38
+ 'use an explicit join model with `has_many :through` instead.'.freeze
39
+ RESTRICT_ON_SEND = %i[create_table].freeze
40
+
41
+ def_node_matcher :id_false_pair?, <<~PATTERN
42
+ (pair (sym :id) (false))
43
+ PATTERN
44
+
45
+ def on_send(node)
46
+ id_false = node.each_descendant(:pair).find { |pair| id_false_pair?(pair) }
47
+ return unless id_false
48
+
49
+ add_offense(id_false)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -10,6 +10,10 @@ module RuboCop
10
10
  # auditing, and ordering records, and adding them later is much more
11
11
  # painful than including them up front.
12
12
  #
13
+ # This rule applies to **every** `create_table` call, including those
14
+ # with `id: false`. If a genuinely timestamp-less table is needed, add
15
+ # an explicit `# rubocop:disable` with a rationale comment.
16
+ #
13
17
  # ❌
14
18
  # create_table :ai_api_failures do |t|
15
19
  # t.belongs_to :ai_chat_message, null: false, foreign_key: true
@@ -24,9 +28,6 @@ module RuboCop
24
28
  # t.timestamps
25
29
  # end
26
30
  #
27
- # NOTE: This cop skips tables declared with `id: false` (typically join
28
- # tables), where omitting timestamps is a deliberate choice.
29
- #
30
31
  # @example
31
32
  # # bad
32
33
  # create_table :users do |t|
@@ -54,13 +55,7 @@ module RuboCop
54
55
  ...)
55
56
  PATTERN
56
57
 
57
- def_node_matcher :id_false_option?, <<~PATTERN
58
- (pair (sym :id) (false))
59
- PATTERN
60
-
61
58
  def on_send(node)
62
- return if skip_table?(node)
63
-
64
59
  block = node.parent
65
60
  return unless block&.block_type?
66
61
  return if timestamps_present?(block.body)
@@ -72,10 +67,6 @@ module RuboCop
72
67
 
73
68
  private
74
69
 
75
- def skip_table?(node)
76
- node.each_descendant(:pair).any? { |pair| id_false_option?(pair) }
77
- end
78
-
79
70
  def timestamps_present?(body)
80
71
  return false if body.nil?
81
72
 
@@ -0,0 +1,56 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Rails
5
+ # Use `ApplicationRecord.transaction` instead of `SomeModel.transaction` outside model files.
6
+ #
7
+ # ## Rationale
8
+ # When using `transaction` outside of a model, `SomeModel.transaction`
9
+ # reads as if there is a meaningful link to that model when there isn't.
10
+ # `ApplicationRecord.transaction` is functionally identical and makes
11
+ # it clear that the transaction has no special relationship to any
12
+ # particular model.
13
+ #
14
+ # ❌ (in a controller or service)
15
+ # Post.transaction do
16
+ # ...
17
+ # end
18
+ #
19
+ # ✔️
20
+ # ApplicationRecord.transaction do
21
+ # ...
22
+ # end
23
+ #
24
+ # @example
25
+ # # bad (in a controller or service)
26
+ # Order.transaction do
27
+ # order.save!
28
+ # end
29
+ #
30
+ # # good
31
+ # ApplicationRecord.transaction do
32
+ # order.save!
33
+ # end
34
+ class ApplicationRecordTransaction < Base
35
+ extend AutoCorrector
36
+
37
+ ALLOWED_RECEIVERS = %w[ApplicationRecord ActiveRecord::Base].freeze
38
+
39
+ MSG = 'Use `ApplicationRecord.transaction` instead of `%<receiver>s.transaction` outside model files.'.freeze
40
+ RESTRICT_ON_SEND = %i[transaction].freeze
41
+
42
+ def on_send(node)
43
+ receiver = node.receiver
44
+ return if receiver.nil?
45
+ return unless receiver.const_type?
46
+ return if ALLOWED_RECEIVERS.include?(receiver.source)
47
+
48
+ add_offense(receiver, message: format(MSG, receiver: receiver.source)) do |corrector|
49
+ corrector.replace(receiver, 'ApplicationRecord')
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end