Line data Source code
1 : /// [TODO]: Is there a way to use an dependency to make json output instead we do it ourself?
2 : /// [TODO]: Is using json the best here? Or we can use msgpack here?
3 :
4 : // wok_diag -- batching diagnostic sink implementation. See wok_diag.h for
5 : // the contract: item granularity, cascade suppression at an already-reported
6 : // offset, and a hard cap with a single trailing E-TOO-MANY.
7 :
8 : #include "wok_diag.h"
9 :
10 : #include <stdarg.h>
11 : #include <stdbool.h>
12 : #include <string.h>
13 :
14 : // One entry per WokDiagCode, in declaration order (the enum is generated
15 : // from the same X-macro, so the indices line up).
16 : #define WOK_X(name, text) [name] = text,
17 : static const char *const wok_diag_text_table[] = {WOK_DIAG_CODES(WOK_X)};
18 : #undef WOK_X
19 :
20 : static_assert(sizeof(wok_diag_text_table) / sizeof(wok_diag_text_table[0]) ==
21 : WOK_DIAG_CODE_COUNT,
22 : "wok_diag text table must have one entry per WokDiagCode");
23 :
24 92 : WOK_READONLY const char *wok_diag_code_text(WokDiagCode code) {
25 92 : return wok_diag_text_table[code];
26 : }
27 :
28 : struct WokDiagSink {
29 : WokArena *arena;
30 : const char *path;
31 : const char *src;
32 : usize src_len;
33 :
34 : WokDiag *diags;
35 : usize count;
36 : usize capacity;
37 :
38 : bool has_last_off;
39 : u32 last_off;
40 :
41 : bool too_many_emitted;
42 :
43 : u32 *line_starts; // lazily built; nullptr until first use
44 : usize line_count;
45 : };
46 :
47 95502 : WokDiagSink *wok_diag_new(WokArena *arena, const char *path, const char *src,
48 : usize src_len) {
49 95502 : WokDiagSink *s = WOK_NEW(arena, WokDiagSink);
50 95502 : s->arena = arena;
51 95502 : s->path = path;
52 95502 : s->src = src;
53 95502 : s->src_len = src_len;
54 95502 : s->diags = nullptr;
55 95502 : s->count = 0;
56 95502 : s->capacity = 0;
57 95502 : s->has_last_off = false;
58 95502 : s->last_off = 0;
59 95502 : s->too_many_emitted = false;
60 95502 : s->line_starts = nullptr;
61 95502 : s->line_count = 0;
62 95502 : return s;
63 : }
64 :
65 107024 : static void wok_diag_grow(WokDiagSink *s) {
66 107024 : if (s->count < s->capacity) return;
67 :
68 34502 : usize new_capacity = s->capacity == 0 ? (usize)4 : s->capacity * 2;
69 34502 : WokDiag *grown = WOK_NEW_N(s->arena, WokDiag, new_capacity);
70 34502 : if (s->count > 0) memcpy(grown, s->diags, s->count * sizeof(WokDiag));
71 34502 : s->diags = grown;
72 34502 : s->capacity = new_capacity;
73 : }
74 :
75 107024 : static void wok_diag_append(WokDiagSink *s, WokDiagCode code, u32 off,
76 : u32 len, const char *msg) {
77 107024 : wok_diag_grow(s);
78 :
79 107024 : char *owned = wok_arena_copy(s->arena, msg, strlen(msg));
80 107024 : s->diags[s->count] = (WokDiag){.code = code, .off = off, .len = len, .msg = owned};
81 107024 : s->count += 1;
82 107024 : s->has_last_off = true;
83 107024 : s->last_off = off;
84 107024 : }
85 :
86 678387 : void wok_diag_add_text(WokDiagSink *s, WokDiagCode code, u32 off, u32 len,
87 : const char *msg) {
88 678387 : if (s->too_many_emitted) return;
89 :
90 110872 : if (s->count >= WOK_DIAG_CAP) {
91 3772 : s->too_many_emitted = true;
92 3772 : wok_diag_append(s, WOK_E_TOO_MANY, off, 0, "too many errors; stopping here");
93 3772 : return;
94 : }
95 :
96 107100 : if (s->has_last_off && off == s->last_off) return;
97 :
98 103252 : wok_diag_append(s, code, off, len, msg);
99 : }
100 :
101 678380 : void wok_diag_add(WokDiagSink *s, WokDiagCode code, u32 off, u32 len,
102 : const char *fmt, ...) {
103 678380 : char buf[512];
104 678380 : va_list ap;
105 678380 : va_start(ap, fmt);
106 678380 : (void)vsnprintf(buf, sizeof buf, fmt, ap);
107 678380 : va_end(ap);
108 :
109 678380 : wok_diag_add_text(s, code, off, len, buf);
110 678380 : }
111 :
112 93224 : usize wok_diag_count(const WokDiagSink *s) { return s->count; }
113 :
114 87 : const WokDiag *wok_diag_at(const WokDiagSink *s, usize i) { return &s->diags[i]; }
115 :
116 0 : bool wok_diag_full(const WokDiagSink *s) { return s->too_many_emitted; }
117 :
118 94 : static void wok_diag_build_lines(WokDiagSink *s) {
119 94 : if (s->line_starts != nullptr) return;
120 :
121 : usize line_count = 1;
122 11716 : for (usize i = 0; i < s->src_len; i++) {
123 11670 : if (s->src[i] == '\n') line_count += 1;
124 : }
125 :
126 46 : u32 *starts = WOK_NEW_N(s->arena, u32, line_count);
127 46 : starts[0] = 0;
128 46 : usize idx = 1;
129 11716 : for (usize i = 0; i < s->src_len; i++) {
130 11670 : if (s->src[i] == '\n') {
131 439 : starts[idx] = (u32)(i + 1);
132 439 : idx += 1;
133 : }
134 : }
135 :
136 46 : s->line_starts = starts;
137 46 : s->line_count = line_count;
138 : }
139 :
140 94 : void wok_diag_position(const WokDiagSink *sink, u32 off, u32 *line,
141 : u32 *col) {
142 : // The line table is a cache: build it on first use even though the sink
143 : // is observed through a const pointer here.
144 94 : WokDiagSink *s = (WokDiagSink *)sink;
145 94 : wok_diag_build_lines(s);
146 :
147 : // Rightmost line whose start is <= off. line_starts[0] == 0 always
148 : // satisfies this, so the search invariant holds from the first iteration;
149 : // an off past the end of the source simply resolves to the last line.
150 94 : usize lo = 0;
151 94 : usize hi = s->line_count;
152 461 : while (lo + 1 < hi) {
153 273 : usize mid = lo + (hi - lo) / 2;
154 273 : if (s->line_starts[mid] <= off) {
155 : lo = mid;
156 : } else {
157 115 : hi = mid;
158 : }
159 : }
160 :
161 94 : *line = (u32)(lo + 1);
162 94 : *col = off - s->line_starts[lo] + 1;
163 94 : }
164 :
165 3 : void wok_diag_render(const WokDiagSink *s, FILE *out) {
166 6 : for (usize i = 0; i < s->count; i++) {
167 3 : const WokDiag *d = &s->diags[i];
168 3 : u32 line;
169 3 : u32 col;
170 3 : wok_diag_position(s, d->off, &line, &col);
171 : // No code here, deliberately. The human-facing line stays
172 : // `file:line:col: message`, which is what an editor's error regex and a
173 : // reader both want; the machine-readable code is in the JSON Lines
174 : // renderer below, which is where a tool should be looking for it.
175 3 : (void)fprintf(out, "%s:%u:%u: %s\n", s->path, line, col, d->msg);
176 : }
177 3 : }
178 :
179 : // Emits a JSON string. It does NOT validate UTF-8: that happens once, when
180 : // the source file is read, and every diagnostic message is built from ASCII
181 : // literals plus text the scanner already validated. Two places used to break
182 : // that invariant and both were fixed at the source rather than patched here --
183 : // the scanner spelled an unknown escape with the raw byte, and the parser
184 : // truncated a quoted token on a BYTE boundary, cutting characters in half on
185 : // valid input. test/test_diag.c pins the invariant end to end.
186 105 : static void wok_diag_json_string(FILE *out, const char *text) {
187 105 : (void)fputc('"', out);
188 1190 : for (const unsigned char *p = (const unsigned char *)text; *p != 0; p++) {
189 1085 : unsigned char c = *p;
190 1085 : switch (c) {
191 3 : case '"': (void)fputs("\\\"", out); break;
192 2 : case '\\': (void)fputs("\\\\", out); break;
193 1 : case '\n': (void)fputs("\\n", out); break;
194 1 : case '\t': (void)fputs("\\t", out); break;
195 0 : case '\r': (void)fputs("\\r", out); break;
196 1078 : default:
197 1078 : if (c < 0x20) (void)fprintf(out, "\\u%04x", (unsigned int)c);
198 1078 : else (void)fputc(c, out);
199 : break;
200 : }
201 : }
202 105 : (void)fputc('"', out);
203 105 : }
204 :
205 13 : void wok_diag_render_jsonl(const WokDiagSink *s, FILE *out) {
206 48 : for (usize i = 0; i < s->count; i++) {
207 35 : const WokDiag *d = &s->diags[i];
208 35 : u32 line;
209 35 : u32 col;
210 35 : wok_diag_position(s, d->off, &line, &col);
211 :
212 35 : (void)fputs("{\"file\":", out);
213 35 : wok_diag_json_string(out, s->path);
214 35 : (void)fprintf(out, ",\"line\":%u,\"col\":%u,\"code\":", line, col);
215 35 : wok_diag_json_string(out, wok_diag_code_text(d->code));
216 35 : (void)fputs(",\"message\":", out);
217 35 : wok_diag_json_string(out, d->msg);
218 35 : (void)fputs("}\n", out); // one record per line: that is the format
219 : }
220 13 : }
|