Skip to content

Commit

Permalink
Adds support for default error reporting (#83)
Browse files Browse the repository at this point in the history
- By raising SyntaxTree::Parser::ParseError with default arguments
  it returns much nicer error messages.
  • Loading branch information
davidwessman authored Apr 25, 2024
1 parent dbcdf3f commit 2922893
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 55 deletions.
147 changes: 111 additions & 36 deletions lib/syntax_tree/erb/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ module ERB
class Parser
# This is the parent class of any kind of errors that will be raised by
# the parser.
class ParseError < StandardError
end

# This error occurs when a certain token is expected in a certain place
# but is not found. Sometimes this is handled internally because some
# elements are optional. Other times it is not and it is raised to end the
# parsing process.
class MissingTokenError < ParseError
class MissingTokenError < SyntaxTree::Parser::ParseError
end

attr_reader :source, :tokens
Expand Down Expand Up @@ -52,7 +50,13 @@ def parse_any_tag

if tag.is_a?(Doctype)
if @found_doctype
raise(ParseError, "Only one doctype element is allowed")
raise(
SyntaxTree::Parser::ParseError.new(
"Only one doctype element is allowed",
tag.location.start_line,
0
)
)
else
@found_doctype = true
end
Expand Down Expand Up @@ -123,8 +127,13 @@ def make_tokens
# abc
enum.yield :text, $&, index, line
else
raise ParseError,
"Unexpected character at #{index}: #{source[index]}"
raise(
SyntaxTree::Parser::ParseError.new(
"Unexpected character at #{index}: #{source[index]}",
line,
0
)
)
end
in :erb_start
case source[index..]
Expand Down Expand Up @@ -207,8 +216,13 @@ def make_tokens
# abc
enum.yield :text, $&, index, line
else
raise ParseError,
"Unexpected character in string at #{index}: #{source[index]}"
raise(
SyntaxTree::Parser::ParseError.new(
"Unexpected character in string at #{index}: #{source[index]}",
line,
0
)
)
end
in :string_double_quote
case source[index..]
Expand All @@ -230,8 +244,13 @@ def make_tokens
# abc
enum.yield :text, $&, index, line
else
raise ParseError,
"Unexpected character in string at #{index}: #{source[index]}"
raise(
SyntaxTree::Parser::ParseError.new(
"Unexpected character in string at #{index}: #{source[index]}",
line,
0
)
)
end
in :inside
case source[index..]
Expand Down Expand Up @@ -284,8 +303,13 @@ def make_tokens
enum.yield :string_open_single_quote, $&, index, line
state << :string_single_quote
else
raise ParseError,
"Unexpected character at #{index}: #{source[index]}"
raise(
SyntaxTree::Parser::ParseError.new(
"Unexpected character at #{index}: #{source[index]}",
line,
0
)
)
end
end

Expand All @@ -304,7 +328,13 @@ def consume(expected)
type, value, index, line = tokens.peek

if expected != type
raise MissingTokenError, "expected #{expected} got #{type}"
raise(
MissingTokenError.new(
"expected #{expected} got #{type}",
line,
index
)
)
end

tokens.next
Expand Down Expand Up @@ -335,7 +365,9 @@ def maybe
# Otherwise we'll return the value returned by the block.
def atleast
result = yield
raise MissingTokenError if result.nil?
if result.nil?
raise(MissingTokenError.new("No matching token", nil, nil))
end
result
end

Expand Down Expand Up @@ -372,7 +404,13 @@ def parse_html_opening_tag
name = consume(:name)

if name.value =~ /\A[@:#]/
raise ParseError, "Invalid html-tag name #{name}"
raise(
SyntaxTree::Parser::ParseError.new(
"Invalid html-tag name #{name}",
name.location.start_line,
0
)
)
end

attributes =
Expand Down Expand Up @@ -431,15 +469,21 @@ def parse_html_element

if closing.nil?
raise(
ParseError,
"Missing closing tag for <#{opening.name.value}> at #{opening.location}"
SyntaxTree::Parser::ParseError.new(
"Missing closing tag for <#{opening.name.value}> at #{opening.location}",
opening.location.start_line,
0
)
)
end

if closing.name.value != opening.name.value
raise(
ParseError,
"Expected closing tag for <#{opening.name.value}> but got <#{closing.name.value}> at #{closing.location}"
SyntaxTree::Parser::ParseError.new(
"Expected closing tag for <#{opening.name.value}> but got <#{closing.name.value}> at #{closing.location}",
closing.location.start_line,
0
)
)
end

Expand All @@ -462,8 +506,11 @@ def parse_erb_case(erb_node)
unless erb_tag.is_a?(ErbCaseWhen) || erb_tag.is_a?(ErbElse) ||
erb_tag.is_a?(ErbEnd)
raise(
ParseError,
"Found no matching erb-tag to the if-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching erb-tag to the if-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end

Expand All @@ -484,8 +531,11 @@ def parse_erb_case(erb_node)
)
else
raise(
ParseError,
"Found no matching when- or else-tag to the case-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching when- or else-tag to the case-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end
end
Expand All @@ -501,8 +551,11 @@ def parse_erb_if(erb_node)

unless erb_tag.is_a?(ErbControl) || erb_tag.is_a?(ErbEnd)
raise(
ParseError,
"Found no matching erb-tag to the if-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching erb-tag to the if-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end

Expand Down Expand Up @@ -530,8 +583,11 @@ def parse_erb_if(erb_node)
)
else
raise(
ParseError,
"Found no matching elsif- or else-tag to the if-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching elsif- or else-tag to the if-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end
end
Expand All @@ -543,8 +599,11 @@ def parse_erb_else(erb_node)

unless erb_end.is_a?(ErbEnd)
raise(
ParseError,
"Found no matching end-tag for the else-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching end-tag for the else-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end

Expand Down Expand Up @@ -582,8 +641,11 @@ def parse_erb_tag

if !closing_tag.is_a?(ErbClose)
raise(
ParseError,
"Found no matching closing tag for the erb-tag at #{opening_tag.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching closing tag for the erb-tag at #{opening_tag.location}",
opening_tag.location.start_line,
0
)
)
end

Expand Down Expand Up @@ -615,8 +677,11 @@ def parse_erb_tag

unless erb_end.is_a?(ErbEnd)
raise(
ParseError,
"Found no matching end-tag for the do-tag at #{erb_node.location}"
SyntaxTree::Parser::ParseError.new(
"Found no matching end-tag for the do-tag at #{erb_node.location}",
erb_node.location.start_line,
0
)
)
end

Expand All @@ -630,13 +695,23 @@ def parse_erb_tag
erb_node
end
end
rescue MissingTokenError => error
rescue SyntaxTree::Parser::ParseError => error
# If we have parsed tokens that we cannot process after we parsed <%, we should throw a ParseError
# and not let it be handled by a `maybe`.
if opening_tag
message =
if error.message.include?("Could not parse ERB-tag")
error.message
else
"Could not parse ERB-tag: #{error.message}"
end

raise(
ParseError,
"Could not parse ERB-tag at #{opening_tag.location}"
SyntaxTree::Parser::ParseError.new(
message,
opening_tag.location.start_line,
0
)
)
else
raise(error)
Expand Down
23 changes: 20 additions & 3 deletions test/erb_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ def test_empty_file
end

def test_missing_erb_end_tag
assert_raises(SyntaxTree::ERB::Parser::ParseError) do
assert_raises(SyntaxTree::Parser::ParseError) do
ERB.parse("<% if no_end_tag %>")
end
end

def test_missing_erb_block_end_tag
assert_raises(SyntaxTree::ERB::Parser::ParseError) do
assert_raises(SyntaxTree::Parser::ParseError) do
ERB.parse("<% no_end_tag do %>")
end
end

def test_missing_erb_case_end_tag
assert_raises(SyntaxTree::ERB::Parser::ParseError) do
assert_raises(SyntaxTree::Parser::ParseError) do
ERB.parse("<% case variabel %>\n<% when 1>\n Hello\n")
end
end
Expand All @@ -35,6 +35,23 @@ def test_erb_code_with_non_ascii
assert_instance_of(SyntaxTree::ERB::ErbNode, parsed.elements.first)
end

def test_erb_errors
example = <<-HTML
<ul>
<% if @items.each do |i|%>
<li><%= i %></li>
<% end.blank? %>
<li>No items</li>
<% end %>
</ul>
HTML
ERB.parse(example)
rescue SyntaxTree::Parser::ParseError => error
assert_equal(2, error.lineno)
assert_equal(0, error.column)
assert_match(/Could not parse ERB-tag/, error.message)
end

def test_if_and_end_in_same_output_tag_short
source = "<%= if true\n what\nend %>"
expected = "<%= what if true %>\n"
Expand Down
Loading

0 comments on commit 2922893

Please sign in to comment.