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