rubocop 1.62.0 → 1.63.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +23 -3
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +12 -3
  5. data/lib/rubocop/config.rb +33 -10
  6. data/lib/rubocop/config_obsoletion.rb +1 -1
  7. data/lib/rubocop/cop/base.rb +37 -0
  8. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +3 -1
  9. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -0
  10. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +122 -28
  11. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +14 -7
  12. data/lib/rubocop/cop/layout/redundant_line_break.rb +8 -2
  13. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -4
  14. data/lib/rubocop/cop/lint/debugger.rb +27 -2
  15. data/lib/rubocop/cop/lint/redundant_with_index.rb +4 -0
  16. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  17. data/lib/rubocop/cop/mixin/code_length.rb +12 -1
  18. data/lib/rubocop/cop/mixin/method_complexity.rb +15 -6
  19. data/lib/rubocop/cop/mixin/multiline_expression_indentation.rb +1 -1
  20. data/lib/rubocop/cop/mixin/safe_assignment.rb +1 -1
  21. data/lib/rubocop/cop/naming/block_forwarding.rb +31 -12
  22. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  23. data/lib/rubocop/cop/naming/inclusive_language.rb +1 -2
  24. data/lib/rubocop/cop/style/alias.rb +1 -0
  25. data/lib/rubocop/cop/style/class_vars.rb +3 -3
  26. data/lib/rubocop/cop/style/collection_compact.rb +3 -3
  27. data/lib/rubocop/cop/style/copyright.rb +16 -11
  28. data/lib/rubocop/cop/style/eval_with_location.rb +2 -0
  29. data/lib/rubocop/cop/style/exact_regexp_match.rb +2 -1
  30. data/lib/rubocop/cop/style/for.rb +2 -0
  31. data/lib/rubocop/cop/style/format_string.rb +9 -9
  32. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -1
  33. data/lib/rubocop/cop/style/map_into_array.rb +175 -0
  34. data/lib/rubocop/cop/style/map_to_hash.rb +1 -1
  35. data/lib/rubocop/cop/style/map_to_set.rb +1 -1
  36. data/lib/rubocop/cop/style/multiline_method_signature.rb +10 -1
  37. data/lib/rubocop/cop/style/nil_comparison.rb +2 -0
  38. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  39. data/lib/rubocop/cop/style/redundant_argument.rb +24 -1
  40. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +5 -4
  41. data/lib/rubocop/cop/style/redundant_each.rb +1 -1
  42. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  43. data/lib/rubocop/cop/style/redundant_line_continuation.rb +8 -14
  44. data/lib/rubocop/cop/style/redundant_percent_q.rb +1 -1
  45. data/lib/rubocop/cop/team.rb +3 -0
  46. data/lib/rubocop/cop/utils/regexp_ranges.rb +1 -1
  47. data/lib/rubocop/directive_comment.rb +10 -8
  48. data/lib/rubocop/formatter/clang_style_formatter.rb +3 -7
  49. data/lib/rubocop/formatter/tap_formatter.rb +3 -7
  50. data/lib/rubocop/lockfile.rb +34 -4
  51. data/lib/rubocop/rspec/expect_offense.rb +15 -7
  52. data/lib/rubocop/rspec/shared_contexts.rb +13 -1
  53. data/lib/rubocop/runner.rb +3 -0
  54. data/lib/rubocop/target_ruby.rb +1 -1
  55. data/lib/rubocop/version.rb +2 -2
  56. data/lib/rubocop.rb +1 -0
  57. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71f9543fd4a2366245df52234cec7a2492a2e4df631fec99e466de8b82cde5d7
4
- data.tar.gz: 99bf7b324f099186e5918469d3fd5d954e91317cb572e96f48a253c4d4ace709
3
+ metadata.gz: 8eadae0a09b33bf6a4ae3621ad1b86ba4eff0e7601d3ceb89dcf115759e9c022
4
+ data.tar.gz: ae113bb4d3eab63b4a551e36541f14d4e3bf3aae3354b853fb45960e80b90bbb
5
5
  SHA512:
