Skip to content
Merged
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
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