rubocop 1.62.1 → 1.63.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +22 -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 +40 -1
  7. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -0
  8. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -2
  9. data/lib/rubocop/cop/lint/debugger.rb +27 -2
  10. data/lib/rubocop/cop/lint/empty_conditional_body.rb +1 -1
  11. data/lib/rubocop/cop/lint/mixed_case_range.rb +9 -4
  12. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  13. data/lib/rubocop/cop/lint/redundant_with_index.rb +3 -0
  14. data/lib/rubocop/cop/lint/unreachable_loop.rb +8 -2
  15. data/lib/rubocop/cop/mixin/code_length.rb +12 -1
  16. data/lib/rubocop/cop/mixin/method_complexity.rb +15 -6
  17. data/lib/rubocop/cop/mixin/safe_assignment.rb +1 -1
  18. data/lib/rubocop/cop/naming/block_forwarding.rb +31 -12
  19. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  20. data/lib/rubocop/cop/naming/inclusive_language.rb +1 -2
  21. data/lib/rubocop/cop/style/alias.rb +1 -0
  22. data/lib/rubocop/cop/style/arguments_forwarding.rb +2 -1
  23. data/lib/rubocop/cop/style/collection_compact.rb +3 -3
  24. data/lib/rubocop/cop/style/copyright.rb +16 -11
  25. data/lib/rubocop/cop/style/eval_with_location.rb +3 -1
  26. data/lib/rubocop/cop/style/exact_regexp_match.rb +2 -1
  27. data/lib/rubocop/cop/style/format_string.rb +9 -9
  28. data/lib/rubocop/cop/style/map_into_array.rb +175 -0
  29. data/lib/rubocop/cop/style/map_to_hash.rb +1 -1
  30. data/lib/rubocop/cop/style/map_to_set.rb +1 -1
  31. data/lib/rubocop/cop/style/redundant_argument.rb +24 -1
  32. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +1 -1
  33. data/lib/rubocop/cop/style/redundant_each.rb +1 -1
  34. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  35. data/lib/rubocop/cop/style/redundant_line_continuation.rb +8 -14
  36. data/lib/rubocop/cop/style/redundant_percent_q.rb +1 -1
  37. data/lib/rubocop/cop/style/require_order.rb +1 -1
  38. data/lib/rubocop/cop/team.rb +3 -0
  39. data/lib/rubocop/formatter/clang_style_formatter.rb +3 -7
  40. data/lib/rubocop/formatter/tap_formatter.rb +3 -7
  41. data/lib/rubocop/lockfile.rb +44 -7
  42. data/lib/rubocop/rspec/expect_offense.rb +8 -0
  43. data/lib/rubocop/rspec/shared_contexts.rb +13 -1
  44. data/lib/rubocop/runner.rb +3 -0
  45. data/lib/rubocop/version.rb +2 -2
  46. data/lib/rubocop.rb +1 -0
  47. metadata +5 -4
@@ -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
 
@@ -141,7 +141,7 @@ module RuboCop
141
141
  end
142
142
 
143
143
  def check_file(node, file_node)
144
- return true if special_file_keyword?(file_node)
144
+ return if special_file_keyword?(file_node)
145
145
 
146
146
  message = format(MSG_INCORRECT_FILE,
147
147
  method_name: node.method_name,
@@ -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
+ # ret = []
24
+ # src.each { |e| ret << e * 2 } # `<<` method may mutate `ret`
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)
@@ -131,7 +131,7 @@ module RuboCop
131
131
  end
132
132
 
133
133
  def in_same_section?(node1, node2)
134
- !node1.source_range.with(end_pos: node2.source_range.end_pos).source.include?("\n\n")
134
+ !node1.source_range.join(node2.source_range.end).source.include?("\n\n")
135
135
  end
136
136
  end
137
137
  end
@@ -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 ||= Bundler.default_lockfile if bundler_lock_parser_defined?
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,20 +30,48 @@ 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)
33
62
 
34
- lockfile = Bundler.read_file(Bundler.default_lockfile)
35
- @parser = lockfile ? Bundler::LockfileParser.new(lockfile) : nil
36
- rescue Bundler::BundlerError
37
- nil
63
+ @parser = if @lockfile_path
64
+ begin
65
+ lockfile = ::Bundler.read_file(@lockfile_path)
66
+ ::Bundler::LockfileParser.new(lockfile) if lockfile
67
+ rescue ::Bundler::BundlerError
68
+ nil
69
+ end
70
+ end
71
+ end
72
+
73
+ def bundler_lock_parser_defined?
74
+ Object.const_defined?(:Bundler) && Bundler.const_defined?(:LockfileParser)
38
75
  end
39
76
  end
40
77
  end
@@ -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.2'
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'