irb 1.16.0 → 1.18.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.
data/lib/irb/ruby-lex.rb CHANGED
@@ -4,47 +4,13 @@
4
4
  # by Keiju ISHITSUKA(keiju@ruby-lang.org)
5
5
  #
6
6
 
7
- require "ripper"
7
+ require "prism"
8
8
  require "jruby" if RUBY_ENGINE == "jruby"
9
9
  require_relative "nesting_parser"
10
10
 
11
11
  module IRB
12
12
  # :stopdoc:
13
13
  class RubyLex
14
- ASSIGNMENT_NODE_TYPES = [
15
- # Local, instance, global, class, constant, instance, and index assignment:
16
- # "foo = bar",
17
- # "@foo = bar",
18
- # "$foo = bar",
19
- # "@@foo = bar",
20
- # "::Foo = bar",
21
- # "a::Foo = bar",
22
- # "Foo = bar"
23
- # "foo.bar = 1"
24
- # "foo[1] = bar"
25
- :assign,
26
-
27
- # Operation assignment:
28
- # "foo += bar"
29
- # "foo -= bar"
30
- # "foo ||= bar"
31
- # "foo &&= bar"
32
- :opassign,
33
-
34
- # Multiple assignment:
35
- # "foo, bar = 1, 2
36
- :massign,
37
- ]
38
-
39
- ERROR_TOKENS = [
40
- :on_parse_error,
41
- :compile_error,
42
- :on_assign_error,
43
- :on_alias_error,
44
- :on_class_name_error,
45
- :on_param_error
46
- ]
47
-
48
14
  LTYPE_TOKENS = %i[
49
15
  on_heredoc_beg on_tstring_beg
50
16
  on_regexp_beg on_symbeg on_backtick
@@ -79,239 +45,173 @@ module IRB
79
45
  end
80
46
  end
81
47
 
82
- class << self
83
- def compile_with_errors_suppressed(code, line_no: 1)
84
- begin
85
- result = yield code, line_no
86
- rescue ArgumentError
87
- # Ruby can issue an error for the code if there is an
88
- # incomplete magic comment for encoding in it. Force an
89
- # expression with a new line before the code in this
90
- # case to prevent magic comment handling. To make sure
91
- # line numbers in the lexed code remain the same,
92
- # decrease the line number by one.
93
- code = ";\n#{code}"
94
- line_no -= 1
95
- result = yield code, line_no
96
- end
97
- result
98
- end
99
-
100
- def generate_local_variables_assign_code(local_variables)
101
- # Some reserved words could be a local variable
102
- # Example: def f(if: 1); binding.irb; end
103
- # These reserved words should be removed from assignment code
104
- local_variables -= RESERVED_WORDS
105
- "#{local_variables.join('=')}=nil;" unless local_variables.empty?
106
- end
107
-
108
- # Some part of the code is not included in Ripper's token.
109
- # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
110
- # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
111
- def interpolate_ripper_ignored_tokens(code, tokens)
112
- line_positions = [0]
113
- code.lines.each do |line|
114
- line_positions << line_positions.last + line.bytesize
115
- end
116
- prev_byte_pos = 0
117
- interpolated = []
118
- prev_line = 1
119
- tokens.each do |t|
120
- line, col = t.pos
121
- byte_pos = line_positions[line - 1] + col
122
- if prev_byte_pos < byte_pos
123
- tok = code.byteslice(prev_byte_pos...byte_pos)
124
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
125
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
126
- prev_line += tok.count("\n")
127
- end
128
- interpolated << t
129
- prev_byte_pos = byte_pos + t.tok.bytesize
130
- prev_line += t.tok.count("\n")
131
- end
132
- if prev_byte_pos < code.bytesize
133
- tok = code.byteslice(prev_byte_pos..)
134
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
135
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
136
- end
137
- interpolated
138
- end
139
-
140
- def ripper_lex_without_warning(code, local_variables: [])
141
- verbose, $VERBOSE = $VERBOSE, nil
142
- lvars_code = generate_local_variables_assign_code(local_variables)
143
- original_code = code
144
- if lvars_code
145
- code = "#{lvars_code}\n#{code}"
146
- line_no = 0
147
- else
148
- line_no = 1
149
- end
150
-
151
- compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
152
- lexer = Ripper::Lexer.new(inner_code, '-', line_no)
153
- tokens = []
154
- lexer.scan.each do |t|
155
- next if t.pos.first == 0
156
- prev_tk = tokens.last
157
- position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
158
- if position_overlapped
159
- tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
160
- else
161
- tokens << t
162
- end
163
- end
164
- interpolate_ripper_ignored_tokens(original_code, tokens)
165
- end
166
- ensure
167
- $VERBOSE = verbose
168
- end
169
- end
170
-
171
48
  def check_code_state(code, local_variables:)
172
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
173
- opens = NestingParser.open_tokens(tokens)
174
- [tokens, opens, code_terminated?(code, tokens, opens, local_variables: local_variables)]
49
+ parse_lex_result = Prism.parse_lex(code, scopes: [local_variables])
50
+
51
+ opens = NestingParser.open_nestings(parse_lex_result)
52
+ lines = code.lines
53
+ tokens = parse_lex_result.value[1].map(&:first).sort_by {|t| t.location.start_offset }
54
+ continue = should_continue?(tokens, lines.last, lines.size)
55
+ [continue, opens, code_terminated?(code, continue, opens, local_variables: local_variables)]
175
56
  end
176
57
 
177
- def code_terminated?(code, tokens, opens, local_variables:)
58
+ def code_terminated?(code, continue, opens, local_variables:)
178
59
  case check_code_syntax(code, local_variables: local_variables)
179
60
  when :unrecoverable_error
180
61
  true
181
62
  when :recoverable_error
182
63
  false
183
64
  when :other_error
184
- opens.empty? && !should_continue?(tokens)
65
+ opens.empty? && !continue
185
66
  when :valid
186
- !should_continue?(tokens)
67
+ !continue
187
68
  end
188
69
  end
189
70
 
190
71
  def assignment_expression?(code, local_variables:)
191
- # Try to parse the code and check if the last of possibly multiple
192
- # expressions is an assignment type.
193
-
194
- # If the expression is invalid, Ripper.sexp should return nil which will
195
- # result in false being returned. Any valid expression should return an
196
- # s-expression where the second element of the top level array is an
197
- # array of parsed expressions. The first element of each expression is the
198
- # expression's type.
199
- verbose, $VERBOSE = $VERBOSE, nil
200
- code = "#{RubyLex.generate_local_variables_assign_code(local_variables) || 'nil;'}\n#{code}"
201
- # Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
202
- node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
203
- ASSIGNMENT_NODE_TYPES.include?(node_type)
204
- ensure
205
- $VERBOSE = verbose
72
+ # Parse the code and check if the last of possibly multiple
73
+ # expressions is an assignment node.
74
+ program_node = Prism.parse(code, scopes: [local_variables]).value
75
+ node = program_node.statements.body.last
76
+ case node
77
+ when nil
78
+ # Empty code, comment-only code or invalid code
79
+ false
80
+ when Prism::CallNode
81
+ # a.b = 1, a[b] = 1
82
+ # Prism::CallNode#equal_loc is only available in prism >= 1.7.0
83
+ if node.name == :[]=
84
+ # Distinguish between `a[k] = v` from `a.[]= k, v`, `a.[]=(k, v)`
85
+ node.opening == '['
86
+ else
87
+ node.name.end_with?('=')
88
+ end
89
+ when Prism::MatchWriteNode
90
+ # /(?<lvar>)/ =~ a, Class name is *WriteNode but not an assignment.
91
+ false
92
+ else
93
+ # a = 1, @a = 1, $a = 1, @@a = 1, A = 1, a += 1, a &&= 1, a.b += 1, and so on
94
+ node.class.name.match?(/WriteNode/)
95
+ end
206
96
  end
207
97
 
208
- def should_continue?(tokens)
209
- # Look at the last token and check if IRB need to continue reading next line.
210
- # Example code that should continue: `a\` `a +` `a.`
211
- # Trailing spaces, newline, comments are skipped
212
- return true if tokens.last&.event == :on_sp && tokens.last.tok == "\\\n"
213
-
214
- tokens.reverse_each do |token|
215
- case token.event
216
- when :on_sp, :on_nl, :on_ignored_nl, :on_comment, :on_embdoc_beg, :on_embdoc, :on_embdoc_end
217
- # Skip
218
- when :on_regexp_end, :on_heredoc_end, :on_semicolon
219
- # State is EXPR_BEG but should not continue
220
- return false
98
+ def should_continue?(tokens, line, line_num)
99
+ # Check if the line ends with \\. Then IRB should continue reading next line.
100
+ # Space and backslash are not included in Prism token, so find trailing text after last non-newline token position.
101
+ trailing = line
102
+ tokens.reverse_each do |t|
103
+ break if t.location.start_line < line_num
104
+ if t.location.start_line == line_num &&
105
+ t.location.end_line == line_num &&
106
+ t.type != :IGNORED_NEWLINE &&
107
+ t.type != :NEWLINE &&
108
+ t.type != :EOF
109
+ trailing = line.byteslice(t.location.end_column..)
110
+ trailing ||= '' # in case end_line is wrong (e.g. `"\C-`)
111
+ break
112
+ end
113
+ end
114
+ return true if trailing.match?(/\A\s*\\\n?\z/)
115
+
116
+ # "1 + \n" and "foo.\n" should continue.
117
+ pos = tokens.size - 1
118
+ ignored_newline_found = false
119
+ while pos >= 0
120
+ case tokens[pos].type
121
+ when :EMBDOC_BEGIN, :EMBDOC_LINE, :EMBDOC_END, :COMMENT, :EOF
122
+ pos -= 1
123
+ when :IGNORED_NEWLINE
124
+ pos -= 1
125
+ ignored_newline_found = true
221
126
  else
222
- # Endless range should not continue
223
- return false if token.event == :on_op && token.tok.match?(/\A\.\.\.?\z/)
224
-
225
- # EXPR_DOT and most of the EXPR_BEG should continue
226
- return token.state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_DOT)
127
+ break
227
128
  end
