diff options
Diffstat (limited to 'src/cli/diagnostics.zig')
| -rw-r--r-- | src/cli/diagnostics.zig | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/src/cli/diagnostics.zig b/src/cli/diagnostics.zig new file mode 100644 index 000000000..80dc06b5a --- /dev/null +++ b/src/cli/diagnostics.zig @@ -0,0 +1,124 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const build_config = @import("../build_config.zig"); + +/// A diagnostic message from parsing. This is used to provide additional +/// human-friendly warnings and errors about the parsed data. +/// +/// All of the memory for the diagnostic is allocated from the arena +/// associated with the config structure. If an arena isn't available +/// then diagnostics are not supported. +pub const Diagnostic = struct { + location: Location = .none, + key: [:0]const u8 = "", + message: [:0]const u8, + + /// The possible locations for a diagnostic message. This is used + /// to provide context for the message. + pub const Location = union(enum) { + none, + cli: usize, + file: struct { + path: []const u8, + line: usize, + }, + + pub fn fromIter(iter: anytype) Location { + const Iter = t: { + const T = @TypeOf(iter); + break :t switch (@typeInfo(T)) { + .Pointer => |v| v.child, + .Struct => T, + else => return .none, + }; + }; + + if (!@hasDecl(Iter, "location")) return .none; + return iter.location() orelse .none; + } + }; + + /// Write the full user-friendly diagnostic message to the writer. + pub fn write(self: *const Diagnostic, writer: anytype) !void { + switch (self.location) { + .none => {}, + .cli => |index| try writer.print("cli:{}:", .{index}), + .file => |file| try writer.print( + "{s}:{}:", + .{ file.path, file.line }, + ), + } + + if (self.key.len > 0) { + try writer.print("{s}: ", .{self.key}); + } else if (self.location != .none) { + try writer.print(" ", .{}); + } + + try writer.print("{s}", .{self.message}); + } +}; + +/// A list of diagnostics. The "_diagnostics" field must be this type +/// for diagnostics to be supported. If this field is an incorrect type +/// a compile-time error will be raised. +/// +/// This is implemented as a simple wrapper around an array list +/// so that we can inject some logic around adding diagnostics +/// and potentially in the future structure them differently. +pub const DiagnosticList = struct { + /// The list of diagnostics. + list: std.ArrayListUnmanaged(Diagnostic) = .{}, + + /// Precomputed data for diagnostics. This is used specifically + /// when we build libghostty so that we can precompute the messages + /// and return them via the C API without allocating memory at + /// call time. + precompute: Precompute = precompute_init, + + const precompute_enabled = switch (build_config.artifact) { + // We enable precompute for tests so that the logic is + // semantically analyzed and run. + .exe, .wasm_module => builtin.is_test, + + // We specifically want precompute for libghostty. + .lib => true, + }; + const Precompute = if (precompute_enabled) struct { + messages: std.ArrayListUnmanaged([:0]const u8) = .{}, + } else void; + const precompute_init: Precompute = if (precompute_enabled) .{} else {}; + + pub fn append( + self: *DiagnosticList, + alloc: Allocator, + diag: Diagnostic, + ) Allocator.Error!void { + try self.list.append(alloc, diag); + errdefer _ = self.list.pop(); + + if (comptime precompute_enabled) { + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + try diag.write(buf.writer()); + + const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0); + errdefer alloc.free(owned); + + try self.precompute.messages.append(alloc, owned); + errdefer _ = self.precompute.messages.pop(); + + assert(self.precompute.messages.items.len == self.list.items.len); + } + } + + pub fn empty(self: *const DiagnosticList) bool { + return self.list.items.len == 0; + } + + pub fn items(self: *DiagnosticList) []Diagnostic { + return self.list.items; + } +}; |
