Server : LiteSpeed System : Linux ncr1.int3rnet.net 4.18.0-553.123.2.lve.el8.x86_64 #1 SMP Thu May 7 23:17:13 UTC 2026 x86_64 User : digitaln ( 1704) PHP Version : 8.1.34 Disable Function : NONE Directory : /opt/cpanel/ea-ruby27/root/usr/local/share/gems/gems/irb-1.18.0/lib/irb/ |
# frozen_string_literal: true
require 'prism'
module IRB
module NestingParser
NestingElem = Struct.new(:pos, :event, :tok)
class NestingVisitor < Prism::Visitor
def initialize
# Array of [column, priority(+1/-1), NestingElem(open) or nil(close)] per line.
# priority is +1 for open, -1 for close so that close comes before open when sorted.
# Example:
# if cond
# else
# end
# `else` closes `if` at column 0 first and then opens `else` at column 0 next.
@lines = []
# Array of open heredoc NestingElem per line
@heredocs = []
end
def nestings
size = [@lines.size, @heredocs.size].max
nesting = []
size.times.map do |line_index|
@lines[line_index]&.sort_by { |col, pri| [col, pri] }&.each do |col, pri, elem|
if elem
nesting << elem
else
nesting.pop
end
end
@heredocs[line_index]&.sort_by { |elem| elem.pos[1] }&.reverse_each do |elem|
nesting << elem
end
nesting.dup
end
end
def heredoc_open(node)
elem = NestingElem.new([node.location.start_line, node.location.start_column], :on_heredoc_beg, node.opening)
(@heredocs[node.location.start_line - 1] ||= []) << elem
end
def open(line, column, elem)
(@lines[line - 1] ||= []) << [column, +1, elem]
end
def close(line, column)
(@lines[line - 1] ||= []) << [column, -1]
end
# Checks if a node (if, while, etc) is a modifier form that does not need `end` closing.
# modifier node: `a if b`, non-modifier node: `if a; b; end`
def modifier_node?(node, keyword_loc)
!(keyword_loc && node.location.start_line == keyword_loc.start_line && node.location.start_column == keyword_loc.start_column)
end
def open_location(location, type, tok)
open(location.start_line, location.start_column, NestingElem.new([location.start_line, location.start_column], type, tok))
end
def close_location(location)
close(location.end_line, location.end_column)
end
def close_location_start(location)
close(location.start_line, location.start_column)
end
def close_end_keyword_loc(node)
close_location(node.end_keyword_loc) if node.end_keyword == 'end'
end
def close_closing_loc(node)
close_location(node.closing_loc) if node.closing_loc && !node.closing.empty?
end
def visit_for_node(node)
super
open_location(node.location, :on_kw, 'for')
close_end_keyword_loc(node)
end
def visit_while_node(node)
super
return if modifier_node?(node, node.keyword_loc)
open_location(node.location, :on_kw, 'while')
close_closing_loc(node)
end
def visit_until_node(node)
super
return if modifier_node?(node, node.keyword_loc)
open_location(node.location, :on_kw, 'until')
close_closing_loc(node)
end
def visit_if_node(node)
super
return if !node.if_keyword || modifier_node?(node, node.if_keyword_loc)
open_location(node.location, :on_kw, node.if_keyword)
if node.subsequent
close_location_start(node.subsequent.location)
else
close_end_keyword_loc(node)
end
end
def visit_unless_node(node)
super
return if modifier_node?(node, node.keyword_loc)
open_location(node.location, :on_kw, 'unless')
if node.else_clause
close_location_start(node.else_clause.location)
else
close_end_keyword_loc(node)
end
end
def visit_case_node(node)
super
open_location(node.location, :on_kw, 'case')
if node.else_clause
close_location_start(node.else_clause.location)
else
close_end_keyword_loc(node)
end
end
alias visit_case_match_node visit_case_node
def visit_when_node(node)
super
close_location_start(node.location)
open_location(node.location, :on_kw, 'when')
end
def visit_in_node(node)
super
close_location_start(node.location)
open_location(node.location, :on_kw, 'in')
end
def visit_else_node(node)
super
if node.else_keyword == 'else'
open_location(node.location, :on_kw, 'else')
close_end_keyword_loc(node)
end
end
def visit_ensure_node(node)
super
return if modifier_node?(node, node.ensure_keyword_loc)
close_location_start(node.location)
open_location(node.location, :on_kw, 'ensure')
end
def visit_rescue_node(node)
super
return if modifier_node?(node, node.keyword_loc)
close_location_start(node.location)
open_location(node.location, :on_kw, 'rescue')
end
def visit_begin_node(node)
super
if node.begin_keyword
open_location(node.location, :on_kw, 'begin')
close_end_keyword_loc(node)
end
end
def visit_block_node(node)
super
open_location(node.location, node.opening == '{' ? :on_lbrace : :on_kw, node.opening)
close_closing_loc(node)
end
def visit_array_node(node)
super
type =
case node.opening
when nil
# `x = 1, 2` doesn't have opening
nil
when '['
:bracket
when /\A%W/
:on_words_beg
when /\A%w/
:on_qwords_beg
when /\A%I/
:on_symbols_beg
when /\A%i/
:on_qsymbols_beg
end
if type
open_location(node.location, type, node.opening)
close_closing_loc(node)
end
end
def visit_hash_node(node)
super
open_location(node.location, :on_lbrace, '{')
close_closing_loc(node)
end
def heredoc_string_like(node, type)
if node.opening&.start_with?('<<')
heredoc_open(node)
# Heredoc closing contains trailing newline. We need to exclude it
close_location_start(node.closing_loc) if node.closing_loc && !node.closing.empty?
elsif node.opening
return if node.opening == '?' && node.closing.nil? # Character literal has no closing
open_location(node.location, type, node.opening)
if node.closing && node.closing != ''
# Closing of `"#{\n` is "\n". We need to treat it as not-closed.
close_location_start(node.closing_loc) if node.opening.match?(/\n\z/) || node.closing != "\n"
end
end
end
def visit_embedded_statements_node(node)
super
open_location(node.location, :on_embexpr_beg, '#{')
close_closing_loc(node)
end
def visit_interpolated_string_node(node)
super
heredoc_string_like(node, :on_tstring_beg)
end
alias visit_string_node visit_interpolated_string_node
def visit_interpolated_x_string_node(node)
super
heredoc_string_like(node, :on_backtick)
end
alias visit_x_string_node visit_interpolated_x_string_node
def visit_symbol_node(node)
super
unless node.opening.nil? || node.opening.empty? || node.opening == ':'
# :"sym" or %s[sym]
open_location(node.location, :on_symbeg, node.opening)
close_closing_loc(node)
end
end
alias visit_interpolated_symbol_node visit_symbol_node
def visit_regular_expression_node(node)
super
open_location(node.location, :on_regexp_beg, node.opening)
close_closing_loc(node)
end
alias visit_interpolated_regular_expression_node visit_regular_expression_node
def visit_parentheses_node(node)
super
open_location(node.location, :on_lparen, '(')
close_closing_loc(node)
end
def visit_call_node(node)
super
type =
case node.opening
when '('
:on_lparen
when '['
:on_lbracket
end
if type
open_location(node.opening_loc, type, node.opening)
close_closing_loc(node)
end
end
def visit_block_parameters_node(node)
super
if node.opening == '('
open_location(node.location, :on_lparen, '(')
close_closing_loc(node)
end
end
def visit_lambda_node(node)
super
open_location(node.opening_loc, :on_tlambeg, node.opening)
close_closing_loc(node)
end
def visit_super_node(node)
super
if node.lparen
open_location(node.lparen_loc, :on_lparen, '(')
close_location(node.rparen_loc) if node.rparen == ')'
end
end
alias visit_yield_node visit_super_node
alias visit_defined_node visit_super_node
def visit_def_node(node)
super
open_location(node.location, :on_kw, 'def')
if node.lparen == '('
open_location(node.lparen_loc, :on_lparen, '(')
close_location(node.rparen_loc) if node.rparen == ')'
end
if node.equal
close_location(node.equal_loc)
else
close_end_keyword_loc(node)
end
end
def visit_class_node(node)
super
open_location(node.location, :on_kw, 'class')
close_end_keyword_loc(node)
end
alias visit_singleton_class_node visit_class_node
def visit_module_node(node)
super
open_location(node.location, :on_kw, 'module')
close_end_keyword_loc(node)
end
end
class << self
# Return a list of open nesting elements at EOF position
def open_nestings(parse_lex_result)
parse_by_line(parse_lex_result).last[1]
end
# Calculates nesting information [prev_opens, next_opens, min_depth] for each line.
# Example code
# ["hello
# world"+(
# First line
# prev_opens: []
# next_opens: [lbracket, tstring_beg]
# min_depth: 0 (minimum at beginning of line)
# Second line
# prev_opens: [lbracket, tstring_beg]
# next_opens: [lbracket, lparen]
# min_depth: 1 (minimum just after tstring_end)
def parse_by_line(parse_lex_result)
visitor = NestingVisitor.new
node, tokens = parse_lex_result.value
node.accept(visitor)
tokens.each do |token,|
case token.type
when :EMBDOC_BEGIN
visitor.open_location(token.location, :on_embdoc_beg, '=begin')
when :EMBDOC_END
visitor.close_location_start(token.location)
end
end
nestings = visitor.nestings
last_nesting = nestings.last || []
num_lines = parse_lex_result.source.source.lines.size
num_lines.times.map do |i|
prev_opens = i == 0 ? [] : nestings[i - 1] || last_nesting
opens = nestings[i] || last_nesting
min_depth = prev_opens.zip(opens).take_while { |s, e| s == e }.size
[prev_opens, opens, min_depth]
end
end
end
end
end