Skip to main content

padlock_source/
fixgen.rs

1// padlock-source/src/fixgen.rs
2//
3// Generate reordered struct source text, unified diffs, and in-place rewrites.
4//
5// Fix quality: when the original source is available, field declarations
6// (including attributes, doc-comments, visibility modifiers, and guard
7// annotations) are extracted verbatim and reordered — nothing is
8// synthesised from IR type names. IR-based generation is used only as a
9// fallback when the original text cannot be parsed into per-field chunks.
10
11use padlock_core::ir::{StructLayout, optimal_order};
12use similar::{ChangeTag, TextDiff};
13
14/// Render a reordered C/C++ struct definition as source text.
15///
16/// Uses the field names already present in the layout — type names come from
17/// the `TypeInfo::Primitive/Opaque` name stored during source parsing.
18pub fn generate_c_fix(layout: &StructLayout) -> String {
19    let optimal = optimal_order(layout);
20    let mut out = format!("struct {} {{\n", layout.name);
21    for field in &optimal {
22        let ty = field_type_name(field);
23        out.push_str(&format!("    {ty} {};\n", field.name));
24    }
25    out.push_str("};\n");
26    out
27}
28
29/// Render a reordered Rust struct definition as source text.
30pub fn generate_rust_fix(layout: &StructLayout) -> String {
31    let optimal = optimal_order(layout);
32    // Detect tuple struct: all field names are `_N` (digit-only suffix)
33    let is_tuple = optimal
34        .iter()
35        .all(|f| f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit()));
36    if is_tuple {
37        let types: Vec<String> = optimal
38            .iter()
39            .map(|f| field_type_name(f).to_string())
40            .collect();
41        return format!("struct {}({});\n", layout.name, types.join(", "));
42    }
43    let mut out = format!("struct {} {{\n", layout.name);
44    for field in &optimal {
45        let ty = field_type_name(field);
46        out.push_str(&format!("    {}: {ty},\n", field.name));
47    }
48    out.push_str("}\n");
49    out
50}
51
52/// Render a reordered Go struct definition as source text.
53pub fn generate_go_fix(layout: &StructLayout) -> String {
54    let optimal = optimal_order(layout);
55    let mut out = format!("type {} struct {{\n", layout.name);
56    for field in &optimal {
57        let ty = field_type_name(field);
58        out.push_str(&format!("\t{}\t{ty}\n", field.name));
59    }
60    out.push_str("}\n");
61    out
62}
63
64/// Produce a unified diff between `original` and `fixed` source text.
65pub fn unified_diff(original: &str, fixed: &str, context_lines: usize) -> String {
66    if original == fixed {
67        return String::from("(no changes)\n");
68    }
69    let diff = TextDiff::from_lines(original, fixed);
70    let mut out = String::new();
71    for (idx, group) in diff.grouped_ops(context_lines).iter().enumerate() {
72        if idx > 0 {
73            out.push_str("...\n");
74        }
75        for op in group {
76            for change in diff.iter_changes(op) {
77                let prefix = match change.tag() {
78                    ChangeTag::Delete => "-",
79                    ChangeTag::Insert => "+",
80                    ChangeTag::Equal => " ",
81                };
82                out.push_str(&format!("{prefix} {}", change.value()));
83                if !change.value().ends_with('\n') {
84                    out.push('\n');
85                }
86            }
87        }
88    }
89    out
90}
91
92// ── source-aware field chunk extraction ───────────────────────────────────────
93//
94// Each language extracts "field chunks" — the verbatim source text for one
95// field declaration, including any preceding doc comments and attributes.
96// The list is keyed by field name so callers can look up chunks by the IR
97// field names and reorder them.
98
99/// Split a Rust struct body (the text between `{` and `}`, exclusive) into
100/// field chunks, preserving attributes, doc comments, and visibility modifiers.
101///
102/// Returns `Vec<(field_name, raw_chunk_text)>` in declaration order.
103/// The `raw_chunk_text` includes the field declaration line and its trailing
104/// comma; attributes/doc-comments that appear immediately before the field
105/// are included in that field's chunk.
106///
107/// Chunk boundaries are determined by `,` at bracket depth 0, matching how
108/// Rust struct fields are separated. The `>` character is tracked conservatively:
109/// if depth goes negative it is reset to 0 (handles `->` and comparison operators
110/// in default expressions, though those are rare in struct bodies).
111pub fn extract_rust_field_chunks(body: &str) -> Vec<(String, String)> {
112    let mut result: Vec<(String, String)> = Vec::new();
113    let mut depth: i32 = 0; // tracks < ( [ nesting within a field declaration
114    let mut chunk_start = 0usize;
115    let bytes = body.as_bytes();
116    let mut i = 0usize;
117
118    while i < bytes.len() {
119        match bytes[i] {
120            // Line comments: skip to EOL (don't count brackets inside them)
121            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
122                while i < bytes.len() && bytes[i] != b'\n' {
123                    i += 1;
124                }
125            }
126            // Block comments: skip to */
127            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
128                i += 2;
129                while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
130                    i += 1;
131                }
132                i += 2;
133            }
134            // String literals: skip to closing quote
135            b'"' => {
136                i += 1;
137                while i < bytes.len() {
138                    if bytes[i] == b'\\' {
139                        i += 2;
140                        continue;
141                    }
142                    if bytes[i] == b'"' {
143                        i += 1;
144                        break;
145                    }
146                    i += 1;
147                }
148            }
149            b'<' | b'(' | b'[' => {
150                depth += 1;
151                i += 1;
152            }
153            b'>' | b')' | b']' => {
154                depth = (depth - 1).max(0);
155                i += 1;
156            }
157            // Curly braces (e.g. struct default field values, closure syntax):
158            // just skip past them; they don't appear in normal struct bodies
159            b'{' | b'}' => {
160                i += 1;
161            }
162            b',' if depth == 0 => {
163                i += 1; // include the comma in the chunk
164                let chunk = &body[chunk_start..i];
165                if let Some(name) = rust_field_name_from_chunk(chunk) {
166                    result.push((name, chunk.to_string()));
167                }
168                chunk_start = i;
169            }
170            _ => {
171                i += 1;
172            }
173        }
174    }
175
176    // Handle last field (may not have trailing comma)
177    let tail = body[chunk_start..].trim();
178    if !tail.is_empty() {
179        // Use the full slice (including leading whitespace) for the chunk
180        let chunk = &body[chunk_start..];
181        if let Some(name) = rust_field_name_from_chunk(chunk) {
182            result.push((name, chunk.to_string()));
183        }
184    }
185
186    result
187}
188
189/// Extract the field name from a Rust field chunk.
190/// Handles leading attributes (`#[...]`), doc comments (`///`), and pub
191/// visibility (`pub`, `pub(crate)`, `pub(super)`, `pub(in path::to::mod)`).
192fn rust_field_name_from_chunk(chunk: &str) -> Option<String> {
193    for line in chunk.lines() {
194        let s = line.trim();
195        if s.is_empty() || s.starts_with("//") || s.starts_with("#[") || s.starts_with("#![") {
196            continue;
197        }
198        return rust_field_name_from_decl_line(s);
199    }
200    None
201}
202
203/// Parse `[pub[(...)]] field_name: Type` and return the field name.
204fn rust_field_name_from_decl_line(line: &str) -> Option<String> {
205    let mut s = line.trim();
206
207    // Strip visibility
208    if let Some(rest) = s.strip_prefix("pub") {
209        let rest = rest.trim_start();
210        if rest.starts_with('(') {
211            // pub(crate), pub(super), pub(in path) — find the closing ')'
212            let end = rest.find(')')?;
213            s = rest[end + 1..].trim_start();
214        } else {
215            s = rest;
216        }
217    }
218
219    // The field name ends at the first ':' not followed by ':'
220    let mut depth: i32 = 0;
221    for (idx, c) in s.char_indices() {
222        match c {
223            '<' | '(' | '[' => depth += 1,
224            '>' | ')' | ']' => depth = (depth - 1).max(0),
225            ':' if depth == 0 => {
226                // Make sure this ':' is the field separator, not '::'
227                if s[idx + 1..].starts_with(':') {
228                    continue; // qualified path
229                }
230                let name = s[..idx].trim().to_string();
231                if !name.is_empty()
232                    && name.chars().all(|c| c.is_alphanumeric() || c == '_')
233                    && !name.starts_with(|c: char| c.is_ascii_digit())
234                {
235                    return Some(name);
236                }
237                return None;
238            }
239            _ => {}
240        }
241    }
242    None
243}
244
245/// Split a C/C++ struct body (text between `{` and `}`, exclusive) into
246/// field chunks separated by `;` at depth 0.
247///
248/// Each chunk includes any preceding `//` or `/* */` comments.
249pub fn extract_c_field_chunks(body: &str) -> Vec<(String, String)> {
250    let mut result: Vec<(String, String)> = Vec::new();
251    let mut depth: i32 = 0;
252    let mut chunk_start = 0usize;
253    let bytes = body.as_bytes();
254    let mut i = 0usize;
255
256    while i < bytes.len() {
257        match bytes[i] {
258            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
259                while i < bytes.len() && bytes[i] != b'\n' {
260                    i += 1;
261                }
262            }
263            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
264                i += 2;
265                while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
266                    i += 1;
267                }
268                i += 2;
269            }
270            b'"' => {
271                i += 1;
272                while i < bytes.len() {
273                    if bytes[i] == b'\\' {
274                        i += 2;
275                        continue;
276                    }
277                    if bytes[i] == b'"' {
278                        i += 1;
279                        break;
280                    }
281                    i += 1;
282                }
283            }
284            b'<' | b'(' | b'[' | b'{' => {
285                depth += 1;
286                i += 1;
287            }
288            b'>' | b')' | b']' | b'}' => {
289                depth = (depth - 1).max(0);
290                i += 1;
291            }
292            b';' if depth == 0 => {
293                i += 1;
294                let chunk = &body[chunk_start..i];
295                if !chunk.trim().is_empty()
296                    && let Some(name) = c_field_name_from_chunk(chunk)
297                {
298                    result.push((name, chunk.to_string()));
299                }
300                chunk_start = i;
301            }
302            _ => {
303                i += 1;
304            }
305        }
306    }
307    result
308}
309
310/// Extract a C/C++ field name from a chunk (everything up to and including `;`).
311/// The field name is the last identifier before the `;`, stripping pointer
312/// declarators and array declarators.
313fn c_field_name_from_chunk(chunk: &str) -> Option<String> {
314    // Strip comments to get just the code text
315    let code: String = chunk
316        .lines()
317        .filter(|l| !l.trim().starts_with("//"))
318        .collect::<Vec<_>>()
319        .join(" ");
320
321    // Tokenise by whitespace and punctuation; look for the last identifier-like
322    // token before `;`, skipping keywords and type names
323    let stripped = code.trim_end_matches(';').trim();
324    // Strip array declarator: `field[N]` → `field`
325    let stripped = if let Some(bracket) = stripped.rfind('[') {
326        stripped[..bracket].trim()
327    } else {
328        stripped
329    };
330    // Strip pointer declarators at the end
331    let stripped = stripped
332        .trim_start_matches('*')
333        .trim_end_matches('*')
334        .trim();
335
336    // The last whitespace-separated token is the field name
337    let last = stripped.split_whitespace().next_back()?;
338    // Strip leading `*` (pointer declarator attached to name)
339    let last = last.trim_start_matches('*').trim_end_matches('*');
340
341    if last.chars().all(|c| c.is_alphanumeric() || c == '_')
342        && !last.is_empty()
343        && !last.starts_with(|c: char| c.is_ascii_digit())
344        && !is_c_keyword(last)
345    {
346        Some(last.to_string())
347    } else {
348        None
349    }
350}
351
352fn is_c_keyword(s: &str) -> bool {
353    matches!(
354        s,
355        "const"
356            | "volatile"
357            | "restrict"
358            | "unsigned"
359            | "signed"
360            | "short"
361            | "long"
362            | "int"
363            | "char"
364            | "float"
365            | "double"
366            | "void"
367            | "struct"
368            | "union"
369            | "enum"
370            | "typedef"
371            | "extern"
372            | "static"
373            | "inline"
374            | "auto"
375            | "register"
376            | "bool"
377            | "_Bool"
378            | "uint8_t"
379            | "uint16_t"
380            | "uint32_t"
381            | "uint64_t"
382            | "int8_t"
383            | "int16_t"
384            | "int32_t"
385            | "int64_t"
386            | "size_t"
387            | "ssize_t"
388            | "ptrdiff_t"
389            | "uintptr_t"
390            | "intptr_t"
391    )
392}
393
394/// Split a Go struct body (text between `{` and `}`, exclusive) into
395/// field chunks, one per non-blank non-comment line.
396pub fn extract_go_field_chunks(body: &str) -> Vec<(String, String)> {
397    let mut result: Vec<(String, String)> = Vec::new();
398    for line in body.lines() {
399        let s = line.trim();
400        if s.is_empty() || s.starts_with("//") {
401            continue;
402        }
403        if let Some(name) = go_field_name_from_line(s) {
404            result.push((name, format!("{line}\n")));
405        }
406    }
407    result
408}
409
410fn go_field_name_from_line(line: &str) -> Option<String> {
411    // field_name[, field_name] Type [// comment]
412    // OR: EmbeddedType [// comment]
413    let code = if let Some(pos) = line.find("//") {
414        line[..pos].trim()
415    } else {
416        line.trim()
417    };
418    let first = code.split_whitespace().next()?;
419    let name = first.trim_end_matches(',');
420    if name
421        .chars()
422        .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
423        && !name.is_empty()
424    {
425        // Use unqualified name for qualified embedded types (e.g. sync.Mutex → Mutex)
426        let simple = name.split('.').next_back().unwrap_or(name);
427        Some(simple.to_string())
428    } else {
429        None
430    }
431}
432
433/// Split a Zig struct body (text between `{` and `}`, exclusive) into
434/// field chunks separated by `,` at depth 0.
435pub fn extract_zig_field_chunks(body: &str) -> Vec<(String, String)> {
436    // Zig field declarations end with `,` — same tokenisation as Rust
437    let mut result: Vec<(String, String)> = Vec::new();
438    let mut depth: i32 = 0;
439    let mut chunk_start = 0usize;
440    let bytes = body.as_bytes();
441    let mut i = 0usize;
442
443    while i < bytes.len() {
444        match bytes[i] {
445            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
446                while i < bytes.len() && bytes[i] != b'\n' {
447                    i += 1;
448                }
449            }
450            b'"' => {
451                i += 1;
452                while i < bytes.len() {
453                    if bytes[i] == b'\\' {
454                        i += 2;
455                        continue;
456                    }
457                    if bytes[i] == b'"' {
458                        i += 1;
459                        break;
460                    }
461                    i += 1;
462                }
463            }
464            b'<' | b'(' | b'[' => {
465                depth += 1;
466                i += 1;
467            }
468            b'>' | b')' | b']' => {
469                depth = (depth - 1).max(0);
470                i += 1;
471            }
472            b'{' | b'}' => {
473                i += 1;
474            }
475            b',' if depth == 0 => {
476                i += 1;
477                let chunk = &body[chunk_start..i];
478                if let Some(name) = zig_field_name_from_chunk(chunk) {
479                    result.push((name, chunk.to_string()));
480                }
481                chunk_start = i;
482            }
483            _ => {
484                i += 1;
485            }
486        }
487    }
488    let tail = body[chunk_start..].trim();
489    if !tail.is_empty() {
490        let chunk = &body[chunk_start..];
491        if let Some(name) = zig_field_name_from_chunk(chunk) {
492            result.push((name, chunk.to_string()));
493        }
494    }
495    result
496}
497
498fn zig_field_name_from_chunk(chunk: &str) -> Option<String> {
499    for line in chunk.lines() {
500        let s = line.trim();
501        if s.is_empty() || s.starts_with("//") {
502            continue;
503        }
504        // field_name: Type
505        let colon = s.find(':')?;
506        let name = s[..colon].trim().to_string();
507        if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
508            return Some(name);
509        }
510        return None;
511    }
512    None
513}
514
515// ── source-aware fix generators ───────────────────────────────────────────────
516//
517// These functions generate reordered struct source by extracting the original
518// field chunks and reordering them rather than synthesising from IR type names.
519// They fall back to the IR-based generators when chunk extraction fails.
520
521/// Generate a source-preserving Rust fix: reorder field chunks extracted from
522/// `struct_source` (the original `struct Name { ... }` text) according to the
523/// optimal field order.
524///
525/// Preserves `#[serde(...)]`, `pub`, `pub(crate)`, doc comments (`///`), and
526/// any other leading attribute/comment lines verbatim.
527pub fn generate_rust_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
528    if let Some(result) = try_source_aware_rust(layout, struct_source) {
529        return result;
530    }
531    generate_rust_fix(layout)
532}
533
534fn try_source_aware_rust(layout: &StructLayout, struct_source: &str) -> Option<String> {
535    // Detect tuple struct: `struct Name(...)` — body delimited by parens not braces.
536    let is_tuple = layout
537        .fields
538        .iter()
539        .all(|f| f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit()));
540
541    if is_tuple {
542        return try_source_aware_rust_tuple(layout, struct_source);
543    }
544
545    let brace_open = struct_source.find('{')?;
546    // Find the matching close brace using match_braces
547    let body_with_close = &struct_source[brace_open..];
548    let body_len = match_braces(body_with_close)?;
549    let body = &body_with_close[1..body_len - 1]; // between { and }
550
551    let chunks = extract_rust_field_chunks(body);
552    if chunks.is_empty() {
553        return None;
554    }
555
556    let chunk_map: std::collections::HashMap<&str, &str> = chunks
557        .iter()
558        .map(|(n, c)| (n.as_str(), c.as_str()))
559        .collect();
560
561    let optimal = optimal_order(layout);
562    // Verify all optimal fields have chunks; if any is missing, fall back
563    if optimal
564        .iter()
565        .any(|f| !chunk_map.contains_key(f.name.as_str()))
566    {
567        return None;
568    }
569
570    let header = &struct_source[..=brace_open];
571    let mut result = header.to_string();
572    result.push('\n');
573    for field in &optimal {
574        result.push_str(chunk_map[field.name.as_str()]);
575    }
576    // Ensure there's a newline before the closing brace
577    if !result.ends_with('\n') {
578        result.push('\n');
579    }
580    result.push('}');
581    // Preserve anything after the closing brace (e.g. impl blocks on next lines)
582    let after = &struct_source[brace_open + body_len..];
583    result.push_str(after);
584    Some(result)
585}
586
587/// Source-aware fix for tuple structs: `struct Name(T0, T1, ...);`
588/// Field names are `_0`, `_1`, … matching the IR names.
589fn try_source_aware_rust_tuple(layout: &StructLayout, struct_source: &str) -> Option<String> {
590    let paren_open = struct_source.find('(')?;
591    let body_with_close = &struct_source[paren_open..];
592    // Find matching closing paren
593    let paren_len = match_parens(body_with_close)?;
594    let body = &body_with_close[1..paren_len - 1]; // between ( and )
595
596    // Split by `,` at depth 0 to get individual type chunks (in order)
597    let type_chunks = extract_tuple_type_chunks(body);
598    if type_chunks.is_empty() {
599        return None;
600    }
601
602    // The original chunks are in declaration order: chunk[0] → `_0`, etc.
603    // Build a map from index-name to the type chunk text.
604    let chunk_map: std::collections::HashMap<String, &str> = type_chunks
605        .iter()
606        .enumerate()
607        .map(|(i, c)| (format!("_{i}"), c.as_str()))
608        .collect();
609
610    let optimal = optimal_order(layout);
611    if optimal.iter().any(|f| !chunk_map.contains_key(&f.name)) {
612        return None;
613    }
614
615    // Reconstruct: preserve header up to and including `(`
616    let header = &struct_source[..=paren_open];
617    let mut result = header.to_string();
618    let reordered: Vec<&str> = optimal.iter().map(|f| chunk_map[&f.name]).collect();
619    result.push_str(&reordered.join(", "));
620    result.push(')');
621    // Preserve trailing `;` and anything after
622    let after = &struct_source[paren_open + paren_len..];
623    result.push_str(after);
624    Some(result)
625}
626
627/// Split a tuple struct body (text between `(` and `)`) by `,` at depth 0.
628fn extract_tuple_type_chunks(body: &str) -> Vec<String> {
629    let mut result = Vec::new();
630    let mut depth: i32 = 0;
631    let mut chunk_start = 0usize;
632    let bytes = body.as_bytes();
633    let mut i = 0usize;
634
635    while i < bytes.len() {
636        match bytes[i] {
637            b'<' | b'[' => {
638                depth += 1;
639                i += 1;
640            }
641            b'>' | b']' => {
642                depth = (depth - 1).max(0);
643                i += 1;
644            }
645            b'(' => {
646                depth += 1;
647                i += 1;
648            }
649            b')' => {
650                depth = (depth - 1).max(0);
651                i += 1;
652            }
653            b',' if depth == 0 => {
654                let chunk = body[chunk_start..i].trim().to_string();
655                if !chunk.is_empty() {
656                    result.push(chunk);
657                }
658                i += 1;
659                chunk_start = i;
660            }
661            _ => {
662                i += 1;
663            }
664        }
665    }
666    let tail = body[chunk_start..].trim().to_string();
667    if !tail.is_empty() {
668        result.push(tail);
669    }
670    result
671}
672
673/// Find the matching `)` from the start of `s` (which must begin with `(`).
674/// Returns byte index one past the closing `)`.
675fn match_parens(s: &str) -> Option<usize> {
676    let mut depth = 0usize;
677    for (i, c) in s.char_indices() {
678        match c {
679            '(' => depth += 1,
680            ')' => {
681                depth -= 1;
682                if depth == 0 {
683                    return Some(i + 1);
684                }
685            }
686            _ => {}
687        }
688    }
689    None
690}
691
692/// Generate a source-preserving C/C++ fix.
693pub fn generate_c_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
694    if let Some(result) = try_source_aware_c(layout, struct_source) {
695        return result;
696    }
697    generate_c_fix(layout)
698}
699
700fn try_source_aware_c(layout: &StructLayout, struct_source: &str) -> Option<String> {
701    let brace_open = struct_source.find('{')?;
702    let body_with_close = &struct_source[brace_open..];
703    let body_len = match_braces(body_with_close)?;
704    let body = &body_with_close[1..body_len - 1];
705
706    let chunks = extract_c_field_chunks(body);
707    if chunks.is_empty() {
708        return None;
709    }
710
711    let chunk_map: std::collections::HashMap<&str, &str> = chunks
712        .iter()
713        .map(|(n, c)| (n.as_str(), c.as_str()))
714        .collect();
715
716    let optimal = optimal_order(layout);
717    if optimal
718        .iter()
719        .any(|f| !chunk_map.contains_key(f.name.as_str()))
720    {
721        return None;
722    }
723
724    let header = &struct_source[..=brace_open];
725    let mut result = header.to_string();
726    result.push('\n');
727    for field in &optimal {
728        result.push_str(chunk_map[field.name.as_str()]);
729    }
730    if !result.ends_with('\n') {
731        result.push('\n');
732    }
733    result.push('}');
734    let close_end = brace_open + body_len;
735    let after = &struct_source[close_end..];
736    result.push_str(after);
737    Some(result)
738}
739
740/// Generate a source-preserving Go fix.
741pub fn generate_go_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
742    if let Some(result) = try_source_aware_go(layout, struct_source) {
743        return result;
744    }
745    generate_go_fix(layout)
746}
747
748fn try_source_aware_go(layout: &StructLayout, struct_source: &str) -> Option<String> {
749    let brace_open = struct_source.find('{')?;
750    let body_with_close = &struct_source[brace_open..];
751    let body_len = match_braces(body_with_close)?;
752    let body = &body_with_close[1..body_len - 1];
753
754    let chunks = extract_go_field_chunks(body);
755    if chunks.is_empty() {
756        return None;
757    }
758
759    let chunk_map: std::collections::HashMap<&str, &str> = chunks
760        .iter()
761        .map(|(n, c)| (n.as_str(), c.as_str()))
762        .collect();
763
764    let optimal = optimal_order(layout);
765    if optimal
766        .iter()
767        .any(|f| !chunk_map.contains_key(f.name.as_str()))
768    {
769        return None;
770    }
771
772    let header = &struct_source[..=brace_open];
773    let mut result = header.to_string();
774    result.push('\n');
775    for field in &optimal {
776        result.push_str(chunk_map[field.name.as_str()]);
777    }
778    if !result.ends_with('\n') {
779        result.push('\n');
780    }
781    result.push('}');
782    let close_end = brace_open + body_len;
783    let after = &struct_source[close_end..];
784    result.push_str(after);
785    Some(result)
786}
787
788/// Generate a source-preserving Zig fix.
789pub fn generate_zig_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
790    if let Some(result) = try_source_aware_zig(layout, struct_source) {
791        return result;
792    }
793    generate_zig_fix(layout)
794}
795
796fn try_source_aware_zig(layout: &StructLayout, struct_source: &str) -> Option<String> {
797    let brace_open = struct_source.find('{')?;
798    let body_with_close = &struct_source[brace_open..];
799    let body_len = match_braces(body_with_close)?;
800    let body = &body_with_close[1..body_len - 1];
801
802    let chunks = extract_zig_field_chunks(body);
803    if chunks.is_empty() {
804        return None;
805    }
806
807    let chunk_map: std::collections::HashMap<&str, &str> = chunks
808        .iter()
809        .map(|(n, c)| (n.as_str(), c.as_str()))
810        .collect();
811
812    let optimal = optimal_order(layout);
813    if optimal
814        .iter()
815        .any(|f| !chunk_map.contains_key(f.name.as_str()))
816    {
817        return None;
818    }
819
820    let header = &struct_source[..=brace_open];
821    let mut result = header.to_string();
822    result.push('\n');
823    for field in &optimal {
824        result.push_str(chunk_map[field.name.as_str()]);
825    }
826    if !result.ends_with('\n') {
827        result.push('\n');
828    }
829    result.push('}');
830    let close_end = brace_open + body_len;
831    let after = &struct_source[close_end..];
832    result.push_str(after);
833    Some(result)
834}
835
836// ── span finders ──────────────────────────────────────────────────────────────
837
838/// Count matching braces from the start of `s` (which must begin with `{`).
839/// Returns the byte index one past the matching `}`.
840fn match_braces(s: &str) -> Option<usize> {
841    let mut depth = 0usize;
842    for (i, c) in s.char_indices() {
843        match c {
844            '{' => depth += 1,
845            '}' => {
846                depth -= 1;
847                if depth == 0 {
848                    return Some(i + 1);
849                }
850            }
851            _ => {}
852        }
853    }
854    None
855}
856
857/// Consume an optional trailing semicolon (after optional whitespace) at `pos`.
858fn consume_semicolon(source: &str, pos: usize) -> usize {
859    let rest = &source[pos..];
860    let ws = rest.len()
861        - rest
862            .trim_start_matches(|c: char| c.is_whitespace() && c != '\n')
863            .len();
864    let after_ws = &rest[ws..];
865    if after_ws.starts_with(';') {
866        pos + ws + 1
867    } else {
868        pos
869    }
870}
871
872/// Find the byte range of a named struct/union in C/C++ source.
873/// The range covers from `struct/union Name` through the closing `};`.
874pub fn find_c_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
875    for kw in &["struct", "union"] {
876        let needle = format!("{kw} {struct_name}");
877        let mut search_from = 0usize;
878        while let Some(rel) = source[search_from..].find(&needle) {
879            let start = search_from + rel;
880            let after_name = start + needle.len();
881            // Ensure the character after the name is a boundary (space, `{`, newline)
882            let boundary = source[after_name..].chars().next();
883            if matches!(
884                boundary,
885                Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
886            ) {
887                // Find the opening brace (may have whitespace between name and `{`)
888                if let Some(brace_rel) = source[after_name..].find('{') {
889                    let brace_start = after_name + brace_rel;
890                    // Verify no word characters between name end and brace
891                    if source[after_name..brace_start]
892                        .chars()
893                        .all(|c| c.is_whitespace())
894                        && let Some(body_len) = match_braces(&source[brace_start..])
895                    {
896                        let end = consume_semicolon(source, brace_start + body_len);
897                        return Some(start..end);
898                    }
899                }
900            }
901            search_from = start + 1;
902        }
903    }
904    None
905}
906
907/// Find the byte range of a named struct in Rust source (`struct Name { ... }`).
908pub fn find_rust_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
909    let needle = format!("struct {struct_name}");
910    let mut search_from = 0usize;
911    while let Some(rel) = source[search_from..].find(&needle) {
912        let start = search_from + rel;
913        let after_name = start + needle.len();
914        let boundary = source[after_name..].chars().next();
915        if matches!(
916            boundary,
917            Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
918        ) && let Some(brace_rel) = source[after_name..].find('{')
919        {
920            let brace_start = after_name + brace_rel;
921            if source[after_name..brace_start]
922                .chars()
923                .all(|c| c.is_whitespace())
924                && let Some(body_len) = match_braces(&source[brace_start..])
925            {
926                // Rust structs have no trailing `;` (unit structs do, but we skip those)
927                return Some(start..brace_start + body_len);
928            }
929        }
930        search_from = start + 1;
931    }
932    None
933}
934
935/// Find the byte range of a named struct in Go source (`type Name struct { ... }`).
936pub fn find_go_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
937    let needle = format!("type {struct_name} struct");
938    let mut search_from = 0usize;
939    while let Some(rel) = source[search_from..].find(&needle) {
940        let start = search_from + rel;
941        let after_kw = start + needle.len();
942        if let Some(brace_rel) = source[after_kw..].find('{') {
943            let brace_start = after_kw + brace_rel;
944            if source[after_kw..brace_start]
945                .chars()
946                .all(|c| c.is_whitespace())
947                && let Some(body_len) = match_braces(&source[brace_start..])
948            {
949                return Some(start..brace_start + body_len);
950            }
951        }
952        search_from = start + 1;
953    }
954    None
955}
956
957// ── in-place rewriters ────────────────────────────────────────────────────────
958
959/// Apply C/C++ struct reorderings in-place, returning the modified source.
960/// Each layout in `layouts` is looked up by name; matched structs are replaced
961/// with the optimally-ordered definition. Field declarations (including comments
962/// and annotations such as `GUARDED_BY`) are preserved verbatim from the original
963/// source when possible; IR-based generation is used as a fallback.
964/// Replacements are applied back-to-front so byte offsets remain valid.
965pub fn apply_fixes_c(source: &str, layouts: &[&StructLayout]) -> String {
966    apply_fixes_with_source(
967        source,
968        layouts,
969        find_c_struct_span,
970        generate_c_fix_from_source,
971    )
972}
973
974/// Apply Rust struct reorderings in-place, returning the modified source.
975/// Preserves `pub`, `pub(crate)`, `#[serde(...)]`, `/// doc-comments`, and other
976/// attributes verbatim; falls back to IR-based generation when source cannot be parsed.
977pub fn apply_fixes_rust(source: &str, layouts: &[&StructLayout]) -> String {
978    apply_fixes_with_source(
979        source,
980        layouts,
981        find_rust_struct_span,
982        generate_rust_fix_from_source,
983    )
984}
985
986/// Apply Go struct reorderings in-place, returning the modified source.
987/// Preserves field tags and comments verbatim; falls back to IR-based generation.
988pub fn apply_fixes_go(source: &str, layouts: &[&StructLayout]) -> String {
989    apply_fixes_with_source(
990        source,
991        layouts,
992        find_go_struct_span,
993        generate_go_fix_from_source,
994    )
995}
996
997/// Render a reordered Zig struct definition as source text.
998/// Zig structs are declared as `const Name = struct { ... };`.
999/// If the layout is packed, the output uses `packed struct`.
1000pub fn generate_zig_fix(layout: &StructLayout) -> String {
1001    let optimal = optimal_order(layout);
1002    let qualifier = if layout.is_packed { "packed " } else { "" };
1003    let mut out = format!("const {} = {}struct {{\n", layout.name, qualifier);
1004    for field in &optimal {
1005        let ty = field_type_name(field);
1006        out.push_str(&format!("    {}: {ty},\n", field.name));
1007    }
1008    out.push_str("};\n");
1009    out
1010}
1011
1012/// Find the byte range of a named Zig struct in source.
1013/// Matches `const Name = [packed|extern ]struct { ... };`.
1014pub fn find_zig_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
1015    // Match `const Name =` (with optional whitespace variations)
1016    let needle = format!("const {struct_name}");
1017    let mut search_from = 0usize;
1018    while let Some(rel) = source[search_from..].find(&needle) {
1019        let start = search_from + rel;
1020        let after_name = start + needle.len();
1021        // Must be followed by whitespace then `=`
1022        let rest = source[after_name..].trim_start();
1023        if !rest.starts_with('=') {
1024            search_from = start + 1;
1025            continue;
1026        }
1027        // Find `struct` keyword after `=`
1028        let after_eq = after_name + source[after_name..].find('=')? + 1;
1029        let after_eq_rest = &source[after_eq..];
1030        // Skip optional `packed` or `extern` modifiers
1031        if let Some(struct_rel) = after_eq_rest.find("struct") {
1032            // Check no non-whitespace/identifier characters between = and struct
1033            // (beyond optional packed/extern modifiers)
1034            let prefix = &after_eq_rest[..struct_rel];
1035            let prefix_clean = prefix.trim();
1036            if prefix_clean.is_empty() || prefix_clean == "packed" || prefix_clean == "extern" {
1037                let struct_kw_end = after_eq + struct_rel + "struct".len();
1038                if let Some(brace_rel) = source[struct_kw_end..].find('{') {
1039                    let brace_start = struct_kw_end + brace_rel;
1040                    if source[struct_kw_end..brace_start]
1041                        .chars()
1042                        .all(|c| c.is_whitespace())
1043                        && let Some(body_len) = match_braces(&source[brace_start..])
1044                    {
1045                        let end = consume_semicolon(source, brace_start + body_len);
1046                        return Some(start..end);
1047                    }
1048                }
1049            }
1050        }
1051        search_from = start + 1;
1052    }
1053    None
1054}
1055
1056/// Apply Zig struct reorderings in-place, returning the modified source.
1057/// Preserves field comments and annotations verbatim; falls back to IR-based generation.
1058pub fn apply_fixes_zig(source: &str, layouts: &[&StructLayout]) -> String {
1059    apply_fixes_with_source(
1060        source,
1061        layouts,
1062        find_zig_struct_span,
1063        generate_zig_fix_from_source,
1064    )
1065}
1066
1067/// Source-aware variant of `apply_fixes`: passes the original struct source text
1068/// (extracted from the span) to the generator, enabling verbatim field preservation.
1069fn apply_fixes_with_source(
1070    source: &str,
1071    layouts: &[&StructLayout],
1072    find_span: fn(&str, &str) -> Option<std::ops::Range<usize>>,
1073    generate: fn(&StructLayout, &str) -> String,
1074) -> String {
1075    // Collect (start, end, replacement) for each matching layout
1076    let mut replacements: Vec<(usize, usize, String)> = layouts
1077        .iter()
1078        .filter_map(|layout| {
1079            let span = find_span(source, &layout.name)?;
1080            let struct_source = &source[span.clone()];
1081            let fixed = generate(layout, struct_source);
1082            Some((span.start, span.end, fixed))
1083        })
1084        .collect();
1085
1086    // Sort by start offset ascending, then apply in reverse so offsets stay valid
1087    replacements.sort_by_key(|(start, _, _)| *start);
1088
1089    let mut result = source.to_string();
1090    for (start, end, fixed) in replacements.into_iter().rev() {
1091        result.replace_range(start..end, &fixed);
1092    }
1093    result
1094}
1095
1096fn field_type_name(field: &padlock_core::ir::Field) -> &str {
1097    match &field.ty {
1098        padlock_core::ir::TypeInfo::Primitive { name, .. }
1099        | padlock_core::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
1100        padlock_core::ir::TypeInfo::Pointer { .. } => "void*",
1101        padlock_core::ir::TypeInfo::Array { .. } => "/* array */",
1102        padlock_core::ir::TypeInfo::Struct(l) => l.name.as_str(),
1103    }
1104}
1105
1106// ── tests ─────────────────────────────────────────────────────────────────────
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111    use padlock_core::ir::test_fixtures::connection_layout;
1112
1113    #[test]
1114    fn c_fix_starts_with_struct() {
1115        let out = generate_c_fix(&connection_layout());
1116        assert!(out.starts_with("struct Connection {"));
1117    }
1118
1119    #[test]
1120    fn c_fix_contains_all_fields() {
1121        let out = generate_c_fix(&connection_layout());
1122        assert!(out.contains("timeout"));
1123        assert!(out.contains("port"));
1124        assert!(out.contains("is_active"));
1125        assert!(out.contains("is_tls"));
1126    }
1127
1128    #[test]
1129    fn c_fix_puts_largest_align_first() {
1130        let out = generate_c_fix(&connection_layout());
1131        let timeout_pos = out.find("timeout").unwrap();
1132        let is_active_pos = out.find("is_active").unwrap();
1133        assert!(timeout_pos < is_active_pos);
1134    }
1135
1136    #[test]
1137    fn rust_fix_uses_colon_syntax() {
1138        let out = generate_rust_fix(&connection_layout());
1139        assert!(out.contains(": f64"));
1140    }
1141
1142    #[test]
1143    fn unified_diff_marks_changes() {
1144        let orig = "struct T { char a; double b; };\n";
1145        let fixed = "struct T { double b; char a; };\n";
1146        let diff = unified_diff(orig, fixed, 1);
1147        assert!(diff.contains('-') || diff.contains('+'));
1148    }
1149
1150    #[test]
1151    fn unified_diff_identical_is_no_changes() {
1152        assert_eq!(unified_diff("x\n", "x\n", 3), "(no changes)\n");
1153    }
1154
1155    // ── span finders ──────────────────────────────────────────────────────────
1156
1157    #[test]
1158    fn find_c_struct_span_basic() {
1159        let src = "struct Foo { int x; char y; };\nstruct Bar { double z; };\n";
1160        let span = find_c_struct_span(src, "Foo").unwrap();
1161        let text = &src[span];
1162        assert!(text.starts_with("struct Foo"));
1163        assert!(!text.contains("Bar"));
1164    }
1165
1166    #[test]
1167    fn find_c_struct_span_missing_returns_none() {
1168        let src = "struct Other { int x; };";
1169        assert!(find_c_struct_span(src, "Missing").is_none());
1170    }
1171
1172    #[test]
1173    fn find_rust_struct_span_basic() {
1174        let src = "struct Foo {\n    x: u32,\n    y: u8,\n}\n";
1175        let span = find_rust_struct_span(src, "Foo").unwrap();
1176        assert!(src[span].starts_with("struct Foo"));
1177    }
1178
1179    #[test]
1180    fn find_go_struct_span_basic() {
1181        let src = "type Foo struct {\n\tX int32\n\tY bool\n}\n";
1182        let span = find_go_struct_span(src, "Foo").unwrap();
1183        assert!(src[span].starts_with("type Foo struct"));
1184    }
1185
1186    // ── apply_fixes ───────────────────────────────────────────────────────────
1187
1188    #[test]
1189    fn apply_fixes_c_reorders_in_place() {
1190        // Connection has char/double/char/int — after fix, double should come first
1191        let src = "struct Connection { bool is_active; double timeout; bool is_tls; int port; };\n";
1192        let layout = connection_layout();
1193        let fixed = apply_fixes_c(src, &[&layout]);
1194        let timeout_pos = fixed.find("timeout").unwrap();
1195        let is_active_pos = fixed.find("is_active").unwrap();
1196        assert!(
1197            timeout_pos < is_active_pos,
1198            "double should appear before bool after reorder"
1199        );
1200    }
1201
1202    #[test]
1203    fn apply_fixes_rust_reorders_in_place() {
1204        let src = "struct Connection {\n    is_active: bool,\n    timeout: f64,\n    is_tls: bool,\n    port: i32,\n}\n";
1205        let layout = connection_layout();
1206        let fixed = apply_fixes_rust(src, &[&layout]);
1207        let timeout_pos = fixed.find("timeout").unwrap();
1208        let is_active_pos = fixed.find("is_active").unwrap();
1209        assert!(timeout_pos < is_active_pos);
1210    }
1211
1212    #[test]
1213    fn go_fix_uses_tab_syntax() {
1214        let layout = connection_layout();
1215        let out = generate_go_fix(&layout);
1216        assert!(out.starts_with("type Connection struct"));
1217        assert!(out.contains('\t'));
1218    }
1219
1220    #[test]
1221    fn zig_fix_uses_const_struct_syntax() {
1222        let out = generate_zig_fix(&connection_layout());
1223        assert!(out.starts_with("const Connection = struct {"));
1224        assert!(out.ends_with("};\n"));
1225    }
1226
1227    #[test]
1228    fn find_zig_struct_span_basic() {
1229        let src = "const S = struct {\n    x: u32,\n    y: u8,\n};\n";
1230        let span = find_zig_struct_span(src, "S").unwrap();
1231        assert!(src[span].starts_with("const S = struct"));
1232    }
1233
1234    #[test]
1235    fn find_zig_struct_span_packed() {
1236        let src = "const S = packed struct {\n    x: u32,\n    y: u8,\n};\n";
1237        let span = find_zig_struct_span(src, "S").unwrap();
1238        assert!(src[span].contains("packed struct"));
1239    }
1240
1241    #[test]
1242    fn find_zig_struct_span_missing_returns_none() {
1243        let src = "const Other = struct { x: u8 };\n";
1244        assert!(find_zig_struct_span(src, "Missing").is_none());
1245    }
1246
1247    #[test]
1248    fn apply_fixes_zig_reorders_in_place() {
1249        use crate::parse_source_str;
1250        use padlock_core::arch::X86_64_SYSV;
1251        let src = "const S = struct {\n    a: u8,\n    b: u64,\n};\n";
1252        let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1253        let layout = &layouts[0];
1254        let fixed = apply_fixes_zig(src, &[layout]);
1255        // b (u64, align 8) should come before a (u8)
1256        let b_pos = fixed.find("b:").unwrap();
1257        let a_pos = fixed.find("a:").unwrap();
1258        assert!(
1259            b_pos < a_pos,
1260            "u64 field should come before u8 after reorder"
1261        );
1262    }
1263
1264    // ── fix quality: source-aware preservation ────────────────────────────────
1265
1266    #[test]
1267    fn rust_fix_preserves_pub_visibility() {
1268        let src = "struct S {\n    pub a: u8,\n    pub b: u64,\n}\n";
1269        use crate::parse_source_str;
1270        use padlock_core::arch::X86_64_SYSV;
1271        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1272        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1273        // pub keyword must appear before both fields
1274        assert!(fixed.contains("pub b: u64"), "pub on b must be preserved");
1275        assert!(fixed.contains("pub a: u8"), "pub on a must be preserved");
1276        // b (u64, align 8) should appear before a (u8, align 1)
1277        assert!(fixed.find("pub b").unwrap() < fixed.find("pub a").unwrap());
1278    }
1279
1280    #[test]
1281    fn rust_fix_preserves_doc_comments() {
1282        let src = concat!(
1283            "struct S {\n",
1284            "    /// small field\n",
1285            "    a: u8,\n",
1286            "    /// large field\n",
1287            "    b: u64,\n",
1288            "}\n"
1289        );
1290        use crate::parse_source_str;
1291        use padlock_core::arch::X86_64_SYSV;
1292        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1293        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1294        assert!(
1295            fixed.contains("/// large field"),
1296            "doc comment for b must survive"
1297        );
1298        assert!(
1299            fixed.contains("/// small field"),
1300            "doc comment for a must survive"
1301        );
1302        // The doc comment for b must appear before the doc comment for a
1303        assert!(
1304            fixed.find("large field").unwrap() < fixed.find("small field").unwrap(),
1305            "doc comment ordering must follow field ordering"
1306        );
1307    }
1308
1309    #[test]
1310    fn rust_fix_preserves_serde_attributes() {
1311        let src = concat!(
1312            "struct S {\n",
1313            "    #[serde(skip)]\n",
1314            "    a: u8,\n",
1315            "    #[serde(rename = \"big\")]\n",
1316            "    b: u64,\n",
1317            "}\n"
1318        );
1319        use crate::parse_source_str;
1320        use padlock_core::arch::X86_64_SYSV;
1321        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1322        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1323        assert!(
1324            fixed.contains("#[serde(skip)]"),
1325            "serde attribute on a must survive"
1326        );
1327        assert!(
1328            fixed.contains("#[serde(rename = \"big\")]"),
1329            "serde attribute on b must survive"
1330        );
1331    }
1332
1333    #[test]
1334    fn rust_fix_preserves_pub_crate_visibility() {
1335        let src = "struct S {\n    pub(crate) a: u8,\n    pub(crate) b: u64,\n}\n";
1336        use crate::parse_source_str;
1337        use padlock_core::arch::X86_64_SYSV;
1338        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1339        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1340        assert!(
1341            fixed.contains("pub(crate) b: u64"),
1342            "pub(crate) on b must be preserved"
1343        );
1344        assert!(
1345            fixed.contains("pub(crate) a: u8"),
1346            "pub(crate) on a must be preserved"
1347        );
1348    }
1349
1350    #[test]
1351    fn c_fix_preserves_guarded_by_comments() {
1352        let src = concat!(
1353            "struct S {\n",
1354            "    char a; // GUARDED_BY(mu)\n",
1355            "    double b; // large field\n",
1356            "};\n"
1357        );
1358        use crate::parse_source_str;
1359        use padlock_core::arch::X86_64_SYSV;
1360        let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1361        let fixed = apply_fixes_c(src, &[&layouts[0]]);
1362        assert!(
1363            fixed.contains("GUARDED_BY(mu)"),
1364            "guard annotation comment must survive reorder"
1365        );
1366        // double should come before char
1367        assert!(fixed.find("double b").unwrap() < fixed.find("char a").unwrap());
1368    }
1369
1370    #[test]
1371    fn go_fix_preserves_field_tags() {
1372        let src = concat!("type S struct {\n", "\ta uint8\n", "\tb uint64\n", "}\n");
1373        use crate::parse_source_str;
1374        use padlock_core::arch::X86_64_SYSV;
1375        let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1376        let fixed = apply_fixes_go(src, &[&layouts[0]]);
1377        // b (8 bytes) should appear before a (1 byte)
1378        assert!(fixed.find("\tb uint64").unwrap() < fixed.find("\ta uint8").unwrap());
1379    }
1380
1381    #[test]
1382    fn zig_fix_preserves_field_comments() {
1383        let src = concat!(
1384            "const S = struct {\n",
1385            "    // small\n",
1386            "    a: u8,\n",
1387            "    // large\n",
1388            "    b: u64,\n",
1389            "};\n"
1390        );
1391        use crate::parse_source_str;
1392        use padlock_core::arch::X86_64_SYSV;
1393        let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1394        let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1395        assert!(fixed.contains("// large"), "comment for b must survive");
1396        assert!(fixed.contains("// small"), "comment for a must survive");
1397        // b should appear first
1398        assert!(fixed.find("// large").unwrap() < fixed.find("// small").unwrap());
1399    }
1400
1401    // ── bad weather: fix quality fallback ─────────────────────────────────────
1402
1403    #[test]
1404    fn rust_fix_from_source_falls_back_when_no_open_brace() {
1405        // Struct source that is malformed (no `{`): must not panic, falls back to IR
1406        let layout = connection_layout();
1407        let out = generate_rust_fix_from_source(&layout, "struct Connection");
1408        // IR fallback produces valid Rust syntax
1409        assert!(out.starts_with("struct Connection {"));
1410    }
1411
1412    #[test]
1413    fn c_fix_from_source_falls_back_when_chunks_empty() {
1414        // Body with no parseable fields — chunk extraction returns empty vec,
1415        // triggering IR fallback
1416        let layout = connection_layout();
1417        let out = generate_c_fix_from_source(&layout, "struct Connection { /* no fields */ };");
1418        assert!(out.starts_with("struct Connection {"));
1419        assert!(out.contains("timeout"));
1420    }
1421
1422    #[test]
1423    fn zig_fix_from_source_falls_back_on_missing_field_name() {
1424        // IR field names don't match chunk names → fallback to IR
1425        let layout = connection_layout();
1426        let out =
1427            generate_zig_fix_from_source(&layout, "const Connection = struct { x: u8, y: u64, };");
1428        // IR fallback must still produce all fields from the layout
1429        assert!(out.contains("timeout"));
1430    }
1431
1432    // ── Go fix tests ─────────────────────────────────────────────────────────
1433
1434    #[test]
1435    fn go_fix_reorders_fields() {
1436        let layout = connection_layout();
1437        let out = generate_go_fix(&layout);
1438        // timeout (align 8) must come before bools (align 1)
1439        let pos_timeout = out.find("timeout").unwrap();
1440        let pos_port = out.find("port").unwrap();
1441        let pos_bool = out.find("is_active").unwrap();
1442        assert!(pos_timeout < pos_bool, "timeout must precede booleans");
1443        assert!(pos_port < pos_bool, "port must precede booleans");
1444    }
1445
1446    #[test]
1447    fn go_fix_from_source_preserves_verbatim_field_lines() {
1448        let layout = connection_layout();
1449        let src = r#"type Connection struct {
1450	is_active bool
1451	timeout   f64
1452	is_tls    bool
1453	port      i32
1454}"#;
1455        let out = generate_go_fix_from_source(&layout, src);
1456        // The reordered output must contain the exact verbatim field lines
1457        assert!(out.contains("timeout   f64"), "verbatim timeout line");
1458        assert!(out.contains("port      i32"), "verbatim port line");
1459        // timeout must appear before is_active in the output
1460        let pos_timeout = out.find("timeout").unwrap();
1461        let pos_is_active = out.find("is_active").unwrap();
1462        assert!(
1463            pos_timeout < pos_is_active,
1464            "timeout must come before is_active"
1465        );
1466    }
1467
1468    #[test]
1469    fn apply_fixes_go_rewrites_struct_in_file() {
1470        let src = "package p\n\ntype Point struct {\n\tFlag bool\n\tX    int64\n\tY    int32\n}\n";
1471        // Build a minimal layout: Flag(bool,1), X(i64,8), Y(i32,4)
1472        // optimal: X(8) → Y(4) → Flag(1)
1473        use padlock_core::arch::X86_64_SYSV;
1474        use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
1475        let layout = StructLayout {
1476            name: "Point".into(),
1477            total_size: 16,
1478            align: 8,
1479            fields: vec![
1480                Field {
1481                    name: "Flag".into(),
1482                    ty: TypeInfo::Primitive {
1483                        name: "bool".into(),
1484                        size: 1,
1485                        align: 1,
1486                    },
1487                    offset: 0,
1488                    size: 1,
1489                    align: 1,
1490                    source_file: None,
1491                    source_line: None,
1492                    access: AccessPattern::Unknown,
1493                },
1494                Field {
1495                    name: "X".into(),
1496                    ty: TypeInfo::Primitive {
1497                        name: "int64".into(),
1498                        size: 8,
1499                        align: 8,
1500                    },
1501                    offset: 8,
1502                    size: 8,
1503                    align: 8,
1504                    source_file: None,
1505                    source_line: None,
1506                    access: AccessPattern::Unknown,
1507                },
1508                Field {
1509                    name: "Y".into(),
1510                    ty: TypeInfo::Primitive {
1511                        name: "int32".into(),
1512                        size: 4,
1513                        align: 4,
1514                    },
1515                    offset: 16,
1516                    size: 4,
1517                    align: 4,
1518                    source_file: None,
1519                    source_line: None,
1520                    access: AccessPattern::Unknown,
1521                },
1522            ],
1523            source_file: None,
1524            source_line: None,
1525            arch: &X86_64_SYSV,
1526            is_packed: false,
1527            is_union: false,
1528            is_repr_rust: false,
1529            suppressed_findings: vec![],
1530        };
1531        let fixed = apply_fixes_go(src, &[&layout]);
1532        // X (int64, align 8) must appear before Flag (bool, align 1)
1533        let pos_x = fixed.find("\tX ").unwrap();
1534        let pos_flag = fixed.find("\tFlag").unwrap();
1535        assert!(pos_x < pos_flag, "X must precede Flag after reorder");
1536        // Package declaration must be preserved
1537        assert!(fixed.starts_with("package p\n"), "package line preserved");
1538    }
1539}