irb 1.14.3 → 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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +5 -0
  3. data/CONTRIBUTING.md +52 -0
  4. data/EXTEND_IRB.md +3 -0
  5. data/Gemfile +11 -1
  6. data/README.md +13 -310
  7. data/doc/COMMAND_LINE_OPTIONS.md +69 -0
  8. data/doc/COMPARED_WITH_PRY.md +22 -0
  9. data/doc/Configurations.md +274 -0
  10. data/doc/EXTEND_IRB.md +122 -0
  11. data/doc/Index.md +705 -0
  12. data/doc/irb/irb.rd.ja +1 -1
  13. data/lib/irb/color.rb +251 -162
  14. data/lib/irb/color_printer.rb +10 -9
  15. data/lib/irb/command/base.rb +35 -0
  16. data/lib/irb/command/copy.rb +83 -0
  17. data/lib/irb/command/history.rb +1 -1
  18. data/lib/irb/command/internal_helpers.rb +6 -3
  19. data/lib/irb/command/ls.rb +6 -4
  20. data/lib/irb/completion.rb +69 -52
  21. data/lib/irb/context.rb +100 -64
  22. data/lib/irb/debug.rb +3 -3
  23. data/lib/irb/default_commands.rb +4 -1
  24. data/lib/irb/easter-egg.rb +3 -1
  25. data/lib/irb/ext/multi-irb.rb +2 -0
  26. data/lib/irb/history.rb +4 -0
  27. data/lib/irb/init.rb +6 -1
  28. data/lib/irb/input-method.rb +158 -122
  29. data/lib/irb/inspector.rb +12 -7
  30. data/lib/irb/lc/help-message +2 -2
  31. data/lib/irb/lc/ja/help-message +1 -1
  32. data/lib/irb/nesting_parser.rb +362 -213
  33. data/lib/irb/pager.rb +127 -6
  34. data/lib/irb/ruby-lex.rb +225 -299
  35. data/lib/irb/ruby_logo.aa +4 -0
  36. data/lib/irb/source_finder.rb +12 -18
  37. data/lib/irb/startup_message.rb +83 -0
  38. data/lib/irb/statement.rb +21 -0
  39. data/lib/irb/version.rb +2 -2
  40. data/lib/irb/workspace.rb +9 -0
  41. data/lib/irb.rb +112 -927
  42. data/man/irb.1 +2 -0
  43. metadata +42 -12
  44. data/.document +0 -8
  45. data/Rakefile +0 -52
  46. data/bin/console +0 -6
  47. data/bin/setup +0 -6
  48. data/irb.gemspec +0 -46
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
@@ -52,241 +18,200 @@ module IRB
52
18
  on_words_beg on_qwords_beg
53
19
  ]
54
20
 
21
+ RESERVED_WORDS = %i[
22
+ __ENCODING__ __LINE__ __FILE__
23
+ BEGIN END
24
+ alias and
25
+ begin break
26
+ case class
27
+ def defined? do
28
+ else elsif end ensure
29
+ false for
30
+ if in
31
+ module
32
+ next nil not
33
+ or
34
+ redo rescue retry return
35
+ self super
36
+ then true
37
+ undef unless until
38
+ when while
39
+ yield
40
+ ]
41
+
55
42
  class TerminateLineInput < StandardError
56
43
  def initialize
57
44
  super("Terminate Line Input")
58
45
  end
59
46
  end
60
47
 
