Skip to content

add a fuzz test for zig fmt and fix two bugs #23793

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
172 changes: 172 additions & 0 deletions lib/std/zig/parser_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6107,6 +6107,47 @@ test "zig fmt: indentation of comments within catch, else, orelse" {
);
}

test "zig fmt: seperate errors in error sets with comments" {
try testTransform(
\\error{
\\ /// This error is very bad!
\\ A, B}
\\
,
\\error{
\\ /// This error is very bad!
\\ A,
\\ B,
\\}
\\
);

try testTransform(
\\error{
\\ A, B
\\ // something important
\\}
\\
,
\\error{
\\ A,
\\ B,
\\ // something important
\\}
\\
);
}

test "zig fmt: proper escape checks" {
try testTransform(
\\@"\x41\x42\!"
\\
,
\\@"AB\!"
\\
);
}

test "recovery: top level" {
try testError(
\\test "" {inline}
Expand Down Expand Up @@ -6528,3 +6569,134 @@ fn testError(source: [:0]const u8, expected_errors: []const Error) !void {
try std.testing.expectEqual(expected, tree.errors[i].tag);
}
}

test "fuzz zig fmt" {
try std.testing.fuzz({}, fuzzTestOneRender, .{});
}

fn parseTokens(
fba: std.mem.Allocator,
source: [:0]const u8,
) error{ Invalid, OutOfMemory }!struct {
toks: std.zig.Ast.TokenList,
maybe_rewriteable: bool,
} {
@disableInstrumentation();
// Byte-order marker can be stripped
var maybe_rewriteable: bool = std.mem.startsWith(u8, source, "\xEF\xBB\xBF");

var tokens: std.zig.Ast.TokenList = .{};
try tokens.ensureTotalCapacity(fba, source.len / 2);
var tokenizer: std.zig.Tokenizer = .init(source);
while (true) {
const tok = tokenizer.next();
switch (tok.tag) {
.invalid,
.invalid_periodasterisks,
=> return error.Invalid,
// Extra colons can be removed
.keyword_asm,
// Qualifiers can be reordered
// keyword_const is intentionally excluded since it is used in other contexts and
// having only one qualifier will never lead to reordering.
.keyword_addrspace,
.keyword_align,
.keyword_allowzero,
.keyword_callconv,
.keyword_linksection,
.keyword_volatile,
=> maybe_rewriteable = true,
// Labeled statements can sometimes be (questionably) rewritten due to ambigous grammer
// ex: `O: for (x) |T| (break O: T)` -> `O: O: for (x) |T| (break :O T)`
.keyword_for,
.keyword_while,
.l_brace,
=> {
const tags = tokens.items(.tag);
maybe_rewriteable = maybe_rewriteable or (tags.len >= 2 and
tags[tags.len - 2] == .identifier and tags[tags.len - 1] == .colon);
},
// #23754
.container_doc_comment => maybe_rewriteable = true,
// Quoted identifiers can be unquoted
.identifier => maybe_rewriteable = maybe_rewriteable or source[tok.loc.start] == '@',
else => {},
}
try tokens.append(fba, .{
.tag = tok.tag,
.start = @intCast(tok.loc.start),
});
if (tok.tag == .eof) break;
}
return .{
.toks = tokens,
.maybe_rewriteable = maybe_rewriteable,
};
}

fn parseAstFromTokens(
fba: std.mem.Allocator,
source: [:0]const u8,
toks: std.zig.Ast.TokenList,
) error{OutOfMemory}!std.zig.Ast {
var parser: @import("Parse.zig") = .{
.source = source,
.gpa = fba,
.tokens = toks.slice(),
.errors = .{},
.nodes = .{},
.extra_data = .{},
.scratch = .{},
.tok_i = 0,
};
try parser.nodes.ensureTotalCapacity(fba, 1 + toks.len / 2);
try parser.parseRoot();
return .{
.source = source,
.mode = .zig,
.tokens = parser.tokens,
.nodes = parser.nodes.slice(),
.extra_data = parser.extra_data.items,
.errors = parser.errors.items,
};
}

/// Checks equivelence of non-whitespace characters
/// If there are commas in `bytes`, then it is checked they are also present in `rendered`. Extra
/// commas in `rendered` are considered equivelent.
fn isRewritten(bytes: []const u8, rendered: []const u8) bool {
@disableInstrumentation();
var i: usize = 0;
for (bytes) |c| switch (c) {
' ', '\r', '\t', '\n' => {},
else => while (true) {
if (i == rendered.len) return true;
defer i += 1;
switch (rendered[i]) {
' ', '\r', '\n' => {},
',' => if (c == ',') break,
else => |n| if (c != n) return false else break,
}
},
};
for (rendered[i..]) |c| switch (c) {
' ', '\n', ',' => {},
else => return true,
};
return false;
}

fn fuzzTestOneRender(_: void, bytes: []const u8) anyerror!void {
if (bytes.len < 2) return;
const mem_limit: u16 = @bitCast(bytes[0..2].*);

var fba_ctx = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..mem_limit]);
const fba = fba_ctx.allocator();
const source = fba.dupeZ(u8, bytes[2..]) catch return;
const toks = parseTokens(fba, source) catch return;
const tree = parseAstFromTokens(fba, source, toks.toks) catch return;
if (tree.errors.len != 0) return;

