rubocop 1.67.0 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +40 -0
  4. data/lib/rubocop/cached_data.rb +12 -4
  5. data/lib/rubocop/cli/command/execute_runner.rb +1 -1
  6. data/lib/rubocop/cli/command/version.rb +2 -2
  7. data/lib/rubocop/cop/autocorrect_logic.rb +22 -2
  8. data/lib/rubocop/cop/correctors/alignment_corrector.rb +1 -12
  9. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +10 -0
  10. data/lib/rubocop/cop/layout/leading_comment_space.rb +29 -1
  11. data/lib/rubocop/cop/layout/space_before_brackets.rb +5 -5
  12. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +4 -0
  13. data/lib/rubocop/cop/lint/duplicate_branch.rb +39 -4
  14. data/lib/rubocop/cop/lint/non_atomic_file_operation.rb +7 -0
  15. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +9 -0
  16. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +3 -1
  17. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +88 -0
  18. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +4 -1
  19. data/lib/rubocop/cop/mixin/check_line_breakable.rb +10 -0
  20. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +24 -0
  21. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +3 -1
  22. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  23. data/lib/rubocop/cop/offense.rb +2 -3
  24. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +79 -0
  25. data/lib/rubocop/cop/style/bitwise_predicate.rb +100 -0
  26. data/lib/rubocop/cop/style/block_delimiters.rb +17 -2
  27. data/lib/rubocop/cop/style/combinable_defined.rb +115 -0
  28. data/lib/rubocop/cop/style/endless_method.rb +1 -14
  29. data/lib/rubocop/cop/style/guard_clause.rb +14 -1
  30. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +67 -0
  31. data/lib/rubocop/cop/style/map_into_array.rb +6 -1
  32. data/lib/rubocop/cop/style/multiple_comparison.rb +28 -39
  33. data/lib/rubocop/cop/style/redundant_line_continuation.rb +20 -2
  34. data/lib/rubocop/cop/style/redundant_parentheses.rb +8 -10
  35. data/lib/rubocop/cop/style/safe_navigation.rb +12 -0
  36. data/lib/rubocop/cop/style/safe_navigation_chain_length.rb +52 -0
  37. data/lib/rubocop/cop/style/ternary_parentheses.rb +25 -4
  38. data/lib/rubocop/cop/variable_force/assignment.rb +18 -3
  39. data/lib/rubocop/cop/variable_force/branch.rb +1 -1
  40. data/lib/rubocop/cop/variable_force/variable.rb +5 -1
  41. data/lib/rubocop/cop/variable_force/variable_table.rb +2 -2
  42. data/lib/rubocop/cops_documentation_generator.rb +11 -9
  43. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  44. data/lib/rubocop/runner.rb +16 -8
  45. data/lib/rubocop/target_ruby.rb +1 -1
  46. data/lib/rubocop/version.rb +27 -8
  47. data/lib/rubocop.rb +8 -0
  48. metadata +15 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8127fcc41c5b7ffb95a175f82e8a411dfdec0be2b126c7e0868114519d92d01
4
- data.tar.gz: 2760fa954ef847d049bdd065dec77518c912f5b9050320fe419b9476393b58ea
3
+ metadata.gz: 7f0623068e9eac24fe4b1f5c7e4987d3a8adc7a2ec7f5f65c6cfb9d0ad9d1278
4
+ data.tar.gz: 9abb03dde6881cb7c11a21ee947c33a7d8874b9893f81e81cd7617f52f1bb714
5
5
  SHA512:
6
- metadata.gz: 87ac1904b5fac462a0b76627aac384ddb57493a3620c3ebc929efa9429ee8ad355038caaef2d41192abfcb726d70d85309a442eb93feab8d034544368be32dfd
7
- data.tar.gz: a011cbaced00ecab1ba1f6ea57983e92c7c872a08c88e450b9aea054bad61cc0f6acae3e1a50c4e4bf545e86ff339000d19995527ff906f0282e2613e7b2638d
6
+ metadata.gz: 3392244d7137845de4f7c9867695350666750b349879ac4796590d090b0b1a182f21fb9c96f6d284bfd9eaf21a7594f8b7de2d59da62458400a709430bb6f7a2
7
+ data.tar.gz: e2a5a3dd61755c091a91ea99828846fa875b3b7ca8ffaea67425245755e147159691c587a7a80089a428480b7ad07fef8da1be7dc209ac0a60bb46cff3a37d0f
data/README.md CHANGED
@@ -52,7 +52,7 @@ To prevent an unwanted RuboCop update you might want to use a conservative versi
52
52
  in your `Gemfile`:
53
53
 
54
54
  ```rb
55
- gem 'rubocop', '~> 1.67', require: false
55
+ gem 'rubocop', '~> 1.68', require: false
56
56
  ```
57
57
 
