Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Reader] Add context to parse errors #7

Merged
merged 4 commits into from
Nov 2, 2016
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

##### Enhancements

* None.
* Add context to parse errors.
[Samuel Giddins](https://github.com/segiddins)
[#5](https://github.com/CocoaPods/Nanaimo/issues/5)

##### Bug Fixes

Expand Down
46 changes: 36 additions & 10 deletions lib/nanaimo/reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ class ParseError < Error
# @return [String] The contents of the plist.
#
attr_accessor :plist_string

def to_s
"[!] #{super}#{context}"
end

def context(n = 2)
line_number, column = location
line_number -= 1
lines = plist_string.split(NEWLINE)

s = line_number.succ.to_s.size
indent = "#{' ' * s}# "
indicator = "#{line_number.succ}> "

m = ::String.new("\n")
m << "#{indent}-------------------------------------------\n"
m << lines[[line_number - n, 0].max...line_number].map do |l|
"#{indent}#{l}\n"
end.join
m << "#{indicator}#{lines[line_number]}\n"
m << ' ' * (column + s + 2) << "^\n"
m << Array(lines[line_number.succ..[lines.count.pred, line_number + n].min]).map do |l|
l.strip.empty? ? '' : "#{indent}#{l}\n"
end.join
m << "#{indent}-------------------------------------------\n"
end
end

# @param plist_contents [String]
Expand Down Expand Up @@ -73,7 +99,7 @@ def parse!
root_object = parse_object

eat_whitespace!
raise_parser_error ParseError, "unrecognized characters #{@scanner.rest.inspect} after parsing" unless @scanner.eos?
raise_parser_error ParseError, 'Found additional characters after parsing the root plist object' unless @scanner.eos?

Nanaimo::Plist.new(root_object, plist_format)
end
Expand All @@ -93,7 +119,7 @@ def read_string_encoding
def parse_object
_comment = skip_to_non_space_matching_annotations
start_pos = @scanner.pos
raise_parser_error ParseError, 'Unexpected eos while parsing' if @scanner.eos?
raise_parser_error ParseError, 'Unexpected end of string while parsing' if @scanner.eos?
if @scanner.skip(/\{/)
parse_dictionary
elsif @scanner.skip(/\(/)
Expand All @@ -112,15 +138,15 @@ def parse_object

def parse_string
eat_whitespace!
unless match = @scanner.scan(%r{[\w/.$-]+}o)
raise_parser_error ParseError, "not a valid string at index #{@scanner.pos} (char is #{current_character.inspect})"
unless match = @scanner.scan(%r{[\w/.$]+})
raise_parser_error ParseError, "Invalid character #{current_character.inspect} in unquoted string"
end
Nanaimo::String.new(match, nil)
end

def parse_quotedstring(quote)
unless string = @scanner.scan(/(?:([^#{quote}\\]|\\.)*)#{quote}/)
raise_parser_error ParseError, "unterminated quoted string started at #{@scanner.pos}, expected #{quote} but never found it"
raise_parser_error ParseError, "Unterminated quoted string, expected #{quote} but never found it"
end
string = Unicode.unquotify_string(string.chomp!(quote))
Nanaimo::QuotedString.new(string, nil)
Expand All @@ -137,7 +163,7 @@ def parse_array
eat_whitespace!
break if @scanner.skip(/\)/)
unless @scanner.skip(/,/)
raise_parser_error ParseError, "Array #{objects} missing ',' in between objects"
raise_parser_error ParseError, "Array missing ',' in between objects"
end
end

Expand All @@ -153,7 +179,7 @@ def parse_dictionary
key = parse_object
eat_whitespace!
unless @scanner.skip(/=/)
raise_parser_error ParseError, "Dictionary missing value after key #{key.inspect} at index #{@scanner.pos}, expected '=' and got #{current_character.inspect}"
raise_parser_error ParseError, "Dictionary missing value for key #{key.as_ruby.inspect}, expected '=' and found #{current_character.inspect}"
end

value = parse_object
Expand All @@ -162,7 +188,7 @@ def parse_dictionary
eat_whitespace!
break if @scanner.skip(/}/)
unless @scanner.skip(/;/)
raise_parser_error ParseError, "Dictionary (#{objects}) missing ';' after key-value pair (#{key} = #{value}) at index #{@scanner.pos} (got #{current_character})"
raise_parser_error ParseError, "Dictionary missing ';' after key-value pair for #{key.as_ruby.inspect}, found #{current_character.inspect}"
end
end

Expand All @@ -189,7 +215,7 @@ def current_character

def read_singleline_comment
unless comment = @scanner.scan_until(NEWLINE)
raise_parser_error ParseError, "failed to terminate single line comment #{@scanner.rest.inspect}"
raise_parser_error ParseError, 'Failed to terminate single line comment'
end
comment
end
Expand All @@ -208,7 +234,7 @@ def eat_whitespace!

def read_multiline_comment
unless annotation = @scanner.scan(%r{(?:.+?)(?=\*/)}m)
raise_parser_error ParseError, "#{@scanner.rest.inspect} failed to terminate multiline comment"
raise_parser_error ParseError, 'Failed to terminate multiline comment'
end
@scanner.skip(%r{\*/})

Expand Down
2 changes: 1 addition & 1 deletion lib/nanaimo/writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def write_object(object)
when Data
write_data(object)
else
raise "Cannot write #{object} to an ascii plist"
raise "Cannot write #{object.inspect} to an ascii plist"
end
write_annotation(object) if pretty
output
Expand Down
214 changes: 205 additions & 9 deletions spec/nanaimo/reader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,6 @@ module Nanaimo
expect(subject).to eq Nanaimo::Dictionary.new({ Nanaimo::String.new('key', '') => Nanaimo::String.new('$PROJECT_DIR/mogenerator/mogenerator', '') }, '')
end
end

describe 'that contain `-`' do
let(:unquoted_string) { 'foo/bar-baz' }

it 'are parsed correctly' do
expect(subject).to eq Nanaimo::Dictionary.new({ Nanaimo::String.new('key', '') => Nanaimo::String.new('foo/bar-baz', '') }, '')
end
end
end

describe 'quoted strings' do
Expand All @@ -147,9 +139,213 @@ module Nanaimo
let(:data) { '<12 3>' }

it 'raises an informative error' do
expect { subject }.to raise_error(Reader::ParseError, 'Data has an uneven number of hex digits')
expect { subject }.to raise_error(Reader::ParseError, <<-E)
[!] Data has an uneven number of hex digits
# -------------------------------------------
1> {key = <12 3>}
^
# -------------------------------------------
E
end
end
end

describe 'parse errors' do
shared_examples_for 'parse errors' do |name, plist, expected_error|
it "raises an informative error #{name}" do
if expected_error.is_a?(::String)
prefix = expected_error.scan(/^[ \t]*(?=\S)/).min
expected_error.gsub!(/^#{prefix}/, '')
end

expect { Reader.new(plist).parse! }
.to raise_error(Reader::ParseError) do |e|
if e.is_a?(Regexp)
expect(e.to_s).to match(expected_error)
else
expect(e.to_s).to eq(expected_error)
end
end
end
end

include_examples 'parse errors',
'with a dictionary without an `=`',
'{ a = ; }',
<<-E
[!] Invalid character ";" in unquoted string
# -------------------------------------------
1> { a = ; }
^
# -------------------------------------------
E

include_examples 'parse errors',
'with an unterminated array',
<<-PLIST,
(
a,
b,
(
c
)
PLIST
<<-E
[!] Array missing ',' in between objects
# -------------------------------------------
# c
# )
7>\s\s
^
# -------------------------------------------
E

include_examples 'parse errors',
'with an error in the middle of the plist',
<<-PLIST,
(
#{"a,\n" * 1000}
c,
d,
e,,,
f,
g,
#{"z,\n" * 250}
zz
)
PLIST
<<-E
[!] Invalid character "," in unquoted string
# -------------------------------------------
# c,
# d,
1005> e,,,
^
# f,
# g,
# -------------------------------------------
E

include_examples 'parse errors',
'with an array missing a comma between elements',
<<-PLIST,
(
a,
b,
(
c
d
)
)
PLIST
<<-E
[!] Array missing ',' in between objects
# -------------------------------------------
# (
# c
6> d
^
# )
# )
# -------------------------------------------
E

include_examples 'parse errors',
'with an unterminated dictionary',
<<-PLIST,
{
a = e;
b = j;
d = (
c
);
PLIST
<<-E
[!] Unexpected end of string while parsing
# -------------------------------------------
# c
# );
7>\s\s
^
# -------------------------------------------
E

include_examples 'parse errors',
'with an unterminated dictionary pair',
<<-PLIST,
{
a = e;
b = j;
d = (
c
)
PLIST
<<-E
[!] Dictionary missing ';' after key-value pair for "d", found ""
# -------------------------------------------
# c
# )
7>\s\s
^
# -------------------------------------------
E

include_examples 'parse errors',
'with a dictionary without an `=`',
'{a}',
<<-E
[!] Dictionary missing value for key "a", expected '=' and found "}"
# -------------------------------------------
1> {a}
^
# -------------------------------------------
E

include_examples 'parse errors',
'with a dictionary without a value',
'{ a = ; }',
<<-E
[!] Invalid character ";" in unquoted string
# -------------------------------------------
1> { a = ; }
^
# -------------------------------------------
E

include_examples 'parse errors',
'with an unterminated quoted string',
"{\na = 'bcd\n}",
<<-E
[!] Unterminated quoted string, expected ' but never found it
# -------------------------------------------
# {
2> a = 'bcd
^
# }
# -------------------------------------------
E

include_examples 'parse errors',
'with non-whitespace after the root object',
'abcd a',
<<-E
[!] Found additional characters after parsing the root plist object
# -------------------------------------------
1> abcd a
^
# -------------------------------------------
E

include_examples 'parse errors',
'when a data is unterminated',
'<1234',
<<-E
[!] Data missing closing '>'
# -------------------------------------------
1> <1234
^
# -------------------------------------------
E
end
end
end