diff --git a/src/Compilation.zig b/src/Compilation.zig index b00f135813..a0506e78e2 100644 --- a/src/Compilation.zig +++ b/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); } }, } diff --git a/src/test.zig b/src/test.zig index cba11d7a25..a1ae2fe238 100644 --- a/src/test.zig +++ b/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) { diff --git a/test/compile_errors.zig b/test/compile_errors.zig index f79d2b8e92..1337917323 100644 --- a/test/compile_errors.zig +++ b/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 {