rubocop-performance 1.5.0 → 1.7.0

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