rubocop 1.60.2 → 1.63.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/assets/output.css.erb +159 -0
  4. data/assets/output.html.erb +1 -160
  5. data/config/default.yml +64 -15
  6. data/lib/rubocop/cli/command/auto_generate_config.rb +12 -3
  7. data/lib/rubocop/cli/command/lsp.rb +2 -2
  8. data/lib/rubocop/cli.rb +6 -1
  9. data/lib/rubocop/config.rb +37 -10
  10. data/lib/rubocop/config_finder.rb +12 -2
  11. data/lib/rubocop/config_obsoletion.rb +1 -1
  12. data/lib/rubocop/config_validator.rb +14 -5
  13. data/lib/rubocop/cop/autocorrect_logic.rb +6 -1
  14. data/lib/rubocop/cop/base.rb +52 -6
  15. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +4 -8
  16. data/lib/rubocop/cop/correctors/for_to_each_corrector.rb +5 -13
  17. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +5 -1
  18. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -0
  19. data/lib/rubocop/cop/internal_affairs/method_name_end_with.rb +8 -6
  20. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +122 -28
  21. data/lib/rubocop/cop/internal_affairs/redundant_expect_offense_arguments.rb +34 -0
  22. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  23. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +14 -7
  24. data/lib/rubocop/cop/layout/end_alignment.rb +3 -1
  25. data/lib/rubocop/cop/layout/redundant_line_break.rb +11 -3
  26. data/lib/rubocop/cop/layout/space_before_block_braces.rb +19 -10
  27. data/lib/rubocop/cop/layout/space_inside_hash_literal_braces.rb +1 -1
  28. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -4
  29. data/lib/rubocop/cop/lint/debugger.rb +27 -2
  30. data/lib/rubocop/cop/lint/empty_conditional_body.rb +1 -1
  31. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -9
  32. data/lib/rubocop/cop/lint/redundant_with_index.rb +4 -0
  33. data/lib/rubocop/cop/lint/rescue_type.rb +1 -3
  34. data/lib/rubocop/cop/lint/script_permission.rb +3 -3
  35. data/lib/rubocop/cop/lint/syntax.rb +1 -1
  36. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -2
  37. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  38. data/lib/rubocop/cop/lint/void.rb +6 -1
  39. data/lib/rubocop/cop/mixin/code_length.rb +12 -1
  40. data/lib/rubocop/cop/mixin/method_complexity.rb +15 -6
  41. data/lib/rubocop/cop/mixin/multiline_expression_indentation.rb +1 -1
  42. data/lib/rubocop/cop/mixin/safe_assignment.rb +1 -1
  43. data/lib/rubocop/cop/naming/block_forwarding.rb +31 -12
  44. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  45. data/lib/rubocop/cop/naming/inclusive_language.rb +1 -2
  46. data/lib/rubocop/cop/naming/predicate_name.rb +2 -2
  47. data/lib/rubocop/cop/registry.rb +1 -1
  48. data/lib/rubocop/cop/style/alias.rb +1 -0
  49. data/lib/rubocop/cop/style/arguments_forwarding.rb +29 -8
  50. data/lib/rubocop/cop/style/case_like_if.rb +1 -1
  51. data/lib/rubocop/cop/style/class_vars.rb +3 -3
  52. data/lib/rubocop/cop/style/collection_compact.rb +3 -3
  53. data/lib/rubocop/cop/style/commented_keyword.rb +5 -2
  54. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -5
  55. data/lib/rubocop/cop/style/copyright.rb +16 -11
  56. data/lib/rubocop/cop/style/eval_with_location.rb +2 -0
  57. data/lib/rubocop/cop/style/exact_regexp_match.rb +2 -1
  58. data/lib/rubocop/cop/style/for.rb +2 -0
  59. data/lib/rubocop/cop/style/format_string.rb +9 -9
  60. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -1
  61. data/lib/rubocop/cop/style/hash_syntax.rb +6 -2
  62. data/lib/rubocop/cop/style/inverse_methods.rb +8 -8
  63. data/lib/rubocop/cop/style/invertible_unless_condition.rb +10 -5
  64. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +5 -8
  65. data/lib/rubocop/cop/style/map_into_array.rb +175 -0
  66. data/lib/rubocop/cop/style/map_to_hash.rb +1 -1
  67. data/lib/rubocop/cop/style/map_to_set.rb +1 -1
  68. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +1 -1
  69. data/lib/rubocop/cop/style/multiline_method_signature.rb +10 -1
  70. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +4 -0
  71. data/lib/rubocop/cop/style/nil_comparison.rb +2 -0
  72. data/lib/rubocop/cop/style/object_then.rb +5 -3
  73. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -3
  74. data/lib/rubocop/cop/style/raise_args.rb +4 -1
  75. data/lib/rubocop/cop/style/redundant_argument.rb +25 -2
  76. data/lib/rubocop/cop/style/redundant_assignment.rb +10 -2
  77. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +5 -4
  78. data/lib/rubocop/cop/style/redundant_each.rb +1 -1
  79. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  80. data/lib/rubocop/cop/style/redundant_line_continuation.rb +5 -0
  81. data/lib/rubocop/cop/style/redundant_percent_q.rb +1 -1
  82. data/lib/rubocop/cop/style/redundant_return.rb +6 -0
  83. data/lib/rubocop/cop/style/sample.rb +1 -3
  84. data/lib/rubocop/cop/team.rb +3 -0
  85. data/lib/rubocop/cop/utils/regexp_ranges.rb +1 -1
  86. data/lib/rubocop/cops_documentation_generator.rb +4 -2
  87. data/lib/rubocop/directive_comment.rb +10 -8
  88. data/lib/rubocop/formatter/clang_style_formatter.rb +3 -7
  89. data/lib/rubocop/formatter/html_formatter.rb +30 -10
  90. data/lib/rubocop/formatter/offense_count_formatter.rb +12 -2
  91. data/lib/rubocop/formatter/tap_formatter.rb +3 -7
  92. data/lib/rubocop/lockfile.rb +34 -4
  93. data/lib/rubocop/lsp/logger.rb +1 -1
  94. data/lib/rubocop/lsp/routes.rb +1 -1
  95. data/lib/rubocop/lsp/runtime.rb +1 -1
  96. data/lib/rubocop/lsp/server.rb +5 -2
  97. data/lib/rubocop/lsp/severity.rb +1 -1
  98. data/lib/rubocop/lsp.rb +29 -0
  99. data/lib/rubocop/magic_comment.rb +1 -1
  100. data/lib/rubocop/options.rb +11 -0
  101. data/lib/rubocop/path_util.rb +6 -2
  102. data/lib/rubocop/rspec/cop_helper.rb +8 -2
  103. data/lib/rubocop/rspec/expect_offense.rb +16 -8
  104. data/lib/rubocop/rspec/shared_contexts.rb +49 -18
  105. data/lib/rubocop/rspec/support.rb +2 -1
  106. data/lib/rubocop/runner.rb +12 -2
  107. data/lib/rubocop/target_finder.rb +84 -78
  108. data/lib/rubocop/target_ruby.rb +82 -80
  109. data/lib/rubocop/version.rb +19 -4
  110. data/lib/rubocop.rb +1 -0
  111. metadata +10 -6
