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    if !body.starts_with('\n') {
573        result.push('\n');
574    }
575    for field in &optimal {
576        result.push_str(chunk_map[field.name.as_str()]);
577    }
578    // Ensure there's a newline before the closing brace
579    if !result.ends_with('\n') {
580        result.push('\n');
581    }
582    result.push('}');
583    // Preserve anything after the closing brace (e.g. impl blocks on next lines)
584    let after = &struct_source[brace_open + body_len..];
585    result.push_str(after);
586    Some(result)
587}
588
589/// Source-aware fix for tuple structs: `struct Name(T0, T1, ...);`
590/// Field names are `_0`, `_1`, … matching the IR names.
591fn try_source_aware_rust_tuple(layout: &StructLayout, struct_source: &str) -> Option<String> {
592    let paren_open = struct_source.find('(')?;
593    let body_with_close = &struct_source[paren_open..];
594    // Find matching closing paren
595    let paren_len = match_parens(body_with_close)?;
596    let body = &body_with_close[1..paren_len - 1]; // between ( and )
597
598    // Split by `,` at depth 0 to get individual type chunks (in order)
599    let type_chunks = extract_tuple_type_chunks(body);
600    if type_chunks.is_empty() {
601        return None;
602    }
603
604    // The original chunks are in declaration order: chunk[0] → `_0`, etc.
605    // Build a map from index-name to the type chunk text.
606    let chunk_map: std::collections::HashMap<String, &str> = type_chunks
607        .iter()
608        .enumerate()
609        .map(|(i, c)| (format!("_{i}"), c.as_str()))
610        .collect();
611
612    let optimal = optimal_order(layout);
613    if optimal.iter().any(|f| !chunk_map.contains_key(&f.name)) {
614        return None;
615    }
616
617    // Reconstruct: preserve header up to and including `(`
618    let header = &struct_source[..=paren_open];
619    let mut result = header.to_string();
620    let reordered: Vec<&str> = optimal.iter().map(|f| chunk_map[&f.name]).collect();
621    result.push_str(&reordered.join(", "));
622    result.push(')');
623    // Preserve trailing `;` and anything after
624    let after = &struct_source[paren_open + paren_len..];
625    result.push_str(after);
626    Some(result)
627}
628
629/// Split a tuple struct body (text between `(` and `)`) by `,` at depth 0.
630fn extract_tuple_type_chunks(body: &str) -> Vec<String> {
631    let mut result = Vec::new();
632    let mut depth: i32 = 0;
633    let mut chunk_start = 0usize;
634    let bytes = body.as_bytes();
635    let mut i = 0usize;
636
637    while i < bytes.len() {
638        match bytes[i] {
639            b'<' | b'[' => {
640                depth += 1;
641                i += 1;
642            }
643            b'>' | b']' => {
644                depth = (depth - 1).max(0);
645                i += 1;
646            }
647            b'(' => {
648                depth += 1;
649                i += 1;
650            }
651            b')' => {
652                depth = (depth - 1).max(0);
653                i += 1;
654            }
655            b',' if depth == 0 => {
656                let chunk = body[chunk_start..i].trim().to_string();
657                if !chunk.is_empty() {
658                    result.push(chunk);
659                }
660                i += 1;
661                chunk_start = i;
662            }
663            _ => {
664                i += 1;
665            }
666        }
667    }
668    let tail = body[chunk_start..].trim().to_string();
669    if !tail.is_empty() {
670        result.push(tail);
671    }
672    result
673}
674
675/// Find the matching `)` from the start of `s` (which must begin with `(`).
676/// Returns byte index one past the closing `)`.
677fn match_parens(s: &str) -> Option<usize> {
678    let mut depth = 0usize;
679    for (i, c) in s.char_indices() {
680        match c {
681            '(' => depth += 1,
682            ')' => {
683                depth -= 1;
684                if depth == 0 {
685                    return Some(i + 1);
686                }
687            }
688            _ => {}
689        }
690    }
691    None
692}
693
694/// Generate a source-preserving C/C++ fix.
695pub fn generate_c_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
696    if let Some(result) = try_source_aware_c(layout, struct_source) {
697        return result;
698    }
699    generate_c_fix(layout)
700}
701
702fn try_source_aware_c(layout: &StructLayout, struct_source: &str) -> Option<String> {
703    let brace_open = struct_source.find('{')?;
704    let body_with_close = &struct_source[brace_open..];
705    let body_len = match_braces(body_with_close)?;
706    let body = &body_with_close[1..body_len - 1];
707
708    let chunks = extract_c_field_chunks(body);
709    if chunks.is_empty() {
710        return None;
711    }
712
713    let chunk_map: std::collections::HashMap<&str, &str> = chunks
714        .iter()
715        .map(|(n, c)| (n.as_str(), c.as_str()))
716        .collect();
717
718    let optimal = optimal_order(layout);
719    if optimal
720        .iter()
721        .any(|f| !chunk_map.contains_key(f.name.as_str()))
722    {
723        return None;
724    }
725
726    let header = &struct_source[..=brace_open];
727    let mut result = header.to_string();
728    if !body.starts_with('\n') {
729        result.push('\n');
730    }
731    for field in &optimal {
732        result.push_str(chunk_map[field.name.as_str()]);
733    }
734    if !result.ends_with('\n') {
735        result.push('\n');
736    }
737    result.push('}');
738    let close_end = brace_open + body_len;
739    let after = &struct_source[close_end..];
740    result.push_str(after);
741    Some(result)
742}
743
744/// Generate a source-preserving Go fix.
745pub fn generate_go_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
746    if let Some(result) = try_source_aware_go(layout, struct_source) {
747        return result;
748    }
749    generate_go_fix(layout)
750}
751
752fn try_source_aware_go(layout: &StructLayout, struct_source: &str) -> Option<String> {
753    let brace_open = struct_source.find('{')?;
754    let body_with_close = &struct_source[brace_open..];
755    let body_len = match_braces(body_with_close)?;
756    let body = &body_with_close[1..body_len - 1];
757
758    let chunks = extract_go_field_chunks(body);
759    if chunks.is_empty() {
760        return None;
761    }
762
763    let chunk_map: std::collections::HashMap<&str, &str> = chunks
764        .iter()
765        .map(|(n, c)| (n.as_str(), c.as_str()))
766        .collect();
767
768    let optimal = optimal_order(layout);
769    if optimal
770        .iter()
771        .any(|f| !chunk_map.contains_key(f.name.as_str()))
772    {
773        return None;
774    }
775
776    let header = &struct_source[..=brace_open];
777    let mut result = header.to_string();
778    if !body.starts_with('\n') {
779        result.push('\n');
780    }
781    for field in &optimal {
782        result.push_str(chunk_map[field.name.as_str()]);
783    }
784    if !result.ends_with('\n') {
785        result.push('\n');
786    }
787    result.push('}');
788    let close_end = brace_open + body_len;
789    let after = &struct_source[close_end..];
790    result.push_str(after);
791    Some(result)
792}
793
794/// Generate a source-preserving Zig fix.
795pub fn generate_zig_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
796    if let Some(result) = try_source_aware_zig(layout, struct_source) {
797        return result;
798    }
799    generate_zig_fix(layout)
800}
801
802fn try_source_aware_zig(layout: &StructLayout, struct_source: &str) -> Option<String> {
803    let brace_open = struct_source.find('{')?;
804    let body_with_close = &struct_source[brace_open..];
805    let body_len = match_braces(body_with_close)?;
806    let body = &body_with_close[1..body_len - 1];
807
808    let chunks = extract_zig_field_chunks(body);
809    if chunks.is_empty() {
810        return None;
811    }
812
813    let chunk_map: std::collections::HashMap<&str, &str> = chunks
814        .iter()
815        .map(|(n, c)| (n.as_str(), c.as_str()))
816        .collect();
817
818    let optimal = optimal_order(layout);
819    if optimal
820        .iter()
821        .any(|f| !chunk_map.contains_key(f.name.as_str()))
822    {
823        return None;
824    }
825
826    let header = &struct_source[..=brace_open];
827    let mut result = header.to_string();
828    if !body.starts_with('\n') {
829        result.push('\n');
830    }
831    for field in &optimal {
832        result.push_str(chunk_map[field.name.as_str()]);
833    }
834    if !result.ends_with('\n') {
835        result.push('\n');
836    }
837    result.push('}');
838    let close_end = brace_open + body_len;
839    let after = &struct_source[close_end..];
840    result.push_str(after);
841    Some(result)
842}
843
844// ── span finders ──────────────────────────────────────────────────────────────
845
846/// Count matching braces from the start of `s` (which must begin with `{`).
847/// Returns the byte index one past the matching `}`.
848fn match_braces(s: &str) -> Option<usize> {
849    let mut depth = 0usize;
850    for (i, c) in s.char_indices() {
851        match c {
852            '{' => depth += 1,
853            '}' => {
854                depth -= 1;
855                if depth == 0 {
856                    return Some(i + 1);
857                }
858            }
859            _ => {}
860        }
861    }
862    None
863}
864
865/// Consume an optional trailing semicolon (after optional whitespace) at `pos`.
866fn consume_semicolon(source: &str, pos: usize) -> usize {
867    let rest = &source[pos..];
868    let ws = rest.len()
869        - rest
870            .trim_start_matches(|c: char| c.is_whitespace() && c != '\n')
871            .len();
872    let after_ws = &rest[ws..];
873    if after_ws.starts_with(';') {
874        pos + ws + 1
875    } else {
876        pos
877    }
878}
879
880/// Find the byte range of a named struct/union in C/C++ source.
881/// The range covers from `struct/union Name` through the closing `};`.
882pub fn find_c_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
883    for kw in &["struct", "union"] {
884        let needle = format!("{kw} {struct_name}");
885        let mut search_from = 0usize;
886        while let Some(rel) = source[search_from..].find(&needle) {
887            let start = search_from + rel;
888            let after_name = start + needle.len();
889            // Ensure the character after the name is a boundary (space, `{`, newline)
890            let boundary = source[after_name..].chars().next();
891            if matches!(
892                boundary,
893                Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
894            ) {
895                // Find the opening brace (may have whitespace between name and `{`)
896                if let Some(brace_rel) = source[after_name..].find('{') {
897                    let brace_start = after_name + brace_rel;
898                    // Verify no word characters between name end and brace
899                    if source[after_name..brace_start]
900                        .chars()
901                        .all(|c| c.is_whitespace())
902                        && let Some(body_len) = match_braces(&source[brace_start..])
903                    {
904                        let end = consume_semicolon(source, brace_start + body_len);
905                        return Some(start..end);
906                    }
907                }
908            }
909            search_from = start + 1;
910        }
911    }
912    None
913}
914
915/// Find the byte range of a named struct in Rust source (`struct Name { ... }`).
916pub fn find_rust_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
917    let needle = format!("struct {struct_name}");
918    let mut search_from = 0usize;
919    while let Some(rel) = source[search_from..].find(&needle) {
920        let start = search_from + rel;
921        let after_name = start + needle.len();
922        let boundary = source[after_name..].chars().next();
923        if matches!(
924            boundary,
925            Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
926        ) && let Some(brace_rel) = source[after_name..].find('{')
927        {
928            let brace_start = after_name + brace_rel;
929            if source[after_name..brace_start]
930                .chars()
931                .all(|c| c.is_whitespace())
932                && let Some(body_len) = match_braces(&source[brace_start..])
933            {
934                // Rust structs have no trailing `;` (unit structs do, but we skip those)
935                return Some(start..brace_start + body_len);
936            }
937        }
938        search_from = start + 1;
939    }
940    None
941}
942
943/// Find the byte range of a named struct in Go source (`type Name struct { ... }`).
944pub fn find_go_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
945    let needle = format!("type {struct_name} struct");
946    let mut search_from = 0usize;
947    while let Some(rel) = source[search_from..].find(&needle) {
948        let start = search_from + rel;
949        let after_kw = start + needle.len();
950        if let Some(brace_rel) = source[after_kw..].find('{') {
951            let brace_start = after_kw + brace_rel;
952            if source[after_kw..brace_start]
953                .chars()
954                .all(|c| c.is_whitespace())
955                && let Some(body_len) = match_braces(&source[brace_start..])
956            {
957                return Some(start..brace_start + body_len);
958            }
959        }
960        search_from = start + 1;
961    }
962    None
963}
964
965// ── in-place rewriters ────────────────────────────────────────────────────────
966
967/// Apply C/C++ struct reorderings in-place, returning the modified source.
968/// Each layout in `layouts` is looked up by name; matched structs are replaced
969/// with the optimally-ordered definition. Field declarations (including comments
970/// and annotations such as `GUARDED_BY`) are preserved verbatim from the original
971/// source when possible; IR-based generation is used as a fallback.
972/// Replacements are applied back-to-front so byte offsets remain valid.
973pub fn apply_fixes_c(source: &str, layouts: &[&StructLayout]) -> String {
974    apply_fixes_with_source(
975        source,
976        layouts,
977        find_c_struct_span,
978        generate_c_fix_from_source,
979    )
980}
981
982/// Apply Rust struct reorderings in-place, returning the modified source.
983/// Preserves `pub`, `pub(crate)`, `#[serde(...)]`, `/// doc-comments`, and other
984/// attributes verbatim; falls back to IR-based generation when source cannot be parsed.
985pub fn apply_fixes_rust(source: &str, layouts: &[&StructLayout]) -> String {
986    apply_fixes_with_source(
987        source,
988        layouts,
989        find_rust_struct_span,
990        generate_rust_fix_from_source,
991    )
992}
993
994/// Apply Go struct reorderings in-place, returning the modified source.
995/// Preserves field tags and comments verbatim; falls back to IR-based generation.
996pub fn apply_fixes_go(source: &str, layouts: &[&StructLayout]) -> String {
997    apply_fixes_with_source(
998        source,
999        layouts,
1000        find_go_struct_span,
1001        generate_go_fix_from_source,
1002    )
1003}
1004
1005/// Render a reordered Zig struct definition as source text.
1006/// Zig structs are declared as `const Name = struct { ... };`.
1007/// If the layout is packed, the output uses `packed struct`.
1008pub fn generate_zig_fix(layout: &StructLayout) -> String {
1009    let optimal = optimal_order(layout);
1010    let qualifier = if layout.is_packed { "packed " } else { "" };
1011    let mut out = format!("const {} = {}struct {{\n", layout.name, qualifier);
1012    for field in &optimal {
1013        let ty = field_type_name(field);
1014        out.push_str(&format!("    {}: {ty},\n", field.name));
1015    }
1016    out.push_str("};\n");
1017    out
1018}
1019
1020/// Find the byte range of a named Zig struct in source.
1021/// Matches `const Name = [packed|extern ]struct { ... };`.
1022pub fn find_zig_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
1023    // Match `const Name =` (with optional whitespace variations)
1024    let needle = format!("const {struct_name}");
1025    let mut search_from = 0usize;
1026    while let Some(rel) = source[search_from..].find(&needle) {
1027        let start = search_from + rel;
1028        let after_name = start + needle.len();
1029        // Must be followed by whitespace then `=`
1030        let rest = source[after_name..].trim_start();
1031        if !rest.starts_with('=') {
1032            search_from = start + 1;
1033            continue;
1034        }
1035        // Find `struct` keyword after `=`
1036        let after_eq = after_name + source[after_name..].find('=')? + 1;
1037        let after_eq_rest = &source[after_eq..];
1038        // Skip optional `packed` or `extern` modifiers
1039        if let Some(struct_rel) = after_eq_rest.find("struct") {
1040            // Check no non-whitespace/identifier characters between = and struct
1041            // (beyond optional packed/extern modifiers)
1042            let prefix = &after_eq_rest[..struct_rel];
1043            let prefix_clean = prefix.trim();
1044            if prefix_clean.is_empty() || prefix_clean == "packed" || prefix_clean == "extern" {
1045                let struct_kw_end = after_eq + struct_rel + "struct".len();
1046                if let Some(brace_rel) = source[struct_kw_end..].find('{') {
1047                    let brace_start = struct_kw_end + brace_rel;
1048                    if source[struct_kw_end..brace_start]
1049                        .chars()
1050                        .all(|c| c.is_whitespace())
1051                        && let Some(body_len) = match_braces(&source[brace_start..])
1052                    {
1053                        let end = consume_semicolon(source, brace_start + body_len);
1054                        return Some(start..end);
1055                    }
1056                }
1057            }
1058        }
1059        search_from = start + 1;
1060    }
1061    None
1062}
1063
1064/// Apply Zig struct reorderings in-place, returning the modified source.
1065/// Preserves field comments and annotations verbatim; falls back to IR-based generation.
1066pub fn apply_fixes_zig(source: &str, layouts: &[&StructLayout]) -> String {
1067    apply_fixes_with_source(
1068        source,
1069        layouts,
1070        find_zig_struct_span,
1071        generate_zig_fix_from_source,
1072    )
1073}
1074
1075/// Source-aware variant of `apply_fixes`: passes the original struct source text
1076/// (extracted from the span) to the generator, enabling verbatim field preservation.
1077fn apply_fixes_with_source(
1078    source: &str,
1079    layouts: &[&StructLayout],
1080    find_span: fn(&str, &str) -> Option<std::ops::Range<usize>>,
1081    generate: fn(&StructLayout, &str) -> String,
1082) -> String {
1083    // Collect (start, end, replacement) for each matching layout
1084    let mut replacements: Vec<(usize, usize, String)> = layouts
1085        .iter()
1086        .filter_map(|layout| {
1087            let span = find_span(source, &layout.name)?;
1088            let struct_source = &source[span.clone()];
1089            let fixed = generate(layout, struct_source);
1090            Some((span.start, span.end, fixed))
1091        })
1092        .collect();
1093
1094    // Sort by start offset ascending, then apply in reverse so offsets stay valid
1095    replacements.sort_by_key(|(start, _, _)| *start);
1096
1097    let mut result = source.to_string();
1098    for (start, end, fixed) in replacements.into_iter().rev() {
1099        result.replace_range(start..end, &fixed);
1100    }
1101    result
1102}
1103
1104fn field_type_name(field: &padlock_core::ir::Field) -> &str {
1105    match &field.ty {
1106        padlock_core::ir::TypeInfo::Primitive { name, .. }
1107        | padlock_core::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
1108        padlock_core::ir::TypeInfo::Pointer { .. } => "void*",
1109        padlock_core::ir::TypeInfo::Array { .. } => "/* array */",
1110        padlock_core::ir::TypeInfo::Struct(l) => l.name.as_str(),
1111    }
1112}
1113
1114// ── tests ─────────────────────────────────────────────────────────────────────
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119    use padlock_core::ir::test_fixtures::connection_layout;
1120
1121    #[test]
1122    fn c_fix_starts_with_struct() {
1123        let out = generate_c_fix(&connection_layout());
1124        assert!(out.starts_with("struct Connection {"));
1125    }
1126
1127    #[test]
1128    fn c_fix_contains_all_fields() {
1129        let out = generate_c_fix(&connection_layout());
1130        assert!(out.contains("timeout"));
1131        assert!(out.contains("port"));
1132        assert!(out.contains("is_active"));
1133        assert!(out.contains("is_tls"));
1134    }
1135
1136    #[test]
1137    fn c_fix_puts_largest_align_first() {
1138        let out = generate_c_fix(&connection_layout());
1139        let timeout_pos = out.find("timeout").unwrap();
1140        let is_active_pos = out.find("is_active").unwrap();
1141        assert!(timeout_pos < is_active_pos);
1142    }
1143
1144    #[test]
1145    fn rust_fix_uses_colon_syntax() {
1146        let out = generate_rust_fix(&connection_layout());
1147        assert!(out.contains(": f64"));
1148    }
1149
1150    #[test]
1151    fn unified_diff_marks_changes() {
1152        let orig = "struct T { char a; double b; };\n";
1153        let fixed = "struct T { double b; char a; };\n";
1154        let diff = unified_diff(orig, fixed, 1);
1155        assert!(diff.contains('-') || diff.contains('+'));
1156    }
1157
1158    #[test]
1159    fn unified_diff_identical_is_no_changes() {
1160        assert_eq!(unified_diff("x\n", "x\n", 3), "(no changes)\n");
1161    }
1162
1163    // ── span finders ──────────────────────────────────────────────────────────
1164
1165    #[test]
1166    fn find_c_struct_span_basic() {
1167        let src = "struct Foo { int x; char y; };\nstruct Bar { double z; };\n";
1168        let span = find_c_struct_span(src, "Foo").unwrap();
1169        let text = &src[span];
1170        assert!(text.starts_with("struct Foo"));
1171        assert!(!text.contains("Bar"));
1172    }
1173
1174    #[test]
1175    fn find_c_struct_span_missing_returns_none() {
1176        let src = "struct Other { int x; };";
1177        assert!(find_c_struct_span(src, "Missing").is_none());
1178    }
1179
1180    #[test]
1181    fn find_rust_struct_span_basic() {
1182        let src = "struct Foo {\n    x: u32,\n    y: u8,\n}\n";
1183        let span = find_rust_struct_span(src, "Foo").unwrap();
1184        assert!(src[span].starts_with("struct Foo"));
1185    }
1186
1187    #[test]
1188    fn find_go_struct_span_basic() {
1189        let src = "type Foo struct {\n\tX int32\n\tY bool\n}\n";
1190        let span = find_go_struct_span(src, "Foo").unwrap();
1191        assert!(src[span].starts_with("type Foo struct"));
1192    }
1193
1194    // ── apply_fixes ───────────────────────────────────────────────────────────
1195
1196    #[test]
1197    fn apply_fixes_c_reorders_in_place() {
1198        // Connection has char/double/char/int — after fix, double should come first
1199        let src = "struct Connection { bool is_active; double timeout; bool is_tls; int port; };\n";
1200        let layout = connection_layout();
1201        let fixed = apply_fixes_c(src, &[&layout]);
1202        let timeout_pos = fixed.find("timeout").unwrap();
1203        let is_active_pos = fixed.find("is_active").unwrap();
1204        assert!(
1205            timeout_pos < is_active_pos,
1206            "double should appear before bool after reorder"
1207        );
1208    }
1209
1210    #[test]
1211    fn apply_fixes_rust_reorders_in_place() {
1212        let src = "struct Connection {\n    is_active: bool,\n    timeout: f64,\n    is_tls: bool,\n    port: i32,\n}\n";
1213        let layout = connection_layout();
1214        let fixed = apply_fixes_rust(src, &[&layout]);
1215        let timeout_pos = fixed.find("timeout").unwrap();
1216        let is_active_pos = fixed.find("is_active").unwrap();
1217        assert!(timeout_pos < is_active_pos);
1218    }
1219
1220    #[test]
1221    fn go_fix_uses_tab_syntax() {
1222        let layout = connection_layout();
1223        let out = generate_go_fix(&layout);
1224        assert!(out.starts_with("type Connection struct"));
1225        assert!(out.contains('\t'));
1226    }
1227
1228    #[test]
1229    fn zig_fix_uses_const_struct_syntax() {
1230        let out = generate_zig_fix(&connection_layout());
1231        assert!(out.starts_with("const Connection = struct {"));
1232        assert!(out.ends_with("};\n"));
1233    }
1234
1235    #[test]
1236    fn find_zig_struct_span_basic() {
1237        let src = "const S = struct {\n    x: u32,\n    y: u8,\n};\n";
1238        let span = find_zig_struct_span(src, "S").unwrap();
1239        assert!(src[span].starts_with("const S = struct"));
1240    }
1241
1242    #[test]
1243    fn find_zig_struct_span_packed() {
1244        let src = "const S = packed struct {\n    x: u32,\n    y: u8,\n};\n";
1245        let span = find_zig_struct_span(src, "S").unwrap();
1246        assert!(src[span].contains("packed struct"));
1247    }
1248
1249    #[test]
1250    fn find_zig_struct_span_missing_returns_none() {
1251        let src = "const Other = struct { x: u8 };\n";
1252        assert!(find_zig_struct_span(src, "Missing").is_none());
1253    }
1254
1255    #[test]
1256    fn apply_fixes_zig_reorders_in_place() {
1257        use crate::parse_source_str;
1258        use padlock_core::arch::X86_64_SYSV;
1259        let src = "const S = struct {\n    a: u8,\n    b: u64,\n};\n";
1260        let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1261        let layout = &layouts[0];
1262        let fixed = apply_fixes_zig(src, &[layout]);
1263        // b (u64, align 8) should come before a (u8)
1264        let b_pos = fixed.find("b:").unwrap();
1265        let a_pos = fixed.find("a:").unwrap();
1266        assert!(
1267            b_pos < a_pos,
1268            "u64 field should come before u8 after reorder"
1269        );
1270    }
1271
1272    // ── fix quality: source-aware preservation ────────────────────────────────
1273
1274    #[test]
1275    fn rust_fix_preserves_pub_visibility() {
1276        let src = "struct S {\n    pub a: u8,\n    pub b: u64,\n}\n";
1277        use crate::parse_source_str;
1278        use padlock_core::arch::X86_64_SYSV;
1279        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1280        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1281        // pub keyword must appear before both fields
1282        assert!(fixed.contains("pub b: u64"), "pub on b must be preserved");
1283        assert!(fixed.contains("pub a: u8"), "pub on a must be preserved");
1284        // b (u64, align 8) should appear before a (u8, align 1)
1285        assert!(fixed.find("pub b").unwrap() < fixed.find("pub a").unwrap());
1286    }
1287
1288    #[test]
1289    fn rust_fix_preserves_doc_comments() {
1290        let src = concat!(
1291            "struct S {\n",
1292            "    /// small field\n",
1293            "    a: u8,\n",
1294            "    /// large field\n",
1295            "    b: u64,\n",
1296            "}\n"
1297        );
1298        use crate::parse_source_str;
1299        use padlock_core::arch::X86_64_SYSV;
1300        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1301        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1302        assert!(
1303            fixed.contains("/// large field"),
1304            "doc comment for b must survive"
1305        );
1306        assert!(
1307            fixed.contains("/// small field"),
1308            "doc comment for a must survive"
1309        );
1310        // The doc comment for b must appear before the doc comment for a
1311        assert!(
1312            fixed.find("large field").unwrap() < fixed.find("small field").unwrap(),
1313            "doc comment ordering must follow field ordering"
1314        );
1315    }
1316
1317    #[test]
1318    fn rust_fix_preserves_serde_attributes() {
1319        let src = concat!(
1320            "struct S {\n",
1321            "    #[serde(skip)]\n",
1322            "    a: u8,\n",
1323            "    #[serde(rename = \"big\")]\n",
1324            "    b: u64,\n",
1325            "}\n"
1326        );
1327        use crate::parse_source_str;
1328        use padlock_core::arch::X86_64_SYSV;
1329        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1330        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1331        assert!(
1332            fixed.contains("#[serde(skip)]"),
1333            "serde attribute on a must survive"
1334        );
1335        assert!(
1336            fixed.contains("#[serde(rename = \"big\")]"),
1337            "serde attribute on b must survive"
1338        );
1339    }
1340
1341    #[test]
1342    fn rust_fix_preserves_pub_crate_visibility() {
1343        let src = "struct S {\n    pub(crate) a: u8,\n    pub(crate) b: u64,\n}\n";
1344        use crate::parse_source_str;
1345        use padlock_core::arch::X86_64_SYSV;
1346        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1347        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1348        assert!(
1349            fixed.contains("pub(crate) b: u64"),
1350            "pub(crate) on b must be preserved"
1351        );
1352        assert!(
1353            fixed.contains("pub(crate) a: u8"),
1354            "pub(crate) on a must be preserved"
1355        );
1356    }
1357
1358    #[test]
1359    fn c_fix_preserves_guarded_by_comments() {
1360        let src = concat!(
1361            "struct S {\n",
1362            "    char a; // GUARDED_BY(mu)\n",
1363            "    double b; // large field\n",
1364            "};\n"
1365        );
1366        use crate::parse_source_str;
1367        use padlock_core::arch::X86_64_SYSV;
1368        let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1369        let fixed = apply_fixes_c(src, &[&layouts[0]]);
1370        assert!(
1371            fixed.contains("GUARDED_BY(mu)"),
1372            "guard annotation comment must survive reorder"
1373        );
1374        // double should come before char
1375        assert!(fixed.find("double b").unwrap() < fixed.find("char a").unwrap());
1376    }
1377
1378    #[test]
1379    fn go_fix_preserves_field_tags() {
1380        let src = concat!("type S struct {\n", "\ta uint8\n", "\tb uint64\n", "}\n");
1381        use crate::parse_source_str;
1382        use padlock_core::arch::X86_64_SYSV;
1383        let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1384        let fixed = apply_fixes_go(src, &[&layouts[0]]);
1385        // b (8 bytes) should appear before a (1 byte)
1386        assert!(fixed.find("\tb uint64").unwrap() < fixed.find("\ta uint8").unwrap());
1387    }
1388
1389    #[test]
1390    fn zig_fix_preserves_field_comments() {
1391        let src = concat!(
1392            "const S = struct {\n",
1393            "    // small\n",
1394            "    a: u8,\n",
1395            "    // large\n",
1396            "    b: u64,\n",
1397            "};\n"
1398        );
1399        use crate::parse_source_str;
1400        use padlock_core::arch::X86_64_SYSV;
1401        let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1402        let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1403        assert!(fixed.contains("// large"), "comment for b must survive");
1404        assert!(fixed.contains("// small"), "comment for a must survive");
1405        // b should appear first
1406        assert!(fixed.find("// large").unwrap() < fixed.find("// small").unwrap());
1407    }
1408
1409    // ── bad weather: fix quality fallback ─────────────────────────────────────
1410
1411    #[test]
1412    fn rust_fix_from_source_falls_back_when_no_open_brace() {
1413        // Struct source that is malformed (no `{`): must not panic, falls back to IR
1414        let layout = connection_layout();
1415        let out = generate_rust_fix_from_source(&layout, "struct Connection");
1416        // IR fallback produces valid Rust syntax
1417        assert!(out.starts_with("struct Connection {"));
1418    }
1419
1420    #[test]
1421    fn c_fix_from_source_falls_back_when_chunks_empty() {
1422        // Body with no parseable fields — chunk extraction returns empty vec,
1423        // triggering IR fallback
1424        let layout = connection_layout();
1425        let out = generate_c_fix_from_source(&layout, "struct Connection { /* no fields */ };");
1426        assert!(out.starts_with("struct Connection {"));
1427        assert!(out.contains("timeout"));
1428    }
1429
1430    #[test]
1431    fn zig_fix_from_source_falls_back_on_missing_field_name() {
1432        // IR field names don't match chunk names → fallback to IR
1433        let layout = connection_layout();
1434        let out =
1435            generate_zig_fix_from_source(&layout, "const Connection = struct { x: u8, y: u64, };");
1436        // IR fallback must still produce all fields from the layout
1437        assert!(out.contains("timeout"));
1438    }
1439
1440    // ── Go fix tests ─────────────────────────────────────────────────────────
1441
1442    #[test]
1443    fn go_fix_reorders_fields() {
1444        let layout = connection_layout();
1445        let out = generate_go_fix(&layout);
1446        // timeout (align 8) must come before bools (align 1)
1447        let pos_timeout = out.find("timeout").unwrap();
1448        let pos_port = out.find("port").unwrap();
1449        let pos_bool = out.find("is_active").unwrap();
1450        assert!(pos_timeout < pos_bool, "timeout must precede booleans");
1451        assert!(pos_port < pos_bool, "port must precede booleans");
1452    }
1453
1454    #[test]
1455    fn go_fix_from_source_preserves_verbatim_field_lines() {
1456        let layout = connection_layout();
1457        let src = r#"type Connection struct {
1458	is_active bool
1459	timeout   f64
1460	is_tls    bool
1461	port      i32
1462}"#;
1463        let out = generate_go_fix_from_source(&layout, src);
1464        // The reordered output must contain the exact verbatim field lines
1465        assert!(out.contains("timeout   f64"), "verbatim timeout line");
1466        assert!(out.contains("port      i32"), "verbatim port line");
1467        // timeout must appear before is_active in the output
1468        let pos_timeout = out.find("timeout").unwrap();
1469        let pos_is_active = out.find("is_active").unwrap();
1470        assert!(
1471            pos_timeout < pos_is_active,
1472            "timeout must come before is_active"
1473        );
1474    }
1475
1476    #[test]
1477    fn apply_fixes_go_rewrites_struct_in_file() {
1478        let src = "package p\n\ntype Point struct {\n\tFlag bool\n\tX    int64\n\tY    int32\n}\n";
1479        // Build a minimal layout: Flag(bool,1), X(i64,8), Y(i32,4)
1480        // optimal: X(8) → Y(4) → Flag(1)
1481        use padlock_core::arch::X86_64_SYSV;
1482        use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
1483        let layout = StructLayout {
1484            name: "Point".into(),
1485            total_size: 16,
1486            align: 8,
1487            fields: vec![
1488                Field {
1489                    name: "Flag".into(),
1490                    ty: TypeInfo::Primitive {
1491                        name: "bool".into(),
1492                        size: 1,
1493                        align: 1,
1494                    },
1495                    offset: 0,
1496                    size: 1,
1497                    align: 1,
1498                    source_file: None,
1499                    source_line: None,
1500                    access: AccessPattern::Unknown,
1501                },
1502                Field {
1503                    name: "X".into(),
1504                    ty: TypeInfo::Primitive {
1505                        name: "int64".into(),
1506                        size: 8,
1507                        align: 8,
1508                    },
1509                    offset: 8,
1510                    size: 8,
1511                    align: 8,
1512                    source_file: None,
1513                    source_line: None,
1514                    access: AccessPattern::Unknown,
1515                },
1516                Field {
1517                    name: "Y".into(),
1518                    ty: TypeInfo::Primitive {
1519                        name: "int32".into(),
1520                        size: 4,
1521                        align: 4,
1522                    },
1523                    offset: 16,
1524                    size: 4,
1525                    align: 4,
1526                    source_file: None,
1527                    source_line: None,
1528                    access: AccessPattern::Unknown,
1529                },
1530            ],
1531            source_file: None,
1532            source_line: None,
1533            arch: &X86_64_SYSV,
1534            is_packed: false,
1535            is_union: false,
1536            is_repr_rust: false,
1537            suppressed_findings: vec![],
1538            uncertain_fields: Vec::new(),
1539        };
1540        let fixed = apply_fixes_go(src, &[&layout]);
1541        // X (int64, align 8) must appear before Flag (bool, align 1)
1542        let pos_x = fixed.find("\tX ").unwrap();
1543        let pos_flag = fixed.find("\tFlag").unwrap();
1544        assert!(pos_x < pos_flag, "X must precede Flag after reorder");
1545        // Package declaration must be preserved
1546        assert!(fixed.starts_with("package p\n"), "package line preserved");
1547    }
1548
1549    // ── regression: no blank line after opening brace ─────────────────────────
1550
1551    #[test]
1552    fn c_fix_no_blank_line_after_opening_brace() {
1553        use crate::parse_source_str;
1554        use padlock_core::arch::X86_64_SYSV;
1555        let src = "struct S {\n    char a;\n    double b;\n};\n";
1556        let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1557        let fixed = apply_fixes_c(src, &[&layouts[0]]);
1558        // The character immediately after '{' must be '\n' followed by a non-'\n'
1559        // character (i.e. no blank line).
1560        let brace = fixed.find('{').unwrap();
1561        let after_brace = &fixed[brace + 1..];
1562        assert!(
1563            !after_brace.starts_with("\n\n"),
1564            "C fix must not insert a blank line after '{{': got {:?}",
1565            &after_brace[..after_brace.len().min(20)]
1566        );
1567    }
1568
1569    #[test]
1570    fn go_fix_no_blank_line_after_opening_brace() {
1571        use crate::parse_source_str;
1572        use padlock_core::arch::X86_64_SYSV;
1573        let src = "type S struct {\n\ta uint8\n\tb uint64\n}\n";
1574        let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1575        let fixed = apply_fixes_go(src, &[&layouts[0]]);
1576        let brace = fixed.find('{').unwrap();
1577        let after_brace = &fixed[brace + 1..];
1578        assert!(
1579            !after_brace.starts_with("\n\n"),
1580            "Go fix must not insert a blank line after '{{': got {:?}",
1581            &after_brace[..after_brace.len().min(20)]
1582        );
1583    }
1584
1585    #[test]
1586    fn zig_fix_no_blank_line_after_opening_brace() {
1587        use crate::parse_source_str;
1588        use padlock_core::arch::X86_64_SYSV;
1589        let src = "const S = struct {\n    a: u8,\n    b: u64,\n};\n";
1590        let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1591        let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1592        let brace = fixed.find('{').unwrap();
1593        let after_brace = &fixed[brace + 1..];
1594        assert!(
1595            !after_brace.starts_with("\n\n"),
1596            "Zig fix must not insert a blank line after '{{': got {:?}",
1597            &after_brace[..after_brace.len().min(20)]
1598        );
1599    }
1600
1601    #[test]
1602    fn rust_fix_no_blank_line_after_opening_brace() {
1603        use crate::parse_source_str;
1604        use padlock_core::arch::X86_64_SYSV;
1605        let src = "struct S {\n    a: u8,\n    b: u64,\n}\n";
1606        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1607        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1608        let brace = fixed.find('{').unwrap();
1609        let after_brace = &fixed[brace + 1..];
1610        assert!(
1611            !after_brace.starts_with("\n\n"),
1612            "Rust fix must not insert a blank line after '{{': got {:?}",
1613            &after_brace[..after_brace.len().min(20)]
1614        );
1615    }
1616
1617    // ── single-field struct: no-op reorder ────────────────────────────────────
1618
1619    #[test]
1620    fn rust_fix_single_field_struct_unchanged() {
1621        use crate::parse_source_str;
1622        use padlock_core::arch::X86_64_SYSV;
1623        let src = "struct S {\n    x: u64,\n}\n";
1624        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1625        // Single-field struct has no ReorderSuggestion, but generate_rust_fix_from_source
1626        // must still produce valid (identical) output without panicking.
1627        let result = generate_rust_fix_from_source(&layouts[0], src);
1628        assert!(result.contains("x: u64"), "single field must be present");
1629        assert!(
1630            !result.contains("\n\n"),
1631            "no blank line in single-field output"
1632        );
1633    }
1634
1635    #[test]
1636    fn c_fix_single_field_struct_unchanged() {
1637        use crate::parse_source_str;
1638        use padlock_core::arch::X86_64_SYSV;
1639        let src = "struct S {\n    double x;\n};\n";
1640        let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1641        let result = generate_c_fix_from_source(&layouts[0], src);
1642        assert!(result.contains("double x"), "single field must be present");
1643        assert!(
1644            !result.contains("\n\n"),
1645            "no blank line in single-field output"
1646        );
1647    }
1648
1649    // ── trailing comma vs no trailing comma ───────────────────────────────────
1650
1651    #[test]
1652    fn rust_fix_preserves_trailing_comma() {
1653        use crate::parse_source_str;
1654        use padlock_core::arch::X86_64_SYSV;
1655        // Last field has a trailing comma — output should keep it
1656        let src = "struct S {\n    a: u8,\n    b: u64,\n}\n";
1657        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1658        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1659        // b should come first; trailing comma on a must survive
1660        assert!(
1661            fixed.contains("b: u64,"),
1662            "trailing comma on reordered-first field must be preserved"
1663        );
1664        assert!(
1665            fixed.contains("a: u8,"),
1666            "trailing comma on last field must be preserved"
1667        );
1668    }
1669
1670    #[test]
1671    fn rust_fix_no_trailing_comma_on_last_field() {
1672        use crate::parse_source_str;
1673        use padlock_core::arch::X86_64_SYSV;
1674        // Last field has NO trailing comma — the chunk must not gain one
1675        let src = "struct S {\n    a: u8,\n    b: u64\n}\n";
1676        let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1677        let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1678        // b (no comma in original) should now be first field; verify the chunk is verbatim
1679        assert!(
1680            fixed.contains("b: u64"),
1681            "b field must be present after reorder"
1682        );
1683        // The output must not have two closing-brace lines (structural integrity)
1684        assert_eq!(fixed.chars().filter(|&c| c == '}').count(), 1);
1685    }
1686}