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 2177           static int eshu_xs_is_module_line(const char *content, const char *eol) {
55 2177           const char *p = content;
56 2177 100         if (eol - p < 8) return 0;
57 1676 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 1960           static int eshu_xs_is_label(const char *content, const char *eol,
67             int *is_boot) {
68 1960           const char *p = content;
69             const char *start;
70             int len;
71              
72 1960           *is_boot = 0;
73              
74             /* must start with alpha */
75 1960 100         if (!isalpha((unsigned char)*p)) return 0;
76              
77 1451           start = p;
78 8882 100         while (p < eol && (isalpha((unsigned char)*p) || *p == '_'))
    100          
    100          
79 7431           p++;
80 1451           len = (int)(p - start);
81              
82             /* must be followed by ':' (possibly with whitespace) */
83 2222 100         while (p < eol && (*p == ' ' || *p == '\t')) p++;
    100          
    50          
84 1451 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 151 100         if (p + 1 < eol && *(p + 1) == ':') return 0;
    50          
89              
90             /* Known XS labels */
91 151 100         if ((len == 4 && memcmp(start, "CODE", 4) == 0) ||
    100          
    100          
92 85 50         (len == 4 && memcmp(start, "INIT", 4) == 0) ||
    100          
93 84 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 148 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 148           return 1;
118             }
119              
120             /* Check for BOOT: label specifically */
121 1966           static int eshu_xs_is_boot_label(const char *content, const char *eol) {
122 1966           const char *p = content;
123 1966 100         if (eol - p < 5) return 0;
124 1740 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 424           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 6952 100         while (p < end) {
159 6528           char c = *p;
160              
161 6528           switch (ctx->c_ctx.state) {
162 6214           case ESHU_CODE:
163 6214 100         if (c == '{') {
164 34           ctx->c_depth++;
165 6180 100         } else if (c == '}') {
166 10           ctx->c_depth--;
167 10 50         if (ctx->c_depth < 0) ctx->c_depth = 0;
168 6170 100         } else if (c == '"') {
169 33           ctx->c_ctx.state = ESHU_STRING_DQ;
170 6137 50         } else if (c == '\'') {
171 0           ctx->c_ctx.state = ESHU_CHAR_LIT;
172 6137 100         } else if (c == '/' && p + 1 < end && *(p + 1) == '/') {
    50          
    50          
173 0           return; /* line comment */
174 6137 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 6214           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 6528           p++;
201             }
202             }
203              
204             /* ══════════════════════════════════════════════════════════════════
205             * Process a single XS line
206             * ══════════════════════════════════════════════════════════════════ */
207              
208 2383           static void eshu_xs_process_line(eshu_xs_ctx_t *ctx, eshu_buf_t *out,
209             const char *line_start, const char *eol) {
210 2383           const char *content = eshu_skip_leading_ws(line_start);
211             int line_len;
212              
213             /* empty line — preserve it */
214 2383 100         if (content >= eol) {
215 206           eshu_buf_putc(out, '\n');
216 206           return;
217             }
218              
219 2177           line_len = (int)(eol - content);
220              
221             /* ── C mode: before MODULE = ── */
222 2177 100         if (ctx->mode == ESHU_XS_C_MODE) {
223             /* Check if this is the MODULE line */
224 210 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 177           eshu_c_process_line(&ctx->c_ctx, out, line_start, eol);
236 177           return;
237             }
238              
239             /* ── XS mode ── */
240              
241             /* Another MODULE line resets */
242 1967 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 1966 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 1966 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 1960           int is_boot = 0;
277 1960 100         if (eshu_xs_is_label(content, eol, &is_boot)) {
278 148           ctx->section = ESHU_XS_LABEL;
279 148           ctx->c_depth = 0;
280 148           ctx->is_boot = 0;
281 148           ctx->c_ctx.state = ESHU_CODE;
282             /* Labels at depth 1 */
283 148           eshu_emit_indent(out, 1, &ctx->cfg);
284 148           eshu_buf_write_trimmed(out, content, line_len);
285 148           eshu_buf_putc(out, '\n');
286 148           ctx->section = ESHU_XS_BODY;
287 148           return;
288             }
289             }
290              
291             /* Check for preprocessor in XS mode */
292 1812 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 1809           switch (ctx->section) {
301 250           case ESHU_XS_NONE: {
302             /* XSUB return type / function signature — depth 0 */
303 250           eshu_buf_write_trimmed(out, content, line_len);
304 250           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 250           const char *pp = content;
310 2537 100         while (pp < eol) {
311 2456 100         if (*pp == '(') {
312 169           ctx->section = ESHU_XS_PARAMS;
313 169           break;
314             }
315 2287           pp++;
316             }
317             }
318 250           break;
319             }
320              
321 1135           case ESHU_XS_PARAMS:
322             /* Parameter declarations at depth 1 */
323 1135           eshu_emit_indent(out, 1, &ctx->cfg);
324 1135           eshu_buf_write_trimmed(out, content, line_len);
325 1135           eshu_buf_putc(out, '\n');
326 1135           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 424           case ESHU_XS_BODY: {
336             /* Code inside a label body — base depth 2, or 1 for BOOT */
337 424 100         int base = ctx->is_boot ? 1 : 2;
338 424           int depth = base + ctx->c_depth;
339              
340             /* If line starts with '}', dedent first */
341 424 100         if (*content == '}') {
342 10           depth--;
343 10 50         if (depth < base) depth = base;
344             }
345              
346 424           eshu_emit_indent(out, depth, &ctx->cfg);
347 424           eshu_buf_write_trimmed(out, content, line_len);
348 424           eshu_buf_putc(out, '\n');
349              
350             /* Scan for C nesting changes */
351 424           eshu_xs_scan_c_nesting(ctx, content, eol);
352 424           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 2383           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 2383 100         if (content >= eol) {
366 206 100         if (ctx->mode == ESHU_XS_XSUB_MODE) {
367 181           ctx->section = ESHU_XS_NONE;
368 181           ctx->c_depth = 0;
369 181           ctx->is_boot = 0;
370 181           ctx->c_ctx.state = ESHU_CODE;
371             }
372 206           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 2177 100         if (ctx->mode == ESHU_XS_XSUB_MODE &&
378 1967 100         ctx->c_depth == 0 &&
379 1771 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 2416 100         while (p < end) {
404 2383           const char *eol = eshu_find_eol(p);
405 2383           const char *content = eshu_skip_leading_ws(p);
406              
407             /* Check for XSUB boundary (blank lines reset section) */
408 2383           eshu_xs_check_xsub_boundary(&ctx, content, eol);
409              
410 2383 50         if (eshu_in_range(cfg, line_num)) {
411 2383           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 2383           p = eol;
421 2383 50         if (*p == '\n') p++;
422 2383           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 */