rubocop 1.62.1 → 1.63.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +23 -3
  4. data/lib/rubocop/config.rb +33 -10
  5. data/lib/rubocop/config_obsoletion.rb +1 -1
  6. data/lib/rubocop/cop/base.rb +37 -0
  7. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -0
  8. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -4
  9. data/lib/rubocop/cop/lint/debugger.rb +27 -2
  10. data/lib/rubocop/cop/lint/redundant_with_index.rb +3 -0
  11. data/lib/rubocop/cop/mixin/code_length.rb +12 -1
  12. data/lib/rubocop/cop/mixin/method_complexity.rb +15 -6
  13. data/lib/rubocop/cop/mixin/safe_assignment.rb +1 -1
  14. data/lib/rubocop/cop/naming/block_forwarding.rb +31 -12
  15. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  16. data/lib/rubocop/cop/naming/inclusive_language.rb +1 -2
  17. data/lib/rubocop/cop/style/alias.rb +1 -0
  18. data/lib/rubocop/cop/style/collection_compact.rb +3 -3
  19. data/lib/rubocop/cop/style/copyright.rb +16 -11
  20. data/lib/rubocop/cop/style/eval_with_location.rb +2 -0
  21. data/lib/rubocop/cop/style/exact_regexp_match.rb +2 -1
  22. data/lib/rubocop/cop/style/format_string.rb +9 -9
  23. data/lib/rubocop/cop/style/map_into_array.rb +175 -0
  24. data/lib/rubocop/cop/style/map_to_hash.rb +1 -1
  25. data/lib/rubocop/cop/style/map_to_set.rb +1 -1
  26. data/lib/rubocop/cop/style/redundant_argument.rb +24 -1
  27. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +1 -1
  28. data/lib/rubocop/cop/style/redundant_each.rb +1 -1
  29. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  30. data/lib/rubocop/cop/style/redundant_line_continuation.rb +8 -14
  31. data/lib/rubocop/cop/style/redundant_percent_q.rb +1 -1
  32. data/lib/rubocop/cop/team.rb +3 -0
  33. data/lib/rubocop/formatter/clang_style_formatter.rb +3 -7
  34. data/lib/rubocop/formatter/tap_formatter.rb +3 -7
  35. data/lib/rubocop/lockfile.rb +34 -4
  36. data/lib/rubocop/rspec/expect_offense.rb +8 -0
  37. data/lib/rubocop/rspec/shared_contexts.rb +13 -1
  38. data/lib/rubocop/runner.rb +3 -0
  39. data/lib/rubocop/version.rb +2 -2
  40. data/lib/rubocop.rb +1 -0
  41. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8daa4ef7dd74b8608039d7dfda5f1e3fe739738949390a6f1347acaccf7bd1c4
4
- data.tar.gz: 271a978db956def596a5b06a7fd69d189d807b27db0acdda03fd6200b95ef541
3
+ metadata.gz: 8eadae0a09b33bf6a4ae3621ad1b86ba4eff0e7601d3ceb89dcf115759e9c022
4
+ data.tar.gz: ae113bb4d3eab63b4a551e36541f14d4e3bf3aae3354b853fb45960e80b90bbb
5
5
  SHA512:
6
- metadata.gz: 484294f43781f114ba01470b22331ef17648219e26dc4ba2f28073f609c909af2997c47e1f1aad62860124f6f4594a2e536cd793f05352a78d588f979732a290
7
- data.tar.gz: 6a8313e61422bcd568eea747c0f9a88f61f50620b4dd4f478da0bdd3e067c1aa8bcf70e15744e02118d4c525eb0632fae3cc19caf73db6db1f63ea76dbb84b45
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
@@ -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
@@ -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
 
@@ -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?
@@ -33,8 +33,10 @@ module RuboCop
33
33
  MSG_EACH_WITH_INDEX = 'Use `each` instead of `each_with_index`.'
34
34
  MSG_WITH_INDEX = 'Remove redundant `with_index`.'
35
35
 
36
+ # rubocop:disable Metrics/AbcSize
36
37
  def on_block(node)
37
38
  return unless node.receiver
39
+ return if node.method?(:with_index) && !node.receiver.receiver
38
40
  return unless (send = redundant_with_index?(node))
39
41
 
40
42
  range = with_index_range(send)
@@ -48,6 +50,7 @@ module RuboCop
48
50
  end
49
51
  end
50
52
  end
53
+ # rubocop:enable Metrics/AbcSize
51
54
 
52
55
  alias on_numblock on_block
53
56
 
@@ -36,7 +36,7 @@ module RuboCop
36
36
  length = calculator.calculate
37
37
  return if length <= max_length
38
38
 
39
- location = node.casgn_type? ? node.loc.name : node.source_range
39
+ location = location(node)
40
40
 
41
41
  add_offense(location, message: message(length, max_length)) { self.max = length }
42
42
  end
@@ -54,6 +54,17 @@ module RuboCop
54
54
  foldable_types: count_as_one
55
55
  )
56
56
  end
