rubocop 1.88.0 → 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 +72 -12
- 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 +19 -12
- data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
- data/lib/rubocop/cop/bundler.rb +19 -0
- data/lib/rubocop/cop/commissioner.rb +13 -11
- data/lib/rubocop/cop/corrector.rb +1 -1
- data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
- data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
- data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
- data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
- data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
- data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
- data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
- 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/block_alignment.rb +17 -0
- data/lib/rubocop/cop/layout/class_structure.rb +14 -4
- data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
- data/lib/rubocop/cop/layout/condition_position.rb +13 -3
- data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
- data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
- data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
- data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
- data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
- data/lib/rubocop/cop/layout/indentation_width.rb +31 -1
- data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
- data/lib/rubocop/cop/layout/line_length.rb +95 -17
- data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +18 -3
- data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
- data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
- 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/assignment_in_condition.rb +13 -1
- 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/debugger.rb +13 -1
- 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/numeric_operation_with_constant_result.rb +7 -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 +44 -2
- data/lib/rubocop/cop/lint/to_json.rb +8 -1
- data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
- data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
- data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
- data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
- data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
- data/lib/rubocop/cop/lint/void.rb +19 -8
- data/lib/rubocop/cop/lint.rb +176 -0
- data/lib/rubocop/cop/metrics/method_length.rb +1 -1
- data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
- 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/allowed_methods.rb +5 -5
- data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
- 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/forbidden_pattern.rb +5 -1
- data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
- data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
- data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
- 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 +43 -7
- data/lib/rubocop/cop/mixin.rb +78 -77
- data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
- data/lib/rubocop/cop/naming/file_name.rb +4 -3
- data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
- data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
- data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
- data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
- 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/io_methods.rb +1 -1
- data/lib/rubocop/cop/security/marshal_load.rb +1 -1
- data/lib/rubocop/cop/security.rb +19 -0
- data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
- data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
- 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/collection_compact.rb +1 -1
- data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
- data/lib/rubocop/cop/style/copyright.rb +6 -1
- data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
- data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
- data/lib/rubocop/cop/style/dir_empty.rb +4 -0
- 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/documentation_method.rb +5 -1
- data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
- data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
- data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
- data/lib/rubocop/cop/style/empty_literal.rb +7 -2
- data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
- data/lib/rubocop/cop/style/endless_method.rb +0 -10
- data/lib/rubocop/cop/style/env_home.rb +4 -0
- data/lib/rubocop/cop/style/even_odd.rb +11 -1
- data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
- data/lib/rubocop/cop/style/file_null.rb +4 -2
- data/lib/rubocop/cop/style/format_string.rb +13 -1
- data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
- data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
- data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
- data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
- data/lib/rubocop/cop/style/if_unless_modifier.rb +11 -52
- data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
- data/lib/rubocop/cop/style/inline_comment.rb +1 -1
- data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
- data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
- data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
- data/lib/rubocop/cop/style/lambda.rb +11 -1
- data/lib/rubocop/cop/style/lambda_call.rb +11 -0
- data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
- data/lib/rubocop/cop/style/map_into_array.rb +1 -1
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
- data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
- data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
- data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
- data/lib/rubocop/cop/style/missing_respond_to_missing.rb +40 -7
- data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
- data/lib/rubocop/cop/style/module_function.rb +4 -2
- data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
- data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
- data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
- 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/nil_lambda.rb +8 -0
- data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
- data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
- data/lib/rubocop/cop/style/option_hash.rb +1 -1
- data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
- data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
- data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
- data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
- 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_current_directory_in_path.rb +13 -1
- data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
- data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
- data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
- data/lib/rubocop/cop/style/redundant_format.rb +40 -3
- data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
- data/lib/rubocop/cop/style/redundant_line_continuation.rb +80 -129
- data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
- data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
- data/lib/rubocop/cop/style/redundant_self.rb +9 -0
- data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
- data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
- data/lib/rubocop/cop/style/semicolon.rb +4 -4
- data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
- 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/string_hash_keys.rb +1 -0
- data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
- data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
- data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
- data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
- data/lib/rubocop/cop/style.rb +310 -0
- data/lib/rubocop/cop/team.rb +66 -7
- 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 +100 -33
- 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
|
|
@@ -15,6 +15,13 @@ module RuboCop
|
|
|
15
15
|
# can't determine the type of `x`. If `x` is an `Array` or `String`, it doesn't perform
|
|
16
16
|
# a numeric operation.
|
|
17
17
|
#
|
|
18
|
+
# @safety
|
|
19
|
+
# This cop is unsafe because the autocorrection drops the operands, which
|
|
20
|
+
# discards any side effects of evaluating them and can change behavior when
|
|
21
|
+
# the result is not actually constant. For example, `x / x` raises
|
|
22
|
+
# `ZeroDivisionError` when `x` is `0`, and returns `Float::NAN` (not `1`)
|
|
23
|
+
# when `x` is `0.0`; replacing it with `1` silences that.
|
|
24
|
+
#
|
|
18
25
|
# @example
|
|
19
26
|
#
|
|
20
27
|
# # bad
|
|
@@ -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]
|
|
@@ -109,9 +145,11 @@ module RuboCop
|
|
|
109
145
|
when :optarg
|
|
110
146
|
send_arg.source == def_arg_name.to_s
|
|
111
147
|
when :kwoptarg, :kwarg
|
|
112
|
-
send_arg
|
|
148
|
+
keyword_hash_argument?(send_arg) &&
|
|
113
149
|
send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
|
|
114
150
|
when :kwrestarg
|
|
151
|
+
return false unless keyword_hash_argument?(send_arg)
|
|
152
|
+
|
|
115
153
|
send_arg.each_child_node(:kwsplat, :forwarded_kwrestarg).any? do |child|
|
|
116
154
|
child.source == def_arg.source
|
|
117
155
|
end
|
|
@@ -120,6 +158,10 @@ module RuboCop
|
|
|
120
158
|
end
|
|
121
159
|
end
|
|
122
160
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
161
|
+
|
|
162
|
+
def keyword_hash_argument?(send_arg)
|
|
163
|
+
send_arg.hash_type? && !send_arg.braces?
|
|
164
|
+
end
|
|
123
165
|
end
|
|
124
166
|
end
|
|
125
167
|
end
|
|
@@ -36,9 +36,16 @@ module RuboCop
|
|
|
36
36
|
# The following used `*_args` because `to_json(*args)` has
|
|
37
37
|
# an offense of `Lint/UnusedMethodArgument` cop if `*args`
|
|
38
38
|
# is not used.
|
|
39
|
-
|
|
39
|
+
if (opening_parenthesis = node.each_child_node(:args).first.loc.begin)
|
|
40
|
+
# Explicit empty parentheses (`def to_json()`) are already present,
|
|
41
|
+
# so insert the argument inside them to avoid producing `to_json(*_args)()`.
|
|
42
|
+
corrector.insert_after(opening_parenthesis, '*_args')
|
|
43
|
+
else
|
|
44
|
+
corrector.insert_after(node.loc.name, '(*_args)')
|
|
45
|
+
end
|
|
40
46
|
end
|
|
41
47
|
end
|
|
48
|
+
alias on_defs on_def
|
|
42
49
|
end
|
|
43
50
|
end
|
|
44
51
|
end
|
|
@@ -44,9 +44,7 @@ module RuboCop
|
|
|
44
44
|
|
|
45
45
|
def on_regexp(node)
|
|
46
46
|
RuboCop::Util.silence_warnings do
|
|
47
|
-
node.parsed_tree
|
|
48
|
-
detect_offenses(node, expr)
|
|
49
|
-
end
|
|
47
|
+
detect_offenses_in_tree(node, node.parsed_tree)
|
|
50
48
|
end
|
|
51
49
|
end
|
|
52
50
|
|
|
@@ -55,19 +53,43 @@ module RuboCop
|
|
|
55
53
|
return if node.each_descendant(:dstr).any?
|
|
56
54
|
|
|
57
55
|
regexp_constructor(node) do |text|
|
|
58
|
-
parse_regexp(text.value)
|
|
59
|
-
detect_offenses(text, expr)
|
|
60
|
-
end
|
|
56
|
+
detect_offenses_in_tree(text, parse_regexp(text.value))
|
|
61
57
|
end
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
private
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
# When a character class opens with a bare `]` (e.g. `[^]]`), `regexp_parser` parses
|
|
63
|
+
# `[^]` / `[]` as an empty set and reports the closing `]` as a separate literal.
|
|
64
|
+
# Ruby treats that `]` as the end of the class, not as an unescaped bracket,
|
|
65
|
+
# so the first `]` following an empty set must be skipped.
|
|
66
|
+
def detect_offenses_in_tree(node, tree)
|
|
67
|
+
return unless tree
|
|
68
|
+
|
|
69
|
+
skip_class_closer = false
|
|
70
|
+
tree.each_expression do |expr|
|
|
71
|
+
if empty_character_set?(expr)
|
|
72
|
+
skip_class_closer = true
|
|
73
|
+
elsif expr.type?(:literal)
|
|
74
|
+
skip_class_closer = detect_offenses(node, expr, skip_class_closer)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def empty_character_set?(expr)
|
|
80
|
+
expr.type?(:set) && expr.expressions.empty?
|
|
81
|
+
end
|
|
68
82
|
|
|
83
|
+
def detect_offenses(node, expr, skip_class_closer)
|
|
69
84
|
expr.text.scan(/(?<!\\)\]/) do
|
|
70
85
|
pos = Regexp.last_match.begin(0)
|
|
86
|
+
|
|
87
|
+
# The first `]` following an empty `[^]` / `[]` set closes the character class.
|
|
88
|
+
if skip_class_closer
|
|
89
|
+
skip_class_closer = false
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
# If the unescaped bracket is the first character of the regexp, Ruby does not warn.
|
|
72
94
|
# `pos` is relative to the sub-expression, so add its start offset (`expr.ts`).
|
|
73
95
|
next if (expr.ts + pos).zero?
|
|
@@ -78,6 +100,8 @@ module RuboCop
|
|
|
78
100
|
corrector.replace(location, '\]')
|
|
79
101
|
end
|
|
80
102
|
end
|
|
103
|
+
|
|
104
|
+
skip_class_closer
|
|
81
105
|
end
|
|
82
106
|
|
|
83
107
|
def range_at_index(node, index, offset)
|
|
@@ -52,10 +52,15 @@ module RuboCop
|
|
|
52
52
|
def on_begin(node)
|
|
53
53
|
expressions = *node
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
# Once a flow-of-control statement is reached, every following statement
|
|
56
|
+
# in the block is unreachable, not just the one immediately after it.
|
|
57
|
+
flow_reached = false
|
|
58
|
+
expressions.each_with_index do |expression, index|
|
|
59
|
+
if flow_reached
|
|
60
|
+
add_offense(expression)
|
|
61
|
+
elsif index < expressions.size - 1 && flow_expression?(expression)
|
|
62
|
+
flow_reached = true
|
|
63
|
+
end
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -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
|
|
@@ -36,31 +36,31 @@ module RuboCop
|
|
|
36
36
|
RESTRICT_ON_SEND = %i[+ - * / **].freeze
|
|
37
37
|
|
|
38
38
|
# @!method useless_operation?(node)
|
|
39
|
-
def_node_matcher :useless_operation?,
|
|
39
|
+
def_node_matcher :useless_operation?, <<~PATTERN
|
|
40
|
+
(call ${lvar ivar cvar gvar const (send nil? _)} $_ (int $_))
|
|
41
|
+
PATTERN
|
|
40
42
|
|
|
41
43
|
# @!method useless_abbreviated_assignment?(node)
|
|
42
|
-
def_node_matcher :useless_abbreviated_assignment?,
|
|
44
|
+
def_node_matcher :useless_abbreviated_assignment?, <<~PATTERN
|
|
45
|
+
(op-asgn ${lvasgn ivasgn cvasgn gvasgn casgn} $_ (int $_))
|
|
46
|
+
PATTERN
|
|
43
47
|
|
|
44
48
|
def on_send(node)
|
|
45
|
-
return unless useless_operation?(node)
|
|
46
|
-
|
|
47
|
-
variable, operation, number = useless_operation?(node)
|
|
49
|
+
return unless (receiver, operation, number = useless_operation?(node))
|
|
48
50
|
return unless useless?(operation, number)
|
|
49
51
|
|
|
50
52
|
add_offense(node) do |corrector|
|
|
51
|
-
corrector.replace(node,
|
|
53
|
+
corrector.replace(node, receiver.source)
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
alias on_csend on_send
|
|
55
57
|
|
|
56
58
|
def on_op_asgn(node)
|
|
57
|
-
return unless useless_abbreviated_assignment?(node)
|
|
58
|
-
|
|
59
|
-
variable, operation, number = useless_abbreviated_assignment?(node)
|
|
59
|
+
return unless (variable, operation, number = useless_abbreviated_assignment?(node))
|
|
60
60
|
return unless useless?(operation, number)
|
|
61
61
|
|
|
62
62
|
add_offense(node) do |corrector|
|
|
63
|
-
corrector.replace(node, "#{variable} = #{variable}")
|
|
63
|
+
corrector.replace(node, "#{variable.source} = #{variable.source}")
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -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
|