6
- metadata.gz: a0128453654e2f6c5b5000c70931edb3dcccc2d1eb54c08b4e1af6ea393f7192ad0aeaaf9c07a136138fee6be7702caa22cdd3e0f3a46a25a6508102ac0759a1
7
- data.tar.gz: 989aade51c2112e02b3d4e130b99d1096116fca3a8cc0d47e8c25de8eea419afb242969f9a48c7a8d04ab1b9a8563dc138b65e9b364d89758ebcdd3444147893
6
+ metadata.gz: 6343643a7515b53e3e880a6cb92e256f37a3c1ee42c757af0506bf6457d900d36ea95b48bef2c6e008b5a96ba957f28b448a8e62da10ef1c27e6fe88dfbc1935
7
+ data.tar.gz: c67f434f594eb5b3f609d1e36140c04a6c79ce784b2d614c0e57ed4d22f7bea437a4edf89de0b976c425c7a2fcaf54b46ce66391eb9e1b61f60fa03bd46fdebc
data/README.md CHANGED
@@ -53,7 +53,7 @@ To prevent an unwanted RuboCop update you might want to use a conservative versi
53
53
  in your `Gemfile`:
54
54
 
55
55
  ```rb
56
- gem 'rubocop', '~> 1.62', require: false
56
+ gem 'rubocop', '~> 1.63', require: false
57
57
  ```
58
58
 
