Ruby 4.1.0dev (2026-08-07 revision 60e3de26be955d1908dabd0ad66970acbd08c521)
errors_format.c
2
3#include "prism/internal/allocator.h"
4#include "prism/internal/buffer.h"
5#include "prism/internal/diagnostic.h"
6#include "prism/internal/encoding.h"
7#include "prism/internal/line_offset_list.h"
8#include "prism/internal/parser.h"
9
10#include <assert.h>
11#include <inttypes.h>
12#include <stdbool.h>
13#include <stdlib.h>
14#include <string.h>
15
22static bool
23location_is_utf8(const pm_parser_t *parser, const pm_location_t *location) {
24 const pm_line_offset_list_t *line_offsets = &parser->line_offsets;
25 const size_t start_line = (size_t) pm_line_offset_list_line_column(line_offsets, location->start, 1).line;
26 const size_t end_line = (size_t) pm_line_offset_list_line_column(line_offsets, location->start + location->length, 1).line;
27
28 const uint8_t *cursor = parser->start + line_offsets->offsets[start_line - 1];
29 const uint8_t *end = (end_line == line_offsets->size) ? parser->end : (parser->start + line_offsets->offsets[end_line]);
30
31 while (cursor < end) {
32 size_t width = pm_encoding_utf_8_char_width(cursor, end - cursor);
33 if (width == 0) return false;
34
35 cursor += width;
36 }
37
38 return true;
39}
40
41typedef struct {
42 pm_diagnostic_t *diagnostic;
43 int32_t line;
44 uint32_t column_start;
45 uint32_t column_end;
46 size_t idx;
48
49static int
50pm_rich_error_compare(const void *a, const void *b) {
51 const pm_rich_error_t *left = (const pm_rich_error_t *) a;
52 const pm_rich_error_t *right = (const pm_rich_error_t *) b;
53
54 if (left->line != right->line) {
55 return (left->line < right->line) ? -1 : 1;
56 }
57
58 if (left->column_start != right->column_start) {
59 return (left->column_start < right->column_start) ? -1 : 1;
60 }
61
62 if (left->idx != right->idx) {
63 return (left->idx < right->idx) ? 1 : -1;
64 }
65
66 return 0;
67}
68
69#define COLOR_BOLD "\033[1m"
70#define COLOR_GRAY "\033[2m"
71#define COLOR_RED "\033[1;31m"
72#define COLOR_RESET "\033[m"
73#define TRUNCATE 30
74
75typedef struct {
76 const char *number_prefix;
77 const char *blank_prefix;
78 const char *divider;
79 size_t blank_prefix_length;
80 size_t divider_length;
82
87static void
88pm_error_line_format_init(pm_error_line_format_t *format, pm_errors_format_type_t format_type, int32_t first_line, int32_t last_line) {
89 /* If we have a maximum line number that is negative, then we are going to
90 * use the absolute value for comparison but multiply by 10 to additionally
91 * have a column for the negative sign. */
92 if (first_line < 0) first_line = (-first_line) * 10;
93 if (last_line < 0) last_line = (-last_line) * 10;
94 int32_t max_line = first_line > last_line ? first_line : last_line;
95
96 if (max_line < 10) {
97 if (format_type > PM_ERRORS_FORMAT_PLAIN) {
98 *format = (pm_error_line_format_t) {
99 .number_prefix = COLOR_GRAY "%1" PRIi32 " | " COLOR_RESET,
100 .blank_prefix = COLOR_GRAY " | " COLOR_RESET,
101 .divider = COLOR_GRAY " ~~~~~" COLOR_RESET "\n"
102 };
103 } else {
104 *format = (pm_error_line_format_t) {
105 .number_prefix = "%1" PRIi32 " | ",
106 .blank_prefix = " | ",
107 .divider = " ~~~~~\n"
108 };
109 }
110 } else if (max_line < 100) {
111 if (format_type > PM_ERRORS_FORMAT_PLAIN) {
112 *format = (pm_error_line_format_t) {
113 .number_prefix = COLOR_GRAY "%2" PRIi32 " | " COLOR_RESET,
114 .blank_prefix = COLOR_GRAY " | " COLOR_RESET,
115 .divider = COLOR_GRAY " ~~~~~~" COLOR_RESET "\n"
116 };
117 } else {
118 *format = (pm_error_line_format_t) {
119 .number_prefix = "%2" PRIi32 " | ",
120 .blank_prefix = " | ",
121 .divider = " ~~~~~~\n"
122 };
123 }
124 } else if (max_line < 1000) {
125 if (format_type > PM_ERRORS_FORMAT_PLAIN) {
126 *format = (pm_error_line_format_t) {
127 .number_prefix = COLOR_GRAY "%3" PRIi32 " | " COLOR_RESET,
128 .blank_prefix = COLOR_GRAY " | " COLOR_RESET,
129 .divider = COLOR_GRAY " ~~~~~~~" COLOR_RESET "\n"
130 };
131 } else {
132 *format = (pm_error_line_format_t) {
133 .number_prefix = "%3" PRIi32 " | ",
134 .blank_prefix = " | ",
135 .divider = " ~~~~~~~\n"
136 };
137 }
138 } else if (max_line < 10000) {
139 if (format_type > PM_ERRORS_FORMAT_PLAIN) {
140 *format = (pm_error_line_format_t) {
141 .number_prefix = COLOR_GRAY "%4" PRIi32 " | " COLOR_RESET,
142 .blank_prefix = COLOR_GRAY " | " COLOR_RESET,
143 .divider = COLOR_GRAY " ~~~~~~~~" COLOR_RESET "\n"
144 };
145 } else {
146 *format = (pm_error_line_format_t) {
147 .number_prefix = "%4" PRIi32 " | ",
148 .blank_prefix = " | ",
149 .divider = " ~~~~~~~~\n"
150 };
151 }
152 } else {
153 if (format_type > PM_ERRORS_FORMAT_PLAIN) {
154 *format = (pm_error_line_format_t) {
155 .number_prefix = COLOR_GRAY "%5" PRIi32 " | " COLOR_RESET,
156 .blank_prefix = COLOR_GRAY " | " COLOR_RESET,
157 .divider = COLOR_GRAY " ~~~~~~~~" COLOR_RESET "\n"
158 };
159 } else {
160 *format = (pm_error_line_format_t) {
161 .number_prefix = "%5" PRIi32 " | ",
162 .blank_prefix = " | ",
163 .divider = " ~~~~~~~~\n"
164 };
165 }
166 }
167
168 format->blank_prefix_length = strlen(format->blank_prefix);
169 format->divider_length = strlen(format->divider);
170}
171
172static void
173pm_error_format_line(const pm_parser_t *parser, pm_buffer_t *buffer, const char *number_prefix, int32_t line, uint32_t column_start, uint32_t column_end) {
174 int32_t line_delta = line - parser->start_line;
175 assert(line_delta >= 0);
176
177 size_t index = (size_t) line_delta;
178 assert(index < parser->line_offsets.size);
179
180 const uint8_t *start = parser->start + parser->line_offsets.offsets[index];
181 const uint8_t *end;
182
183 if (index >= parser->line_offsets.size - 1) {
184 end = parser->end;
185 } else {
186 end = parser->start + parser->line_offsets.offsets[index + 1];
187 }
188
189 pm_buffer_append_format(buffer, number_prefix, line);
190
191 /* Here we determine if we should truncate the end of the line. Note that
192 * this is written to avoid computing start + column_end, which could be
193 * more than one past the end of the source when the error is at the end
194 * of the input. */
195 bool truncate_end = false;
196 if ((column_end != 0) && ((end - start) - ((ptrdiff_t) column_end) >= TRUNCATE)) {
197 const uint8_t *end_candidate = start + column_end + TRUNCATE;
198
199 for (const uint8_t *cursor = start; cursor < end_candidate;) {
200 size_t char_width = pm_parser_encoding_char_width(parser, cursor, parser->end - cursor);
201
202 /* If we failed to decode a character, then just bail out and
203 * truncate at the fixed width. */
204 if (char_width == 0) break;
205
206 /* If this next character would go past the end candidate,
207 * then we need to truncate before it. */
208 if (cursor + char_width > end_candidate) {
209 end_candidate = cursor;
210 break;
211 }
212
213 cursor += char_width;
214 }
215
216 end = end_candidate;
217 truncate_end = true;
218 }
219
220 /* Here we determine if we should truncate the start of the line. */
221 if (column_start >= TRUNCATE) {
222 pm_buffer_append_string(buffer, "... ", 4);
223 start += column_start;
224 }
225
226 pm_buffer_append_string(buffer, (const char *) start, (size_t) (end - start));
227
228 if (truncate_end) {
229 pm_buffer_append_string(buffer, " ...\n", 5);
230 } else if (end == parser->end && end > parser->start && end[-1] != '\n') {
231 pm_buffer_append_byte(buffer, '\n');
232 }
233}
234
235static void
236pm_rich_errors_format(const pm_parser_t *parser, pm_buffer_t *buffer, pm_errors_format_type_t format_type, size_t nerrors, pm_rich_error_t *rich_errors, bool inline_messages) {
238 pm_error_line_format_init(&format, format_type, rich_errors[0].line, rich_errors[nerrors - 1].line);
239
240 /* We are going to iterate through every error in our error list and display
241 * it. While we are iterating, we will display some padding lines of the
242 * source before the error to give some context. We will be careful not to
243 * display the same line twice in case the errors are close enough in the
244 * source. */
245 int32_t last_line = parser->start_line - 1;
246 uint32_t last_column_start = 0;
247
248 for (size_t idx = 0; idx < nerrors; idx++) {
249 pm_rich_error_t *rich_error = rich_errors + idx;
250
251 /* Here we determine how many lines of padding of the source to display,
252 * based on the difference from the last line that was displayed. */
253 if (rich_error->line - last_line > 1) {
254 if (rich_error->line - last_line > 2) {
255 if ((idx != 0) && (rich_error->line - last_line > 3)) {
256 pm_buffer_append_string(buffer, format.divider, format.divider_length);
257 }
258
259 pm_buffer_append_string(buffer, " ", 2);
260 pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line - 2, 0, 0);
261 }
262
263 pm_buffer_append_string(buffer, " ", 2);
264 pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line - 1, 0, 0);
265 }
266
267 /* If this is the first error or we are on a new line, then we will
268 * display the line that has the error in it. */
269 if ((idx == 0) || (rich_error->line != last_line)) {
270 switch (format_type) {
272 pm_buffer_append_string(buffer, "> ", 2);
273 break;
275 pm_buffer_append_format(buffer, "%s", COLOR_BOLD "> " COLOR_RESET);
276 break;
278 pm_buffer_append_format(buffer, "%s", COLOR_RED "> " COLOR_RESET);
279 break;
280 }
281
282 last_column_start = rich_error->column_start;
283
284 /* Find the maximum column end of all the errors on this line. */
285 uint32_t column_end = rich_error->column_end;
286 for (size_t next_idx = idx + 1; next_idx < nerrors; next_idx++) {
287 if (rich_errors[next_idx].line != rich_error->line) break;
288 if (rich_errors[next_idx].column_end > column_end) column_end = rich_errors[next_idx].column_end;
289 }
290
291 pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line, rich_error->column_start, column_end);
292 }
293
294 const uint8_t *start = parser->start + parser->line_offsets.offsets[rich_error->line - parser->start_line];
295 if (start == parser->end) pm_buffer_append_byte(buffer, '\n');
296
297 /* Now we will display the actual error message. We will do this by
298 * first putting the prefix to the line, then a bunch of blank spaces
299 * depending on the column, then as many tildes as we need to display
300 * the width of the error, then the error message itself.
301 *
302 * Note that this doesn't take into account the width of the actual
303 * character when displayed in the terminal. For some east-asian
304 * languages or emoji, this means it can be thrown off pretty badly. We
305 * will need to solve this eventually. */
306 pm_buffer_append_string(buffer, " ", 2);
307 pm_buffer_append_string(buffer, format.blank_prefix, format.blank_prefix_length);
308
309 size_t column = 0;
310 if (last_column_start >= TRUNCATE) {
311 pm_buffer_append_string(buffer, " ", 4);
312 column = last_column_start;
313 }
314
315 while (column < rich_error->column_start) {
316 pm_buffer_append_byte(buffer, ' ');
317
318 size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column));
319 column += (char_width == 0 ? 1 : char_width);
320 }
321
322 switch (format_type) {
324 pm_buffer_append_byte(buffer, '^');
325 break;
327 pm_buffer_append_format(buffer, "%s^", COLOR_BOLD);
328 break;
330 pm_buffer_append_format(buffer, "%s^", COLOR_RED);
331 break;
332 }
333
334 size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column));
335 column += (char_width == 0 ? 1 : char_width);
336
337 while (column < rich_error->column_end) {
338 pm_buffer_append_byte(buffer, '~');
339
340 size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column));
341 column += (char_width == 0 ? 1 : char_width);
342 }
343
344 if (format_type != PM_ERRORS_FORMAT_PLAIN) {
345 pm_buffer_append_format(buffer, "%s", COLOR_RESET);
346 }
347
348 if (inline_messages) {
349 const char *message = pm_diagnostic_message(rich_error->diagnostic);
350
351 pm_buffer_append_byte(buffer, ' ');
352 pm_buffer_append_string(buffer, message, strlen(message));
353 }
354
355 pm_buffer_append_byte(buffer, '\n');
356
357 /* Here we determine how many lines of padding to display after the
358 * diagnostic, depending on where the next diagnostic is in source. */
359 last_line = rich_error->line;
360 int32_t next_line;
361
362 if (idx == nerrors - 1) {
363 next_line = ((int32_t) parser->line_offsets.size) + parser->start_line;
364
365 /* If the file ends with a newline, subtract one from our
366 * "next_line" so that we do not output an extra line at the end of
367 * the file. */
368 if ((parser->start + parser->line_offsets.offsets[parser->line_offsets.size - 1]) == parser->end) {
369 next_line--;
370 }
371 } else {
372 next_line = rich_errors[idx + 1].line;
373 }
374
375 if (next_line - last_line > 1) {
376 pm_buffer_append_string(buffer, " ", 2);
377 pm_error_format_line(parser, buffer, format.number_prefix, ++last_line, 0, 0);
378 }
379
380 if (next_line - last_line > 1) {
381 pm_buffer_append_string(buffer, " ", 2);
382 pm_error_format_line(parser, buffer, format.number_prefix, ++last_line, 0, 0);
383 }
384 }
385}
386
387static void
388pm_rich_error_init(const pm_parser_t *parser, pm_rich_error_t *rich_error, pm_diagnostic_t *diagnostic, pm_location_t *location) {
389 const pm_line_offset_list_t *line_offsets = &parser->line_offsets;
390 pm_line_column_t start_line_column = pm_line_offset_list_line_column(line_offsets, location->start, parser->start_line);
391 pm_line_column_t end_line_column = pm_line_offset_list_line_column(line_offsets, location->start + location->length, parser->start_line);
392
393 uint32_t column_end;
394 if (start_line_column.line == end_line_column.line) {
395 column_end = end_line_column.column;
396 } else {
397 column_end = (uint32_t) (line_offsets->offsets[start_line_column.line - parser->start_line + 1] - line_offsets->offsets[start_line_column.line - parser->start_line] - 1);
398 }
399
400 /* Ensure we have at least one column of error. */
401 if (start_line_column.column == column_end) column_end++;
402
403 *rich_error = (pm_rich_error_t) {
404 .diagnostic = diagnostic,
405 .line = start_line_column.line,
406 .column_start = start_line_column.column,
407 .column_end = column_end
408 };
409}
410
412pm_errors_format(const pm_parser_t *parser, pm_buffer_t *buffer, pm_errors_format_type_t format_type) {
413 size_t nerrors = parser->error_list.size;
414 assert(nerrors > 0);
415
416 int filepath_length = (int) pm_string_length(&parser->filepath);
417 const char *filepath = (const char *) pm_string_source(&parser->filepath);
418
419 size_t rich_errors_size = nerrors * sizeof(pm_rich_error_t);
420 if (rich_errors_size / sizeof(pm_rich_error_t) != nerrors) abort();
421
422 pm_rich_error_t *rich_errors = xmalloc(rich_errors_size);
423 if (rich_errors == NULL) abort();
424
425 bool is_utf8 = true;
426 size_t rich_errors_idx = 0;
427
428 for (pm_diagnostic_t *diagnostic = (pm_diagnostic_t *) parser->error_list.head; diagnostic != NULL; diagnostic = (pm_diagnostic_t *) diagnostic->node.next) {
429 pm_location_t location = pm_diagnostic_location(diagnostic);
430
431 switch (pm_diagnostic_error_level(diagnostic)) {
433 if (is_utf8 && !location_is_utf8(parser, &location)) {
434 is_utf8 = false;
435 }
436
437 pm_rich_error_init(parser, &rich_errors[rich_errors_idx], diagnostic, &location);
438 rich_errors[rich_errors_idx].idx = rich_errors_idx;
439 rich_errors_idx++;
440 break;
441 }
443 /* If we get an argument error, we want to format it and return it
444 * immediately. They should include a snippet of the source if
445 * possible. */
446 pm_buffer_append_format(
447 buffer,
448 "%.*s:%" PRIi32 ": %s",
449 filepath_length,
450 filepath,
451 pm_line_offset_list_line(&parser->line_offsets, location.start, parser->start_line),
452 pm_diagnostic_message(diagnostic)
453 );
454
455 if (location_is_utf8(parser, &location)) {
456 pm_buffer_append_byte(buffer, '\n');
457 pm_rich_error_init(parser, rich_errors, diagnostic, &location);
458 pm_rich_errors_format(parser, buffer, format_type, 1, rich_errors, false);
459 }
460
461 xfree_sized(rich_errors, rich_errors_size);
463 }
464 case PM_ERROR_LEVEL_LOAD: {
465 /* If we get a load error, we want to format it and return it
466 * immediately. It should only include the diagnostic message. */
467 const char *message = pm_diagnostic_message(diagnostic);
468 pm_buffer_append_string(buffer, message, strlen(message));
469
470 xfree_sized(rich_errors, rich_errors_size);
471 return PM_ERROR_LEVEL_LOAD;
472 }
473 }
474 }
475
476 assert(rich_errors_idx == nerrors);
477
478 /* The header displays the line of the first error in parse order, which
479 * is the first entry in the array because it has not been sorted yet. */
480 pm_buffer_append_format(
481 buffer,
482 "%.*s:%" PRIi32 ": syntax error%s found\n",
483 filepath_length,
484 filepath,
485 rich_errors[0].line,
486 (nerrors > 1) ? "s" : ""
487 );
488
489 if (is_utf8) {
490 /* In here, we have all of the rich diagnostic information and we know
491 * the snippets are valid UTF-8. We will sort the errors so that they
492 * are displayed in the order that they appear in the source, and then
493 * format them all together with the source code snippets. */
494 qsort(rich_errors, nerrors, sizeof(pm_rich_error_t), pm_rich_error_compare);
495 pm_rich_errors_format(parser, buffer, format_type, nerrors, rich_errors, true);
496 } else {
497 /* In here, we have content in the source that is not valid UTF-8. In
498 * that case, we do not want to attempt to format the source code
499 * snippets because we do not know how the terminal will handle it. The
500 * diagnostics are displayed in the order that they were generated,
501 * which is the order of the array before it has been sorted. */
502 for (size_t idx = 0; idx < nerrors; idx++) {
503 pm_rich_error_t *rich_error = &rich_errors[idx];
504
505 if (idx > 0) pm_buffer_append_byte(buffer, '\n');
506 pm_buffer_append_format(
507 buffer,
508 "%.*s:%" PRIi32 ": %s",
509 filepath_length,
510 filepath,
511 rich_error->line,
512 pm_diagnostic_message(rich_error->diagnostic)
513 );
514 }
515 }
516
517 xfree_sized(rich_errors, rich_errors_size);
519}
pm_error_level_t
The levels of errors generated during parsing.
Definition diagnostic.h:23
@ PM_ERROR_LEVEL_ARGUMENT
For errors that should raise an argument error.
Definition diagnostic.h:28
@ PM_ERROR_LEVEL_LOAD
For errors that should raise a load error.
Definition diagnostic.h:31
@ PM_ERROR_LEVEL_SYNTAX
For errors that should raise a syntax error.
Definition diagnostic.h:25
A function for formatting the errors in a parser into a buffer.
pm_errors_format_type_t
The type of formatting to use when formatting errors.
@ PM_ERRORS_FORMAT_PLAIN
Format errors in a plain format with no colors or styles.
@ PM_ERRORS_FORMAT_COLOR
Format errors in a color format with bold and colors.
@ PM_ERRORS_FORMAT_STYLE
Format errors in a style format with bold only.
#define xmalloc
Old name of ruby_xmalloc.
Definition xmalloc.h:53
C99 shim for <stdbool.h>
A line and column in a string.
uint32_t column
The column in bytes.
int32_t line
The line number.
A list of offsets of the start of lines in a string.
uint32_t * offsets
The list of offsets.
size_t size
The number of offsets in the list.
This struct represents a slice in the source code, defined by an offset and a length.
Definition ast.h:569
uint32_t start
The offset of the location from the start of the source.
Definition ast.h:571
uint32_t length
The length of the location.
Definition ast.h:574