rubocop 1.88.2 → 1.90.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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +139 -16
  3. data/exe/rubocop +1 -1
  4. data/lib/rubocop/cached_data.rb +7 -2
  5. data/lib/rubocop/cli/command/execute_runner.rb +31 -0
  6. data/lib/rubocop/cli.rb +2 -4
  7. data/lib/rubocop/comment_config/directive_range.rb +32 -0
  8. data/lib/rubocop/comment_config/disable_next.rb +97 -0
  9. data/lib/rubocop/comment_config.rb +81 -49
  10. data/lib/rubocop/config.rb +76 -5
  11. data/lib/rubocop/config_loader.rb +1 -2
  12. data/lib/rubocop/config_loader_resolver.rb +1 -2
  13. data/lib/rubocop/config_validator.rb +39 -3
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/badge.rb +8 -0
  16. data/lib/rubocop/cop/base.rb +40 -6
  17. data/lib/rubocop/cop/bundler.rb +19 -0
  18. data/lib/rubocop/cop/corrector.rb +4 -4
  19. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  20. data/lib/rubocop/cop/gemspec.rb +22 -0
  21. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  22. data/lib/rubocop/cop/generator.rb +4 -0
  23. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  24. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  25. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  26. data/lib/rubocop/cop/layout/class_structure.rb +108 -26
  27. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  28. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  29. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  30. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  31. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  32. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  33. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  34. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  35. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  36. data/lib/rubocop/cop/layout/line_length.rb +84 -12
  37. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  38. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  39. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  40. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  41. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  42. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +31 -34
  43. data/lib/rubocop/cop/layout.rb +114 -0
  44. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  45. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  46. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  47. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  48. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  49. data/lib/rubocop/cop/lint/constant_reassignment.rb +18 -21
  50. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  51. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  52. data/lib/rubocop/cop/lint/deprecated_reference.rb +194 -0
  53. data/lib/rubocop/cop/lint/duplicate_methods.rb +300 -22
  54. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  55. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  56. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  57. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  58. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -6
  59. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +4 -7
  60. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  61. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  62. data/lib/rubocop/cop/lint/name_typo.rb +270 -0
  63. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -4
  64. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  65. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  66. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  67. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  68. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  69. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  70. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  71. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +8 -2
  72. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  73. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  74. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  75. data/lib/rubocop/cop/lint/to_enum_arguments.rb +38 -3
  76. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  77. data/lib/rubocop/cop/lint/unused_private_method.rb +234 -0
  78. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  80. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  81. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  82. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  83. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  84. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  85. data/lib/rubocop/cop/lint/void.rb +18 -8
  86. data/lib/rubocop/cop/lint.rb +178 -0
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  88. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  89. data/lib/rubocop/cop/metrics.rb +31 -0
  90. data/lib/rubocop/cop/migration/department_name.rb +1 -6
  91. data/lib/rubocop/cop/migration.rb +13 -0
  92. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  93. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  94. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  95. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  96. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  97. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  98. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  99. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  100. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  101. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  102. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  103. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  104. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  105. data/lib/rubocop/cop/mixin/statement_modifier.rb +46 -4
  106. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  107. data/lib/rubocop/cop/mixin.rb +82 -77
  108. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  109. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  110. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  111. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  112. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  113. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  114. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  115. data/lib/rubocop/cop/naming.rb +31 -0
  116. data/lib/rubocop/cop/offense.rb +37 -10
  117. data/lib/rubocop/cop/registry.rb +180 -46
  118. data/lib/rubocop/cop/security.rb +19 -0
  119. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  120. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  121. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  122. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  124. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  125. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +79 -20
  127. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  128. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  129. data/lib/rubocop/cop/style/conditional_assignment.rb +23 -2
  130. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  131. data/lib/rubocop/cop/style/copyright.rb +5 -1
  132. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  133. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +76 -12
  134. data/lib/rubocop/cop/style/documentation.rb +47 -0
  135. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  136. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  137. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  138. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  139. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  140. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  141. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  142. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  143. data/lib/rubocop/cop/style/if_unless_modifier.rb +8 -50
  144. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  145. data/lib/rubocop/cop/style/lambda.rb +4 -0
  146. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  147. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +22 -7
  148. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  149. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  150. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  151. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  152. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  153. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  154. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  155. data/lib/rubocop/cop/style/negative_array_index.rb +3 -3
  156. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  157. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  158. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  159. data/lib/rubocop/cop/style/redundant_assignment.rb +12 -2
  160. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  161. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  162. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  163. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  164. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  165. data/lib/rubocop/cop/style/redundant_format.rb +14 -7
  166. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  167. data/lib/rubocop/cop/style/redundant_parentheses.rb +65 -67
  168. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  169. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  170. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  171. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  172. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  173. data/lib/rubocop/cop/style/safe_navigation.rb +11 -2
  174. data/lib/rubocop/cop/style/sample.rb +11 -6
  175. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  176. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  177. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  178. data/lib/rubocop/cop/style/sole_nested_conditional.rb +21 -5
  179. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  180. data/lib/rubocop/cop/style/static_class.rb +21 -0
  181. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  182. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  183. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  184. data/lib/rubocop/cop/style/time_now.rb +41 -0
  185. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  186. data/lib/rubocop/cop/style.rb +312 -0
  187. data/lib/rubocop/cop/team.rb +59 -6
  188. data/lib/rubocop/cop/util.rb +2 -4
  189. data/lib/rubocop/cop/variable_force/variable.rb +19 -8
  190. data/lib/rubocop/cops_documentation_generator.rb +4 -7
  191. data/lib/rubocop/directive_comment.rb +39 -3
  192. data/lib/rubocop/feature_loader.rb +0 -6
  193. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  194. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  195. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  196. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  197. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  198. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  199. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  200. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  201. data/lib/rubocop/lockfile.rb +18 -0
  202. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  203. data/lib/rubocop/lsp/routes.rb +89 -22
  204. data/lib/rubocop/lsp/runtime.rb +4 -0
  205. data/lib/rubocop/lsp/server.rb +4 -0
  206. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  207. data/lib/rubocop/mcp/server.rb +1 -2
  208. data/lib/rubocop/options.rb +10 -6
  209. data/lib/rubocop/path_util.rb +1 -2
  210. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  211. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  212. data/lib/rubocop/project_index_loader.rb +16 -0
  213. data/lib/rubocop/rake_task.rb +1 -2
  214. data/lib/rubocop/result_cache.rb +6 -4
  215. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  216. data/lib/rubocop/rspec/shared_contexts.rb +16 -2
  217. data/lib/rubocop/runner.rb +71 -43
  218. data/lib/rubocop/server/cache.rb +11 -7
  219. data/lib/rubocop/server/cli.rb +2 -4
  220. data/lib/rubocop/server.rb +14 -0
  221. data/lib/rubocop/target_finder.rb +4 -1
  222. data/lib/rubocop/target_ruby.rb +1 -1
  223. data/lib/rubocop/todo_audit.rb +111 -0
  224. data/lib/rubocop/version.rb +3 -5
  225. data/lib/rubocop.rb +20 -626
  226. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  227. metadata +28 -5