59
59
  See [our versioning policy](https://docs.rubocop.org/rubocop/versioning.html) for further details.
data/config/default.yml CHANGED
@@ -156,13 +156,14 @@ AllCops:
156
156
  # included.
157
157
  SuggestExtensions:
158
158
  rubocop-rails: [rails]
159
- rubocop-rspec: [rspec, rspec-rails]
159
+ rubocop-rspec: [rspec]
160
160
  rubocop-minitest: [minitest]
161
161
  rubocop-sequel: [sequel]
162
162
  rubocop-rake: [rake]
163
163
  rubocop-graphql: [graphql]
164
164
  rubocop-capybara: [capybara]
165
165
  rubocop-factory_bot: [factory_bot, factory_bot_rails]
166
+ rubocop-rspec_rails: [rspec-rails]
166
167
  # Enable/Disable checking the methods extended by Active Support.
167
168
  ActiveSupportExtensionsEnabled: false
168
169
 
@@ -1657,7 +1658,7 @@ Lint/Debugger:
1657
1658
  Description: 'Check for debugger calls.'
1658
1659
  Enabled: true
1659
1660
  VersionAdded: '0.14'
1660
- VersionChanged: '1.46'
1661
+ VersionChanged: '1.63'
1661
1662
  DebuggerMethods:
1662
1663
  # Groups are available so that a specific group can be disabled in
1663
1664
  # a user's configuration, but are otherwise not significant.
@@ -1670,8 +1671,14 @@ Lint/Debugger:
1670
1671
  - Kernel.byebug
1671
1672
  - Kernel.remote_byebug
1672
1673
  Capybara:
1674
+ - page.save_and_open_page
1675
+ - page.save_and_open_screenshot
1676
+ - page.save_page
1677
+ - page.save_screenshot
1673
1678
  - save_and_open_page
1674
1679
  - save_and_open_screenshot
1680
+ - save_page
1681
+ - save_screenshot
1675
1682
  debug.rb:
1676
1683
  - binding.b
1677
1684
  - binding.break
@@ -1693,6 +1700,11 @@ Lint/Debugger:
1693
1700
  - jard
1694
1701
  WebConsole:
1695
1702
  - binding.console
1703
+ DebuggerRequires:
1704
+ debug.rb:
1705
+ - debug/open
1706
+ - debug/open_nonstop
1707
+ - debug/start
1696
1708
 
1697
1709
  Lint/DeprecatedClassMethods:
1698
1710
  Description: 'Check for deprecated class method calls.'
@@ -2762,7 +2774,8 @@ Naming/FileName:
2762
2774
  VersionChanged: '1.23'
2763
2775
  # Camel case file names listed in `AllCops:Include` and all file names listed
2764
2776
  # in `AllCops:Exclude` are excluded by default. Add extra excludes here.
2765
- Exclude: []
2777
+ Exclude:
2778
+ - Rakefile.rb
2766
2779
  # When `true`, requires that each source file should define a class or module
2767
2780
  # with a name which matches the file name (converted to ... case).
2768
2781
  # It further expects it to be nested inside modules which match the names
@@ -4276,6 +4289,13 @@ Style/MapCompactWithConditionalBlock:
4276
4289
  Enabled: pending
4277
4290
  VersionAdded: '1.30'
4278
4291
 
4292
+ Style/MapIntoArray:
4293
+ Description: 'Checks for usages of `each` with `<<`, `push`, or `append` which can be replaced by `map`.'
4294
+ StyleGuide: '#functional-code'
4295
+ Enabled: pending
4296
+ VersionAdded: '1.63'
4297
+ Safe: false
4298
+
4279
4299
  Style/MapToHash:
4280
4300
  Description: 'Prefer `to_h` with a block over `map.to_h`.'
4281
4301
  Enabled: pending
@@ -17,7 +17,10 @@ module RuboCop
17
17
 
18
18
  PHASE_1_OVERRIDDEN = '(skipped because the default Layout/LineLength:Max is overridden)'
19
19
  PHASE_1_DISABLED = '(skipped because Layout/LineLength is disabled)'
20
- PHASE_1_SKIPPED = '(skipped because a list of cops is passed to the `--only` flag)'
20
+ PHASE_1_SKIPPED_ONLY_COPS =
21
+ '(skipped because a list of cops is passed to the `--only` flag)'
22
+ PHASE_1_SKIPPED_ONLY_EXCLUDE =
23
+ '(skipped because only excludes will be generated due to `--auto-gen-only-exclude` flag)'
21
24
 
22
25
  def run
23
26
  add_formatter
@@ -29,12 +32,14 @@ module RuboCop
29
32
  private
30
33
 
31
34
  def maybe_run_line_length_cop
32
- if !line_length_enabled?(@config_store.for_pwd)
35
+ if only_exclude?
36
+ skip_line_length_cop(PHASE_1_SKIPPED_ONLY_EXCLUDE)
37
+ elsif !line_length_enabled?(@config_store.for_pwd)
33
38
  skip_line_length_cop(PHASE_1_DISABLED)
34
39
  elsif !same_max_line_length?(@config_store.for_pwd, ConfigLoader.default_configuration)
35
40
  skip_line_length_cop(PHASE_1_OVERRIDDEN)
36
41
  elsif options_has_only_flag?
37
- skip_line_length_cop(PHASE_1_SKIPPED)
42
+ skip_line_length_cop(PHASE_1_SKIPPED_ONLY_COPS)
38
43
  else
39
44
  run_line_length_cop
40
45
  end
@@ -65,6 +70,10 @@ module RuboCop
65
70
  @options[:only]
66
71
  end
67
72
 
73
+ def only_exclude?
74
+ @options[:auto_gen_only_exclude]
75
+ end
76
+
68
77
  # Do an initial run with only Layout/LineLength so that cops that
69
78
  # depend on Layout/LineLength:Max get the correct value for that
70
79
  # parameter.
@@ -263,6 +263,7 @@ module RuboCop
263
263
  PathUtil.smart_path(@loaded_path)
264
264
  end
265
265
 
266
+ # @return [String, nil]
266
267
  def bundler_lock_file_path
267
268
  return nil unless loaded_path
268
269
 
@@ -286,27 +287,49 @@ module RuboCop
286
287
  end
287
288
  end
288
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
+
289
296
  def inspect # :nodoc:
290
297
  "#<#{self.class.name}:#{object_id} @loaded_path=#{loaded_path}>"
291
298
  end
292
299
 
293
300
  private
294
301
 
302
+ # @return [Float, nil] The Rails version as a `major.minor` Float.
295
303
  def target_rails_version_from_bundler_lock_file
296
304
  @target_rails_version_from_bundler_lock_file ||= read_rails_version_from_bundler_lock_file
297
305
  end
298
306
 
307
+ # @return [Float, nil] The Rails version as a `major.minor` Float.
299
308
  def read_rails_version_from_bundler_lock_file
300
- lock_file_path = bundler_lock_file_path
301
- return nil unless lock_file_path
302
-
303
- File.foreach(lock_file_path) do |line|
304
- # If Rails (or one of its frameworks) is in Gemfile.lock or gems.lock, there should be
305
- # a line like:
306
- # railties (X.X.X)
307
- result = line.match(/^\s+railties\s+\((\d+\.\d+)/)
308
- return result.captures.first.to_f if result
309
- 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
310
333
  end
311
334
 
312
335
  def enable_cop?(qualified_cop_name, cop_options)
@@ -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)
@@ -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 }
@@ -245,6 +269,7 @@ module RuboCop
245
269
  end