57
+
58
+ def location(node)
59
+ return node.loc.name if node.casgn_type?
60
+
61
+ if LSP.enabled?
62
+ end_range = node.loc.respond_to?(:name) ? node.loc.name : node.loc.begin
63
+ node.source_range.begin.join(end_range)
64
+ else
65
+ node.source_range
66
+ end
67
+ end
57
68
  end
58
69
  end
59
70
  end
@@ -49,13 +49,13 @@ module RuboCop
49
49
 
50
50
  return unless complexity > max
51
51
 
52
- msg = format(self.class::MSG,
53
- method: method_name,
54
- complexity: complexity,
55
- abc_vector: abc_vector,
56
- max: max)
52
+ msg = format(
53
+ self.class::MSG,
54
+ method: method_name, complexity: complexity, abc_vector: abc_vector, max: max
55
+ )
56
+ location = location(node)
57
57
 
58
- add_offense(node, message: msg) { self.max = complexity.ceil }
58
+ add_offense(location, message: msg) { self.max = complexity.ceil }
59
59
  end
60
60
 
61
61
  def complexity(body)
@@ -69,6 +69,15 @@ module RuboCop
69
69
  end
70
70
  score
71
71
  end
72
+
73
+ def location(node)
74
+ if LSP.enabled?
75
+ end_range = node.loc.respond_to?(:name) ? node.loc.name : node.loc.begin
76
+ node.source_range.begin.join(end_range)
77
+ else
78
+ node.source_range
79
+ end
80
+ end
72
81
  end
73
82
  end
74
83
  end
@@ -14,7 +14,7 @@ module RuboCop
14
14
  def_node_matcher :empty_condition?, '(begin)'
15
15
 
16
16
  # @!method setter_method?(node)
17
- def_node_matcher :setter_method?, '[(send ...) setter_method?]'
17
+ def_node_matcher :setter_method?, '[(call ...) setter_method?]'
18
18
 
19
19
  # @!method safe_assignment?(node)
20
20
  def_node_matcher :safe_assignment?, '(begin {equals_asgn? #setter_method?})'
@@ -51,29 +51,25 @@ module RuboCop
51
51
  [Lint::AmbiguousOperator, Style::ArgumentsForwarding]
52
52
  end
53
53
 
54
- # rubocop:disable Metrics/CyclomaticComplexity
55
54
  def on_def(node)
56
55
  return if node.arguments.empty?
57
56
 
58
57
  last_argument = node.last_argument
59
58
  return if expected_block_forwarding_style?(node, last_argument)
60
59
 
61
- invalid_syntax = false
62
- node.each_descendant(:block_pass) do |block_pass_node|
63
- next if block_pass_node.children.first&.sym_type? ||
64
- last_argument.source != block_pass_node.source
60
+ forwarded_args = node.each_descendant(:block_pass).with_object([]) do |block_pass, result|
61
+ return nil if invalidates_syntax?(block_pass)
62
+ next unless block_argument_name_matched?(block_pass, last_argument)
65
63
 
66
- if block_pass_node.each_ancestor(:block, :numblock).any?
67
- invalid_syntax = true
68
- next
69
- end
64
+ result << block_pass
65
+ end
70
66
 
71
- register_offense(block_pass_node, node)
67
+ forwarded_args.each do |forwarded_arg|
68
+ register_offense(forwarded_arg, node)
72
69
  end
73
70
 
74
- register_offense(last_argument, node) unless invalid_syntax
71
+ register_offense(last_argument, node)
75
72
  end
76
- # rubocop:enable Metrics/CyclomaticComplexity
77
73
  alias on_defs on_def
78
74
 
79
75
  private
@@ -88,6 +84,29 @@ module RuboCop
88
84
  end
89
85
  end
90
86
 
87
+ def block_argument_name_matched?(block_pass_node, last_argument)
88
+ return false if block_pass_node.children.first&.sym_type?
89
+
90
+ last_argument.source == block_pass_node.source
91
+ end
92
+
93
+ # Prevents the following syntax error:
94
+ #
95
+ # # foo.rb
96
+ # def foo(&)
97
+ # block_method do
98
+ # bar(&)
99
+ # end
100
+ # end
101
+ #
102
+ # $ ruby -vc foo.rb
103
+ # ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-darwin22]
104
+ # foo.rb: foo.rb:4: anonymous block parameter is also used within block (SyntaxError)
105
+ #
106
+ def invalidates_syntax?(block_pass_node)
107
+ block_pass_node.each_ancestor(:block, :numblock).any?
108
+ end
109
+
91
110
  def use_kwarg_in_method_definition?(node)
92
111
  node.arguments.each_descendant(:kwarg, :kwoptarg).any?
93
112
  end
@@ -57,7 +57,7 @@ module RuboCop
57
57
  file_path = processed_source.file_path
58
58
  return if config.file_to_exclude?(file_path) || config.allowed_camel_case_file?(file_path)
59
59
 
60
- for_bad_filename(file_path) { |range, msg| add_offense(range, message: msg) }
60
+ for_bad_filename(file_path)
61
61
  end
62
62
 
63
63
  private
@@ -71,7 +71,7 @@ module RuboCop
71
71
  msg = other_message(basename) unless bad_filename_allowed?