@@ -19,25 +19,27 @@ module RuboCop
19
19
  # x != y # or x = !y
20
20
  #
21
21
  class AmbiguousAssignment < Base
22
+ include CheckAssignment
22
23
  include RangeHelp
23
24
 
24
25
  MSG = 'Suspicious assignment detected. Did you mean `%<op>s`?'
25
26
 
26
- SIMPLE_ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn casgn].freeze
27
-
28
27
  MISTAKES = { '=-' => '-=', '=+' => '+=', '=*' => '*=', '=!' => '!=' }.freeze
29
28
 
30
- def on_asgn(node)
31
- return unless (rhs = node.expression)
29
+ alias on_csend on_send
30
+
31
+ private
32
32
 
33
- range = range_between(node.loc.operator.end_pos - 1, rhs.source_range.begin_pos + 1)
33
+ def check_assignment(node, rhs)
34
+ return unless rhs
35
+ return unless (operator = node.loc.operator)
36
+
37
+ range = range_between(operator.end_pos - 1, rhs.source_range.begin_pos + 1)
34
38
  source = range.source
35
39
  return unless MISTAKES.key?(source)
36
40
 
37
41
  add_offense(range, message: format(MSG, op: MISTAKES[source]))
38
42
  end
39
-
40
- SIMPLE_ASSIGNMENT_TYPES.each { |asgn_type| alias_method :"on_#{asgn_type}", :on_asgn }
41
43
  end
