Skip to content

Commit 6701ffe

Browse files
committed
Prism::ParseResult#continuable?
An API to determine if more input could fix the existing syntax errors.
1 parent c5882ce commit 6701ffe

File tree

4 files changed

+166
-0
lines changed

4 files changed

+166
-0
lines changed

lib/prism/parse_result.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,78 @@ def failure?
938938
!success?
939939
end
940940

941+
# Returns true if the parsed source is an incomplete expression that could
942+
# become valid with additional input. This is useful for REPL contexts (such
943+
# as IRB) where the user may be entering a multi-line expression one line at
944+
# a time and the implementation needs to determine whether to wait for more
945+
# input or to evaluate what has been entered so far.
946+
#
947+
# Concretely, this returns true when every error present is caused by the
948+
# parser reaching the end of the input before a construct was closed (e.g.
949+
# an unclosed string, array, block, or keyword), and returns false when any
950+
# error is caused by a token that makes the input structurally invalid
951+
# regardless of what might follow (e.g. a stray `end`, `]`, or `)` with no
952+
# matching opener).
953+
#
954+
# Examples:
955+
#
956+
# Prism.parse("1 + [").continuable? #=> true (unclosed array)
957+
# Prism.parse("1 + ]").continuable? #=> false (stray ])
958+
# Prism.parse("tap do").continuable? #=> true (unclosed block)
959+
# Prism.parse("end.tap do").continuable? #=> false (stray end)
960+
#
961+
#--
962+
#: () -> bool
963+
def continuable?
964+
return false if errors.empty?
965+
966+
offset = source.source.bytesize
967+
errors.all? { |error| CONTINUABLE.include?(error.type) || error.location.start_offset == offset }
968+
end
969+
970+
# The set of error types whose location the parser places at the opening
971+
# token of an unclosed construct rather than at the end of the source. These
972+
# errors always indicate incomplete input regardless of their byte position,
973+
# so they are checked by type rather than by location.
974+
#--
975+
#: Array[Symbol]
976+
CONTINUABLE = %i[
977+
begin_term
978+
begin_upcase_term
979+
block_param_pipe_term
980+
block_term_brace
981+
block_term_end
982+
case_missing_conditions
983+
case_term
984+
class_term
985+
conditional_term
986+
conditional_term_else
987+
def_term
988+
embdoc_term
989+
end_upcase_term
990+
for_term
991+
hash_term
992+
heredoc_term
993+
lambda_term_brace
994+
lambda_term_end
995+
list_i_lower_term
996+
list_i_upper_term
997+
list_w_lower_term
998+
list_w_upper_term
999+
module_term
1000+
regexp_term
1001+
rescue_term
1002+
string_interpolated_term
1003+
string_literal_eof
1004+
symbol_term_dynamic
1005+
symbol_term_interpolated
1006+
until_term
1007+
while_term
1008+
xstring_term
1009+
].freeze
1010+
1011+
private_constant :CONTINUABLE
1012+
9411013
# Create a code units cache for the given encoding.
9421014
#--
9431015
#: (Encoding encoding) -> _CodeUnitsCache

rbi/generated/prism/parse_result.rbi

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sig/generated/prism/parse_result.rbs

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/prism/errors_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,41 @@ def test_unclosed_heredoc_and_interpolation
109109
assert_nil(statement.parts[0].statements)
110110
end
111111

112+
def test_continuable
113+
# Valid input is not continuable (nothing to continue).
114+
refute_predicate Prism.parse("1 + 1"), :continuable?
115+
refute_predicate Prism.parse(""), :continuable?
116+
117+
# Stray closing tokens make input non-continuable regardless of what
118+
# follows (matches the feature-request examples exactly).
119+
refute_predicate Prism.parse("1 + ]"), :continuable?
120+
refute_predicate Prism.parse("end.tap do"), :continuable?
121+
122+
# Unclosed constructs are continuable.
123+
assert_predicate Prism.parse("1 + ["), :continuable?
124+
assert_predicate Prism.parse("tap do"), :continuable?
125+
126+
# Unclosed keywords.
127+
assert_predicate Prism.parse("def foo"), :continuable?
128+
assert_predicate Prism.parse("class Foo"), :continuable?
129+
assert_predicate Prism.parse("module Foo"), :continuable?
130+
assert_predicate Prism.parse("if true"), :continuable?
131+
assert_predicate Prism.parse("while true"), :continuable?
132+
assert_predicate Prism.parse("begin"), :continuable?
133+
assert_predicate Prism.parse("for x in [1]"), :continuable?
134+
135+
# Unclosed delimiters.
136+
assert_predicate Prism.parse("{"), :continuable?
137+
assert_predicate Prism.parse("foo("), :continuable?
138+
assert_predicate Prism.parse('"hello'), :continuable?
139+
assert_predicate Prism.parse("'hello"), :continuable?
140+
assert_predicate Prism.parse("<<~HEREDOC\nhello"), :continuable?
141+
142+
# A mix: stray end plus an unclosed block is not continuable because the
143+
# stray end cannot be fixed by appending more input.
144+
refute_predicate Prism.parse("end\ntap do"), :continuable?
145+
end
146+
112147
private
113148

114149
def assert_errors(filepath, version)

0 commit comments

Comments
 (0)