forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 2
/
shadowed_exception.rb
165 lines (146 loc) · 5.38 KB
/
shadowed_exception.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# Checks for a rescued exception that get shadowed by a
# less specific exception being rescued before a more specific
# exception is rescued.
#
# An exception is considered shadowed if it is rescued after its
# ancestor is, or if it and its ancestor are both rescued in the
# same `rescue` statement. In both cases, the more specific rescue is
# unnecessary because it is covered by rescuing the less specific
# exception. (ie. `rescue Exception, StandardError` has the same behavior
# whether `StandardError` is included or not, because all ``StandardError``s
# are rescued by `rescue Exception`).
#
# @example
#
# # bad
#
# begin
# something
# rescue Exception
# handle_exception
# rescue StandardError
# handle_standard_error
# end
#
# # bad
# begin
# something
# rescue Exception, StandardError
# handle_error
# end
#
# # good
#
# begin
# something
# rescue StandardError
# handle_standard_error
# rescue Exception
# handle_exception
# end
#
# # good, however depending on runtime environment.
# #
# # This is a special case for system call errors.
# # System dependent error code depends on runtime environment.
# # For example, whether `Errno::EAGAIN` and `Errno::EWOULDBLOCK` are
# # the same error code or different error code depends on environment.
# # This good case is for `Errno::EAGAIN` and `Errno::EWOULDBLOCK` with
# # the same error code.
# begin
# something
# rescue Errno::EAGAIN, Errno::EWOULDBLOCK
# handle_standard_error
# end
#
class ShadowedException < Base
include RescueNode
include RangeHelp
MSG = 'Do not shadow rescued Exceptions.'
def on_rescue(node)
return if rescue_modifier?(node)
_body, *rescues, _else = *node
rescued_groups = rescued_groups_for(rescues)
rescue_group_rescues_multiple_levels = rescued_groups.any? do |group|
contains_multiple_levels_of_exceptions?(group)
end
return if !rescue_group_rescues_multiple_levels && sorted?(rescued_groups)
add_offense(offense_range(rescues))
end
private
def offense_range(rescues)
shadowing_rescue = find_shadowing_rescue(rescues)
expression = shadowing_rescue.source_range
range_between(expression.begin_pos, expression.end_pos)
end
def rescued_groups_for(rescues)
rescues.map { |group| evaluate_exceptions(group) }
end
def contains_multiple_levels_of_exceptions?(group)
# Always treat `Exception` as the highest level exception.
return true if group.size > 1 && group.include?(Exception)
group.combination(2).any? { |a, b| compare_exceptions(a, b) }
end
def compare_exceptions(exception, other_exception)
if system_call_err?(exception) && system_call_err?(other_exception)
# This condition logic is for special case.
# System dependent error code depends on runtime environment.
# For example, whether `Errno::EAGAIN` and `Errno::EWOULDBLOCK` are
# the same error code or different error code depends on runtime
# environment. This checks the error code for that.
exception.const_get(:Errno) != other_exception.const_get(:Errno) &&
exception <=> other_exception
else
exception && other_exception && exception <=> other_exception
end
end
def system_call_err?(error)
error && error.ancestors[1] == SystemCallError
end
def evaluate_exceptions(group)
rescued_exceptions = group.exceptions
if rescued_exceptions.any?
rescued_exceptions.each_with_object([]) do |exception, converted|
RuboCop::Util.silence_warnings do
# Avoid printing deprecation warnings about constants
converted << Kernel.const_get(exception.source)
end
rescue NameError
converted << nil
end
else
# treat an empty `rescue` as `rescue StandardError`
[StandardError]
end
end
def sorted?(rescued_groups)
rescued_groups.each_cons(2).all? do |x, y|
if x.include?(Exception)
false
elsif y.include?(Exception) ||
# consider sorted if a group is empty or only contains
# `nil`s
x.none? || y.none?
true
else
(x <=> y || 0) <= 0
end
end
end
def find_shadowing_rescue(rescues)
rescued_groups = rescued_groups_for(rescues)
rescued_groups.zip(rescues).each do |group, res|
return res if contains_multiple_levels_of_exceptions?(group)
end
rescued_groups.each_cons(2).with_index do |group_pair, i|
return rescues[i] unless sorted?(group_pair)
end
end
end
end
end
end