diff --git a/doc/langref/grammar.peg b/doc/langref/grammar.peg index 83f75509cac2d5649a47ab3a506c4a24919b68b5..5053da2c0bf135addfd995d1f56d8b2dae8bacbc 100644 --- a/doc/langref/grammar.peg +++ b/doc/langref/grammar.peg @@ -1,4 +1,4 @@ -Root <- ContainerMembers skip eof +Root <- initial_doc_comment? ContainerMembers skip eof # *** Top level *** ContainerMembers <- container_doc_comment? ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*) @@ -446,14 +446,20 @@ string_char <- '\\"' / !["] non_control_utf8 + +# We forbid same-line doc comments to disambiguate the mapping to e.g. struct +# fields for documentation generation tooling. However, this requires a special +# case in the grammar to handle a doc comment right at the beginning of a file. +initial_doc_comment <- (skip '///' non_control_utf8* newline)+ skip !'//!' !KEYWORD_test !KEYWORD_comptime !eof container_doc_comment <- (skip '//!' non_control_utf8* newline)+ -doc_comment <- (skip '///' non_control_utf8* newline)+ +doc_comment <- skip_require_newline (skip '///' non_control_utf8* newline)+ line_comment <- '//' ![!/] non_control_utf8* newline / '////' non_control_utf8* newline line_string <- '\\\\' non_control_utf8* newline newline <- "\n" / "\r\n" / eof skip <- ([ \n\t\r] / line_comment)* +skip_require_newline <- [ \t\r]* ([\n] / line_comment) skip pre_op_white <- ([ \n\t\r] / line_comment)+ post_op_white <- [ \n\t\r] skip diff --git a/lib/std/zig/parser_fuzz.zig b/lib/std/zig/parser_fuzz.zig index c226dfe58dda7375d4407951161466ec7e8419ab..cd6e7ea7dad1c3978fc3c022d19bef0b085c2c27 100644 --- a/lib/std/zig/parser_fuzz.zig +++ b/lib/std/zig/parser_fuzz.zig @@ -25,6 +25,7 @@ test "operator whitespace" { \\} ); } + // Found using AFL++ test "doc comment or division operator" { try checkAgainstOracle("0=0///\n0"); @@ -36,6 +37,17 @@ test "double ampersand" { try checkAgainstOracle("test{&&0;}"); // ok } +// Found using AFL++ +test "newline required before doc comment not at start of file" { + try checkAgainstOracle("0,///\n0"); // error + try checkAgainstOracle("///\n0"); // ok + try checkAgainstOracle(" ///\n0"); // ok + try checkAgainstOracle("\n///\n0"); // ok + try checkAgainstOracle("///"); // error + try checkAgainstOracle("///\n//!"); + try checkAgainstOracle("///\ntest {}"); +} + fn checkAgainstOracle(source: [:0]const u8) !void { var fba_buf: [1 << 18]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&fba_buf); diff --git a/lib/std/zig/parser_generated_oracle.zig b/lib/std/zig/parser_generated_oracle.zig index 7be3e2e4a15d99b287f33d1553f10e1ec5fff176..4f775052cd2ccda652c4ed14d9dc50afb40fb5e9 100644 --- a/lib/std/zig/parser_generated_oracle.zig +++ b/lib/std/zig/parser_generated_oracle.zig @@ -16,7 +16,7 @@ const Parser = struct { pub fn parseRoot(p: *Parser) bool { return blk_0: { const pos_0 = p.i; - if (p.parseContainerMembers() and p.parseskip() and p.parseeof()) break :blk_0 true; + if ((p.parseinitial_doc_comment() or true) and p.parseContainerMembers() and p.parseskip() and p.parseeof()) break :blk_0 true; p.i = pos_0; break :blk_0 false; }; @@ -2330,6 +2330,60 @@ const Parser = struct { break :blk_0 false; }; } + pub fn parseinitial_doc_comment(p: *Parser) bool { + return blk_0: { + const pos_0 = p.i; + if (blk_1: { + var match_1 = false; + while (blk_3: { + const pos_3 = p.i; + if (p.parseskip() and blk_4: { + if (std.mem.startsWith(u8, p.source[p.i..], "///")) { + p.i += 3; + break :blk_4 true; + } + break :blk_4 false; + } and blk_4: { + while (p.parsenon_control_utf8()) {} + break :blk_4 true; + } and p.parsenewline()) break :blk_3 true; + p.i = pos_3; + break :blk_3 false; + }) { + match_1 = true; + } + break :blk_1 match_1; + } and p.parseskip() and blk_1: { + const pos_1 = p.i; + const match_1 = blk_2: { + if (std.mem.startsWith(u8, p.source[p.i..], "//!")) { + p.i += 3; + break :blk_2 true; + } + break :blk_2 false; + }; + p.i = pos_1; + break :blk_1 !match_1; + } and blk_1: { + const pos_1 = p.i; + const match_1 = p.parseKEYWORD_test(); + p.i = pos_1; + break :blk_1 !match_1; + } and blk_1: { + const pos_1 = p.i; + const match_1 = p.parseKEYWORD_comptime(); + p.i = pos_1; + break :blk_1 !match_1; + } and blk_1: { + const pos_1 = p.i; + const match_1 = p.parseeof(); + p.i = pos_1; + break :blk_1 !match_1; + }) break :blk_0 true; + p.i = pos_0; + break :blk_0 false; + }; + } pub fn parsecontainer_doc_comment(p: *Parser) bool { return blk_0: { const pos_0 = p.i; @@ -2361,7 +2415,7 @@ const Parser = struct { pub fn parsedoc_comment(p: *Parser) bool { return blk_0: { const pos_0 = p.i; - if (blk_1: { + if (p.parseskip_require_newline() and blk_1: { var match_1 = false; while (blk_3: { const pos_3 = p.i; @@ -2496,6 +2550,40 @@ const Parser = struct { break :blk_0 false; }; } + pub fn parseskip_require_newline(p: *Parser) bool { + return blk_0: { + const pos_0 = p.i; + if (blk_1: { + while ((p.i < p.source.len and switch (p.source[p.i]) { + ' '...' ', + '\t'...'\t', + '\r'...'\r', + => blk_2: { + p.i += 1; + break :blk_2 true; + }, + else => false, + })) {} + break :blk_1 true; + } and blk_2: { + const pos_2 = p.i; + if ((p.i < p.source.len and switch (p.source[p.i]) { + '\n'...'\n', + => blk_3: { + p.i += 1; + break :blk_3 true; + }, + else => false, + })) break :blk_2 true; + p.i = pos_2; + if (p.parseline_comment()) break :blk_2 true; + p.i = pos_2; + break :blk_2 false; + } and p.parseskip()) break :blk_0 true; + p.i = pos_0; + break :blk_0 false; + }; + } pub fn parsepre_op_white(p: *Parser) bool { return blk_0: { const pos_0 = p.i;