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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +67 -11
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +6 -4
  8. data/lib/rubocop/cop/bundler.rb +19 -0
  9. data/lib/rubocop/cop/corrector.rb +1 -1
  10. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  11. data/lib/rubocop/cop/gemspec.rb +22 -0
  12. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  13. data/lib/rubocop/cop/generator.rb +4 -0
  14. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  15. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  16. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  17. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  18. data/lib/rubocop/cop/layout/line_length.rb +85 -12
  19. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  20. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  21. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  22. data/lib/rubocop/cop/layout.rb +114 -0
  23. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  26. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  27. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  28. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  29. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  30. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  31. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  32. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  33. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  34. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  35. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  36. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  37. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  38. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  39. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  40. data/lib/rubocop/cop/lint/void.rb +17 -6
  41. data/lib/rubocop/cop/lint.rb +176 -0
  42. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  43. data/lib/rubocop/cop/metrics.rb +31 -0
  44. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  45. data/lib/rubocop/cop/migration.rb +13 -0
  46. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  47. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  48. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  49. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  50. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  51. data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
  52. data/lib/rubocop/cop/mixin.rb +78 -77
  53. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  54. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  55. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  56. data/lib/rubocop/cop/naming.rb +31 -0
  57. data/lib/rubocop/cop/offense.rb +10 -6
  58. data/lib/rubocop/cop/registry.rb +153 -43
  59. data/lib/rubocop/cop/security.rb +19 -0
  60. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  61. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  62. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  63. data/lib/rubocop/cop/style/copyright.rb +6 -1
  64. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  65. data/lib/rubocop/cop/style/documentation.rb +47 -0
  66. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  67. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
  68. data/lib/rubocop/cop/style/lambda.rb +4 -0
  69. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  70. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  71. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  72. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  73. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  74. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  75. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  76. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  77. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  78. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  79. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  80. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  81. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  82. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  83. data/lib/rubocop/cop/style/static_class.rb +21 -0
  84. data/lib/rubocop/cop/style.rb +310 -0
  85. data/lib/rubocop/cop/team.rb +48 -3
  86. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  87. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  88. data/lib/rubocop/directive_comment.rb +2 -0
  89. data/lib/rubocop/feature_loader.rb +0 -6
  90. data/lib/rubocop/lsp/routes.rb +41 -3
  91. data/lib/rubocop/lsp/runtime.rb +4 -0
  92. data/lib/rubocop/lsp/server.rb +4 -0
  93. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  94. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  95. data/lib/rubocop/project_index_loader.rb +16 -0
  96. data/lib/rubocop/result_cache.rb +6 -4
  97. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  98. data/lib/rubocop/runner.rb +45 -20
  99. data/lib/rubocop/server/cache.rb +10 -5
  100. data/lib/rubocop/target_ruby.rb +1 -1
  101. data/lib/rubocop/version.rb +1 -1
  102. data/lib/rubocop.rb +17 -626
  103. metadata +18 -3
@@ -12,6 +12,17 @@ module RuboCop
12
12
  # Specific cops can be allowed with the `AllowedCops` configuration. Note that
13
13
  # if this configuration is set, `rubocop:disable all` is still disallowed.
14
14
  #
15
+ # Alternatively, specific cops can be disallowed with the `DisallowedCops`
16
+ # configuration. When `DisallowedCops` is set, only directives for the listed
17
+ # cops (and `all`) will be flagged. This is useful when you want
18
+ # to protect a small set of critical cops from being disabled rather than
19
+ # allowlisting all other cops. `AllowedCops` and `DisallowedCops` should not
20
+ # both be set at the same time; if `DisallowedCops` is set, it takes precedence.
21
+ #
22
+ # This cop cannot be disabled via directive comments when it is explicitly
23
+ # enabled with `Enabled: true`. This prevents users from bypassing the cop
24
+ # with `# rubocop:disable Style/DisableCopsWithinSourceCodeDirective`.
25
+ #
15
26
  # @example
16
27
  # # bad
17
28
  # # rubocop:disable Metrics/AbcSize
@@ -30,6 +41,17 @@ module RuboCop
30
41
  # end
31
42
  # # rubocop:enable Metrics/AbcSize
32
43
  #