@@ -244,6 +244,10 @@ module RuboCop
244
244
  end
245
245
  end
246
246
 
247
+ def parser_engine
248
+ @parser_engine ||= for_all_cops.fetch('ParserEngine', :parser_whitequark).to_sym
249
+ end
250
+
247
251
  def target_rails_version
248
252
  @target_rails_version ||=
249
253
  if for_all_cops['TargetRailsVersion']
@@ -259,6 +263,7 @@ module RuboCop
259
263
  PathUtil.smart_path(@loaded_path)
260
264
  end
261
265
 
266
+ # @return [String, nil]
262
267
  def bundler_lock_file_path
263
268
  return nil unless loaded_path
264
269
 
@@ -282,27 +287,49 @@ module RuboCop
282
287
  end
283
288
  end
284
289
 
290
+ # Returns target's locked gem versions (i.e. from Gemfile.lock or gems.locked)
291
+ # @returns [Hash{String => Gem::Version}] The locked gem versions, keyed by the gems' names.
292
+ def gem_versions_in_target
293
+ @gem_versions_in_target ||= read_gem_versions_from_target_lockfile
294
+ end
295
+
285
296
  def inspect # :nodoc:
286
297
  "#<#{self.class.name}:#{object_id} @loaded_path=#{loaded_path}>"
287
298
  end
288
299
 
289
300
  private
290
301
 
302
+ # @return [Float, nil] The Rails version as a `major.minor` Float.
291
303
  def target_rails_version_from_bundler_lock_file
292
304
  @target_rails_version_from_bundler_lock_file ||= read_rails_version_from_bundler_lock_file
293
305
  end
294
306
 
