rubocop 1.67.0 → 1.68.0

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