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,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'did_you_mean'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module RuboCop
|
|
10
|
+
module Cop
|
|
11
|
+
module Lint
|
|
12
|
+
# Checks for probable typos in constant and method names: a name that
|
|
13
|
+
# does not resolve anywhere in the project, used in a namespace the
|
|
14
|
+
# project does define, with a close-named sibling to suggest instead.
|
|
15
|
+
#
|
|
16
|
+
# The check is powered by the project-wide index, so it only runs when
|
|
17
|
+
# `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
|
|
18
|
+
# Without the index the cop does nothing.
|
|
19
|
+
#
|
|
20
|
+
# Constants are checked only in qualified references (`Foo::Bar`) whose
|
|
21
|
+
# namespace resolves in the index; bare names cannot be distinguished
|
|
22
|
+
# from constants provided by gems or the standard library. Methods are
|
|
23
|
+
# checked only in calls on constant receivers (`Foo.bar`) whose entire
|
|
24
|
+
# indexed ancestry is resolved, so methods gained through gem classes or
|
|
25
|
+
# dynamic definitions never produce offenses. In both cases an offense
|
|
26
|
+
# requires a similarly named alternative to exist — an unknown name
|
|
27
|
+
# alone is not reported, since the index does not see gems or the
|
|
28
|
+
# standard library.
|
|
29
|
+
#
|
|
30
|
+
# Names that appear as symbols or inside string literals in the same
|
|
31
|
+
# file are never reported, since they usually belong to runtime
|
|
32
|
+
# definitions the index cannot see (`stub_const`, `const_set`,
|
|
33
|
+
# `define_method`, and the like).
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# # bad - Services::UserCraetor is not defined, Services::UserCreator is
|
|
37
|
+
# Services::UserCraetor.new
|
|
38
|
+
#
|
|
39
|
+
# # good
|
|
40
|
+
# Services::UserCreator.new
|
|
41
|
+
#
|
|
42
|
+
# # bad - Report.generate_sumary is not defined, Report.generate_summary is
|
|
43
|
+
# Report.generate_sumary
|
|
44
|
+
#
|
|
45
|
+
# # good
|
|
46
|
+
# Report.generate_summary
|
|
47
|
+
#
|
|
48
|
+
# @example CheckConstants: false
|
|
49
|
+
# # good - constant references are not checked
|
|
50
|
+
# Services::UserCraetor.new
|
|
51
|
+
#
|
|
52
|
+
# @example CheckMethods: false
|
|
53
|
+
# # good - method calls are not checked
|
|
54
|
+
# Report.generate_sumary
|
|
55
|
+
#
|
|
56
|
+
# @example AllowedNames: ['generate_sumary']
|
|
57
|
+
# # good - the name is explicitly allowed
|
|
58
|
+
# Report.generate_sumary
|
|
59
|
+
#
|
|
60
|
+
class NameTypo < Base
|
|
61
|
+
include ProjectIndexHelp
|
|
62
|
+
|
|
63
|
+
CONSTANT_MSG = 'Possible typo: `%<name>s` is not defined in `%<namespace>s`. ' \
|
|
64
|
+
'Did you mean `%<suggestion>s`?'
|
|
65
|
+
METHOD_MSG = 'Possible typo: `%<receiver>s` does not respond to `%<name>s`. ' \
|
|
66
|
+
'Did you mean `%<suggestion>s`?'
|
|
67
|
+
|
|
68
|
+
METHOD_MEMBER_REGEXP = /#([a-zA-Z_]\w*[?!]?)\(\)\z/.freeze
|
|
69
|
+
LITERAL_IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!]?/.freeze
|
|
70
|
+
|
|
71
|
+
def on_const(node)
|
|
72
|
+
return unless check?('CheckConstants') && checkable_constant?(node)
|
|
73
|
+
|
|
74
|
+
suggestion = constant_typo_suggestion(node)
|
|
75
|
+
return unless suggestion
|
|
76
|
+
|
|
77
|
+
message = format(CONSTANT_MSG, name: node.short_name,
|
|
78
|
+
namespace: node.namespace.const_name,
|
|
79
|
+
suggestion: suggestion)
|
|
80
|
+
add_offense(node.loc.name, message: message)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def on_send(node)
|
|
84
|
+
return unless check?('CheckMethods')
|
|
85
|
+
return unless node.receiver&.const_type?
|
|
86
|
+
return if allowed_name?(node.method_name) || defined_check?(node)
|
|
87
|
+
|
|
88
|
+
suggestion = method_typo_suggestion(node)
|
|
89
|
+
return unless suggestion
|
|
90
|
+
|
|
91
|
+
message = format(METHOD_MSG, receiver: node.receiver.const_name,
|
|
92
|
+
name: node.method_name,
|
|
93
|
+
suggestion: suggestion)
|
|
94
|
+
add_offense(node.loc.selector, message: message)
|
|
95
|
+
end
|
|
96
|
+
alias on_csend on_send
|
|
97
|
+
|
|
98
|
+
def on_new_investigation
|
|
99
|
+
@literal_names = nil
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def check?(key)
|
|
106
|
+
project_index && defined?(DidYouMean::SpellChecker) && cop_config.fetch(key, true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def checkable_constant?(node)
|
|
110
|
+
node.namespace&.const_type? &&
|
|
111
|
+
!allowed_name?(node.short_name) &&
|
|
112
|
+
!definition_identifier?(node) && !defined_check?(node)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def allowed_name?(name)
|
|
116
|
+
cop_config.fetch('AllowedNames', []).include?(name.to_s)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The last segment of a class, module or constant definition is being
|
|
120
|
+
# defined, not referenced.
|
|
121
|
+
def definition_identifier?(node)
|
|
122
|
+
node.parent&.defined_module
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `defined?(Foo::Bar)` probes whether a name exists; unknown names
|
|
126
|
+
# there are deliberate.
|
|
127
|
+
def defined_check?(node)
|
|
128
|
+
node.each_ancestor(:defined?).any?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def constant_typo_suggestion(node)
|
|
132
|
+
return nil if literal_names.include?(node.short_name.to_s)
|
|
133
|
+
|
|
134
|
+
namespace = resolve_constant_in_index(node.namespace)
|
|
135
|
+
return nil unless namespace.is_a?(Rubydex::Namespace)
|
|
136
|
+
return nil if namespace.find_member(node.short_name.to_s)
|
|
137
|
+
|
|
138
|
+
spell_check(node.short_name, constant_member_names(namespace))
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Writers are indexed under the reader's name, so setter calls are
|
|
144
|
+
# verified and spell-checked through the base name.
|
|
145
|
+
def method_typo_suggestion(node)
|
|
146
|
+
setter = setter_call?(node)
|
|
147
|
+
base = setter ? node.method_name.to_s.delete_suffix('=') : node.method_name.to_s
|
|
148
|
+
return nil if literal_names.include?(base)
|
|
149
|
+
|
|
150
|
+
declaration = unknown_method_owner(node, base)
|
|
151
|
+
return nil unless declaration
|
|
152
|
+
|
|
153
|
+
suggestion = spell_check(base, method_member_names(declaration))
|
|
154
|
+
suggestion && setter ? "#{suggestion}=" : suggestion
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The receiver's declaration when it resolves in the index, its whole
|
|
160
|
+
# ancestry is resolved, and the method is not found — nil otherwise.
|
|
161
|
+
def unknown_method_owner(node, base)
|
|
162
|
+
declaration = resolve_constant_in_index(node.receiver)
|
|
163
|
+
return nil unless declaration.is_a?(Rubydex::Namespace)
|
|
164
|
+
return nil if responds_in_index?(declaration, node.method_name.to_s, base)
|
|
165
|
+
return nil unless fully_resolved_index_ancestry?(declaration)
|
|
166
|
+
|
|
167
|
+
declaration
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def setter_call?(node)
|
|
171
|
+
node.assignment_method? && !node.operator_method?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def responds_in_index?(declaration, name, base)
|
|
175
|
+
# Instance methods of a module are also callable on the module
|
|
176
|
+
# itself when exposed with `module_function`.
|
|
177
|
+
[name, base].uniq.any? do |candidate|
|
|
178
|
+
member_name = "#{candidate}()"
|
|
179
|
+
indexed_singleton_member(declaration, member_name) ||
|
|
180
|
+
declaration.find_member(member_name)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Names mentioned as symbols or inside string literals in the current
|
|
185
|
+
# file belong to runtime definitions (`stub_const`, `const_set`,
|
|
186
|
+
# `define_method`) that the index cannot see.
|
|
187
|
+
def literal_names
|
|
188
|
+
@literal_names ||=
|
|
189
|
+
processed_source.ast.each_descendant(:sym, :str)
|
|
190
|
+
.with_object(Set.new) do |literal, names|
|
|
191
|
+
if literal.sym_type?
|
|
192
|
+
names << literal.value.to_s
|
|
193
|
+
else
|
|
194
|
+
literal.value.scan(LITERAL_IDENTIFIER_PATTERN) { |token| names << token }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def constant_member_names(namespace)
|
|
200
|
+
namespace.members.filter_map do |member|
|
|
201
|
+
name = member.name
|
|
202
|
+
name.split('::').last unless name.include?('#') || name.include?('<')
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def method_member_names(declaration)
|
|
207
|
+
scopes = declaration.ancestors.filter_map { |ancestor| indexed_singleton_of(ancestor) }
|
|
208
|
+
scopes << declaration
|
|
209
|
+
|
|
210
|
+
scopes.flat_map do |scope|
|
|
211
|
+
scope.members.filter_map { |member| member.name[METHOD_MEMBER_REGEXP, 1] }
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def spell_check(name, dictionary)
|
|
216
|
+
return nil if dictionary.empty?
|
|
217
|
+
|
|
218
|
+
DidYouMean::SpellChecker.new(dictionary: dictionary.uniq).correct(name.to_s).first
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -9,6 +9,13 @@ module RuboCop
|
|
|
9
9
|
# safe navigation operator after a safe navigation operator.
|
|
10
10
|
# This cop checks for the problem outlined above.
|
|
11
11
|
#
|
|
12
|
+
# @safety
|
|
13
|
+
# This cop's autocorrection is unsafe because it changes the behavior of
|
|
14
|
+
# the code: `x&.foo.bar` raises `NoMethodError` when `x` or `x&.foo` is `nil`,
|
|
15
|
+
# whereas the corrected `x&.foo&.bar` returns `nil` instead. The autocorrection
|
|
16
|
+
# also assumes that extending safe navigation through the chain is intended,
|
|
17
|
+
# while removing the first `&.` may be what was intended instead.
|
|
18
|
+
#
|
|
12
19
|
# @example
|
|
13
20
|
#
|
|
14
21
|
# # bad
|
|
@@ -71,7 +71,9 @@ module RuboCop
|
|
|
71
71
|
send_arg && argument_match?(send_arg, def_arg)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
all_present &&
|
|
74
|
+
all_present &&
|
|
75
|
+
!extra_positional_arguments?(arguments, def_node) &&
|
|
76
|
+
!extra_keyword_arguments?(arguments, def_node)
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
# The enumerator re-invokes the method with these arguments, so passing more
|
|
@@ -99,6 +101,40 @@ module RuboCop
|
|
|
99
101
|
def_node.arguments.select { |arg| arg.type?(:arg, :optarg) }
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
# The enumerator re-invokes the method with these arguments, so passing
|
|
105
|
+
# unexpected keyword arguments raises `ArgumentError` at runtime.
|
|
106
|
+
def extra_keyword_arguments?(arguments, def_node)
|
|
107
|
+
return false if variadic_keyword_parameters?(def_node)
|
|
108
|
+
return false if expandable_keyword_arguments?(arguments)
|
|
109
|
+
|
|
110
|
+
(passed_keyword_arguments(arguments) - keyword_parameters(def_node)).any?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def variadic_keyword_parameters?(def_node)
|
|
114
|
+
def_node.arguments.any? { |arg| arg.type?(:kwrestarg, :forward_arg) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A call-side keyword splat or argument forwarding can expand to any set of keywords.
|
|
118
|
+
def expandable_keyword_arguments?(arguments)
|
|
119
|
+
arguments.any? do |arg|
|
|
120
|
+
arg.forwarded_args_type? || arg.each_child_node(:kwsplat, :forwarded_kwrestarg).any?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def passed_keyword_arguments(arguments)
|
|
125
|
+
arguments.filter_map do |arg|
|
|
126
|
+
next unless arg.hash_type?
|
|
127
|
+
|
|
128
|
+
arg.pairs.filter_map { |pair| pair.key.value if pair.key.sym_type? }
|
|
129
|
+
end.flatten
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def keyword_parameters(def_node)
|
|
133
|
+
def_node.arguments.filter_map do |arg|
|
|
134
|
+
arg.children[0] if arg.type?(:kwarg, :kwoptarg)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
102
138
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
103
139
|
def argument_match?(send_arg, def_arg)
|
|
104
140
|
def_arg_name = def_arg.children[0]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Lint
|
|
6
|
+
# Checks for private instance methods that are not referenced anywhere
|
|
7
|
+
# in the project.
|
|
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
|
+
# A method counts as referenced when a call with its name appears anywhere
|
|
14
|
+
# in the indexed project (regardless of the receiver), when it is the source
|
|
15
|
+
# of an `alias`, or when its name appears in the same file as a symbol or
|
|
16
|
+
# inside a string literal (covering `send(:name)` and declarative DSLs like
|
|
17
|
+
# `before_action :name`). Methods defined in classes or modules with
|
|
18
|
+
# descendants are not checked, since they may be invoked through `super`
|
|
19
|
+
# or inherited dispatch, and neither are methods whose names are built
|
|
20
|
+
# dynamically (e.g. `send("do_#{action}")`).
|
|
21
|
+
#
|
|
22
|
+
# The cop is disabled by default because symbol-based references from
|
|
23
|
+
# *other* files (e.g. a Rails callback declared in a concern) cannot be
|
|
24
|
+
# detected and would be reported as false positives. It is best suited
|
|
25
|
+
# for occasional dead-code sweeps rather than permanent enforcement.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# # bad - `helper` is never referenced anywhere in the project
|
|
29
|
+
# class Service
|
|
30
|
+
# def call
|
|
31
|
+
# do_something
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# private
|
|
35
|
+
#
|
|
36
|
+
# def helper
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# class Service
|
|
42
|
+
# def call
|
|
43
|
+
# do_something(helper)
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# private
|
|
47
|
+
#
|
|
48
|
+
# def helper
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
class UnusedPrivateMethod < Base
|
|
53
|
+
include ProjectIndexHelp
|
|
54
|
+
|
|
55
|
+
MSG = 'Private method `%<method>s` appears to be unused.'
|
|
56
|
+
|
|
57
|
+
IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!=]?/.freeze
|
|
58
|
+
|
|
59
|
+
# Methods invoked implicitly by the Ruby runtime.
|
|
60
|
+
IMPLICITLY_INVOKED_METHODS = %i[initialize initialize_copy initialize_clone
|
|
61
|
+
initialize_dup method_missing respond_to_missing?
|
|
62
|
+
marshal_dump marshal_load].to_set.freeze
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# The reference-name set is derived once per index and shared by the
|
|
66
|
+
# per-file cop instances.
|
|
67
|
+
def reference_names_cache
|
|
68
|
+
@reference_names_cache ||= {}.compare_by_identity
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_new_investigation
|
|
73
|
+
@literal_names = nil
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_def(node)
|
|
78
|
+
return unless project_index
|
|
79
|
+
|
|
80
|
+
declaration = checkable_declaration(node)
|
|
81
|
+
return unless declaration
|
|
82
|
+
return if referenced?(node.method_name)
|
|
83
|
+
return if owner_with_descendants?(declaration) || override?(declaration, node.method_name)
|
|
84
|
+
|
|
85
|
+
message = format(MSG, method: node.method_name)
|
|
86
|
+
add_offense(node.loc.keyword.join(node.loc.name), message: message)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def checkable_declaration(node)
|
|
92
|
+
return nil if IMPLICITLY_INVOKED_METHODS.include?(node.method_name)
|
|
93
|
+
return nil if node.each_ancestor(:any_def, :any_block, :sclass).any?
|
|
94
|
+
return nil unless (namespace_node = node.each_ancestor(:class, :module).first)
|
|
95
|
+
|
|
96
|
+
declaration = method_declaration(node, namespace_node)
|
|
97
|
+
declaration if declaration&.private?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def method_declaration(node, namespace_node)
|
|
101
|
+
namespace = resolve_constant_in_index(namespace_node.identifier)
|
|
102
|
+
return nil unless namespace.is_a?(Rubydex::Namespace)
|
|
103
|
+
|
|
104
|
+
namespace.member("#{node.method_name}()")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def referenced?(method_name)
|
|
108
|
+
name = method_name.to_s
|
|
109
|
+
|
|
110
|
+
referenced_names.include?(name) || literal_names.include?(name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def referenced_names
|
|
114
|
+
self.class.reference_names_cache[project_index] ||=
|
|
115
|
+
project_index.method_references
|
|
116
|
+
.to_set { |reference| reference.name.delete_suffix('()') }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Symbol literals and identifier-like tokens inside string literals in the
|
|
120
|
+
# current file, covering `send(:name)`, declarative DSLs like
|
|
121
|
+
# `before_action :name` and names embedded in strings (e.g. node-pattern
|
|
122
|
+
# `#helper` references).
|
|
123
|
+
def literal_names
|
|
124
|
+
@literal_names ||=
|
|
125
|
+
processed_source.ast.each_descendant(:sym, :str)
|
|
126
|
+
.with_object(Set.new) do |literal, names|
|
|
127
|
+
if literal.sym_type?
|
|
128
|
+
names << literal.value.to_s
|
|
129
|
+
else
|
|
130
|
+
literal.value.scan(IDENTIFIER_PATTERN) { |token| names << token }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A method defined in a class or module with descendants may be invoked
|
|
136
|
+
# through `super` or inherited dispatch, which the index does not track.
|
|
137
|
+
def owner_with_descendants?(declaration)
|
|
138
|
+
owner = declaration.owner
|
|
139
|
+
return false unless owner.is_a?(Rubydex::Namespace)
|
|
140
|
+
|
|
141
|
+
owner.descendants.any? { |descendant| descendant.name != owner.name }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# An override of an inherited method may be invoked polymorphically
|
|
145
|
+
# (e.g. a framework hook), even when no direct reference exists.
|
|
146
|
+
def override?(declaration, method_name)
|
|
147
|
+
owner = declaration.owner
|
|
148
|
+
return false unless owner.is_a?(Rubydex::Namespace)
|
|
149
|
+
|
|
150
|
+
owner.ancestors.any? do |ancestor|
|
|
151
|
+
ancestor.name != owner.name && ancestor.member("#{method_name}()")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -116,6 +116,8 @@ module RuboCop
|
|
|
116
116
|
# so stop searching once a class/module boundary is crossed without a match.
|
|
117
117
|
return nil if ancestor.type?(:class, :module, :sclass)
|
|
118
118
|
end
|
|
119
|
+
|
|
120
|
+
nil
|
|
119
121
|
end
|
|
120
122
|
|
|
121
123
|
# `ruby2_keywords` is only allowed if there's a `restarg` and no keyword arguments
|
|
@@ -16,11 +16,14 @@ module RuboCop
|
|
|
16
16
|
# enumerator.each { |item| item >= 2 } #=> [2, 3]
|
|
17
17
|
# ----
|
|
18
18
|
#
|
|
19
|
-
# NOTE:
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
19
|
+
# NOTE: The last expression in an assignment method definition such as `def foo=(arg)`
|
|
20
|
+
# is not flagged. Ruby discards it (the method returns its argument), but the method can
|
|
21
|
+
# still be called directly and its return value relied upon, so flagging it would be a
|
|
22
|
+
# false positive for this lint.
|
|
23
|
+
#
|
|
24
|
+
# NOTE: A constant used in a void context is flagged but not autocorrected, since
|
|
25
|
+
# referencing a constant can trigger autoloading side effects (e.g. forcing a file to
|
|
26
|
+
# load before a monkey-patch), so removing it may change behavior.
|
|
24
27
|
#
|
|
25
28
|
# @example CheckForMethodsWithNoSideEffects: false (default)
|
|
26
29
|
# # bad
|
|
@@ -109,7 +112,7 @@ module RuboCop
|
|
|
109
112
|
def check_begin(node)
|
|
110
113
|
expressions = *node
|
|
111
114
|
inside_each_block = node.each_ancestor(:any_block).first&.method?(:each)
|
|
112
|
-
expressions.pop if !in_void_context?(node) || inside_each_block
|
|
115
|
+
expressions.pop if !in_void_context?(node) || inside_each_block || setter_method?(node)
|
|
113
116
|
expressions.each do |expr|
|
|
114
117
|
check_void_op(expr) { inside_each_block }
|
|
115
118
|
check_expression(expr)
|
|
@@ -244,6 +247,10 @@ module RuboCop
|
|
|
244
247
|
parent.respond_to?(:void_context?) && parent.void_context?
|
|
245
248
|
end
|
|
246
249
|
|
|
250
|
+
def setter_method?(node)
|
|
251
|
+
node.parent&.any_def_type? && node.parent.assignment_method?
|
|
252
|
+
end
|
|
253
|
+
|
|
247
254
|
def autocorrect_void_op(corrector, node)
|
|
248
255
|
if node.arguments.empty?
|
|
249
256
|
corrector.replace(node, node.receiver.source)
|
|
@@ -258,6 +265,10 @@ module RuboCop
|
|
|
258
265
|
end
|
|
259
266
|
|
|
260
267
|
def autocorrect_void_expression(corrector, node)
|
|
268
|
+
# Referencing a constant can trigger autoloading side effects (e.g. forcing a file
|
|
269
|
+
# to load before a monkey-patch), so removing it may change behavior. Flag but don't
|
|
270
|
+
# autocorrect, leaving it to the user.
|
|
271
|
+
return if node.const_type? && !node.special_keyword?
|
|
261
272
|
return if node.parent.type?(:if, :case, :when, :case_match, :in_pattern)
|
|
262
273
|
return if (def_node = node.each_ancestor(:any_def).first) && def_node.assignment_method?
|
|
263
274
|
|