44
+ # @example DisallowedCops: [Lint/Void]
45
+ # # bad
46
+ # # rubocop:disable Lint/Void
47
+ # foo
48
+ # # rubocop:enable Lint/Void
49
+ #
50
+ # # good
51
+ # # rubocop:disable Metrics/AbcSize
52
+ # foo
53
+ # # rubocop:enable Metrics/AbcSize
54
+ #
33
55
  class DisableCopsWithinSourceCodeDirective < Base
34
56
  extend AutoCorrector
35
57
 
@@ -40,7 +62,7 @@ module RuboCop
40
62
  def on_new_investigation
41
63
  processed_source.comments.each do |comment|
42
64
  directive_cops = directive_cops(comment)
43
- disallowed_cops = directive_cops - allowed_cops
65
+ disallowed_cops = compute_disallowed_cops(directive_cops)
44
66
 
45
67
  next unless disallowed_cops.any?
46
68
 
@@ -50,8 +72,20 @@ module RuboCop
50
72
 
51
73
  private
52
74
 
75
+ def compute_disallowed_cops(directive_cops)
76
+ if disallowed_cops_config.any?
77
+ if directive_cops.include?('all')
78
+ directive_cops
79
+ else
80
+ directive_cops.uniq.select { |cop| disallowed_cops_config.include?(cop) }
81
+ end
82
+ else
83
+ directive_cops.reject { |cop| allowed_cops.include?(cop) }
84
+ end
85
+ end
86
+
53
87
  def register_offense(comment, directive_cops, disallowed_cops)
54
- message = if any_cops_allowed?
88
+ message = if any_cops_allowed? || disallowed_cops_config.any?
55
89
  format(MSG_FOR_COPS, cops: "`#{disallowed_cops.join('`, `')}`")
56
90
  else
57
91
  MSG
@@ -75,12 +109,16 @@ module RuboCop
75
109
  end
76
110
 
77
111
  def allowed_cops
78
- Array(cop_config['AllowedCops'])
112
+ @allowed_cops ||= Array(cop_config['AllowedCops']).to_set
79
113
  end
80
114
 
81
115
  def any_cops_allowed?
82
116
  allowed_cops.any?
83
117
  end
118
+
119
+ def disallowed_cops_config
120
+ @disallowed_cops_config ||= Array(cop_config['DisallowedCops']).to_set
121
+ end
84
122
  end
85
123
  end
86
124
  end
@@ -13,6 +13,11 @@ module RuboCop
13
13
  # a `#:nodoc:` comment next to it. Likewise, `#:nodoc: all` does the
14
14
  # same for all its children.
15
15
  #
16
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
17
+ # installed, a reopened class or module is not reported when any of its
18
+ # other definition sites (in the same or another file) carries a
19
+ # documentation comment.
20
+ #
16
21
  # @example
17
22
  # # bad
18
23
  # class Person
@@ -71,10 +76,15 @@ module RuboCop
71
76
  #
72
77
  class Documentation < Base
73
78
  include DocumentationComment
79
+ include ProjectIndexHelp
74
80
  include RangeHelp
75
81
 
76
82
  MSG = 'Missing top-level documentation comment for `%<type>s %<identifier>s`.'
77
83
 
84
+ # Comments that configure tooling rather than document the code.
85
+ DIRECTIVE_COMMENT_REGEXP = /\A\#\s*(rubocop:|frozen_string_literal:|encoding:|
86
+ shareable_constant_value:|typed:|-\*-)/x.freeze
87
+
78
88
  # @!method constant_definition?(node)
79
89
  def_node_matcher :constant_definition?, '{class module casgn}'
80
90
 
@@ -109,12 +119,49 @@ module RuboCop
109
119
  return if constant_allowed?(node)
110
120
  return if nodoc_self_or_outer_module?(node)
111
121
  return if include_statement_only?(body)
122
+ return if documented_elsewhere?(node)
123
+
124
+ add_documentation_offense(node)
125
+ end
112
126
 
127
+ def add_documentation_offense(node)
113
128
  range = range_between(node.source_range.begin_pos, node.loc.name.end_pos)
114
129
  message = format(MSG, type: node.type, identifier: identifier(node))
115
130
  add_offense(range, message: message)
116
131
  end
117
132
 
