lang F0.3: multi-message diagnostic bundling + help-blocks
Feature 0 complete. addNote/addHelp bundle notes and help-blocks under a primary diagnostic (handle from new addId/addFmtId); help blocks carry an optional fix-it line that substitutes the suggested source. renderExtended now renders primary -> notes -> helps with blank-line separators. Wire the CLI to the extended renderer (renderErrors -> renderStderr) and flip render_style default to .extended; the previous renderErrors -> renderDebug path bypassed render() entirely, so flipping the field alone was a no-op. 13 diagnostic snapshots re-rendered to the extended format.
This commit is contained in:
165
src/errors.zig
165
src/errors.zig
@@ -5,6 +5,7 @@ pub const Level = enum {
|
||||
err,
|
||||
warn,
|
||||
note,
|
||||
help,
|
||||
};
|
||||
|
||||
pub const RenderStyle = enum {
|
||||
@@ -103,6 +104,15 @@ pub const Diagnostic = struct {
|
||||
message: []const u8,
|
||||
span: ?Span,
|
||||
source_file: ?[]const u8 = null,
|
||||
|
||||
/// Index into `DiagnosticList.items` of the primary `err`/`warn` this
|
||||
/// note/help is bundled under. `null` for a standalone primary.
|
||||
primary: ?usize = null,
|
||||
|
||||
/// For a `.help`: the suggested replacement source shown on the excerpt
|
||||
/// line in place of the original code. `null` renders the help as a bare
|
||||
/// suggestion line with no code block.
|
||||
fix_code: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const DiagnosticList = struct {
|
||||
@@ -112,7 +122,7 @@ pub const DiagnosticList = struct {
|
||||
file_name: []const u8,
|
||||
current_source_file: ?[]const u8 = null,
|
||||
import_sources: ?*const std.StringHashMap([:0]const u8) = null,
|
||||
render_style: RenderStyle = .compact,
|
||||
render_style: RenderStyle = .extended,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList {
|
||||
return .{
|
||||
@@ -127,25 +137,75 @@ pub const DiagnosticList = struct {
|
||||
}
|
||||
|
||||
pub fn add(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) void {
|
||||
_ = self.addId(level, message, span);
|
||||
}
|
||||
|
||||
/// Append a primary diagnostic, returning its index in `items`. The index
|
||||
/// is the handle passed to `addNote` / `addHelp` to bundle related
|
||||
/// messages under it. On a deduplicated hit the existing index is
|
||||
/// returned, so bundling still attaches to the surviving primary.
|
||||
pub fn addId(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) usize {
|
||||
// Deduplicate: skip if same level+span+message already exists
|
||||
for (self.items.items) |d| {
|
||||
for (self.items.items, 0..) |d, i| {
|
||||
if (d.level == level and std.mem.eql(u8, d.message, message)) {
|
||||
const a = d.span orelse continue;
|
||||
const b = span orelse continue;
|
||||
if (a.start == b.start and a.end == b.end) return;
|
||||
if (a.start == b.start and a.end == b.end) return i;
|
||||
}
|
||||
}
|
||||
const idx = self.items.items.len;
|
||||
self.items.append(self.allocator, .{
|
||||
.level = level,
|
||||
.message = message,
|
||||
.span = span,
|
||||
.source_file = self.current_source_file,
|
||||
}) catch {};
|
||||
}) catch return idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
pub fn addFmt(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) void {
|
||||
_ = self.addFmtId(level, span, fmt, args);
|
||||
}
|
||||
|
||||
pub fn addFmtId(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) usize {
|
||||
const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error";
|
||||
self.add(level, message, span);
|
||||
return self.addId(level, message, span);
|
||||
}
|
||||
|
||||
/// Attach a `note:` block to the primary diagnostic at `primary_id`.
|
||||
/// Notes render after the primary, in insertion order, before any helps.
|
||||
pub fn addNote(self: *DiagnosticList, primary_id: usize, span: ?Span, message: []const u8) void {
|
||||
self.items.append(self.allocator, .{
|
||||
.level = .note,
|
||||
.message = message,
|
||||
.span = span,
|
||||
.source_file = self.current_source_file,
|
||||
.primary = primary_id,
|
||||
}) catch {};
|
||||
}
|
||||
|
||||
pub fn addNoteFmt(self: *DiagnosticList, primary_id: usize, span: ?Span, comptime fmt: []const u8, args: anytype) void {
|
||||
const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error";
|
||||
self.addNote(primary_id, span, message);
|
||||
}
|
||||
|
||||
/// Attach a `help:` block to the primary diagnostic at `primary_id`.
|
||||
/// When `fix_code` is non-null it is shown on the excerpt line in place of
|
||||
/// the original source as a fix-it suggestion.
|
||||
pub fn addHelp(self: *DiagnosticList, primary_id: usize, span: ?Span, message: []const u8, fix_code: ?[]const u8) void {
|
||||
self.items.append(self.allocator, .{
|
||||
.level = .help,
|
||||
.message = message,
|
||||
.span = span,
|
||||
.source_file = self.current_source_file,
|
||||
.primary = primary_id,
|
||||
.fix_code = fix_code,
|
||||
}) catch {};
|
||||
}
|
||||
|
||||
pub fn addHelpFmt(self: *DiagnosticList, primary_id: usize, span: ?Span, fix_code: ?[]const u8, comptime fmt: []const u8, args: anytype) void {
|
||||
const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error";
|
||||
self.addHelp(primary_id, span, message, fix_code);
|
||||
}
|
||||
|
||||
pub fn hasErrors(self: *const DiagnosticList) bool {
|
||||
@@ -175,11 +235,7 @@ pub const DiagnosticList = struct {
|
||||
|
||||
pub fn renderCompact(self: *const DiagnosticList, writer: anytype) !void {
|
||||
for (self.items.items) |d| {
|
||||
const level_str = switch (d.level) {
|
||||
.err => "error",
|
||||
.warn => "warning",
|
||||
.note => "note",
|
||||
};
|
||||
const level_str = levelStr(d.level);
|
||||
if (d.span) |span| {
|
||||
const resolved = self.resolveSourceAndFile(d);
|
||||
const loc = SourceLoc.compute(resolved.source, span.start);
|
||||
@@ -190,25 +246,47 @@ pub const DiagnosticList = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render every primary diagnostic, each followed by its bundled notes
|
||||
/// (insertion order) and then its bundled helps. Bundles are separated by
|
||||
/// a blank line; blocks within a bundle are too.
|
||||
pub fn renderExtended(self: *const DiagnosticList, writer: anytype) !void {
|
||||
for (self.items.items) |d| {
|
||||
try self.renderExtendedOne(writer, d);
|
||||
var first = true;
|
||||
for (self.items.items, 0..) |d, i| {
|
||||
if (d.primary != null) continue;
|
||||
if (!first) try writer.writeAll("\n");
|
||||
first = false;
|
||||
try self.renderBlock(writer, d, true);
|
||||
|
||||
for (self.items.items) |n| {
|
||||
if (n.primary == i and n.level == .note) {
|
||||
try writer.writeAll("\n");
|
||||
try self.renderBlock(writer, n, true);
|
||||
}
|
||||
}
|
||||
for (self.items.items) |h| {
|
||||
if (h.primary == i and h.level == .help) {
|
||||
try writer.writeAll("\n");
|
||||
try self.renderBlock(writer, h, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn renderExtendedOne(self: *const DiagnosticList, writer: anytype, d: Diagnostic) !void {
|
||||
const level_str = switch (d.level) {
|
||||
.err => "error",
|
||||
.warn => "warning",
|
||||
.note => "note",
|
||||
};
|
||||
|
||||
/// Render one diagnostic as a header line, optional `-->` location arrow,
|
||||
/// and a code excerpt with caret underline. A `.help` with `fix_code`
|
||||
/// substitutes the suggested source on the excerpt line and omits the
|
||||
/// arrow (`show_arrow == false`).
|
||||
fn renderBlock(self: *const DiagnosticList, writer: anytype, d: Diagnostic, show_arrow: bool) !void {
|
||||
const level_str = levelStr(d.level);
|
||||
try writer.print("{s}: {s}\n", .{ level_str, d.message });
|
||||
|
||||
const span = d.span orelse return;
|
||||
const resolved = self.resolveSourceAndFile(d);
|
||||
const loc = SourceLoc.compute(resolved.source, span.start);
|
||||
try writer.print(" --> {s}:{d}:{d}\n", .{ resolved.file_name, loc.line, loc.col });
|
||||
|
||||
if (show_arrow) {
|
||||
const loc = SourceLoc.compute(resolved.source, span.start);
|
||||
try writer.print(" --> {s}:{d}:{d}\n", .{ resolved.file_name, loc.line, loc.col });
|
||||
}
|
||||
|
||||
var ctx = extractContext(self.allocator, resolved.source, span) catch return;
|
||||
defer ctx.deinit(self.allocator);
|
||||
@@ -220,6 +298,23 @@ pub const DiagnosticList = struct {
|
||||
try writeRepeated(writer, ' ', line_num_width + 1);
|
||||
try writer.writeAll("|\n");
|
||||
|
||||
// Fix-it: show the suggested replacement on the primary span's line
|
||||
// instead of the original source, with carets under the change.
|
||||
if (d.fix_code) |fix| {
|
||||
const ln = ctx.lines[0].line_num;
|
||||
const pad = line_num_width - digitCount(ln);
|
||||
try writeRepeated(writer, ' ', pad);
|
||||
try writer.print("{d} | {s}\n", .{ ln, fix });
|
||||
|
||||
try writeRepeated(writer, ' ', line_num_width + 1);
|
||||
try writer.writeAll("| ");
|
||||
try writeRepeated(writer, ' ', ctx.start_col - 1);
|
||||
const caret_count = if (ctx.end_col > ctx.start_col) ctx.end_col - ctx.start_col else 1;
|
||||
try writeRepeated(writer, '^', caret_count);
|
||||
try writer.writeAll("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ctx.lines, 0..) |line, idx| {
|
||||
const pad = line_num_width - digitCount(line.line_num);
|
||||
try writeRepeated(writer, ' ', pad);
|
||||
@@ -239,6 +334,15 @@ pub const DiagnosticList = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn levelStr(level: Level) []const u8 {
|
||||
return switch (level) {
|
||||
.err => "error",
|
||||
.warn => "warning",
|
||||
.note => "note",
|
||||
.help => "help",
|
||||
};
|
||||
}
|
||||
|
||||
fn digitCount(n: u32) usize {
|
||||
if (n == 0) return 1;
|
||||
var count: usize = 0;
|
||||
@@ -252,13 +356,22 @@ pub const DiagnosticList = struct {
|
||||
while (i < n) : (i += 1) try writer.writeByte(byte);
|
||||
}
|
||||
|
||||
/// Render diagnostics to stderr respecting `render_style`. This is the
|
||||
/// path the CLI uses; `renderDebug` remains a compact fallback.
|
||||
pub fn renderStderr(self: *const DiagnosticList) void {
|
||||
var aw = std.Io.Writer.Allocating.init(self.allocator);
|
||||
self.render(&aw.writer) catch {
|
||||
self.renderDebug();
|
||||
return;
|
||||
};
|
||||
var result = aw.writer.toArrayList();
|
||||
defer result.deinit(self.allocator);
|
||||
std.debug.print("{s}", .{result.items});
|
||||
}
|
||||
|
||||
pub fn renderDebug(self: *const DiagnosticList) void {
|
||||
for (self.items.items) |d| {
|
||||
const level_str = switch (d.level) {
|
||||
.err => "error",
|
||||
.warn => "warning",
|
||||
.note => "note",
|
||||
};
|
||||
const level_str = levelStr(d.level);
|
||||
if (d.span) |span| {
|
||||
const resolved = self.resolveSourceAndFile(d);
|
||||
const loc = SourceLoc.compute(resolved.source, span.start);
|
||||
|
||||
Reference in New Issue
Block a user