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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
# frozen_string_literal: true
module SyntaxSuggest
# Represents a single line of code of a given source file
#
# This object contains metadata about the line such as
# amount of indentation, if it is empty or not, and
# lexical data, such as if it has an `end` or a keyword
# in it.
#
# Visibility of lines can be toggled off. Marking a line as invisible
# indicates that it should not be used for syntax checks.
# It's functionally the same as commenting it out.
#
# Example:
#
# line = CodeLine.from_source("def foo\n").first
# line.number => 1
# line.empty? # => false
# line.visible? # => true
# line.mark_invisible
# line.visible? # => false
#
class CodeLine
TRAILING_SLASH = ("\\" + $/).freeze
# Returns an array of CodeLine objects
# from the source string
def self.from_source(source, lines: nil)
lines ||= source.lines
lex_array_for_line = LexAll.new(source: source, source_lines: lines).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
lines.map.with_index do |line, index|
CodeLine.new(
line: line,
index: index,
lex: lex_array_for_line[index + 1]
)
end
end
attr_reader :line, :index, :lex, :line_number, :indent
def initialize(line:, index:, lex:)
@lex = lex
@line = line
@index = index
@original = line
@line_number = @index + 1
strip_line = line.dup
strip_line.lstrip!
if strip_line.empty?
@empty = true
@indent = 0
else
@empty = false
@indent = line.length - strip_line.length
end
set_kw_end
end
# Used for stable sort via indentation level
#
# Ruby's sort is not "stable" meaning that when
# multiple elements have the same value, they are
# not guaranteed to return in the same order they
# were put in.
#
# So when multiple code lines have the same indentation
# level, they're sorted by their index value which is unique
# and consistent.
#
# This is mostly needed for consistency of the test suite
def indent_index
@indent_index ||= [indent, index]
end
alias_method :number, :line_number
# Returns true if the code line is determined
# to contain a keyword that matches with an `end`
#
# For example: `def`, `do`, `begin`, `ensure`, etc.
def is_kw?
@is_kw
end
# Returns true if the code line is determined
# to contain an `end` keyword
def is_end?
@is_end
end
# Used to hide lines
#
# The search alorithm will group lines into blocks
# then if those blocks are determined to represent
# valid code they will be hidden
def mark_invisible
@line = ""
end
# Means the line was marked as "invisible"
# Confusingly, "empty" lines are visible...they
# just don't contain any source code other than a newline ("\n").
def visible?
!line.empty?
end
# Opposite or `visible?` (note: different than `empty?`)
def hidden?
!visible?
end
# An `empty?` line is one that was originally left
# empty in the source code, while a "hidden" line
# is one that we've since marked as "invisible"
def empty?
@empty
end
# Opposite of `empty?` (note: different than `visible?`)
def not_empty?
!empty?
end
# Renders the given line
#
# Also allows us to represent source code as
# an array of code lines.
#
# When we have an array of code line elements
# calling `join` on the array will call `to_s`
# on each element, which essentially converts
# it back into it's original source string.
def to_s
line
end
# When the code line is marked invisible
# we retain the original value of it's line
# this is useful for debugging and for
# showing extra context
#
# DisplayCodeWithLineNumbers will render
# all lines given to it, not just visible
# lines, it uses the original method to
# obtain them.
attr_reader :original
# Comparison operator, needed for equality
# and sorting
def <=>(other)
index <=> other.index
end
# [Not stable API]
#
# Lines that have a `on_ignored_nl` type token and NOT
# a `BEG` type seem to be a good proxy for the ability
# to join multiple lines into one.
#
# This predicate method is used to determine when those
# two criteria have been met.
#
# The one known case this doesn't handle is:
#
# Ripper.lex <<~EOM
# a &&
# b ||
# c
# EOM
#
# For some reason this introduces `on_ignore_newline` but with BEG type
def ignore_newline_not_beg?
@ignore_newline_not_beg
end
# Determines if the given line has a trailing slash
#
# lines = CodeLine.from_source(<<~EOM)
# it "foo" \
# EOM
# expect(lines.first.trailing_slash?).to eq(true)
#
def trailing_slash?
last = @lex.last
return false unless last
return false unless last.type == :on_sp
last.token == TRAILING_SLASH
end
# Endless method detection
#
# From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab
# Detecting a "oneliner" seems to need a state machine.
# This can be done by looking mostly at the "state" (last value):
#
# ENDFN -> BEG (token = '=' ) -> END
#
private def set_kw_end
oneliner_count = 0
in_oneliner_def = nil
kw_count = 0
end_count = 0
@ignore_newline_not_beg = false
@lex.each do |lex|
kw_count += 1 if lex.is_kw?
end_count += 1 if lex.is_end?
if lex.type == :on_ignored_nl
@ignore_newline_not_beg = !lex.expr_beg?
end
if in_oneliner_def.nil?
in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
# Continue
elsif lex.state.allbits?(Ripper::EXPR_BEG)
in_oneliner_def = :BODY if lex.token == "="
elsif lex.state.allbits?(Ripper::EXPR_END)
# We found an endless method, count it
oneliner_count += 1 if in_oneliner_def == :BODY
in_oneliner_def = nil
else
in_oneliner_def = nil
end
end
kw_count -= oneliner_count
@is_kw = (kw_count - end_count) > 0
@is_end = (end_count - kw_count) > 0
end
end
end
|