133
+ # A reopened class or module is typically documented at a single
134
+ # definition site. When `AllCops/UseProjectIndex` is enabled, the
135
+ # documentation requirement is satisfied by a comment on any other
136
+ # definition of the same class or module.
137
+ def documented_elsewhere?(node)
138
+ return false unless project_index
139
+
140
+ declaration = resolve_constant_in_index(node.identifier)
141
+ return false unless declaration.is_a?(Rubydex::Namespace)
142
+
143
+ declaration.definitions.any? do |definition|
144
+ other_definition?(definition, node) && documenting_comments?(definition)
145
+ end
146
+ rescue StandardError
147
+ false
148
+ end
149
+
150
+ def other_definition?(definition, node)
151
+ location = definition.location
152
+ return false unless location.uri.start_with?(FILE_URI_PREFIX)
153
+
154
+ !same_file?(location.to_file_path, processed_source.file_path) ||
155
+ location.to_display.start_line != node.first_line
156
+ end
157
+
158
+ def documenting_comments?(definition)
159
+ definition.comments.to_a.any? do |comment|
160
+ text = comment.string.strip
161
+ !text.empty? && !DIRECTIVE_COMMENT_REGEXP.match?(text)
162
+ end
163
+ end
164
+
118
165
  def nodoc_self_or_outer_module?(node)
119
166
  nodoc_comment?(node) ||
120
167
  (compact_namespace?(node) && nodoc_comment?(outer_module(node).first))
@@ -206,16 +206,6 @@ module RuboCop
206
206
  body.each_descendant(:str).any?(&:heredoc?)
207
207
  end
208
208
 
209
- def correct_to_multiline(corrector, node)
210
- replacement = <<~RUBY.strip
211
- def #{receiver(node)}#{node.method_name}#{arguments(node)}
212
- #{node.body.source}
213
- end
214
- RUBY
215
-
216
- corrector.replace(node, replacement)
217
- end
218
-
219
209
  def endless_replacement(node)
220
210
  <<~RUBY.strip
221
211
  def #{receiver(node)}#{node.method_name}#{arguments(node)} = #{node.body.source}
@@ -72,10 +72,8 @@ module RuboCop
72
72
  # do_something
73
73
  # end
74
74
  #
75
- class IfUnlessModifier < Base # rubocop:disable Metrics/ClassLength
75
+ class IfUnlessModifier < Base
76
76
  include StatementModifier
77
- include LineLengthHelp
78
- include AllowedPattern
79
77
  include RangeHelp
80
78
  include CommentsHelp
81
79
  extend AutoCorrector
@@ -83,6 +81,10 @@ module RuboCop
83
81
  MSG_USE_MODIFIER = 'Favor modifier `%<keyword>s` usage when having a ' \
84
82
  'single-line body. Another good alternative is ' \
85
83
  'the usage of control flow `&&`/`||`.'
84
+ MSG_USE_MODIFIER_PARENS = 'Favor modifier `%<keyword>s` usage when having a ' \
85
+ 'single-line body. Wrap the expression in parentheses ' \
86
+ 'to keep the current behavior, as it is part of a ' \
87
+ 'larger expression.'
86
88
  MSG_USE_NORMAL = 'Modifier form of `%<keyword>s` makes the line too long.'
87
89
 
88
90
  def self.autocorrect_incompatible_with
@@ -143,7 +145,7 @@ module RuboCop
143
145
 
144
146
  def message(node)
145
147
  if single_line_as_modifier?(node) && !named_capture_in_condition?(node)
146
- MSG_USE_MODIFIER
148
+ parenthesize?(node) ? MSG_USE_MODIFIER_PARENS : MSG_USE_MODIFIER
147
149
  elsif too_long_due_to_modifier?(node)
148
150
  MSG_USE_NORMAL
149
151
  end
@@ -188,52 +190,9 @@ module RuboCop
188
190
  max_line_length.between?(source_length - comment.source_range.length, source_length)
189
191
  end
190
192
 
191
- def allowed_patterns
192
- line_length_config = config.for_cop('Layout/LineLength')
193
- line_length_config['AllowedPatterns'] || line_length_config['IgnoredPatterns'] || []
194
- end
195
-
196
193
  def too_long_single_line?(node)
197
- return false unless max_line_length
198
-
199
194
  range = node.source_range