307
+ # @return [Float, nil] The Rails version as a `major.minor` Float.
295
308
  def read_rails_version_from_bundler_lock_file
296
- lock_file_path = bundler_lock_file_path
297
- return nil unless lock_file_path
298
-
299
- File.foreach(lock_file_path) do |line|
300
- # If Rails (or one of its frameworks) is in Gemfile.lock or gems.lock, there should be
301
- # a line like:
302
- # railties (X.X.X)
303
- result = line.match(/^\s+railties\s+\((\d+\.\d+)/)
304
- return result.captures.first.to_f if result
305
- end
309
+ return nil unless gem_versions_in_target
310
+
311
+ # Look for `railties` instead of `rails`, to support apps that only use a subset of `rails`
312
+ # See https://github.com/rubocop/rubocop/pull/11289
313
+ rails_version_in_target = gem_versions_in_target['railties']
314
+ return nil unless rails_version_in_target
315
+
316
+ gem_version_to_major_minor_float(rails_version_in_target)
317
+ end
318
+
319
+ # @param [Gem::Version] gem_version an object like `Gem::Version.new("7.1.2.3")`
320
+ # @return [Float] The major and minor version, like `7.1`
321
+ def gem_version_to_major_minor_float(gem_version)
322
+ segments = gem_version.canonical_segments
323
+ # segments.fetch(0).to_f + (segments.fetch(1, 0.0).to_f / 10)
324
+ Float("#{segments.fetch(0)}.#{segments.fetch(1, 0)}")
325
+ end
326
+
327
+ # @returns [Hash{String => Gem::Version}] The locked gem versions, keyed by the gems' names.
328
+ def read_gem_versions_from_target_lockfile
329
+ lockfile_path = bundler_lock_file_path
330
+ return nil unless lockfile_path
331
+
332
+ Lockfile.new(lockfile_path).gem_versions
306
333
  end
307
334
 
308
335
  def enable_cop?(qualified_cop_name, cop_options)
@@ -17,8 +17,8 @@ module RuboCop
17
17
  attr_writer :project_root
18
18
 
19
19
  def find_config_path(target_dir)
20
- find_project_dotfile(target_dir) || find_user_dotfile || find_user_xdg_config ||
21
- DEFAULT_FILE
20
+ find_project_dotfile(target_dir) || find_project_root_dot_config ||
21
+ find_user_dotfile || find_user_xdg_config || DEFAULT_FILE
22
22
  end
23
23
 
24
24
  # Returns the path RuboCop inferred as the root of the project. No file
@@ -41,6 +41,16 @@ module RuboCop
41
41
  find_file_upwards(DOTFILE, target_dir, project_root)
42
42
  end
43
43
 
44
+ def find_project_root_dot_config
45
+ return unless project_root
46
+
47
+ dotfile = File.join(project_root, '.config', DOTFILE)
48
+ return dotfile if File.exist?(dotfile)
49
+
50
+ xdg_config = File.join(project_root, '.config', 'rubocop', XDG_CONFIG)
51
+ xdg_config if File.exist?(xdg_config)
52
+ end
53
+
44
54
  def find_user_dotfile
45
55
  return unless ENV.key?('HOME')
46
56
 
@@ -52,7 +52,7 @@ module RuboCop
52
52
  def load_rules # rubocop:disable Metrics/AbcSize
53
53
  rules = LOAD_RULES_CACHE[self.class.files] ||=
54
54
  self.class.files.each_with_object({}) do |filename, hash|
55
- hash.merge!(YAML.safe_load(File.read(filename))) do |_key, first, second|
55
+ hash.merge!(YAML.safe_load(File.read(filename)) || {}) do |_key, first, second|
56
56
  case first
57
57
  when Hash
58
58
  first.merge(second)
@@ -18,6 +18,7 @@ module RuboCop
18
18
  # @api private
19
19
  CONFIG_CHECK_KEYS = %w[Enabled Safe SafeAutoCorrect AutoCorrect].to_set.freeze
20
20
  CONFIG_CHECK_DEPARTMENTS = %w[pending override_department].freeze
21
+ CONFIG_CHECK_AUTOCORRECTS = %w[always contextual disabled].freeze
21
22
  private_constant :CONFIG_CHECK_KEYS, :CONFIG_CHECK_DEPARTMENTS
22
23
 
23
24
  def_delegators :@config, :smart_loaded_path, :for_all_cops
@@ -248,23 +249,31 @@ module RuboCop
248
249
  end
249
250
  end
250
251
 
252
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
251
253
  def check_cop_config_value(hash, parent = nil)
252
254
  hash.each do |key, value|
253
255
  check_cop_config_value(value, key) if value.is_a?(Hash)
254
256
 
255
257
  next unless CONFIG_CHECK_KEYS.include?(key) && value.is_a?(String)
256
258
 
257
- next if key == 'Enabled' && CONFIG_CHECK_DEPARTMENTS.include?(value)
259
+ if key == 'Enabled' && !CONFIG_CHECK_DEPARTMENTS.include?(value)
260
+ supposed_values = 'a boolean'
261
+ elsif key == 'AutoCorrect' && !CONFIG_CHECK_AUTOCORRECTS.include?(value)
262
+ supposed_values = '`always`, `contextual`, `disabled`, or a boolean'
263
+ else
264
+ next
265
+ end
258
266
 
259
- raise ValidationError, msg_not_boolean(parent, key, value)
267
+ raise ValidationError, param_error_message(parent, key, value, supposed_values)
260
268
  end
261
269
  end
270
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
262
271
 
263
272
  # FIXME: Handling colors in exception messages like this is ugly.
264
- def msg_not_boolean(parent, key, value)
273
+ def param_error_message(parent, key, value, supposed_values)
265
274
  "#{Rainbow('').reset}" \
266
- "Property #{Rainbow(key).yellow} of cop #{Rainbow(parent).yellow} " \
267
- "is supposed to be a boolean and #{Rainbow(value).yellow} is not."
275
+ "Property #{Rainbow(key).yellow} of #{Rainbow(parent).yellow} cop " \
276
+ "is supposed to be #{supposed_values} and #{Rainbow(value).yellow} is not."
268
277
  end
269
278
  end
270
279
  end
@@ -32,7 +32,12 @@ module RuboCop
32
32
  # allow turning off autocorrect on a cop by cop basis
33
33
  return true unless cop_config
34
34
 
35
- return false if cop_config['AutoCorrect'] == false
35
+ # `false` is the same as `disabled` for backward compatibility.
36
+ return false if ['disabled', false].include?(cop_config['AutoCorrect'])
37
+
38
+ # When LSP is enabled, it is considered as editing source code,
39
+ # and autocorrection with `AutoCorrect: contextual` will not be performed.
40
+ return false if contextual_autocorrect? && LSP.enabled?
36
41
 
37
42
  # :safe_autocorrect is a derived option based on several command-line
38
43
  # arguments - see RuboCop::Options#add_autocorrection_options
@@ -70,6 +70,7 @@ module RuboCop
70
70
 
71
71
  def self.inherited(subclass)
72
72
  super
73
+ subclass.instance_variable_set(:@gem_requirements, gem_requirements.dup)
73
74
  Registry.global.enlist(subclass)
74
75
  end
75
76
 
@@ -126,6 +127,29 @@ module RuboCop
126
127
  false
127
128
  end
128
129
 
130
+ ## Gem requirements
131
+
132
+ @gem_requirements = {}
133
+
134
+ class << self
135
+ attr_reader :gem_requirements
136
+
137
+ # Register a version requirement for the given gem name.
138
+ # This cop will be skipped unless the target satisfies *all* requirements.
139
+ # @param [String] gem_name
140
+ # @param [Array<String>] version_requirements The version requirements,
141
+ # using the same syntax as a Gemfile, e.g. ">= 1.2.3"
142
+ #
143
+ # If omitted, any version of the gem will be accepted.
144
+ #
145
+ # https://guides.rubygems.org/patterns/#declaring-dependencies
146
+ #
147
+ # @api public
148
+ def requires_gem(gem_name, *version_requirements)
149
+ @gem_requirements[gem_name] = Gem::Requirement.new(version_requirements)
150
+ end
151
+ end
152
+
129
153
  def initialize(config = nil, options = nil)
130
154
  @config = config || Config.new
131
155
  @options = options || { debug: false }
@@ -232,6 +256,10 @@ module RuboCop
232
256
  @config.target_ruby_version
233
257
  end
234
258
 
259
+ def parser_engine
260
+ @config.parser_engine
261
+ end
262
+
235
263
  def target_rails_version
236
264
  @config.target_rails_version
237
265
  end
@@ -241,6 +269,7 @@ module RuboCop
241
269
  end
242
270
 
243
271
  def relevant_file?(file)
272
+ return false unless target_satisfies_all_gem_version_requirements?
244
273
  return true unless @config.clusivity_config_for_badge?(self.class.badge)
245
274
 
246
275
  file == RuboCop::AST::ProcessedSource::STRING_SOURCE_NAME ||
@@ -254,7 +283,7 @@ module RuboCop
254
283
 
255
284
  # There should be very limited reasons for a Cop to do it's own parsing
256
285
  def parse(source, path = nil)
257
- ProcessedSource.new(source, target_ruby_version, path)
286
+ ProcessedSource.new(source, target_ruby_version, path, parser_engine: parser_engine)
258
287
  end
259
288
 
260
289
  # @api private
@@ -305,6 +334,17 @@ module RuboCop
305
334
  @current_original = original
306
335
  end
307
336
 
337
+ # @api private
338
+ def always_autocorrect?
339
+ # `true` is the same as `'always'` for backward compatibility.
340
+ ['always', true].include?(cop_config.fetch('AutoCorrect', 'always'))
341
+ end
342
+
343
+ # @api private
344
+ def contextual_autocorrect?
345
+ cop_config.fetch('AutoCorrect', 'always') == 'contextual'
346
+ end
347
+
308
348
  def inspect # :nodoc:
309
349
  "#<#{self.class.name}:#{object_id} @config=#{@config} @options=#{@options}>"
310
350
  end
@@ -389,7 +429,7 @@ module RuboCop
389
429
  def use_corrector(range, corrector)
390
430
  if autocorrect?
391
431
  attempt_correction(range, corrector)
392
- elsif corrector && cop_config.fetch('AutoCorrect', true)
432
+ elsif corrector && (always_autocorrect? || (contextual_autocorrect? && !LSP.enabled?))
393
433
  :uncorrected
394
434
  else
395
435
  :unsupported
@@ -482,10 +522,16 @@ module RuboCop
482
522
  )
