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