Commit 293603f040

Ian Johnson <ian@ianjohnson.dev>
2025-02-15 21:59:32
Autodoc: report syntax errors to user
Additionally, this commit streamlines the way unparseable files are handled, by giving them the AST of an empty file. This avoids bugs in the rest of the Autodoc logic trying to work with invalid ASTs.
1 parent e5174c7
Changed files (1)
lib
docs
lib/docs/wasm/Walk.zig
@@ -406,15 +406,11 @@ pub const ModuleIndex = enum(u32) {
 };
 
 pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
-    const ast = try parse(bytes);
+    const ast = try parse(file_name, bytes);
+    assert(ast.errors.len == 0);
     const file_index: File.Index = @enumFromInt(files.entries.len);
     try files.put(gpa, file_name, .{ .ast = ast });
 
-    if (ast.errors.len > 0) {
-        log.err("can't index '{s}' because it has syntax errors", .{file_index.path()});
-        return file_index;
-    }
-
     var w: Walk = .{
         .file = file_index,
     };
@@ -434,20 +430,41 @@ pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
     return file_index;
 }
 
-fn parse(source: []u8) Oom!Ast {
+/// Parses a file and returns its `Ast`. If the file cannot be parsed, returns
+/// the `Ast` of an empty file, so that the rest of the Autodoc logic does not
+/// need to handle parse errors.
+fn parse(file_name: []const u8, source: []u8) Oom!Ast {
     // Require every source file to end with a newline so that Zig's tokenizer
     // can continue to require null termination and Autodoc implementation can
     // avoid copying source bytes from the decompressed tar file buffer.
     const adjusted_source: [:0]const u8 = s: {
         if (source.len == 0)
             break :s "";
-
-        assert(source[source.len - 1] == '\n');
+        if (source[source.len - 1] != '\n') {
+            log.err("{s}: expected newline at end of file", .{file_name});
+            break :s "";
+        }
         source[source.len - 1] = 0;
         break :s source[0 .. source.len - 1 :0];
     };
 
-    return Ast.parse(gpa, adjusted_source, .zig);
+    var ast = try Ast.parse(gpa, adjusted_source, .zig);
+    if (ast.errors.len > 0) {
+        defer ast.deinit(gpa);
+
+        const token_offsets = ast.tokens.items(.start);
+        var rendered_err: std.ArrayListUnmanaged(u8) = .{};
+        defer rendered_err.deinit(gpa);
+        for (ast.errors) |err| {
+            const err_offset = token_offsets[err.token] + ast.errorOffset(err);
+            const err_loc = std.zig.findLineColumn(ast.source, err_offset);
+            rendered_err.clearRetainingCapacity();
+            try ast.renderError(err, rendered_err.writer(gpa));
+            log.err("{s}:{}:{}: {s}", .{ file_name, err_loc.line + 1, err_loc.column + 1, rendered_err.items });
+        }
+        return Ast.parse(gpa, "", .zig);
+    }
+    return ast;
 }
 
 pub const Scope = struct {