Skip to content

Commit 7d31d25

Browse files
Refine Style/GlobalExceptionVariables.
1 parent 7567114 commit 7d31d25

File tree

2 files changed

+123
-41
lines changed

2 files changed

+123
-41
lines changed

lib/rubocop/socketry/style/global_exception_variables.rb

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
module RuboCop
99
module Socketry
1010
module Style
11-
# A RuboCop cop that warns against using global exception variables.
11+
# A RuboCop cop that warns against using global exception variables in unsafe contexts.
1212
#
1313
# This cop discourages the use of:
1414
# - `$!` (last exception)
@@ -17,26 +17,52 @@ module Style
1717
# - `$ERROR_POSITION` (English name for `$@`)
1818
#
1919
# These global variables are implicit and can make code harder to understand.
20-
# Instead, use explicit exception handling with rescue blocks and local variables.
20+
#
21+
# However, this cop allows their use in safe contexts where the scope is well-defined:
22+
# - Inside rescue blocks (well-defined scope)
23+
# - In rescue modifiers (`expression rescue $!`)
24+
# - In method parameter defaults (`def foo(error = $!)`, `def bar(error: $!)`)
25+
#
26+
# This cop specifically flags their use in unsafe contexts:
27+
# - Inside ensure blocks (extremely unsafe - exception state is unpredictable)
28+
# - Outside of exception handling contexts
2129
#
2230
# @example
23-
# # bad
31+
# # bad - unsafe in ensure block
2432
# begin
2533
# risky_operation
26-
# rescue
34+
# ensure
35+
# log($!.message) if $! # unsafe!
36+
# end
37+
#
38+
# # bad - outside exception handling
39+
# def process
2740
# puts $!.message
28-
2941
# end
3042
#
31-
# # good
43+
# # good - explicit exception handling
3244
# begin
3345
# risky_operation
3446
# rescue => error
3547
# puts error.message
36-
# puts error.backtrace.first
3748
# end
49+
#
50+
# # allowed - inside rescue block (well-defined scope)
51+
# begin
52+
# risky_operation
53+
# rescue
54+
# puts $!.message
55+
# end
56+
#
57+
# # allowed - rescue modifier
58+
# result = risky_operation rescue $!
59+
#
60+
# # allowed - parameter defaults
61+
# def foo(error = $!)
62+
# def bar(error: $!)
3863
class GlobalExceptionVariables < RuboCop::Cop::Base
39-
MSG = "Avoid using global exception variable `%<variable>s`. Use explicit exception handling with `rescue => error` instead."
64+
MSG = "Avoid using global exception variable `%<variable>s` in %<context>s. Use explicit exception handling with `rescue => error` instead."
65+
ENSURE_MSG = "Using global exception variable `%<variable>s` in an ensure block is extremely unsafe."
4066

4167
EXCEPTION_VARIABLES = %i[$! $@ $ERROR_INFO $ERROR_POSITION].freeze
4268

@@ -45,11 +71,53 @@ def on_gvar(node)
4571

4672
return unless EXCEPTION_VARIABLES.include?(variable_name)
4773

74+
# Allow in parameter defaults (explicitly opting in)
75+
return if in_parameter_default?(node)
76+
77+
# Allow in rescue modifier (well-defined scope)
78+
return if in_rescue_modifier?(node)
79+
80+
# Allow in rescue block (well-defined scope)
81+
return if in_rescue_block?(node)
82+
83+
# Flag if in ensure block (extremely unsafe)
84+
if in_ensure_block?(node)
85+
add_offense(
86+
node,
87+
message: format(ENSURE_MSG, variable: variable_name)
88+
)
89+
return
90+
end
91+
92+
# Flag in all other contexts
4893
add_offense(
4994
node,
50-
message: format(MSG, variable: variable_name)
95+
message: format(MSG, variable: variable_name, context: "this context")
5196
)
5297
end
98+
99+
private
100+
101+
def in_parameter_default?(node)
102+
node.each_ancestor(:args, :optarg, :kwoptarg).any?
103+
end
104+
105+
def in_rescue_modifier?(node)
106+
node.each_ancestor(:rescue).any? do |ancestor|
107+
# A rescue modifier has no resbody children
108+
# e.g., `expression rescue $!` is a rescue node with 2 children: expression and handler
109+
# A regular rescue has resbody children
110+
!ancestor.children.any?{|child| child.is_a?(RuboCop::AST::Node) && child.type == :resbody}
111+
end
112+
end
113+
114+
def in_rescue_block?(node)
115+
node.each_ancestor(:resbody).any?
116+
end
117+
118+
def in_ensure_block?(node)
119+
node.each_ancestor(:ensure).any?
120+
end
53121
end
54122
end
55123
end