228
129
  end
229
- false
130
+
131
+ # If IGNORED_NEWLINE token is following non-newline non-semicolon token, it should continue.
132
+ # Special case: treat `1..` and `1...` as not continuing.
133
+ ignored_newline_found && pos >= 0 && !%i[DOT_DOT DOT_DOT_DOT NEWLINE SEMICOLON].include?(tokens[pos].type)
230
134
  end
231
135
 
232
136
  def check_code_syntax(code, local_variables:)
233
- lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
234
- code = "#{lvars_code}\n#{code}"
235
-
236
- begin # check if parser error are available
237
- verbose, $VERBOSE = $VERBOSE, nil
238
- case RUBY_ENGINE
239
- when 'ruby'
240
- self.class.compile_with_errors_suppressed(code) do |inner_code, line_no|
241
- RubyVM::InstructionSequence.compile(inner_code, nil, nil, line_no)
242
- end
243
- when 'jruby'
244
- JRuby.compile_ir(code)
137
+ result = Prism.lex(code, scopes: [local_variables])
138
+ if result.success?
139
+ :valid
140
+ elsif result.respond_to?(:continuable?)
141
+ result.continuable? ? :recoverable_error : :unrecoverable_error
142
+ else # For Prism <= 1.9.0. Drop this branch when IRB requires Prism >= 1.10.0.
143
+ check_syntax_error_heuristics(result)
144
+ end
145
+ end
146
+
147
+ # Prism <= 1.9.0 does not have `ParseResult#continuable?` method.
148
+ # Fallback to legacy heuristics based on error messages and error locations.
149
+ def check_syntax_error_heuristics(prism_parse_result)
150
+
151
+ # Get the token excluding trailing comments and newlines
152
+ # to compare error location with the last or second-last meaningful token location
153
+ tokens = prism_parse_result.value.map(&:first)
154
+ until tokens.empty?
155
+ case tokens.last.type
156
+ when :COMMENT, :NEWLINE, :IGNORED_NEWLINE, :EMBDOC_BEGIN, :EMBDOC_LINE, :EMBDOC_END, :EOF
157
+ tokens.pop
245
158
  else