483
523
  end
484
524
 
485
- # This experimental feature has been under consideration for a while.
486
- # @api private
487
- def lsp_mode?
488
- ARGV.include?('--lsp')
525
+ def target_satisfies_all_gem_version_requirements?
526
+ self.class.gem_requirements.all? do |gem_name, version_req|
527
+ all_gem_versions_in_target = @config.gem_versions_in_target
528
+ next false unless all_gem_versions_in_target
529
+
530
+ gem_version_in_target = all_gem_versions_in_target[gem_name]
531
+ next false unless gem_version_in_target
532
+
533
+ version_req.satisfied_by?(gem_version_in_target)
534
+ end
489
535
  end
490
536
  end
491
537
  end
@@ -34,18 +34,14 @@ module RuboCop
34
34
  end
35
35
 
36
36
  def offending_range
37
+ begin_range = block_node.source_range.begin
38
+
37
39
  if block_node.arguments?
38
- replacement_range(argument_node.source_range.end_pos)
40
+ begin_range.join(argument_node.source_range.end)
39
41
  else
40
- replacement_range(block_node.loc.begin.end_pos)
42
+ begin_range.join(block_node.loc.begin.end)
41
43
  end
42
44
  end
43
-
44
- def replacement_range(end_pos)
45
- Parser::Source::Range.new(block_node.source_range.source_buffer,
46
- block_node.source_range.begin_pos,
47
- end_pos)
48
- end
49
45
  end
