-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathio_readlines.rb
112 lines (92 loc) · 3.72 KB
/
io_readlines.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Identifies places where inefficient `readlines` method
# can be replaced by `each_line` to avoid fully loading file content into memory.
#
# @example
#
# # bad
# File.readlines('testfile').each { |l| puts l }
# IO.readlines('testfile', chomp: true).each { |l| puts l }
#
# conn.readlines(10).map { |l| l.size }
# file.readlines.find { |l| l.start_with?('#') }
# file.readlines.each { |l| puts l }
#
# # good
# File.open('testfile', 'r').each_line { |l| puts l }
# IO.open('testfile').each_line(chomp: true) { |l| puts l }
#
# conn.each_line(10).map { |l| l.size }
# file.each_line.find { |l| l.start_with?('#') }
# file.each_line { |l| puts l }
#
class IoReadlines < Base
include RangeHelp
extend AutoCorrector
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
RESTRICT_ON_SEND = (Enumerable.instance_methods + [:each]).freeze
def_node_matcher :readlines_on_class?, <<~PATTERN
$(send $(send (const nil? {:IO :File}) :readlines ...) _)
PATTERN
def_node_matcher :readlines_on_instance?, <<~PATTERN
$(send $(send ${nil? !const_type?} :readlines ...) _ ...)
PATTERN
def on_send(node)
return unless (captured_values = readlines_on_class?(node) || readlines_on_instance?(node))
enumerable_call, readlines_call, receiver = *captured_values
range = offense_range(enumerable_call, readlines_call)
good_method = build_good_method(enumerable_call)
bad_method = build_bad_method(enumerable_call)
add_offense(range, message: format(MSG, good: good_method, bad: bad_method)) do |corrector|
autocorrect(corrector, enumerable_call, readlines_call, receiver)
end
end
private
def autocorrect(corrector, enumerable_call, readlines_call, receiver)
# We cannot safely correct `.readlines` method called on IO/File classes
# due to its signature and we are not sure with implicit receiver
# if it is called in the context of some instance or mentioned class.
return if receiver.nil?
range = correction_range(enumerable_call, readlines_call)
if readlines_call.arguments?
call_args = build_call_args(readlines_call.arguments)
replacement = "each_line(#{call_args})"
else
replacement = 'each_line'
end
corrector.replace(range, replacement)
end
def offense_range(enumerable_call, readlines_call)
readlines_pos = readlines_call.loc.selector.begin_pos
enumerable_pos = enumerable_call.loc.selector.end_pos
range_between(readlines_pos, enumerable_pos)
end
def build_good_method(enumerable_call)
if enumerable_call.method?(:each)
'each_line'
else
"each_line.#{enumerable_call.method_name}"
end
end
def build_bad_method(enumerable_call)
"readlines.#{enumerable_call.method_name}"
end
def correction_range(enumerable_call, readlines_call)
begin_pos = readlines_call.loc.selector.begin_pos
end_pos = if enumerable_call.method?(:each)
enumerable_call.source_range.end_pos
else
enumerable_call.loc.dot.begin_pos
end
range_between(begin_pos, end_pos)
end
def build_call_args(call_args_node)
call_args_node.map(&:source).join(', ')
end
end
end
end
end