246
270
 
247
271
  def relevant_file?(file)
272
+ return false unless target_satisfies_all_gem_version_requirements?
248
273
  return true unless @config.clusivity_config_for_badge?(self.class.badge)
249
274
 
250
275
  file == RuboCop::AST::ProcessedSource::STRING_SOURCE_NAME ||
@@ -496,6 +521,18 @@ module RuboCop
496
521
  range.end_pos + @current_offset
497
522
  )
498
523
  end
524
+
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
535
+ end
499
536
  end
500
537
  end
501
538
  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)
@@ -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
 
@@ -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
@@ -27,9 +27,9 @@ module RuboCop
27
27
  MSG = 'Add an empty line after magic comments.'
28
28
 
29
29
  def on_new_investigation
30
- return unless processed_source.ast &&
31
- (last_magic_comment = last_magic_comment(processed_source))
32
- return if processed_source[last_magic_comment.loc.line].strip.empty?
30
+ return unless (last_magic_comment = last_magic_comment(processed_source))
31
+ return unless (next_line = processed_source[last_magic_comment.loc.line])
32
+ return if next_line.strip.empty?
33
33
 
34
34
  offending_range = offending_range(last_magic_comment)
35
35
 
@@ -46,18 +46,25 @@ module RuboCop
46
46
 
47
47
  # Find the last magic comment in the source file.
48
48
  #
49
- # Take all comments that precede the first line of code, select the
49
+ # Take all comments that precede the first line of code (or just take
50
+ # them all in the case when there is no code), select the
50
51
  # magic comments, and return the last magic comment in the file.
51
52
  #
52
53
  # @return [Parser::Source::Comment] if magic comments exist before code
53
54
  # @return [nil] otherwise
54
55
  def last_magic_comment(source)
55
- source
56
- .comments
57
- .take_while { |comment| comment.loc.line < source.ast.loc.line }
56
+ comments_before_code(source)
58
57
  .reverse
59
58
  .find { |comment| MagicComment.parse(comment.text).any? }
60
59
  end
60
+
61
+ def comments_before_code(source)
62
+ if source.ast
63
+ source.comments.take_while { |comment| comment.loc.line < source.ast.loc.line }
64
+ else
65
+ source.comments
66
+ end
67
+ end
61
68
  end
62
69
  end
63
70
  end
@@ -84,8 +84,14 @@ module RuboCop
84
84
  end
85
85
 
86
86
  def offense?(node)
87
- node.multiline? && !too_long?(node) && suitable_as_single_line?(node) &&
88
- !index_access_call_chained?(node) && !configured_to_not_be_inspected?(node)
87
+ return false if !node.multiline? || too_long?(node) || !suitable_as_single_line?(node)
88
+ return require_backslash?(node) if node.and_type? || node.or_type?
89
+
90
+ !index_access_call_chained?(node) && !configured_to_not_be_inspected?(node)
91
+ end
92
+
93
+ def require_backslash?(node)
94
+ processed_source.lines[node.loc.operator.line - 1].end_with?('\\')
89
95
  end
