improve recovery from invalid container members

Instead of trying to find the end of the block or the next comma/semicolon
we no try to find the next token that can start a container member.
master
Vexu 2020-05-14 12:09:40 +03:00
parent 89f2923a8a
commit a32e240540
No known key found for this signature in database
GPG Key ID: 59AEB8936E16A6AC
2 changed files with 99 additions and 20 deletions

View File

@ -59,10 +59,13 @@ fn parseRoot(arena: *Allocator, it: *TokenIterator, tree: *Tree) Allocator.Error
node.* = .{
.decls = try parseContainerMembers(arena, it, tree),
.eof_token = eatToken(it, .Eof) orelse blk: {
// parseContainerMembers will try to skip as much
// invalid tokens as it can so this can only be a '}'
const tok = eatToken(it, .RBrace).?;
try tree.errors.push(.{
.ExpectedContainerMembers = .{ .token = it.index },
.ExpectedContainerMembers = .{ .token = tok },
});
break :blk undefined;
break :blk tok;
},
};
return node;
@ -101,7 +104,7 @@ fn parseContainerMembers(arena: *Allocator, it: *TokenIterator, tree: *Tree) All
if (parseTestDecl(arena, it, tree) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
findEndOfBlock(it);
findNextContainerMember(it);
continue;
},
}) |node| {
@ -116,7 +119,7 @@ fn parseContainerMembers(arena: *Allocator, it: *TokenIterator, tree: *Tree) All
if (parseTopLevelComptime(arena, it, tree) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
findEndOfBlock(it);
findNextContainerMember(it);
continue;
},
}) |node| {
@ -178,8 +181,8 @@ fn parseContainerMembers(arena: *Allocator, it: *TokenIterator, tree: *Tree) All
if (parseContainerField(arena, it, tree) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
// attempt to recover by finding a comma
findToken(it, .Comma);
// attempt to recover
findNextContainerMember(it);
continue;
},
}) |node| {
@ -198,7 +201,21 @@ fn parseContainerMembers(arena: *Allocator, it: *TokenIterator, tree: *Tree) All
const field = node.cast(Node.ContainerField).?;
field.doc_comments = doc_comments;
try list.push(node);
const comma = eatToken(it, .Comma) orelse break;
const comma = eatToken(it, .Comma) orelse {
// try to continue parsing
const index = it.index;
findNextContainerMember(it);
switch (it.peek().?.id) {
.Eof, .RBrace => break,
else => {
// add error and continue
try tree.errors.push(.{
.ExpectedToken = .{ .token = index, .expected_id = .Comma },
});
continue;
}
}
};
if (try parseAppendedDocComment(arena, it, tree, comma)) |appended_comment|
field.doc_comments = appended_comment;
continue;
@ -210,22 +227,63 @@ fn parseContainerMembers(arena: *Allocator, it: *TokenIterator, tree: *Tree) All
.UnattachedDocComment = .{ .token = doc_comments.?.firstToken() },
});
}
break;
switch (it.peek().?.id) {
.Eof, .RBrace => break,
else => {
// this was likely not supposed to end yet,
// try to find the next declaration
const index = it.index;
findNextContainerMember(it);
try tree.errors.push(.{
.ExpectedContainerMembers = .{ .token = index },
});
},
}
}
return list;
}
/// Attempts to find a closing brace.
fn findEndOfBlock(it: *TokenIterator) void {
var count: u32 = 0;
fn findNextContainerMember(it: *TokenIterator) void {
var level: u32 = 0;
while (true) {
const tok = nextToken(it);
switch (tok.ptr.id) {
.LBrace => count += 1,
.RBrace => {
if (count <= 1) return;
count -= 1;
// any of these can start a new top level declaration
.Keyword_test,
.Keyword_comptime,
.Keyword_pub,
.Keyword_export,
.Keyword_extern,
.Keyword_inline,
.Keyword_noinline,
.Keyword_usingnamespace,
.Keyword_threadlocal,
.Keyword_const,
.Keyword_var,
.Keyword_fn,
.Identifier,
=> {
if (level == 0) {
putBackToken(it, tok.index);
return;
}
},
.Comma, .Semicolon => {
// this decl was likely meant to end here
if (level == 0) {
return;
}
},
.LParen, .LBracket, .LBrace => level += 1,
.RParen, .RBracket, .RBrace => {
if (level == 0) {
// end of container, exit
putBackToken(it, tok.index);
return;
}
level -= 1;
},
.Eof => {
putBackToken(it, tok.index);
@ -338,9 +396,7 @@ fn parseTopLevelDecl(arena: *Allocator, it: *TokenIterator, tree: *Tree) Error!?
if (parseFnProto(arena, it, tree) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
// this fn will likely have a body so we
// use findEndOfBlock instead of findToken.
findEndOfBlock(it);
findNextContainerMember(it);
return error.ParseError;
},
}) |node| {
@ -381,7 +437,7 @@ fn parseTopLevelDecl(arena: *Allocator, it: *TokenIterator, tree: *Tree) Error!?
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
// try to skip to next decl
findToken(it, .Semicolon);
findNextContainerMember(it);
return error.ParseError;
},
}) |node| {
@ -413,7 +469,7 @@ fn parseTopLevelDecl(arena: *Allocator, it: *TokenIterator, tree: *Tree) Error!?
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => {
// try to skip to next decl
findToken(it, .Semicolon);
findNextContainerMember(it);
return error.ParseError;
},
};
@ -3215,6 +3271,8 @@ fn expectToken(it: *TokenIterator, tree: *Tree, id: Token.Id) Error!TokenIndex {
try tree.errors.push(.{
.ExpectedToken = .{ .token = token.index, .expected_id = id },
});
// go back so that we can recover properly
putBackToken(it, token.index);
return error.ParseError;
}
return token.index;

View File

@ -119,6 +119,25 @@ test "recovery: missing semicolon" {
});
}
test "recovery: invalid container members" {
try testError(
\\usingnamespace;
\\foo+
\\bar@,
\\while (a == 2) { test "" {}}
\\test "" {
\\ a && b
\\}
, &[_]Error{
.ExpectedExpr,
.ExpectedToken,
.ExpectedToken,
.ExpectedContainerMembers,
.InvalidAnd,
.ExpectedToken,
});
}
test "zig fmt: top-level fields" {
try testCanonical(
\\a: did_you_know,
@ -2953,6 +2972,8 @@ test "zig fmt: extern without container keyword returns error" {
\\
, &[_]Error{
.ExpectedExpr,
.ExpectedVarDeclOrFn,
.ExpectedContainerMembers,
});
}