42
44
  end
43
45
  end
@@ -6,6 +6,11 @@ module RuboCop
6
6
  # Checks for ambiguous block association with method
7
7
  # when param passed without parentheses.
8
8
  #
9
+ # This cop also detects `do...end` blocks that are likely intended for
10
+ # an enumerable method in the arguments but actually bind to the outer
11
+ # method call. For example, in `render json: data.map do |x| x end`,
12
+ # Ruby parses the `do...end` block as belonging to `render`, not `map`.
13
+ #
9
14
  # This cop can customize allowed methods with `AllowedMethods`.
10
15
  # By default, there are no allowed methods.
11
16
  #
@@ -20,6 +25,18 @@ module RuboCop
20
25
  # # or (different meaning)
21
26
  # some_method(a) { |val| puts val }
22
27
  #
28
+ # # bad
29
+ # render json: data.map do |item|
30
+ # item.to_h
31
+ # end
32
+ #
33
+ # # good
34
+ # render json: data.map { |item| item.to_h }
35
+ #
36
+ # # good
37
+ # mapped = data.map { |item| item.to_h }
38
+ # render json: mapped
39
+ #
23
40
  # # good
24
41
  # # Operator methods require no disambiguation
25
42
  # foo == bar { |b| b.baz }
@@ -58,6 +75,19 @@ module RuboCop
58
75
  MSG = 'Parenthesize the param `%<param>s` to make sure that the ' \
59
76
  'block will be associated with the `%<method>s` method ' \
60
77
  'call.'
78
+ MSG_DO_END_BLOCK = '`%<inner_method>s` is called without a block because the ' \
79
+ '`do` block binds to `%<outer_method>s`. ' \
80
+ 'Use braces or extract to a variable.'
81
+
82
+ BLOCK_METHODS = %i[
83
+ map collect flat_map collect_concat
84
+ select filter find_all reject
85
+ find detect
86
+ each each_with_object each_with_index
87
+ reduce inject
88
+ sort_by min_by max_by
89
+ group_by filter_map
90
+ ].to_set.freeze
61
91
 
62
92
  def on_send(node)
63
93
  return unless node.arguments?
@@ -74,8 +104,36 @@ module RuboCop
74
104
  end
75
105
  alias on_csend on_send
76
106
 
107
+ def on_block(node)
108
+ return if node.braces?
109
+
110
+ send_node = node.send_node
111
+ block_method_arg = find_ambiguous_block_method(node)
112
+ return unless block_method_arg
113
+
114
+ add_offense(block_method_arg,
115
+ message: format(MSG_DO_END_BLOCK,
116
+ inner_method: block_method_arg.method_name,
117
+ outer_method: send_node.method_name))
118
+ end
119
+ alias on_numblock on_block
120
+ alias on_itblock on_block
121
+
77
122
  private
78
123
 
124
+ def find_ambiguous_block_method(block_node)
125
+ send_node = block_node.send_node
126
+ return unless send_node.send_type? && send_node.arguments?
127
+ return if send_node.parenthesized?
128
+
129
+ block_method_arg = find_block_method_arg(send_node)
130
+ return unless block_method_arg
131
+ return if allowed_method?(block_method_arg.method_name) ||
132
+ matches_allowed_pattern?(block_method_arg.source)
133
+
134
+ block_method_arg
135
+ end
136
+
79
137
  def ambiguous_block_association?(send_node)
80
138
  send_node.last_argument.any_block_type? && !send_node.last_argument.send_node.arguments?
81
139
  end
@@ -92,6 +150,19 @@ module RuboCop
92
150
  format(MSG, param: block_param.source, method: block_param.send_node.source)
93
151
  end
94
152
 
153
+ # Only the call the `do` block could actually have attached to is a candidate:
154
+ # the one whose source ends where the arguments end. This skips calls chained
155
+ # into another call (`foo bar.map.to_a`) and calls buried inside a
156
+ # parenthesized subcall (`foo bar(baz.map)`), which were never block candidates.
157
+ def find_block_method_arg(send_node)
158
+ end_pos = send_node.source_range.end_pos
159
+
160
+ send_node.last_argument.each_node(:call).find do |node|
161
+ node.source_range.end_pos == end_pos &&
162
+ BLOCK_METHODS.include?(node.method_name) && !node.arguments?
163
+ end
164
+ end
165
+
95
166
  def wrap_in_parentheses(corrector, node)