72
72
  end
73
73
 
74
- yield source_range(processed_source.buffer, 1, 0), msg if msg
74
+ add_global_offense(msg) if msg
75
75
  end
76
76
 
77
77
  def perform_class_and_module_naming_checks(file_path, basename)
@@ -207,8 +207,7 @@ module RuboCop
207
207
  message = create_multiple_word_message_for_file(words)
208
208
  end
209
209
 
210
- range = source_range(processed_source.buffer, 1, 0)
211
- add_offense(range, message: message)
210
+ add_global_offense(message)
212
211
  end
213
212
 
214
213
  def create_single_word_message_for_file(word)
@@ -41,6 +41,7 @@ module RuboCop
41
41
  def on_send(node)
42
42
  return unless node.command?(:alias_method)
43
43
  return unless style == :prefer_alias && alias_keyword_possible?(node)
44
+ return unless node.arguments.count == 2
44
45
 
45
46
  msg = format(MSG_ALIAS_METHOD, current: lexical_scope_type(node))
46
47
  add_offense(node.loc.selector, message: msg) do |corrector|
@@ -19,9 +19,7 @@ module RuboCop
19
19
  # @example
20
20
  # # bad
21
21
  # array.reject(&:nil?)
22
- # array.delete_if(&:nil?)
23
22
  # array.reject { |e| e.nil? }
24
- # array.delete_if { |e| e.nil? }
25
23
  # array.select { |e| !e.nil? }
26
24
  # array.grep_v(nil)
27
25
  # array.grep_v(NilClass)
@@ -31,7 +29,9 @@ module RuboCop
31
29
  #
32
30
  # # bad
33
31
  # hash.reject!(&:nil?)
32
+ # array.delete_if(&:nil?)
34
33
  # hash.reject! { |k, v| v.nil? }
34
+ # array.delete_if { |e| e.nil? }
35
35
  # hash.select! { |k, v| !v.nil? }
36
36
  #
37
37
  # # good
@@ -127,7 +127,7 @@ module RuboCop
127
127
  end
128
128
 
129
129
  def good_method_name(node)
130
- if node.bang_method?
130
+ if node.bang_method? || node.method?(:delete_if)
131
131
  'compact!'
132
132
  else
133
133
  'compact'
@@ -28,18 +28,27 @@ module RuboCop
28
28
  def on_new_investigation
29
29
  return if notice.empty? || notice_found?(processed_source)
30
30
 
31
- add_offense(offense_range, message: format(MSG, notice: notice)) do |corrector|
32
- verify_autocorrect_notice!
33
-
34
- token = insert_notice_before(processed_source)
35
- range = token.nil? ? range_between(0, 0) : token.pos
36
-
37
- corrector.insert_before(range, "#{autocorrect_notice}\n")
31
+ verify_autocorrect_notice!
32
+ message = format(MSG, notice: notice)
33
+ if processed_source.blank?
34
+ add_global_offense(message)
35
+ else
36
+ offense_range = source_range(processed_source.buffer, 1, 0)
37
+ add_offense(offense_range, message: message) do |corrector|
38
+ autocorrect(corrector)
39
+ end
38
40
  end
39
41
  end
40
42
 
41
43
  private
42
44
 
45
+ def autocorrect(corrector)
46
+ token = insert_notice_before(processed_source)
47
+ range = token.nil? ? range_between(0, 0) : token.pos
48
+
49
+ corrector.insert_before(range, "#{autocorrect_notice}\n")
50
+ end
51
+
43
52
  def notice
44
53
  cop_config['Notice']
45
54
  end
@@ -48,10 +57,6 @@ module RuboCop
48
57
  cop_config['AutocorrectNotice']
49
58
  end
50
59
 
51
- def offense_range
52
- source_range(processed_source.buffer, 1, 0)
53
- end
54
-
55
60
  def verify_autocorrect_notice!
56
61
  raise Warning, AUTOCORRECT_EMPTY_WARNING if autocorrect_notice.empty?
57
62
 
@@ -155,6 +155,8 @@ module RuboCop
155
155
 
156
156
  def check_line(node, code)
157
157
  line_node = node.last_argument
158
+ return if line_node.variable? || (line_node.send_type? && !line_node.method?(:+))
159
+
158
160
  line_diff = line_difference(line_node, code)
159
161
  if line_diff.zero?
160
162
  add_offense_for_same_line(node, line_node)
@@ -38,12 +38,13 @@ module RuboCop
38
38
  PATTERN
39
39
 
40
40
  def on_send(node)
41
+ return unless (receiver = node.receiver)
41
42
  return unless (regexp = exact_regexp_match(node))
42
43
 
43
44
  parsed_regexp = Regexp::Parser.parse(regexp)
44
45
  return unless exact_match_pattern?(parsed_regexp)
45
46
 
46
- prefer = "#{node.receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
47
+ prefer = "#{receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
47
48
 
48
49
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
49
50
  corrector.replace(node, prefer)
@@ -25,27 +25,27 @@ module RuboCop
25
25
  #
26
26
  # @example EnforcedStyle: format (default)
27
27
  # # bad