246
- catch(:valid) do
247
- eval("BEGIN { throw :valid, true }\n#{code}")
248
- false
249
- end
159
+ break
250
160
  end
251
- rescue EncodingError
252
- # This is for a hash with invalid encoding symbol, {"\xAE": 1}
253
- :unrecoverable_error
254
- rescue SyntaxError => e
255
- case e.message
256
- when /unexpected keyword_end/
257
- # "syntax error, unexpected keyword_end"
258
- #
259
- # example:
260
- # if (
261
- # end
262
- #
263
- # example:
264
- # end
265
- return :unrecoverable_error
266
- when /unexpected '\.'/
267
- # "syntax error, unexpected '.'"
268
- #
269
- # example:
270
- # .
271
- return :unrecoverable_error
272
- when /unexpected tREGEXP_BEG/
273
- # "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
274
- #
275
- # example:
276
- # method / f /
161
+ end
162
+
163
+ unknown = false
164
+ prism_parse_result.errors.each do |error|
165
+ case error.message
166
+ when /unexpected character literal|incomplete expression at|unexpected .%.|too short escape sequence/i
167
+ # Ignore these errors. Likely to appear only at the end of code.
168
+ # `[a, b ?` unexpected character literal, incomplete expression at
169
+ # `p a, %` unexpected '%'
170
+ # `/\u` too short escape sequence
171
+ when /unexpected write target/i
172
+ # `a,b` recoverable by `=v`
173
+ # `a,b,` recoverable by `c=v`
174
+ tok = tokens.last
175
+ tok = tokens[-2] if tok&.type == :COMMA
176
+ return :unrecoverable_error if tok && error.location.end_offset < tok.location.end_offset
177
+ when /(invalid|unexpected) (?:break|next|redo)/i
178
+ # Hard to check correctly, so treat it as always recoverable.
179
+ # `(break;1)` recoverable by `.f while true`
180
+ when / meets end of file|unexpected end-of-input|unterminated |cannot parse|could not parse/i
181
+ # These are recoverable errors if there is no other unrecoverable error
182
+ # `/aaa` unterminated regexp meets end of file
183
+ # `def f` unexpected end-of-input
184
+ # `"#{` unterminated string
185
+ # `:"aa` cannot parse the string part
186
+ # `def f =` could not parse the endless method body
187
+ when /is not allowed|unexpected .+ ignoring it/i
188
+ # `@@` `$--` is not allowed
189
+ # `)`, `end` unexpected ')', ignoring it
277
190
  return :unrecoverable_error