58
58
  See [our versioning policy](https://docs.rubocop.org/rubocop/versioning.html) for further details.
data/config/default.yml CHANGED
@@ -1038,6 +1038,7 @@ Layout/LeadingCommentSpace:
1038
1038
  AllowDoxygenCommentStyle: false
1039
1039
  AllowGemfileRubyComment: false
1040
1040
  AllowRBSInlineAnnotation: false
1041
+ AllowSteepAnnotation: false
1041
1042
 
1042
1043
  Layout/LeadingEmptyLines:
1043
1044
  Description: Check for unnecessary blank lines at the beginning of a file.
@@ -1785,6 +1786,7 @@ Lint/DuplicateBranch:
1785
1786
  VersionChanged: '1.7'
1786
1787
  IgnoreLiteralBranches: false
1787
1788
  IgnoreConstantBranches: false
1789
+ IgnoreDuplicateElseBranch: false
1788
1790
 
1789
1791
  Lint/DuplicateCaseCondition:
1790
1792
  Description: 'Do not repeat values in case conditionals.'
@@ -2455,6 +2457,11 @@ Lint/UnderscorePrefixedVariableName:
2455
2457
  VersionAdded: '0.21'
2456
2458
  AllowKeywordBlockArguments: false
2457
2459
 
2460
+ Lint/UnescapedBracketInRegexp:
2461
+ Description: 'Checks for unescaped literal `]` in Regexp.'
2462
+ Enabled: pending
2463
+ VersionAdded: '1.68'
2464
+
2458
2465
  Lint/UnexpectedBlockArity:
2459
2466
  Description: 'Looks for blocks that have fewer arguments that the calling method expects.'
2460
2467
  Enabled: pending
@@ -3143,6 +3150,12 @@ Style/Alias:
3143
3150
  - prefer_alias
3144
3151
  - prefer_alias_method
3145
3152
 
3153
+ Style/AmbiguousEndlessMethodDefinition:
3154
+ Description: 'Checks for endless methods inside operators of lower precedence.'
3155
+ StyleGuide: '#ambiguous-endless-method-defintions'
3156
+ Enabled: pending
3157
+ VersionAdded: '1.68'
3158
+
3146
3159
  Style/AndOr:
3147
3160
  Description: 'Use &&/|| instead of and/or.'
3148
3161
  StyleGuide: '#no-and-or-or'
@@ -3250,6 +3263,13 @@ Style/BisectedAttrAccessor:
3250
3263
  Enabled: true
3251
3264
  VersionAdded: '0.87'
3252
3265
 
3266
+ Style/BitwisePredicate:
3267
+ Description: 'Prefer bitwise predicate methods over direct comparison operations.'
3268
+ StyleGuide: '#bitwise-predicate-methods'
3269
+ Enabled: pending
3270
+ Safe: false
3271
+ VersionAdded: '1.68'
3272
+
3253
3273
  Style/BlockComments:
3254
3274
  Description: 'Do not use block comments.'
3255
3275
  StyleGuide: '#no-block-comments'
@@ -3533,6 +3553,11 @@ Style/ColonMethodDefinition:
3533
3553
  Enabled: true
3534
3554
  VersionAdded: '0.52'
3535
3555
 
3556
+ Style/CombinableDefined:
3557
+ Description: 'Checks successive `defined?` calls that can be combined into a single call.'
3558
+ Enabled: pending
3559
+ VersionAdded: '1.68'
3560
+
3536
3561
  Style/CombinableLoops:
3537
3562
  Description: >-
3538
3563
  Checks for places where multiple consecutive loops over the same data
@@ -4256,6 +4281,14 @@ Style/IpAddresses:
4256
4281
  - '**/gems.rb'
4257
4282
  - '**/*.gemspec'
4258
4283
 
4284
+ Style/KeywordArgumentsMerging:
4285
+ Description: >-
4286
+ When passing an existing hash as keyword arguments, provide additional arguments
4287
+ directly rather than using `merge`.
4288
+ StyleGuide: '#merging-keyword-arguments'
4289
+ Enabled: pending
4290
+ VersionAdded: '1.68'
4291
+
4259
4292
  Style/KeywordParametersOrder:
4260
4293
  Description: 'Enforces that optional keyword parameters are placed at the end of the parameters list.'
4261
4294
  StyleGuide: '#keyword-parameters-order'
@@ -5241,6 +5274,13 @@ Style/SafeNavigation:
5241
5274
  # Maximum length of method chains for register an offense.
5242
5275
  MaxChainLength: 2
5243
5276
 
5277
+ Style/SafeNavigationChainLength:
5278
+ Description: 'Enforces safe navigation chains length to not exceed the configured maximum.'
5279
+ StyleGuide: '#safe-navigation'
5280
+ Enabled: pending
5281
+ VersionAdded: '1.68'
5282
+ Max: 2
5283
+
5244
5284
  Style/Sample:
5245
5285
  Description: >-
5246
5286
  Use `sample` instead of `shuffle.first`,
@@ -45,15 +45,13 @@ module RuboCop
45
45
 
46
46
  # Restore an offense object loaded from a JSON file.
47
47
  def deserialize_offenses(offenses)
48
- source_buffer = Parser::Source::Buffer.new(@filename)
49
- source_buffer.source = File.read(@filename, encoding: Encoding::UTF_8)
50
48
  offenses.map! do |o|
51
- location = location_from_source_buffer(o, source_buffer)
49
+ location = location_from_source_buffer(o)
52
50
  Cop::Offense.new(o['severity'], location, o['message'], o['cop_name'], o['status'].to_sym)
53
51
  end
54
52
  end
55
53
 
56
- def location_from_source_buffer(offense, source_buffer)
54
+ def location_from_source_buffer(offense)
57
55
  begin_pos = offense['location']['begin_pos']
58
56
  end_pos = offense['location']['end_pos']
59
57
  if begin_pos.zero? && end_pos.zero?
@@ -62,5 +60,15 @@ module RuboCop
62
60
  Parser::Source::Range.new(source_buffer, begin_pos, end_pos)
63
61
  end
64
62
  end
63
+
64
+ # Delay creation until needed. Some type of offenses will have no buffer associated with them
65
+ # and be global only. For these, trying to create the buffer will likely fail, for example
66
+ # because of unknown encoding comments.
67
+ def source_buffer
68
+ @source_buffer ||= begin
69
+ source = File.read(@filename, encoding: Encoding::UTF_8)
70
+ Parser::Source::Buffer.new(@filename, source: source)
71
+ end
72
+ end
65
73
  end
66
74
  end
@@ -78,7 +78,7 @@ module RuboCop
78
78
  Please, report your problems to RuboCop's issue tracker.
79
79
  #{bug_tracker_uri}
80
80
  Mention the following information in the issue report:
81
- #{RuboCop::Version.version(debug: true)}
81
+ #{RuboCop::Version.verbose}
82
82
  WARNING
83
83
  end
84
84
 
@@ -9,8 +9,8 @@ module RuboCop
9
9
  self.command_name = :version
10
10
 
11
11
  def run
12
- puts RuboCop::Version.version(debug: false) if @options[:version]
13
- puts RuboCop::Version.version(debug: true, env: env) if @options[:verbose_version]
12
+ puts RuboCop::Version::STRING if @options[:version]
13
+ puts RuboCop::Version.verbose(env: env) if @options[:verbose_version]
14
14
  end
15
15
  end
16
16
  end
@@ -49,7 +49,9 @@ module RuboCop
49
49
  private
50
50
 
51
51
  def disable_offense(offense_range)
52
- range = surrounding_heredoc(offense_range) || surrounding_percent_array(offense_range)
52
+ range = surrounding_heredoc(offense_range) ||
53
+ surrounding_percent_array(offense_range) ||
54
+ string_continuation(offense_range)
53
55
 
54
56
  if range
55
57
  disable_offense_before_and_after(range_by_lines(range))
@@ -88,10 +90,28 @@ module RuboCop
88
90
  end
89
91
 
90
92
  percent_array.map(&:source_range).find do |range|
91
- offense_range.begin_pos > range.begin_pos && range.overlaps?(offense_range)
93
+ range_overlaps_offense?(offense_range, range)
92
94
  end
93
95
  end
94
96
 
97
+ def string_continuation(offense_range)
98
+ return nil if offense_range.empty?
99
+
100
+ string_continuation_nodes = processed_source.ast.each_descendant.filter_map do |node|
101
+ range_by_lines(node.source_range) if string_continuation?(node)
102
+ end
103
+
104
+ string_continuation_nodes.find { |range| range_overlaps_offense?(offense_range, range) }
105
+ end
106
+
107
+ def range_overlaps_offense?(offense_range, range)
108
+ offense_range.begin_pos > range.begin_pos && range.overlaps?(offense_range)
109
+ end
110
+
111
+ def string_continuation?(node)
112
+ (node.str_type? || node.dstr_type? || node.xstr_type?) && node.source.match?(/\\\s*$/)
113
+ end
114
+
95
115
  def range_of_first_line(range)
96
116
  begin_of_first_line = range.begin_pos - range.column
97
117
  end_of_first_line = begin_of_first_line + range.source_line.length
@@ -47,7 +47,7 @@ module RuboCop
47
47
  if column_delta.positive? && range.resize(1).source != "\n"
48
48
  corrector.insert_before(range, ' ' * column_delta)
49
49
  elsif /\A[ \t]+\z/.match?(range.source)
50
- remove(range, corrector)
50
+ corrector.remove(range)
51
51
  end
52
52
  end
53
53
 
@@ -96,17 +96,6 @@ module RuboCop
96
96
  end
97
97
  end
98
98
 
99
- def remove(range, corrector)
100
- original_stderr = $stderr
101
- $stderr = StringIO.new # Avoid error messages on console
102
- corrector.remove(range)
103
- rescue RuntimeError
104
- range = range_between(range.begin_pos + 1, range.end_pos + 1)
105
- retry if /^ +$/.match?(range.source)
106
- ensure
107
- $stderr = original_stderr
108
- end
109
-
110
99
  def each_line(expr)
111
100
  line_begin_pos = expr.begin_pos
112
101
  expr.source.each_line do |line|
@@ -94,6 +94,16 @@ module RuboCop
94
94
  end
95
95
 
96
96
  def substitute_escaped_delimiters(content, delimiters)
97
+ if delimiters.first != delimiters.last
98
+ # With different delimiters (eg. `[]`, `()`), if there are the same
99
+ # number of each, escaping is not necessary
100
+ delimiter_counts = delimiters.each_with_object({}) do |delimiter, counts|
101
+ counts[delimiter] = content.count(delimiter)
102
+ end
103
+
104
+ return content if delimiter_counts[delimiters.first] == delimiter_counts[delimiters.last]
105
+ end
106
+
97
107
  delimiters.each { |delim| content.gsub!(delim, "\\#{delim}") }
98
108
  end
99
109
 
@@ -67,19 +67,39 @@ module RuboCop
67
67
  # attr_reader :name #: String
68
68
  # attr_reader :age #: Integer?
69
69
  #
70
+ # @example AllowSteepAnnotation: false (default)
71
+ #
72
+ # # bad
73
+ # [1, 2, 3].each_with_object([]) do |n, list| #$ Array[Integer]
74
+ # list << n
75
+ # end
76
+ #
77
+ # name = 'John' #: String
78
+ #
79
+ # @example AllowSteepAnnotation: true
80
+ #
81
+ # # good
82
+ #
83
+ # [1, 2, 3].each_with_object([]) do |n, list| #$ Array[Integer]
84
+ # list << n
85
+ # end
86
+ #
87
+ # name = 'John' #: String
88
+ #
70
89
  class LeadingCommentSpace < Base
71
90
  include RangeHelp
72
91
  extend AutoCorrector
73
92
 
74
93
  MSG = 'Missing space after `#`.'
75
94
 
76
- def on_new_investigation # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
95
+ def on_new_investigation # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
96
  processed_source.comments.each do |comment|
78
97
  next unless /\A(?!#\+\+|#--)(#+[^#\s=])/.match?(comment.text)
79
98
  next if comment.loc.line == 1 && allowed_on_first_line?(comment)
80
99
  next if doxygen_comment_style?(comment)
81
100
  next if gemfile_ruby_comment?(comment)
82
101
  next if rbs_inline_annotation?(comment)
102
+ next if steep_annotation?(comment)
83
103
 
84
104
  add_offense(comment) do |corrector|
85
105
  expr = comment.source_range
@@ -142,6 +162,14 @@ module RuboCop
142
162
  def rbs_inline_annotation?(comment)
143
163
  allow_rbs_inline_annotation? && comment.text.start_with?(/#:|#\[.+\]/)
144
164
  end
165
+
166
+ def allow_steep_annotation?
167
+ cop_config['AllowSteepAnnotation']
168
+ end
169
+
170
+ def steep_annotation?(comment)
171
+ allow_steep_annotation? && comment.text.start_with?(/#[$:]/)
172
+ end
145
173
  end
146
174
  end
147
175
  end
@@ -33,12 +33,12 @@ module RuboCop
33
33
  private
34
34
 
35
35
  def offense_range(node, begin_pos)
36
- if reference_variable_with_brackets?(node)
37
- receiver_end_pos = node.receiver.source_range.end_pos
38
- selector_begin_pos = node.loc.selector.begin_pos
39
- return if receiver_end_pos >= selector_begin_pos
40
- return if dot_before_brackets?(node, receiver_end_pos, selector_begin_pos)
36
+ receiver_end_pos = node.receiver.source_range.end_pos
37
+ selector_begin_pos = node.loc.selector.begin_pos
38
+ return if receiver_end_pos >= selector_begin_pos
39
+ return if dot_before_brackets?(node, receiver_end_pos, selector_begin_pos)
41
40
 
41
+ if reference_variable_with_brackets?(node)
42
42
  range_between(receiver_end_pos, selector_begin_pos)
43
43
  elsif node.method?(:[]=)
44
44
  offense_range_for_assignment(node, begin_pos)
@@ -82,6 +82,10 @@ module RuboCop
82
82
  include RangeHelp
83
83
  extend AutoCorrector
84
84
 
85
+ def self.autocorrect_incompatible_with
86
+ [Style::BlockDelimiters]
87
+ end
88
+
85
89
  def on_block(node)
86
90
  return if node.keywords?
87
91
 
@@ -15,6 +15,9 @@ module RuboCop
15
15
  # With `IgnoreConstantBranches: true`, branches are not registered
16
16
  # as offenses if they return a constant value.
17
17
  #
18
+ # With `IgnoreDuplicateElseBranch: true`, in conditionals with multiple branches,
19
+ # duplicate 'else' branches are not registered as offenses.
20
+ #
18
21
  # @example
19
22
  # # bad
20
23
  # if foo
@@ -83,21 +86,37 @@ module RuboCop
83
86
  # else MEDIUM_SIZE
84
87
  # end
85
88
  #
89
+ # @example IgnoreDuplicateElseBranch: true
90
+ # # good
91
+ # if foo
92
+ # do_foo
93
+ # elsif bar
94
+ # do_bar
95
+ # else
96
+ # do_foo
97
+ # end
98
+ #
86
99
  class DuplicateBranch < Base
87
100
  MSG = 'Duplicate branch body detected.'
88
101
 
89
102
  def on_branching_statement(node)
90
- branches(node).each_with_object(Set.new) do |branch, previous|
91
- next unless consider_branch?(branch)
103
+ branches = branches(node)
104
+ branches.each_with_object(Set.new) do |branch, previous|
105
+ next unless consider_branch?(branches, branch)
92
106
 
93
107
  add_offense(offense_range(branch)) unless previous.add?(branch)
94
108
  end
95
109
  end
96
- alias on_if on_branching_statement
97
110
  alias on_case on_branching_statement
98
111
  alias on_case_match on_branching_statement
99
112
  alias on_rescue on_branching_statement
100
113
 
114
+ def on_if(node)
115
+ # Ignore 'elsif' nodes, because we don't want to check them separately whether
116
+ # the 'else' branch is duplicated. We want to check only on the outermost conditional.
117
+ on_branching_statement(node) unless node.elsif?
118
+ end
119
+
101
120
  private
102
121
 
103
122
  def offense_range(duplicate_branch)
@@ -118,10 +137,14 @@ module RuboCop
118
137
  node.branches.compact
119
138
  end
120
139
 
121
- def consider_branch?(branch)
140
+ def consider_branch?(branches, branch)
122
141
  return false if ignore_literal_branches? && literal_branch?(branch)
123
142
  return false if ignore_constant_branches? && const_branch?(branch)
124
143
 
144
+ if ignore_duplicate_else_branches? && duplicate_else_branch?(branches, branch)
145
+ return false
146
+ end
147
+
125
148
  true
126
149
  end
127
150
 
@@ -133,6 +156,10 @@ module RuboCop
133
156
  cop_config.fetch('IgnoreConstantBranches', false)
134
157
  end
135
158
 
159
+ def ignore_duplicate_else_branches?
160
+ cop_config.fetch('IgnoreDuplicateElseBranch', false)
161
+ end
162
+
136
163
  def literal_branch?(branch) # rubocop:disable Metrics/CyclomaticComplexity
137
164
  return false if !branch.literal? || branch.xstr_type?
138
165
  return true if branch.basic_literal?
@@ -147,6 +174,14 @@ module RuboCop
147
174
  def const_branch?(branch)
148
175
  branch.const_type?
149
176
  end
177
+
178
+ def duplicate_else_branch?(branches, branch)
179
+ return false unless (parent = branch.parent)
180
+
181
+ branches.size > 2 &&
182
+ branch.equal?(branches.last) &&
183
+ parent.respond_to?(:else?) && parent.else?
184
+ end
150
185
  end
151
186
  end
152
187
  end
@@ -134,6 +134,7 @@ module RuboCop
134
134
 
135
135
  corrector.replace(node.child_nodes.first.loc.name, 'FileUtils')
136
136
  corrector.replace(node.loc.selector, replacement_method(node))
137
+ corrector.insert_before(node.last_argument, 'mode: ') if require_mode_keyword?(node)
137
138
  end
138
139
 
139
140
  def replacement_method(node)
@@ -152,6 +153,12 @@ module RuboCop
152
153
  force_method_name?(node) || force_option?(node)
153
154
  end
154
155
 
156
+ def require_mode_keyword?(node)
157
+ return false unless node.receiver.const_name == 'Dir'
158
+
159
+ replacement_method(node) == 'mkdir_p' && node.arguments.length == 2
160
+ end
161
+
155
162
  def force_option?(node)
156
163
  node.arguments.any? { |arg| force?(arg) }
157
164
  end
@@ -38,6 +38,8 @@ module RuboCop
38
38
  PATTERN
39
39
 
40
40
  def on_send(node)
41
+ return unless require_safe_navigation?(node)
42
+
41
43
  bad_method?(node) do |safe_nav, method|
42
44
  return if nil_methods.include?(method) || PLUS_MINUS_METHODS.include?(node.method_name)
43
45
 
@@ -52,6 +54,13 @@ module RuboCop
52
54
 
53
55
  private
54
56
 
57
+ def require_safe_navigation?(node)
58
+ parent = node.parent
59
+ return true unless parent&.and_type?
60
+
61
+ parent.rhs != node || parent.lhs.receiver != parent.rhs.receiver
62
+ end
63
+
55
64
  # @param [Parser::Source::Range] offense_range
56
65
  # @param [RuboCop::AST::SendNode] send_node
57
66
  # @return [String]
@@ -94,7 +94,9 @@ module RuboCop
94
94
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
95
95
 
96
96
  def already_appropriate_call?(operand, dot_op)
97
- (operand.safe_navigation? && dot_op == '&.') || (operand.dot? && dot_op == '.')
97
+ return true if operand.safe_navigation? && dot_op == '&.'
98
+
99
+ (operand.dot? || operand.operator_method?) && dot_op == '.'
98
100
  end
99
101
 
100
102
  def register_offense(operand, dot_operator)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for Regexpes (both literals and via `Regexp.new` / `Regexp.compile`)
7
+ # that contain unescaped `]` characters.
8
+ #
9
+ # It emulates the following Ruby warning:
10
+ #
11
+ # [source,ruby]
12
+ # ----
13
+ # $ ruby -e '/abc]123/'
14
+ # -e:1: warning: regular expression has ']' without escape: /abc]123/
15
+ # ----
16
+ #
17
+ # @example
18
+ # # bad
19
+ # /abc]123/
20
+ # %r{abc]123}
21
+ # Regexp.new('abc]123')
22
+ # Regexp.compile('abc]123')
23
+ #
24
+ # # good
25
+ # /abc\]123/
26
+ # %r{abc\]123}
27
+ # Regexp.new('abc\]123')
28
+ # Regexp.compile('abc\]123')
29
+ #
30
+ class UnescapedBracketInRegexp < Base
31
+ extend AutoCorrector
32
+
33
+ MSG = 'Regular expression has `]` without escape.'
34
+ RESTRICT_ON_SEND = %i[new compile].freeze
35
+
36
+ # @!method regexp_constructor(node)
37
+ def_node_search :regexp_constructor, <<~PATTERN
38
+ (send
39
+ (const {nil? cbase} :Regexp) {:new :compile}
40
+ $str
41
+ ...
42
+ )
43
+ PATTERN
44
+
45
+ def on_regexp(node)
46
+ RuboCop::Util.silence_warnings do
47
+ node.parsed_tree&.each_expression do |expr|
48
+ detect_offenses(node, expr)
49
+ end
50
+ end
51
+ end
52
+
53
+ def on_send(node)
54
+ # Ignore nodes that contain interpolation
55
+ return if node.each_descendant(:dstr).any?
56
+
57
+ regexp_constructor(node) do |text|
58
+ Regexp::Parser.parse(text.value)&.each_expression do |expr|
59
+ detect_offenses(text, expr)
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def detect_offenses(node, expr)
67
+ return unless expr.type?(:literal)
68
+
69
+ expr.text.scan(/(?<!\\)\]/) do
70
+ pos = Regexp.last_match.begin(0)
71
+ next if pos.zero? # if the unescaped bracket is the first character, Ruby does not warn
72
+
73
+ location = range_at_index(node, expr.ts, pos)
74
+
75
+ add_offense(location) do |corrector|
76
+ corrector.replace(location, '\]')
77
+ end
78
+ end
79
+ end
80
+
81
+ def range_at_index(node, index, offset)
82
+ adjustment = index + offset
83
+ node.loc.begin.end.adjust(begin_pos: adjustment, end_pos: adjustment + 1)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -14,11 +14,14 @@ module RuboCop
14
14
  # and ||/or is shorthand for a sequence of ifs, so they also add one.
15
15
  # Loops can be said to have an exit condition, so they add one.
16
16
  # Blocks that are calls to builtin iteration methods
17
- # (e.g. `ary.map{...}) also add one, others are ignored.
17
+ # (e.g. `ary.map{...}`) also add one, others are ignored.
18
+ #
19
+ # @example
18
20
  #
19
21
  # def each_child_node(*types) # count begins: 1
20
22
  # unless block_given? # unless: +1
21
23
  # return to_enum(__method__, *types)
24
+ # end
22
25
  #
23
26
  # children.each do |child| # each{}: +1
24
27
  # next unless child.is_a?(Node) # unless: +1
@@ -44,6 +44,8 @@ module RuboCop
44
44
  module CheckLineBreakable
45
45
  def extract_breakable_node(node, max)
46
46
  if node.send_type?
47
+ return if chained_to_heredoc?(node)
48
+
47
49
  args = process_args(node.arguments)
48
50
  return extract_breakable_node_from_elements(node, args, max)
49
51
  elsif node.def_type?
@@ -222,6 +224,14 @@ module RuboCop
222
224
 
223
225
  !node.single_line?
224
226
  end
227
+
228
+ def chained_to_heredoc?(node)
229
+ while (node = node.receiver)
230
+ return true if (node.str_type? || node.dstr_type? || node.xstr_type?) && node.heredoc?
231
+ end
232
+
233
+ false
234
+ end
225
235
  end
226
236
  end
227
237
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for rewriting endless methods to normal method definitions
6
+ module EndlessMethodRewriter
7
+ def correct_to_multiline(corrector, node)
8
+ replacement = <<~RUBY.strip
9
+ def #{node.method_name}#{arguments(node)}
10
+ #{node.body.source}
11
+ end
12
+ RUBY
13
+
14
+ corrector.replace(node, replacement)
15
+ end
16
+
17
+ private
18
+
19
+ def arguments(node, missing = '')
20
+ node.arguments.any? ? node.arguments.source : missing
21
+ end
22
+ end
23
+ end
24
+ end
@@ -29,7 +29,9 @@ module RuboCop
29
29
  end
30
30
 
31
31
  def uninterpolated_string?(node)
32
- node.str_type? || (node.dstr_type? && node.each_descendant(:begin).none?)
32
+ node.str_type? || (
33
+ node.dstr_type? && node.each_descendant(:begin, :ivar, :cvar, :gvar).none?
34
+ )
33
35
  end
34
36
 
35
37
  def uninterpolated_heredoc?(node)
@@ -48,7 +48,7 @@ module RuboCop
48
48
  MSG = 'Use %<style>s block forwarding.'
49
49
 
50
50
  def self.autocorrect_incompatible_with
51
- [Lint::AmbiguousOperator, Style::ArgumentsForwarding]
51
+ [Lint::AmbiguousOperator, Style::ArgumentsForwarding, Style::ExplicitBlockArgument]
52
52
  end
53
53
 
54
54
  def on_def(node)