28
- # puts sprintf('%10s', 'hoge')
29
- # puts '%10s' % 'hoge'
28
+ # puts sprintf('%10s', 'foo')
29
+ # puts '%10s' % 'foo'
30
30
  #
31
31
  # # good
32
- # puts format('%10s', 'hoge')
32
+ # puts format('%10s', 'foo')
33
33
  #
34
34
  # @example EnforcedStyle: sprintf
35
35
  # # bad
36
- # puts format('%10s', 'hoge')
37
- # puts '%10s' % 'hoge'
36
+ # puts format('%10s', 'foo')
37
+ # puts '%10s' % 'foo'
38
38
  #
39
39
  # # good
40
- # puts sprintf('%10s', 'hoge')
40
+ # puts sprintf('%10s', 'foo')
41
41
  #
42
42
  # @example EnforcedStyle: percent
43
43
  # # bad
44
- # puts format('%10s', 'hoge')
45
- # puts sprintf('%10s', 'hoge')
44
+ # puts format('%10s', 'foo')
45
+ # puts sprintf('%10s', 'foo')
46
46
  #
47
47
  # # good
48
- # puts '%10s' % 'hoge'
48
+ # puts '%10s' % 'foo'
49
49
  #
50
50
  class FormatString < Base
51
51
  include ConfigurableEnforcedStyle
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for usages of `each` with `<<`, `push`, or `append` which
7
+ # can be replaced by `map`.
8
+ #
9
+ # If `PreferredMethods` is configured for `map` in `Style/CollectionMethods`,
10
+ # this cop uses the specified method for replacement.
11
+ #
12
+ # NOTE: The return value of `Enumerable#each` is `self`, whereas the
13
+ # return value of `Enumerable#map` is an `Array`. They are not autocorrected
14
+ # when a return value could be used because these types differ.
15
+ #
16
+ # NOTE: It only detects when the mapping destination is a local variable
17
+ # initialized as an empty array and referred to only by the pushing operation.
18
+ # This is because, if not, it's challenging to statically guarantee that the
19
+ # mapping destination variable remains an empty array:
20
+ #
21
+ # [source,ruby]
22
+ # ----
23
+ # @dest = []
24
+ # src.each { |e| @dest << e * 2 } # `src` method may mutate `@dest`
25
+ #
26
+ # dest = []
27
+ # src.each { |e| dest << transform(e, dest) } # `transform` method may mutate `dest`
28
+ # ----
29
+ #
30
+ # @safety
31
+ # This cop is unsafe because not all objects that have an `each`
32
+ # method also have a `map` method (e.g. `ENV`). Additionally, for calls
33
+ # with a block, not all objects that have a `map` method return an array
34
+ # (e.g. `Enumerator::Lazy`).
35
+ #
36
+ # @example
37
+ # # bad
38
+ # dest = []
39
+ # src.each { |e| dest << e * 2 }
40
+ # dest
41
+ #
42
+ # # good
43
+ # dest = src.map { |e| e * 2 }
44
+ #
45
+ # # good - contains another operation
46
+ # dest = []
47
+ # src.each { |e| dest << e * 2; puts e }
48
+ # dest
49
+ #
50
+ class MapIntoArray < Base
51
+ include RangeHelp
52
+ extend AutoCorrector
53
+
54
+ MSG = 'Use `%<new_method_name>s` instead of `each` to map elements into an array.'
55
+
56
+ # @!method each_block_with_push?(node)
57
+ def_node_matcher :each_block_with_push?, <<-PATTERN
58
+ [
59
+ ^({begin kwbegin} ...)
60
+ ({block numblock} (send _ :each) _
61
+ (send (lvar _) {:<< :push :append} _))
62
+ ]
63
+ PATTERN
64
+
65
+ # @!method empty_array_asgn?(node)
66
+ def_node_matcher :empty_array_asgn?, '(lvasgn _ (array))'
67
+
68
+ # @!method lvar_ref?(node, name)
69
+ def_node_matcher :lvar_ref?, '(lvar %1)'
70
+
71
+ def self.joining_forces
72
+ VariableForce
73
+ end
74
+
75
+ def after_leaving_scope(scope, _variable_table)
76
+ (@scopes ||= []) << scope
77
+ end
78
+
79
+ def on_block(node)
80
+ return unless each_block_with_push?(node)
81
+
82
+ dest_var = find_dest_var(node)
83
+ return unless (asgn = find_closest_assignment(node, dest_var))
84
+ return unless empty_array_asgn?(asgn)
85
+ return unless dest_used_only_for_mapping?(node, dest_var, asgn)
86
+
87
+ register_offense(node, dest_var, asgn)
88
+ end
89
+
90
+ alias on_numblock on_block
91
+
92
+ private
93
+
94
+ def find_dest_var(block)
95
+ node = block.body.receiver
96
+ name = node.children.first
97
+
98
+ candidates = @scopes.lazy.filter_map { |s| s.variables[name] }
99
+ candidates.find { |v| v.references.any? { |n| n.node.equal?(node) } }
100
+ end
101
+
102
+ def find_closest_assignment(block, dest_var)
103
+ dest_var.assignments.reverse_each.lazy.map(&:node).find do |node|
104
+ node.source_range.end_pos < block.source_range.begin_pos
105
+ end
106
+ end
107
+
108
+ def dest_used_only_for_mapping?(block, dest_var, asgn)
109
+ range = asgn.source_range.join(block.source_range)
110
+
111
+ asgn.parent.equal?(block.parent) &&
112
+ dest_var.references.one? { |r| range.contains?(r.node.source_range) } &&
113
+ dest_var.assignments.one? { |a| range.contains?(a.node.source_range) }
114
+ end
115
+
116
+ def register_offense(block, dest_var, asgn)
117
+ add_offense(block, message: format(MSG, new_method_name: new_method_name)) do |corrector|
118
+ next if return_value_used?(block)
119
+
120
+ corrector.replace(block.send_node.selector, new_method_name)
121
+ remove_assignment(corrector, asgn)
122
+ correct_push_node(corrector, block.body)
123
+ correct_return_value_handling(corrector, block, dest_var)
124
+ end
125
+ end
126
+
127
+ def new_method_name
128
+ default = 'map'
129
+ alternative = config.for_cop('Style/CollectionMethods').dig('PreferredMethods', default)
130
+ alternative || default
131
+ end
132
+
133
+ def return_value_used?(node)
134
+ parent = node.parent
135
+
136
+ case parent&.type
137
+ when nil
138
+ false
139
+ when :begin, :kwbegin
140
+ !node.right_sibling && return_value_used?(parent)
141
+ when :block, :numblock
142
+ !parent.void_context?
143
+ else
144
+ true
145
+ end
146
+ end
147
+
148
+ def remove_assignment(corrector, asgn)
149
+ range = range_with_surrounding_space(asgn.source_range, side: :right)
150
+ range = range_with_surrounding_space(range, side: :right, newlines: false)
151
+
152
+ corrector.remove(range)
153
+ end
154
+
155
+ def correct_push_node(corrector, push_node)
156
+ range = push_node.source_range
157
+ arg_range = push_node.first_argument.source_range
158
+
159
+ corrector.remove(range_between(range.begin_pos, arg_range.begin_pos))
160
+ corrector.remove(range_between(arg_range.end_pos, range.end_pos))
161
+ end
162
+
163
+ def correct_return_value_handling(corrector, block, dest_var)
164
+ next_node = block.right_sibling
165
+
166
+ if lvar_ref?(next_node, dest_var.name)
167
+ corrector.remove(range_with_surrounding_space(next_node.source_range, side: :left))
168
+ end
169
+
170
+ corrector.insert_before(block, "#{dest_var.name} = ")
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -55,7 +55,7 @@ module RuboCop
55
55
  message = format(MSG, method: map_node.loc.selector.source, dot: to_h_node.loc.dot.source)