278
- when /unterminated (?:string|regexp) meets end of file/
279
- # "unterminated regexp meets end of file"
280
- #
281
- # example:
282
- # /
283
- #
284
- # "unterminated string meets end of file"
285
- #
286
- # example:
287
- # '
288
- return :recoverable_error
289
- when /unexpected end-of-input/
290
- # "syntax error, unexpected end-of-input, expecting keyword_end"
291
- #
292
- # example:
293
- # if true
294
- # hoge
295
- # if false
296
- # fuga
297
- # end
298
- return :recoverable_error
191
+ when /unexpected |invalid |dynamic constant assignment|can't set variable|can't change the value|is not valid to get|variable capture in alternative pattern/i
192
+ # Likely to be unrecoverable except when the error is at the last token location.
193
+ # Unexpected: `class a`, `tap(&`, `def f(a,`
194
+ # Invalid: `a ? b :`, `/\u{`, `"\M-`
195
+ # `a,B` recoverable by `.c=v` dynamic constant assignment
196
+ # `a,$1` recoverable by `.f=v` Can't set variable
197
+ # `a,self` recoverable by `.f=v` Can't change the value of self
198
+ # `p foo?:` recoverable by `v` is not valid to get
199
+ # `x in 1|{x:` recoverable by `1}` variable capture in alternative pattern
200
+ return :unrecoverable_error if tokens.last && error.location.end_offset <= tokens.last.location.start_offset
299
201
  else
300
- return :other_error
202
+ unknown = true
301
203
  end
