rubocop 1.62.0 → 1.63.0

Sign up to get free protection for your applications and to get access to all the features.
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?