forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 2
/
shadowed_exception.rb
171 lines (150 loc) · 5.39 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
166
167
168
169
170
171
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# This cop checks for a rescued exception that get shadowed by a
# less specific exception being rescued before a more specific
# exception is rescued.
#
# @example
#
# # bad
#
# begin
# something
# rescue Exception
# handle_exception
# rescue StandardError
# handle_standard_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.loc.expression
range_between(expression.begin_pos, expression.end_pos)
end
def rescued_groups_for(rescues)
rescues.map do |group|
evaluate_exceptions(group)
end
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? do |a, b|
compare_exceptions(a, b)
end
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|
# FIXME: Workaround `rubocop:disable` comment for JRuby.
# https://github.com/jruby/jruby/issues/6642
# rubocop:disable Style/RedundantBegin
begin
RuboCop::Util.silence_warnings do
# Avoid printing deprecation warnings about constants
converted << Kernel.const_get(exception.source)
end
rescue NameError
converted << nil
end
# rubocop:enable Style/RedundantBegin
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
# @param [RuboCop::AST::Node] rescue_group is a node of array_type
def rescued_exceptions(rescue_group)
klasses = *rescue_group
klasses.map do |klass|
next unless klass.const_type?
klass.source
end.compact
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