302
- ensure
303
- $VERBOSE = verbose
304
204
  end
305
- :valid
205
+ unknown ? :other_error : :recoverable_error
306
206
  end
307
207
 
308
208
  def calc_indent_level(opens)
309
209
  indent_level = 0
310
- opens.each_with_index do |t, index|
311
- case t.event
210
+ opens.each_with_index do |elem, index|
211
+ case elem.event
312
212
  when :on_heredoc_beg
313
213
  if opens[index + 1]&.event != :on_heredoc_beg
314
- if t.tok.match?(/^<<[~-]/)
214
+ if elem.tok.match?(/^<<[~-]/)
315
215
  indent_level += 1
316
216
  else
317
217
  indent_level = 0
@@ -320,50 +220,50 @@ module IRB
320
220
  when :on_tstring_beg, :on_regexp_beg, :on_symbeg, :on_backtick
321
221
  # No indent: "", //, :"", ``
322
222
  # Indent: %(), %r(), %i(), %x()
323
- indent_level += 1 if t.tok.start_with? '%'
223
+ indent_level += 1 if elem.tok.start_with? '%'
324
224
  when :on_embdoc_beg
325
225
  indent_level = 0
326
226
  else
327
- indent_level += 1 unless t.tok == 'alias' || t.tok == 'undef'
227
+ indent_level += 1 unless elem.tok == 'alias' || elem.tok == 'undef'
328
228
  end
329
229
  end
330
230
  indent_level
331
231
  end
332
232
 
333
- FREE_INDENT_TOKENS = %i[on_tstring_beg on_backtick on_regexp_beg on_symbeg]
233
+ FREE_INDENT_NESTINGS = %i[on_tstring_beg on_backtick on_regexp_beg on_symbeg]
334
234
 
335
- def free_indent_token?(token)
336
- FREE_INDENT_TOKENS.include?(token&.event)
235
+ def free_indent_nesting_element?(elem)
236
+ FREE_INDENT_NESTINGS.include?(elem&.event)
337
237
  end
338
238
 
339
239
  # Calculates the difference of pasted code's indent and indent calculated from tokens
340
240
  def indent_difference(lines, line_results, line_index)
341
241
  loop do
342
- _tokens, prev_opens, _next_opens, min_depth = line_results[line_index]
343
- open_token = prev_opens.last
344
- if !open_token || (open_token.event != :on_heredoc_beg && !free_indent_token?(open_token))
242
+ prev_opens, _next_opens, min_depth = line_results[line_index]
243
+ open_elem = prev_opens.last
244
+ if !open_elem || (open_elem.event != :on_heredoc_beg && !free_indent_nesting_element?(open_elem))
345
245
  # If the leading whitespace is an indent, return the difference
346
246
  indent_level = calc_indent_level(prev_opens.take(min_depth))
347
247
  calculated_indent = 2 * indent_level
348
248
  actual_indent = lines[line_index][/^ */].size
349
249
  return actual_indent - calculated_indent
350
- elsif open_token.event == :on_heredoc_beg && open_token.tok.match?(/^<<[^-~]/)
250
+ elsif open_elem.event == :on_heredoc_beg && open_elem.tok.match?(/^<<[^-~]/)
351
251
  return 0
352
252
  end
353
253
  # If the leading whitespace is not an indent but part of a multiline token
354
254
  # Calculate base_indent of the multiline token's beginning line
355
- line_index = open_token.pos[0] - 1
255
+ line_index = open_elem.pos[0] - 1
356
256
  end
357
257
  end
358
258
 
359
- def process_indent_level(tokens, lines, line_index, is_newline)
360
- line_results = NestingParser.parse_by_line(tokens)
259
+ def process_indent_level(parse_lex_result, lines, line_index, is_newline)
260
+ line_results = NestingParser.parse_by_line(parse_lex_result)
361
261
  result = line_results[line_index]
362
262
  if result
363
- _tokens, prev_opens, next_opens, min_depth = result
263
+ prev_opens, next_opens, min_depth = result
364
264
  else
365
265
  # When last line is empty
366
- prev_opens = next_opens = line_results.last[2]
266
+ prev_opens = next_opens = line_results.last[1]
367
267
  min_depth = next_opens.size