61
- class << self
62
- def compile_with_errors_suppressed(code, line_no: 1)
63
- begin
64
- result = yield code, line_no
65
- rescue ArgumentError
66
- # Ruby can issue an error for the code if there is an
67
- # incomplete magic comment for encoding in it. Force an
68
- # expression with a new line before the code in this
69
- # case to prevent magic comment handling. To make sure
70
- # line numbers in the lexed code remain the same,
71
- # decrease the line number by one.
72
- code = ";\n#{code}"
73
- line_no -= 1
74
- result = yield code, line_no
75
- end
76
- result
77
- end
78
-
79
- def generate_local_variables_assign_code(local_variables)
80
- "#{local_variables.join('=')}=nil;" unless local_variables.empty?
81
- end
82
-
83
- # Some part of the code is not included in Ripper's token.
84
- # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
85
- # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
86
- def interpolate_ripper_ignored_tokens(code, tokens)
87
- line_positions = [0]
88
- code.lines.each do |line|
89
- line_positions << line_positions.last + line.bytesize
90
- end
91
- prev_byte_pos = 0
92
- interpolated = []
93
- prev_line = 1
94
- tokens.each do |t|
95
- line, col = t.pos
96
- byte_pos = line_positions[line - 1] + col
97
- if prev_byte_pos < byte_pos
98
- tok = code.byteslice(prev_byte_pos...byte_pos)
99
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
100
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
101
- prev_line += tok.count("\n")
102
- end
103
- interpolated << t
104
- prev_byte_pos = byte_pos + t.tok.bytesize
105
- prev_line += t.tok.count("\n")
106
- end
107
- if prev_byte_pos < code.bytesize
108
- tok = code.byteslice(prev_byte_pos..)
109
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
110
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
111
- end
112
- interpolated
113
- end
114
-
115
- def ripper_lex_without_warning(code, local_variables: [])
116
- verbose, $VERBOSE = $VERBOSE, nil
117
- lvars_code = generate_local_variables_assign_code(local_variables)
118
- original_code = code
119
- if lvars_code
120
- code = "#{lvars_code}\n#{code}"
121
- line_no = 0
122
- else
123
- line_no = 1
124
- end
125
-
126
- compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
127
- lexer = Ripper::Lexer.new(inner_code, '-', line_no)
128
- tokens = []
129
- lexer.scan.each do |t|
130
- next if t.pos.first == 0
131
- prev_tk = tokens.last
132
- position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
133
- if position_overlapped
134
- tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
135
- else
136
- tokens << t
137
- end
138
- end
139
- interpolate_ripper_ignored_tokens(original_code, tokens)
140
- end
141
- ensure
142
- $VERBOSE = verbose
143
- end
144
- end
145
-
146
48
  def check_code_state(code, local_variables:)
147
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
148
- opens = NestingParser.open_tokens(tokens)
149
- [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)]
150
56
  end
151
57
 
152
- def code_terminated?(code, tokens, opens, local_variables:)
58
+ def code_terminated?(code, continue, opens, local_variables:)
153
59
  case check_code_syntax(code, local_variables: local_variables)
154
60
  when :unrecoverable_error
155
61
  true
156
62
  when :recoverable_error
157
63
  false
158
64
  when :other_error
159
- opens.empty? && !should_continue?(tokens)
65
+ opens.empty? && !continue
160
66
  when :valid
161
- !should_continue?(tokens)
67
+ !continue
162
68
  end
163
69
  end
164
70
 
165
71
  def assignment_expression?(code, local_variables:)
166
- # Try to parse the code and check if the last of possibly multiple
167
- # expressions is an assignment type.
168
-
169
- # If the expression is invalid, Ripper.sexp should return nil which will
170
- # result in false being returned. Any valid expression should return an
171
- # s-expression where the second element of the top level array is an
172
- # array of parsed expressions. The first element of each expression is the
173
- # expression's type.
174
- verbose, $VERBOSE = $VERBOSE, nil
175
- code = "#{RubyLex.generate_local_variables_assign_code(local_variables) || 'nil;'}\n#{code}"
176
- # Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
177
- node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
178
- ASSIGNMENT_NODE_TYPES.include?(node_type)
179
- ensure
180
- $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
181
96
  end
182
97
 
183
- def should_continue?(tokens)
184
- # Look at the last token and check if IRB need to continue reading next line.
185
- # Example code that should continue: `a\` `a +` `a.`
186
- # Trailing spaces, newline, comments are skipped
187
- return true if tokens.last&.event == :on_sp && tokens.last.tok == "\\\n"
188
-
189
- tokens.reverse_each do |token|
190
- case token.event
191
- when :on_sp, :on_nl, :on_ignored_nl, :on_comment, :on_embdoc_beg, :on_embdoc, :on_embdoc_end
192
- # Skip
193
- when :on_regexp_end, :on_heredoc_end, :on_semicolon
194
- # State is EXPR_BEG but should not continue
195
- 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
196
126
  else
197
- # Endless range should not continue
198
- return false if token.event == :on_op && token.tok.match?(/\A\.\.\.?\z/)
199
-
200
- # EXPR_DOT and most of the EXPR_BEG should continue
201
- return token.state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_DOT)
127
+ break
202
128
  end
203
129
  end
204
- 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)
205
134
  end
206
135
 
207
136
  def check_code_syntax(code, local_variables:)
208
- lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
209
- code = "#{lvars_code}\n#{code}"
210
-
211
- begin # check if parser error are available
212
- verbose, $VERBOSE = $VERBOSE, nil
213
- case RUBY_ENGINE
214
- when 'ruby'
215
- self.class.compile_with_errors_suppressed(code) do |inner_code, line_no|
216
- RubyVM::InstructionSequence.compile(inner_code, nil, nil, line_no)
217
- end
218
- when 'jruby'
219
- 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
220
158
  else
221
- catch(:valid) do
222
- eval("BEGIN { throw :valid, true }\n#{code}")
223
- false
224
- end
159
+ break
225
160
  end