200
- return false unless range.single_line?
201
- return false unless line_length_enabled_at_line?(range.first_line)
202
-
203
- line = range.source_line
204
- return false if line_length(line) <= max_line_length
205
-
206
- too_long_line_based_on_config?(range, line)
207
- end
208
-
209
- def too_long_line_based_on_config?(range, line)
210
- return false if matches_allowed_pattern?(line)
211
-
212
- too_long = too_long_line_based_on_allow_cop_directives?(range, line)
213
- return too_long unless too_long == :undetermined
214
-
215
- too_long_line_based_on_allow_uri?(line)
216
- end
217
-
218
- def too_long_line_based_on_allow_cop_directives?(range, line)
219
- if allow_cop_directives? && directive_on_source_line?(range.line - 1)
220
- return line_length_without_directive(line) > max_line_length
221
- end
222
-
223
- :undetermined
224
- end
225
-
226
- def too_long_line_based_on_allow_uri?(line)
227
- if allow_uri?
228
- uri_range = find_excessive_range(line, :uri)
229
- return false if uri_range && allowed_position?(line, uri_range)
230
- end
231
-
232
- true
233
- end
234
-
235
- def line_length_enabled_at_line?(line)
236
- processed_source.comment_config.cop_enabled_at_line?('Layout/LineLength', line)
195
+ range.single_line? && !acceptable_line_length?(range.source_line, range.first_line)
237
196
  end
238
197
 
239
198
  def named_capture_in_condition?(node)
@@ -61,6 +61,10 @@ module RuboCop
61
61
  }
62
62
  }.freeze
63
63
 
64
+ def self.autocorrect_incompatible_with
65
+ [Style::SymbolProc]
66
+ end
67
+
64
68
  def on_block(node)
65
69
  return unless node.lambda?
66
70
 
@@ -8,11 +8,26 @@ module RuboCop
8
8
  # rubocop:disable Metrics/ModuleLength, Metrics/CyclomaticComplexity
9
9
  module OmitParentheses
10
10
  include RangeHelp
11
+ include ReparsedEquivalence
11
12
 
12
13
  TRAILING_WHITESPACE_REGEX = /\s+\Z/.freeze
13
14
  OMIT_MSG = 'Omit parentheses for method calls with arguments.'
14
15
  private_constant :OMIT_MSG
15
16
 
17
+ def on_investigation_end
18
+ # Each candidate's exact correction is verified by reparsing before
19
+ # the offense is registered, so an omission that would change how
20
+ # the code parses is never reported or offered.
21
+ verified_by_reparse(@pending_omit_offenses || []).each do |node|
22
+ add_offense(offense_range(node), message: OMIT_MSG) do |corrector|
23
+ autocorrect(corrector, node)
24
+ end
25
+ end
26
+ @pending_omit_offenses = []
27
+
28
+ super
29
+ end
30
+
16
31
  private
17
32
 
18
33
  def omit_parentheses(node) # rubocop:disable Metrics/PerceivedComplexity
@@ -26,9 +41,11 @@ module RuboCop
26
41
  return if allowed_camel_case_method_call?(node)
27
42
  return if allowed_string_interpolation_method_call?(node)
28
43
 
29
- add_offense(offense_range(node), message: OMIT_MSG) do |corrector|
30
- autocorrect(corrector, node)
31
- end
44
+ (@pending_omit_offenses ||= []) << node
45
+ end
46
+
47
+ def apply_reparse_correction(corrector, node)
48
+ autocorrect(corrector, node)
32
49
  end
33
50
 
34
51
  def autocorrect(corrector, node)
@@ -28,6 +28,11 @@ module RuboCop
28
28
  # delegator.upcase # => FOO
29
29
  # ----
30
30
  #
31
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
32
+ # installed, `respond_to_missing?` defined in another definition of the
33
+ # same class or module (e.g. a reopening in another file) also
34
+ # satisfies the check.
35
+ #
31
36
  # @example
32
37
  # # bad
33
38
  # def method_missing(name, *args)
@@ -52,11 +57,14 @@ module RuboCop
52
57
  # end
53
58
  #
54
59
  class MissingRespondToMissing < Base
60
+ include ProjectIndexHelp
61
+
55
62
  MSG = 'When using `method_missing`, define `respond_to_missing?`.'
56
63
 
57
64
  def on_def(node)
58
65
  return unless node.method?(:method_missing)
59
66
  return if implements_respond_to_missing?(node)
67
+ return if respond_to_missing_elsewhere?(node)
60
68
 
61
69
  add_offense(node)
62
70
  end
@@ -64,6 +72,28 @@ module RuboCop
64
72
 
65
73
  private
66
74
 
75
+ # When `AllCops/UseProjectIndex` is enabled, `respond_to_missing?`
76
+ # defined in another definition of the same class or module (e.g. a
77
+ # reopening in another file) also satisfies the check. Ancestors are
78
+ # not consulted: overriding `method_missing` warrants a matching
79
+ # `respond_to_missing?` for the same class.
80
+ def respond_to_missing_elsewhere?(node)
81
+ return false unless project_index
82
+ return false unless (namespace_node = node.each_ancestor(:class, :module).first)
83
+
84
+ declaration = resolve_constant_in_index(namespace_node.identifier)
85
+ return false unless declaration.is_a?(Rubydex::Namespace)
86
+
87
+ scope = singleton_definition?(node) ? indexed_singleton_of(declaration) : declaration
88
+ !scope&.member('respond_to_missing?()').nil?
89
+ rescue StandardError
90
+ false
91
+ end
92
+
93
+ def singleton_definition?(node)
94
+ node.defs_type? || node.each_ancestor(:sclass, :class, :module).first&.sclass_type?
95
+ end
96
+
67
97
  def implements_respond_to_missing?(node)
68
98
  scope = enclosing_scope(node)
69
99
  search_root = scope || node.parent
@@ -36,7 +36,13 @@ module RuboCop
36
36
  private
37
37
 
38
38
  def non_modifier_then?(node)
39
- node.multiline? && node.then? && node.loc.begin.line != node.if_branch&.loc&.line
39
+ node.multiline? && own_then?(node) && node.loc.begin.line != node.if_branch&.loc&.line
40
+ end
41
+
42
+ # Prism gives an `elsif` with no `then` of its own the previous branch's `then` location,
43
+ # which lies outside the node.
44
+ def own_then?(node)
45
+ node.then? && node.source_range.contains?(node.loc.begin)
40
46
  end
41
47
  end
42
48
  end
@@ -70,48 +70,59 @@ module RuboCop
70
70
  (send $_ :== ${lvar call})
71
71
  PATTERN
72
72
 
73
- # rubocop:disable Metrics/AbcSize
74
73
  def on_or(node)
75
- root_of_or_node = root_of_or_node(node)
76
- return unless node == root_of_or_node
74
+ return unless node == root_of_or_node(node)
77
75
  return unless nested_comparison?(node)
78
-
79
- return unless (variable, values = find_offending_var(node))
76
+ return unless (variable, values, skipped = find_offending_var(node))
80
77
  return if values.size < comparisons_threshold
81
78
 
82
79
  range = offense_range(values)
80
+ # Bail when an allowed method comparison sits between the collected
81
+ # values: collapsing them into a single `include?` would drop it.
82
+ return if skipped_within_range?(skipped, range)
83
83
 
84
84
  add_offense(range) do |corrector|
85
- elements = values.map(&:source).join(', ')
86
- argument = variable.lvar_type? ? variable_name(variable) : variable.source
87
- prefer_method = "[#{elements}].include?(#{argument})"
88
-
89
- corrector.replace(range, prefer_method)
85
+ corrector.replace(range, preferred_method(variable, values))
90
86
  end
91
87
  end
92
- # rubocop:enable Metrics/AbcSize
93
88
 
94
89
  private
95
90
 
96
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
- def find_offending_var(node, variables = Set.new, values = [])
91
+ def skipped_within_range?(skipped, range)
92
+ skipped.any? { |node_part| range.contains?(node_part.source_range) }
93
+ end
94
+
95
+ def preferred_method(variable, values)
96
+ elements = values.map(&:source).join(', ')
97
+ argument = variable.lvar_type? ? variable_name(variable) : variable.source
98
+ "[#{elements}].include?(#{argument})"
99
+ end
100
+
101
+ def find_offending_var(node, variables = Set.new, values = [], skipped = [])
98
102
  if node.or_type?
99
- find_offending_var(node.lhs, variables, values)
100
- find_offending_var(node.rhs, variables, values)
103
+ find_offending_var(node.lhs, variables, values, skipped)
104
+ find_offending_var(node.rhs, variables, values, skipped)
101
105
  elsif simple_double_comparison?(node)
102
106
  return
103
- elsif (var, obj = simple_comparison(node))
104
- return if allow_method_comparison? && obj.call_type?
107
+ elsif (comparison = simple_comparison(node))
108
+ collect_comparison(node, comparison, variables, values, skipped)
109
+ end
105
110
 
