rubocop-performance 1.5.0 → 1.7.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +5 -1
  4. data/config/default.yml +75 -6
  5. data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +76 -0
  6. data/lib/rubocop/cop/mixin/sort_block.rb +28 -0
  7. data/lib/rubocop/cop/performance/ancestors_include.rb +45 -0
  8. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +43 -0
  9. data/lib/rubocop/cop/performance/bind_call.rb +87 -0
  10. data/lib/rubocop/cop/performance/caller.rb +3 -3
  11. data/lib/rubocop/cop/performance/casecmp.rb +5 -3
  12. data/lib/rubocop/cop/performance/chain_array_allocation.rb +1 -1
  13. data/lib/rubocop/cop/performance/compare_with_block.rb +2 -2
  14. data/lib/rubocop/cop/performance/count.rb +3 -3
  15. data/lib/rubocop/cop/performance/delete_prefix.rb +96 -0
  16. data/lib/rubocop/cop/performance/delete_suffix.rb +96 -0
  17. data/lib/rubocop/cop/performance/detect.rb +1 -1
  18. data/lib/rubocop/cop/performance/double_start_end_with.rb +2 -2
  19. data/lib/rubocop/cop/performance/end_with.rb +36 -13
  20. data/lib/rubocop/cop/performance/fixed_size.rb +1 -1
  21. data/lib/rubocop/cop/performance/flat_map.rb +1 -1
  22. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +1 -1
  23. data/lib/rubocop/cop/performance/io_readlines.rb +127 -0
  24. data/lib/rubocop/cop/performance/open_struct.rb +1 -1
  25. data/lib/rubocop/cop/performance/range_include.rb +10 -8
  26. data/lib/rubocop/cop/performance/redundant_block_call.rb +3 -3
  27. data/lib/rubocop/cop/performance/redundant_match.rb +2 -2
  28. data/lib/rubocop/cop/performance/redundant_merge.rb +21 -8
  29. data/lib/rubocop/cop/performance/redundant_sort_block.rb +53 -0
  30. data/lib/rubocop/cop/performance/redundant_string_chars.rb +137 -0
  31. data/lib/rubocop/cop/performance/regexp_match.rb +13 -13
  32. data/lib/rubocop/cop/performance/reverse_each.rb +3 -2
  33. data/lib/rubocop/cop/performance/reverse_first.rb +78 -0
  34. data/lib/rubocop/cop/performance/size.rb +35 -37
  35. data/lib/rubocop/cop/performance/sort_reverse.rb +54 -0
  36. data/lib/rubocop/cop/performance/squeeze.rb +70 -0
  37. data/lib/rubocop/cop/performance/start_with.rb +36 -16
  38. data/lib/rubocop/cop/performance/string_include.rb +57 -0
  39. data/lib/rubocop/cop/performance/string_replacement.rb +4 -11
  40. data/lib/rubocop/cop/performance/times_map.rb +1 -1
  41. data/lib/rubocop/cop/performance/unfreeze_string.rb +3 -7
  42. data/lib/rubocop/cop/performance/uri_default_parser.rb +1 -1
  43. data/lib/rubocop/cop/performance_cops.rb +15 -0
  44. data/lib/rubocop/performance/inject.rb +1 -1
  45. data/lib/rubocop/performance/version.rb +1 -1
  46. metadata +25 -11
@@ -24,14 +24,14 @@ module RuboCop
24
24
  MSG_FIRST = 'Use `%<method>s(%<n>d..%<n>d).first`' \
25
25
  ' instead of `%<method>s.first`.'
26
26
 
27
- def_node_matcher :slow_caller?, <<-PATTERN
27
+ def_node_matcher :slow_caller?, <<~PATTERN
28
28
  {
29
29
  (send nil? {:caller :caller_locations})
30
30
  (send nil? {:caller :caller_locations} int)
31
31
  }
32
32
  PATTERN
33
33
 
