Commit da75eb0d79

r00ster91 <r00ster91@proton.me>
2022-07-11 23:10:39
Compilation: indent multiline error messages properly
Co-authored-by: Veikka Tuominen <git@vexu.eu>
1 parent ade9bd9
src/Compilation.zig
@@ -344,6 +344,20 @@ pub const AllErrors = struct {
             /// Does not include the trailing newline.
             source_line: ?[]const u8,
             notes: []Message = &.{},
+
+            /// Splits the error message up into lines to properly indent them
+            /// to allow for long, good-looking error messages.
+            ///
+            /// This is used to split the message in `@compileError("hello\nworld")` for example.
+            fn writeMsg(src: @This(), stderr: anytype, indent: usize) !void {
+                var lines = mem.split(u8, src.msg, "\n");
+                while (lines.next()) |line| {
+                    try stderr.writeAll(line);
+                    if (lines.index == null) break;
+                    try stderr.writeByte('\n');
+                    try stderr.writeByteNTimes(' ', indent);
+                }
+            }
         },
         plain: struct {
             msg: []const u8,
@@ -367,35 +381,41 @@ pub const AllErrors = struct {
             std.debug.getStderrMutex().lock();
             defer std.debug.getStderrMutex().unlock();
             const stderr = std.io.getStdErr();
-            return msg.renderToStdErrInner(ttyconf, stderr, "error:", .Red, 0) catch return;
+            return msg.renderToWriter(ttyconf, stderr.writer(), "error", .Red, 0) catch return;
         }
 
-        fn renderToStdErrInner(
+        pub fn renderToWriter(
             msg: Message,
             ttyconf: std.debug.TTY.Config,
-            stderr_file: std.fs.File,
+            stderr: anytype,
             kind: []const u8,
             color: std.debug.TTY.Color,
             indent: usize,
         ) anyerror!void {
-            const stderr = stderr_file.writer();
+            var counting_writer = std.io.countingWriter(stderr);
+            const counting_stderr = counting_writer.writer();
             switch (msg) {
                 .src => |src| {
-                    try stderr.writeByteNTimes(' ', indent);
+                    try counting_stderr.writeByteNTimes(' ', indent);
                     ttyconf.setColor(stderr, .Bold);
-                    try stderr.print("{s}:{d}:{d}: ", .{
+                    try counting_stderr.print("{s}:{d}:{d}: ", .{
                         src.src_path,
                         src.line + 1,
                         src.column + 1,
                     });
                     ttyconf.setColor(stderr, color);
-                    try stderr.writeAll(kind);
+                    try counting_stderr.writeAll(kind);
+                    try counting_stderr.writeAll(": ");
+                    // This is the length of the part before the error message:
+                    // e.g. "file.zig:4:5: error: "
+                    const prefix_len = @intCast(usize, counting_stderr.context.bytes_written);
                     ttyconf.setColor(stderr, .Reset);
                     ttyconf.setColor(stderr, .Bold);
                     if (src.count == 1) {
-                        try stderr.print(" {s}\n", .{src.msg});
+                        try src.writeMsg(stderr, prefix_len);
+                        try stderr.writeByte('\n');
                     } else {
-                        try stderr.print(" {s}", .{src.msg});
+                        try src.writeMsg(stderr, prefix_len);
                         ttyconf.setColor(stderr, .Dim);
                         try stderr.print(" ({d} times)\n", .{src.count});
                     }
@@ -414,24 +434,25 @@ pub const AllErrors = struct {
                         }
                     }
                     for (src.notes) |note| {
-                        try note.renderToStdErrInner(ttyconf, stderr_file, "note:", .Cyan, indent);
+                        try note.renderToWriter(ttyconf, stderr, "note", .Cyan, indent);
                     }
                 },
                 .plain => |plain| {
                     ttyconf.setColor(stderr, color);
                     try stderr.writeByteNTimes(' ', indent);
                     try stderr.writeAll(kind);
+                    try stderr.writeAll(": ");
                     ttyconf.setColor(stderr, .Reset);
                     if (plain.count == 1) {
-                        try stderr.print(" {s}\n", .{plain.msg});
+                        try stderr.print("{s}\n", .{plain.msg});
                     } else {
-                        try stderr.print(" {s}", .{plain.msg});
+                        try stderr.print("{s}", .{plain.msg});
                         ttyconf.setColor(stderr, .Dim);
                         try stderr.print(" ({d} times)\n", .{plain.count});
                     }
                     ttyconf.setColor(stderr, .Reset);
                     for (plain.notes) |note| {
-                        try note.renderToStdErrInner(ttyconf, stderr_file, "error:", .Red, indent + 4);
+                        try note.renderToWriter(ttyconf, stderr, "error", .Red, indent + 4);
                     }
                 },
             }
src/test.zig
@@ -1690,12 +1690,25 @@ pub const TestContext = struct {
                                         tmp_dir_path_plus_slash,
                                     );
 
+                                    var buf: [1024]u8 = undefined;
+                                    const rendered_msg = blk: {
+                                        var msg: Compilation.AllErrors.Message = actual_error;
+                                        msg.src.src_path = case_msg.src.src_path;
+                                        msg.src.notes = &.{};
+                                        var fib = std.io.fixedBufferStream(&buf);
+                                        try msg.renderToWriter(.no_color, fib.writer(), "error", .Red, 0);
+                                        var it = std.mem.split(u8, fib.getWritten(), "error: ");
+                                        _ = it.next();
+                                        const rendered = it.rest();
+                                        break :blk rendered[0 .. rendered.len - 1]; // trim final newline
+                                    };
+
                                     if (src_path_ok and
                                         (case_msg.src.line == std.math.maxInt(u32) or
                                         actual_msg.line == case_msg.src.line) and
                                         (case_msg.src.column == std.math.maxInt(u32) or
                                         actual_msg.column == case_msg.src.column) and
-                                        std.mem.eql(u8, expected_msg, actual_msg.msg) and
+                                        std.mem.eql(u8, expected_msg, rendered_msg) and
                                         case_msg.src.kind == .@"error" and
                                         actual_msg.count == case_msg.src.count)
                                     {
test/compile_errors.zig
@@ -138,6 +138,42 @@ pub fn addCases(ctx: *TestContext) !void {
         "tmp.zig:2:1: error: invalid character: '\\t'",
     });
 
+    {
+        const case = ctx.obj("multiline error messages", .{});
+        case.backend = .stage2;
+
+        case.addError(
+            \\comptime {
+            \\    @compileError("hello\nworld");
+            \\}
+        , &[_][]const u8{
+            \\:2:5: error: hello
+            \\             world
+        });
+
+        case.addError(
+            \\comptime {
+            \\    @compileError(
+            \\        \\
+            \\        \\hello!
+            \\        \\I'm a multiline error message.
+            \\        \\I hope to be very useful!
+            \\        \\
+            \\        \\also I will leave this trailing newline here if you don't mind
+            \\        \\
+            \\    );
+            \\}
+        , &[_][]const u8{
+            \\:2:5: error: 
+            \\             hello!
+            \\             I'm a multiline error message.
+            \\             I hope to be very useful!
+            \\             
+            \\             also I will leave this trailing newline here if you don't mind
+            \\             
+        });
+    }
+
     // TODO test this in stage2, but we won't even try in stage1
     //ctx.objErrStage1("inline fn calls itself indirectly",
     //    \\export fn foo() void {