106
- variables << var
107
- return if variables.size > 1
111
+ [variables.first, values, skipped] if variables.any?
112
+ end
108
113
 
109
- values << obj
114
+ def collect_comparison(node, comparison, variables, values, skipped)
115
+ var, obj = comparison
116
+ if allow_method_comparison? && obj.call_type?
117
+ skipped << node
118
+ return
110
119
  end
111
120
 
112
- [variables.first, values] if variables.any?
121
+ variables << var
122
+ return if variables.size > 1
123
+
124
+ values << obj
113
125
  end
114
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
115
126
 
116
127
  def offense_range(values)
117
128
  values.first.parent.source_range.begin.join(values.last.parent.source_range.end)
@@ -89,6 +89,9 @@ module RuboCop
89
89
  # end
90
90
  # end.freeze
91
91
  #
92
+ # # good - `Data.define` declares an immutable value type
93
+ # CONST = Data.define(:foo, :bar)
94
+ #
92
95
  # @example
93
96
  # # Magic comment - shareable_constant_value: literal
94
97
  #
@@ -319,6 +322,8 @@ module RuboCop
319
322
  (const _ _)
320
323
  (send (const {nil? cbase} :Struct) :new ...)
321
324
  (block (send (const {nil? cbase} :Struct) :new ...) ...)
325
+ (send (const {nil? cbase} :Data) :define ...)
326
+ (block (send (const {nil? cbase} :Data) :define ...) ...)
322
327
  (send _ :freeze)
323
328
  (send {float int} {:+ :- :* :** :/ :% :<<} _)
324
329
  (send _ {:+ :- :* :** :/ :%} {float int})
@@ -91,8 +91,9 @@ module RuboCop
91
91
  end
92
92
  end
93
93
 
94
+ # rubocop:disable Metrics/CyclomaticComplexity
94
95
  def range_with_length_subtraction?(range_node, array_receiver)
95
- return false unless range_node.range_type?
96
+ return false unless range_node&.range_type?
96
97
 
97
98
  range_end = range_node.end
98
99
  range_start = range_node.begin
@@ -107,6 +108,7 @@ module RuboCop
107
108
 
108
109
  receivers_match_strict?(length_receiver, array_receiver)
109
110
  end
111
+ # rubocop:enable Metrics/CyclomaticComplexity
110
112
 
111
113
  def handle_range_pattern(receiver, range_node, index_arg)
112
114
  range_end = range_node.end
@@ -5,6 +5,9 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for redundant assignment before returning.
7
7
  #
8
+ # When there are comments between the assignment and reference,
9
+ # the cop will report an offense but it will not autocorrect.
10
+ #
8
11
  # @example
9
12
  # # bad
10
13
  # def test
@@ -99,6 +102,8 @@ module RuboCop
99
102
  def check_begin_node(node)
100
103
  if (assignment = redundant_assignment?(node))
101
104
  add_offense(assignment) do |corrector|
105
+ next if comments_between_assignment_and_reference?(assignment)
106
+
102
107
  expression = assignment.children[1]
103
108
  corrector.replace(assignment, expression.source)
104
109
  corrector.remove(assignment.right_sibling)
@@ -108,6 +113,12 @@ module RuboCop
108
113
  check_branch(last_expr)
109
114
  end
110
115
  end
116
+
117
+ def comments_between_assignment_and_reference?(assignment)
118
+ line_span = assignment.source_range.line..assignment.right_sibling.source_range.line
119
+
120
+ processed_source.each_comment_in_lines(line_span).any?
121
+ end
111
122
  end
112
123
  end
113
124
  end
@@ -14,6 +14,11 @@ module RuboCop
14
14
  # to prevent conflicting rules. This is because it respects user configurations
15
15
  # that want to enable `Lint/ConstantResolution` cop which is disabled by default.
16
16
  #
17
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
18
+ # installed, a leading `::` inside a namespace is also reported when the
19
+ # constant provably resolves to the same declaration with and without
20
+ # the base, i.e. nothing in the surrounding nesting shadows it.
21
+ #
17
22
  # @example
18
23
  # # bad
19
24
  # ::Const
@@ -41,13 +46,14 @@ module RuboCop
41
46
  # ::Const
42
47
  # end
43
48
  class RedundantConstantBase < Base
49
+ include ProjectIndexHelp
44
50
  extend AutoCorrector
45
51
 
