rubocop 1.88.2 → 1.89.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.
- checksums.yaml +4 -4
- data/config/default.yml +67 -11
- data/lib/rubocop/comment_config.rb +35 -19
- data/lib/rubocop/config.rb +53 -0
- data/lib/rubocop/config_validator.rb +38 -1
- data/lib/rubocop/cop/badge.rb +8 -0
- data/lib/rubocop/cop/base.rb +6 -4
- data/lib/rubocop/cop/bundler.rb +19 -0
- data/lib/rubocop/cop/corrector.rb +1 -1
- data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
- data/lib/rubocop/cop/gemspec.rb +22 -0
- data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
- data/lib/rubocop/cop/generator.rb +4 -0
- data/lib/rubocop/cop/layout/class_structure.rb +8 -2
- data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
- data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
- data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
- data/lib/rubocop/cop/layout/line_length.rb +85 -12
- data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
- data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
- data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
- data/lib/rubocop/cop/layout.rb +114 -0
- data/lib/rubocop/cop/lazy_loader.rb +41 -0
- data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
- data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
- data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
- data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
- data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
- data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
- data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
- data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
- data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
- data/lib/rubocop/cop/lint/missing_super.rb +31 -1
- data/lib/rubocop/cop/lint/name_typo.rb +223 -0
- data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
- data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
- data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
- data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
- data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
- data/lib/rubocop/cop/lint/void.rb +17 -6
- data/lib/rubocop/cop/lint.rb +176 -0
- data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
- data/lib/rubocop/cop/metrics.rb +31 -0
- data/lib/rubocop/cop/migration/department_name.rb +0 -4
- data/lib/rubocop/cop/migration.rb +13 -0
- data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
- data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
- data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
- data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
- data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
- data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
- data/lib/rubocop/cop/mixin.rb +78 -77
- data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
- data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
- data/lib/rubocop/cop/naming/variable_number.rb +3 -1
- data/lib/rubocop/cop/naming.rb +31 -0
- data/lib/rubocop/cop/offense.rb +10 -6
- data/lib/rubocop/cop/registry.rb +153 -43
- data/lib/rubocop/cop/security.rb +19 -0
- data/lib/rubocop/cop/style/array_intersect.rb +24 -5
- data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
- data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
- data/lib/rubocop/cop/style/copyright.rb +6 -1
- data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
- data/lib/rubocop/cop/style/documentation.rb +47 -0
- data/lib/rubocop/cop/style/endless_method.rb +0 -10
- data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
- data/lib/rubocop/cop/style/lambda.rb +4 -0
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
- data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
- data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
- data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
- data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
- data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
- data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
- data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
- data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
- data/lib/rubocop/cop/style/redundant_format.rb +12 -3
- data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
- data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
- data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
- data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
- data/lib/rubocop/cop/style/static_class.rb +21 -0
- data/lib/rubocop/cop/style.rb +310 -0
- data/lib/rubocop/cop/team.rb +48 -3
- data/lib/rubocop/cop/variable_force/variable.rb +18 -6
- data/lib/rubocop/cops_documentation_generator.rb +1 -1
- data/lib/rubocop/directive_comment.rb +2 -0
- data/lib/rubocop/feature_loader.rb +0 -6
- data/lib/rubocop/lsp/routes.rb +41 -3
- data/lib/rubocop/lsp/runtime.rb +4 -0
- data/lib/rubocop/lsp/server.rb +4 -0
- data/lib/rubocop/lsp/stdin_runner.rb +20 -0
- data/lib/rubocop/pending_cops_reporter.rb +1 -6
- data/lib/rubocop/project_index_loader.rb +16 -0
- data/lib/rubocop/result_cache.rb +6 -4
- data/lib/rubocop/rspec/shared_contexts.rb +14 -2
- data/lib/rubocop/runner.rb +45 -20
- data/lib/rubocop/server/cache.rb +10 -5
- data/lib/rubocop/target_ruby.rb +1 -1
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop.rb +17 -626
- metadata +18 -3
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Lint
|
|
6
|
+
# Checks for calls to methods and references to constants that are documented
|
|
7
|
+
# as deprecated with a YARD `@deprecated` tag.
|
|
8
|
+
#
|
|
9
|
+
# The check is powered by the project-wide index, so it only runs when
|
|
10
|
+
# `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
|
|
11
|
+
# Without the index the cop does nothing.
|
|
12
|
+
#
|
|
13
|
+
# Only references that can be resolved without type inference are checked:
|
|
14
|
+
# constants, method calls without an explicit receiver (or with `self`),
|
|
15
|
+
# which are looked up in the enclosing class or module and its ancestry,
|
|
16
|
+
# and calls whose receiver is a constant, which are looked up in that
|
|
17
|
+
# namespace's singleton class. Calls on arbitrary objects are not checked.
|
|
18
|
+
#
|
|
19
|
+
# References made from a definition that is itself deprecated are allowed,
|
|
20
|
+
# so deprecated implementations can keep calling each other.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # Given a deprecated method and constant:
|
|
24
|
+
# #
|
|
25
|
+
# # class Api
|
|
26
|
+
# # # @deprecated Use `#new_method` instead.
|
|
27
|
+
# # def old_method
|
|
28
|
+
# # end
|
|
29
|
+
# #
|
|
30
|
+
# # # @deprecated
|
|
31
|
+
# # OLD_TIMEOUT = 10
|
|
32
|
+
# # end
|
|
33
|
+
#
|
|
34
|
+
# # bad
|
|
35
|
+
# class Client < Api
|
|
36
|
+
# def call
|
|
37
|
+
# old_method
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# def timeout
|
|
41
|
+
# OLD_TIMEOUT
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # good
|
|
46
|
+
# class Client < Api
|
|
47
|
+
# def call
|
|
48
|
+
# new_method
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# def timeout
|
|
52
|
+
# NEW_TIMEOUT
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
class DeprecatedReference < Base
|
|
57
|
+
include ProjectIndexHelp
|
|
58
|
+
|
|
59
|
+
METHOD_MSG = 'Method `%<name>s` is deprecated%<detail>s'
|
|
60
|
+
CONSTANT_MSG = 'Constant `%<name>s` is deprecated%<detail>s'
|
|
61
|
+
|
|
62
|
+
DEPRECATED_TAG = '@deprecated'
|
|
63
|
+
DEPRECATION_DETAIL = /#{DEPRECATED_TAG}\s+(.+)/.freeze
|
|
64
|
+
|
|
65
|
+
def on_new_investigation
|
|
66
|
+
@namespace_cache = {}
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_send(node)
|
|
71
|
+
return unless project_index
|
|
72
|
+
|
|
73
|
+
declaration = method_declaration_for(node)
|
|
74
|
+
return unless declaration && deprecated?(declaration)
|
|
75
|
+
return if within_deprecated_definition?(node)
|
|
76
|
+
|
|
77
|
+
message = format(METHOD_MSG, name: node.method_name, detail: detail_for(declaration))
|
|
78
|
+
add_offense(node.loc.selector, message: message)
|
|
79
|
+
end
|
|
80
|
+
alias on_csend on_send
|
|
81
|
+
|
|
82
|
+
def on_const(node)
|
|
83
|
+
return unless project_index
|
|
84
|
+
return if node.parent&.defined_module
|
|
85
|
+
|
|
86
|
+
declaration = resolve_constant_in_index(node)
|
|
87
|
+
return unless declaration && deprecated?(declaration)
|
|
88
|
+
return if within_deprecated_definition?(node)
|
|
89
|
+
|
|
90
|
+
message = format(CONSTANT_MSG, name: node.const_name, detail: detail_for(declaration))
|
|
91
|
+
add_offense(node, message: message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def method_declaration_for(node)
|
|
97
|
+
receiver = node.receiver
|
|
98
|
+
|
|
99
|
+
if receiver.nil? || receiver.self_type?
|
|
100
|
+
implicit_receiver_declaration(node)
|
|
101
|
+
elsif receiver.const_type?
|
|
102
|
+
const_receiver_declaration(node, receiver)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def implicit_receiver_declaration(node)
|
|
107
|
+
namespace = enclosing_namespace(node)
|
|
108
|
+
return nil unless namespace.is_a?(Rubydex::Namespace)
|
|
109
|
+
|
|
110
|
+
if singleton_context?(node)
|
|
111
|
+
indexed_singleton_member(namespace, "#{node.method_name}()")
|
|
112
|
+
else
|
|
113
|
+
namespace.find_member("#{node.method_name}()")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def const_receiver_declaration(node, receiver)
|
|
118
|
+
namespace = resolve_constant_in_index(receiver)
|
|
119
|
+
return nil unless namespace.is_a?(Rubydex::Namespace)
|
|
120
|
+
|
|
121
|
+
indexed_singleton_member(namespace, "#{node.method_name}()")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def enclosing_namespace(node)
|
|
125
|
+
namespace_node = node.each_ancestor(:class, :module).first
|
|
126
|
+
return project_index['Object'] unless namespace_node
|
|
127
|
+
|
|
128
|
+
@namespace_cache.fetch(namespace_node) do
|
|
129
|
+
@namespace_cache[namespace_node] = resolve_constant_in_index(namespace_node.identifier)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Whether an implicit-receiver call runs with the class or module itself
|
|
134
|
+
# as `self` (directly in a namespace body, in a singleton method, or
|
|
135
|
+
# inside `class << self`) rather than inside an instance method.
|
|
136
|
+
def singleton_context?(node)
|
|
137
|
+
instance_def_seen = false
|
|
138
|
+
|
|
139
|
+
node.each_ancestor do |ancestor|
|
|
140
|
+
case ancestor.type
|
|
141
|
+
when :defs, :sclass
|
|
142
|
+
return true
|
|
143
|
+
when :def
|
|
144
|
+
instance_def_seen = true
|
|
145
|
+
when :class, :module
|
|
146
|
+
return !instance_def_seen
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def deprecated?(declaration)
|
|
154
|
+
definitions = declaration.definitions.to_a
|
|
155
|
+
|
|
156
|
+
definitions.any? && definitions.all?(&:deprecated?)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# References from a definition that is itself documented as deprecated
|
|
160
|
+
# are allowed.
|
|
161
|
+
def within_deprecated_definition?(node)
|
|
162
|
+
node.each_ancestor(:any_def, :class, :module).any? do |ancestor|
|
|
163
|
+
comments = processed_source.ast_with_comments[ancestor]
|
|
164
|
+
comments.any? { |comment| comment.text.include?(DEPRECATED_TAG) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def detail_for(declaration)
|
|
169
|
+
text = deprecation_text(declaration)
|
|
170
|
+
return '.' if text.nil? || text.empty?
|
|
171
|
+
|
|
172
|
+
text.end_with?('.') ? ": #{text}" : ": #{text}."
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def deprecation_text(declaration)
|
|
176
|
+
tag_comment = declaration.definitions.to_a.filter_map do |definition|
|
|
177
|
+
definition.comments.to_a.map(&:string).find { |string| string.include?(DEPRECATED_TAG) }
|
|
178
|
+
end.first
|
|
179
|
+
|
|
180
|
+
tag_comment && tag_comment[DEPRECATION_DETAIL, 1]&.strip
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -10,6 +10,17 @@ module RuboCop
|
|
|
10
10
|
# the developer intends to suppress Ruby's method redefinition warnings.
|
|
11
11
|
# See https://bugs.ruby-lang.org/issues/13574.
|
|
12
12
|
#
|
|
13
|
+
# By default the cop can only detect duplicates within a single file.
|
|
14
|
+
# When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
|
|
15
|
+
# the cop additionally consults the project-wide index and reports methods
|
|
16
|
+
# whose duplicate definition lives in another file.
|
|
17
|
+
#
|
|
18
|
+
# NOTE: The project index does not record whether a definition in another
|
|
19
|
+
# file is wrapped in a conditional, so a platform-specific redefinition in
|
|
20
|
+
# another file may still be reported. Aliasing the method to itself (see above)
|
|
21
|
+
# before redefining marks the redefinition as intentional and is respected
|
|
22
|
+
# across files.
|
|
23
|
+
#
|
|
13
24
|
# @example
|
|
14
25
|
#
|
|
15
26
|
# # bad
|
|
@@ -118,16 +129,45 @@ module RuboCop
|
|
|
118
129
|
# delegate :foo, to: :bar
|
|
119
130
|
# end
|
|
120
131
|
#
|
|
132
|
+
# @example DelegatingMethods: ['delegate', 'expose'] (default: ['delegate'])
|
|
133
|
+
# # A project's own `delegate`-shaped macros can be registered so the
|
|
134
|
+
# # methods they define are recognized (with
|
|
135
|
+
# # `AllCops/ActiveSupportExtensionsEnabled: true`).
|
|
136
|
+
#
|
|
137
|
+
# # bad
|
|
138
|
+
# def foo
|
|
139
|
+
# 1
|
|
140
|
+
# end
|
|
141
|
+
#
|
|
142
|
+
# expose :foo, to: :bar
|
|
143
|
+
#
|
|
121
144
|
class DuplicateMethods < Base # rubocop:disable Metrics/ClassLength
|
|
145
|
+
include ProjectIndexHelp
|
|
146
|
+
|
|
122
147
|
MSG = 'Method `%<method>s` is defined at both %<defined>s and %<current>s.'
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
|
|
149
|
+
# Method names the cop registers that can be looked up in the project index:
|
|
150
|
+
# a fully qualified namespace followed by `#` (instance) or `.` (singleton)
|
|
151
|
+
# and the method name.
|
|
152
|
+
INDEXABLE_METHOD_NAME =
|
|
153
|
+
/\A(?<owner>[A-Z]\w*(?:::[A-Z]\w*)*)(?<separator>[#.])(?<name>[^#.]+)\z/.freeze
|
|
154
|
+
# No `RESTRICT_ON_SEND`: the delegating method names are configurable
|
|
155
|
+
# (`DelegatingMethods`), so `on_send` must see every call. The handler
|
|
156
|
+
# returns quickly for calls that are not method definitions, and
|
|
157
|
+
# benchmarks show no measurable cost over the previous fixed list.
|
|
126
158
|
|
|
127
159
|
def initialize(config = nil, options = nil)
|
|
128
160
|
super
|
|
129
161
|
@definitions = {}
|
|
130
162
|
@scopes = Hash.new { |hash, key| hash[key] = [] }
|
|
163
|
+
@self_aliased = Set.new
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def on_new_investigation
|
|
167
|
+
# The self-alias trick declares an intentional redefinition only within
|
|
168
|
+
# the file that uses it, so the tracked names do not carry over.
|
|
169
|
+
@self_aliased = Set.new
|
|
170
|
+
super
|
|
131
171
|
end
|
|
132
172
|
|
|
133
173
|
def on_def(node)
|
|
@@ -157,7 +197,11 @@ module RuboCop
|
|
|
157
197
|
def on_alias(node)
|
|
158
198
|
name, original_name = method_alias?(node)
|
|
159
199
|
return unless name && original_name
|
|
160
|
-
|
|
200
|
+
|
|
201
|
+
if name == original_name
|
|
202
|
+
track_self_alias(node, name)
|
|
203
|
+
return
|
|
204
|
+
end
|
|
161
205
|
return if node.ancestors.any?(&:if_type?)
|
|
162
206
|
|
|
163
207
|
found_instance_method(node, name)
|
|
@@ -168,9 +212,12 @@ module RuboCop
|
|
|
168
212
|
(send nil? :alias_method (sym $_name) (sym $_original_name))
|
|
169
213
|
PATTERN
|
|
170
214
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
215
|
+
# Matches the argument shape of an Active Support `delegate` call
|
|
216
|
+
# (`delegate :a, :b, to: :target`), regardless of the method name; the
|
|
217
|
+
# name is checked separately against `DelegatingMethods`.
|
|
218
|
+
# @!method delegate_args(node)
|
|
219
|
+
def_node_matcher :delegate_args, <<~PATTERN
|
|
220
|
+
(send nil? _
|
|
174
221
|
({sym str} $_)+
|
|
175
222
|
(hash <(pair (sym :to) {sym str}) ...>)
|
|
176
223
|
)
|
|
@@ -204,17 +251,27 @@ module RuboCop
|
|
|
204
251
|
...)
|
|
205
252
|
PATTERN
|
|
206
253
|
|
|
254
|
+
# @!method class_new_block?(node)
|
|
255
|
+
def_node_matcher :class_new_block?, <<~PATTERN
|
|
256
|
+
(block
|
|
257
|
+
(send (const _ :Class) :new ...)
|
|
258
|
+
...)
|
|
259
|
+
PATTERN
|
|
260
|
+
|
|
207
261
|
def on_send(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
208
262
|
name, original_name = alias_method?(node)
|
|
209
263
|
|
|
210
264
|
if name && original_name
|
|
211
|
-
|
|
265
|
+
if name == original_name
|
|
266
|
+
track_self_alias(node, name)
|
|
267
|
+
return
|
|
268
|
+
end
|
|
212
269
|
return if inside_condition?(node)
|
|
213
270
|
|
|
214
271
|
found_instance_method(node, name)
|
|
215
272
|
elsif (attr = node.attribute_accessor?)
|
|
216
273
|
on_attr(node, *attr)
|
|
217
|
-
elsif
|
|
274
|
+
elsif delegating_method?(node) && (names = delegate_args(node))
|
|
218
275
|
return if inside_condition?(node)
|
|
219
276
|
|
|
220
277
|
on_delegate(node, names)
|
|
@@ -257,6 +314,19 @@ module RuboCop
|
|
|
257
314
|
current: source_location(node))
|
|
258
315
|
end
|
|
259
316
|
|
|
317
|
+
# Recognizing a delegating call as a method definition is Active
|
|
318
|
+
# Support behavior, so it stays gated on `ActiveSupportExtensionsEnabled`
|
|
319
|
+
# as it was when only `delegate` was supported. `DelegatingMethods` lets
|
|
320
|
+
# a project register its own `delegate`-shaped macros.
|
|
321
|
+
def delegating_method?(node)
|
|
322
|
+
active_support_extensions_enabled? &&
|
|
323
|
+
delegating_methods.include?(node.method_name.to_s)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def delegating_methods
|
|
327
|
+
cop_config.fetch('DelegatingMethods', ['delegate'])
|
|
328
|
+
end
|
|
329
|
+
|
|
260
330
|
def on_delegate(node, method_names)
|
|
261
331
|
name_prefix = delegate_prefix(node)
|
|
262
332
|
|
|
@@ -318,13 +388,27 @@ module RuboCop
|
|
|
318
388
|
parent = anon_block.parent
|
|
319
389
|
return unless parent&.type?(:any_block, :begin, :call, :casgn, :any_def)
|
|
320
390
|
|
|
321
|
-
if (receiver =
|
|
391
|
+
if (receiver = scope_receiver(parent, anon_block))
|
|
322
392
|
"#{receiver.source}.#{parent.method_name}"
|
|
323
393
|
elsif !parent.begin_type? || parent.parent&.any_block_type?
|
|
324
|
-
|
|
394
|
+
anon_block_identity(anon_block)
|
|
325
395
|
end
|
|
326
396
|
end
|
|
327
397
|
|
|
398
|
+
# When a Class.new block is passed as an argument to a named-receiver
|
|
399
|
+
# method call (e.g. T.cast(Class.new(Base) do ... end, ...)), the
|
|
400
|
+
# receiver-based scope id (e.g. "T.cast") is the same for every call,
|
|
401
|
+
# causing false positives for methods defined in distinct anonymous
|
|
402
|
+
# classes. Return nil so the block falls through to the unique
|
|
403
|
+
# source-location-based scope id.
|
|
404
|
+
# Module.new blocks are excluded because they may be intentionally
|
|
405
|
+
# mixed into the same target via prepend/include/extend.
|
|
406
|
+
def scope_receiver(parent, anon_block)
|
|
407
|
+
return if class_new_block?(anon_block) && parent.call_type?
|
|
408
|
+
|
|
409
|
+
named_receiver(parent)
|
|
410
|
+
end
|
|
411
|
+
|
|
328
412
|
def named_receiver(node)
|
|
329
413
|
receiver = node.receiver
|
|
330
414
|
receiver unless class_or_module_new_block?(receiver)
|
|
@@ -340,27 +424,45 @@ module RuboCop
|
|
|
340
424
|
found_method(node, "#{singleton_receiver_node.method_name}.#{name}")
|
|
341
425
|
end
|
|
342
426
|
|
|
343
|
-
# rubocop:disable Metrics/AbcSize
|
|
344
427
|
def found_method(node, method_name, scope_id: nil)
|
|
345
428
|
key = method_key(node, method_name)
|
|
346
429
|
key = "#{key}@#{scope_id}" if scope_id
|
|
347
430
|
scope = node.each_ancestor(:rescue, :ensure).first&.type
|
|
348
431
|
|
|
349
432
|
if @definitions.key?(key)
|
|
350
|
-
|
|
351
|
-
@definitions[key] = node
|
|
352
|
-
@scopes[scope] << key
|
|
353
|
-
return
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
message = message_for_dup(node, method_name, key)
|
|
357
|
-
|
|
358
|
-
add_offense(location(node), message: message)
|
|
433
|
+
found_redefinition(node, method_name, key, scope)
|
|
359
434
|
else
|
|
360
435
|
@definitions[key] = node
|
|
436
|
+
check_cross_file_duplicate(node, method_name) if scope_id.nil? && scope.nil?
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def found_redefinition(node, method_name, key, scope)
|
|
441
|
+
if scope && !@scopes[scope].include?(key)
|
|
442
|
+
@definitions[key] = node
|
|
443
|
+
@scopes[scope] << key
|
|
444
|
+
elsif intentional_cross_file_redefinition?(node, method_name, key)
|
|
445
|
+
@definitions[key] = node
|
|
446
|
+
else
|
|
447
|
+
add_offense(location(node), message: message_for_dup(node, method_name, key))
|
|
361
448
|
end
|
|
362
449
|
end
|
|
363
|
-
|
|
450
|
+
|
|
451
|
+
# The self-alias trick (`alias foo foo` or `alias_method :foo, :foo` right before
|
|
452
|
+
# a `def`) suppresses Ruby's method redefinition warning, signaling an intentional
|
|
453
|
+
# redefinition of a method defined in another file.
|
|
454
|
+
def intentional_cross_file_redefinition?(node, method_name, key)
|
|
455
|
+
@self_aliased.include?(method_name) &&
|
|
456
|
+
@definitions[key].source_range.source_buffer.name !=
|
|
457
|
+
node.source_range.source_buffer.name
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def track_self_alias(node, name)
|
|
461
|
+
scope = node.parent_module_name
|
|
462
|
+
return unless scope
|
|
463
|
+
|
|
464
|
+
@self_aliased << "#{humanize_scope(scope)}#{name}"
|
|
465
|
+
end
|
|
364
466
|
|
|
365
467
|
def method_key(node, method_name)
|
|
366
468
|
if (ancestor_def = node.each_ancestor(:any_def).first)
|
|
@@ -439,6 +541,84 @@ module RuboCop
|
|
|
439
541
|
path = smart_path(range.source_buffer.name)
|
|
440
542
|
"#{path}:#{range.line}"
|
|
441
543
|
end
|
|
544
|
+
|
|
545
|
+
def check_cross_file_duplicate(node, method_name)
|
|
546
|
+
return unless project_index
|
|
547
|
+
return if @self_aliased.include?(method_name)
|
|
548
|
+
return if node.each_ancestor(:any_def).any?
|
|
549
|
+
return unless (prior = cross_file_prior_definition(method_name))
|
|
550
|
+
|
|
551
|
+
message = format(MSG, method: method_name,
|
|
552
|
+
defined: index_source_location(prior),
|
|
553
|
+
current: source_location(node))
|
|
554
|
+
add_offense(location(node), message: message)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def cross_file_prior_definition(method_name)
|
|
558
|
+
return unless (match = INDEXABLE_METHOD_NAME.match(method_name))
|
|
559
|
+
|
|
560
|
+
definitions = definitions_in_other_files(
|
|
561
|
+
indexed_definitions(match[:owner], match[:separator], match[:name])
|
|
562
|
+
)
|
|
563
|
+
return if definitions.empty? || cross_file_self_alias_trick?(definitions)
|
|
564
|
+
|
|
565
|
+
first_indexed_definition(definitions)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def indexed_definitions(owner, separator, name)
|
|
569
|
+
namespace = separator == '.' ? "#{owner}::<#{owner.split('::').last}>" : owner
|
|
570
|
+
|
|
571
|
+
if name.match?(/\A\w+=\z/)
|
|
572
|
+
# Rubydex indexes `attr_writer :foo` under `foo` rather than `foo=`, so
|
|
573
|
+
# writer definitions come from both the `foo=` and the `foo` declarations.
|
|
574
|
+
indexed_declaration_definitions(namespace, name) +
|
|
575
|
+
indexed_declaration_definitions(namespace, name.delete_suffix('='))
|
|
576
|
+
.select { |definition| writer_attr_definition?(definition) }
|
|
577
|
+
else
|
|
578
|
+
indexed_declaration_definitions(namespace, name).grep_v(Rubydex::AttrWriterDefinition)
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def indexed_declaration_definitions(namespace, name)
|
|
583
|
+
project_index["#{namespace}##{name}()"]&.definitions.to_a
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def writer_attr_definition?(definition)
|
|
587
|
+
definition.is_a?(Rubydex::AttrWriterDefinition) ||
|
|
588
|
+
definition.is_a?(Rubydex::AttrAccessorDefinition)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# An alias of the method alongside one of its definitions in another file may be
|
|
592
|
+
# the self-alias trick marking an intentional redefinition there, so no offense
|
|
593
|
+
# is registered. A genuine `alias` duplicate is still reported when the alias
|
|
594
|
+
# itself is inspected.
|
|
595
|
+
def cross_file_self_alias_trick?(definitions)
|
|
596
|
+
aliases, others = definitions.partition do |definition|
|
|
597
|
+
definition.is_a?(Rubydex::MethodAliasDefinition)
|
|
598
|
+
end
|
|
599
|
+
alias_paths = aliases.map { |definition| definition.location.to_file_path }
|
|
600
|
+
|
|
601
|
+
others.any? { |definition| alias_paths.include?(definition.location.to_file_path) }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def first_indexed_definition(definitions)
|
|
605
|
+
definitions.find { |definition| !definition.is_a?(Rubydex::MethodAliasDefinition) }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def index_source_location(definition)
|
|
609
|
+
location = definition.location
|
|
610
|
+
"#{smart_path(location.to_file_path)}:#{location.to_display.start_line}"
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Internal identity for an anonymous block, used as a scope key.
|
|
614
|
+
# Includes the source range's begin position to distinguish blocks
|
|
615
|
+
# that share the same line (e.g. two Class.new calls separated by `;`).
|
|
616
|
+
# The user-facing offense message still uses `source_location`, which
|
|
617
|
+
# shows only `path:line`.
|
|
618
|
+
def anon_block_identity(anon_block)
|
|
619
|
+
range = anon_block.source_range
|
|
620
|
+
"#{smart_path(range.source_buffer.name)}:#{range.line}:#{range.begin_pos}"
|
|
621
|
+
end
|
|
442
622
|
end
|
|
443
623
|
end
|
|
444
624
|
end
|
|
@@ -12,6 +12,11 @@ module RuboCop
|
|
|
12
12
|
# exception class handle `StandardError` and its subclasses,
|
|
13
13
|
# but not `Exception` and its subclasses.
|
|
14
14
|
#
|
|
15
|
+
# When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
|
|
16
|
+
# installed, indirect inheritance is also detected: a class whose parent
|
|
17
|
+
# (defined anywhere in the project) ultimately inherits from `Exception`
|
|
18
|
+
# is reported, without autocorrection.
|
|
19
|
+
#
|
|
15
20
|
# @example EnforcedStyle: standard_error (default)
|
|
16
21
|
# # bad
|
|
17
22
|
#
|
|
@@ -39,9 +44,11 @@ module RuboCop
|
|
|
39
44
|
# C = Class.new(RuntimeError)
|
|
40
45
|
class InheritException < Base
|
|
41
46
|
include ConfigurableEnforcedStyle
|
|
47
|
+
include ProjectIndexHelp
|
|
42
48
|
extend AutoCorrector
|
|
43
49
|
|
|
44
50
|
MSG = 'Inherit from `%<prefer>s` instead of `Exception`.'
|
|
51
|
+
INDIRECT_MSG = 'Inherit from `%<prefer>s` instead of `Exception` (inherited via `%<via>s`).'
|
|
45
52
|
PREFERRED_BASE_CLASS = {
|
|
46
53
|
runtime_error: 'RuntimeError',
|
|
47
54
|
standard_error: 'StandardError'
|
|
@@ -57,13 +64,20 @@ module RuboCop
|
|
|
57
64
|
PATTERN
|
|
58
65
|
|
|
59
66
|
def on_class(node)
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
parent_class = node.parent_class
|
|
68
|
+
return unless parent_class
|
|
69
|
+
|
|
70
|
+
if exception_class?(parent_class)
|
|
71
|
+
return if inherit_exception_class_with_omitted_namespace?(node)
|
|
72
|
+
|
|
73
|
+
add_offense(parent_class, message: message(parent_class)) do |corrector|
|
|
74
|
+
corrector.replace(parent_class, preferred_base_class)
|
|
75
|
+
end
|
|
76
|
+
elsif (via = inherits_exception_via(parent_class))
|
|
77
|
+
# No autocorrection: the `Exception` inheritance lives at another
|
|
78
|
+
# class' definition site, possibly in another file.
|
|
79
|
+
message = format(INDIRECT_MSG, prefer: preferred_base_class, via: via)
|
|
80
|
+
add_offense(parent_class, message: message)
|
|
67
81
|
end
|
|
68
82
|
end
|
|
69
83
|
|
|
@@ -88,6 +102,38 @@ module RuboCop
|
|
|
88
102
|
class_node.const_name == 'Exception'
|
|
89
103
|
end
|
|
90
104
|
|
|
105
|
+
# When `AllCops/UseProjectIndex` is enabled, indirect inheritance is
|
|
106
|
+
# detected by walking the parent's indexed ancestry: `Exception` itself
|
|
107
|
+
# is not indexed, so a chain ending in it shows up as an ancestor whose
|
|
108
|
+
# superclass reference is unresolved and literally named `Exception`.
|
|
109
|
+
# Returns the name of that ancestor, or `nil`.
|
|
110
|
+
def inherits_exception_via(parent_class)
|
|
111
|
+
return nil unless project_index && parent_class.const_type?
|
|
112
|
+
|
|
113
|
+
declaration = resolve_constant_in_index(parent_class)
|
|
114
|
+
return nil unless declaration.is_a?(Rubydex::Class)
|
|
115
|
+
|
|
116
|
+
exception_ancestor(declaration)&.name
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def exception_ancestor(declaration)
|
|
122
|
+
declaration.ancestors.find do |ancestor|
|
|
123
|
+
ancestor.is_a?(Rubydex::Class) && exception_superclass_reference?(ancestor)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def exception_superclass_reference?(ancestor)
|
|
128
|
+
ancestor.definitions.any? do |definition|
|
|
129
|
+
next false unless definition.is_a?(Rubydex::ClassDefinition)
|
|
130
|
+
|
|
131
|
+
superclass = definition.superclass
|
|
132
|
+
superclass.is_a?(Rubydex::UnresolvedConstantReference) &&
|
|
133
|
+
superclass.name.delete_prefix('::') == 'Exception'
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
91
137
|
def inherit_exception_class_with_omitted_namespace?(class_node)
|
|
92
138
|
return false if class_node.parent_class.namespace&.cbase_type?
|
|
93
139
|
|
|
@@ -254,6 +254,11 @@ module RuboCop
|
|
|
254
254
|
def correct_if_node(node, cond)
|
|
255
255
|
result = condition_evaluation?(node, cond)
|
|
256
256
|
|
|
257
|
+
# When the branch that survives the literal condition is missing,
|
|
258
|
+
# there is no meaningful correction, so no offense is registered.
|
|
259
|
+
surviving_branch = result ? node.if_branch : node.else_branch
|
|
260
|
+
return if surviving_branch.nil? && (node.elsif? || node.else?)
|
|
261
|
+
|
|
257
262
|
new_node = if node.elsif? && result
|
|
258
263
|
"else\n #{range_with_comments(node.if_branch).source}"
|
|
259
264
|
elsif node.elsif? && !result
|
|
@@ -163,7 +163,7 @@ module RuboCop
|
|
|
163
163
|
when :float
|
|
164
164
|
node.children.last.to_f.to_s
|
|
165
165
|
when :str
|
|
166
|
-
|
|
166
|
+
escape_string_content(node.value.inspect)
|
|
167
167
|
when :sym
|
|
168
168
|
autocorrected_value_in_hash_for_symbol(node)
|
|
169
169
|
when :array
|
|
@@ -21,6 +21,13 @@ module RuboCop
|
|
|
21
21
|
# `AllowedParentClasses` option to specify which classes should be allowed
|
|
22
22
|
# *in addition to* `Object` and `BasicObject`.
|
|
23
23
|
#
|
|
24
|
+
# When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
|
|
25
|
+
# the constructor check additionally consults the project-wide index: if the
|
|
26
|
+
# class' entire ancestry is resolvable and no ancestor defines `initialize`,
|
|
27
|
+
# no offense is registered, since `super` would only reach the no-op
|
|
28
|
+
# `Object#initialize`. Classes whose ancestry contains an unresolvable
|
|
29
|
+
# superclass or mixin (e.g. one defined in a gem) are still reported.
|
|
30
|
+
#
|
|
24
31
|
# @example
|
|
25
32
|
# # bad
|
|
26
33
|
# class Employee < Person
|
|
@@ -83,6 +90,8 @@ module RuboCop
|
|
|
83
90
|
# end
|
|
84
91
|
#
|
|
85
92
|
class MissingSuper < Base
|
|
93
|
+
include ProjectIndexHelp
|
|
94
|
+
|
|
86
95
|
CONSTRUCTOR_MSG = 'Call `super` to initialize state of the parent class.'
|
|
87
96
|
CALLBACK_MSG = 'Call `super` to invoke callback defined in the parent class.'
|
|
88
97
|
|
|
@@ -140,7 +149,8 @@ module RuboCop
|
|
|
140
149
|
|
|
141
150
|
!allowed_class?(super_class)
|
|
142
151
|
elsif (class_node = node.each_ancestor(:class).first)
|
|
143
|
-
class_node.parent_class && !allowed_class?(class_node.parent_class)
|
|
152
|
+
class_node.parent_class && !allowed_class?(class_node.parent_class) &&
|
|
153
|
+
!index_verified_stateless_ancestry?(class_node)
|
|
144
154
|
else
|
|
145
155
|
false
|
|
146
156
|
end
|
|
@@ -153,6 +163,26 @@ module RuboCop
|
|
|
153
163
|
def allowed_classes
|
|
154
164
|
@allowed_classes ||= STATELESS_CLASSES + cop_config.fetch('AllowedParentClasses', [])
|
|
155
165
|
end
|
|
166
|
+
|
|
167
|
+
# With the project index, the offense is skipped when the class' whole ancestry
|
|
168
|
+
# is resolvable and none of the inherited ancestors defines `initialize` —
|
|
169
|
+
# `super` would only reach the no-op `Object#initialize`. An unresolvable
|
|
170
|
+
# superclass or mixin anywhere in the chain (e.g. a class from a gem) means
|
|
171
|
+
# the ancestry cannot be verified and the offense is kept.
|
|
172
|
+
def index_verified_stateless_ancestry?(class_node)
|
|
173
|
+
return false unless project_index
|
|
174
|
+
|
|
175
|
+
declaration = resolve_constant_in_index(class_node.identifier)
|
|
176
|
+
return false unless declaration.is_a?(Rubydex::Class)
|
|
177
|
+
|
|
178
|
+
ancestors = declaration.ancestors.to_a
|
|
179
|
+
inherited = ancestors.reject { |ancestor| ancestor.name == declaration.name }
|
|
180
|
+
return false if inherited.any? { |ancestor| ancestor.member('initialize()') }
|
|
181
|
+
|
|
182
|
+
# `extend` affects the singleton class and cannot introduce an
|
|
183
|
+
# inherited `initialize`, so unresolved extends are tolerated.
|
|
184
|
+
fully_resolved_index_ancestry?(declaration, ignore_extend: true)
|
|
185
|
+
end
|
|
156
186
|
end
|
|
157
187
|
end
|
|
158
188
|
end
|