226
- rescue EncodingError
227
- # This is for a hash with invalid encoding symbol, {"\xAE": 1}
228
- :unrecoverable_error
229
- rescue SyntaxError => e
230
- case e.message
231
- when /unexpected keyword_end/
232
- # "syntax error, unexpected keyword_end"
233
- #
234
- # example:
235
- # if (
236
- # end
237
- #
238
- # example:
239
- # end
240
- return :unrecoverable_error
241
- when /unexpected '\.'/
242
- # "syntax error, unexpected '.'"
243
- #
244
- # example:
245
- # .
246
- return :unrecoverable_error
247
- when /unexpected tREGEXP_BEG/
248
- # "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
249
- #
250
- # example:
251
- # 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
252
190
  return :unrecoverable_error
253
- when /unterminated (?:string|regexp) meets end of file/
254
- # "unterminated regexp meets end of file"
255
- #
256
- # example:
257
- # /
258
- #
259
- # "unterminated string meets end of file"
260
- #
261
- # example:
262
- # '
263
- return :recoverable_error
264
- when /unexpected end-of-input/
265
- # "syntax error, unexpected end-of-input, expecting keyword_end"
266
- #
267
- # example:
268
- # if true
269
- # hoge
270
- # if false
271
- # fuga
272
- # end
273
- 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
274
201
  else
275
- return :other_error
202
+ unknown = true
276
203
  end
277
- ensure
278
- $VERBOSE = verbose
279
204
  end
280
- :valid
205
+ unknown ? :other_error : :recoverable_error
281
206
  end
282
207
 
283
208
  def calc_indent_level(opens)
284
209
  indent_level = 0
285
- opens.each_with_index do |t, index|
286
- case t.event
210
+ opens.each_with_index do |elem, index|
211
+ case elem.event
287
212
  when :on_heredoc_beg
288
213
  if opens[index + 1]&.event != :on_heredoc_beg
289
- if t.tok.match?(/^<<[~-]/)
214
+ if elem.tok.match?(/^<<[~-]/)
290
215
  indent_level += 1
291
216
  else
292
217
  indent_level = 0
@@ -295,50 +220,50 @@ module IRB
295
220
  when :on_tstring_beg, :on_regexp_beg, :on_symbeg, :on_backtick
296
221
  # No indent: "", //, :"", ``
297
222
  # Indent: %(), %r(), %i(), %x()
298
- indent_level += 1 if t.tok.start_with? '%'
223
+ indent_level += 1 if elem.tok.start_with? '%'
299
224
  when :on_embdoc_beg
300
225
  indent_level = 0
301
226
  else
302
- indent_level += 1 unless t.tok == 'alias' || t.tok == 'undef'
227
+ indent_level += 1 unless elem.tok == 'alias' || elem.tok == 'undef'
303
228
  end
304
229
  end
305
230
  indent_level
306
231
  end
307
232
 
308
- 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]
309
234
 
310
- def free_indent_token?(token)
311
- FREE_INDENT_TOKENS.include?(token&.event)
235
+ def free_indent_nesting_element?(elem)
236
+ FREE_INDENT_NESTINGS.include?(elem&.event)
312
237
  end
313
238
 
314
239
  # Calculates the difference of pasted code's indent and indent calculated from tokens
315
240
  def indent_difference(lines, line_results, line_index)
316
241
  loop do
317
- _tokens, prev_opens, _next_opens, min_depth = line_results[line_index]
318
- open_token = prev_opens.last
319
- 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))
320
245
  # If the leading whitespace is an indent, return the difference
321
246
  indent_level = calc_indent_level(prev_opens.take(min_depth))
322
247
  calculated_indent = 2 * indent_level
323
248
  actual_indent = lines[line_index][/^ */].size
324
249
  return actual_indent - calculated_indent
325
- elsif open_token.event == :on_heredoc_beg && open_token.tok.match?(/^<<[^-~]/)
250
+ elsif open_elem.event == :on_heredoc_beg && open_elem.tok.match?(/^<<[^-~]/)
326
251
  return 0
327
252
  end
328
253
  # If the leading whitespace is not an indent but part of a multiline token
329
254
  # Calculate base_indent of the multiline token's beginning line
330
- line_index = open_token.pos[0] - 1
255
+ line_index = open_elem.pos[0] - 1
331
256
  end
332
257
  end
333
258
 
334
- def process_indent_level(tokens, lines, line_index, is_newline)
335
- 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)
336
261
  result = line_results[line_index]
337
262
  if result
338
- _tokens, prev_opens, next_opens, min_depth = result
263
+ prev_opens, next_opens, min_depth = result
339
264
  else
