Skip to content

Commit 0e06a0e

Browse files
committed
[Fix #642] Add new Rails/TransactionExitStatement cop
1 parent da9d225 commit 0e06a0e

File tree

6 files changed

+131
-0
lines changed

6 files changed

+131
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,3 +539,4 @@
539539
[@tachyons]: https://github.com/tachyons
540540
[@composerinteralia]: https://github.com/composerinteralia
541541
[@agrobbin]: https://github.com/agrobbin
542+
[@teckwan]: https://github.com/teckwan
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#642](https://github.com/rubocop/rubocop-rails/issues/642): New cop `Rails/TransactionExitStatement` to disallow `return`, `break` and `throw` in transactions. ([@teckwan][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,11 @@ Rails/TimeZoneAssignment:
875875
- spec/**/*.rb
876876
- test/**/*.rb
877877

878+
Rails/TransactionExitStatement:
879+
Description: 'Avoid the usage of `return`, `break` and `throw` in transaction blocks.'
880+
Enabled: pending
881+
VersionAdded: '<<next>>'
882+
878883
Rails/UniqBeforePluck:
879884
Description: 'Prefer the use of uniq or distinct before pluck.'
880885
Enabled: true
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# This cop checks for the use of exit statements (namely `return`,
7+
# `break` and `throw`) in transactions. This is due to the eventual
8+
# unexpected behavior when using ActiveRecord >= 7, where transactions
9+
# exitted using these statements are being rollbacked rather than
10+
# committed (pre ActiveRecord 7 behavior).
11+
#
12+
# As alternatives, it would be more intuitive to explicitly raise an
13+
# error when rollback is desired, and to use `next` when commit is
14+
# desired.
15+
#
16+
# @example
17+
# # bad
18+
# ApplicationRecord.transaction do
19+
# return if user.active?
20+
# end
21+
#
22+
# # bad
23+
# ApplicationRecord.transaction do
24+
# break if user.active?
25+
# end
26+
#
27+
# # bad
28+
# ApplicationRecord.transaction do
29+
# throw if user.active?
30+
# end
31+
#
32+
# # good
33+
# ApplicationRecord.transaction do
34+
# # Rollback
35+
# raise "User is active" if user.active?
36+
# end
37+
#
38+
# # good
39+
# ApplicationRecord.transaction do
40+
# # Commit
41+
# next if user.active?
42+
# end
43+
#
44+
# @see https://github.com/rails/rails/commit/15aa4200e083
45+
class TransactionExitStatement < Base
46+
MSG = <<~MSG.chomp
47+
Exit statement `%<statement>s` is not allowed. Use `raise` (rollback) or `next` (commit).
48+
MSG
49+
50+
RESTRICT_ON_SEND = %i[transaction].freeze
51+
52+
def_node_search :exit_statements, <<~PATTERN
53+
({return | break | send nil? :throw} ...)
54+
PATTERN
55+
56+
def on_send(node)
57+
parent = node.parent
58+
59+
return unless parent&.block_type?
60+
61+
exit_statements(parent.body).each do |statement_node|
62+
statement = if statement_node.return_type?
63+
'return'
64+
elsif statement_node.break_type?
65+
'break'
66+
else
67+
statement_node.method_name
68+
end
69+
message = format(MSG, statement: statement)
70+
71+
add_offense(statement_node, message: message)
72+
end
73+
end
74+
end
75+
end
76+
end
77+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
require_relative 'rails/squished_sql_heredocs'
104104
require_relative 'rails/time_zone'
105105
require_relative 'rails/time_zone_assignment'
106+
require_relative 'rails/transaction_exit_statement'
106107
require_relative 'rails/uniq_before_pluck'
107108
require_relative 'rails/unique_validation_without_index'
108109
require_relative 'rails/unknown_env'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::TransactionExitStatement, :config do
4+
it 'registers an offense when `return` is used in transactions' do
5+
expect_offense(<<~RUBY)
6+
ApplicationRecord.transaction do
7+
return if user.active?
8+
^^^^^^ Exit statement `return` is not allowed. Use `raise` (rollback) or `next` (commit).
9+
end
10+
RUBY
11+
end
12+
13+
it 'registers an offense when `break` is used in transactions' do
14+
expect_offense(<<~RUBY)
15+
ApplicationRecord.transaction do
16+
break if user.active?
17+
^^^^^ Exit statement `break` is not allowed. Use `raise` (rollback) or `next` (commit).
18+
end
19+
RUBY
20+
end
21+
22+
it 'registers an offense when `throw` is used in transactions' do
23+
expect_offense(<<~RUBY)
24+
ApplicationRecord.transaction do
25+
throw if user.active?
26+
^^^^^ Exit statement `throw` is not allowed. Use `raise` (rollback) or `next` (commit).
27+
end
28+
RUBY
29+
end
30+
31+
it 'does not register an offense when `next` is used in transactions' do
32+
expect_no_offenses(<<~RUBY)
33+
ApplicationRecord.transaction do
34+
next if user.active?
35+
end
36+
RUBY
37+
end
38+
39+
it 'does not register an offense when `raise` is used in transactions' do
40+
expect_no_offenses(<<~RUBY)
41+
ApplicationRecord.transaction do
42+
raise if user.active?
43+
end
44+
RUBY
45+
end
46+
end

0 commit comments

Comments
 (0)