Skip to content

Commit

Permalink
parser: detect mismatched/misleading indentation
Browse files Browse the repository at this point in the history
Closes #35
  • Loading branch information
hryx committed Jan 29, 2023
1 parent 23b7d28 commit 7101c8c
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 1 deletion.
14 changes: 14 additions & 0 deletions lib/std/zig/Ast.zig
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,16 @@ pub fn renderError(tree: Ast, parse_error: Error, stream: anytype) !void {
}),
}
},

.mismatched_indentation => {
return stream.writeAll("statement indentation mismatched with siblings");
},
.outdented_block => {
return stream.writeAll("block is outdented from surrounding block");
},
.previous_indentation => {
return stream.writeAll("previous indentation level set here");
},
}
}

Expand Down Expand Up @@ -2748,6 +2758,10 @@ pub const Error = struct {
wrong_equal_var_decl,
var_const_decl,

mismatched_indentation,
outdented_block,
previous_indentation,

zig_style_container,
previous_field,
next_field,
Expand Down
64 changes: 64 additions & 0 deletions lib/std/zig/parse.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub fn parse(gpa: Allocator, source: [:0]const u8) Allocator.Error!Ast {
.extra_data = .{},
.scratch = .{},
.tok_i = 0,
.current_block_indent_defining_token = 0,
};
defer parser.errors.deinit(gpa);
defer parser.nodes.deinit(gpa);
Expand Down Expand Up @@ -75,6 +76,7 @@ const Parser = struct {
nodes: Ast.NodeList,
extra_data: std.ArrayListUnmanaged(Node.Index),
scratch: std.ArrayListUnmanaged(Node.Index),
current_block_indent_defining_token: TokenIndex,

const SmallSpan = union(enum) {
zero_or_one: Node.Index,
Expand Down Expand Up @@ -251,6 +253,8 @@ const Parser = struct {
///
/// ComptimeDecl <- KEYWORD_comptime Block
fn parseContainerMembers(p: *Parser) !Members {
p.current_block_indent_defining_token = 0;

const scratch_top = p.scratch.items.len;
defer p.scratch.shrinkRetainingCapacity(scratch_top);

Expand Down Expand Up @@ -2044,15 +2048,64 @@ const Parser = struct {
/// Block <- LBRACE Statement* RBRACE
fn parseBlock(p: *Parser) !Node.Index {
const lbrace = p.eatToken(.l_brace) orelse return null_node;

const scratch_top = p.scratch.items.len;
defer p.scratch.shrinkRetainingCapacity(scratch_top);

const parent_block_tok = p.current_block_indent_defining_token;
defer p.current_block_indent_defining_token = parent_block_tok;
const parent_indent = p.tokenColumn(parent_block_tok);

var prev_tok = lbrace;
var sibling: ?struct { indent: u32, tok: TokenIndex } = null;
while (true) {
if (p.token_tags[p.tok_i] == .r_brace) break;

// In the case that this statement opens a block (if, while, comptime, etc.),
// track the first token so the error message points to the offending position.
const stmt_tok = p.tok_i;
p.current_block_indent_defining_token = stmt_tok;

const statement = try p.expectStatementRecoverable();
if (statement == 0) break;

if (!p.tokensOnSameLine(prev_tok, stmt_tok)) {
const col = p.tokenColumn(stmt_tok);
if (sibling) |sib| {
// Indentation mismatched with siblings
if (col != sib.indent) {
try p.warnMsg(.{
.tag = .mismatched_indentation,
.token = stmt_tok,
});
try p.warnMsg(.{
.tag = .previous_indentation,
.is_note = true,
.token = sib.tok,
});
}
} else if (col < parent_indent) {
// New block outdented from parent
try p.warnMsg(.{
.tag = .outdented_block,
.token = stmt_tok,
});
try p.warnMsg(.{
.tag = .previous_indentation,
.is_note = true,
.token = parent_block_tok,
});
} else {
// First statement on a new line defines the sibling indent level
sibling = .{ .indent = p.tokenColumn(stmt_tok), .tok = stmt_tok };
}
}
prev_tok = stmt_tok;

try p.scratch.append(p.gpa, statement);
}
_ = try p.expectToken(.r_brace);

const semicolon = (p.token_tags[p.tok_i - 2] == .semicolon);
const statements = p.scratch.items[scratch_top..];
switch (statements.len) {
Expand Down Expand Up @@ -3810,6 +3863,17 @@ const Parser = struct {
return std.mem.indexOfScalar(u8, p.source[p.token_starts[token1]..p.token_starts[token2]], '\n') == null;
}

fn tokenColumn(p: *Parser, token: TokenIndex) u32 {
var i = p.token_starts[token];
while (i > 0) : (i -= 1) {
if (p.source[i] == '\n') {
i += 1;
break;
}
}
return p.token_starts[token] - i;
}

fn eatToken(p: *Parser, tag: Token.Tag) ?TokenIndex {
return if (p.token_tags[p.tok_i] == tag) p.nextToken() else null;
}
Expand Down
117 changes: 116 additions & 1 deletion lib/std/zig/parser_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2438,7 +2438,7 @@ test "zig fmt: add comma on last switch prong" {
\\ InitArg.None,
\\ InitArg.Enum => { }
\\}
\\ switch (self.init_arg_expr) {
\\switch (self.init_arg_expr) {
\\ InitArg.Type => |t| { },
\\ InitArg.None,
\\ InitArg.Enum => { }//line comment
Expand Down Expand Up @@ -6053,6 +6053,115 @@ test "ampersand" {
, &.{});
}

test "misleading indentation" {
try testError(
\\fn f() void {
\\ if (x)
\\ gotoFail();
\\ gotoFail();
\\}
, &[_]Error{
.mismatched_indentation,
.previous_indentation,
});
try testError(
\\fn f() void {
\\ ok();
\\no = 2;
\\}
, &[_]Error{
.mismatched_indentation,
.previous_indentation,
});
try testError(
\\fn f() void {
\\ ok();
\\ { no = 1; }
\\}
, &[_]Error{
.mismatched_indentation,
.previous_indentation,
});
try testError(
\\comptime {
\\ ok = 1;
\\ no = 2;
\\}
, &[_]Error{
.mismatched_indentation,
.previous_indentation,
});
try testError(
\\fn f() void {
\\ if (x) {
\\ no = 1;
\\ }
\\}
, &[_]Error{
.outdented_block,
.previous_indentation,
});
try testError(
\\fn f() void {
\\ { {
\\ _ = 1;
\\ } }
\\}
, &[_]Error{
.outdented_block,
.previous_indentation,
});
// Nested in a sub-container
try testError(
\\fn f() void {
\\ const T = struct {
\\ fn g() void {
\\ ok = 1;
\\ no = 2;
\\ }
\\ };
\\}
, &[_]Error{
.mismatched_indentation,
.previous_indentation,
});
}

test "acceptable indentation" {
// Same line as { do not define indentation level
try testNoError(
\\fn f() void { _ = 1; _ = 2; }
);
try testNoError(
\\fn f() void { _ = 1;
\\ _ = 2; _ = 3;
\\ { _ = 4; }
\\}
);
try testNoError(
\\fn f() void {
\\if (x) {
\\_ = 1;
\\}
\\}
);
// Container level decls do not matter
try testNoError(
\\const x = true;
\\ const y = false;
);
// Containers have independent indentation levels
try testNoError(
\\fn f() void {
\\ const T = struct {
\\ fn g() void {
\\ _ = 1;
\\ }
\\ };
\\}
);
}

const std = @import("std");
const mem = std.mem;
const print = std.debug.print;
Expand Down Expand Up @@ -6126,3 +6235,9 @@ fn testError(source: [:0]const u8, expected_errors: []const Error) !void {
try std.testing.expectEqual(expected, tree.errors[i].tag);
}
}

fn testNoError(source: [:0]const u8) !void {
var fba = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..]);
var b: bool = undefined;
_ = try testParse(source, fba.allocator(), &b);
}
14 changes: 14 additions & 0 deletions test/cases/compile_errors/mismatched_indentation.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
fn f() void {
var x = false;
if (x)
gotoFail();
gotoFail();
}
fn gotoFail() void {}

// error
// backend=stage2
// target=native
//
// :5:9: error: statement indentation mismatched with siblings
// :2:5: note: previous indentation level set here
10 changes: 10 additions & 0 deletions test/cases/compile_errors/outdented_block.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
comptime {
_ = "outdented";
}

// error
// backend=stage2
// target=native
//
// :2:1: error: block is outdented from surrounding block
// :1:2: note: previous indentation level set here

0 comments on commit 7101c8c

Please sign in to comment.