340
265
  # When last line is empty
341
- prev_opens = next_opens = line_results.last[2]
266
+ prev_opens = next_opens = line_results.last[1]
342
267
  min_depth = next_opens.size
343
268
  end
344
269
 
@@ -348,39 +273,39 @@ module IRB
348
273
 
349
274
  preserve_indent = lines[line_index - (is_newline ? 1 : 0)][/^ */].size
350
275
 
351
- prev_open_token = prev_opens.last
352
- next_open_token = next_opens.last
276
+ prev_open_elem = prev_opens.last
277
+ next_open_elem = next_opens.last
353
278
 
354
- # Calculates base indent for pasted code on the line where prev_open_token is located
355
- # irb(main):001:1* if a # base_indent is 2, indent calculated from tokens is 0
356
- # irb(main):002:1* if b # base_indent is 6, indent calculated from tokens is 2
357
- # irb(main):003:0> c # base_indent is 6, indent calculated from tokens is 4
358
- if prev_open_token
359
- 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
360
285
  else
361
286
  base_indent = 0
362
287
  end
363
288
 
364
- if free_indent_token?(prev_open_token)
365
- 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
366
291
  # First newline inside free-indent token
367
292
  base_indent + indent
368
293
  else
369
294
  # Accept any number of indent inside free-indent token
370
295
  preserve_indent
371
296
  end
372
- elsif prev_open_token&.event == :on_embdoc_beg || next_open_token&.event == :on_embdoc_beg
373
- 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
374
299
  # Accept any number of indent inside embdoc content
375
300
  preserve_indent
376
301
  else
377
302
  # =begin or =end
378
303
  0
379
304
  end
380
- elsif prev_open_token&.event == :on_heredoc_beg
381
- tok = prev_open_token.tok
305
+ elsif prev_open_elem&.event == :on_heredoc_beg
306
+ tok = prev_open_elem.tok
382
307
  if prev_opens.size <= next_opens.size
383
- 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
384
309
  # First line in heredoc
385
310
  tok.match?(/^<<[-~]/) ? base_indent + indent : indent
386
311
  elsif tok.match?(/^<<~/)
@@ -400,15 +325,15 @@ module IRB
400
325
  end
401
326
  end
402
327
 
403
- def ltype_from_open_tokens(opens)
404
- start_token = opens.reverse_each.find do |tok|
405
- 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)
406
331
  end
407
- return nil unless start_token
332
+ return nil unless start_nesting
408
333
 
409
- case start_token&.event
334
+ case start_nesting&.event
410
335
  when :on_tstring_beg
411
- case start_token&.tok
336
+ case start_nesting&.tok
412
337
  when ?" then ?"
413
338
  when /^%.$/ then ?"
414
339
  when /^%Q.$/ then ?"
@@ -423,50 +348,51 @@ module IRB
423
348
  when :on_qsymbols_beg then ?]
424
349
  when :on_symbols_beg then ?]
425
350
  when :on_heredoc_beg
426
- start_token&.tok =~ /<<[-~]?(['"`])\w+\1/
351
+ start_nesting&.tok =~ /<<[-~]?(['"`])\w+\1/
427
352
  $1 || ?"
428
353
  else
429
354
  nil
430
355
  end
431
356
  end
432
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
433
374
  def check_termination_in_prev_line(code, local_variables:)
434
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
435
- past_first_newline = false
436
- index = tokens.rindex do |t|
437
- # traverse first token before last line
438
- if past_first_newline
439
- if t.tok.include?("\n")
440
- true
441
- end
442
- elsif t.tok.include?("\n")
443
- past_first_newline = true
444
- false
445
- else
446
- false
447
- end
448
- end
375
+ lines = code.lines
376
+ return false if lines.size < 2
449
377
 
450
- if index
451
- first_token = nil
452
- last_line_tokens = tokens[(index + 1)..(tokens.size - 1)]
453
- last_line_tokens.each do |t|
454
- unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event)
455
- first_token = t
456
- break
457
- end
458
- end
378
+ prev_line_result = Prism.parse(lines[...-1].join, scopes: [local_variables])
379
+ return false unless prev_line_result.success?
459
380
 
460
- if first_token && first_token.state != Ripper::EXPR_DOT
461
- tokens_without_last_line = tokens[0..index]
462
- code_without_last_line = tokens_without_last_line.map(&:tok).join
463
- opens_without_last_line = NestingParser.open_tokens(tokens_without_last_line)
464
- if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line, local_variables: local_variables)
465
- return last_line_tokens.map(&:tok).join
466
- end
467
- 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
468
387
  end
469
- 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
470
396
  end
471
397
  end
472
398
  # :startdoc: