Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions lib/yard/parser/ruby/ruby_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,18 @@ def on_sp(tok)

def visit_event(node)
map = @map[MAPPINGS[node.type]]
lstart, sstart = *(map ? map.pop : [lineno, @ns_charno - 1])

# Pattern matching and `in` syntax creates :case nodes without 'case' tokens,
# fall back to the first child node.
if node.type == :case && (!map || map.empty?) && (child_node = node[0])
lstart = child_node.line_range.first
sstart = child_node.source_range.first
else
lstart, sstart = *(map ? map.pop : [lineno, @ns_charno - 1])
end

raise "Cannot determine start of node #{node} around #{file}:#{lineno}" if lstart.nil? || sstart.nil?

node.source_range = Range.new(sstart, @ns_charno - 1)
node.line_range = Range.new(lstart, lineno)
if node.respond_to?(:block)
Expand All @@ -259,7 +270,10 @@ def visit_event_arr(node)
def visit_ns_token(token, data, ast_token = false)
add_token(token, data)
ch = charno
@last_ns_token = [token, data]

# For purposes of tracking parsing state, don't treat keywords as such
# where used as a symbol identifier.
@last_ns_token = [@last_ns_token && @last_ns_token.first == :symbeg ? :symbol : token, data]
@charno += data.length
@ns_charno = charno
@newline = [:semicolon, :comment, :kw, :op, :lparen, :lbrace].include?(token)
Expand Down
96 changes: 96 additions & 0 deletions spec/parser/ruby/ruby_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -553,5 +553,101 @@ def add(x) = x + 1

expect(Registry.at('A#add').docstring).to eq('Adds two numbers')
end if RUBY_VERSION >= '3.'

it "doesn't crash with pattern matching following a case statement" do
code = <<-RUBY
case value
when 1
"number"
end

{} => {}
RUBY

expect {
YARD::Parser::Ruby::RubyParser.new(code, nil).parse
}.not_to raise_error
end if RUBY_VERSION >= '3.'

it "doesn't crash with `next` following `:def` symbol after initial `next`" do
code = <<-RUBY
foo do
next
if :def
next
end
end
RUBY

expect {
YARD::Parser::Ruby::RubyParser.new(code, nil).parse
}.not_to raise_error
end

it "doesn't crash with mixed pattern matching syntaxes" do
code = <<-RUBY
case foo
in bar
return
end
return if foo && foo in []
RUBY

expect {
parser = YARD::Parser::Ruby::RubyParser.new(code, nil)
parser.parse
}.not_to raise_error
end if RUBY_VERSION >= '2.7'

it "provides correct range for various pattern matching statements" do
patterns = [
"{} => {}",
"{a: 1} => {b: 2}",
"{x: 'test'} => result",
"foo in []"
]

patterns.each do |pattern|
parser = YARD::Parser::Ruby::RubyParser.new(pattern, nil)
ast = parser.parse.root

case_node = nil
ast.traverse do |node|
if node.type == :case
case_node = node
break
end
end

expect(case_node).not_to be_nil, "Pattern #{pattern} should create a case node"
actual_text = pattern[case_node.source_range]
expect(actual_text).to eq(pattern),
"Pattern #{pattern} should have source range covering the full expression, got #{actual_text.inspect}"
end
end if RUBY_VERSION >= '3.'

it "provides correct range for `next` statement following `def` _symbol_" do
code = <<-RUBY
foo do
if :def
next
end
end
RUBY

parser = YARD::Parser::Ruby::RubyParser.new(code, nil)
ast = parser.parse.root

next_node = nil
ast.traverse do |node|
if node.type == :next
next_node = node
break
end
end

expect(next_node.line_range).to eq(3..3)
expect(code[next_node.source_range]).to eq('next')
end
end
end if HAVE_RIPPER
Loading