50
46
  end
51
47
  end
@@ -15,6 +15,8 @@ module RuboCop
15
15
  end
16
16
 
17
17
  def call(corrector)
18
+ offending_range = for_node.source_range.begin.join(end_range)
19
+
18
20
  corrector.replace(offending_range, correction)
19
21
  end
20
22
 
@@ -40,11 +42,11 @@ module RuboCop
40
42
  collection_node.range_type? || collection_node.or_type? || collection_node.and_type?
41
43
  end
42
44
 
43
- def end_position
45
+ def end_range
44
46
  if for_node.do?
45
- keyword_begin.end_pos
47
+ keyword_begin.end
46
48
  else
47
- collection_end.end_pos
49
+ collection_end.end
48
50
  end
49
51
  end
50
52
 
@@ -59,16 +61,6 @@ module RuboCop
59
61
  collection_node.source_range
60
62
  end
61
63
  end
62
-
63
- def offending_range
64
- replacement_range(end_position)
65
- end
66
-
67
- def replacement_range(end_pos)
68
- Parser::Source::Range.new(for_node.source_range.source_buffer,
69
- for_node.source_range.begin_pos,
70
- end_pos)
71
- end
72
64
  end
73
65
  end
74
66
  end
@@ -76,7 +76,9 @@ module RuboCop
76
76
  PATTERN
77
77
 
78
78
  def on_new_investigation
79
- add_global_offense(MISSING_MSG) unless required_ruby_version?(processed_source.ast)
79
+ return if processed_source.ast && required_ruby_version?(processed_source.ast)
80
+
81
+ add_global_offense(MISSING_MSG)
80
82
  end
81
83
 
82
84
  def on_send(node)
@@ -110,6 +112,8 @@ module RuboCop
110
112
  end
111
113
  end
112
114
 
115
+ return unless required_ruby_version
116
+
113
117
  required_ruby_version.str_content.scan(/\d/).first(2).join('.')
114
118
  end
115
119
 
@@ -45,6 +45,7 @@ module RuboCop
45
45
  EXPECT_OFFENSE_DESCRIPTION_MAPPING = {
46
46
  /\A(does not|doesn't) (register|find|flag|report)/ => 'registers',
47
47
  /\A(does not|doesn't) add (a|an|any )?offense/ => 'registers an offense',
48
+ /\Aregisters no offense/ => 'registers an offense',
48
49
  /\Aaccepts\b/ => 'registers'
49
50
  }.freeze
50
51
 
@@ -30,6 +30,7 @@ module RuboCop
30
30
  extend AutoCorrector
31
31
 
32
32
  MSG = 'Use `%<method_name>s` instead of `%<method_suffix>s`.'
33
+ RESTRICT_ON_SEND = %i[end_with?].freeze
33
34
  SUGGEST_METHOD_FOR_SUFFIX = {
34
35
  '=' => 'assignment_method?',
35
36
  '!' => 'bang_method?',
@@ -51,14 +52,15 @@ module RuboCop
51
52
 
52
53
  def on_send(node)
53
54
  method_name_end_with?(node) do |method_name_node, end_with_arg|
55
+ next unless method_name_node.receiver
56
+
57
+ preferred_method = SUGGEST_METHOD_FOR_SUFFIX[end_with_arg.value]
54
58
  range = range(method_name_node, node)
55
- message = format(
56
- MSG,
57
- method_name: SUGGEST_METHOD_FOR_SUFFIX[end_with_arg.value],
58
- method_suffix: range.source
59
- )
59
+ message = format(MSG, method_name: preferred_method, method_suffix: range.source)
60
60
 
61
- add_offense(range, message: message)
61
+ add_offense(range, message: message) do |corrector|
62
+ corrector.replace(range, preferred_method)
63
+ end
62
64
  end
63
65
  end
64
66
  alias on_csend on_send
@@ -26,10 +26,18 @@ module RuboCop
26
26
  MSG = 'Precede `%<method>s` with a `@!method` YARD directive.'
27
27
  MSG_WRONG_NAME = '`@!method` YARD directive has invalid method name, ' \
28
28
  'use `%<expected>s` instead of `%<actual>s`.'
29
+ MSG_MISSING_SCOPE_SELF = 'Follow the `@!method` YARD directive with ' \
30
+ '`@!scope class` if it is a class method.'
31
+ MSG_WRONG_SCOPE_SELF = 'Do not use the `@!scope class` YARD directive if it ' \
32
+ 'is not a class method.'
29
33
  MSG_TOO_MANY = 'Multiple `@!method` YARD directives found for this matcher.'
30
34
 
31
35
  RESTRICT_ON_SEND = %i[def_node_matcher def_node_search].to_set.freeze
32
- REGEXP = /^\s*#\s*@!method\s+(?<method_name>[a-z0-9_]+[?!]?)(?:\((?<args>.*)\))?/.freeze
36
+ REGEXP_METHOD = /
37
+ ^\s*\#\s*
38
+ @!method\s+(?<receiver>self\.)?(?<method_name>[a-z0-9_]+[?!]?)(?:\((?<args>.*)\))?
39
+ /x.freeze
40
+ REGEXP_SCOPE = /^\s*\#\s*@!scope class/.freeze
33
41
 
34
42
  # @!method pattern_matcher?(node)
35
43
  def_node_matcher :pattern_matcher?, <<~PATTERN
@@ -40,14 +48,15 @@ module RuboCop
40
48
  return if node.arguments.none?
41
49
  return unless valid_method_name?(node)
42
50
 
43
- actual_name = node.first_argument.value
51
+ actual_name = node.first_argument.value.to_s
52
+
53
+ # Ignore cases where the method has a receiver that isn't self
54
+ return if actual_name.include?('.') && !actual_name.start_with?('self.')
55
+
44
56
  directives = method_directives(node)
45
57
  return too_many_directives(node) if directives.size > 1
46
58
 
47
- directive = directives.first
48
- return if directive_correct?(directive, actual_name)
49
-
50
- register_offense(node, directive, actual_name)
59
+ process_directive(node, actual_name, directives.first)
51
60
  end
52
61
 
53
62
  private
@@ -58,44 +67,112 @@ module RuboCop
58
67
 
59
68
  def method_directives(node)
60
69
  comments = processed_source.ast_with_comments[node]
61
-
62
- comments.filter_map do |comment|
63
- match = comment.text.match(REGEXP)
70
+ group_comments(comments).filter_map do |comment_method, comment_scope|
71
+ match = comment_method.text.match(REGEXP_METHOD)
64
72
  next unless match
65
73
 
66
- { node: comment, method_name: match[:method_name], args: match[:args] }
74
+ {
75
+ node_method: comment_method,
76
+ node_scope: comment_scope,
77
+ method_name: match[:method_name],
78
+ args: match[:args],
79
+ receiver: match[:receiver],
80
+ has_scope_directive: comment_scope&.text&.match?(REGEXP_SCOPE)
81
+ }
82
+ end
83
+ end
84
+
85
+ def group_comments(comments)
86
+ result = []
87
+ comments.each.with_index do |comment, index|
88
+ # Grab the scope directive if it is preceded by a method directive
89
+ if comment.text.include?('@!method')
90
+ result << if (next_comment = comments[index + 1])&.text&.include?('@!scope')
91
+ [comment, next_comment]
92
+ else
93
+ [comment, nil]
94
+ end
95
+ end
67
96
  end
97
+ result
68
98
  end
69
99
 
70
100
  def too_many_directives(node)
71
101
  add_offense(node, message: MSG_TOO_MANY)
72
102
  end
73
103
 
74
- def directive_correct?(directive, actual_name)
75
- directive && directive[:method_name] == actual_name.to_s
104
+ def process_directive(node, actual_name, directive)
105
+ return unless (offense_type = directive_offense_type(directive, actual_name))
106
+
107
+ register_offense(offense_type, node, directive, actual_name)
108
+ end
109
+
110
+ def directive_offense_type(directive, actual_name)
111
+ return :missing_directive unless directive
112
+
113
+ return :wrong_scope if wrong_scope(directive, actual_name)
114
+ return :no_scope if no_scope(directive, actual_name)
115
+
116
+ # The method directive being prefixed by 'self.' is always an offense.
117
+ # The matched method_name does not contain the receiver but the
118
+ # def_node_match method name may so it must be removed.
119
+ if directive[:method_name] != remove_receiver(actual_name) || directive[:receiver]
120
+ :wrong_name
121
+ end
122
+ end
123
+
124
+ def wrong_scope(directive, actual_name)
125
+ !actual_name.start_with?('self.') && directive[:has_scope_directive]
126
+ end
127
+
128
+ def no_scope(directive, actual_name)
129
+ actual_name.start_with?('self.') && !directive[:has_scope_directive]
76
130
  end
77
131
 
78
- def register_offense(node, directive, actual_name)
79
- message = formatted_message(directive, actual_name, node.method_name)
132
+ def register_offense(offense_type, node, directive, actual_name)
133
+ message = formatted_message(offense_type, directive, actual_name, node.method_name)
80
134
 
81
135
  add_offense(node, message: message) do |corrector|
82
- if directive
83
- correct_directive(corrector, directive, actual_name)
84
- else
85
- insert_directive(corrector, node, actual_name)
136
+ case offense_type
137
+ when :wrong_name
138
+ correct_method_directive(corrector, directive, actual_name)
139
+ when :wrong_scope
140
+ remove_scope_directive(corrector, directive)
141
+ when :no_scope
142
+ insert_scope_directive(corrector, directive[:node_method])
143
+ when :missing_directive
144
+ insert_method_directive(corrector, node, actual_name)
86
145
  end
87
146
  end
88
147
  end
89
148
 
90
- def formatted_message(directive, actual_name, method_name)
91
- if directive
92
- format(MSG_WRONG_NAME, expected: actual_name, actual: directive[:method_name])
93
- else
149
+ # rubocop:disable Metrics/MethodLength
150
+ def formatted_message(offense_type, directive, actual_name, method_name)
151
+ case offense_type
152
+ when :wrong_name
153
+ # Add the receiver to the name when showing an offense
154
+ current_name = if directive[:receiver]
155
+ directive[:receiver] + directive[:method_name]
156
+ else
157
+ directive[:method_name]
158
+ end
159
+ # The correct name will never include a receiver, remove it
160
+ format(MSG_WRONG_NAME, expected: remove_receiver(actual_name), actual: current_name)
161
+ when :wrong_scope
162
+ MSG_WRONG_SCOPE_SELF
163
+ when :no_scope
164
+ MSG_MISSING_SCOPE_SELF
165
+ when :missing_directive
94
166
  format(MSG, method: method_name)
95
167
  end
96
168
  end
169
+ # rubocop:enable Metrics/MethodLength
170
+
171
+ def remove_receiver(current)
172
+ current.delete_prefix('self.')
173
+ end
97
174
 
98
- def insert_directive(corrector, node, actual_name)
175
+ def insert_method_directive(corrector, node, actual_name)
99
176
  # If the pattern matcher uses arguments (`%1`, `%2`, etc.), include them in the directive
100
177
  arguments = pattern_arguments(node.arguments[1].source)
101
178
 
@@ -107,6 +184,14 @@ module RuboCop
107
184
  corrector.insert_before(range, directive)
108
185
  end
109
186
 
187
+ def insert_scope_directive(corrector, node)
188
+ range = range_with_surrounding_space(node.source_range, side: :left, newlines: false)
189
+ indentation = range.source.match(/^\s*/)[0]
190
+ directive = "\n#{indentation}# @!scope class"
191
+
192
+ corrector.insert_after(node, directive)
193
+ end
194
+
110
195
  def pattern_arguments(pattern)
111
196
  arguments = %w[node]
112
197
  max_pattern_var = pattern.scan(/(?<=%)\d+/).map(&:to_i).max
@@ -134,12 +219,21 @@ module RuboCop
134
219
  end
135
220
  end
136
221
 
137
- def correct_directive(corrector, directive, actual_name)
138
- correct = "@!method #{actual_name}"
139
- regexp = /@!method\s+#{Regexp.escape(directive[:method_name])}/
222
+ def correct_method_directive(corrector, directive, actual_name)
223
+ correct = "@!method #{remove_receiver(actual_name)}"
224
+ current_name = (directive[:receiver] || '') + directive[:method_name]
225
+ regexp = /@!method\s+#{Regexp.escape(current_name)}/
226
+
227
+ replacement = directive[:node_method].text.gsub(regexp, correct)
228
+ corrector.replace(directive[:node_method], replacement)
229
+ end
140
230
 
141
- replacement = directive[:node].text.gsub(regexp, correct)
142
- corrector.replace(directive[:node], replacement)
231
+ def remove_scope_directive(corrector, directive)
232
+ range = range_by_whole_lines(
233
+ directive[:node_scope].source_range,
234
+ include_final_newline: true
235
+ )
236
+ corrector.remove(range)
143
237
  end
144
238
  end
145
239
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module InternalAffairs
6
+ # Checks for redundant arguments of `RuboCop::RSpec::ExpectOffense`'s methods.
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # expect_no_offenses('code', keyword: keyword)
12
+ #
13
+ # # good
14
+ # expect_no_offenses('code')
15
+ #
16
+ class RedundantExpectOffenseArguments < Base
17
+ extend AutoCorrector
18
+
19
+ MSG = 'Remove the redundant arguments.'
20
+ RESTRICT_ON_SEND = %i[expect_no_offenses].freeze
21
+
22
+ def on_send(node)
23
+ return if node.arguments.one? || !node.arguments[1]&.hash_type?
24
+
25
+ range = node.first_argument.source_range.end.join(node.last_argument.source_range.end)
26
+
27
+ add_offense(range) do |corrector|
28
+ corrector.remove(range)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -20,6 +20,7 @@ require_relative 'internal_affairs/offense_location_keyword'
20
20
  require_relative 'internal_affairs/processed_source_buffer_name'
21
21
  require_relative 'internal_affairs/redundant_context_config_parameter'
22
22
  require_relative 'internal_affairs/redundant_described_class_as_subject'
23
+ require_relative 'internal_affairs/redundant_expect_offense_arguments'
23
24
  require_relative 'internal_affairs/redundant_let_rubocop_config_new'
24
25
  require_relative 'internal_affairs/redundant_location_argument'
25
26
  require_relative 'internal_affairs/redundant_message_argument'