96
167
  range = node.loc.selector.end.join(node.first_argument.source_range.begin)
97
168
 
@@ -83,14 +83,13 @@ module RuboCop
83
83
  yield range.end if range.end
84
84
  end
85
85
 
86
- # rubocop:disable Metrics/CyclomaticComplexity
86
+ # rubocop:disable-next Metrics/CyclomaticComplexity
87
87
  def acceptable?(node)
88
88
  node.begin_type? ||
89
89
  node.literal? || rational_literal?(node) ||
90
90
  node.variable? || node.const_type? || node.self_type? ||
91
91
  (node.call_type? && acceptable_call?(node))
92
92
  end
93
- # rubocop:enable Metrics/CyclomaticComplexity
94
93
 
95
94
  def acceptable_call?(node)
96
95
  return true if node.unary_operation?
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for calls that pass the wrong number of positional arguments to a
7
+ # method whose definition is known from the project index.
8
+ #
9
+ # The check is powered by the project-wide index, so it only runs when
10
+ # `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
11
+ # Without the index the cop does nothing.
12
+ #
13
+ # Only calls on constant receivers (`Foo.bar`) are considered, and only
14
+ # when the receiver resolves in the index, its entire ancestry is resolved
15
+ # (so a method inherited from a gem or the standard library never produces
16
+ # an offense), and the called method resolves to a single, unambiguous
17
+ # signature. Calls that forward arguments (`*`, `**`, `...`) are skipped
18
+ # because their argument count is not statically known. Keyword arguments
19
+ # passed to a method that declares no keyword parameters are counted as the
20
+ # single positional hash they collapse into, matching Ruby's semantics.
21
+ #
22
+ # Constructor calls (`Foo.new`) and keyword-argument validation are out of
23
+ # scope: only positional arity of explicitly defined singleton methods is
24
+ # checked.
25
+ #
26
+ # @example
27
+ # # Given the project defines:
28
+ # # class Report
29
+ # # def self.generate(source, format); end
30
+ # # end
31
+ #
32
+ # # bad
33
+ # Report.generate(source)
34
+ #
35
+ # # bad
36
+ # Report.generate(source, :pdf, :extra)
37
+ #
38
+ # # good
39
+ # Report.generate(source, :pdf)
40
+ #
41
+ class ArgumentMismatch < Base
42
+ include ProjectIndexHelp
43
+ include IndexedMethodArity
44
+
45
+ MSG = 'Wrong number of arguments to `%<method>s` (given %<given>d, expected %<expected>s).'
46
+
47
+ def on_send(node)
48
+ return unless project_index
49
+ return unless node.receiver&.const_type?
50
+
51
+ shape = resolved_signature_shape(node)
52
+ return unless shape
53
+
54
+ min, max, has_keywords = shape
55
+ given = positional_argument_count(node, has_keywords)
56
+ return if given.nil? || arity_satisfied?(given, min, max)
57
+
58
+ add_offense(node.loc.selector, message: message(node, given, min, max))
59
+ end
60
+ alias on_csend on_send
61
+
62
+ private
63
+
64
+ def message(node, given, min, max)
65
+ format(MSG, method: node.method_name, given: given, expected: expected_range(min, max))
66
+ end
67
+
68
+ # `[min, max, has_keywords]` for the called singleton method, or nil when
69
+ # the receiver or its ancestry is not resolved.
70
+ def resolved_signature_shape(node)
71
+ declaration = resolve_constant_in_index(node.receiver)
72
+ return nil unless declaration.is_a?(Rubydex::Namespace)
73
+ return nil unless fully_resolved_index_ancestry?(declaration)
74
+
75
+ singleton_signature_shape(declaration, node.method_name)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+
80
+ def singleton_signature_shape(declaration, method_name)
81
+ member = indexed_singleton_member(declaration, "#{method_name}()")
82
+ return nil unless member
83
+
84
+ indexed_member_shape(member)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -175,10 +175,19 @@ module RuboCop
175
175
  end
176
176
 
177
177
  def ancestor_namespaces(node)
178
- node
179
- .each_ancestor(:class, :module)
180
- .map { |ancestor| ancestor.identifier.short_name }
181
- .reverse
178
+ ancestors = node.each_ancestor(:class, :module).reverse_each
179
+
180
+ ancestors.with_object([]) do |ancestor, namespaces|
181
+ append_namespaces(namespaces, ancestor.identifier)
182
+ end
183
+ end
184
+
185
+ # Compact definitions (e.g. `module A::B`) contribute every path segment,
186
+ # and an absolute one (e.g. `module ::A::B`) discards the enclosing namespaces.
187
+ def append_namespaces(namespaces, identifier)
188
+ namespaces.clear if identifier.absolute?
189
+ namespaces.concat(identifier_namespaces(identifier))
190
+ namespaces << identifier.short_name
182
191
  end
183
192
 
184
193
  def unconditional_definition?(node)
@@ -188,14 +197,11 @@ module RuboCop
188
197
  end
189
198
 
190
199
  def definition_name(node)
191
- identifier = node.identifier
200
+ namespaces = ancestor_namespaces(node)
201
+ append_namespaces(namespaces, node.identifier)
202
+ constant = namespaces.pop
192
203
 
193
- if identifier.namespace&.cbase_type?
194
- fully_qualified_name_for([], identifier.short_name)
195
- else
196
- namespaces = ancestor_namespaces(node) + identifier_namespaces(identifier)
197
- fully_qualified_name_for(namespaces, identifier.short_name)
198
- end
204
+ fully_qualified_name_for(namespaces, constant)
199
205
  end
200
206
 
201
207
  def identifier_namespaces(identifier)
@@ -209,21 +215,12 @@ module RuboCop
209
215
  def report_cross_file_collision(node, fully_qualified_name, display_name)
210
216
  return unless project_index
211
217
  return unless (declaration = project_index[fully_qualified_name.delete_prefix('::')])
212
- return unless (prior = prior_definition_in_other_file(declaration))
218
+ return unless (prior = prior_definition_in_other_file(declaration.definitions))
213
219
 
214
220
  msg = format(CROSS_FILE_MSG, constant: display_name, path: prior.location.to_file_path)
215
221
 
216
222
  add_offense(node, message: msg)
217
223
  end
218
-
219
- def prior_definition_in_other_file(declaration)
220
- current = processed_source.file_path
221
-
222
- declaration.definitions.find do |definition|
223
- other = definition.location.to_file_path
224
- !File.identical?(other, current)
225
- end
226
- end
227
224
  end
228
225
  end
229
226
  end
@@ -20,6 +20,12 @@ module RuboCop
20
20
  # to prevent conflicting rules. This is because it respects user configurations
21
21
  # that want to enable this cop which is disabled by default.
22
22
  #
23
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
24
+ # installed, only genuinely ambiguous constants are reported: those
25
+ # that resolve to a different declaration through the surrounding
26
+ # nesting than they would fully qualified. This makes the cop practical
27
+ # to enable without `Only`/`Ignore` lists.
28
+ #
23
29
  # @example
24
30
  # # By default checks every constant
25
31
  #
@@ -60,6 +66,8 @@ module RuboCop
60
66
  # Login
61
67
  #
62
68
  class ConstantResolution < Base
69
+ include ProjectIndexHelp
70
+
63
71
  MSG = 'Fully qualify this constant to avoid possibly ambiguous resolution.'
64
72
 
65
73
  # @!method unqualified_const?(node)
@@ -69,12 +77,29 @@ module RuboCop
69
77
 
70
78
  def on_const(node)
71
79
  return if !unqualified_const?(node) || node.parent&.defined_module || node.loc.nil?
80
+ return if project_index && !ambiguous_resolution?(node)
72
81
 
73
82
  add_offense(node)
74
83
  end
75
84
 
76
85
  private
77
86
 
87
+ # When `AllCops/UseProjectIndex` is enabled, only genuinely ambiguous
88
+ # references are reported: the constant resolves to a different
89
+ # declaration through the lexical nesting than it would fully
90
+ # qualified. Names that resolve identically either way, or that the
91
+ # index cannot resolve at all (e.g. constants from gems), are not
92
+ # reported.
93
+ def ambiguous_resolution?(node)
94
+ name = node.short_name.to_s
95
+ relative = project_index.resolve_constant(name, lexical_nesting_of(node))
96
+ absolute = project_index.resolve_constant(name, [])
97
+
98
+ !relative.nil? && !absolute.nil? && relative.name != absolute.name
99
+ rescue StandardError
100
+ false
101
+ end
102
+
78
103
  def const_name?(name)
79
104
  name = name.to_s
80
105
  (allowed_names.empty? || allowed_names.include?(name)) && !ignored_names.include?(name)
@@ -42,29 +42,70 @@ module RuboCop
42
42
  # # good
43
43
  # # rubocop:disable Layout/LineLength -- comment
44
44
  #
45
+ # # bad
46
+ # # rucocop:disable Layout/LineLength
47
+ #
48
+ # # good
49
+ # # rubocop:disable Layout/LineLength
50
+ #
51
+ # # bad
52
+ # # rubocop:disable Layout/LineLenght
53
+ #
54
+ # # good
55
+ # # rubocop:disable Layout/LineLength
56
+ #
45
57
  class CopDirectiveSyntax < Base
46
58
  COMMON_MSG = 'Malformed directive comment detected.'
47
59
 
48
60
  MISSING_MODE_NAME_MSG = 'The mode name is missing.'
49
- INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `todo`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
61
+ INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `todo`, `todo-next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
50
62
  MISSING_COP_NAME_MSG = 'The cop name is missing.'
51
63
  MALFORMED_COP_NAMES_MSG = 'Cop names must be separated by commas. ' \
52
64
  'Comment in the directive must start with `--`.'
65
+ NEXT_DIRECTIVE_AT_EOL_MSG = 'A `-next` directive must be on its own line, above the ' \
66
+ 'statement it applies to.'
67
+ INVALID_KEYWORD_MSG = 'The directive keyword must be `rubocop`, not `%<keyword>s`.'
68
+ UNKNOWN_COP_MSG = 'Unknown cop name `%<name>s`%<suggestion>s.'
69
+
70
+ # A comment that looks like an attempted directive: any keyword
71
+ # followed by a colon and a valid mode name.
72
+ NEAR_MISS_KEYWORD_REGEXP = /
73
+ \A\#+\s*(?<keyword>[A-Za-z][\w-]*)\s*:\s*
74
+ (?:#{DirectiveComment::MODES_PATTERN})\b
75
+ /x.freeze
53
76
 
54
77
  def on_new_investigation
55
78
  processed_source.comments.each do |comment|
56
79
  directive_comment = DirectiveComment.new(comment)
57
- next unless directive_comment.start_with_marker?
58
- next unless directive_comment.malformed?
59
-
60
- message = offense_message(directive_comment)
61
- add_offense(comment, message: message)
80
+ if directive_comment.start_with_marker?
81
+ check_directive(comment, directive_comment)
82
+ elsif (keyword = near_miss_keyword(comment))
83
+ message = format("#{COMMON_MSG} #{INVALID_KEYWORD_MSG}", keyword: keyword)
84
+ add_offense(comment, message: message)
85
+ end
62
86
  end
63
87
  end
64
88
 
65
89
  private
66
90
 
67
- # rubocop:disable Metrics/MethodLength
91
+ def check_directive(comment, directive_comment)
92
+ if directive_comment.malformed?
93
+ add_offense(comment, message: offense_message(directive_comment))
94
+ elsif misplaced_next_directive?(directive_comment)
95
+ add_offense(comment, message: NEXT_DIRECTIVE_AT_EOL_MSG)
96
+ elsif (name = unknown_cop_name(directive_comment))
97
+ add_offense(comment, message: unknown_cop_message(directive_comment, name))
98
+ end
99
+ end
100
+
101
+ # An EOL `disable-next` is not honored - it must fail loudly instead
102
+ # of silently doing nothing.
103
+ def misplaced_next_directive?(directive_comment)
104
+ directive_comment.disable_next? &&
105
+ !processed_source.comment_config.comment_only_line?(directive_comment.line_number)
106
+ end
107
+
108
+ # rubocop:disable-next Metrics/MethodLength
68
109
  def offense_message(directive_comment)
69
110
  comment = directive_comment.comment
70
111
  after_marker = comment.text.sub(DirectiveComment::DIRECTIVE_MARKER_REGEXP, '')
@@ -81,7 +122,42 @@ module RuboCop
81
122
 
82
123
  "#{COMMON_MSG} #{additional_msg}"
83
124
  end
84
- # rubocop:enable Metrics/MethodLength
125
+
126
+ def near_miss_keyword(comment)
127
+ match = comment.text.match(NEAR_MISS_KEYWORD_REGEXP)
128
+ return unless match
129
+
130
+ keyword = match[:keyword]
131
+ return if keyword == 'rubocop'
132
+
133
+ keyword if similar_to_rubocop?(keyword)
134
+ end
135
+
136
+ def similar_to_rubocop?(keyword)
137
+ return true if keyword.casecmp?('rubocop')
138
+ # `DidYouMean` is not always available - see `NameSimilarity`.
139
+ return false unless defined?(DidYouMean::Levenshtein)
140
+
141
+ DidYouMean::Levenshtein.distance(keyword.downcase, 'rubocop') <= 2
142
+ end
143
+
144
+ def unknown_cop_name(directive_comment)
145
+ return if directive_comment.push? || directive_comment.pop? || directive_comment.all_cops?
146
+
147
+ registry = directive_comment.cop_registry
148
+ directive_comment.raw_cop_names.find do |name|
149
+ next false if registry.department?(name)
150
+
151
+ qualified = registry.qualified_cop_name(name, nil, warn: false)
152
+ !registry.contains_cop_matching?([qualified])
153
+ end
154
+ end
155
+
156
+ def unknown_cop_message(directive_comment, name)
157
+ similar = NameSimilarity.find_similar_name(name, directive_comment.cop_registry.names)
158
+ suggestion = similar ? " (did you mean `#{similar}`?)" : ''
159
+ format(UNKNOWN_COP_MSG, name: name, suggestion: suggestion)
160
+ end
85
161
  end