368
268
  end
369
269
 
@@ -373,39 +273,39 @@ module IRB
373
273
 
374
274
  preserve_indent = lines[line_index - (is_newline ? 1 : 0)][/^ */].size
375
275
 
376
- prev_open_token = prev_opens.last
377
- next_open_token = next_opens.last
276
+ prev_open_elem = prev_opens.last
277
+ next_open_elem = next_opens.last
378
278
 
379
- # Calculates base indent for pasted code on the line where prev_open_token is located
380
- # irb(main):001:1* if a # base_indent is 2, indent calculated from tokens is 0
381
- # irb(main):002:1* if b # base_indent is 6, indent calculated from tokens is 2
382
- # irb(main):003:0> c # base_indent is 6, indent calculated from tokens is 4
383
- if prev_open_token
384
- base_indent = [0, indent_difference(lines, line_results, prev_open_token.pos[0] - 1)].max
279
+ # Calculates base indent for pasted code on the line where prev_open_elem is located
280
+ # irb(main):001:1* if a # base_indent is 2, indent calculated from nestings is 0
281
+ # irb(main):002:1* if b # base_indent is 6, indent calculated from nestings is 2
282
+ # irb(main):003:0> c # base_indent is 6, indent calculated from nestings is 4
283
+ if prev_open_elem
284
+ base_indent = [0, indent_difference(lines, line_results, prev_open_elem.pos[0] - 1)].max
385
285
  else
386
286
  base_indent = 0
387
287
  end
388
288
 
389
- if free_indent_token?(prev_open_token)
390
- if is_newline && prev_open_token.pos[0] == line_index
289
+ if free_indent_nesting_element?(prev_open_elem)
290
+ if is_newline && prev_open_elem.pos[0] == line_index
391
291
  # First newline inside free-indent token
392
292
  base_indent + indent
393
293
  else
394
294
  # Accept any number of indent inside free-indent token
395
295
  preserve_indent
396
296
  end
397
- elsif prev_open_token&.event == :on_embdoc_beg || next_open_token&.event == :on_embdoc_beg
398
- if prev_open_token&.event == next_open_token&.event
297
+ elsif prev_open_elem&.event == :on_embdoc_beg || next_open_elem&.event == :on_embdoc_beg
298
+ if prev_open_elem&.event == next_open_elem&.event
399
299
  # Accept any number of indent inside embdoc content
400
300
  preserve_indent
401
301
  else
402
302
  # =begin or =end
403
303
  0
404
304
  end
405
- elsif prev_open_token&.event == :on_heredoc_beg
406
- tok = prev_open_token.tok
305
+ elsif prev_open_elem&.event == :on_heredoc_beg
306
+ tok = prev_open_elem.tok
407
307
  if prev_opens.size <= next_opens.size
408
- if is_newline && lines[line_index].empty? && line_results[line_index - 1][1].last != next_open_token
308
+ if is_newline && lines[line_index].empty? && line_results[line_index - 1][0].last != next_open_elem
409
309
  # First line in heredoc
410
310
  tok.match?(/^<<[-~]/) ? base_indent + indent : indent
411
311
  elsif tok.match?(/^<<~/)
@@ -425,15 +325,15 @@ module IRB
425
325
  end
426
326
  end
427
327
 
428
- def ltype_from_open_tokens(opens)
429
- start_token = opens.reverse_each.find do |tok|
430
- LTYPE_TOKENS.include?(tok.event)
328
+ def ltype_from_open_nestings(opens)
329
+ start_nesting = opens.reverse_each.find do |elem|
330
+ LTYPE_TOKENS.include?(elem.event)
431
331
  end
432
- return nil unless start_token
332
+ return nil unless start_nesting
433
333
 
434
- case start_token&.event
334
+ case start_nesting&.event
435
335
  when :on_tstring_beg
436
- case start_token&.tok
336
+ case start_nesting&.tok
437
337
  when ?" then ?"
438
338
  when /^%.$/ then ?"
439
339
  when /^%Q.$/ then ?"