const rendered = tree.render(fba) catch return;
if (!toks.maybe_rewriteable and isRewritten(source, rendered)) return error.TestFailed;
}
75 changes: 44 additions & 31 deletions lib/std/zig/render.zig
Original file line number Diff line number Diff line change
Expand Up @@ -753,39 +753,52 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
try renderToken(r, lbrace, .none);
try renderIdentifier(r, lbrace + 1, .none, .eagerly_unquote); // identifier
return renderToken(r, rbrace, space);
} else if (tree.tokenTag(rbrace - 1) == .comma) {
// There is a trailing comma so render each member on a new line.
try ais.pushIndent(.normal);
try renderToken(r, lbrace, .newline);
var i = lbrace + 1;
while (i < rbrace) : (i += 1) {
if (i > lbrace + 1) try renderExtraNewlineToken(r, i);
switch (tree.tokenTag(i)) {
.doc_comment => try renderToken(r, i, .newline),
.identifier => {
try ais.pushSpace(.comma);
try renderIdentifier(r, i, .comma, .eagerly_unquote);
ais.popSpace();
},
.comma => {},
else => unreachable,
}
}
ais.popIndent();
return renderToken(r, rbrace, space);
} else {
// There is no trailing comma so render everything on one line.
try renderToken(r, lbrace, .space);
var i = lbrace + 1;
while (i < rbrace) : (i += 1) {
switch (tree.tokenTag(i)) {
.doc_comment => unreachable, // TODO
.identifier => try renderIdentifier(r, i, .comma_space, .eagerly_unquote),
.comma => {},
else => unreachable,
// If there is a trailing comma, comment, or document comment, then render each
// item on its own line.
const multi_line = tree.tokenTag(rbrace - 1) == .comma or
hasComment(tree, lbrace, rbrace) or
blk: {
var i = lbrace + 1;
break :blk while (i < rbrace) : (i += 1) {
if (tree.tokenTag(i) == .doc_comment)
break true;
} else false;
};

if (multi_line) {
// There is a trailing comma so render each member on a new line.
try ais.pushIndent(.normal);
try renderToken(r, lbrace, .newline);
var i = lbrace + 1;
while (i < rbrace) : (i += 1) {
if (i > lbrace + 1) try renderExtraNewlineToken(r, i);
switch (tree.tokenTag(i)) {
.doc_comment => try renderToken(r, i, .newline),
.identifier => {
try ais.pushSpace(.comma);
try renderIdentifier(r, i, .comma, .eagerly_unquote);
ais.popSpace();
},
.comma => {},
else => unreachable,
}
}
ais.popIndent();
return renderToken(r, rbrace, space);
} else {
// There is no trailing comma so render everything on one line.
try renderToken(r, lbrace, .space);
var i = lbrace + 1;
while (i < rbrace) : (i += 1) {
switch (tree.tokenTag(i)) {
.identifier => try renderIdentifier(r, i, .comma_space, .eagerly_unquote),
.comma => {},
else => unreachable,
}
}
return renderToken(r, rbrace, space);
}
return renderToken(r, rbrace, space);
}
},

Expand Down Expand Up @@ -2808,7 +2821,7 @@ fn renderIdentifier(r: *Render, token_index: Ast.TokenIndex, space: Space, quote
},
.failure => return renderQuotedIdentifier(r, token_index, space, false),
}
contents_i += esc_offset;
contents_i = esc_offset;
continue;
},
else => return renderQuotedIdentifier(r, token_index, space, false),
Expand Down
Loading