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