90
96
 
91
97
  def index_access_call_chained?(node)
@@ -50,7 +50,7 @@ module RuboCop
50
50
  MSG_WITHOUT_SAFE_ASSIGNMENT_ALLOWED =
51
51
  'Use `==` if you meant to do a comparison or move the assignment ' \
52
52
  'up out of the condition.'
53
- ASGN_TYPES = [:begin, *AST::Node::EQUALS_ASSIGNMENTS, :send].freeze
53
+ ASGN_TYPES = [:begin, *AST::Node::EQUALS_ASSIGNMENTS, :send, :csend].freeze
54
54
 
55
55
  def on_if(node)
56
56
  return if node.condition.block_type?
@@ -88,9 +88,7 @@ module RuboCop
88
88
  end
89
89
 
90
90
  def skip_children?(asgn_node)
91
- (asgn_node.send_type? && !asgn_node.assignment_method?) ||
92
- empty_condition?(asgn_node) ||
93
- (safe_assignment_allowed? && safe_assignment?(asgn_node))
91
+ empty_condition?(asgn_node) || (safe_assignment_allowed? && safe_assignment?(asgn_node))
94
92
  end
95
93
 
96
94
  def traverse_node(node, &block)
@@ -29,6 +29,11 @@ module RuboCop
29
29
  # MyDebugger.debug_this
30
30
  # ----
31
31
  #
32
+ # Some gems also ship files that will start a debugging session when required,
33
+ # for example `require 'debug/start'` from `ruby/debug`. These requires can
34
+ # be configured through `DebuggerRequires`. It has the same structure as
35
+ # `DebuggerMethods`, which you can read about above.
36
+ #
32
37
  # @example
33
38
  #
34
39
  # # bad (ok during development)
@@ -64,14 +69,20 @@ module RuboCop
64
69
  # def some_method
65
70
  # my_debugger
66
71
  # end
72
+ #
73
+ # @example DebuggerRequires: [my_debugger/start]
74
+ #
75
+ # # bad (ok during development)
76
+ #
77
+ # require 'my_debugger/start'
67
78
  class Debugger < Base
68
79
  MSG = 'Remove debugger entry point `%<source>s`.'
69
80
  BLOCK_TYPES = %i[block numblock kwbegin].freeze
70
81
 
71
82
  def on_send(node)
72
- return if !debugger_method?(node) || assumed_usage_context?(node)
83
+ return if assumed_usage_context?(node)
73
84
 
74
- add_offense(node)
85
+ add_offense(node) if debugger_method?(node) || debugger_require?(node)
75
86
  end
76
87
 
77
88
  private
@@ -87,12 +98,26 @@ module RuboCop
87
98
  end
88
99
  end
89
100
 
101
+ def debugger_requires
102
+ @debugger_requires ||= begin
103
+ config = cop_config.fetch('DebuggerRequires', [])
104
+ config.is_a?(Array) ? config : config.values.flatten
105
+ end
106
+ end
107
+
90
108
  def debugger_method?(send_node)
91
109
  return false if send_node.parent&.send_type? && send_node.parent.receiver == send_node
92
110
 
93
111
  debugger_methods.include?(chained_method_name(send_node))
94
112
  end
95
113
 
114
+ def debugger_require?(send_node)
115
+ return false unless send_node.method?(:require) && send_node.arguments.one?
116
+ return false unless (argument = send_node.first_argument).str_type?
117
+
118
+ debugger_requires.include?(argument.value)
119
+ end
120
+
96
121
  def assumed_usage_context?(node)
97
122
  # Basically, debugger methods are not used as a method argument without arguments.
98
123
  return false unless node.arguments.empty? && node.each_ancestor(:send, :csend).any?