56
56
  add_offense(map_node.loc.selector, message: message) do |corrector|
57
57
  # If the `to_h` call already has a block, do not autocorrect.
58
- next if to_h_node.block_node
58
+ next if to_h_node.block_literal?
59
59
 
60
60
  autocorrect(corrector, to_h_node, map_node)
61
61
  end
@@ -44,7 +44,7 @@ module RuboCop
44
44
  message = format(MSG, method: map_node.loc.selector.source)
45
45
  add_offense(map_node.loc.selector, message: message) do |corrector|
46
46
  # If the `to_set` call already has a block, do not autocorrect.
47
- next if to_set_node.block_node
47
+ next if to_set_node.block_literal?
48
48
 
49
49
  autocorrect(corrector, to_set_node, map_node)
50
50
  end
@@ -81,7 +81,13 @@ module RuboCop
81
81
  redundant_argument = redundant_arg_for_method(node.method_name.to_s)
82
82
  return false if redundant_argument.nil?
83
83
 
84
- node.first_argument.source.sub(/\A'/, '"').sub(/'\z/, '"') == redundant_argument
84
+ target_argument = if node.first_argument.respond_to?(:value)
85
+ node.first_argument.value
86
+ else
87
+ node.first_argument
88
+ end
89
+
90
+ argument_matched?(target_argument, redundant_argument)
85
91
  end
86
92
 
87
93
  def redundant_arg_for_method(method_name)
@@ -98,6 +104,23 @@ module RuboCop
98
104
  range_with_surrounding_space(node.first_argument.source_range, newlines: false)
99
105
  end
100
106
  end
107
+
108
+ def argument_matched?(target_argument, redundant_argument)
109
+ argument = if target_argument.is_a?(AST::Node)
110
+ target_argument.source
111
+ elsif exclude_cntrl_character?(target_argument, redundant_argument)
112
+ target_argument.inspect
113
+ else
114
+ target_argument.to_s
115
+ end
116
+
117
+ argument == redundant_argument
118
+ end
119
+
120
+ def exclude_cntrl_character?(target_argument, redundant_argument)
121
+ !target_argument.to_s.sub(/\A'/, '"').sub(/'\z/, '"').match?(/[[:cntrl:]]/) ||
122
+ !redundant_argument.match?(/[[:cntrl:]]/)
123
+ end
101
124
  end
102
125
  end
103
126
  end
@@ -18,10 +18,10 @@ module RuboCop
18
18
  extend AutoCorrector
19
19
 
20
20
  MSG = 'Remove the redundant current directory path.'
21
+ RESTRICT_ON_SEND = %i[require_relative].freeze
21
22
  CURRENT_DIRECTORY_PATH = './'
22
23
 
23
24
  def on_send(node)
24
- return unless node.method?(:require_relative)
25
25
  return unless (first_argument = node.first_argument)
26
26
  return unless first_argument.str_content&.start_with?(CURRENT_DIRECTORY_PATH)
27
27
  return unless (index = first_argument.source.index(CURRENT_DIRECTORY_PATH))
@@ -86,7 +86,7 @@ module RuboCop
86
86
  def range(node)
87
87
  return node.selector unless node.method?(:each)
88
88
 
89
- if node.parent.call_type?
89
+ if node.parent&.call_type?
90
90
  node.selector.join(node.parent.loc.dot)
91
91
  else
92
92
  node.loc.dot.join(node.selector)
@@ -79,7 +79,7 @@ module RuboCop
79
79
  private_constant :REPLACEMENT_METHODS
80
80
 
81
81
  def on_send(node)
82
- return if node.arguments? || node.block_node
82
+ return if node.arguments? || node.block_literal?
83
83
 
84
84
  select_predicate?(node) do |select_node, filter_method|
85
85
  return if RAILS_METHODS.include?(filter_method) && !active_support_extensions_enabled?
@@ -72,7 +72,7 @@ module RuboCop
72
72
  ALLOWED_STRING_TOKENS = %i[tSTRING tSTRING_CONTENT].freeze
73
73
  ARGUMENT_TYPES = %i[
74
74
  kFALSE kNIL kSELF kTRUE tCONSTANT tCVAR tFLOAT tGVAR tIDENTIFIER tINTEGER tIVAR
75
- tLABEL tLBRACK tLCURLY tLPAREN_ARG tSTRING tSTRING_BEG tSYMBOL tXSTRING_BEG
75
+ tLBRACK tLCURLY tLPAREN_ARG tSTRING tSTRING_BEG tSYMBOL tXSTRING_BEG
76
76
  ].freeze
77
77
 
78
78
  def on_new_investigation
@@ -124,10 +124,8 @@ module RuboCop
124
124
  return true unless (node = find_node_for_line(range.line))
125
125
  return false if argument_newline?(node)
126
126
 
127
- continuation_node = node.parent || node
128
- return false if allowed_type?(node) || allowed_type?(continuation_node)
129
-
130
- continuation_node.source.include?("\n") || continuation_node.source.include?("\\\n")
127
+ source = node.parent ? node.parent.source : node.source
128
+ parse(source.gsub("\\\n", "\n")).valid_syntax?
131
129
  end
132
130
 
133
131
  def inside_string_literal?(range, token)
@@ -142,22 +140,22 @@ module RuboCop
142
140
  current_token.type == :tIDENTIFIER && ARGUMENT_TYPES.include?(next_token.type)
143
141
  end
144
142
 
145
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
143
+ # rubocop:disable Metrics/AbcSize
146
144
  def argument_newline?(node)
147
145
  node = node.to_a.last if node.assignment?
148
146
  return false if node.parenthesized_call?
149
147
 
150
148
  node = node.children.first if node.root? && node.begin_type?
151
149
 
152
- if argument_is_method?(node) || node.begin_type?
153
- argument_newline?(node.children.first)
150
+ if argument_is_method?(node)
151
+ argument_newline?(node.first_argument)
154
152
  else
155
153
  return false unless method_call_with_arguments?(node)
156
154
 
157
- !same_line?(node, node.first_argument)
155
+ node.loc.selector.line != node.first_argument.loc.line
158
156
  end
159
157
  end
160
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
158
+ # rubocop:enable Metrics/AbcSize
161
159
 
162
160
  def find_node_for_line(line)
163
161
  processed_source.ast.each_node do |node|
@@ -165,10 +163,6 @@ module RuboCop
165
163
  end
166
164
  end
167
165
 
168
- def allowed_type?(node)
169
- node.and_type? || node.or_type? || (node.if_type? && node.ternary?)
170
- end
171
-
172
166
  def same_line?(node, line)
173
167
  return false unless (source_range = node.source_range)
174
168
 
@@ -53,7 +53,7 @@ module RuboCop
53
53
  return if interpolated_quotes?(node) || allowed_percent_q?(node)
54
54
 
55
55
  add_offense(node) do |corrector|
56
- delimiter = /^%Q[^"]+$|'/.match?(node.source) ? QUOTE : SINGLE_QUOTE
56
+ delimiter = /\A%Q[^"]+\z|'/.match?(node.source) ? QUOTE : SINGLE_QUOTE
57
57
 
58
58
  corrector.replace(node.loc.begin, delimiter)
59
59
  corrector.replace(node.loc.end, delimiter)
@@ -174,6 +174,9 @@ module RuboCop
174
174
  end
175
175
 
176
176
  def support_target_rails_version?(cop)
177
+ # In this case, the rails version was already checked by `#excluded_file?`
178
+ return true if defined?(RuboCop::Rails::TargetRailsVersion::USES_REQUIRES_GEM_API)
179
+
177
180
  return true unless cop.class.respond_to?(:support_target_rails_version?)
178
181
 
179
182
  cop.class.support_target_rails_version?(cop.target_rails_version)
@@ -24,14 +24,10 @@ module RuboCop
24
24
  message: message(offense)
25
25
  )
26
26
 
27
- begin
28
- return unless valid_line?(offense)
27
+ return unless valid_line?(offense)
29
28
 
30
- report_line(offense.location)
31
- report_highlighted_area(offense.highlighted_area)
32
- rescue IndexError
33
- # range is not on a valid line; perhaps the source file is empty
34
- end
29
+ report_line(offense.location)
30
+ report_highlighted_area(offense.highlighted_area)
35
31
  end
36
32
 
37
33
  def valid_line?(offense)
@@ -53,14 +53,10 @@ module RuboCop
53
53
  message: message(offense)
54
54
  )
55
55
 
56
- begin
57
- return unless valid_line?(offense)
56
+ return unless valid_line?(offense)
58
57
 
59
- report_line(offense.location)
60
- report_highlighted_area(offense.highlighted_area)
61
- rescue IndexError
62
- # range is not on a valid line; perhaps the source file is empty
63
- end
58
+ report_line(offense.location)
59
+ report_highlighted_area(offense.highlighted_area)
64
60
  end
65
61
 
66
62
  def annotate_message(msg)
@@ -5,14 +5,23 @@ module RuboCop
5
5
  # Does not actually resolve gems, just parses the lockfile.
6
6
  # @api private
7
7
  class Lockfile
8
- # Gems that the bundle depends on
8
+ # @param [String, Pathname, nil] lockfile_path
9
+ def initialize(lockfile_path = nil)
10
+ lockfile_path ||= defined?(Bundler) ? Bundler.default_lockfile : nil
11
+
12
+ @lockfile_path = lockfile_path
13
+ end
14
+
15
+ # Gems that the bundle directly depends on.
16
+ # @return [Array<Bundler::Dependency>, nil]
9
17
  def dependencies
10
18
  return [] unless parser
11
19
 
12
20
  parser.dependencies.values
13
21
  end
14
22
 
15
- # All activated gems, including transitive dependencies
23
+ # All activated gems, including transitive dependencies.
24
+ # @return [Array<Bundler::Dependency>, nil]
16
25
  def gems
17
26
  return [] unless parser
18
27
 
@@ -21,17 +30,38 @@ module RuboCop
21
30
  parser.dependencies.values.concat(parser.specs.flat_map(&:dependencies))
22
31
  end
23
32
 
33
+ # Returns the locked versions of gems from this lockfile.
34
+ # @param [Boolean] include_transitive_dependencies: When false, only direct dependencies
35
+ # are returned, i.e. those listed explicitly in the `Gemfile`.
36
+ # @returns [Hash{String => Gem::Version}] The locked gem versions, keyed by the gems' names.
37
+ def gem_versions(include_transitive_dependencies: true)
38
+ return {} unless parser
39
+
40
+ all_gem_versions = parser.specs.to_h { |spec| [spec.name, spec.version] }
41
+
42
+ if include_transitive_dependencies
43
+ all_gem_versions
44
+ else
45
+ direct_dep_names = parser.dependencies.keys
46
+ all_gem_versions.slice(*direct_dep_names)
47
+ end
48
+ end
49
+
50
+ # Whether this lockfile includes the named gem, directly or indirectly.
51
+ # @param [String] name
52
+ # @return [Boolean]
24
53
  def includes_gem?(name)
25
54
  gems.any? { |gem| gem.name == name }
26
55
  end
27
56
 
28
57
  private
29
58
 
59
+ # @return [Bundler::LockfileParser, nil]
30
60
  def parser
31
- return unless defined?(Bundler) && Bundler.default_lockfile
32
61
  return @parser if defined?(@parser)
62
+ return unless @lockfile_path
33
63
 
34
- lockfile = Bundler.read_file(Bundler.default_lockfile)
64
+ lockfile = Bundler.read_file(@lockfile_path)
35
65
  @parser = lockfile ? Bundler::LockfileParser.new(lockfile) : nil
36
66
  rescue Bundler::BundlerError
37
67
  nil
@@ -111,6 +111,7 @@ module RuboCop
111
111
  source
112
112
  end
113
113
 
114
+ # rubocop:disable Metrics/AbcSize
114
115
  def expect_offense(source, file = nil, severity: nil, chomp: false, **replacements)
115
116
  expected_annotations = parse_annotations(source, **replacements)
116
117
  source = expected_annotations.plain_source
@@ -123,8 +124,15 @@ module RuboCop
123
124
  expect(actual_annotations).to eq(expected_annotations), ''
124
125
  expect(@offenses.map(&:severity).uniq).to eq([severity]) if severity
125
126
 
127
+ # Validate that all offenses have a range that formatters can display
128
+ expect do
129
+ @offenses.each { |offense| offense.location.source_line }
130
+ end.not_to raise_error, 'One of the offenses has a misconstructed range, for ' \
131
+ 'example if the offense is on line 1 and the source is empty'
132
+
126
133
  @offenses
127
134
  end
135
+ # rubocop:enable Metrics/AbcSize
128
136
 
129
137
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
130
138
  def expect_correction(correction, loop: true, source: nil)
@@ -110,7 +110,19 @@ RSpec.shared_context 'config' do # rubocop:disable Metrics/BlockLength
110
110
  let(:config) do
111
111
  hash = { 'AllCops' => all_cops_config, cop_class.cop_name => cur_cop_config }.merge!(other_cops)
112
112
 
113
- RuboCop::Config.new(hash, "#{Dir.pwd}/.rubocop.yml")
113
+ config = RuboCop::Config.new(hash, "#{Dir.pwd}/.rubocop.yml")
114
+
115
+ rails_version_in_gemfile = Gem::Version.new(
116
+ rails_version || RuboCop::Config::DEFAULT_RAILS_VERSION
117
+ )
118
+
119
+ allow(config).to receive(:gem_versions_in_target).and_return(
120
+ {
121
+ 'railties' => rails_version_in_gemfile
122
+ }
123
+ )
124
+
125
+ config
114
126
  end
115
127
 
116
128
  let(:cop) { cop_class.new(config, cop_options) }
@@ -20,6 +20,9 @@ module RuboCop
20
20
  message = 'Infinite loop detected'
21
21
  message += " in #{path}" if path
22
22
  message += " and caused by #{root_cause}" if root_cause
23
+ message += ' Hint: Please update to the latest RuboCop version if not already in use,'
24
+ message += ' and report a bug if the issue still occurs on this version.'
25
+ message += ' Please check the latest version at https://rubygems.org/gems/rubocop'
23
26
  super(message)
24
27
  end
25
28
  end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  # This module holds the RuboCop version information.
5
5
  module Version
6
- STRING = '1.62.1'
6
+ STRING = '1.63.0'
7
7
 
8
8
  MSG = '%<version>s (using %<parser_version>s, ' \
9
9
  'rubocop-ast %<rubocop_ast_version>s, ' \
@@ -11,7 +11,7 @@ module RuboCop
11
11
 
12
12
  CANONICAL_FEATURE_NAMES = {
13
13
  'Rspec' => 'RSpec', 'Graphql' => 'GraphQL', 'Md' => 'Markdown', 'Factory_bot' => 'FactoryBot',
14
- 'Thread_safety' => 'ThreadSafety'
14
+ 'Thread_safety' => 'ThreadSafety', 'Rspec_rails' => 'RSpecRails'
15
15
  }.freeze
16
16
  EXTENSION_PATH_NAMES = {
17
17
  'rubocop-md' => 'markdown', 'rubocop-factory_bot' => 'factory_bot'
data/lib/rubocop.rb CHANGED
@@ -557,6 +557,7 @@ require_relative 'rubocop/cop/style/lambda'
557
557
  require_relative 'rubocop/cop/style/lambda_call'
558
558
  require_relative 'rubocop/cop/style/line_end_concatenation'
559
559
  require_relative 'rubocop/cop/style/magic_comment_format'
560
+ require_relative 'rubocop/cop/style/map_into_array'
560
561
  require_relative 'rubocop/cop/style/map_to_hash'
561
562
  require_relative 'rubocop/cop/style/map_to_set'
562
563
  require_relative 'rubocop/cop/style/method_call_without_args_parentheses'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.62.1
4
+ version: 1.63.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2024-03-11 00:00:00.000000000 Z
13
+ date: 2024-04-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json
@@ -778,6 +778,7 @@ files:
778
778
  - lib/rubocop/cop/style/line_end_concatenation.rb
779
779
  - lib/rubocop/cop/style/magic_comment_format.rb
780
780
  - lib/rubocop/cop/style/map_compact_with_conditional_block.rb
781
+ - lib/rubocop/cop/style/map_into_array.rb
781
782
  - lib/rubocop/cop/style/map_to_hash.rb
782
783
  - lib/rubocop/cop/style/map_to_set.rb
783
784
  - lib/rubocop/cop/style/method_call_with_args_parentheses.rb
@@ -1031,9 +1032,9 @@ licenses:
1031
1032
  - MIT
1032
1033
  metadata:
1033
1034
  homepage_uri: https://rubocop.org/
1034
- changelog_uri: https://github.com/rubocop/rubocop/releases/tag/v1.62.1
1035
+ changelog_uri: https://github.com/rubocop/rubocop/releases/tag/v1.63.0
1035
1036
  source_code_uri: https://github.com/rubocop/rubocop/
1036
- documentation_uri: https://docs.rubocop.org/rubocop/1.62/
1037
+ documentation_uri: https://docs.rubocop.org/rubocop/1.63/
1037
1038
  bug_tracker_uri: https://github.com/rubocop/rubocop/issues
1038
1039
  rubygems_mfa_required: 'true'
1039
1040
  post_install_message: