errors.zig (62242B) - Raw
1 const std = @import("std"); 2 const assert = std.debug.assert; 3 const Token = @import("lex.zig").Token; 4 const SourceMappings = @import("source_mapping.zig").SourceMappings; 5 const utils = @import("utils.zig"); 6 const rc = @import("rc.zig"); 7 const res = @import("res.zig"); 8 const ico = @import("ico.zig"); 9 const bmp = @import("bmp.zig"); 10 const parse = @import("parse.zig"); 11 const lang = @import("lang.zig"); 12 const code_pages = @import("code_pages.zig"); 13 const SupportedCodePage = code_pages.SupportedCodePage; 14 const builtin = @import("builtin"); 15 const native_endian = builtin.cpu.arch.endian(); 16 17 pub const Diagnostics = struct { 18 errors: std.ArrayListUnmanaged(ErrorDetails) = .empty, 19 /// Append-only, cannot handle removing strings. 20 /// Expects to own all strings within the list. 21 strings: std.ArrayListUnmanaged([]const u8) = .empty, 22 allocator: std.mem.Allocator, 23 24 pub fn init(allocator: std.mem.Allocator) Diagnostics { 25 return .{ 26 .allocator = allocator, 27 }; 28 } 29 30 pub fn deinit(self: *Diagnostics) void { 31 self.errors.deinit(self.allocator); 32 for (self.strings.items) |str| { 33 self.allocator.free(str); 34 } 35 self.strings.deinit(self.allocator); 36 } 37 38 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void { 39 try self.errors.append(self.allocator, error_details); 40 } 41 42 const SmallestStringIndexType = std.meta.Int(.unsigned, @min( 43 @bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex), 44 @min( 45 @bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex), 46 @bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex), 47 ), 48 )); 49 50 /// Returns the index of the added string as the SmallestStringIndexType 51 /// in order to avoid needing to `@intCast` it at callsites of putString. 52 /// Instead, this function will error if the index would ever exceed the 53 /// smallest FilenameStringIndex of an ErrorDetails type. 54 pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType { 55 if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) { 56 return error.OutOfMemory; // ran out of string indexes 57 } 58 const dupe = try self.allocator.dupe(u8, str); 59 const index = self.strings.items.len; 60 try self.strings.append(self.allocator, dupe); 61 return @intCast(index); 62 } 63 64 pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.io.tty.Config, source_mappings: ?SourceMappings) void { 65 const stderr = std.debug.lockStderrWriter(&.{}); 66 defer std.debug.unlockStderrWriter(); 67 for (self.errors.items) |err_details| { 68 renderErrorMessage(stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return; 69 } 70 } 71 72 pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void { 73 const tty_config = std.io.tty.detectConfig(std.fs.File.stderr()); 74 return self.renderToStdErr(cwd, source, tty_config, source_mappings); 75 } 76 77 pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool { 78 for (self.errors.items) |details| { 79 if (details.err == err) return true; 80 } 81 return false; 82 } 83 84 pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool { 85 for (self.errors.items) |details| { 86 for (errors) |err| { 87 if (details.err == err) return true; 88 } 89 } 90 return false; 91 } 92 }; 93 94 /// Contains enough context to append errors/warnings/notes etc 95 pub const DiagnosticsContext = struct { 96 diagnostics: *Diagnostics, 97 token: Token, 98 /// Code page of the source file at the token location 99 code_page: SupportedCodePage, 100 }; 101 102 pub const ErrorDetails = struct { 103 err: Error, 104 token: Token, 105 /// Code page of the source file at the token location 106 code_page: SupportedCodePage, 107 /// If non-null, should be before `token`. If null, `token` is assumed to be the start. 108 token_span_start: ?Token = null, 109 /// If non-null, should be after `token`. If null, `token` is assumed to be the end. 110 token_span_end: ?Token = null, 111 type: Type = .err, 112 print_source_line: bool = true, 113 extra: Extra = .{ .none = {} }, 114 115 pub const Type = enum { 116 /// Fatal error, stops compilation 117 err, 118 /// Warning that does not affect compilation result 119 warning, 120 /// A note that typically provides further context for a warning/error 121 note, 122 /// An invisible diagnostic that is not printed to stderr but can 123 /// provide information useful when comparing the behavior of different 124 /// implementations. For example, a hint is emitted when a FONTDIR resource 125 /// was included in the .RES file which is significant because rc.exe 126 /// does something different than us, but ultimately it's not important 127 /// enough to be a warning/note. 128 hint, 129 }; 130 131 pub const Extra = union { 132 none: void, 133 expected: Token.Id, 134 number: u32, 135 expected_types: ExpectedTypes, 136 resource: rc.ResourceType, 137 string_and_language: StringAndLanguage, 138 file_open_error: FileOpenError, 139 icon_read_error: IconReadError, 140 icon_dir: IconDirContext, 141 bmp_read_error: BitmapReadError, 142 accelerator_error: AcceleratorError, 143 statement_with_u16_param: StatementWithU16Param, 144 menu_or_class: enum { class, menu }, 145 }; 146 147 comptime { 148 // all fields in the extra union should be 32 bits or less 149 for (std.meta.fields(Extra)) |field| { 150 std.debug.assert(@bitSizeOf(field.type) <= 32); 151 } 152 } 153 154 pub const StatementWithU16Param = enum(u32) { 155 fileversion, 156 productversion, 157 language, 158 }; 159 160 pub const StringAndLanguage = packed struct(u32) { 161 id: u16, 162 language: res.Language, 163 }; 164 165 pub const FileOpenError = packed struct(u32) { 166 err: FileOpenErrorEnum, 167 filename_string_index: FilenameStringIndex, 168 169 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum)); 170 pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError); 171 172 pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum { 173 return switch (err) { 174 inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)), 175 }; 176 } 177 }; 178 179 pub const IconReadError = packed struct(u32) { 180 err: IconReadErrorEnum, 181 icon_type: enum(u1) { cursor, icon }, 182 filename_string_index: FilenameStringIndex, 183 184 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1); 185 pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError); 186 187 pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum { 188 return switch (err) { 189 inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)), 190 }; 191 } 192 }; 193 194 pub const IconDirContext = packed struct(u32) { 195 icon_type: enum(u1) { cursor, icon }, 196 icon_format: ico.ImageFormat, 197 index: u16, 198 bitmap_version: ico.BitmapHeader.Version = .unknown, 199 _: Padding = 0, 200 201 pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat)); 202 }; 203 204 pub const BitmapReadError = packed struct(u32) { 205 err: BitmapReadErrorEnum, 206 filename_string_index: FilenameStringIndex, 207 208 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum)); 209 pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError); 210 211 pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum { 212 return switch (err) { 213 inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)), 214 }; 215 } 216 }; 217 218 pub const BitmapUnsupportedDIB = packed struct(u32) { 219 dib_version: ico.BitmapHeader.Version, 220 filename_string_index: FilenameStringIndex, 221 222 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version)); 223 }; 224 225 pub const AcceleratorError = packed struct(u32) { 226 err: AcceleratorErrorEnum, 227 _: Padding = 0, 228 229 pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum)); 230 pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError); 231 232 pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum { 233 return switch (err) { 234 inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)), 235 }; 236 } 237 }; 238 239 pub const ExpectedTypes = packed struct(u32) { 240 number: bool = false, 241 number_expression: bool = false, 242 string_literal: bool = false, 243 accelerator_type_or_option: bool = false, 244 control_class: bool = false, 245 literal: bool = false, 246 // Note: This being 0 instead of undefined is arbitrary and something of a workaround, 247 // see https://github.com/ziglang/zig/issues/15395 248 _: u26 = 0, 249 250 pub const strings = std.StaticStringMap([]const u8).initComptime(.{ 251 .{ "number", "number" }, 252 .{ "number_expression", "number expression" }, 253 .{ "string_literal", "quoted string literal" }, 254 .{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" }, 255 .{ "control_class", "control class [BUTTON, EDIT, etc]" }, 256 .{ "literal", "unquoted literal" }, 257 }); 258 259 pub fn writeCommaSeparated(self: ExpectedTypes, writer: anytype) !void { 260 const struct_info = @typeInfo(ExpectedTypes).@"struct"; 261 const num_real_fields = struct_info.fields.len - 1; 262 const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields; 263 const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits; 264 const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask; 265 const num_set_bits = @popCount(relevant_bits_only); 266 267 var i: usize = 0; 268 inline for (struct_info.fields) |field_info| { 269 if (field_info.type != bool) continue; 270 if (i == num_set_bits) return; 271 if (@field(self, field_info.name)) { 272 try writer.writeAll(strings.get(field_info.name).?); 273 i += 1; 274 if (num_set_bits > 2 and i != num_set_bits) { 275 try writer.writeAll(", "); 276 } else if (i != num_set_bits) { 277 try writer.writeByte(' '); 278 } 279 if (num_set_bits > 1 and i == num_set_bits - 1) { 280 try writer.writeAll("or "); 281 } 282 } 283 } 284 } 285 }; 286 287 pub const Error = enum { 288 // Lexer 289 unfinished_string_literal, 290 string_literal_too_long, 291 invalid_number_with_exponent, 292 invalid_digit_character_in_number_literal, 293 illegal_byte, 294 illegal_byte_outside_string_literals, 295 illegal_codepoint_outside_string_literals, 296 illegal_byte_order_mark, 297 illegal_private_use_character, 298 found_c_style_escaped_quote, 299 code_page_pragma_missing_left_paren, 300 code_page_pragma_missing_right_paren, 301 code_page_pragma_invalid_code_page, 302 code_page_pragma_not_integer, 303 code_page_pragma_overflow, 304 code_page_pragma_unsupported_code_page, 305 306 // Parser 307 unfinished_raw_data_block, 308 unfinished_string_table_block, 309 /// `expected` is populated. 310 expected_token, 311 /// `expected_types` is populated 312 expected_something_else, 313 /// `resource` is populated 314 resource_type_cant_use_raw_data, 315 /// `resource` is populated 316 id_must_be_ordinal, 317 /// `resource` is populated 318 name_or_id_not_allowed, 319 string_resource_as_numeric_type, 320 ascii_character_not_equivalent_to_virtual_key_code, 321 empty_menu_not_allowed, 322 rc_would_miscompile_version_value_padding, 323 rc_would_miscompile_version_value_byte_count, 324 code_page_pragma_in_included_file, 325 nested_resource_level_exceeds_max, 326 too_many_dialog_controls_or_toolbar_buttons, 327 nested_expression_level_exceeds_max, 328 close_paren_expression, 329 unary_plus_expression, 330 rc_could_miscompile_control_params, 331 dangling_literal_at_eof, 332 disjoint_code_page, 333 334 // Compiler 335 /// `string_and_language` is populated 336 string_already_defined, 337 font_id_already_defined, 338 /// `file_open_error` is populated 339 file_open_error, 340 /// `accelerator_error` is populated 341 invalid_accelerator_key, 342 accelerator_type_required, 343 accelerator_shift_or_control_without_virtkey, 344 rc_would_miscompile_control_padding, 345 rc_would_miscompile_control_class_ordinal, 346 /// `icon_dir` is populated 347 rc_would_error_on_icon_dir, 348 /// `icon_dir` is populated 349 format_not_supported_in_icon_dir, 350 /// `resource` is populated and contains the expected type 351 icon_dir_and_resource_type_mismatch, 352 /// `icon_read_error` is populated 353 icon_read_error, 354 /// `icon_dir` is populated 355 rc_would_error_on_bitmap_version, 356 /// `icon_dir` is populated 357 max_icon_ids_exhausted, 358 /// `bmp_read_error` is populated 359 bmp_read_error, 360 /// `number` is populated and contains a string index for which the string contains 361 /// the bytes of a `u64` (native endian). The `u64` contains the number of ignored bytes. 362 bmp_ignored_palette_bytes, 363 /// `number` is populated and contains a string index for which the string contains 364 /// the bytes of a `u64` (native endian). The `u64` contains the number of missing bytes. 365 bmp_missing_palette_bytes, 366 /// `number` is populated and contains a string index for which the string contains 367 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes. 368 rc_would_miscompile_bmp_palette_padding, 369 resource_header_size_exceeds_max, 370 resource_data_size_exceeds_max, 371 control_extra_data_size_exceeds_max, 372 version_node_size_exceeds_max, 373 fontdir_size_exceeds_max, 374 /// `number` is populated and contains a string index for the filename 375 number_expression_as_filename, 376 /// `number` is populated and contains the control ID that is a duplicate 377 control_id_already_defined, 378 /// `number` is populated and contains the disallowed codepoint 379 invalid_filename, 380 /// `statement_with_u16_param` is populated 381 rc_would_error_u16_with_l_suffix, 382 result_contains_fontdir, 383 /// `number` is populated and contains the ordinal value that the id would be miscompiled to 384 rc_would_miscompile_dialog_menu_id, 385 /// `number` is populated and contains the ordinal value that the value would be miscompiled to 386 rc_would_miscompile_dialog_class, 387 /// `menu_or_class` is populated and contains the type of the parameter statement 388 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal, 389 rc_would_miscompile_dialog_menu_id_starts_with_digit, 390 dialog_menu_id_was_uppercased, 391 duplicate_optional_statement_skipped, 392 invalid_digit_character_in_ordinal, 393 394 // Literals 395 /// `number` is populated 396 rc_would_miscompile_codepoint_whitespace, 397 /// `number` is populated 398 rc_would_miscompile_codepoint_skip, 399 /// `number` is populated 400 rc_would_miscompile_codepoint_bom, 401 tab_converted_to_spaces, 402 403 // General (used in various places) 404 /// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation 405 win32_non_ascii_ordinal, 406 407 // Initialization 408 /// `file_open_error` is populated, but `filename_string_index` is not 409 failed_to_open_cwd, 410 }; 411 412 fn formatToken(ctx: TokenFormatContext, writer: *std.io.Writer) std.io.Writer.Error!void { 413 switch (ctx.token.id) { 414 .eof => return writer.writeAll(ctx.token.id.nameForErrorDisplay()), 415 else => {}, 416 } 417 418 const slice = ctx.token.slice(ctx.source); 419 var src_i: usize = 0; 420 while (src_i < slice.len) { 421 const codepoint = ctx.code_page.codepointAt(src_i, slice) orelse break; 422 defer src_i += codepoint.byte_len; 423 const display_codepoint = codepointForDisplay(codepoint) orelse continue; 424 var buf: [4]u8 = undefined; 425 const utf8_len = std.unicode.utf8Encode(display_codepoint, &buf) catch unreachable; 426 try writer.writeAll(buf[0..utf8_len]); 427 } 428 } 429 430 const TokenFormatContext = struct { 431 token: Token, 432 source: []const u8, 433 code_page: SupportedCodePage, 434 }; 435 436 fn fmtToken(self: ErrorDetails, source: []const u8) std.fmt.Formatter(TokenFormatContext, formatToken) { 437 return .{ .data = .{ 438 .token = self.token, 439 .code_page = self.code_page, 440 .source = source, 441 } }; 442 } 443 444 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void { 445 switch (self.err) { 446 .unfinished_string_literal => { 447 return writer.print("unfinished string literal at '{f}', expected closing '\"'", .{self.fmtToken(source)}); 448 }, 449 .string_literal_too_long => { 450 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number}); 451 }, 452 .invalid_number_with_exponent => { 453 return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)}); 454 }, 455 .invalid_digit_character_in_number_literal => switch (self.type) { 456 .err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"), 457 .note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"), 458 .hint => return, 459 }, 460 .illegal_byte => { 461 return writer.print("character '{f}' is not allowed", .{ 462 std.ascii.hexEscape(self.token.slice(source), .upper), 463 }); 464 }, 465 .illegal_byte_outside_string_literals => { 466 return writer.print("character '{f}' is not allowed outside of string literals", .{ 467 std.ascii.hexEscape(self.token.slice(source), .upper), 468 }); 469 }, 470 .illegal_codepoint_outside_string_literals => { 471 // This is somewhat hacky, but we know that: 472 // - This error is only possible with codepoints outside of the Windows-1252 character range 473 // - So, the only supported code page that could generate this error is UTF-8 474 // Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal 475 // codepoint. 476 // 477 // FIXME: Support other code pages if they become relevant 478 const bytes = self.token.slice(source); 479 const codepoint = std.unicode.utf8Decode(bytes) catch unreachable; 480 return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint}); 481 }, 482 .illegal_byte_order_mark => { 483 return writer.writeAll("byte order mark <U+FEFF> is not allowed"); 484 }, 485 .illegal_private_use_character => { 486 return writer.writeAll("private use character <U+E000> is not allowed"); 487 }, 488 .found_c_style_escaped_quote => { 489 return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)"); 490 }, 491 .code_page_pragma_missing_left_paren => { 492 return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page"); 493 }, 494 .code_page_pragma_missing_right_paren => { 495 return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page"); 496 }, 497 .code_page_pragma_invalid_code_page => { 498 return writer.writeAll("invalid or unknown code page in #pragma code_page"); 499 }, 500 .code_page_pragma_not_integer => { 501 return writer.writeAll("code page is not a valid integer in #pragma code_page"); 502 }, 503 .code_page_pragma_overflow => { 504 return writer.writeAll("code page too large in #pragma code_page"); 505 }, 506 .code_page_pragma_unsupported_code_page => { 507 // We know that the token slice is a well-formed #pragma code_page(N), so 508 // we can skip to the first ( and then get the number that follows 509 const token_slice = self.token.slice(source); 510 var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1; 511 while (std.ascii.isWhitespace(token_slice[number_start])) { 512 number_start += 1; 513 } 514 var number_slice = token_slice[number_start..number_start]; 515 while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) { 516 number_slice.len += 1; 517 } 518 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable; 519 const code_page = code_pages.getByIdentifier(number) catch unreachable; 520 // TODO: Improve or maybe add a note making it more clear that the code page 521 // is valid and that the code page is unsupported purely due to a limitation 522 // in this compiler. 523 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number }); 524 }, 525 .unfinished_raw_data_block => { 526 return writer.print("unfinished raw data block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)}); 527 }, 528 .unfinished_string_table_block => { 529 return writer.print("unfinished STRINGTABLE block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)}); 530 }, 531 .expected_token => { 532 return writer.print("expected '{s}', got '{f}'", .{ self.extra.expected.nameForErrorDisplay(), self.fmtToken(source) }); 533 }, 534 .expected_something_else => { 535 try writer.writeAll("expected "); 536 try self.extra.expected_types.writeCommaSeparated(writer); 537 return writer.print("; got '{f}'", .{self.fmtToken(source)}); 538 }, 539 .resource_type_cant_use_raw_data => switch (self.type) { 540 .err, .warning => try writer.print("expected '<filename>', found '{f}' (resource type '{s}' can't use raw data)", .{ self.fmtToken(source), self.extra.resource.nameForErrorDisplay() }), 541 .note => try writer.print("if '{f}' is intended to be a filename, it must be specified as a quoted string literal", .{self.fmtToken(source)}), 542 .hint => return, 543 }, 544 .id_must_be_ordinal => { 545 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{f}'", .{ self.extra.resource.nameForErrorDisplay(), self.fmtToken(source) }); 546 }, 547 .name_or_id_not_allowed => { 548 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()}); 549 }, 550 .string_resource_as_numeric_type => switch (self.type) { 551 .err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"), 552 .note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"), 553 .hint => return, 554 }, 555 .ascii_character_not_equivalent_to_virtual_key_code => { 556 // TODO: Better wording? This is what the Win32 RC compiler emits. 557 // This occurs when VIRTKEY and a control code is specified ("^c", etc) 558 try writer.writeAll("ASCII character not equivalent to virtual key code"); 559 }, 560 .empty_menu_not_allowed => { 561 try writer.print("empty menu of type '{f}' not allowed", .{self.fmtToken(source)}); 562 }, 563 .rc_would_miscompile_version_value_padding => switch (self.type) { 564 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}), 565 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}), 566 .hint => return, 567 }, 568 .rc_would_miscompile_version_value_byte_count => switch (self.type) { 569 .err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}), 570 .note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}), 571 .hint => return, 572 }, 573 .code_page_pragma_in_included_file => { 574 try writer.print("#pragma code_page is not supported in an included resource file", .{}); 575 }, 576 .nested_resource_level_exceeds_max => switch (self.type) { 577 .err, .warning => { 578 const max = switch (self.extra.resource) { 579 .versioninfo => parse.max_nested_version_level, 580 .menu, .menuex => parse.max_nested_menu_level, 581 else => unreachable, 582 }; 583 return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max }); 584 }, 585 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}), 586 .hint => return, 587 }, 588 .too_many_dialog_controls_or_toolbar_buttons => switch (self.type) { 589 .err, .warning => return writer.print("{s} contains too many {s} (max is {})", .{ self.extra.resource.nameForErrorDisplay(), switch (self.extra.resource) { 590 .toolbar => "buttons", 591 else => "controls", 592 }, std.math.maxInt(u16) }), 593 .note => return writer.print("maximum number of {s} exceeded here", .{switch (self.extra.resource) { 594 .toolbar => "buttons", 595 else => "controls", 596 }}), 597 .hint => return, 598 }, 599 .nested_expression_level_exceeds_max => switch (self.type) { 600 .err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}), 601 .note => return writer.print("maximum expression level exceeded here", .{}), 602 .hint => return, 603 }, 604 .close_paren_expression => { 605 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"); 606 }, 607 .unary_plus_expression => { 608 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 +"); 609 }, 610 .rc_could_miscompile_control_params => switch (self.type) { 611 .err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}), 612 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}), 613 .hint => return, 614 }, 615 .dangling_literal_at_eof => { 616 try writer.writeAll("dangling literal at end-of-file; this is not a problem, but it is likely a mistake"); 617 }, 618 .disjoint_code_page => switch (self.type) { 619 .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", .{}), 620 .note => return writer.print("to avoid unexpected behavior, add a comment (or anything else) above the #pragma code_page line", .{}), 621 .hint => return, 622 }, 623 .string_already_defined => switch (self.type) { 624 .err, .warning => { 625 const language = self.extra.string_and_language.language; 626 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 }); 627 }, 628 .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 }), 629 .hint => return, 630 }, 631 .font_id_already_defined => switch (self.type) { 632 .err => return writer.print("font with id {d} already defined", .{self.extra.number}), 633 .warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}), 634 .note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}), 635 .hint => return, 636 }, 637 .file_open_error => { 638 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) }); 639 }, 640 .invalid_accelerator_key => { 641 try writer.print("invalid accelerator key '{f}': {s}", .{ self.fmtToken(source), @tagName(self.extra.accelerator_error.err) }); 642 }, 643 .accelerator_type_required => { 644 try writer.writeAll("accelerator type [ASCII or VIRTKEY] required when key is an integer"); 645 }, 646 .accelerator_shift_or_control_without_virtkey => { 647 try writer.writeAll("SHIFT or CONTROL used without VIRTKEY"); 648 }, 649 .rc_would_miscompile_control_padding => switch (self.type) { 650 .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)", .{}), 651 .note => return writer.print("to avoid the potential miscompilation, consider adding one more byte to the control data of the control preceding this one", .{}), 652 .hint => return, 653 }, 654 .rc_would_miscompile_control_class_ordinal => switch (self.type) { 655 .err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}), 656 .note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}), 657 .hint => return, 658 }, 659 .rc_would_error_on_icon_dir => switch (self.type) { 660 .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) }), 661 .note => { 662 // The only note supported is one specific to exactly this combination 663 if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable; 664 try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{}); 665 }, 666 .hint => return, 667 }, 668 .format_not_supported_in_icon_dir => { 669 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) }); 670 }, 671 .icon_dir_and_resource_type_mismatch => { 672 const unexpected_type: rc.ResourceType = if (self.extra.resource == .icon) .cursor else .icon; 673 // TODO: Better wording 674 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() }); 675 }, 676 .icon_read_error => { 677 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) }); 678 }, 679 .rc_would_error_on_bitmap_version => switch (self.type) { 680 .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}'", .{ 681 self.extra.icon_dir.index, 682 @tagName(self.extra.icon_dir.icon_type), 683 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(), 684 ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(), 685 }), 686 .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", .{ 687 self.extra.icon_dir.index, 688 @tagName(self.extra.icon_dir.icon_type), 689 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(), 690 }), 691 .note => unreachable, 692 .hint => return, 693 }, 694 .max_icon_ids_exhausted => switch (self.type) { 695 .err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}), 696 .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) }), 697 .hint => return, 698 }, 699 .bmp_read_error => { 700 try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) }); 701 }, 702 .bmp_ignored_palette_bytes => { 703 const bytes = strings[self.extra.number]; 704 const ignored_bytes = std.mem.readInt(u64, bytes[0..8], native_endian); 705 try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes}); 706 }, 707 .bmp_missing_palette_bytes => { 708 const bytes = strings[self.extra.number]; 709 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian); 710 try writer.print("bitmap has {d} missing color palette bytes", .{missing_bytes}); 711 }, 712 .rc_would_miscompile_bmp_palette_padding => { 713 try writer.writeAll("the Win32 RC compiler would erroneously pad out the missing bytes"); 714 if (self.extra.number != 0) { 715 const bytes = strings[self.extra.number]; 716 const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian); 717 try writer.print(" (and the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes}); 718 } 719 }, 720 .resource_header_size_exceeds_max => { 721 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}); 722 }, 723 .resource_data_size_exceeds_max => switch (self.type) { 724 .err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}), 725 .note => return writer.print("maximum data length exceeded here", .{}), 726 .hint => return, 727 }, 728 .control_extra_data_size_exceeds_max => switch (self.type) { 729 .err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}), 730 .note => return writer.print("maximum control data length exceeded here", .{}), 731 .hint => return, 732 }, 733 .version_node_size_exceeds_max => switch (self.type) { 734 .err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}), 735 .note => return writer.print("maximum tree size exceeded while writing this child", .{}), 736 .hint => return, 737 }, 738 .fontdir_size_exceeds_max => switch (self.type) { 739 .err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}), 740 .note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"), 741 .hint => return, 742 }, 743 .number_expression_as_filename => switch (self.type) { 744 .err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"), 745 .note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}), 746 .hint => return, 747 }, 748 .control_id_already_defined => switch (self.type) { 749 .err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}), 750 .note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}), 751 .hint => return, 752 }, 753 .invalid_filename => { 754 const disallowed_codepoint = self.extra.number; 755 if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) { 756 try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))}); 757 } else { 758 try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint}); 759 } 760 }, 761 .rc_would_error_u16_with_l_suffix => switch (self.type) { 762 .err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}), 763 .note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"), 764 .hint => return, 765 }, 766 .result_contains_fontdir => return, 767 .rc_would_miscompile_dialog_menu_id => switch (self.type) { 768 .err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}), 769 .note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}), 770 .hint => return, 771 }, 772 .rc_would_miscompile_dialog_class => switch (self.type) { 773 .err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}), 774 .note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}), 775 .hint => return, 776 }, 777 .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) { 778 .err, .warning => return, 779 .note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}), 780 .hint => return, 781 }, 782 .rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) { 783 .err, .warning => return, 784 .note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"), 785 .hint => return, 786 }, 787 .dialog_menu_id_was_uppercased => return, 788 .duplicate_optional_statement_skipped => { 789 return writer.writeAll("this statement was ignored; when multiple statements of the same type are specified, only the last takes precedence"); 790 }, 791 .invalid_digit_character_in_ordinal => { 792 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values"); 793 }, 794 .rc_would_miscompile_codepoint_whitespace => { 795 const treated_as = self.extra.number >> 8; 796 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 }); 797 }, 798 .rc_would_miscompile_codepoint_skip => { 799 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}); 800 }, 801 .rc_would_miscompile_codepoint_bom => switch (self.type) { 802 .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}), 803 .note => return writer.writeAll("the presence of this codepoint causes all non-ASCII codepoints to be byteswapped by the Win32 RC preprocessor"), 804 .hint => return, 805 }, 806 .tab_converted_to_spaces => switch (self.type) { 807 .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)"), 808 .note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"), 809 .hint => return, 810 }, 811 .win32_non_ascii_ordinal => switch (self.type) { 812 .err, .warning => unreachable, 813 .note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}), 814 .hint => return, 815 }, 816 .failed_to_open_cwd => { 817 try writer.print("failed to open CWD for compilation: {s}", .{@tagName(self.extra.file_open_error.err)}); 818 }, 819 } 820 } 821 822 pub const VisualTokenInfo = struct { 823 before_len: usize, 824 point_offset: usize, 825 after_len: usize, 826 }; 827 828 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize, source: []const u8) VisualTokenInfo { 829 return switch (self.err) { 830 // These can technically be more than 1 byte depending on encoding, 831 // but they always refer to one visual character/grapheme. 832 .illegal_byte, 833 .illegal_byte_outside_string_literals, 834 .illegal_codepoint_outside_string_literals, 835 .illegal_byte_order_mark, 836 .illegal_private_use_character, 837 => .{ 838 .before_len = 0, 839 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start), 840 .after_len = 0, 841 }, 842 else => .{ 843 .before_len = before: { 844 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start); 845 break :before cellCount(self.code_page, source, start, self.token.start); 846 }, 847 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start), 848 .after_len = after: { 849 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end); 850 // end may be less than start when pointing to EOF 851 if (end <= self.token.start) break :after 0; 852 break :after cellCount(self.code_page, source, self.token.start, end) - 1; 853 }, 854 }, 855 }; 856 } 857 }; 858 859 /// Convenience struct only useful when the code page can be inferred from the token 860 pub const ErrorDetailsWithoutCodePage = blk: { 861 const details_info = @typeInfo(ErrorDetails); 862 const fields = details_info.@"struct".fields; 863 var fields_without_codepage: [fields.len - 1]std.builtin.Type.StructField = undefined; 864 var i: usize = 0; 865 for (fields) |field| { 866 if (std.mem.eql(u8, field.name, "code_page")) continue; 867 fields_without_codepage[i] = field; 868 i += 1; 869 } 870 std.debug.assert(i == fields_without_codepage.len); 871 break :blk @Type(.{ .@"struct" = .{ 872 .layout = .auto, 873 .fields = &fields_without_codepage, 874 .decls = &.{}, 875 .is_tuple = false, 876 } }); 877 }; 878 879 fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usize, end_index: usize) usize { 880 // Note: This is an imperfect solution. A proper implementation here would 881 // involve full grapheme cluster awareness + grapheme width data, but oh well. 882 var codepoint_count: usize = 0; 883 var index: usize = start_index; 884 while (index < end_index) { 885 const codepoint = code_page.codepointAt(index, source) orelse break; 886 defer index += codepoint.byte_len; 887 _ = codepointForDisplay(codepoint) orelse continue; 888 codepoint_count += 1; 889 // no need to count more than we will display 890 if (codepoint_count >= max_source_line_codepoints + truncated_str.len) break; 891 } 892 return codepoint_count; 893 } 894 895 const truncated_str = "<...truncated...>"; 896 897 pub fn renderErrorMessage(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 { 898 if (err_details.type == .hint) return; 899 900 const source_line_start = err_details.token.getLineStartForErrorDisplay(source); 901 // Treat tab stops as 1 column wide for error display purposes, 902 // and add one to get a 1-based column 903 const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1; 904 905 const corresponding_span: ?SourceMappings.CorrespondingSpan = if (source_mappings) |mappings| 906 mappings.getCorrespondingSpan(err_details.token.line_number) 907 else 908 null; 909 const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null) 910 source_mappings.?.files.get(corresponding_span.?.filename_offset) 911 else 912 null; 913 914 const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number; 915 916 try tty_config.setColor(writer, .bold); 917 if (corresponding_file) |file| { 918 try writer.writeAll(file); 919 } else { 920 try tty_config.setColor(writer, .dim); 921 try writer.writeAll("<after preprocessor>"); 922 try tty_config.setColor(writer, .reset); 923 try tty_config.setColor(writer, .bold); 924 } 925 try writer.print(":{d}:{d}: ", .{ err_line, column }); 926 switch (err_details.type) { 927 .err => { 928 try tty_config.setColor(writer, .red); 929 try writer.writeAll("error: "); 930 }, 931 .warning => { 932 try tty_config.setColor(writer, .yellow); 933 try writer.writeAll("warning: "); 934 }, 935 .note => { 936 try tty_config.setColor(writer, .cyan); 937 try writer.writeAll("note: "); 938 }, 939 .hint => unreachable, 940 } 941 try tty_config.setColor(writer, .reset); 942 try tty_config.setColor(writer, .bold); 943 try err_details.render(writer, source, strings); 944 try writer.writeByte('\n'); 945 try tty_config.setColor(writer, .reset); 946 947 if (!err_details.print_source_line) { 948 try writer.writeByte('\n'); 949 return; 950 } 951 952 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start); 953 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len, source); 954 const truncated_visual_info = ErrorDetails.VisualTokenInfo{ 955 .before_len = if (visual_info.point_offset > max_source_line_codepoints and visual_info.before_len > 0) 956 (visual_info.before_len + 1) -| (visual_info.point_offset - max_source_line_codepoints) 957 else 958 visual_info.before_len, 959 .point_offset = @min(max_source_line_codepoints + 1, visual_info.point_offset), 960 .after_len = if (visual_info.point_offset > max_source_line_codepoints) 961 @min(truncated_str.len - 3, visual_info.after_len) 962 else 963 @min(max_source_line_codepoints - visual_info.point_offset + (truncated_str.len - 2), visual_info.after_len), 964 }; 965 966 // Need this to determine if the 'line originated from' note is worth printing 967 var source_line_for_display_buf: [max_source_line_bytes]u8 = undefined; 968 const source_line_for_display = writeSourceSlice(&source_line_for_display_buf, source_line, err_details.code_page); 969 970 try writer.writeAll(source_line_for_display.line); 971 if (source_line_for_display.truncated) { 972 try tty_config.setColor(writer, .dim); 973 try writer.writeAll(truncated_str); 974 try tty_config.setColor(writer, .reset); 975 } 976 try writer.writeByte('\n'); 977 978 try tty_config.setColor(writer, .green); 979 const num_spaces = truncated_visual_info.point_offset - truncated_visual_info.before_len; 980 try writer.splatByteAll(' ', num_spaces); 981 try writer.splatByteAll('~', truncated_visual_info.before_len); 982 try writer.writeByte('^'); 983 try writer.splatByteAll('~', truncated_visual_info.after_len); 984 try writer.writeByte('\n'); 985 try tty_config.setColor(writer, .reset); 986 987 if (corresponding_span != null and corresponding_file != null) { 988 var worth_printing_lines: bool = true; 989 var initial_lines_err: ?anyerror = null; 990 var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init( 991 cwd, 992 err_details, 993 source_line_for_display.line, 994 corresponding_span.?, 995 corresponding_file.?, 996 ) catch |err| switch (err) { 997 error.NotWorthPrintingLines => blk: { 998 worth_printing_lines = false; 999 break :blk null; 1000 }, 1001 error.NotWorthPrintingNote => return, 1002 else => |e| blk: { 1003 initial_lines_err = e; 1004 break :blk null; 1005 }, 1006 }; 1007 defer if (corresponding_lines) |*cl| cl.deinit(); 1008 1009 try tty_config.setColor(writer, .bold); 1010 if (corresponding_file) |file| { 1011 try writer.writeAll(file); 1012 } else { 1013 try tty_config.setColor(writer, .dim); 1014 try writer.writeAll("<after preprocessor>"); 1015 try tty_config.setColor(writer, .reset); 1016 try tty_config.setColor(writer, .bold); 1017 } 1018 try writer.print(":{d}:{d}: ", .{ err_line, column }); 1019 try tty_config.setColor(writer, .cyan); 1020 try writer.writeAll("note: "); 1021 try tty_config.setColor(writer, .reset); 1022 try tty_config.setColor(writer, .bold); 1023 try writer.writeAll("this line originated from line"); 1024 if (corresponding_span.?.start_line != corresponding_span.?.end_line) { 1025 try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line }); 1026 } else { 1027 try writer.print(" {}", .{corresponding_span.?.start_line}); 1028 } 1029 try writer.print(" of file '{s}'\n", .{corresponding_file.?}); 1030 try tty_config.setColor(writer, .reset); 1031 1032 if (!worth_printing_lines) return; 1033 1034 const write_lines_err: ?anyerror = write_lines: { 1035 if (initial_lines_err) |err| break :write_lines err; 1036 while (corresponding_lines.?.next() catch |err| { 1037 break :write_lines err; 1038 }) |display_line| { 1039 try writer.writeAll(display_line.line); 1040 if (display_line.truncated) { 1041 try tty_config.setColor(writer, .dim); 1042 try writer.writeAll(truncated_str); 1043 try tty_config.setColor(writer, .reset); 1044 } 1045 try writer.writeByte('\n'); 1046 } 1047 break :write_lines null; 1048 }; 1049 if (write_lines_err) |err| { 1050 try tty_config.setColor(writer, .red); 1051 try writer.writeAll(" | "); 1052 try tty_config.setColor(writer, .reset); 1053 try tty_config.setColor(writer, .dim); 1054 try writer.print("unable to print line(s) from file: {s}\n", .{@errorName(err)}); 1055 try tty_config.setColor(writer, .reset); 1056 } 1057 try writer.writeByte('\n'); 1058 } 1059 } 1060 1061 const VisualLine = struct { 1062 line: []u8, 1063 truncated: bool, 1064 }; 1065 1066 const CorrespondingLines = struct { 1067 // enough room for one more codepoint, just so that we don't have to keep 1068 // track of this being truncated, since the extra codepoint will ensure 1069 // the visual line will need to truncate in that case. 1070 line_buf: [max_source_line_bytes + 4]u8 = undefined, 1071 line_len: usize = 0, 1072 visual_line_buf: [max_source_line_bytes]u8 = undefined, 1073 visual_line_len: usize = 0, 1074 truncated: bool = false, 1075 line_num: usize = 1, 1076 initial_line: bool = true, 1077 last_byte: u8 = 0, 1078 at_eof: bool = false, 1079 span: SourceMappings.CorrespondingSpan, 1080 file: std.fs.File, 1081 buffered_reader: std.fs.File.Reader, 1082 code_page: SupportedCodePage, 1083 1084 pub fn init(cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines { 1085 // We don't do line comparison for this error, so don't print the note if the line 1086 // number is different 1087 if (err_details.err == .string_literal_too_long and err_details.token.line_number != corresponding_span.start_line) { 1088 return error.NotWorthPrintingNote; 1089 } 1090 1091 // Don't print the originating line for this error, we know it's really long 1092 if (err_details.err == .string_literal_too_long) { 1093 return error.NotWorthPrintingLines; 1094 } 1095 1096 var corresponding_lines = CorrespondingLines{ 1097 .span = corresponding_span, 1098 .file = try utils.openFileNotDir(cwd, corresponding_file, .{}), 1099 .buffered_reader = undefined, 1100 .code_page = err_details.code_page, 1101 }; 1102 corresponding_lines.buffered_reader = corresponding_lines.file.reader(&.{}); 1103 errdefer corresponding_lines.deinit(); 1104 1105 var fbs = std.io.fixedBufferStream(&corresponding_lines.line_buf); 1106 const writer = fbs.writer(); 1107 1108 try corresponding_lines.writeLineFromStreamVerbatim( 1109 writer, 1110 corresponding_lines.buffered_reader.interface.adaptToOldInterface(), 1111 corresponding_span.start_line, 1112 ); 1113 1114 const visual_line = writeSourceSlice( 1115 &corresponding_lines.visual_line_buf, 1116 corresponding_lines.line_buf[0..corresponding_lines.line_len], 1117 err_details.code_page, 1118 ); 1119 corresponding_lines.visual_line_len = visual_line.line.len; 1120 corresponding_lines.truncated = visual_line.truncated; 1121 1122 // If the lines are the same as they were before preprocessing, skip printing the note entirely 1123 if (corresponding_span.start_line == corresponding_span.end_line and std.mem.eql( 1124 u8, 1125 line_for_comparison, 1126 corresponding_lines.visual_line_buf[0..corresponding_lines.visual_line_len], 1127 )) { 1128 return error.NotWorthPrintingNote; 1129 } 1130 1131 return corresponding_lines; 1132 } 1133 1134 pub fn next(self: *CorrespondingLines) !?VisualLine { 1135 if (self.initial_line) { 1136 self.initial_line = false; 1137 return .{ 1138 .line = self.visual_line_buf[0..self.visual_line_len], 1139 .truncated = self.truncated, 1140 }; 1141 } 1142 if (self.line_num > self.span.end_line) return null; 1143 if (self.at_eof) return error.LinesNotFound; 1144 1145 self.line_len = 0; 1146 self.visual_line_len = 0; 1147 1148 var fbs = std.io.fixedBufferStream(&self.line_buf); 1149 const writer = fbs.writer(); 1150 1151 try self.writeLineFromStreamVerbatim( 1152 writer, 1153 self.buffered_reader.interface.adaptToOldInterface(), 1154 self.line_num, 1155 ); 1156 1157 const visual_line = writeSourceSlice( 1158 &self.visual_line_buf, 1159 self.line_buf[0..self.line_len], 1160 self.code_page, 1161 ); 1162 self.visual_line_len = visual_line.line.len; 1163 1164 return visual_line; 1165 } 1166 1167 fn writeLineFromStreamVerbatim(self: *CorrespondingLines, writer: anytype, input: anytype, line_num: usize) !void { 1168 while (try readByteOrEof(input)) |byte| { 1169 switch (byte) { 1170 '\n', '\r' => { 1171 if (!utils.isLineEndingPair(self.last_byte, byte)) { 1172 const line_complete = self.line_num == line_num; 1173 self.line_num += 1; 1174 if (line_complete) { 1175 self.last_byte = byte; 1176 return; 1177 } 1178 } else { 1179 // reset last_byte to a non-line ending so that 1180 // consecutive CRLF pairs don't get treated as one 1181 // long line ending 'pair' 1182 self.last_byte = 0; 1183 continue; 1184 } 1185 }, 1186 else => { 1187 if (self.line_num == line_num) { 1188 if (writer.writeByte(byte)) { 1189 self.line_len += 1; 1190 } else |err| switch (err) { 1191 error.NoSpaceLeft => {}, 1192 else => |e| return e, 1193 } 1194 } 1195 }, 1196 } 1197 self.last_byte = byte; 1198 } 1199 self.at_eof = true; 1200 // hacky way to get next to return null 1201 self.line_num += 1; 1202 } 1203 1204 fn readByteOrEof(reader: anytype) !?u8 { 1205 return reader.readByte() catch |err| switch (err) { 1206 error.EndOfStream => return null, 1207 else => |e| return e, 1208 }; 1209 } 1210 1211 pub fn deinit(self: *CorrespondingLines) void { 1212 self.file.close(); 1213 } 1214 }; 1215 1216 const max_source_line_codepoints = 120; 1217 const max_source_line_bytes = max_source_line_codepoints * 4; 1218 1219 fn writeSourceSlice(buf: []u8, slice: []const u8, code_page: SupportedCodePage) VisualLine { 1220 var src_i: usize = 0; 1221 var dest_i: usize = 0; 1222 var codepoint_count: usize = 0; 1223 while (src_i < slice.len) { 1224 const codepoint = code_page.codepointAt(src_i, slice) orelse break; 1225 defer src_i += codepoint.byte_len; 1226 const display_codepoint = codepointForDisplay(codepoint) orelse continue; 1227 codepoint_count += 1; 1228 if (codepoint_count > max_source_line_codepoints) { 1229 return .{ .line = buf[0..dest_i], .truncated = true }; 1230 } 1231 const utf8_len = std.unicode.utf8Encode(display_codepoint, buf[dest_i..]) catch unreachable; 1232 dest_i += utf8_len; 1233 } 1234 return .{ .line = buf[0..dest_i], .truncated = false }; 1235 } 1236 1237 fn codepointForDisplay(codepoint: code_pages.Codepoint) ?u21 { 1238 return switch (codepoint.value) { 1239 '\x00'...'\x08', 1240 '\x0E'...'\x1F', 1241 '\x7F', 1242 code_pages.Codepoint.invalid, 1243 => '�', 1244 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines 1245 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up 1246 // in the console as DATA but the compiler reads it as RCDATA) 1247 // 1248 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r 1249 // characters get converted to \n, but may become relevant if another 1250 // preprocessor is used instead. 1251 '\r' => null, 1252 '\t', '\x0B', '\x0C' => ' ', 1253 else => |v| v, 1254 }; 1255 }