File Coverage

include/eshu_xs.h
Criterion Covered Total %
statement 187 220 85.0
branch 148 228 64.9
condition n/a
subroutine n/a
pod n/a
total 335 448 74.7


line stmt bran cond sub pod time code
1             /*
2             * eshu_xs.h — XS file indentation scanner
3             *
4             * Dual-mode: delegates to eshu_c.h for C code above `MODULE =`,
5             * then uses XS-specific rules for XSUB declarations below.
6             */
7              
8             #ifndef ESHU_XS_H
9             #define ESHU_XS_H
10              
11             #include "eshu.h"
12             #include "eshu_c.h"
13              
14             /* ══════════════════════════════════════════════════════════════════
15             * XS mode tracking
16             * ══════════════════════════════════════════════════════════════════ */
17              
18             enum eshu_xs_mode {
19             ESHU_XS_C_MODE, /* before MODULE = line */
20             ESHU_XS_XSUB_MODE /* after MODULE = line */
21             };
22              
23             enum eshu_xs_section {
24             ESHU_XS_NONE, /* between XSUBs / return type line */
25             ESHU_XS_PARAMS, /* parameter declarations */
26             ESHU_XS_LABEL, /* just saw a label (CODE: etc) */
27             ESHU_XS_BODY /* inside a label body */
28             };
29              
30             typedef struct {
31             enum eshu_xs_mode mode;
32             enum eshu_xs_section section;
33             eshu_ctx_t c_ctx; /* C scanner context */
34             int c_depth; /* brace depth in CODE: */
35             int is_boot; /* inside BOOT section */
36             eshu_config_t cfg;
37             } eshu_xs_ctx_t;
38              
39 33           static void eshu_xs_ctx_init(eshu_xs_ctx_t *ctx, const eshu_config_t *cfg) {
40 33           ctx->mode = ESHU_XS_C_MODE;
41 33           ctx->section = ESHU_XS_NONE;
42 33           ctx->c_depth = 0;
43 33           ctx->is_boot = 0;
44 33           ctx->cfg = *cfg;
45 33           eshu_ctx_init(&ctx->c_ctx, cfg);
46 33           }
47              
48             /* ══════════════════════════════════════════════════════════════════
49             * Detection helpers
50             * ══════════════════════════════════════════════════════════════════ */
51              
52             /* Check if the line starts with MODULE followed by optional
53             * whitespace and '=' */
54 1321           static int eshu_xs_is_module_line(const char *content, const char *eol) {
55 1321           const char *p = content;
56 1321 100         if (eol - p < 8) return 0;
57 964 100         if (memcmp(p, "MODULE", 6) != 0) return 0;
58 34           p += 6;
59 68 50         while (p < eol && (*p == ' ' || *p == '\t')) p++;
    100          
    50          
60 34 50         return (p < eol && *p == '=');
    50          
61             }
62              
63             /* XS labels: CODE, INIT, OUTPUT, PREINIT, CLEANUP, POSTCALL,
64             * PPCODE, BOOT, CASE, INTERFACE, INTERFACE_MACRO, PROTOTYPES,
65             * VERSIONCHECK, INCLUDE, FALLBACK, OVERLOAD, ALIAS, ATTRS */
66 1162           static int eshu_xs_is_label(const char *content, const char *eol,
67             int *is_boot) {
68 1162           const char *p = content;
69             const char *start;
70             int len;
71              
72 1162           *is_boot = 0;
73              
74             /* must start with alpha */
75 1162 100         if (!isalpha((unsigned char)*p)) return 0;
76              
77 875           start = p;
78 5070 100         while (p < eol && (isalpha((unsigned char)*p) || *p == '_'))
    100          
    100          
79 4195           p++;
80 875           len = (int)(p - start);
81              
82             /* must be followed by ':' (possibly with whitespace) */
83 1304 100         while (p < eol && (*p == ' ' || *p == '\t')) p++;
    100          
    50          
84 875 100         if (p >= eol || *p != ':') return 0;
    100          
85              
86             /* check it's not a C label like 'default:' or 'case:' */
87             /* it also must not be a :: (package separator) */
88 131 100         if (p + 1 < eol && *(p + 1) == ':') return 0;
    50          
89              
90             /* Known XS labels */
91 131 100         if ((len == 4 && memcmp(start, "CODE", 4) == 0) ||
    100          
    100          
92 75 50         (len == 4 && memcmp(start, "INIT", 4) == 0) ||
    100          
93 74 100         (len == 6 && memcmp(start, "OUTPUT", 6) == 0) ||
    100          
94 32 100         (len == 7 && memcmp(start, "PREINIT", 7) == 0) ||
    100          
95 13 100         (len == 7 && memcmp(start, "CLEANUP", 7) == 0) ||
    50          
96 12 0         (len == 8 && memcmp(start, "POSTCALL", 8) == 0) ||
    100          
97 12 50         (len == 6 && memcmp(start, "PPCODE", 6) == 0) ||
    50          
98 11 0         (len == 4 && memcmp(start, "CASE", 4) == 0) ||
    50          
99 11 0         (len == 9 && memcmp(start, "INTERFACE", 9) == 0) ||
    50          
100 11 0         (len == 15 && memcmp(start, "INTERFACE_MACRO", 15) == 0) ||
    100          
101 11 50         (len == 10 && memcmp(start, "PROTOTYPES", 10) == 0) ||
    50          
102 7 0         (len == 12 && memcmp(start, "VERSIONCHECK", 12) == 0) ||
    100          
103 7 100         (len == 7 && memcmp(start, "INCLUDE", 7) == 0) ||
    50          
104 6 0         (len == 8 && memcmp(start, "FALLBACK", 8) == 0) ||
    50          
105 6 0         (len == 8 && memcmp(start, "OVERLOAD", 8) == 0) ||
    100          
106 6 50         (len == 5 && memcmp(start, "ALIAS", 5) == 0) ||
    50          
107 0 0         (len == 5 && memcmp(start, "ATTRS", 5) == 0)) {
108             /* not a label that belongs to these */
109             } else {
110 3           return 0;
111             }
112              
113 128 100         if (len == 4 && memcmp(start, "BOOT", 4) == 0)
    50          
114 0           *is_boot = 1;
115             /* BOOT is checked above but it's not in the list — add it */
116              
117 128           return 1;
118             }
119              
120             /* Check for BOOT: label specifically */
121 1168           static int eshu_xs_is_boot_label(const char *content, const char *eol) {
122 1168           const char *p = content;
123 1168 100         if (eol - p < 5) return 0;
124 1038 100         if (memcmp(p, "BOOT", 4) != 0) return 0;
125 6           p += 4;
126 6 50         while (p < eol && (*p == ' ' || *p == '\t')) p++;
    50          
    50          
127 6 50         return (p < eol && *p == ':' && (p + 1 >= eol || *(p + 1) != ':'));
    50          
    50          
    0          
128             }
129              
130             /* Detect if line is a new XSUB return type / function header.
131             * In XS mode at depth 0, a line that starts with an identifier
132             * (C type name) at column 0 and is NOT a label indicates
133             * a new XSUB. */
134 0           static int eshu_xs_is_xsub_start(const char *content, const char *eol) {
135 0           const char *p = content;
136              
137             /* Must start with alpha or underscore (type name) */
138 0 0         if (!isalpha((unsigned char)*p) && *p != '_')
    0          
139 0           return 0;
140              
141             /* Skip the identifier */
142 0 0         while (p < eol && (isalnum((unsigned char)*p) || *p == '_' || *p == '*' || *p == ' ' || *p == '\t'))
    0          
    0          
    0          
    0          
    0          
143 0           p++;
144              
145             /* Could be a function name with parens, or just a type */
146             /* This is sufficient — we rely on being in XS mode at depth 0 */
147 0           return 1;
148             }
149              
150             /* ══════════════════════════════════════════════════════════════════
151             * Scan a line for C nesting changes (simplified for XS body)
152             * ══════════════════════════════════════════════════════════════════ */
153              
154 324           static void eshu_xs_scan_c_nesting(eshu_xs_ctx_t *ctx,
155             const char *p, const char *end) {
156             /* Track {}/()/ nesting within XSUB body code using the C scanner's
157             * state machine for strings/comments, but tracking depth locally */
158 5520 100         while (p < end) {
159 5196           char c = *p;
160              
161 5196           switch (ctx->c_ctx.state) {
162 4882           case ESHU_CODE:
163 4882 100         if (c == '{') {
164 24           ctx->c_depth++;
165 4858 100         } else if (c == '}') {
166 10           ctx->c_depth--;
167 10 50         if (ctx->c_depth < 0) ctx->c_depth = 0;
168 4848 100         } else if (c == '"') {
169 33           ctx->c_ctx.state = ESHU_STRING_DQ;
170 4815 50         } else if (c == '\'') {
171 0           ctx->c_ctx.state = ESHU_CHAR_LIT;
172 4815 100         } else if (c == '/' && p + 1 < end && *(p + 1) == '/') {
    50          
    50          
173 0           return; /* line comment */
174 4815 100         } else if (c == '/' && p + 1 < end && *(p + 1) == '*') {
    50          
    50          
175 2           ctx->c_ctx.state = ESHU_COMMENT_BLOCK;
176 2           p++;
177             }
178 4882           break;
179              
180 246           case ESHU_STRING_DQ:
181 246 100         if (c == '\\' && p + 1 < end) p++;
    50          
182 235 100         else if (c == '"') ctx->c_ctx.state = ESHU_CODE;
183 246           break;
184              
185 0           case ESHU_CHAR_LIT:
186 0 0         if (c == '\\' && p + 1 < end) p++;
    0          
187 0 0         else if (c == '\'') ctx->c_ctx.state = ESHU_CODE;
188 0           break;
189              
190 68           case ESHU_COMMENT_BLOCK:
191 68 100         if (c == '*' && p + 1 < end && *(p + 1) == '/') {
    50          
    50          
192 2           ctx->c_ctx.state = ESHU_CODE;
193 2           p++;
194             }
195 68           break;
196              
197 0           default:
198 0           break;
199             }
200 5196           p++;
201             }
202             }
203              
204             /* ══════════════════════════════════════════════════════════════════
205             * Process a single XS line
206             * ══════════════════════════════════════════════════════════════════ */
207              
208 1455           static void eshu_xs_process_line(eshu_xs_ctx_t *ctx, eshu_buf_t *out,
209             const char *line_start, const char *eol) {
210 1455           const char *content = eshu_skip_leading_ws(line_start);
211             int line_len;
212              
213             /* empty line — preserve it */
214 1455 100         if (content >= eol) {
215 134           eshu_buf_putc(out, '\n');
216 134           return;
217             }
218              
219 1321           line_len = (int)(eol - content);
220              
221             /* ── C mode: before MODULE = ── */
222 1321 100         if (ctx->mode == ESHU_XS_C_MODE) {
223             /* Check if this is the MODULE line */
224 152 100         if (eshu_xs_is_module_line(content, eol)) {
225 33           ctx->mode = ESHU_XS_XSUB_MODE;
226 33           ctx->section = ESHU_XS_NONE;
227 33           ctx->c_depth = 0;
228 33           ctx->c_ctx.state = ESHU_CODE;
229             /* MODULE line at column 0 */
230 33           eshu_buf_write_trimmed(out, content, line_len);
231 33           eshu_buf_putc(out, '\n');
232 33           return;
233             }
234             /* Delegate to C scanner */
235 119           eshu_c_process_line(&ctx->c_ctx, out, line_start, eol);
236 119           return;
237             }
238              
239             /* ── XS mode ── */
240              
241             /* Another MODULE line resets */
242 1169 100         if (eshu_xs_is_module_line(content, eol)) {
243 1           ctx->section = ESHU_XS_NONE;
244 1           ctx->c_depth = 0;
245 1           ctx->c_ctx.state = ESHU_CODE;
246 1           eshu_buf_write_trimmed(out, content, line_len);
247 1           eshu_buf_putc(out, '\n');
248 1           return;
249             }
250              
251             /* Block comment continuation in XS body */
252 1168 50         if (ctx->c_ctx.state == ESHU_COMMENT_BLOCK) {
253 0 0         int depth = (ctx->section == ESHU_XS_BODY) ? 2 + ctx->c_depth : 1;
254 0           eshu_emit_indent(out, depth, &ctx->cfg);
255 0           eshu_buf_write_trimmed(out, content, line_len);
256 0           eshu_buf_putc(out, '\n');
257 0           eshu_xs_scan_c_nesting(ctx, content, eol);
258 0           return;
259             }
260              
261             /* Check for BOOT: label (special: depth 0) */
262 1168 100         if (eshu_xs_is_boot_label(content, eol)) {
263 6           ctx->section = ESHU_XS_LABEL;
264 6           ctx->c_depth = 0;
265 6           ctx->is_boot = 1;
266 6           ctx->c_ctx.state = ESHU_CODE;
267             /* BOOT: at depth 0 */
268 6           eshu_buf_write_trimmed(out, content, line_len);
269 6           eshu_buf_putc(out, '\n');
270 6           ctx->section = ESHU_XS_BODY;
271 6           return;
272             }
273              
274             /* Check for XS labels (CODE:, OUTPUT:, etc.) */
275             {
276 1162           int is_boot = 0;
277 1162 100         if (eshu_xs_is_label(content, eol, &is_boot)) {
278 128           ctx->section = ESHU_XS_LABEL;
279 128           ctx->c_depth = 0;
280 128           ctx->is_boot = 0;
281 128           ctx->c_ctx.state = ESHU_CODE;
282             /* Labels at depth 1 */
283 128           eshu_emit_indent(out, 1, &ctx->cfg);
284 128           eshu_buf_write_trimmed(out, content, line_len);
285 128           eshu_buf_putc(out, '\n');
286 128           ctx->section = ESHU_XS_BODY;
287 128           return;
288             }
289             }
290              
291             /* Check for preprocessor in XS mode */
292 1034 100         if (*content == '#') {
293             /* Preprocessor stays at column 0 in XS too */
294 3           eshu_buf_write_trimmed(out, content, line_len);
295 3           eshu_buf_putc(out, '\n');
296 3           return;
297             }
298              
299             /* Determine indent based on current section */
300 1031           switch (ctx->section) {
301 156           case ESHU_XS_NONE: {
302             /* XSUB return type / function signature — depth 0 */
303 156           eshu_buf_write_trimmed(out, content, line_len);
304 156           eshu_buf_putc(out, '\n');
305              
306             /* If line has '(' it might be the function name line;
307             * the next lines will be params until we hit a label */
308             {
309 156           const char *pp = content;
310 1301 100         while (pp < eol) {
311 1244 100         if (*pp == '(') {
312 99           ctx->section = ESHU_XS_PARAMS;
313 99           break;
314             }
315 1145           pp++;
316             }
317             }
318 156           break;
319             }
320              
321 551           case ESHU_XS_PARAMS:
322             /* Parameter declarations at depth 1 */
323 551           eshu_emit_indent(out, 1, &ctx->cfg);
324 551           eshu_buf_write_trimmed(out, content, line_len);
325 551           eshu_buf_putc(out, '\n');
326 551           break;
327              
328 0           case ESHU_XS_LABEL:
329             /* Shouldn't get here — labels transition to BODY immediately */
330 0           eshu_emit_indent(out, 2, &ctx->cfg);
331 0           eshu_buf_write_trimmed(out, content, line_len);
332 0           eshu_buf_putc(out, '\n');
333 0           break;
334              
335 324           case ESHU_XS_BODY: {
336             /* Code inside a label body — base depth 2, or 1 for BOOT */
337 324 100         int base = ctx->is_boot ? 1 : 2;
338 324           int depth = base + ctx->c_depth;
339              
340             /* If line starts with '}', dedent first */
341 324 100         if (*content == '}') {
342 10           depth--;
343 10 50         if (depth < base) depth = base;
344             }
345              
346 324           eshu_emit_indent(out, depth, &ctx->cfg);
347 324           eshu_buf_write_trimmed(out, content, line_len);
348 324           eshu_buf_putc(out, '\n');
349              
350             /* Scan for C nesting changes */
351 324           eshu_xs_scan_c_nesting(ctx, content, eol);
352 324           break;
353             }
354             }
355             }
356              
357             /* ══════════════════════════════════════════════════════════════════
358             * Detect new XSUB boundary — reset section when we see a blank
359             * line or a new return-type line at depth 0 after a completed XSUB
360             * ══════════════════════════════════════════════════════════════════ */
361              
362 1455           static void eshu_xs_check_xsub_boundary(eshu_xs_ctx_t *ctx,
363             const char *content, const char *eol) {
364             /* If we're in XS mode and see a blank line, reset section */
365 1455 100         if (content >= eol) {
366 134 100         if (ctx->mode == ESHU_XS_XSUB_MODE) {
367 111           ctx->section = ESHU_XS_NONE;
368 111           ctx->c_depth = 0;
369 111           ctx->is_boot = 0;
370 111           ctx->c_ctx.state = ESHU_CODE;
371             }
372 134           return;
373             }
374              
375             /* If we're in params/body and hit a line at column 0 that looks
376             * like a new return type, reset */
377 1321 100         if (ctx->mode == ESHU_XS_XSUB_MODE &&
378 1169 100         ctx->c_depth == 0 &&
379 1053 100         (ctx->section == ESHU_XS_PARAMS || ctx->section == ESHU_XS_BODY)) {
380             /* A non-indented line that doesn't look like it belongs to
381             * current XSUB — might be new return type */
382             /* We handle this via blank-line separation instead */
383             }
384             }
385              
386             /* ══════════════════════════════════════════════════════════════════
387             * Public API — indent an XS source string
388             * ══════════════════════════════════════════════════════════════════ */
389              
390 33           static char * eshu_indent_xs(const char *src, size_t src_len,
391             const eshu_config_t *cfg, size_t *out_len) {
392             eshu_xs_ctx_t ctx;
393             eshu_buf_t out;
394 33           const char *p = src;
395 33           const char *end = src + src_len;
396             char *result;
397              
398 33           eshu_xs_ctx_init(&ctx, cfg);
399 33           eshu_buf_init(&out, src_len + 256);
400              
401             {
402 33           int line_num = 1;
403 1488 100         while (p < end) {
404 1455           const char *eol = eshu_find_eol(p);
405 1455           const char *content = eshu_skip_leading_ws(p);
406              
407             /* Check for XSUB boundary (blank lines reset section) */
408 1455           eshu_xs_check_xsub_boundary(&ctx, content, eol);
409              
410 1455 50         if (eshu_in_range(cfg, line_num)) {
411 1455           eshu_xs_process_line(&ctx, &out, p, eol);
412             } else {
413 0           size_t saved = out.len;
414 0           eshu_xs_process_line(&ctx, &out, p, eol);
415 0           out.len = saved;
416 0           eshu_buf_write_trimmed(&out, p, (int)(eol - p));
417 0           eshu_buf_putc(&out, '\n');
418             }
419              
420 1455           p = eol;
421 1455 50         if (*p == '\n') p++;
422 1455           line_num++;
423             }
424             }
425              
426             /* NUL-terminate */
427 33           eshu_buf_putc(&out, '\0');
428 33           out.len--;
429              
430 33           *out_len = out.len;
431 33           result = out.data;
432 33           return result;
433             }
434              
435             #endif /* ESHU_XS_H */