46
52
  MSG = 'Remove redundant `::`.'
47
53
 
48
54
  def on_cbase(node)
49
55
  return if lint_constant_resolution_cop_enabled?
50
- return unless bad?(node)
56
+ return unless bad?(node) || provably_unshadowed?(node)
51
57
 
52
58
  add_offense(node) do |corrector|
53
59
  corrector.remove(node)
@@ -56,6 +62,36 @@ module RuboCop
56
62
 
57
63
  private
58
64
 
65
+ # When `AllCops/UseProjectIndex` is enabled, `::` inside a namespace is
66
+ # also redundant when the first constant segment resolves to the same
67
+ # declaration with and without the base, i.e. nothing in the
68
+ # surrounding nesting shadows it.
69
+ def provably_unshadowed?(node)
70
+ return false unless project_index
71
+ return false unless (segment = first_constant_segment(node))
72
+
73
+ nesting = module_nesting_ancestors_of(node)
74
+ .map { |ancestor| ancestor.identifier.const_name }.reverse
75
+
76
+ !nesting.empty? && same_resolution?(segment, nesting)
77
+ rescue StandardError
78
+ false
79
+ end
80
+
81
+ def same_resolution?(segment, nesting)
82
+ relative = project_index.resolve_constant(segment, nesting)
83
+ absolute = project_index.resolve_constant(segment, [])
84
+
85
+ !relative.nil? && !absolute.nil? && relative.name == absolute.name
86
+ end
87
+
88
+ def first_constant_segment(node)
89
+ parent = node.parent
90
+ return nil unless parent&.const_type?
91
+
92
+ parent.short_name.to_s
93
+ end
94
+
59
95
  def lint_constant_resolution_cop_enabled?
60
96
  lint_constant_resolution_config.fetch('Enabled', false)
61
97
  end
@@ -41,6 +41,10 @@ module RuboCop
41
41
  return unless name_node.value.end_with?('.rb')
42
42
 
43
43
  extension_range = extension_range(name_node)
44
+ # A backslash right before the extension means the path is escaped;
45
+ # dropping the extension would either produce invalid Ruby (a trailing
46
+ # backslash escaping the closing quote) or a confusing path, so skip it.
47
+ return if escaped_extension?(name_node, extension_range)
44
48
 
45
49
  add_offense(extension_range) do |corrector|
46
50
  corrector.remove(extension_range)
@@ -55,6 +59,11 @@ module RuboCop
55
59
 
56
60
  range_between(end_of_path_string - 4, end_of_path_string - 1)
57
61
  end
62
+
63
+ def escaped_extension?(name_node, extension_range)
64
+ offset = extension_range.begin_pos - name_node.source_range.begin_pos
65
+ name_node.source[0...offset].end_with?('\\')
66
+ end
58
67
  end
59
68
  end
60
69
  end
@@ -182,8 +182,8 @@ module RuboCop
182
182
  def unknown_variable_width?(sequence, arguments)
183
183
  return false unless sequence.variable_width?
184
184
 
185
- argument = arguments[sequence.variable_width_argument_number - 1]
186
- !numeric?(argument)
185
+ argument = positional_argument(arguments, sequence.variable_width_argument_number)
186
+ argument.nil? || !numeric?(argument)
187
187
  end
188
188
 
189
189
  # rubocop:disable Metrics/AbcSize
@@ -195,7 +195,7 @@ module RuboCop
195
195
  arguments.delete_at(sequence.variable_width_argument_number - 1)
196
196
  arguments.shift
197
197
  elsif sequence.arg_number
198
- arguments[sequence.arg_number.to_i - 1]
198
+ positional_argument(arguments, sequence.arg_number.to_i)
199
199
  else
200
200
  arguments.shift
201
201
  end
@@ -220,6 +220,15 @@ module RuboCop
220
220
  end
221
221
  end
222
222
 
223
+ # `Array#[]` raises `RangeError` for an index beyond the 64-bit range
224
+ # (e.g. `format('%*9999999999999999999999$d', 1)`) instead of returning `nil`,
225
+ # so the index needs to be checked before accessing the array.
226
+ def positional_argument(arguments, number)
227
+ index = number - 1
228
+
229
+ arguments[index] if index < arguments.size
230
+ end
231
+
223
232
  def numeric?(argument)
224
233
  argument.type?(:numeric, :str) ||
225
234
  rational_number?(argument) ||