Skip to content

Commit

Permalink
Add line number and path to ParseException
Browse files Browse the repository at this point in the history
  • Loading branch information
maxfierke committed May 11, 2024
1 parent 67b18f3 commit 62b4ad5
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 23 deletions.
38 changes: 36 additions & 2 deletions spec/hcl/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ end

describe HCL::Parser do
describe "#parse" do
it "surfaces the Pegmatite error when encountering bad syntax" do
it "surfaces parsing errors w/ line numbers when encountering bad syntax" do
hcl_string = <<-HEREDOC
variable "ami" {
"whats the deal im walkin here"
Expand All @@ -23,7 +23,41 @@ describe HCL::Parser do

expect_raises(
HCL::ParseException,
/^unexpected token at byte offset 23:\n\s+"whats the deal im walkin here"\n\s+\^$/
/^Unable to parse HCL document. Encountered unexpected token at byte offset 23:\n\s+"whats the deal im walkin here"\n\s+\^ \(line 2\)$/
) do
parser.parse!
end
end

it "surfaces parsing errors w/ line numbers and path if provided" do
hcl_string = <<-HEREDOC
variable "ami" {
"whats the deal im walkin here"
HEREDOC

parser = HCL::Parser.new(hcl_string, path: "/some/path/file.hcl")

expect_raises(
HCL::ParseException,
/^Unable to parse HCL document. Encountered unexpected token at byte offset 23:\n\s+"whats the deal im walkin here"\n\s+\^ \(\/some\/path\/file\.hcl:2\)$/
) do
parser.parse!
end
end

it "surfaces helpful error if you forget to add a newline at the end" do
hcl_string = <<-HEREDOC
variable "ami" {
description = "the AMI to use"
}
HEREDOC

parser = HCL::Parser.new(hcl_string)

expect_raises(
HCL::ParseException,
/^Unable to parse HCL document. Encountered unexpected token at byte offset 57:\n\s+}\n\s+\^ \(line 3\)\nDid you forget to add a new line at the end\?$/
) do
parser.parse!
end
Expand Down
43 changes: 36 additions & 7 deletions src/hcl/exceptions.cr
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
module HCL
# Base error class for HCL parsing exceptions
class ParseException < Exception
def initialize(message, source : String? = nil, token : Pegmatite::Token? = nil)
if source && token
super(<<-MSG.strip)
#{message}. At or near '#{source[token[1]...token[2]]}'
MSG
else
super(message)
@line_number : Int32?
@path : Path?

getter :line_number, :path

def initialize(message, source : String? = nil, token : Pegmatite::Token? = nil, offset : Int32? = nil, path : String? = nil)
if token && source
line_number = source[0...token[1]].count('\n') + 1
elsif source && offset
line_number = source[0...offset].count('\n') + 1
end

path = Path[path] if path

message = String.build do |msg|
msg << "Unable to parse HCL document"
if source && token
msg << " at or near '#{source[token[1]...token[2]]}'"
end
msg << ". Encountered "
msg << message

if path && line_number
msg << " (#{path.normalize}:#{line_number})"
elsif line_number
msg << " (line #{line_number})"
end

if source && offset == source.size
msg << "\nDid you forget to add a new line at the end?"
end
end

@line_number = line_number
@path = path

super(message)
end
end

Expand Down
34 changes: 20 additions & 14 deletions src/hcl/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ module HCL
class Parser
@source : String
@source_offset = 0
@source_path : String?
@document : AST::Document?
@parse_trace_io : IO?

getter :document, :source
getter :document, :source, :source_path

def self.parse!(*args, **kwargs)
new(*args, **kwargs).parse!
end

def initialize(source : String | IO, offset = 0, io : IO? = nil)
def initialize(source : String | IO, offset = 0, io : IO? = nil, path : String? = nil)
@source = source.is_a?(IO) ? source.gets_to_end : source
@source_offset = offset
@source_path = source.responds_to?(:path) ? source.path : path
@parse_trace_io = io
end

Expand All @@ -28,18 +30,17 @@ module HCL
peg_iter = Pegmatite::TokenIterator.new(peg_tokens)
build_document(peg_iter, @source)
rescue e : Pegmatite::Pattern::MatchError
raise ParseException.new(e.message)
raise ParseException.new(e.message, source: source, offset: e.offset, path: source_path)
end
end

private def assert_token_kind!(token : Pegmatite::Token, expected_kind)
kind, _, _ = token
assert_token_kind!(kind, expected_kind)
end

private def assert_token_kind!(kind : Symbol, expected_kind)
raise ParseException.new(
"Expected #{expected_kind}, but got #{kind}."
"Expected #{expected_kind}, but got #{kind}.",
source: source,
path: source_path,
token: token,
) unless kind == expected_kind
end

Expand Down Expand Up @@ -67,6 +68,7 @@ module HCL
raise ParseException.new(
"Found '#{kind}' but expected an attribute assignment or block.",
source: source,
path: source_path,
token: token
)
end
Expand Down Expand Up @@ -420,21 +422,22 @@ module HCL
end

private def extract_identifier(main, iter, source)
kind, start, finish = main
assert_token_kind!(kind, :identifier)
assert_token_kind!(main, :identifier)

_, start, finish = main

source[start...finish]
end

private def build_map(main, iter, source) : AST::Map
kind, start, finish = main
assert_token_kind!(kind, :object)
assert_token_kind!(main, :object)

_, start, finish = main

values = {} of String => AST::Node

iter.while_next_is_child_of(main) do |token|
kind, _, _ = token
assert_token_kind!(kind, :attribute)
assert_token_kind!(token, :attribute)

# Gather children as pairs of key/values into the object.
key = build_node(iter.next_as_child_of(token), iter, source).to_s
Expand Down Expand Up @@ -480,6 +483,7 @@ module HCL
raise ParseException.new(
"Found '#{kind}' but expected an attribute assignment or block.",
source: source,
path: source_path,
token: token
)
else
Expand All @@ -495,6 +499,7 @@ module HCL
raise ParseException.new(
"'#{kind}' is not supported within blocks.",
source: source,
path: source_path,
token: token
)
end
Expand Down Expand Up @@ -530,6 +535,7 @@ module HCL
raise ParseException.new(
"Cannot specify additional arguments after a varadic argument (...)",
source: source,
path: source_path,
token: child
) if varadic
args << build_node(child, iter, source)
Expand Down

0 comments on commit 62b4ad5

Please sign in to comment.