@@ -448,50 +348,51 @@ module IRB
448
348
  when :on_qsymbols_beg then ?]
449
349
  when :on_symbols_beg then ?]
450
350
  when :on_heredoc_beg
451
- start_token&.tok =~ /<<[-~]?(['"`])\w+\1/
351
+ start_nesting&.tok =~ /<<[-~]?(['"`])\w+\1/
452
352
  $1 || ?"
453
353
  else
454
354
  nil
455
355
  end
456
356
  end
457
357
 
358
+ # Check if <tt>code.lines[...-1]</tt> is terminated and can be evaluated immediately.
359
+ # Returns the last line string if terminated, otherwise false.
360
+ # Terminated means previous lines(<tt>code.lines[...-1]</tt>) is syntax valid and
361
+ # previous lines and the last line are syntactically separated.
362
+ # Terminated example
363
+ # foo(
364
+ # bar)
365
+ # baz.
366
+ # Unterminated example: previous lines are syntax invalid
367
+ # foo(
368
+ # bar).
369
+ # baz
370
+ # Unterminated example: previous lines are connected to the last line
371
+ # foo(
372
+ # bar)
373
+ # .baz
458
374
  def check_termination_in_prev_line(code, local_variables:)
459
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
460
- past_first_newline = false
461
- index = tokens.rindex do |t|
462
- # traverse first token before last line
463
- if past_first_newline
464
- if t.tok.include?("\n")
465
- true
466
- end
467
- elsif t.tok.include?("\n")
468
- past_first_newline = true
469
- false
470
- else
471
- false
472
- end
473
- end
375
+ lines = code.lines
376
+ return false if lines.size < 2
474
377
 
475
- if index
476
- first_token = nil
477
- last_line_tokens = tokens[(index + 1)..(tokens.size - 1)]
478
- last_line_tokens.each do |t|
479
- unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event)
480
- first_token = t
481
- break
482
- end
483
- end
378
+ prev_line_result = Prism.parse(lines[...-1].join, scopes: [local_variables])
379
+ return false unless prev_line_result.success?
484
380
 
485
- if first_token && first_token.state != Ripper::EXPR_DOT
486
- tokens_without_last_line = tokens[0..index]
487
- code_without_last_line = tokens_without_last_line.map(&:tok).join
488
- opens_without_last_line = NestingParser.open_tokens(tokens_without_last_line)
489
- if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line, local_variables: local_variables)
490
- return last_line_tokens.map(&:tok).join
491
- end
492
- end
381
+ prev_nodes = prev_line_result.value.statements.body
382
+ whole_nodes = Prism.parse(code, scopes: [local_variables]).value.statements.body
383
+
384
+ return false if whole_nodes.size < prev_nodes.size
385
+ return false unless prev_nodes.zip(whole_nodes).all? do |a, b|
386
+ a.location == b.location
493
387
  end
494
- false
388
+
389
+ # If the last line only contain comments, treat it as not connected to handle this case:
390
+ # receiver
391
+ # # comment
392
+ # .method
393
+ return false if lines.last.match?(/\A\s*#/)
394
+
395
+ lines.last
495
396
  end
496
397
  end
497
398
  # :startdoc:
data/lib/irb/ruby_logo.aa CHANGED
@@ -116,3 +116,7 @@ TYPE: UNICODE
116
116
  ⢻⣾⡇ ⠘⣷ ⣼⠃ ⠘⣷⣠⣴⠟⠋ ⠙⢷⣄⢸⣿
117
117
  ⠻⣧⡀ ⠘⣧⣰⡏ ⢀⣠⣤⠶⠛⠉⠛⠛⠛⠛⠛⠛⠻⢶⣶⣶⣶⣶⣶⣤⣤⣽⣿⣿
118
118
  ⠈⠛⠷⢦⣤⣽⣿⣥⣤⣶⣶⡿⠿⠿⠶⠶⠶⠶⠾⠛⠛⠛⠛⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠁
119
+ TYPE: UNICODE_SMALL
120
+ ⢀⡴⠊⢉⡟⢿
121
+ ⣎⣀⣴⡋⡟⣻
122
+ ⣟⣼⣱⣽⣟⣾