Skip to content

Commit b7f4bfa

Browse files
authored
Fix process_continue(rename to should_continue?) and check_code_block(rename to check_code_syntax) (#611)
1 parent f01ff08 commit b7f4bfa

File tree

3 files changed

+94
-57
lines changed

3 files changed

+94
-57
lines changed

lib/irb/cmd/show_source.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def find_end(file, first_line, irb_context)
5858
tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
5959
code = lines[0..lnum].join
6060
prev_tokens.concat chunk
61-
continue = lex.process_continue(prev_tokens)
62-
code_block_open = lex.check_code_block(code, prev_tokens)
63-
if !continue && !code_block_open
61+
continue = lex.should_continue?(prev_tokens)
62+
syntax = lex.check_code_syntax(code)
63+
if !continue && syntax == :valid
6464
return first_line + lnum
6565
end
6666
end

lib/irb/ruby-lex.rb

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def configure_io(io)
8585
# Avoid appending duplicated token. Tokens that include "\n" like multiline tstring_content can exist in multiple lines.
8686
tokens_until_line << token if token != tokens_until_line.last
8787
end
88-
continue = process_continue(tokens_until_line)
88+
continue = should_continue?(tokens_until_line)
8989
prompt(next_opens, continue, line_num_offset)
9090
end
9191
end
@@ -196,7 +196,16 @@ def check_code_state(code)
196196
end
197197

198198
def code_terminated?(code, tokens, opens)
199-
opens.empty? && !process_continue(tokens) && !check_code_block(code, tokens)
199+
case check_code_syntax(code)
200+
when :unrecoverable_error
201+
true
202+
when :recoverable_error
203+
false
204+
when :other_error
205+
opens.empty? && !should_continue?(tokens)
206+
when :valid
207+
!should_continue?(tokens)
208+
end
200209
end
201210

202211
def save_prompt_to_context_io(opens, continue, line_num_offset)
@@ -227,7 +236,7 @@ def readmultiline
227236
return code if terminated
228237

229238
line_offset += 1
230-
continue = process_continue(tokens)
239+
continue = should_continue?(tokens)
231240
save_prompt_to_context_io(opens, continue, line_offset)
232241
end
233242
end
@@ -246,29 +255,33 @@ def each_top_level_statement
246255
end
247256
end
248257

249-
def process_continue(tokens)
250-
# last token is always newline
251-
if tokens.size >= 2 and tokens[-2].event == :on_regexp_end
252-
# end of regexp literal
253-
return false
254-
elsif tokens.size >= 2 and tokens[-2].event == :on_semicolon
255-
return false
256-
elsif tokens.size >= 2 and tokens[-2].event == :on_kw and ['begin', 'else', 'ensure'].include?(tokens[-2].tok)
257-
return false
258-
elsif !tokens.empty? and tokens.last.tok == "\\\n"
259-
return true
260-
elsif tokens.size >= 1 and tokens[-1].event == :on_heredoc_end # "EOH\n"
261-
return false
262-
elsif tokens.size >= 2 and tokens[-2].state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) and tokens[-2].tok !~ /\A\.\.\.?\z/
263-
# end of literal except for regexp
264-
# endless range at end of line is not a continue
265-
return true
258+
def should_continue?(tokens)
259+
# Look at the last token and check if IRB need to continue reading next line.
260+
# Example code that should continue: `a\` `a +` `a.`
261+
# Trailing spaces, newline, comments are skipped
262+
return true if tokens.last&.event == :on_sp && tokens.last.tok == "\\\n"
263+
264+
tokens.reverse_each do |token|
265+
case token.event
266+
when :on_sp, :on_nl, :on_ignored_nl, :on_comment, :on_embdoc_beg, :on_embdoc, :on_embdoc_end
267+
# Skip
268+
when :on_regexp_end, :on_heredoc_end, :on_semicolon
269+
# State is EXPR_BEG but should not continue
270+
return false
271+
else
272+
# Endless range should not continue
273+
return false if token.event == :on_op && token.tok.match?(/\A\.\.\.?\z/)
274+
275+
# EXPR_DOT and most of the EXPR_BEG should continue
276+
return token.state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_DOT)
277+
end
266278
end
267279
false
268280
end
269281