34
- def_node_matcher :caller_with_scope_method?, <<-PATTERN
34
+ def_node_matcher :caller_with_scope_method?, <<~PATTERN
35
35
  {
36
36
  (send #slow_caller? :first)
37
37
  (send #slow_caller? :[] int)
@@ -51,7 +51,7 @@ module RuboCop
51
51
  caller_arg = node.receiver.first_argument
52
52
  n = caller_arg ? int_value(caller_arg) : 1
53
53
 
54
- if node.method_name == :[]
54
+ if node.method?(:[])
55
55
  m = int_value(node.first_argument)
56
56
  n += m
57
57
  format(MSG_BRACE, n: n, m: m, method: method_name)
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Performance
6
6
  # This cop identifies places where a case-insensitive string comparison
7
7
  # can better be implemented using `casecmp`.
8
+ # This cop is unsafe because `String#casecmp` and `String#casecmp?` behave
9
+ # differently when using Non-ASCII characters.
8
10
  #
9
11
  # @example
10
12
  # # bad
@@ -21,21 +23,21 @@ module RuboCop
21
23
  MSG = 'Use `%<good>s` instead of `%<bad>s`.'
22
24
  CASE_METHODS = %i[downcase upcase].freeze
23
25
 
24
- def_node_matcher :downcase_eq, <<-PATTERN
26
+ def_node_matcher :downcase_eq, <<~PATTERN
25
27
  (send
26
28
  $(send _ ${:downcase :upcase})
27
29
  ${:== :eql? :!=}
28
30
  ${str (send _ {:downcase :upcase} ...) (begin str)})
29
31
  PATTERN
30
32
 
31
- def_node_matcher :eq_downcase, <<-PATTERN
33
+ def_node_matcher :eq_downcase, <<~PATTERN
32
34
  (send
33
35
  {str (send _ {:downcase :upcase} ...) (begin str)}
34
36
  ${:== :eql? :!=}
35
37
  $(send _ ${:downcase :upcase}))
36
38
  PATTERN
37
39
 
38
- def_node_matcher :downcase_downcase, <<-PATTERN
40
+ def_node_matcher :downcase_downcase, <<~PATTERN
39
41
  (send
40
42
  $(send _ ${:downcase :upcase})
41
43
  ${:== :eql? :!=}
@@ -51,7 +51,7 @@ module RuboCop
51
51
  '(followed by `return array` if required) instead of chaining '\
52
52
  '`%<method>s...%<second_method>s`.'
53
53
 
54
- def_node_matcher :flat_map_candidate?, <<-PATTERN
54
+ def_node_matcher :flat_map_candidate?, <<~PATTERN
55
55
  {
56
56
  (send (send _ ${#{RETURN_NEW_ARRAY_WHEN_ARGS}} {int lvar ivar cvar gvar}) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
57
57
  (send (block (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY} }) ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
@@ -30,14 +30,14 @@ module RuboCop
30
30
  '`%<compare_method>s { |%<var_a>s, %<var_b>s| %<str_a>s ' \
31
31
  '<=> %<str_b>s }`.'
32
32
 
33
- def_node_matcher :compare?, <<-PATTERN
33
+ def_node_matcher :compare?, <<~PATTERN
34
34
  (block
35
35
  $(send _ {:sort :min :max})
36
36
  (args (arg $_a) (arg $_b))
37
37
  $send)
38
38
  PATTERN
39
39
 
40
- def_node_matcher :replaceable_body?, <<-PATTERN
40
+ def_node_matcher :replaceable_body?, <<~PATTERN
41
41
  (send
42
42
  (send (lvar %1) $_method $...)
43
43
  :<=>
@@ -32,17 +32,17 @@ module RuboCop
32
32
  # make `count` work with a block is to call `to_a.count {...}`.
33
33
  #
34
34
  # Example:
35
- # Model.where(id: [1, 2, 3].select { |m| m.method == true }.size
35
+ # `Model.where(id: [1, 2, 3]).select { |m| m.method == true }.size`
36
36
  #
37
37
  # becomes:
38
38
  #
39
- # Model.where(id: [1, 2, 3]).to_a.count { |m| m.method == true }
39
+ # `Model.where(id: [1, 2, 3]).to_a.count { |m| m.method == true }`
40
40
  class Count < Cop
41
41
  include RangeHelp
42
42
 
43
43
  MSG = 'Use `count` instead of `%<selector>s...%<counter>s`.'
44
44
 
45
- def_node_matcher :count_candidate?, <<-PATTERN
45
+ def_node_matcher :count_candidate?, <<~PATTERN
46
46
  {
47
47
  (send (block $(send _ ${:select :reject}) ...) ${:count :length :size})
48
48
  (send $(send _ ${:select :reject} (:block_pass _)) ${:count :length :size})
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # In Ruby 2.5, `String#delete_prefix` has been added.
7
+ #
8
+ # This cop identifies places where `gsub(/\Aprefix/, '')` and `sub(/\Aprefix/, '')`
9
+ # can be replaced by `delete_prefix('prefix')`.
10
+ #
11
+ # This cop has `SafeMultiline` configuration option that `true` by default because
12
+ # `^prefix` is unsafe as it will behave incompatible with `delete_prefix`
13
+ # for receiver is multiline string.
14
+ #
15
+ # The `delete_prefix('prefix')` method is faster than `gsub(/\Aprefix/, '')`.
16
+ #
17
+ # @example
18
+ #
19
+ # # bad
20
+ # str.gsub(/\Aprefix/, '')
21
+ # str.gsub!(/\Aprefix/, '')
22
+ #
23
+ # str.sub(/\Aprefix/, '')
24
+ # str.sub!(/\Aprefix/, '')
25
+ #
26
+ # # good
27
+ # str.delete_prefix('prefix')
28
+ # str.delete_prefix!('prefix')
29
+ #
30
+ # @example SafeMultiline: true (default)
31
+ #
32
+ # # good
33
+ # str.gsub(/^prefix/, '')
34
+ # str.gsub!(/^prefix/, '')
35
+ # str.sub(/^prefix/, '')
36
+ # str.sub!(/^prefix/, '')
37
+ #
38
+ # @example SafeMultiline: false
39
+ #
40
+ # # bad
41
+ # str.gsub(/^prefix/, '')
42
+ # str.gsub!(/^prefix/, '')
43
+ # str.sub(/^prefix/, '')
44
+ # str.sub!(/^prefix/, '')
45
+ #
46
+ class DeletePrefix < Cop
47
+ extend TargetRubyVersion
48
+ include RegexpMetacharacter
49
+
50
+ minimum_target_ruby_version 2.5
51
+
52
+ MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
53
+
54
+ PREFERRED_METHODS = {
55
+ gsub: :delete_prefix,
56
+ gsub!: :delete_prefix!,
57
+ sub: :delete_prefix,
58
+ sub!: :delete_prefix!
59
+ }.freeze
60
+
61
+ def_node_matcher :delete_prefix_candidate?, <<~PATTERN
62
+ (send $!nil? ${:gsub :gsub! :sub :sub!} (regexp (str $#literal_at_start?) (regopt)) (str $_))
63
+ PATTERN
64
+
65
+ def on_send(node)
66
+ delete_prefix_candidate?(node) do |_, bad_method, _, replace_string|
67
+ return unless replace_string.blank?
68
+
69
+ good_method = PREFERRED_METHODS[bad_method]
70
+
71
+ message = format(MSG, current: bad_method, prefer: good_method)
72
+
73
+ add_offense(node, location: :selector, message: message)
74
+ end
75
+ end
76
+
77
+ def autocorrect(node)
78
+ delete_prefix_candidate?(node) do |receiver, bad_method, regexp_str, _|
79
+ lambda do |corrector|
80
+ good_method = PREFERRED_METHODS[bad_method]
81
+ regexp_str = drop_start_metacharacter(regexp_str)
82
+ regexp_str = interpret_string_escapes(regexp_str)
83
+ string_literal = to_string_literal(regexp_str)
84
+
85
+ new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
86
+
87
+ # TODO: `source_range` is no longer required when RuboCop 0.81 or lower support will be dropped.
88
+ # https://github.com/rubocop-hq/rubocop/commit/82eb350d2cba16
89
+ corrector.replace(node.source_range, new_code)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # In Ruby 2.5, `String#delete_suffix` has been added.
7
+ #
8
+ # This cop identifies places where `gsub(/suffix\z/, '')` and `sub(/suffix\z/, '')`
9
+ # can be replaced by `delete_suffix('suffix')`.
10
+ #
11
+ # This cop has `SafeMultiline` configuration option that `true` by default because
12
+ # `suffix$` is unsafe as it will behave incompatible with `delete_suffix?`
13
+ # for receiver is multiline string.
14
+ #
15
+ # The `delete_suffix('suffix')` method is faster than `gsub(/suffix\z/, '')`.
16
+ #
17
+ # @example
18
+ #
19
+ # # bad
20
+ # str.gsub(/suffix\z/, '')
21
+ # str.gsub!(/suffix\z/, '')
22
+ #
23
+ # str.sub(/suffix\z/, '')
24
+ # str.sub!(/suffix\z/, '')
25
+ #
26
+ # # good
27
+ # str.delete_suffix('suffix')
28
+ # str.delete_suffix!('suffix')
29
+ #
30
+ # @example SafeMultiline: true (default)
31
+ #
32
+ # # good
33
+ # str.gsub(/suffix$/, '')
34
+ # str.gsub!(/suffix$/, '')
35
+ # str.sub(/suffix$/, '')
36
+ # str.sub!(/suffix$/, '')
37
+ #
38
+ # @example SafeMultiline: false
39
+ #
40
+ # # bad
41
+ # str.gsub(/suffix$/, '')
42
+ # str.gsub!(/suffix$/, '')
43
+ # str.sub(/suffix$/, '')
44
+ # str.sub!(/suffix$/, '')
45
+ #
46
+ class DeleteSuffix < Cop
47
+ extend TargetRubyVersion
48
+ include RegexpMetacharacter
49
+
50
+ minimum_target_ruby_version 2.5
51
+
52
+ MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
53
+
54
+ PREFERRED_METHODS = {
55
+ gsub: :delete_suffix,
56
+ gsub!: :delete_suffix!,
57
+ sub: :delete_suffix,
58
+ sub!: :delete_suffix!
59
+ }.freeze
60
+
61
+ def_node_matcher :delete_suffix_candidate?, <<~PATTERN
62
+ (send $!nil? ${:gsub :gsub! :sub :sub!} (regexp (str $#literal_at_end?) (regopt)) (str $_))
63
+ PATTERN
64
+
65
+ def on_send(node)
66
+ delete_suffix_candidate?(node) do |_, bad_method, _, replace_string|
67
+ return unless replace_string.blank?
68
+
69
+ good_method = PREFERRED_METHODS[bad_method]
70
+
71
+ message = format(MSG, current: bad_method, prefer: good_method)
72
+
73
+ add_offense(node, location: :selector, message: message)
74
+ end
75
+ end
76
+
77
+ def autocorrect(node)
78
+ delete_suffix_candidate?(node) do |receiver, bad_method, regexp_str, _|
79
+ lambda do |corrector|
80
+ good_method = PREFERRED_METHODS[bad_method]
81
+ regexp_str = drop_end_metacharacter(regexp_str)
82
+ regexp_str = interpret_string_escapes(regexp_str)
83
+ string_literal = to_string_literal(regexp_str)
84
+
85
+ new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
86
+
87
+ # TODO: `source_range` is no longer required when RuboCop 0.81 or lower support will be dropped.
88
+ # https://github.com/rubocop-hq/rubocop/commit/82eb350d2cba16
89
+ corrector.replace(node.source_range, new_code)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -28,7 +28,7 @@ module RuboCop
28
28
  REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of ' \
29
29
  '`%<first_method>s.%<second_method>s`.'
30
30
 
31
- def_node_matcher :detect_candidate?, <<-PATTERN
31
+ def_node_matcher :detect_candidate?, <<~PATTERN
32
32
  {
33
33
  (send $(block (send _ {:select :find_all}) ...) ${:first :last} $...)
34
34
  (send $(send _ {:select :find_all} ...) ${:first :last} $...)
@@ -75,13 +75,13 @@ module RuboCop
75
75
  cop_config['IncludeActiveSupportAliases']
76
76
  end
77
77
 
78
- def_node_matcher :two_start_end_with_calls, <<-PATTERN
78
+ def_node_matcher :two_start_end_with_calls, <<~PATTERN
79
79
  (or
80
80
  (send $_recv [{:start_with? :end_with?} $_method] $...)
81
81
  (send _recv _method $...))
82
82
  PATTERN
83
83
 
84
- def_node_matcher :check_with_active_support_aliases, <<-PATTERN
84
+ def_node_matcher :check_with_active_support_aliases, <<~PATTERN
85
85
  (or
86
86
  (send $_recv
87
87
  [{:start_with? :starts_with? :end_with? :ends_with?} $_method]
@@ -3,44 +3,67 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Performance
6
- # This cop identifies unnecessary use of a regex where `String#end_with?`
7
- # would suffice.
6
+ # This cop identifies unnecessary use of a regex where `String#end_with?` would suffice.
7
+ #
8
+ # This cop has `SafeMultiline` configuration option that `true` by default because
9
+ # `end$` is unsafe as it will behave incompatible with `end_with?`
10
+ # for receiver is multiline string.
8
11
  #
9
12
  # @example
10
13
  # # bad
11
14
  # 'abc'.match?(/bc\Z/)
15
+ # /bc\Z/.match?('abc')
12
16
  # 'abc' =~ /bc\Z/
17
+ # /bc\Z/ =~ 'abc'
13
18
  # 'abc'.match(/bc\Z/)
19
+ # /bc\Z/.match('abc')
14
20
  #
15
21
  # # good
16
22
  # 'abc'.end_with?('bc')
23
+ #
24
+ # @example SafeMultiline: true (default)
25
+ #
26
+ # # good
27
+ # 'abc'.match?(/bc$/)
28
+ # /bc$/.match?('abc')
29
+ # 'abc' =~ /bc$/
30
+ # /bc$/ =~ 'abc'
31
+ # 'abc'.match(/bc$/)
32
+ # /bc$/.match('abc')
33
+ #
34
+ # @example SafeMultiline: false
35
+ #
36
+ # # bad
37
+ # 'abc'.match?(/bc$/)
38
+ # /bc$/.match?('abc')
39
+ # 'abc' =~ /bc$/
40
+ # /bc$/ =~ 'abc'
41
+ # 'abc'.match(/bc$/)
42
+ # /bc$/.match('abc')
43
+ #
17
44
  class EndWith < Cop
45
+ include RegexpMetacharacter
46
+
18
47
  MSG = 'Use `String#end_with?` instead of a regex match anchored to ' \
19
48
  'the end of the string.'
20
- SINGLE_QUOTE = "'"
21
49
 
22
- def_node_matcher :redundant_regex?, <<-PATTERN
50
+ def_node_matcher :redundant_regex?, <<~PATTERN
23
51
  {(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_end?) (regopt)))
24
- (send (regexp (str $#literal_at_end?) (regopt)) {:match :=~} $_)}
52
+ (send (regexp (str $#literal_at_end?) (regopt)) {:match :match?} $_)
53
+ (match-with-lvasgn (regexp (str $#literal_at_end?) (regopt)) $_)}
25
54
  PATTERN
26
55
 
27
- def literal_at_end?(regex_str)
28
- # is this regexp 'literal' in the sense of only matching literal
29
- # chars, rather than using metachars like . and * and so on?
30
- # also, is it anchored at the end of the string?
31
- regex_str =~ /\A(?:#{LITERAL_REGEX})+\\z\z/
32
- end
33
-
34
56
  def on_send(node)
35
57
  return unless redundant_regex?(node)
36
58
 
37
59
  add_offense(node)
38
60
  end
61
+ alias on_match_with_lvasgn on_send
39
62
 
40
63
  def autocorrect(node)
41
64
  redundant_regex?(node) do |receiver, regex_str|
42
65
  receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
43
- regex_str = regex_str[0..-3] # drop \Z anchor
66
+ regex_str = drop_end_metacharacter(regex_str)
44
67
  regex_str = interpret_string_escapes(regex_str)
45
68
 
46
69
  lambda do |corrector|
@@ -48,7 +48,7 @@ module RuboCop
48
48
  class FixedSize < Cop
49
49
  MSG = 'Do not compute the size of statically sized objects.'
50
50
 
51
- def_node_matcher :counter, <<-MATCHER
51
+ def_node_matcher :counter, <<~MATCHER
52
52
  (send ${array hash str sym} {:count :length :size} $...)
53
53
  MATCHER
54
54
 
@@ -22,7 +22,7 @@ module RuboCop
22
22
  'and `flatten` can be used to flatten ' \
23
23
  'multiple levels.'
24
24
 
25
- def_node_matcher :flat_map_candidate?, <<-PATTERN
25
+ def_node_matcher :flat_map_candidate?, <<~PATTERN
26
26
  (send
27
27
  {
28
28
  (block $(send _ ${:collect :map}) ...)
@@ -37,7 +37,7 @@ module RuboCop
37
37
  # h = { a: 1, b: 2 }; h.value?(nil)
38
38
  #
39
39
  class InefficientHashSearch < Cop
40
- def_node_matcher :inefficient_include?, <<-PATTERN
40
+ def_node_matcher :inefficient_include?, <<~PATTERN
41
41
  (send (send $_ {:keys :values}) :include? _)
42
42
  PATTERN
43
43
 
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies places where inefficient `readlines` method
7
+ # can be replaced by `each_line` to avoid fully loading file content into memory.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # File.readlines('testfile').each { |l| puts l }
13
+ # IO.readlines('testfile', chomp: true).each { |l| puts l }
14
+ #
15
+ # conn.readlines(10).map { |l| l.size }
16
+ # file.readlines.find { |l| l.start_with?('#') }
17
+ # file.readlines.each { |l| puts l }
18
+ #
19
+ # # good
20
+ # File.open('testfile', 'r').each_line { |l| puts l }
21
+ # IO.open('testfile').each_line(chomp: true) { |l| puts l }
22
+ #
23
+ # conn.each_line(10).map { |l| l.size }
24
+ # file.each_line.find { |l| l.start_with?('#') }
25
+ # file.each_line { |l| puts l }
26
+ #
27
+ class IoReadlines < Cop
28
+ include RangeHelp
29
+
30
+ MSG = 'Use `%<good>s` instead of `%<bad>s`.'
31
+ ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).freeze
32
+
33
+ def_node_matcher :readlines_on_class?, <<~PATTERN
34
+ $(send $(send (const nil? {:IO :File}) :readlines ...) #enumerable_method?)
35
+ PATTERN
36
+
37
+ def_node_matcher :readlines_on_instance?, <<~PATTERN
38
+ $(send $(send ${nil? !const_type?} :readlines ...) #enumerable_method? ...)
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ readlines_on_class?(node) do |enumerable_call, readlines_call|
43
+ offense(node, enumerable_call, readlines_call)
44
+ end
45
+
46
+ readlines_on_instance?(node) do |enumerable_call, readlines_call, _|
47
+ offense(node, enumerable_call, readlines_call)
48
+ end
49
+ end
50
+
51
+ def autocorrect(node)
52
+ readlines_on_instance?(node) do |enumerable_call, readlines_call, receiver|
53
+ # We cannot safely correct `.readlines` method called on IO/File classes
54
+ # due to its signature and we are not sure with implicit receiver
55
+ # if it is called in the context of some instance or mentioned class.
56
+ return if receiver.nil?
57
+
58
+ lambda do |corrector|
59
+ range = correction_range(enumerable_call, readlines_call)
60
+
61
+ if readlines_call.arguments?
62
+ call_args = build_call_args(readlines_call.arguments)
63
+ replacement = "each_line(#{call_args})"
64
+ else
65
+ replacement = 'each_line'
66
+ end
67
+
68
+ corrector.replace(range, replacement)
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def enumerable_method?(node)
76
+ ENUMERABLE_METHODS.include?(node.to_sym)
77
+ end
78
+
79
+ def offense(node, enumerable_call, readlines_call)
80
+ range = offense_range(enumerable_call, readlines_call)
81
+ good_method = build_good_method(enumerable_call)
82
+ bad_method = build_bad_method(enumerable_call)
83
+
84
+ add_offense(
85
+ node,
86
+ location: range,
87
+ message: format(MSG, good: good_method, bad: bad_method)
88
+ )
89
+ end
90
+
91
+ def offense_range(enumerable_call, readlines_call)
92
+ readlines_pos = readlines_call.loc.selector.begin_pos
93
+ enumerable_pos = enumerable_call.loc.selector.end_pos
94
+ range_between(readlines_pos, enumerable_pos)
95
+ end
96
+
97
+ def build_good_method(enumerable_call)
98
+ if enumerable_call.method?(:each)
99
+ 'each_line'
100
+ else
101
+ "each_line.#{enumerable_call.method_name}"
102
+ end
103
+ end
104
+
105
+ def build_bad_method(enumerable_call)
106
+ "readlines.#{enumerable_call.method_name}"
107
+ end
108
+
109
+ def correction_range(enumerable_call, readlines_call)
110
+ begin_pos = readlines_call.loc.selector.begin_pos
111
+
112
+ end_pos = if enumerable_call.method?(:each)
113
+ enumerable_call.loc.expression.end_pos
114
+ else
115
+ enumerable_call.loc.dot.begin_pos
116
+ end
117
+
118
+ range_between(begin_pos, end_pos)
119
+ end
120
+
121
+ def build_call_args(call_args_node)
122
+ call_args_node.map(&:source).join(', ')
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end