// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The errors and warnings a stage reports about its input. //! //! A stage that finds something wrong with a WiX source adds a diagnostic //! here, naming where it is, and carries on when it can, so that one run //! reports as much as it can find. It returns `error.Invalid` once it cannot //! go on, or at the end when any error was added. Nothing is printed: the //! caller decides how diagnostics reach a person, with `render` as the //! conventional `file:line:column: severity: message` form. const std = @import("std"); const Allocator = std.mem.Allocator; const Diagnostics = @This(); /// Where every message and copied path is allocated. An arena is the natural /// choice, since diagnostics live as long as the build that produced them. gpa: Allocator, list: std.ArrayList(Diagnostic) = .empty, pub const Severity = enum { @"error", warning, }; /// A position in a WiX source, counted from 1. The column counts bytes. pub const Location = struct { path: []const u8, line: u32, column: u32, /// For a position in an included file, the `` that brought /// it in, which may itself be in an included file. included_from: ?*const Location = null, /// The location of byte `offset` in `text`, which was read from `path`. pub fn at(path: []const u8, text: []const u8, offset: usize) Location { var line: u32 = 1; var line_start: usize = 0; const end = @min(offset, text.len); for (text[0..end], 0..) |c, i| { if (c == '\n') { line += 1; line_start = i + 1; } } return .{ .path = path, .line = line, .column = @intCast(end - line_start + 1) }; } }; pub const Diagnostic = struct { severity: Severity, /// Absent for a problem that belongs to no particular place, such as an /// option given on the command line. location: ?Location, message: []const u8, }; pub fn init(gpa: Allocator) Diagnostics { return .{ .gpa = gpa }; } pub fn add( d: *Diagnostics, severity: Severity, location: ?Location, comptime fmt: []const u8, args: anytype, ) Allocator.Error!void { try d.list.append(d.gpa, .{ .severity = severity, .location = location, .message = try std.fmt.allocPrint(d.gpa, fmt, args), }); } pub fn err(d: *Diagnostics, location: ?Location, comptime fmt: []const u8, args: anytype) Allocator.Error!void { try d.add(.@"error", location, fmt, args); } pub fn warn(d: *Diagnostics, location: ?Location, comptime fmt: []const u8, args: anytype) Allocator.Error!void { try d.add(.warning, location, fmt, args); } pub fn hasErrors(d: *const Diagnostics) bool { for (d.list.items) |item| { if (item.severity == .@"error") return true; } return false; } /// Writes each diagnostic on a line of its own, as /// `path:line:column: error: message`. pub fn render(d: *const Diagnostics, w: *std.Io.Writer) std.Io.Writer.Error!void { for (d.list.items) |item| { if (item.location) |loc| { try w.print("{s}:{d}:{d}: ", .{ loc.path, loc.line, loc.column }); } else { try w.writeAll("zixl: "); } try w.print("{s}: {s}", .{ @tagName(item.severity), item.message }); if (item.location) |loc| { var from = loc.included_from; var first = true; while (from) |f| : (from = f.included_from) { try w.print("{s}included from {s}:{d}", .{ if (first) " (" else ", ", f.path, f.line }); first = false; } if (!first) try w.writeAll(")"); } try w.writeAll("\n"); } } test "a location is counted from 1, in lines and bytes" { const text = "\n \n\n"; const loc: Location = .at("x.wxs", text, std.mem.find(u8, text, "