270-
def check_code_block(code, tokens)
271-
return true if tokens.empty?
282+
def check_code_syntax(code)
283+
lvars_code = RubyLex.generate_local_variables_assign_code(@context.local_variables)
284+
code = "#{lvars_code}\n#{code}"
272285

273286
begin # check if parser error are available
274287
verbose, $VERBOSE = $VERBOSE, nil
@@ -287,6 +300,7 @@ def check_code_block(code, tokens)
287300
end
288301
rescue EncodingError
289302
# This is for a hash with invalid encoding symbol, {"\xAE": 1}
303+
:unrecoverable_error
290304
rescue SyntaxError => e
291305
case e.message
292306
when /unterminated (?:string|regexp) meets end of file/
@@ -299,7 +313,7 @@ def check_code_block(code, tokens)
299313
#
300314
# example:
301315
# '
302-
return true
316+
return :recoverable_error
303317
when /syntax error, unexpected end-of-input/
304318
# "syntax error, unexpected end-of-input, expecting keyword_end"
305319
#
@@ -309,7 +323,7 @@ def check_code_block(code, tokens)
309323
# if false
310324
# fuga
311325
# end
312-
return true
326+
return :recoverable_error
313327
when /syntax error, unexpected keyword_end/
314328
# "syntax error, unexpected keyword_end"
315329
#
@@ -319,41 +333,26 @@ def check_code_block(code, tokens)
319333
#
320334
# example:
321335
# end
322-
return false
336+
return :unrecoverable_error
323337
when /syntax error, unexpected '\.'/
324338
# "syntax error, unexpected '.'"
325339
#
326340
# example:
327341
# .
328-
return false
342+
return :unrecoverable_error
329343
when /unexpected tREGEXP_BEG/
330344
# "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
331345
#
332346
# example:
333347
# method / f /
334-
return false
348+
return :unrecoverable_error
349+
else
350+
return :other_error
335351
end
336352
ensure
337353
$VERBOSE = verbose
338354
end
339-
340-
last_lex_state = tokens.last.state
341-
342-
if last_lex_state.allbits?(Ripper::EXPR_BEG)
343-
return false
344-
elsif last_lex_state.allbits?(Ripper::EXPR_DOT)
345-
return true
346-
elsif last_lex_state.allbits?(Ripper::EXPR_CLASS)
347-
return true
348-
elsif last_lex_state.allbits?(Ripper::EXPR_FNAME)
349-
return true
350-
elsif last_lex_state.allbits?(Ripper::EXPR_VALUE)
351-
return true
352-
elsif last_lex_state.allbits?(Ripper::EXPR_ARG)
353-
return false
354-
end
355-
356-
false
355+
:valid
357356
end
358357

359358
def calc_indent_level(opens)

test/irb/test_ruby_lex.rb

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,33 @@ def assert_row_indenting(lines, row)
8282
end
8383

8484
def assert_indent_level(lines, expected, local_variables: [])
85-
indent_level, _code_block_open = check_state(lines, local_variables: local_variables)
85+
indent_level, _continue, _code_block_open = check_state(lines, local_variables: local_variables)
8686
error_message = "Calculated the wrong number of indent level for:\n #{lines.join("\n")}"
8787
assert_equal(expected, indent_level, error_message)
8888
end
8989

90+
def assert_should_continue(lines, expected, local_variables: [])
91+
_indent_level, continue, _code_block_open = check_state(lines, local_variables: local_variables)
92+
error_message = "Wrong result of should_continue for:\n #{lines.join("\n")}"
93+
assert_equal(expected, continue, error_message)
94+
end
95+
9096
def assert_code_block_open(lines, expected, local_variables: [])
91-
_indent_level, code_block_open = check_state(lines, local_variables: local_variables)
97+
_indent_level, _continue, code_block_open = check_state(lines, local_variables: local_variables)
9298
error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}"
9399
assert_equal(expected, code_block_open, error_message)
94100
end
95101

