Skip to content

Better error messages on missing tokens #132

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

Merged
merged 1 commit into from
Aug 12, 2022
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
1 change: 1 addition & 0 deletions lib/syntax_tree.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "delegate"
require "etc"
require "json"
require "pp"
Expand Down
3 changes: 2 additions & 1 deletion lib/syntax_tree/language_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def format(source, file_extension)
character: 0
}
},
newText: SyntaxTree::HANDLERS[".#{file_extension}"].format(source, print_width)
newText:
SyntaxTree::HANDLERS[".#{file_extension}"].format(source, print_width)
}
end

Expand Down
58 changes: 49 additions & 9 deletions lib/syntax_tree/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ def [](byteindex)
end
end

# This represents all of the tokens coming back from the lexer. It is
# replacing a simple array because it keeps track of the last deleted token
# from the list for better error messages.
class TokenList < SimpleDelegator
attr_reader :last_deleted

def initialize(object)
super
@last_deleted = nil
end

def delete(value)
@last_deleted = super || @last_deleted
end

def delete_at(index)
@last_deleted = super
end
end

# [String] the source being parsed
attr_reader :source

Expand Down Expand Up @@ -124,7 +144,7 @@ def initialize(source, *)
# Most of the time, when a parser event consumes one of these events, it
# will be deleted from the list. So ideally, this list stays pretty short
# over the course of parsing a source string.
@tokens = []
@tokens = TokenList.new([])

# Here we're going to build up a list of SingleByteString or
# MultiByteString objects. They're each going to represent a string in the
Expand Down Expand Up @@ -174,6 +194,33 @@ def current_column
line[column].to_i - line.start
end

# Returns the current location that is being looked at for the parser for
# the purpose of locating the error.
def find_token_error(location)
if location
# If we explicitly passed a location into this find_token_error method,
# that means that's the source of the error, so we'll use that
# information for our error object.
lineno = location.start_line
[lineno, location.start_char - line_counts[lineno - 1].start]
elsif lineno && column
# If there is a line number associated with the current ripper state,
# then we'll use that information to generate the error.
[lineno, column]
elsif (location = tokens.last_deleted&.location)
# If we've already deleted a token from the list of tokens that we are
# consuming, then we'll fall back to that token's location.
lineno = location.start_line
[lineno, location.start_char - line_counts[lineno - 1].start]
else
# Finally, it's possible that when we hit this error the parsing thread
# for ripper has died. In that case, lineno and column both return nil.
# So we're just going to set it to line 1, column 0 in the hopes that
# that makes any sense.
[1, 0]
end
end

# As we build up a list of tokens, we'll periodically need to go backwards
# and find the ones that we've already hit in order to determine the
# location information for nodes that use them. For example, if you have a
Expand Down Expand Up @@ -201,14 +248,7 @@ def find_token(type, value = :any, consume: true, location: nil)
unless index
token = value == :any ? type.name.split("::", 2).last : value
message = "Cannot find expected #{token}"

if location
lineno = location.start_line
column = location.start_char - line_counts[lineno - 1].start
raise ParseError.new(message, lineno, column)
else
raise ParseError.new(message, lineno, column)
end
raise ParseError.new(message, *find_token_error(location))
end

tokens.delete_at(index)
Expand Down
8 changes: 7 additions & 1 deletion test/parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ def test_parses_ripper_methods
end

def test_errors_on_missing_token_with_location
assert_raises(Parser::ParseError) { SyntaxTree.parse("\"foo") }
error = assert_raises(Parser::ParseError) { SyntaxTree.parse("f+\"foo") }
assert_equal(2, error.column)
end

def test_errors_on_missing_end_with_location
error = assert_raises(Parser::ParseError) { SyntaxTree.parse("foo do 1") }
assert_equal(4, error.column)
end

def test_errors_on_missing_token_without_location
Expand Down