Skip to main content

padlock_source/frontends/
go.rs

1// padlock-source/src/frontends/go.rs
2//
3// Extracts struct layouts from Go source using tree-sitter-go.
4// Sizes use Go's platform-native alignment rules (same as C on the target arch).
5
6use padlock_core::arch::ArchConfig;
7use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
8use std::collections::HashSet;
9use tree_sitter::{Node, Parser};
10
11// ── type resolution ───────────────────────────────────────────────────────────
12
13fn go_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
14    match ty.trim() {
15        "bool" => (1, 1),
16        "int8" | "uint8" | "byte" => (1, 1),
17        "int16" | "uint16" => (2, 2),
18        "int32" | "uint32" | "rune" | "float32" => (4, 4),
19        "int64" | "uint64" | "float64" | "complex64" => (8, 8),
20        "complex128" => (16, 16),
21        "int" | "uint" => (arch.pointer_size, arch.pointer_size),
22        "uintptr" => (arch.pointer_size, arch.pointer_size),
23        "string" => (arch.pointer_size * 2, arch.pointer_size), // ptr + len
24        ty if ty.starts_with("[]") => (arch.pointer_size * 3, arch.pointer_size), // ptr+len+cap
25        ty if ty.starts_with("map[") || ty.starts_with("chan ") => {
26            (arch.pointer_size, arch.pointer_size)
27        }
28        ty if ty.starts_with('*') => (arch.pointer_size, arch.pointer_size),
29        // Interface types: two-word fat pointer (type pointer + data pointer).
30        // `error` and `any` are the two universally-known interface names; inline
31        // anonymous interface bodies (`interface{ Method() }`) are caught by the
32        // `starts_with("interface")` arm.
33        //
34        // Locally-declared named interfaces (e.g. `type Reader interface { … }`) are
35        // resolved to 16B by `parse_struct_type` using the phase-1 interface name set
36        // collected by `collect_go_interface_names` — they do not reach this function.
37        //
38        // Qualified names from external packages (e.g. `io.Reader`, `driver.Connector`)
39        // fall through to the `_` arm (pointer_size) and are flagged as `uncertain_fields`
40        // by `parse_struct_type` so the output layer can warn the user.
41        "error" | "any" => (arch.pointer_size * 2, arch.pointer_size),
42        ty if ty.starts_with("interface") => (arch.pointer_size * 2, arch.pointer_size),
43        _ => (arch.pointer_size, arch.pointer_size),
44    }
45}
46
47// ── phase-1: local interface name collection ──────────────────────────────────
48
49/// Scan a Go source tree for `type X interface { ... }` declarations and
50/// return the set of locally-defined interface names.
51///
52/// These names are used in `parse_struct_type` to size named-interface fields
53/// as two-word fat pointers (type-pointer + data-pointer, 16 bytes on 64-bit)
54/// instead of falling through to the generic pointer-sized unknown catch-all.
55fn collect_go_interface_names(source: &str, root: Node<'_>) -> HashSet<String> {
56    let mut names = HashSet::new();
57    let mut stack = vec![root];
58    while let Some(node) = stack.pop() {
59        for i in (0..node.child_count()).rev() {
60            if let Some(child) = node.child(i) {
61                stack.push(child);
62            }
63        }
64        if node.kind() != "type_spec" {
65            continue;
66        }
67        // type_spec: name=type_identifier, type=interface_type
68        let mut iface_name: Option<String> = None;
69        let mut is_interface = false;
70        for i in 0..node.child_count() {
71            let Some(child) = node.child(i) else { continue };
72            match child.kind() {
73                "type_identifier" => {
74                    iface_name = Some(source[child.byte_range()].to_string());
75                }
76                "interface_type" => {
77                    is_interface = true;
78                }
79                _ => {}
80            }
81        }
82        if is_interface && let Some(name) = iface_name {
83            names.insert(name);
84        }
85    }
86    names
87}
88
89// ── tree-sitter walker ────────────────────────────────────────────────────────
90
91fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
92    // Phase 1: collect locally-defined interface names for accurate fat-pointer sizing.
93    let local_interfaces = collect_go_interface_names(source, root);
94
95    let mut layouts = Vec::new();
96    let mut stack = vec![root];
97
98    while let Some(node) = stack.pop() {
99        for i in (0..node.child_count()).rev() {
100            if let Some(c) = node.child(i) {
101                stack.push(c);
102            }
103        }
104
105        // type_declaration → type_spec → struct_type
106        if node.kind() == "type_declaration"
107            && let Some(layout) = parse_type_declaration(source, node, arch, &local_interfaces)
108        {
109            layouts.push(layout);
110        }
111    }
112    layouts
113}
114
115fn parse_type_declaration(
116    source: &str,
117    node: Node<'_>,
118    arch: &'static ArchConfig,
119    local_interfaces: &HashSet<String>,
120) -> Option<StructLayout> {
121    let source_line = node.start_position().row as u32 + 1;
122    let decl_start_byte = node.start_byte();
123    // type_declaration has a type_spec child
124    for i in 0..node.child_count() {
125        let child = node.child(i)?;
126        if child.kind() == "type_spec" {
127            return parse_type_spec(
128                source,
129                child,
130                arch,
131                source_line,
132                decl_start_byte,
133                local_interfaces,
134            );
135        }
136    }
137    None
138}
139
140fn parse_type_spec(
141    source: &str,
142    node: Node<'_>,
143    arch: &'static ArchConfig,
144    source_line: u32,
145    decl_start_byte: usize,
146    local_interfaces: &HashSet<String>,
147) -> Option<StructLayout> {
148    let mut name: Option<String> = None;
149    let mut struct_node: Option<Node> = None;
150
151    for i in 0..node.child_count() {
152        let child = node.child(i)?;
153        match child.kind() {
154            "type_identifier" => name = Some(source[child.byte_range()].to_string()),
155            "struct_type" => struct_node = Some(child),
156            _ => {}
157        }
158    }
159
160    let name = name?;
161    let struct_node = struct_node?;
162    parse_struct_type(
163        source,
164        struct_node,
165        name,
166        arch,
167        source_line,
168        decl_start_byte,
169        local_interfaces,
170    )
171}
172
173fn parse_struct_type(
174    source: &str,
175    node: Node<'_>,
176    name: String,
177    arch: &'static ArchConfig,
178    source_line: u32,
179    decl_start_byte: usize,
180    local_interfaces: &HashSet<String>,
181) -> Option<StructLayout> {
182    let mut raw_fields: Vec<(String, String, Option<String>, u32)> = Vec::new();
183
184    for i in 0..node.child_count() {
185        let child = node.child(i)?;
186        if child.kind() == "field_declaration_list" {
187            for j in 0..child.child_count() {
188                let field_node = child.child(j)?;
189                if field_node.kind() == "field_declaration" {
190                    collect_field_declarations(source, field_node, &mut raw_fields);
191                }
192            }
193        }
194    }
195
196    if raw_fields.is_empty() {
197        return None;
198    }
199
200    // Simulate layout
201    let mut offset = 0usize;
202    let mut struct_align = 1usize;
203    let mut fields: Vec<Field> = Vec::new();
204    let mut uncertain_fields: Vec<String> = Vec::new();
205
206    for (fname, ty_name, guard, field_line) in raw_fields {
207        let (mut size, mut align) = go_type_size_align(&ty_name, arch);
208
209        // Override: a locally-declared interface type is a fat pointer (16B on 64-bit).
210        // go_type_size_align does not know about local names, so we patch here.
211        if local_interfaces.contains(ty_name.as_str()) {
212            size = arch.pointer_size * 2;
213            align = arch.pointer_size;
214        }
215
216        // Qualified types (e.g. `driver.Connector`, `io.Reader`) come from external
217        // packages. Without type information we cannot determine whether they are
218        // interfaces (16B fat pointer) or structs (arbitrary size). Flag them as
219        // uncertain so the output layer can warn the user.
220        let is_pointer = ty_name.starts_with('*');
221        let base_ty = ty_name.trim_start_matches('*');
222        if !is_pointer && base_ty.contains('.') {
223            uncertain_fields.push(fname.clone());
224        }
225
226        if align > 0 {
227            offset = offset.next_multiple_of(align);
228        }
229        struct_align = struct_align.max(align);
230        let access = if let Some(g) = guard {
231            AccessPattern::Concurrent {
232                guard: Some(g),
233                is_atomic: false,
234                is_annotated: true,
235            }
236        } else {
237            AccessPattern::Unknown
238        };
239        fields.push(Field {
240            name: fname,
241            ty: TypeInfo::Primitive {
242                name: ty_name,
243                size,
244                align,
245            },
246            offset,
247            size,
248            align,
249            source_file: None,
250            source_line: Some(field_line),
251            access,
252        });
253        offset += size;
254    }
255    if struct_align > 0 {
256        offset = offset.next_multiple_of(struct_align);
257    }
258
259    Some(StructLayout {
260        name,
261        total_size: offset,
262        align: struct_align,
263        fields,
264        source_file: None,
265        source_line: Some(source_line),
266        arch,
267        is_packed: false,
268        is_union: false,
269        is_repr_rust: false,
270        suppressed_findings: super::suppress::suppressed_from_preceding_source(
271            source,
272            decl_start_byte,
273        ),
274        uncertain_fields,
275    })
276}
277
278/// Extract a guard name from a Go field's trailing line comment.
279///
280/// Recognised forms (must appear after the field type on the same line):
281/// - `// padlock:guard=mu`
282/// - `// guarded_by: mu`
283/// - `// +checklocksprotects:mu` (gVisor-style)
284pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
285    let c = comment.trim();
286    // Strip leading `//` and optional whitespace
287    let body = c.strip_prefix("//").map(str::trim)?;
288
289    // padlock:guard=mu
290    if let Some(rest) = body.strip_prefix("padlock:guard=") {
291        let guard = rest.trim();
292        if !guard.is_empty() {
293            return Some(guard.to_string());
294        }
295    }
296    // guarded_by: mu
297    if let Some(rest) = body
298        .strip_prefix("guarded_by:")
299        .or_else(|| body.strip_prefix("guarded_by ="))
300    {
301        let guard = rest.trim();
302        if !guard.is_empty() {
303            return Some(guard.to_string());
304        }
305    }
306    // +checklocksprotects:mu (gVisor)
307    if let Some(rest) = body.strip_prefix("+checklocksprotects:") {
308        let guard = rest.trim();
309        if !guard.is_empty() {
310            return Some(guard.to_string());
311        }
312    }
313    None
314}
315
316/// Find the trailing line comment on the same source line as `node`.
317fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
318    // The node's end byte is just past the last token on the field line.
319    // Read the rest of that line from the source.
320    let end = node.end_byte();
321    if end >= source.len() {
322        return None;
323    }
324    let rest = &source[end..];
325    // Take only up to the next newline
326    let line = rest.lines().next().unwrap_or("");
327    // Look for `//` in that remainder
328    line.find("//").map(|pos| line[pos..].to_string())
329}
330
331fn collect_field_declarations(
332    source: &str,
333    node: Node<'_>,
334    out: &mut Vec<(String, String, Option<String>, u32)>,
335) {
336    // field_declaration: field_identifier+ type [comment]
337    // OR embedded type (anonymous field): TypeName [comment]
338    let mut field_names: Vec<String> = Vec::new();
339    let mut ty_text: Option<String> = None;
340    let field_line = node.start_position().row as u32 + 1;
341
342    for i in 0..node.child_count() {
343        if let Some(child) = node.child(i) {
344            match child.kind() {
345                "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
346                "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
347                | "map_type" | "channel_type" | "array_type" | "interface_type" => {
348                    ty_text = Some(source[child.byte_range()].trim().to_string());
349                }
350                _ => {}
351            }
352        }
353    }
354
355    let guard =
356        trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
357
358    if !field_names.is_empty() {
359        if let Some(ty) = ty_text {
360            // Normal named fields
361            for name in field_names {
362                out.push((name, ty.clone(), guard.clone(), field_line));
363            }
364        }
365    } else if let Some(ty) = ty_text {
366        // Embedded (anonymous) field: `sync.Mutex` or `Base`.
367        // Go field name is the unqualified type name.
368        // The nested-struct resolution pass in lib.rs will later fill in
369        // the correct size/align from other parsed struct layouts.
370        let simple_name = ty.split('.').next_back().unwrap_or(&ty).to_string();
371        out.push((simple_name, ty, guard, field_line));
372    }
373}
374
375// ── public API ────────────────────────────────────────────────────────────────
376
377pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
378    let mut parser = Parser::new();
379    parser.set_language(&tree_sitter_go::LANGUAGE.into())?;
380    let tree = parser
381        .parse(source, None)
382        .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
383    Ok(extract_structs(source, tree.root_node(), arch))
384}
385
386// ── tests ─────────────────────────────────────────────────────────────────────
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use padlock_core::arch::X86_64_SYSV;
392
393    #[test]
394    fn parse_simple_go_struct() {
395        let src = r#"
396package main
397type Point struct {
398    X int32
399    Y int32
400}
401"#;
402        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
403        assert_eq!(layouts.len(), 1);
404        assert_eq!(layouts[0].name, "Point");
405        assert_eq!(layouts[0].fields.len(), 2);
406    }
407
408    #[test]
409    fn go_layout_with_padding() {
410        let src = "package p\ntype T struct { A bool; B int64 }";
411        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
412        assert_eq!(layouts.len(), 1);
413        let l = &layouts[0];
414        assert_eq!(l.fields[0].offset, 0);
415        assert_eq!(l.fields[1].offset, 8); // bool (1) + 7 pad → 8
416    }
417
418    #[test]
419    fn go_string_is_two_words() {
420        let src = "package p\ntype S struct { Name string }";
421        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
422        assert_eq!(layouts[0].fields[0].size, 16); // ptr + len
423    }
424
425    // ── Go guard comment extraction ────────────────────────────────────────────
426
427    #[test]
428    fn extract_guard_padlock_form() {
429        assert_eq!(
430            extract_guard_from_go_comment("// padlock:guard=mu"),
431            Some("mu".to_string())
432        );
433    }
434
435    #[test]
436    fn extract_guard_guarded_by_form() {
437        assert_eq!(
438            extract_guard_from_go_comment("// guarded_by: counter_lock"),
439            Some("counter_lock".to_string())
440        );
441    }
442
443    #[test]
444    fn extract_guard_checklocksprotects_form() {
445        assert_eq!(
446            extract_guard_from_go_comment("// +checklocksprotects:mu"),
447            Some("mu".to_string())
448        );
449    }
450
451    #[test]
452    fn extract_guard_no_match_returns_none() {
453        assert!(extract_guard_from_go_comment("// just a comment").is_none());
454        assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
455    }
456
457    #[test]
458    fn go_struct_padlock_guard_annotation_sets_concurrent() {
459        let src = r#"package p
460type Cache struct {
461    Readers int64 // padlock:guard=mu
462    Writers int64 // padlock:guard=other_mu
463    Mu      sync.Mutex
464}
465"#;
466        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
467        let l = &layouts[0];
468        // Readers and Writers should be Concurrent with different guards
469        if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
470            assert_eq!(guard.as_deref(), Some("mu"));
471        } else {
472            panic!(
473                "expected Concurrent for Readers, got {:?}",
474                l.fields[0].access
475            );
476        }
477        if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
478            assert_eq!(guard.as_deref(), Some("other_mu"));
479        } else {
480            panic!(
481                "expected Concurrent for Writers, got {:?}",
482                l.fields[1].access
483            );
484        }
485    }
486
487    #[test]
488    fn go_struct_different_guards_same_cache_line_is_false_sharing() {
489        let src = r#"package p
490type HotPath struct {
491    Readers int64 // padlock:guard=lock_a
492    Writers int64 // padlock:guard=lock_b
493}
494"#;
495        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
496        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
497            &layouts[0]
498        ));
499    }
500
501    #[test]
502    fn go_struct_same_guard_is_not_false_sharing() {
503        let src = r#"package p
504type Safe struct {
505    A int64 // padlock:guard=mu
506    B int64 // padlock:guard=mu
507}
508"#;
509        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
510        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
511            &layouts[0]
512        ));
513    }
514
515    // ── interface{} / any sizing ───────────────────────────────────────────────
516
517    #[test]
518    fn interface_field_is_two_words() {
519        // interface{} is a fat pointer: (type pointer, data pointer) = 2×pointer
520        let src = "package p\ntype S struct { V interface{} }";
521        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
522        assert_eq!(layouts[0].fields[0].size, 16); // 2 × 8B on x86-64
523        assert_eq!(layouts[0].fields[0].align, 8);
524    }
525
526    #[test]
527    fn any_field_is_two_words() {
528        // `any` is an alias for `interface{}` since Go 1.18
529        let src = "package p\ntype S struct { V any }";
530        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
531        assert_eq!(layouts[0].fields[0].size, 16); // 2 × 8B on x86-64
532        assert_eq!(layouts[0].fields[0].align, 8);
533    }
534
535    #[test]
536    fn interface_field_same_size_as_error() {
537        // `error` was already two-word; interface{} must match
538        let src_iface = "package p\ntype S struct { V interface{} }";
539        let src_err = "package p\ntype S struct { V error }";
540        let iface = parse_go(src_iface, &X86_64_SYSV).unwrap();
541        let err = parse_go(src_err, &X86_64_SYSV).unwrap();
542        assert_eq!(iface[0].fields[0].size, err[0].fields[0].size);
543    }
544
545    #[test]
546    fn struct_with_mixed_interface_and_ints_has_correct_layout() {
547        // interface{} at offset 0 (size 16, align 8) then int64 at offset 16
548        let src = "package p\ntype S struct { V interface{}; N int64 }";
549        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
550        let l = &layouts[0];
551        assert_eq!(l.fields[0].offset, 0);
552        assert_eq!(l.fields[0].size, 16);
553        assert_eq!(l.fields[1].offset, 16);
554        assert_eq!(l.total_size, 24);
555    }
556
557    #[test]
558    fn inline_interface_with_methods_is_two_words() {
559        // An anonymous interface with methods (e.g. `interface{ Close() error }`) is a
560        // two-word fat pointer — same as `interface{}`.  The tree-sitter node kind is
561        // `interface_type` in both cases so the `ty.starts_with("interface")` match handles
562        // all inline interface bodies.
563        let src = "package p\ntype S struct { Conn interface{ Close() error } }";
564        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
565        assert_eq!(layouts[0].fields[0].size, 16);
566        assert_eq!(layouts[0].fields[0].align, 8);
567    }
568
569    #[test]
570    fn named_cross_package_interface_falls_back_to_pointer_size() {
571        // Named interfaces from other packages (driver.Connector, io.ReadCloser, …)
572        // appear in the AST as `qualified_type` nodes with text like "driver.Connector".
573        // Without go/types resolution we cannot distinguish an interface from a concrete
574        // struct, so they fall back to pointer_size (8B on x86-64) — the same as an
575        // opaque pointer.  This is a known source-analysis limitation; binary (DWARF)
576        // analysis always returns the correct compiler layout.
577        let src = "package p\ntype DB struct { connector driver.Connector }";
578        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
579        // Known limitation: reports 8B, not the actual 16B.
580        assert_eq!(
581            layouts[0].fields[0].size, 8,
582            "named cross-package interface falls back to pointer_size (known limitation)"
583        );
584        // The field must be flagged as uncertain so the output layer can warn the user.
585        assert!(
586            layouts[0]
587                .uncertain_fields
588                .contains(&"connector".to_string()),
589            "qualified-type field should be in uncertain_fields"
590        );
591    }
592
593    // ── local interface type resolution ───────────────────────────────────────
594
595    #[test]
596    fn local_interface_field_is_fat_pointer() {
597        // A named interface declared in the same file must be sized as a two-word
598        // fat pointer (16B on 64-bit), not as a single pointer (8B).
599        let src = r#"package p
600type Reader interface {
601    Read(p []byte) (n int, err error)
602}
603type Buf struct {
604    R Reader
605    N int32
606}
607"#;
608        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
609        let l = layouts.iter().find(|l| l.name == "Buf").expect("Buf");
610        let r = l.fields.iter().find(|f| f.name == "R").expect("R field");
611        assert_eq!(
612            r.size, 16,
613            "local interface must be sized as 16B fat pointer"
614        );
615        assert_eq!(r.align, 8);
616    }
617
618    #[test]
619    fn local_interface_field_not_marked_uncertain() {
620        // A locally-declared interface is resolved; it must NOT appear in uncertain_fields.
621        let src = r#"package p
622type Closer interface { Close() error }
623type File struct { C Closer }
624"#;
625        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
626        let l = layouts.iter().find(|l| l.name == "File").expect("File");
627        assert!(
628            !l.uncertain_fields.contains(&"C".to_string()),
629            "local interface field must not be uncertain"
630        );
631    }
632
633    #[test]
634    fn qualified_type_field_marked_uncertain() {
635        // A qualified type (e.g. `io.Reader`) from an external package cannot be
636        // resolved without go/types; the field must appear in uncertain_fields.
637        let src = "package p\ntype S struct { R io.Reader; N int32 }";
638        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
639        let l = &layouts[0];
640        assert!(
641            l.uncertain_fields.contains(&"R".to_string()),
642            "qualified-type field must be in uncertain_fields"
643        );
644        // Non-qualified field must not be uncertain
645        assert!(
646            !l.uncertain_fields.contains(&"N".to_string()),
647            "plain int32 field must not be uncertain"
648        );
649    }
650
651    #[test]
652    fn pointer_to_qualified_type_not_uncertain() {
653        // `*pkg.Type` is an explicit pointer — size is always pointer_size (8B).
654        // No need to flag it as uncertain since the pointer indirection makes the
655        // type's internal layout irrelevant for padding analysis.
656        let src = "package p\ntype S struct { P *io.Reader }";
657        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
658        let l = &layouts[0];
659        assert!(
660            !l.uncertain_fields.contains(&"P".to_string()),
661            "*qualified.Type pointer must not be uncertain"
662        );
663    }
664
665    // ── embedded struct support ───────────────────────────────────────────────
666
667    #[test]
668    fn embedded_struct_field_uses_type_name_as_field_name() {
669        // `Base` is an embedded field — Go uses the type name as the field name.
670        let src = r#"package p
671type Base struct { X int32 }
672type Derived struct {
673    Base
674    Y int32
675}
676"#;
677        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
678        let derived = layouts
679            .iter()
680            .find(|l| l.name == "Derived")
681            .expect("Derived");
682        // Must have a field named "Base"
683        assert!(
684            derived.fields.iter().any(|f| f.name == "Base"),
685            "embedded field should be named 'Base'"
686        );
687    }
688
689    #[test]
690    fn embedded_qualified_type_uses_unqualified_name() {
691        // `sync.Mutex` embedded — field name should be "Mutex"
692        let src = r#"package p
693type Safe struct {
694    sync.Mutex
695    Value int64
696}
697"#;
698        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
699        let l = layouts.iter().find(|l| l.name == "Safe").expect("Safe");
700        assert!(
701            l.fields.iter().any(|f| f.name == "Mutex"),
702            "embedded sync.Mutex should produce field named 'Mutex'"
703        );
704    }
705
706    #[test]
707    fn embedded_field_has_non_zero_size_from_resolution() {
708        // After lib.rs nested-struct resolution, Base's size should be filled in.
709        // We test via parse_source_str which triggers resolution.
710        let src = r#"package p
711type Inner struct { A int64; B int64 }
712type Outer struct {
713    Inner
714    C int32
715}
716"#;
717        use crate::{SourceLanguage, parse_source_str};
718        let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
719        let outer = layouts.iter().find(|l| l.name == "Outer").expect("Outer");
720        let inner_field = outer
721            .fields
722            .iter()
723            .find(|f| f.name == "Inner")
724            .expect("Inner field");
725        // Inner struct is 16 bytes (two int64s)
726        assert_eq!(
727            inner_field.size, 16,
728            "embedded Inner field should be resolved to 16 bytes"
729        );
730    }
731
732    #[test]
733    fn struct_with_no_embedded_fields_unaffected() {
734        let src = "package p\ntype S struct { A int32; B int64 }";
735        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
736        let l = &layouts[0];
737        assert_eq!(l.fields.len(), 2);
738        assert_eq!(l.fields[0].name, "A");
739        assert_eq!(l.fields[1].name, "B");
740    }
741
742    // ── bad weather: embedded fields ──────────────────────────────────────────
743
744    #[test]
745    fn embedded_unknown_type_falls_back_to_pointer_size() {
746        // If the embedded type is not defined in the file, size = pointer_size
747        let src = "package p\ntype S struct { external.Type\nX int32 }";
748        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
749        let l = layouts.iter().find(|l| l.name == "S").expect("S");
750        let emb = l
751            .fields
752            .iter()
753            .find(|f| f.name == "Type")
754            .expect("Type field");
755        // Falls back to pointer size (8 on x86_64) since type is unknown
756        assert_eq!(emb.size, 8);
757    }
758}