86
162
  end
87
163
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for calls to methods and references to constants that are documented
7
+ # as deprecated with a YARD `@deprecated` tag.
8
+ #
9
+ # The check is powered by the project-wide index, so it only runs when
10
+ # `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
11
+ # Without the index the cop does nothing.
12
+ #
13
+ # Only references that can be resolved without type inference are checked:
14
+ # constants, method calls without an explicit receiver (or with `self`),
15
+ # which are looked up in the enclosing class or module and its ancestry,
16
+ # and calls whose receiver is a constant, which are looked up in that
17
+ # namespace's singleton class. Calls on arbitrary objects are not checked.
18
+ #
19
+ # References made from a definition that is itself deprecated are allowed,
20
+ # so deprecated implementations can keep calling each other.
21
+ #
22
+ # @example
23
+ # # Given a deprecated method and constant:
24
+ # #
25
+ # # class Api
26
+ # # # @deprecated Use `#new_method` instead.
27
+ # # def old_method
28
+ # # end
29
+ # #
30
+ # # # @deprecated
31
+ # # OLD_TIMEOUT = 10
32
+ # # end
33
+ #
34
+ # # bad
35
+ # class Client < Api
36
+ # def call
37
+ # old_method
38
+ # end
39
+ #
40
+ # def timeout
41
+ # OLD_TIMEOUT
42
+ # end
43
+ # end
44
+ #
45
+ # # good
46
+ # class Client < Api
47
+ # def call
48
+ # new_method
49
+ # end
50
+ #
51
+ # def timeout
52
+ # NEW_TIMEOUT
53
+ # end
54
+ # end
55
+ #
56
+ class DeprecatedReference < Base
57
+ include ProjectIndexHelp
58
+
59
+ METHOD_MSG = 'Method `%<name>s` is deprecated%<detail>s'
60
+ CONSTANT_MSG = 'Constant `%<name>s` is deprecated%<detail>s'
61
+
62
+ DEPRECATED_TAG = '@deprecated'
63
+ DEPRECATION_DETAIL = /#{DEPRECATED_TAG}\s+(.+)/.freeze
64
+
65
+ def on_new_investigation
66
+ @namespace_cache = {}
67
+ super
68
+ end
69
+
70
+ def on_send(node)
71
+ return unless project_index
72
+
73
+ declaration = method_declaration_for(node)
74
+ return unless declaration && deprecated?(declaration)
75
+ return if within_deprecated_definition?(node)
76
+
77
+ message = format(METHOD_MSG, name: node.method_name, detail: detail_for(declaration))
78
+ add_offense(node.loc.selector, message: message)
79
+ end
80
+ alias on_csend on_send
81
+
82
+ def on_const(node)
83
+ return unless project_index
84
+ return if node.parent&.defined_module
85
+
86
+ declaration = resolve_constant_in_index(node)
87
+ return unless declaration && deprecated?(declaration)
88
+ return if within_deprecated_definition?(node)
89
+
90
+ message = format(CONSTANT_MSG, name: node.const_name, detail: detail_for(declaration))
91
+ add_offense(node, message: message)
92
+ end
93
+
94
+ private
95
+
96
+ def method_declaration_for(node)
97
+ receiver = node.receiver
98
+
99
+ if receiver.nil? || receiver.self_type?
100
+ implicit_receiver_declaration(node)
101
+ elsif receiver.const_type?
102
+ const_receiver_declaration(node, receiver)
103
+ end
104
+ end
105
+
106
+ def implicit_receiver_declaration(node)
107
+ namespace = enclosing_namespace(node)
108
+ return nil unless namespace.is_a?(Rubydex::Namespace)
109
+
110
+ if singleton_context?(node)
111
+ indexed_singleton_member(namespace, "#{node.method_name}()")
112
+ else
113
+ namespace.find_member("#{node.method_name}()")
114
+ end
115
+ end
116
+
117
+ def const_receiver_declaration(node, receiver)
118
+ namespace = resolve_constant_in_index(receiver)
119
+ return nil unless namespace.is_a?(Rubydex::Namespace)
120
+
121
+ indexed_singleton_member(namespace, "#{node.method_name}()")
122
+ end
123
+
124
+ def enclosing_namespace(node)
125
+ namespace_node = node.each_ancestor(:class, :module).first
126
+ return project_index['Object'] unless namespace_node
127
+
128
+ @namespace_cache.fetch(namespace_node) do
129
+ @namespace_cache[namespace_node] = resolve_constant_in_index(namespace_node.identifier)
130
+ end
131
+ end
132
+
133
+ # Whether an implicit-receiver call runs with the class or module itself
134
+ # as `self` (directly in a namespace body, in a singleton method, or
135
+ # inside `class << self`) rather than inside an instance method.
136
+ def singleton_context?(node)
137
+ instance_def_seen = false
138
+
139
+ node.each_ancestor do |ancestor|
140
+ case ancestor.type
141
+ when :defs, :sclass
142
+ return true
143
+ when :def
144
+ instance_def_seen = true
145
+ when :class, :module
146
+ return !instance_def_seen
147
+ end
148
+ end
149
+
150
+ false
151
+ end
152
+
153
+ # Enumerated in a single pass that stops at the first non-deprecated
154
+ # definition, since materializing every definition of a declaration
155
+ # that is almost never deprecated dominates the cop's cost.
156
+ def deprecated?(declaration)
157
+ any = false
158
+
159
+ declaration.definitions.each do |definition|
160
+ return false unless definition.deprecated?
161
+
162
+ any = true
163
+ end
164
+
165
+ any
166
+ end
167
+
168
+ # References from a definition that is itself documented as deprecated
169
+ # are allowed.
170
+ def within_deprecated_definition?(node)
171
+ node.each_ancestor(:any_def, :class, :module).any? do |ancestor|
172
+ comments = processed_source.ast_with_comments[ancestor]
173
+ comments.any? { |comment| comment.text.include?(DEPRECATED_TAG) }
174
+ end
175
+ end
176
+
177
+ def detail_for(declaration)
178
+ text = deprecation_text(declaration)
179
+ return '.' if text.nil? || text.empty?
180
+
181
+ text.end_with?('.') ? ": #{text}" : ": #{text}."
182
+ end
183
+
184
+ def deprecation_text(declaration)
185
+ tag_comment = declaration.definitions.to_a.filter_map do |definition|
186
+ definition.comments.to_a.map(&:string).find { |string| string.include?(DEPRECATED_TAG) }
187
+ end.first
188
+
189
+ tag_comment && tag_comment[DEPRECATION_DETAIL, 1]&.strip
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end