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    let mut is_generic = false;
151
152    for i in 0..node.child_count() {
153        let child = node.child(i)?;
154        match child.kind() {
155            "type_identifier" => name = Some(source[child.byte_range()].to_string()),
156            "struct_type" => struct_node = Some(child),
157            "type_parameter_list" => is_generic = true,
158            _ => {}
159        }
160    }
161
162    let name = name?;
163
164    if is_generic {
165        eprintln!(
166            "padlock: note: skipping '{name}' — generic struct \
167             (layout depends on type arguments; use binary analysis for accurate results)"
168        );
169        crate::record_skipped(
170            &name,
171            "generic struct — layout depends on type arguments; \
172             use binary analysis for accurate results",
173        );
174        return None;
175    }
176
177    let struct_node = struct_node?;
178    parse_struct_type(
179        source,
180        struct_node,
181        name,
182        arch,
183        source_line,
184        decl_start_byte,
185        local_interfaces,
186    )
187}
188
189fn parse_struct_type(
190    source: &str,
191    node: Node<'_>,
192    name: String,
193    arch: &'static ArchConfig,
194    source_line: u32,
195    decl_start_byte: usize,
196    local_interfaces: &HashSet<String>,
197) -> Option<StructLayout> {
198    let mut raw_fields: Vec<(String, String, Option<String>, u32)> = Vec::new();
199
200    for i in 0..node.child_count() {
201        let child = node.child(i)?;
202        if child.kind() == "field_declaration_list" {
203            for j in 0..child.child_count() {
204                let field_node = child.child(j)?;
205                if field_node.kind() == "field_declaration" {
206                    collect_field_declarations(source, field_node, &mut raw_fields);
207                }
208            }
209        }
210    }
211
212    if raw_fields.is_empty() {
213        return None;
214    }
215
216    // Simulate layout
217    let mut offset = 0usize;
218    let mut struct_align = 1usize;
219    let mut fields: Vec<Field> = Vec::new();
220    let mut uncertain_fields: Vec<String> = Vec::new();
221
222    for (fname, ty_name, guard, field_line) in raw_fields {
223        let (mut size, mut align) = go_type_size_align(&ty_name, arch);
224
225        // Override: a locally-declared interface type is a fat pointer (16B on 64-bit).
226        // go_type_size_align does not know about local names, so we patch here.
227        if local_interfaces.contains(ty_name.as_str()) {
228            size = arch.pointer_size * 2;
229            align = arch.pointer_size;
230        }
231
232        // Qualified types (e.g. `driver.Connector`, `io.Reader`) come from external
233        // packages. Without type information we cannot determine whether they are
234        // interfaces (16B fat pointer) or structs (arbitrary size). Flag them as
235        // uncertain so the output layer can warn the user.
236        let is_pointer = ty_name.starts_with('*');
237        let base_ty = ty_name.trim_start_matches('*');
238        if !is_pointer && base_ty.contains('.') {
239            uncertain_fields.push(fname.clone());
240        }
241
242        if align > 0 {
243            offset = offset.next_multiple_of(align);
244        }
245        struct_align = struct_align.max(align);
246        let access = if let Some(g) = guard {
247            AccessPattern::Concurrent {
248                guard: Some(g),
249                is_atomic: false,
250                is_annotated: true,
251            }
252        } else {
253            AccessPattern::Unknown
254        };
255        fields.push(Field {
256            name: fname,
257            ty: TypeInfo::Primitive {
258                name: ty_name,
259                size,
260                align,
261            },
262            offset,
263            size,
264            align,
265            source_file: None,
266            source_line: Some(field_line),
267            access,
268        });
269        offset += size;
270    }
271    if struct_align > 0 {
272        offset = offset.next_multiple_of(struct_align);
273    }
274
275    Some(StructLayout {
276        name,
277        total_size: offset,
278        align: struct_align,
279        fields,
280        source_file: None,
281        source_line: Some(source_line),
282        arch,
283        is_packed: false,
284        is_union: false,
285        is_repr_rust: false,
286        suppressed_findings: super::suppress::suppressed_from_preceding_source(
287            source,
288            decl_start_byte,
289        ),
290        uncertain_fields,
291    })
292}
293
294/// Extract a guard name from a Go field's trailing line comment.
295///
296/// Recognised forms (must appear after the field type on the same line):
297/// - `// padlock:guard=mu`
298/// - `// guarded_by: mu`
299/// - `// +checklocksprotects:mu` (gVisor-style)
300pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
301    let c = comment.trim();
302    // Strip leading `//` and optional whitespace
303    let body = c.strip_prefix("//").map(str::trim)?;
304
305    // padlock:guard=mu
306    if let Some(rest) = body.strip_prefix("padlock:guard=") {
307        let guard = rest.trim();
308        if !guard.is_empty() {
309            return Some(guard.to_string());
310        }
311    }
312    // guarded_by: mu
313    if let Some(rest) = body
314        .strip_prefix("guarded_by:")
315        .or_else(|| body.strip_prefix("guarded_by ="))
316    {
317        let guard = rest.trim();
318        if !guard.is_empty() {
319            return Some(guard.to_string());
320        }
321    }
322    // +checklocksprotects:mu (gVisor)
323    if let Some(rest) = body.strip_prefix("+checklocksprotects:") {
324        let guard = rest.trim();
325        if !guard.is_empty() {
326            return Some(guard.to_string());
327        }
328    }
329    None
330}
331
332/// Find the trailing line comment on the same source line as `node`.
333fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
334    // The node's end byte is just past the last token on the field line.
335    // Read the rest of that line from the source.
336    let end = node.end_byte();
337    if end >= source.len() {
338        return None;
339    }
340    let rest = &source[end..];
341    // Take only up to the next newline
342    let line = rest.lines().next().unwrap_or("");
343    // Look for `//` in that remainder
344    line.find("//").map(|pos| line[pos..].to_string())
345}
346
347fn collect_field_declarations(
348    source: &str,
349    node: Node<'_>,
350    out: &mut Vec<(String, String, Option<String>, u32)>,
351) {
352    // field_declaration: field_identifier+ type [comment]
353    // OR embedded type (anonymous field): TypeName [comment]
354    let mut field_names: Vec<String> = Vec::new();
355    let mut ty_text: Option<String> = None;
356    let field_line = node.start_position().row as u32 + 1;
357
358    for i in 0..node.child_count() {
359        if let Some(child) = node.child(i) {
360            match child.kind() {
361                "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
362                "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
363                | "map_type" | "channel_type" | "array_type" | "interface_type" => {
364                    ty_text = Some(source[child.byte_range()].trim().to_string());
365                }
366                _ => {}
367            }
368        }
369    }
370
371    let guard =
372        trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
373
374    if !field_names.is_empty() {
375        if let Some(ty) = ty_text {
376            // Normal named fields
377            for name in field_names {
378                out.push((name, ty.clone(), guard.clone(), field_line));
379            }
380        }
381    } else if let Some(ty) = ty_text {
382        // Embedded (anonymous) field: `sync.Mutex` or `Base`.
383        // Go field name is the unqualified type name.
384        // The nested-struct resolution pass in lib.rs will later fill in
385        // the correct size/align from other parsed struct layouts.
386        let simple_name = ty.split('.').next_back().unwrap_or(&ty).to_string();
387        out.push((simple_name, ty, guard, field_line));
388    }
389}
390
391// ── public API ────────────────────────────────────────────────────────────────
392
393pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
394    let mut parser = Parser::new();
395    parser.set_language(&tree_sitter_go::LANGUAGE.into())?;
396    let tree = parser
397        .parse(source, None)
398        .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
399    Ok(extract_structs(source, tree.root_node(), arch))
400}
401
402// ── tests ─────────────────────────────────────────────────────────────────────
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use padlock_core::arch::X86_64_SYSV;
408
409    #[test]
410    fn parse_simple_go_struct() {
411        let src = r#"
412package main
413type Point struct {
414    X int32
415    Y int32
416}
417"#;
418        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
419        assert_eq!(layouts.len(), 1);
420        assert_eq!(layouts[0].name, "Point");
421        assert_eq!(layouts[0].fields.len(), 2);
422    }
423
424    #[test]
425    fn go_layout_with_padding() {
426        let src = "package p\ntype T struct { A bool; B int64 }";
427        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
428        assert_eq!(layouts.len(), 1);
429        let l = &layouts[0];
430        assert_eq!(l.fields[0].offset, 0);
431        assert_eq!(l.fields[1].offset, 8); // bool (1) + 7 pad → 8
432    }
433
434    #[test]
435    fn go_string_is_two_words() {
436        let src = "package p\ntype S struct { Name string }";
437        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
438        assert_eq!(layouts[0].fields[0].size, 16); // ptr + len
439    }
440
441    // ── Go guard comment extraction ────────────────────────────────────────────
442
443    #[test]
444    fn extract_guard_padlock_form() {
445        assert_eq!(
446            extract_guard_from_go_comment("// padlock:guard=mu"),
447            Some("mu".to_string())
448        );
449    }
450
451    #[test]
452    fn extract_guard_guarded_by_form() {
453        assert_eq!(
454            extract_guard_from_go_comment("// guarded_by: counter_lock"),
455            Some("counter_lock".to_string())
456        );
457    }
458
459    #[test]
460    fn extract_guard_checklocksprotects_form() {
461        assert_eq!(
462            extract_guard_from_go_comment("// +checklocksprotects:mu"),
463            Some("mu".to_string())
464        );
465    }
466
467    #[test]
468    fn extract_guard_no_match_returns_none() {
469        assert!(extract_guard_from_go_comment("// just a comment").is_none());
470        assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
471    }
472
473    #[test]
474    fn go_struct_padlock_guard_annotation_sets_concurrent() {
475        let src = r#"package p
476type Cache struct {
477    Readers int64 // padlock:guard=mu
478    Writers int64 // padlock:guard=other_mu
479    Mu      sync.Mutex
480}
481"#;
482        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
483        let l = &layouts[0];
484        // Readers and Writers should be Concurrent with different guards
485        if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
486            assert_eq!(guard.as_deref(), Some("mu"));
487        } else {
488            panic!(
489                "expected Concurrent for Readers, got {:?}",
490                l.fields[0].access
491            );
492        }
493        if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
494            assert_eq!(guard.as_deref(), Some("other_mu"));
495        } else {
496            panic!(
497                "expected Concurrent for Writers, got {:?}",
498                l.fields[1].access
499            );
500        }
501    }
502
503    #[test]
504    fn go_struct_different_guards_same_cache_line_is_false_sharing() {
505        let src = r#"package p
506type HotPath struct {
507    Readers int64 // padlock:guard=lock_a
508    Writers int64 // padlock:guard=lock_b
509}
510"#;
511        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
512        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
513            &layouts[0]
514        ));
515    }
516
517    #[test]
518    fn go_struct_same_guard_is_not_false_sharing() {
519        let src = r#"package p
520type Safe struct {
521    A int64 // padlock:guard=mu
522    B int64 // padlock:guard=mu
523}
524"#;
525        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
526        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
527            &layouts[0]
528        ));
529    }
530
531    // ── interface{} / any sizing ───────────────────────────────────────────────
532
533    #[test]
534    fn interface_field_is_two_words() {
535        // interface{} is a fat pointer: (type pointer, data pointer) = 2×pointer
536        let src = "package p\ntype S struct { V interface{} }";
537        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
538        assert_eq!(layouts[0].fields[0].size, 16); // 2 × 8B on x86-64
539        assert_eq!(layouts[0].fields[0].align, 8);
540    }
541
542    #[test]
543    fn any_field_is_two_words() {
544        // `any` is an alias for `interface{}` since Go 1.18
545        let src = "package p\ntype S struct { V any }";
546        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
547        assert_eq!(layouts[0].fields[0].size, 16); // 2 × 8B on x86-64
548        assert_eq!(layouts[0].fields[0].align, 8);
549    }
550
551    #[test]
552    fn interface_field_same_size_as_error() {
553        // `error` was already two-word; interface{} must match
554        let src_iface = "package p\ntype S struct { V interface{} }";
555        let src_err = "package p\ntype S struct { V error }";
556        let iface = parse_go(src_iface, &X86_64_SYSV).unwrap();
557        let err = parse_go(src_err, &X86_64_SYSV).unwrap();
558        assert_eq!(iface[0].fields[0].size, err[0].fields[0].size);
559    }
560
561    #[test]
562    fn struct_with_mixed_interface_and_ints_has_correct_layout() {
563        // interface{} at offset 0 (size 16, align 8) then int64 at offset 16
564        let src = "package p\ntype S struct { V interface{}; N int64 }";
565        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
566        let l = &layouts[0];
567        assert_eq!(l.fields[0].offset, 0);
568        assert_eq!(l.fields[0].size, 16);
569        assert_eq!(l.fields[1].offset, 16);
570        assert_eq!(l.total_size, 24);
571    }
572
573    #[test]
574    fn inline_interface_with_methods_is_two_words() {
575        // An anonymous interface with methods (e.g. `interface{ Close() error }`) is a
576        // two-word fat pointer — same as `interface{}`.  The tree-sitter node kind is
577        // `interface_type` in both cases so the `ty.starts_with("interface")` match handles
578        // all inline interface bodies.
579        let src = "package p\ntype S struct { Conn interface{ Close() error } }";
580        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
581        assert_eq!(layouts[0].fields[0].size, 16);
582        assert_eq!(layouts[0].fields[0].align, 8);
583    }
584
585    #[test]
586    fn named_cross_package_interface_falls_back_to_pointer_size() {
587        // Named interfaces from other packages (driver.Connector, io.ReadCloser, …)
588        // appear in the AST as `qualified_type` nodes with text like "driver.Connector".
589        // Without go/types resolution we cannot distinguish an interface from a concrete
590        // struct, so they fall back to pointer_size (8B on x86-64) — the same as an
591        // opaque pointer.  This is a known source-analysis limitation; binary (DWARF)
592        // analysis always returns the correct compiler layout.
593        let src = "package p\ntype DB struct { connector driver.Connector }";
594        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
595        // Known limitation: reports 8B, not the actual 16B.
596        assert_eq!(
597            layouts[0].fields[0].size, 8,
598            "named cross-package interface falls back to pointer_size (known limitation)"
599        );
600        // The field must be flagged as uncertain so the output layer can warn the user.
601        assert!(
602            layouts[0]
603                .uncertain_fields
604                .contains(&"connector".to_string()),
605            "qualified-type field should be in uncertain_fields"
606        );
607    }
608
609    // ── local interface type resolution ───────────────────────────────────────
610
611    #[test]
612    fn local_interface_field_is_fat_pointer() {
613        // A named interface declared in the same file must be sized as a two-word
614        // fat pointer (16B on 64-bit), not as a single pointer (8B).
615        let src = r#"package p
616type Reader interface {
617    Read(p []byte) (n int, err error)
618}
619type Buf struct {
620    R Reader
621    N int32
622}
623"#;
624        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
625        let l = layouts.iter().find(|l| l.name == "Buf").expect("Buf");
626        let r = l.fields.iter().find(|f| f.name == "R").expect("R field");
627        assert_eq!(
628            r.size, 16,
629            "local interface must be sized as 16B fat pointer"
630        );
631        assert_eq!(r.align, 8);
632    }
633
634    #[test]
635    fn local_interface_field_not_marked_uncertain() {
636        // A locally-declared interface is resolved; it must NOT appear in uncertain_fields.
637        let src = r#"package p
638type Closer interface { Close() error }
639type File struct { C Closer }
640"#;
641        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
642        let l = layouts.iter().find(|l| l.name == "File").expect("File");
643        assert!(
644            !l.uncertain_fields.contains(&"C".to_string()),
645            "local interface field must not be uncertain"
646        );
647    }
648
649    #[test]
650    fn qualified_type_field_marked_uncertain() {
651        // A qualified type (e.g. `io.Reader`) from an external package cannot be
652        // resolved without go/types; the field must appear in uncertain_fields.
653        let src = "package p\ntype S struct { R io.Reader; N int32 }";
654        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
655        let l = &layouts[0];
656        assert!(
657            l.uncertain_fields.contains(&"R".to_string()),
658            "qualified-type field must be in uncertain_fields"
659        );
660        // Non-qualified field must not be uncertain
661        assert!(
662            !l.uncertain_fields.contains(&"N".to_string()),
663            "plain int32 field must not be uncertain"
664        );
665    }
666
667    #[test]
668    fn pointer_to_qualified_type_not_uncertain() {
669        // `*pkg.Type` is an explicit pointer — size is always pointer_size (8B).
670        // No need to flag it as uncertain since the pointer indirection makes the
671        // type's internal layout irrelevant for padding analysis.
672        let src = "package p\ntype S struct { P *io.Reader }";
673        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
674        let l = &layouts[0];
675        assert!(
676            !l.uncertain_fields.contains(&"P".to_string()),
677            "*qualified.Type pointer must not be uncertain"
678        );
679    }
680
681    // ── embedded struct support ───────────────────────────────────────────────
682
683    #[test]
684    fn embedded_struct_field_uses_type_name_as_field_name() {
685        // `Base` is an embedded field — Go uses the type name as the field name.
686        let src = r#"package p
687type Base struct { X int32 }
688type Derived struct {
689    Base
690    Y int32
691}
692"#;
693        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
694        let derived = layouts
695            .iter()
696            .find(|l| l.name == "Derived")
697            .expect("Derived");
698        // Must have a field named "Base"
699        assert!(
700            derived.fields.iter().any(|f| f.name == "Base"),
701            "embedded field should be named 'Base'"
702        );
703    }
704
705    #[test]
706    fn embedded_qualified_type_uses_unqualified_name() {
707        // `sync.Mutex` embedded — field name should be "Mutex"
708        let src = r#"package p
709type Safe struct {
710    sync.Mutex
711    Value int64
712}
713"#;
714        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
715        let l = layouts.iter().find(|l| l.name == "Safe").expect("Safe");
716        assert!(
717            l.fields.iter().any(|f| f.name == "Mutex"),
718            "embedded sync.Mutex should produce field named 'Mutex'"
719        );
720    }
721
722    #[test]
723    fn embedded_field_has_non_zero_size_from_resolution() {
724        // After lib.rs nested-struct resolution, Base's size should be filled in.
725        // We test via parse_source_str which triggers resolution.
726        let src = r#"package p
727type Inner struct { A int64; B int64 }
728type Outer struct {
729    Inner
730    C int32
731}
732"#;
733        use crate::{SourceLanguage, parse_source_str};
734        let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
735        let outer = layouts.iter().find(|l| l.name == "Outer").expect("Outer");
736        let inner_field = outer
737            .fields
738            .iter()
739            .find(|f| f.name == "Inner")
740            .expect("Inner field");
741        // Inner struct is 16 bytes (two int64s)
742        assert_eq!(
743            inner_field.size, 16,
744            "embedded Inner field should be resolved to 16 bytes"
745        );
746    }
747
748    #[test]
749    fn struct_with_no_embedded_fields_unaffected() {
750        let src = "package p\ntype S struct { A int32; B int64 }";
751        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
752        let l = &layouts[0];
753        assert_eq!(l.fields.len(), 2);
754        assert_eq!(l.fields[0].name, "A");
755        assert_eq!(l.fields[1].name, "B");
756    }
757
758    // ── Go generics ───────────────────────────────────────────────────────────
759
760    #[test]
761    fn go_generic_struct_is_skipped() {
762        // Generic structs cannot be sized without type instantiation.
763        let src = "package p\ntype Pair[T any] struct { First T; Second T }";
764        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
765        assert!(
766            layouts.iter().all(|l| l.name != "Pair"),
767            "generic struct must be skipped"
768        );
769    }
770
771    #[test]
772    fn go_concrete_struct_alongside_generic_is_parsed() {
773        // The generic is skipped but a concrete sibling struct is still parsed.
774        let src = "package p\ntype Pair[T any] struct { First T }\ntype Point struct { X int32; Y int32 }";
775        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
776        assert!(
777            layouts.iter().all(|l| l.name != "Pair"),
778            "Pair must be skipped"
779        );
780        assert!(
781            layouts.iter().any(|l| l.name == "Point"),
782            "Point must be parsed"
783        );
784    }
785
786    // ── bad weather: embedded fields ──────────────────────────────────────────
787
788    #[test]
789    fn embedded_unknown_type_falls_back_to_pointer_size() {
790        // If the embedded type is not defined in the file, size = pointer_size
791        let src = "package p\ntype S struct { external.Type\nX int32 }";
792        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
793        let l = layouts.iter().find(|l| l.name == "S").expect("S");
794        let emb = l
795            .fields
796            .iter()
797            .find(|f| f.name == "Type")
798            .expect("Type field");
799        // Falls back to pointer size (8 on x86_64) since type is unknown
800        assert_eq!(emb.size, 8);
801    }
802}