96102
def check_state(lines, local_variables: [])
97103
context = build_context(local_variables)
98-
tokens = RubyLex.ripper_lex_without_warning(lines.join("\n"), context: context)
104+
code = lines.join("\n")
105+
tokens = RubyLex.ripper_lex_without_warning(code, context: context)
99106
opens = IRB::NestingParser.open_tokens(tokens)
100107
ruby_lex = RubyLex.new(context)
101108
indent_level = ruby_lex.calc_indent_level(opens)
102-
code_block_open = !opens.empty? || ruby_lex.process_continue(tokens)
103-
[indent_level, code_block_open]
109+
continue = ruby_lex.should_continue?(tokens)
110+
terminated = ruby_lex.code_terminated?(code, tokens, opens)
111+
[indent_level, continue, !terminated]
104112
end
105113

106114
def test_interpolate_token_with_heredoc_and_unclosed_embexpr
@@ -235,7 +243,7 @@ def test_symbols
235243
def test_endless_range_at_end_of_line
236244
input_with_prompt = [
237245
PromptRow.new('001:0: :> ', %q(a = 3..)),
238-
PromptRow.new('002:0: :* ', %q()),
246+
PromptRow.new('002:0: :> ', %q()),
239247
]
240248

241249
lines = input_with_prompt.map(&:content)
@@ -256,7 +264,7 @@ def test_heredoc_with_embexpr
256264
PromptRow.new('009:0:]:* ', %q(B)),
257265
PromptRow.new('010:0:]:* ', %q(})),
258266
PromptRow.new('011:0: :> ', %q(])),
259-
PromptRow.new('012:0: :* ', %q()),
267+
PromptRow.new('012:0: :> ', %q()),
260268
]
261269

262270
lines = input_with_prompt.map(&:content)
@@ -285,9 +293,9 @@ def test_heredoc_prompt_with_quotes
285293
def test_backtick_method
286294
input_with_prompt = [
287295
PromptRow.new('001:0: :> ', %q(self.`(arg))),
288-
PromptRow.new('002:0: :* ', %q()),
296+
PromptRow.new('002:0: :> ', %q()),
289297
PromptRow.new('003:0: :> ', %q(def `(); end)),
290-
PromptRow.new('004:0: :* ', %q()),
298+
PromptRow.new('004:0: :> ', %q()),
291299
]
292300

293301
lines = input_with_prompt.map(&:content)
@@ -777,6 +785,36 @@ def test_dynamic_prompt_with_blank_line
777785
assert_dynamic_prompt(lines, expected_prompt_list)
778786
end
779787

788+
def test_should_continue
789+
assert_should_continue(['a'], false)
790+
assert_should_continue(['/a/'], false)
791+
assert_should_continue(['a;'], false)
792+
assert_should_continue(['<<A', 'A'], false)
793+
assert_should_continue(['a...'], false)
794+
assert_should_continue(['a\\', ''], true)
795+
assert_should_continue(['a.'], true)
796+
assert_should_continue(['a+'], true)
797+
assert_should_continue(['a; #comment', '', '=begin', 'embdoc', '=end', ''], false)
798+
assert_should_continue(['a+ #comment', '', '=begin', 'embdoc', '=end', ''], true)
799+
end
800+
801+
def test_code_block_open_with_should_continue
802+
# syntax ok
803+
assert_code_block_open(['a'], false) # continue: false
804+
assert_code_block_open(['a\\', ''], true) # continue: true
805+
806+
# recoverable syntax error code is not terminated
807+
assert_code_block_open(['a+', ''], true)
808+
809+
# unrecoverable syntax error code is terminated
810+
assert_code_block_open(['.; a+', ''], false)
811+
812+
# other syntax error that failed to determine if it is recoverable or not
813+
assert_code_block_open(['@; a'], false)
814+
assert_code_block_open(['@; a+'], true)
815+
assert_code_block_open(['@; (a'], true)
816+
end
817+
780818
def test_broken_percent_literal
781819
tokens = RubyLex.ripper_lex_without_warning('%wwww')
782820
pos_to_index = {}

0 commit comments

Comments
 (0)