mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 22:33:08 +00:00
`std.Io.tty.Config.detect` may be an expensive check (e.g. involving syscalls), and doing it every time we need to print isn't really necessary; under normal usage, we can compute the value once and cache it for the whole program's execution. Since anyone outputting to stderr may reasonably want this information (in fact they are very likely to), it makes sense to cache it and return it from `lockStderrWriter`. Call sites who do not need it will experience no significant overhead, and can just ignore the TTY config with a `const w, _` destructure.
1266 lines
60 KiB
Zig
1266 lines
60 KiB
Zig
const builtin = @import("builtin");
|
||
const native_endian = builtin.cpu.arch.endian();
|
||
|
||
const std = @import("std");
|
||
const Io = std.Io;
|
||
const assert = std.debug.assert;
|
||
const Allocator = std.mem.Allocator;
|
||
|
||
const Token = @import("lex.zig").Token;
|
||
const SourceMappings = @import("source_mapping.zig").SourceMappings;
|
||
const utils = @import("utils.zig");
|
||
const rc = @import("rc.zig");
|
||
const res = @import("res.zig");
|
||
const ico = @import("ico.zig");
|
||
const bmp = @import("bmp.zig");
|
||
const parse = @import("parse.zig");
|
||
const lang = @import("lang.zig");
|
||
const code_pages = @import("code_pages.zig");
|
||
const SupportedCodePage = code_pages.SupportedCodePage;
|
||
|
||
pub const Diagnostics = struct {
|
||
errors: std.ArrayList(ErrorDetails) = .empty,
|
||
/// Append-only, cannot handle removing strings.
|
||
/// Expects to own all strings within the list.
|
||
strings: std.ArrayList([]const u8) = .empty,
|
||
allocator: Allocator,
|
||
io: Io,
|
||
|
||
pub fn init(allocator: Allocator, io: Io) Diagnostics {
|
||
return .{
|
||
.allocator = allocator,
|
||
.io = io,
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *Diagnostics) void {
|
||
self.errors.deinit(self.allocator);
|
||
for (self.strings.items) |str| {
|
||
self.allocator.free(str);
|
||
}
|
||
self.strings.deinit(self.allocator);
|
||
}
|
||
|
||
pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
|
||
try self.errors.append(self.allocator, error_details);
|
||
}
|
||
|
||
const SmallestStringIndexType = std.meta.Int(.unsigned, @min(
|
||
@bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex),
|
||
@min(
|
||
@bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex),
|
||
@bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex),
|
||
),
|
||
));
|
||
|
||
/// Returns the index of the added string as the SmallestStringIndexType
|
||
/// in order to avoid needing to `@intCast` it at callsites of putString.
|
||
/// Instead, this function will error if the index would ever exceed the
|
||
/// smallest FilenameStringIndex of an ErrorDetails type.
|
||
pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType {
|
||
if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) {
|
||
return error.OutOfMemory; // ran out of string indexes
|
||
}
|
||
const dupe = try self.allocator.dupe(u8, str);
|
||
const index = self.strings.items.len;
|
||
try self.strings.append(self.allocator, dupe);
|
||
return @intCast(index);
|
||
}
|
||
|
||
pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
|
||
const io = self.io;
|
||
const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
|
||
defer std.debug.unlockStderrWriter();
|
||
for (self.errors.items) |err_details| {
|
||
renderErrorMessage(io, stderr, ttyconf, cwd, err_details, source, self.strings.items, source_mappings) catch return;
|
||
}
|
||
}
|
||
|
||
pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
|
||
for (self.errors.items) |details| {
|
||
if (details.err == err) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool {
|
||
for (self.errors.items) |details| {
|
||
for (errors) |err| {
|
||
if (details.err == err) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/// Contains enough context to append errors/warnings/notes etc
|
||
pub const DiagnosticsContext = struct {
|
||
diagnostics: *Diagnostics,
|
||
token: Token,
|
||
/// Code page of the source file at the token location
|
||
code_page: SupportedCodePage,
|
||
};
|
||
|
||
pub const ErrorDetails = struct {
|
||
err: Error,
|
||
token: Token,
|
||
/// Code page of the source file at the token location
|
||
code_page: SupportedCodePage,
|
||
/// If non-null, should be before `token`. If null, `token` is assumed to be the start.
|
||
token_span_start: ?Token = null,
|
||
/// If non-null, should be after `token`. If null, `token` is assumed to be the end.
|
||
token_span_end: ?Token = null,
|
||
type: Type = .err,
|
||
print_source_line: bool = true,
|
||
extra: Extra = .{ .none = {} },
|
||
|
||
pub const Type = enum {
|
||
/// Fatal error, stops compilation
|
||
err,
|
||
/// Warning that does not affect compilation result
|
||
warning,
|
||
/// A note that typically provides further context for a warning/error
|
||
note,
|
||
/// An invisible diagnostic that is not printed to stderr but can
|
||
/// provide information useful when comparing the behavior of different
|
||
/// implementations. For example, a hint is emitted when a FONTDIR resource
|
||
/// was included in the .RES file which is significant because rc.exe
|
||
/// does something different than us, but ultimately it's not important
|
||
/// enough to be a warning/note.
|
||
hint,
|
||
};
|
||
|
||
pub const Extra = union {
|
||
none: void,
|
||
expected: Token.Id,
|
||
number: u32,
|
||
expected_types: ExpectedTypes,
|
||
resource: rc.ResourceType,
|
||
string_and_language: StringAndLanguage,
|
||
file_open_error: FileOpenError,
|
||
icon_read_error: IconReadError,
|
||
icon_dir: IconDirContext,
|
||
bmp_read_error: BitmapReadError,
|
||
accelerator_error: AcceleratorError,
|
||
statement_with_u16_param: StatementWithU16Param,
|
||
menu_or_class: enum { class, menu },
|
||
};
|
||
|
||
comptime {
|
||
// all fields in the extra union should be 32 bits or less
|
||
for (std.meta.fields(Extra)) |field| {
|
||
std.debug.assert(@bitSizeOf(field.type) <= 32);
|
||
}
|
||
}
|
||
|
||
pub const StatementWithU16Param = enum(u32) {
|
||
fileversion,
|
||
productversion,
|
||
language,
|
||
};
|
||
|
||
pub const StringAndLanguage = packed struct(u32) {
|
||
id: u16,
|
||
language: res.Language,
|
||
};
|
||
|
||
pub const FileOpenError = packed struct(u32) {
|
||
err: FileOpenErrorEnum,
|
||
filename_string_index: FilenameStringIndex,
|
||
|
||
pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum));
|
||
pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError || std.fs.File.StatError);
|
||
|
||
pub fn enumFromError(err: (std.fs.File.OpenError || std.fs.File.StatError)) FileOpenErrorEnum {
|
||
return switch (err) {
|
||
inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)),
|
||
};
|
||
}
|
||
};
|
||
|
||
pub const IconReadError = packed struct(u32) {
|
||
err: IconReadErrorEnum,
|
||
icon_type: enum(u1) { cursor, icon },
|
||
filename_string_index: FilenameStringIndex,
|
||
|
||
pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1);
|
||
pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError);
|
||
|
||
pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum {
|
||
return switch (err) {
|
||
inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)),
|
||
};
|
||
}
|
||
};
|
||
|
||
pub const IconDirContext = packed struct(u32) {
|
||
icon_type: enum(u1) { cursor, icon },
|
||
icon_format: ico.ImageFormat,
|
||
index: u16,
|
||
bitmap_version: ico.BitmapHeader.Version = .unknown,
|
||
_: Padding = 0,
|
||
|
||
pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat));
|
||
};
|
||
|
||
pub const BitmapReadError = packed struct(u32) {
|
||
err: BitmapReadErrorEnum,
|
||
filename_string_index: FilenameStringIndex,
|
||
|
||
pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum));
|
||
pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError);
|
||
|
||
pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum {
|
||
return switch (err) {
|
||
inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)),
|
||
};
|
||
}
|
||
};
|
||
|
||
pub const BitmapUnsupportedDIB = packed struct(u32) {
|
||
dib_version: ico.BitmapHeader.Version,
|
||
filename_string_index: FilenameStringIndex,
|
||
|
||
pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version));
|
||
};
|
||
|
||
pub const AcceleratorError = packed struct(u32) {
|
||
err: AcceleratorErrorEnum,
|
||
_: Padding = 0,
|
||
|
||
pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum));
|
||
pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError);
|
||
|
||
pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum {
|
||
return switch (err) {
|
||
inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)),
|
||
};
|
||
}
|
||
};
|
||
|
||
pub const ExpectedTypes = packed struct(u32) {
|
||
number: bool = false,
|
||
number_expression: bool = false,
|
||
string_literal: bool = false,
|
||
accelerator_type_or_option: bool = false,
|
||
control_class: bool = false,
|
||
literal: bool = false,
|
||
// Note: This being 0 instead of undefined is arbitrary and something of a workaround,
|
||
// see https://github.com/ziglang/zig/issues/15395
|
||
_: u26 = 0,
|
||
|
||
pub const strings = std.StaticStringMap([]const u8).initComptime(.{
|
||
.{ "number", "number" },
|
||
.{ "number_expression", "number expression" },
|
||
.{ "string_literal", "quoted string literal" },
|
||
.{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" },
|
||
.{ "control_class", "control class [BUTTON, EDIT, etc]" },
|
||
.{ "literal", "unquoted literal" },
|
||
});
|
||
|
||
pub fn writeCommaSeparated(self: ExpectedTypes, writer: *std.Io.Writer) !void {
|
||
const struct_info = @typeInfo(ExpectedTypes).@"struct";
|
||
const num_real_fields = struct_info.fields.len - 1;
|
||
const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields;
|
||
const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits;
|
||
const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask;
|
||
const num_set_bits = @popCount(relevant_bits_only);
|
||
|
||
var i: usize = 0;
|
||
inline for (struct_info.fields) |field_info| {
|
||
if (field_info.type != bool) continue;
|
||
if (i == num_set_bits) return;
|
||
if (@field(self, field_info.name)) {
|
||
try writer.writeAll(strings.get(field_info.name).?);
|
||
i += 1;
|
||
if (num_set_bits > 2 and i != num_set_bits) {
|
||
try writer.writeAll(", ");
|
||
} else if (i != num_set_bits) {
|
||
try writer.writeByte(' ');
|
||
}
|
||
if (num_set_bits > 1 and i == num_set_bits - 1) {
|
||
try writer.writeAll("or ");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
pub const Error = enum {
|
||
// Lexer
|
||
unfinished_string_literal,
|
||
string_literal_too_long,
|
||
invalid_number_with_exponent,
|
||
invalid_digit_character_in_number_literal,
|
||
illegal_byte,
|
||
illegal_byte_outside_string_literals,
|
||
illegal_codepoint_outside_string_literals,
|
||
illegal_byte_order_mark,
|
||
illegal_private_use_character,
|
||
found_c_style_escaped_quote,
|
||
code_page_pragma_missing_left_paren,
|
||
code_page_pragma_missing_right_paren,
|
||
code_page_pragma_invalid_code_page,
|
||
code_page_pragma_not_integer,
|
||
code_page_pragma_overflow,
|
||
code_page_pragma_unsupported_code_page,
|
||
|
||
// Parser
|
||
unfinished_raw_data_block,
|
||
unfinished_string_table_block,
|
||
/// `expected` is populated.
|
||
expected_token,
|
||
/// `expected_types` is populated
|
||
expected_something_else,
|
||
/// `resource` is populated
|
||
resource_type_cant_use_raw_data,
|
||
/// `resource` is populated
|
||
id_must_be_ordinal,
|
||
/// `resource` is populated
|
||
name_or_id_not_allowed,
|
||
string_resource_as_numeric_type,
|
||
ascii_character_not_equivalent_to_virtual_key_code,
|
||
empty_menu_not_allowed,
|
||
rc_would_miscompile_version_value_padding,
|
||
rc_would_miscompile_version_value_byte_count,
|
||
code_page_pragma_in_included_file,
|
||
nested_resource_level_exceeds_max,
|
||
too_many_dialog_controls_or_toolbar_buttons,
|
||
nested_expression_level_exceeds_max,
|
||
close_paren_expression,
|
||
unary_plus_expression,
|
||
rc_could_miscompile_control_params,
|
||
dangling_literal_at_eof,
|
||
disjoint_code_page,
|
||
|
||
// Compiler
|
||
/// `string_and_language` is populated
|
||
string_already_defined,
|
||
font_id_already_defined,
|
||
/// `file_open_error` is populated
|
||
file_open_error,
|
||
/// `accelerator_error` is populated
|
||
invalid_accelerator_key,
|
||
accelerator_type_required,
|
||
accelerator_shift_or_control_without_virtkey,
|
||
rc_would_miscompile_control_padding,
|
||
rc_would_miscompile_control_class_ordinal,
|
||
/// `icon_dir` is populated
|
||
rc_would_error_on_icon_dir,
|
||
/// `icon_dir` is populated
|
||
format_not_supported_in_icon_dir,
|
||
/// `resource` is populated and contains the expected type
|
||
icon_dir_and_resource_type_mismatch,
|
||
/// `icon_read_error` is populated
|
||
icon_read_error,
|
||
/// `icon_dir` is populated
|
||
rc_would_error_on_bitmap_version,
|
||
/// `icon_dir` is populated
|
||
max_icon_ids_exhausted,
|
||
/// `bmp_read_error` is populated
|
||
bmp_read_error,
|
||
/// `number` is populated and contains a string index for which the string contains
|
||
/// the bytes of a `u64` (native endian). The `u64` contains the number of ignored bytes.
|
||
bmp_ignored_palette_bytes,
|
||
/// `number` is populated and contains a string index for which the string contains
|
||
/// the bytes of a `u64` (native endian). The `u64` contains the number of missing bytes.
|
||
bmp_missing_palette_bytes,
|
||
/// `number` is populated and contains a string index for which the string contains
|
||
/// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
|
||
rc_would_miscompile_bmp_palette_padding,
|
||
resource_header_size_exceeds_max,
|
||
resource_data_size_exceeds_max,
|
||
control_extra_data_size_exceeds_max,
|
||
version_node_size_exceeds_max,
|
||
fontdir_size_exceeds_max,
|
||
/// `number` is populated and contains a string index for the filename
|
||
number_expression_as_filename,
|
||
/// `number` is populated and contains the control ID that is a duplicate
|
||
control_id_already_defined,
|
||
/// `number` is populated and contains the disallowed codepoint
|
||
invalid_filename,
|
||
/// `statement_with_u16_param` is populated
|
||
rc_would_error_u16_with_l_suffix,
|
||
result_contains_fontdir,
|
||
/// `number` is populated and contains the ordinal value that the id would be miscompiled to
|
||
rc_would_miscompile_dialog_menu_id,
|
||
/// `number` is populated and contains the ordinal value that the value would be miscompiled to
|
||
rc_would_miscompile_dialog_class,
|
||
/// `menu_or_class` is populated and contains the type of the parameter statement
|
||
rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
|
||
rc_would_miscompile_dialog_menu_id_starts_with_digit,
|
||
dialog_menu_id_was_uppercased,
|
||
duplicate_optional_statement_skipped,
|
||
invalid_digit_character_in_ordinal,
|
||
|
||
// Literals
|
||
/// `number` is populated
|
||
rc_would_miscompile_codepoint_whitespace,
|
||
/// `number` is populated
|
||
rc_would_miscompile_codepoint_skip,
|
||
/// `number` is populated
|
||
rc_would_miscompile_codepoint_bom,
|
||
tab_converted_to_spaces,
|
||
|
||
// General (used in various places)
|
||
/// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
|
||
win32_non_ascii_ordinal,
|
||
|
||
// Initialization
|
||
/// `file_open_error` is populated, but `filename_string_index` is not
|
||
failed_to_open_cwd,
|
||
};
|
||
|
||
fn formatToken(ctx: TokenFormatContext, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||
switch (ctx.token.id) {
|
||
.eof => return writer.writeAll(ctx.token.id.nameForErrorDisplay()),
|
||
else => {},
|
||
}
|
||
|
||
const slice = ctx.token.slice(ctx.source);
|
||
var src_i: usize = 0;
|
||
while (src_i < slice.len) {
|
||
const codepoint = ctx.code_page.codepointAt(src_i, slice) orelse break;
|
||
defer src_i += codepoint.byte_len;
|
||
const display_codepoint = codepointForDisplay(codepoint) orelse continue;
|
||
var buf: [4]u8 = undefined;
|
||
const utf8_len = std.unicode.utf8Encode(display_codepoint, &buf) catch unreachable;
|
||
try writer.writeAll(buf[0..utf8_len]);
|
||
}
|
||
}
|
||
|
||
const TokenFormatContext = struct {
|
||
token: Token,
|
||
source: []const u8,
|
||
code_page: SupportedCodePage,
|
||
};
|
||
|
||
fn fmtToken(self: ErrorDetails, source: []const u8) std.fmt.Alt(TokenFormatContext, formatToken) {
|
||
return .{ .data = .{
|
||
.token = self.token,
|
||
.code_page = self.code_page,
|
||
.source = source,
|
||
} };
|
||
}
|
||
|
||
pub fn render(self: ErrorDetails, writer: *std.Io.Writer, source: []const u8, strings: []const []const u8) !void {
|
||
switch (self.err) {
|
||
.unfinished_string_literal => {
|
||
return writer.print("unfinished string literal at '{f}', expected closing '\"'", .{self.fmtToken(source)});
|
||
},
|
||
.string_literal_too_long => {
|
||
return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
|
||
},
|
||
.invalid_number_with_exponent => {
|
||
return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)});
|
||
},
|
||
.invalid_digit_character_in_number_literal => switch (self.type) {
|
||
.err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"),
|
||
.note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"),
|
||
.hint => return,
|
||
},
|
||
.illegal_byte => {
|
||
return writer.print("character '{f}' is not allowed", .{
|
||
std.ascii.hexEscape(self.token.slice(source), .upper),
|
||
});
|
||
},
|
||
.illegal_byte_outside_string_literals => {
|
||
return writer.print("character '{f}' is not allowed outside of string literals", .{
|
||
std.ascii.hexEscape(self.token.slice(source), .upper),
|
||
});
|
||
},
|
||
.illegal_codepoint_outside_string_literals => {
|
||
// This is somewhat hacky, but we know that:
|
||
// - This error is only possible with codepoints outside of the Windows-1252 character range
|
||
// - So, the only supported code page that could generate this error is UTF-8
|
||
// Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
|
||
// codepoint.
|
||
//
|
||
// FIXME: Support other code pages if they become relevant
|
||
const bytes = self.token.slice(source);
|
||
const codepoint = std.unicode.utf8Decode(bytes) catch unreachable;
|
||
return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint});
|
||
},
|
||
.illegal_byte_order_mark => {
|
||
return writer.writeAll("byte order mark <U+FEFF> is not allowed");
|
||
},
|
||
.illegal_private_use_character => {
|
||
return writer.writeAll("private use character <U+E000> is not allowed");
|
||
},
|
||
.found_c_style_escaped_quote => {
|
||
return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)");
|
||
},
|
||
.code_page_pragma_missing_left_paren => {
|
||
return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page");
|
||
},
|
||
.code_page_pragma_missing_right_paren => {
|
||
return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page");
|
||
},
|
||
.code_page_pragma_invalid_code_page => {
|
||
return writer.writeAll("invalid or unknown code page in #pragma code_page");
|
||
},
|
||
.code_page_pragma_not_integer => {
|
||
return writer.writeAll("code page is not a valid integer in #pragma code_page");
|
||
},
|
||
.code_page_pragma_overflow => {
|
||
return writer.writeAll("code page too large in #pragma code_page");
|
||
},
|
||
.code_page_pragma_unsupported_code_page => {
|
||
// We know that the token slice is a well-formed #pragma code_page(N), so
|
||
// we can skip to the first ( and then get the number that follows
|
||
const token_slice = self.token.slice(source);
|
||
var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1;
|
||
while (std.ascii.isWhitespace(token_slice[number_start])) {
|
||
number_start += 1;
|
||
}
|
||
var number_slice = token_slice[number_start..number_start];
|
||
while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) {
|
||
number_slice.len += 1;
|
||
}
|
||
const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
|
||
const code_page = code_pages.getByIdentifier(number) catch unreachable;
|
||
// TODO: Improve or maybe add a note making it more clear that the code page
|
||
// is valid and that the code page is unsupported purely due to a limitation
|
||
// in this compiler.
|
||
return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
|
||
},
|
||
.unfinished_raw_data_block => {
|
||
return writer.print("unfinished raw data block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
|
||
},
|
||
.unfinished_string_table_block => {
|
||
return writer.print("unfinished STRINGTABLE block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
|
||
},
|
||
.expected_token => {
|
||
return writer.print("expected '{s}', got '{f}'", .{ self.extra.expected.nameForErrorDisplay(), self.fmtToken(source) });
|
||
},
|
||
.expected_something_else => {
|
||
try writer.writeAll("expected ");
|
||
try self.extra.expected_types.writeCommaSeparated(writer);
|
||
return writer.print("; got '{f}'", .{self.fmtToken(source)});
|
||
},
|
||
.resource_type_cant_use_raw_data => switch (self.type) {
|
||
.err, .warning => try writer.print("expected '<filename>', found '{f}' (resource type '{s}' can't use raw data)", .{ self.fmtToken(source), self.extra.resource.nameForErrorDisplay() }),
|
||
.note => try writer.print("if '{f}' is intended to be a filename, it must be specified as a quoted string literal", .{self.fmtToken(source)}),
|
||
.hint => return,
|
||
},
|
||
.id_must_be_ordinal => {
|
||
try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{f}'", .{ self.extra.resource.nameForErrorDisplay(), self.fmtToken(source) });
|
||
},
|
||
.name_or_id_not_allowed => {
|
||
try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
|
||
},
|
||
.string_resource_as_numeric_type => switch (self.type) {
|
||
.err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"),
|
||
.note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"),
|
||
.hint => return,
|
||
},
|
||
.ascii_character_not_equivalent_to_virtual_key_code => {
|
||
// TODO: Better wording? This is what the Win32 RC compiler emits.
|
||
// This occurs when VIRTKEY and a control code is specified ("^c", etc)
|
||
try writer.writeAll("ASCII character not equivalent to virtual key code");
|
||
},
|
||
.empty_menu_not_allowed => {
|
||
try writer.print("empty menu of type '{f}' not allowed", .{self.fmtToken(source)});
|
||
},
|
||
.rc_would_miscompile_version_value_padding => switch (self.type) {
|
||
.err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_miscompile_version_value_byte_count => switch (self.type) {
|
||
.err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}),
|
||
.hint => return,
|
||
},
|
||
.code_page_pragma_in_included_file => {
|
||
try writer.print("#pragma code_page is not supported in an included resource file", .{});
|
||
},
|
||
.nested_resource_level_exceeds_max => switch (self.type) {
|
||
.err, .warning => {
|
||
const max = switch (self.extra.resource) {
|
||
.versioninfo => parse.max_nested_version_level,
|
||
.menu, .menuex => parse.max_nested_menu_level,
|
||
else => unreachable,
|
||
};
|
||
return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max });
|
||
},
|
||
.note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
|
||
.hint => return,
|
||
},
|
||
.too_many_dialog_controls_or_toolbar_buttons => switch (self.type) {
|
||
.err, .warning => return writer.print("{s} contains too many {s} (max is {})", .{ self.extra.resource.nameForErrorDisplay(), switch (self.extra.resource) {
|
||
.toolbar => "buttons",
|
||
else => "controls",
|
||
}, std.math.maxInt(u16) }),
|
||
.note => return writer.print("maximum number of {s} exceeded here", .{switch (self.extra.resource) {
|
||
.toolbar => "buttons",
|
||
else => "controls",
|
||
}}),
|
||
.hint => return,
|
||
},
|
||
.nested_expression_level_exceeds_max => switch (self.type) {
|
||
.err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}),
|
||
.note => return writer.print("maximum expression level exceeded here", .{}),
|
||
.hint => return,
|
||
},
|
||
.close_paren_expression => {
|
||
try writer.writeAll("the Win32 RC compiler would accept ')' as a valid expression, but it would be skipped over and potentially lead to unexpected outcomes");
|
||
},
|
||
.unary_plus_expression => {
|
||
try writer.writeAll("the Win32 RC compiler may accept '+' as a unary operator here, but it is not supported in this implementation; consider omitting the unary +");
|
||
},
|
||
.rc_could_miscompile_control_params => switch (self.type) {
|
||
.err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
|
||
.hint => return,
|
||
},
|
||
.dangling_literal_at_eof => {
|
||
try writer.writeAll("dangling literal at end-of-file; this is not a problem, but it is likely a mistake");
|
||
},
|
||
.disjoint_code_page => switch (self.type) {
|
||
.err, .warning => return writer.print("#pragma code_page as the first thing in the .rc script can cause the input and output code pages to become out-of-sync", .{}),
|
||
.note => return writer.print("to avoid unexpected behavior, add a comment (or anything else) above the #pragma code_page line", .{}),
|
||
.hint => return,
|
||
},
|
||
.string_already_defined => switch (self.type) {
|
||
.err, .warning => {
|
||
const language = self.extra.string_and_language.language;
|
||
return writer.print("string with id {d} (0x{X}) already defined for language {f}", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language });
|
||
},
|
||
.note => return writer.print("previous definition of string with id {d} (0x{X}) here", .{ self.extra.string_and_language.id, self.extra.string_and_language.id }),
|
||
.hint => return,
|
||
},
|
||
.font_id_already_defined => switch (self.type) {
|
||
.err => return writer.print("font with id {d} already defined", .{self.extra.number}),
|
||
.warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}),
|
||
.note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}),
|
||
.hint => return,
|
||
},
|
||
.file_open_error => {
|
||
try writer.print("unable to open file '{s}': {s}", .{ strings[self.extra.file_open_error.filename_string_index], @tagName(self.extra.file_open_error.err) });
|
||
},
|
||
.invalid_accelerator_key => {
|
||
try writer.print("invalid accelerator key '{f}': {s}", .{ self.fmtToken(source), @tagName(self.extra.accelerator_error.err) });
|
||
},
|
||
.accelerator_type_required => {
|
||
try writer.writeAll("accelerator type [ASCII or VIRTKEY] required when key is an integer");
|
||
},
|
||
.accelerator_shift_or_control_without_virtkey => {
|
||
try writer.writeAll("SHIFT or CONTROL used without VIRTKEY");
|
||
},
|
||
.rc_would_miscompile_control_padding => switch (self.type) {
|
||
.err, .warning => return writer.print("the padding before this control would be miscompiled by the Win32 RC compiler (it would insert 2 extra bytes of padding)", .{}),
|
||
.note => return writer.print("to avoid the potential miscompilation, consider adding one more byte to the control data of the control preceding this one", .{}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_miscompile_control_class_ordinal => switch (self.type) {
|
||
.err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_error_on_icon_dir => switch (self.type) {
|
||
.err, .warning => return writer.print("the resource at index {} of this {s} has the format '{s}'; this would be an error in the Win32 RC compiler", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type), @tagName(self.extra.icon_dir.icon_format) }),
|
||
.note => {
|
||
// The only note supported is one specific to exactly this combination
|
||
if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable;
|
||
try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{});
|
||
},
|
||
.hint => return,
|
||
},
|
||
.format_not_supported_in_icon_dir => {
|
||
try writer.print("resource with format '{s}' (at index {}) is not allowed in {s} resource groups", .{ @tagName(self.extra.icon_dir.icon_format), self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) });
|
||
},
|
||
.icon_dir_and_resource_type_mismatch => {
|
||
const unexpected_type: rc.ResourceType = if (self.extra.resource == .icon) .cursor else .icon;
|
||
// TODO: Better wording
|
||
try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
|
||
},
|
||
.icon_read_error => {
|
||
try writer.print("unable to read {s} file '{s}': {s}", .{ @tagName(self.extra.icon_read_error.icon_type), strings[self.extra.icon_read_error.filename_string_index], @tagName(self.extra.icon_read_error.err) });
|
||
},
|
||
.rc_would_error_on_bitmap_version => switch (self.type) {
|
||
.err => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this version is no longer allowed and should be upgraded to '{s}'", .{
|
||
self.extra.icon_dir.index,
|
||
@tagName(self.extra.icon_dir.icon_type),
|
||
self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
|
||
ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(),
|
||
}),
|
||
.warning => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this would be an error in the Win32 RC compiler", .{
|
||
self.extra.icon_dir.index,
|
||
@tagName(self.extra.icon_dir.icon_type),
|
||
self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
|
||
}),
|
||
.note => unreachable,
|
||
.hint => return,
|
||
},
|
||
.max_icon_ids_exhausted => switch (self.type) {
|
||
.err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}),
|
||
.note => try writer.print("maximum icon/cursor id exceeded at index {} of this {s}", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) }),
|
||
.hint => return,
|
||
},
|
||
.bmp_read_error => {
|
||
try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) });
|
||
},
|
||
.bmp_ignored_palette_bytes => {
|
||
const bytes = strings[self.extra.number];
|
||
const ignored_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
|
||
try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes});
|
||
},
|
||
.bmp_missing_palette_bytes => {
|
||
const bytes = strings[self.extra.number];
|
||
const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
|
||
try writer.print("bitmap has {d} missing color palette bytes", .{missing_bytes});
|
||
},
|
||
.rc_would_miscompile_bmp_palette_padding => {
|
||
try writer.writeAll("the Win32 RC compiler would erroneously pad out the missing bytes");
|
||
if (self.extra.number != 0) {
|
||
const bytes = strings[self.extra.number];
|
||
const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
|
||
try writer.print(" (and the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes});
|
||
}
|
||
},
|
||
.resource_header_size_exceeds_max => {
|
||
try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
|
||
},
|
||
.resource_data_size_exceeds_max => switch (self.type) {
|
||
.err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
|
||
.note => return writer.print("maximum data length exceeded here", .{}),
|
||
.hint => return,
|
||
},
|
||
.control_extra_data_size_exceeds_max => switch (self.type) {
|
||
.err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
|
||
.note => return writer.print("maximum control data length exceeded here", .{}),
|
||
.hint => return,
|
||
},
|
||
.version_node_size_exceeds_max => switch (self.type) {
|
||
.err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
|
||
.note => return writer.print("maximum tree size exceeded while writing this child", .{}),
|
||
.hint => return,
|
||
},
|
||
.fontdir_size_exceeds_max => switch (self.type) {
|
||
.err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
|
||
.note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"),
|
||
.hint => return,
|
||
},
|
||
.number_expression_as_filename => switch (self.type) {
|
||
.err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"),
|
||
.note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}),
|
||
.hint => return,
|
||
},
|
||
.control_id_already_defined => switch (self.type) {
|
||
.err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}),
|
||
.note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}),
|
||
.hint => return,
|
||
},
|
||
.invalid_filename => {
|
||
const disallowed_codepoint = self.extra.number;
|
||
if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) {
|
||
try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))});
|
||
} else {
|
||
try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint});
|
||
}
|
||
},
|
||
.rc_would_error_u16_with_l_suffix => switch (self.type) {
|
||
.err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}),
|
||
.note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"),
|
||
.hint => return,
|
||
},
|
||
.result_contains_fontdir => return,
|
||
.rc_would_miscompile_dialog_menu_id => switch (self.type) {
|
||
.err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_miscompile_dialog_class => switch (self.type) {
|
||
.err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}),
|
||
.note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) {
|
||
.err, .warning => return,
|
||
.note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}),
|
||
.hint => return,
|
||
},
|
||
.rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) {
|
||
.err, .warning => return,
|
||
.note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"),
|
||
.hint => return,
|
||
},
|
||
.dialog_menu_id_was_uppercased => return,
|
||
.duplicate_optional_statement_skipped => {
|
||
return writer.writeAll("this statement was ignored; when multiple statements of the same type are specified, only the last takes precedence");
|
||
},
|
||
.invalid_digit_character_in_ordinal => {
|
||
return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
|
||
},
|
||
.rc_would_miscompile_codepoint_whitespace => {
|
||
const treated_as = self.extra.number >> 8;
|
||
return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (it would get treated as U+{X:0>4})", .{ self.extra.number, treated_as });
|
||
},
|
||
.rc_would_miscompile_codepoint_skip => {
|
||
return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (the codepoint would be missing from the compiled resource)", .{self.extra.number});
|
||
},
|
||
.rc_would_miscompile_codepoint_bom => switch (self.type) {
|
||
.err, .warning => return writer.print("codepoint U+{X:0>4} within a string literal would cause the entire file to be miscompiled by the Win32 RC compiler", .{self.extra.number}),
|
||
.note => return writer.writeAll("the presence of this codepoint causes all non-ASCII codepoints to be byteswapped by the Win32 RC preprocessor"),
|
||
.hint => return,
|
||
},
|
||
.tab_converted_to_spaces => switch (self.type) {
|
||
.err, .warning => return writer.writeAll("the tab character(s) in this string will be converted into a variable number of spaces (determined by the column of the tab character in the .rc file)"),
|
||
.note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"),
|
||
.hint => return,
|
||
},
|
||
.win32_non_ascii_ordinal => switch (self.type) {
|
||
.err, .warning => unreachable,
|
||
.note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}),
|
||
.hint => return,
|
||
},
|
||
.failed_to_open_cwd => {
|
||
try writer.print("failed to open CWD for compilation: {s}", .{@tagName(self.extra.file_open_error.err)});
|
||
},
|
||
}
|
||
}
|
||
|
||
pub const VisualTokenInfo = struct {
|
||
before_len: usize,
|
||
point_offset: usize,
|
||
after_len: usize,
|
||
};
|
||
|
||
pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize, source: []const u8) VisualTokenInfo {
|
||
return switch (self.err) {
|
||
// These can technically be more than 1 byte depending on encoding,
|
||
// but they always refer to one visual character/grapheme.
|
||
.illegal_byte,
|
||
.illegal_byte_outside_string_literals,
|
||
.illegal_codepoint_outside_string_literals,
|
||
.illegal_byte_order_mark,
|
||
.illegal_private_use_character,
|
||
=> .{
|
||
.before_len = 0,
|
||
.point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
|
||
.after_len = 0,
|
||
},
|
||
else => .{
|
||
.before_len = before: {
|
||
const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
|
||
break :before cellCount(self.code_page, source, start, self.token.start);
|
||
},
|
||
.point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
|
||
.after_len = after: {
|
||
const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
|
||
// end may be less than start when pointing to EOF
|
||
if (end <= self.token.start) break :after 0;
|
||
break :after cellCount(self.code_page, source, self.token.start, end) - 1;
|
||
},
|
||
},
|
||
};
|
||
}
|
||
};
|
||
|
||
/// Convenience struct only useful when the code page can be inferred from the token
|
||
pub const ErrorDetailsWithoutCodePage = blk: {
|
||
const details_info = @typeInfo(ErrorDetails);
|
||
const fields = details_info.@"struct".fields;
|
||
var fields_without_codepage: [fields.len - 1]std.builtin.Type.StructField = undefined;
|
||
var i: usize = 0;
|
||
for (fields) |field| {
|
||
if (std.mem.eql(u8, field.name, "code_page")) continue;
|
||
fields_without_codepage[i] = field;
|
||
i += 1;
|
||
}
|
||
std.debug.assert(i == fields_without_codepage.len);
|
||
break :blk @Type(.{ .@"struct" = .{
|
||
.layout = .auto,
|
||
.fields = &fields_without_codepage,
|
||
.decls = &.{},
|
||
.is_tuple = false,
|
||
} });
|
||
};
|
||
|
||
fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usize, end_index: usize) usize {
|
||
// Note: This is an imperfect solution. A proper implementation here would
|
||
// involve full grapheme cluster awareness + grapheme width data, but oh well.
|
||
var codepoint_count: usize = 0;
|
||
var index: usize = start_index;
|
||
while (index < end_index) {
|
||
const codepoint = code_page.codepointAt(index, source) orelse break;
|
||
defer index += codepoint.byte_len;
|
||
_ = codepointForDisplay(codepoint) orelse continue;
|
||
codepoint_count += 1;
|
||
// no need to count more than we will display
|
||
if (codepoint_count >= max_source_line_codepoints + truncated_str.len) break;
|
||
}
|
||
return codepoint_count;
|
||
}
|
||
|
||
const truncated_str = "<...truncated...>";
|
||
|
||
pub fn renderErrorMessage(
|
||
io: Io,
|
||
writer: *std.Io.Writer,
|
||
tty_config: std.Io.tty.Config,
|
||
cwd: std.fs.Dir,
|
||
err_details: ErrorDetails,
|
||
source: []const u8,
|
||
strings: []const []const u8,
|
||
source_mappings: ?SourceMappings,
|
||
) !void {
|
||
if (err_details.type == .hint) return;
|
||
|
||
const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
|
||
// Treat tab stops as 1 column wide for error display purposes,
|
||
// and add one to get a 1-based column
|
||
const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
|
||
|
||
const corresponding_span: ?SourceMappings.CorrespondingSpan = if (source_mappings) |mappings|
|
||
mappings.getCorrespondingSpan(err_details.token.line_number)
|
||
else
|
||
null;
|
||
const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null)
|
||
source_mappings.?.files.get(corresponding_span.?.filename_offset)
|
||
else
|
||
null;
|
||
|
||
const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number;
|
||
|
||
try tty_config.setColor(writer, .bold);
|
||
if (corresponding_file) |file| {
|
||
try writer.writeAll(file);
|
||
} else {
|
||
try tty_config.setColor(writer, .dim);
|
||
try writer.writeAll("<after preprocessor>");
|
||
try tty_config.setColor(writer, .reset);
|
||
try tty_config.setColor(writer, .bold);
|
||
}
|
||
try writer.print(":{d}:{d}: ", .{ err_line, column });
|
||
switch (err_details.type) {
|
||
.err => {
|
||
try tty_config.setColor(writer, .red);
|
||
try writer.writeAll("error: ");
|
||
},
|
||
.warning => {
|
||
try tty_config.setColor(writer, .yellow);
|
||
try writer.writeAll("warning: ");
|
||
},
|
||
.note => {
|
||
try tty_config.setColor(writer, .cyan);
|
||
try writer.writeAll("note: ");
|
||
},
|
||
.hint => unreachable,
|
||
}
|
||
try tty_config.setColor(writer, .reset);
|
||
try tty_config.setColor(writer, .bold);
|
||
try err_details.render(writer, source, strings);
|
||
try writer.writeByte('\n');
|
||
try tty_config.setColor(writer, .reset);
|
||
|
||
if (!err_details.print_source_line) {
|
||
try writer.writeByte('\n');
|
||
return;
|
||
}
|
||
|
||
const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
|
||
const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len, source);
|
||
const truncated_visual_info = ErrorDetails.VisualTokenInfo{
|
||
.before_len = if (visual_info.point_offset > max_source_line_codepoints and visual_info.before_len > 0)
|
||
(visual_info.before_len + 1) -| (visual_info.point_offset - max_source_line_codepoints)
|
||
else
|
||
visual_info.before_len,
|
||
.point_offset = @min(max_source_line_codepoints + 1, visual_info.point_offset),
|
||
.after_len = if (visual_info.point_offset > max_source_line_codepoints)
|
||
@min(truncated_str.len - 3, visual_info.after_len)
|
||
else
|
||
@min(max_source_line_codepoints - visual_info.point_offset + (truncated_str.len - 2), visual_info.after_len),
|
||
};
|
||
|
||
// Need this to determine if the 'line originated from' note is worth printing
|
||
var source_line_for_display_buf: [max_source_line_bytes]u8 = undefined;
|
||
const source_line_for_display = writeSourceSlice(&source_line_for_display_buf, source_line, err_details.code_page);
|
||
|
||
try writer.writeAll(source_line_for_display.line);
|
||
if (source_line_for_display.truncated) {
|
||
try tty_config.setColor(writer, .dim);
|
||
try writer.writeAll(truncated_str);
|
||
try tty_config.setColor(writer, .reset);
|
||
}
|
||
try writer.writeByte('\n');
|
||
|
||
try tty_config.setColor(writer, .green);
|
||
const num_spaces = truncated_visual_info.point_offset - truncated_visual_info.before_len;
|
||
try writer.splatByteAll(' ', num_spaces);
|
||
try writer.splatByteAll('~', truncated_visual_info.before_len);
|
||
try writer.writeByte('^');
|
||
try writer.splatByteAll('~', truncated_visual_info.after_len);
|
||
try writer.writeByte('\n');
|
||
try tty_config.setColor(writer, .reset);
|
||
|
||
if (corresponding_span != null and corresponding_file != null) {
|
||
var worth_printing_lines: bool = true;
|
||
var initial_lines_err: ?anyerror = null;
|
||
var file_reader_buf: [max_source_line_bytes * 2]u8 = undefined;
|
||
var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init(
|
||
io,
|
||
cwd,
|
||
err_details,
|
||
source_line_for_display.line,
|
||
corresponding_span.?,
|
||
corresponding_file.?,
|
||
&file_reader_buf,
|
||
) catch |err| switch (err) {
|
||
error.NotWorthPrintingLines => blk: {
|
||
worth_printing_lines = false;
|
||
break :blk null;
|
||
},
|
||
error.NotWorthPrintingNote => return,
|
||
else => |e| blk: {
|
||
initial_lines_err = e;
|
||
break :blk null;
|
||
},
|
||
};
|
||
defer if (corresponding_lines) |*cl| cl.deinit();
|
||
|
||
try tty_config.setColor(writer, .bold);
|
||
if (corresponding_file) |file| {
|
||
try writer.writeAll(file);
|
||
} else {
|
||
try tty_config.setColor(writer, .dim);
|
||
try writer.writeAll("<after preprocessor>");
|
||
try tty_config.setColor(writer, .reset);
|
||
try tty_config.setColor(writer, .bold);
|
||
}
|
||
try writer.print(":{d}:{d}: ", .{ err_line, column });
|
||
try tty_config.setColor(writer, .cyan);
|
||
try writer.writeAll("note: ");
|
||
try tty_config.setColor(writer, .reset);
|
||
try tty_config.setColor(writer, .bold);
|
||
try writer.writeAll("this line originated from line");
|
||
if (corresponding_span.?.start_line != corresponding_span.?.end_line) {
|
||
try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line });
|
||
} else {
|
||
try writer.print(" {}", .{corresponding_span.?.start_line});
|
||
}
|
||
try writer.print(" of file '{s}'\n", .{corresponding_file.?});
|
||
try tty_config.setColor(writer, .reset);
|
||
|
||
if (!worth_printing_lines) return;
|
||
|
||
const write_lines_err: ?anyerror = write_lines: {
|
||
if (initial_lines_err) |err| break :write_lines err;
|
||
while (corresponding_lines.?.next() catch |err| {
|
||
break :write_lines err;
|
||
}) |display_line| {
|
||
try writer.writeAll(display_line.line);
|
||
if (display_line.truncated) {
|
||
try tty_config.setColor(writer, .dim);
|
||
try writer.writeAll(truncated_str);
|
||
try tty_config.setColor(writer, .reset);
|
||
}
|
||
try writer.writeByte('\n');
|
||
}
|
||
break :write_lines null;
|
||
};
|
||
if (write_lines_err) |err| {
|
||
try tty_config.setColor(writer, .red);
|
||
try writer.writeAll(" | ");
|
||
try tty_config.setColor(writer, .reset);
|
||
try tty_config.setColor(writer, .dim);
|
||
try writer.print("unable to print line(s) from file: {s}\n", .{@errorName(err)});
|
||
try tty_config.setColor(writer, .reset);
|
||
}
|
||
try writer.writeByte('\n');
|
||
}
|
||
}
|
||
|
||
const VisualLine = struct {
|
||
line: []u8,
|
||
truncated: bool,
|
||
};
|
||
|
||
const CorrespondingLines = struct {
|
||
// enough room for one more codepoint, just so that we don't have to keep
|
||
// track of this being truncated, since the extra codepoint will ensure
|
||
// the visual line will need to truncate in that case.
|
||
line_buf: [max_source_line_bytes + 4]u8 = undefined,
|
||
line_len: usize = 0,
|
||
visual_line_buf: [max_source_line_bytes]u8 = undefined,
|
||
visual_line_len: usize = 0,
|
||
truncated: bool = false,
|
||
line_num: usize = 1,
|
||
initial_line: bool = true,
|
||
last_byte: u8 = 0,
|
||
at_eof: bool = false,
|
||
span: SourceMappings.CorrespondingSpan,
|
||
file: std.fs.File,
|
||
file_reader: std.fs.File.Reader,
|
||
code_page: SupportedCodePage,
|
||
|
||
pub fn init(
|
||
io: Io,
|
||
cwd: std.fs.Dir,
|
||
err_details: ErrorDetails,
|
||
line_for_comparison: []const u8,
|
||
corresponding_span: SourceMappings.CorrespondingSpan,
|
||
corresponding_file: []const u8,
|
||
file_reader_buf: []u8,
|
||
) !CorrespondingLines {
|
||
// We don't do line comparison for this error, so don't print the note if the line
|
||
// number is different
|
||
if (err_details.err == .string_literal_too_long and err_details.token.line_number != corresponding_span.start_line) {
|
||
return error.NotWorthPrintingNote;
|
||
}
|
||
|
||
// Don't print the originating line for this error, we know it's really long
|
||
if (err_details.err == .string_literal_too_long) {
|
||
return error.NotWorthPrintingLines;
|
||
}
|
||
|
||
var corresponding_lines = CorrespondingLines{
|
||
.span = corresponding_span,
|
||
.file = try utils.openFileNotDir(cwd, corresponding_file, .{}),
|
||
.code_page = err_details.code_page,
|
||
.file_reader = undefined,
|
||
};
|
||
corresponding_lines.file_reader = corresponding_lines.file.reader(io, file_reader_buf);
|
||
errdefer corresponding_lines.deinit();
|
||
|
||
try corresponding_lines.writeLineFromStreamVerbatim(
|
||
&corresponding_lines.file_reader.interface,
|
||
corresponding_span.start_line,
|
||
);
|
||
|
||
const visual_line = writeSourceSlice(
|
||
&corresponding_lines.visual_line_buf,
|
||
corresponding_lines.line_buf[0..corresponding_lines.line_len],
|
||
err_details.code_page,
|
||
);
|
||
corresponding_lines.visual_line_len = visual_line.line.len;
|
||
corresponding_lines.truncated = visual_line.truncated;
|
||
|
||
// If the lines are the same as they were before preprocessing, skip printing the note entirely
|
||
if (corresponding_span.start_line == corresponding_span.end_line and std.mem.eql(
|
||
u8,
|
||
line_for_comparison,
|
||
corresponding_lines.visual_line_buf[0..corresponding_lines.visual_line_len],
|
||
)) {
|
||
return error.NotWorthPrintingNote;
|
||
}
|
||
|
||
return corresponding_lines;
|
||
}
|
||
|
||
pub fn next(self: *CorrespondingLines) !?VisualLine {
|
||
if (self.initial_line) {
|
||
self.initial_line = false;
|
||
return .{
|
||
.line = self.visual_line_buf[0..self.visual_line_len],
|
||
.truncated = self.truncated,
|
||
};
|
||
}
|
||
if (self.line_num > self.span.end_line) return null;
|
||
if (self.at_eof) return error.LinesNotFound;
|
||
|
||
self.line_len = 0;
|
||
self.visual_line_len = 0;
|
||
|
||
try self.writeLineFromStreamVerbatim(
|
||
&self.file_reader.interface,
|
||
self.line_num,
|
||
);
|
||
|
||
const visual_line = writeSourceSlice(
|
||
&self.visual_line_buf,
|
||
self.line_buf[0..self.line_len],
|
||
self.code_page,
|
||
);
|
||
self.visual_line_len = visual_line.line.len;
|
||
|
||
return visual_line;
|
||
}
|
||
|
||
fn writeLineFromStreamVerbatim(self: *CorrespondingLines, input: *std.Io.Reader, line_num: usize) !void {
|
||
while (try readByteOrEof(input)) |byte| {
|
||
switch (byte) {
|
||
'\n', '\r' => {
|
||
if (!utils.isLineEndingPair(self.last_byte, byte)) {
|
||
const line_complete = self.line_num == line_num;
|
||
self.line_num += 1;
|
||
if (line_complete) {
|
||
self.last_byte = byte;
|
||
return;
|
||
}
|
||
} else {
|
||
// reset last_byte to a non-line ending so that
|
||
// consecutive CRLF pairs don't get treated as one
|
||
// long line ending 'pair'
|
||
self.last_byte = 0;
|
||
continue;
|
||
}
|
||
},
|
||
else => {
|
||
if (self.line_num == line_num and self.line_len < self.line_buf.len) {
|
||
self.line_buf[self.line_len] = byte;
|
||
self.line_len += 1;
|
||
}
|
||
},
|
||
}
|
||
self.last_byte = byte;
|
||
}
|
||
self.at_eof = true;
|
||
// hacky way to get next to return null
|
||
self.line_num += 1;
|
||
}
|
||
|
||
fn readByteOrEof(reader: *std.Io.Reader) !?u8 {
|
||
return reader.takeByte() catch |err| switch (err) {
|
||
error.EndOfStream => return null,
|
||
else => |e| return e,
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *CorrespondingLines) void {
|
||
self.file.close();
|
||
}
|
||
};
|
||
|
||
const max_source_line_codepoints = 120;
|
||
const max_source_line_bytes = max_source_line_codepoints * 4;
|
||
|
||
fn writeSourceSlice(buf: []u8, slice: []const u8, code_page: SupportedCodePage) VisualLine {
|
||
var src_i: usize = 0;
|
||
var dest_i: usize = 0;
|
||
var codepoint_count: usize = 0;
|
||
while (src_i < slice.len) {
|
||
const codepoint = code_page.codepointAt(src_i, slice) orelse break;
|
||
defer src_i += codepoint.byte_len;
|
||
const display_codepoint = codepointForDisplay(codepoint) orelse continue;
|
||
codepoint_count += 1;
|
||
if (codepoint_count > max_source_line_codepoints) {
|
||
return .{ .line = buf[0..dest_i], .truncated = true };
|
||
}
|
||
const utf8_len = std.unicode.utf8Encode(display_codepoint, buf[dest_i..]) catch unreachable;
|
||
dest_i += utf8_len;
|
||
}
|
||
return .{ .line = buf[0..dest_i], .truncated = false };
|
||
}
|
||
|
||
fn codepointForDisplay(codepoint: code_pages.Codepoint) ?u21 {
|
||
return switch (codepoint.value) {
|
||
'\x00'...'\x08',
|
||
'\x0E'...'\x1F',
|
||
'\x7F',
|
||
code_pages.Codepoint.invalid,
|
||
=> '<27>',
|
||
// \r is seemingly ignored by the RC compiler so skipping it when printing source lines
|
||
// could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
|
||
// in the console as DATA but the compiler reads it as RCDATA)
|
||
//
|
||
// NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
|
||
// characters get converted to \n, but may become relevant if another
|
||
// preprocessor is used instead.
|
||
'\r' => null,
|
||
'\t', '\x0B', '\x0C' => ' ',
|
||
else => |v| v,
|
||
};
|
||
}
|