test/rubocop/socketry/style/global_exception_variables.rb

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
let(:config) {RuboCop::Config.new}
1010
let(:cop) {subject.new(config)}
1111

12-
# Test $! variable
13-
with "code using $!" do
12+
# Test $! variable in rescue block (allowed - well-defined scope)
13+
with "code using $! in rescue block" do
1414
let(:source) do
1515
<<~RUBY
1616
begin
@@ -21,18 +21,17 @@
2121
RUBY
2222
end
2323

24-
it "registers an offense" do
24+
it "does not register an offense" do
2525
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
2626
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
2727
report = investigator.investigate(processed_source)
2828
offenses = report.offenses
29-
expect(offenses).not.to be(:empty?)
30-
expect(offenses.first.message).to be(:include?, "$!")
29+
expect(offenses).to be(:empty?)
3130
end
3231
end
3332

34-
# Test $@ variable
35-
with "code using $@" do
33+
# Test $@ variable in rescue block (allowed - well-defined scope)
34+
with "code using $@ in rescue block" do
3635
let(:source) do
3736
<<~RUBY
3837
begin
@@ -43,59 +42,74 @@
4342
RUBY
4443
end
4544

46-
it "registers an offense" do
45+
it "does not register an offense" do
4746
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
4847
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
4948
report = investigator.investigate(processed_source)
5049
offenses = report.offenses
51-
expect(offenses).not.to be(:empty?)
52-
expect(offenses.first.message).to be(:include?, "$@")
50+
expect(offenses).to be(:empty?)
5351
end
5452
end
5553

56-
# Test $ERROR_INFO variable
57-
with "code using $ERROR_INFO" do
54+
# Test $! in ensure block (extremely unsafe - should be flagged)
55+
with "code using $! in ensure block" do
5856
let(:source) do
5957
<<~RUBY
60-
require 'English'
6158
begin
6259
risky_operation
63-
rescue
64-
puts $ERROR_INFO.message
60+
ensure
61+
log($!.message) if $!
6562
end
6663
RUBY
6764
end
6865

69-
it "registers an offense" do
66+
it "registers offenses with unsafe message" do
7067
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
7168
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
7269
report = investigator.investigate(processed_source)
7370
offenses = report.offenses
74-
expect(offenses).not.to be(:empty?)
75-
expect(offenses.first.message).to be(:include?, "$ERROR_INFO")
71+
expect(offenses.size).to be == 2
72+
expect(offenses.first.message).to be(:include?, "extremely unsafe")
7673
end
7774
end
7875

79-
# Test $ERROR_POSITION variable
80-
with "code using $ERROR_POSITION" do
76+
# Test $! in rescue modifier (allowed)
77+
with "code using $! in rescue modifier" do
8178
let(:source) do
8279
<<~RUBY
83-
require 'English'
84-
begin
85-
risky_operation
86-
rescue
87-
puts $ERROR_POSITION.first
80+
result = risky_operation rescue $!
81+
RUBY
82+
end
83+
84+
it "does not register an offense" do
85+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
86+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
87+
report = investigator.investigate(processed_source)
88+
offenses = report.offenses
89+
expect(offenses).to be(:empty?)
90+
end
91+
end
92+
93+
# Test $! in parameter defaults (allowed - explicitly opting in)
94+
with "code using $! in parameter defaults" do
95+
let(:source) do
96+
<<~RUBY
97+
def foo(error = $!)
98+
puts error
99+
end
100+
101+
def bar(error: $!)
102+
puts error
88103
end
89104
RUBY
90105
end
91106

92-
it "registers an offense" do
107+
it "does not register an offense" do
93108
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
94109
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
95110
report = investigator.investigate(processed_source)
96111
offenses = report.offenses
97-
expect(offenses).not.to be(:empty?)
98-
expect(offenses.first.message).to be(:include?, "$ERROR_POSITION")
112+
expect(offenses).to be(:empty?)
99113
end
100114
end
101115

@@ -143,8 +157,8 @@
143157
end
144158
end
145159

146-
# Test multiple uses in same block
147-
with "code using multiple global exception variables" do
160+
# Test multiple uses in same rescue block (allowed - well-defined scope)
161+
with "code using multiple global exception variables in rescue block" do
148162
let(:source) do
149163
<<~RUBY
150164
begin
@@ -156,12 +170,12 @@
156170
RUBY
157171
end
158172

159-
it "registers multiple offenses" do
173+
it "does not register offenses" do
160174
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
161175
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
162176
report = investigator.investigate(processed_source)
163177
offenses = report.offenses
164-
expect(offenses.size).to be == 2
178+
expect(offenses).to be(:empty?)
165179
end
166180
end
167181

0 commit comments

Comments
 (0)