layout_audit/dwarf/
context.rs

1use crate::error::{Error, Result};
2use crate::loader::{DwarfSlice, LoadedDwarf};
3use crate::types::{MemberLayout, SourceLocation, StructLayout};
4use gimli::{AttributeValue, DebuggingInformationEntry, Dwarf, Unit};
5
6use super::TypeResolver;
7use super::expr::{evaluate_member_offset, try_simple_offset};
8use super::{debug_info_ref_to_unit_offset, read_u64_from_attr};
9
10/// Prefixes for Go runtime internal types that should be filtered.
11/// Grouped by category for maintainability.
12/// Note: The actual filtering uses an optimized first-byte match in `is_go_internal_type()`.
13/// This constant serves as documentation and is used by tests to verify consistency.
14#[cfg(test)]
15const GO_INTERNAL_PREFIXES: &[&str] = &[
16    // Runtime and standard library
17    "runtime.",
18    "runtime/",
19    "internal/",
20    "reflect.",
21    "sync.",
22    "sync/",
23    "syscall.",
24    "unsafe.",
25    // Compiler-generated prefixes
26    "go.",
27    "go:",
28    // Type descriptors
29    "type:",
30    "type..",
31    "type.*[",
32    // Map/channel internals
33    "hash<",
34    "bucket<",
35    "hmap",
36    "hchan",
37    "waitq<",
38    "sudog",
39    // Interface dispatch
40    "itab",
41    "iface",
42    "eface",
43    // Function values
44    "funcval",
45    // Generics (Go 1.18+)
46    "go.shape.",
47    "groupReference<",
48    // Stack-related runtime types
49    "stackObject",
50    "stackScan",
51    "stackfreelist",
52    "stkframe",
53    // Slice representations
54    "[]",
55    "[]*",
56    // No-algorithm types
57    "noalg.",
58];
59
60/// Check if a type name is a Go runtime internal type that should be filtered.
61/// These are compiler/runtime-generated types not useful for layout analysis.
62pub fn is_go_internal_type(name: &str) -> bool {
63    // Fast path: check first byte to filter by common prefix groups
64    let Some(&first) = name.as_bytes().first() else {
65        return false; // Empty string is not internal
66    };
67
68    // Check prefixes grouped by first byte for performance
69    let matches_prefix = match first {
70        b'r' => {
71            name.starts_with("runtime.")
72                || name.starts_with("runtime/")
73                || name.starts_with("reflect.")
74        }
75        b's' => {
76            name.starts_with("sync.")
77                || name.starts_with("sync/")
78                || name.starts_with("syscall.")
79                || name.starts_with("sudog")
80                || name.starts_with("stackObject")
81                || name.starts_with("stackScan")
82                || name.starts_with("stackfreelist")
83                || name.starts_with("stkframe")
84        }
85        b'i' => {
86            name.starts_with("internal/") || name.starts_with("itab") || name.starts_with("iface")
87        }
88        b'g' => {
89            name.starts_with("go.")
90                || name.starts_with("go:")
91                || name.starts_with("groupReference<")
92        }
93        b't' => {
94            name.starts_with("type:") || name.starts_with("type..") || name.starts_with("type.*[")
95        }
96        b'u' => name.starts_with("unsafe."),
97        b'h' => name.starts_with("hash<") || name.starts_with("hmap") || name.starts_with("hchan"),
98        b'b' => name.starts_with("bucket<"),
99        b'w' => name.starts_with("waitq<"),
100        b'e' => name.starts_with("eface"),
101        b'f' => name.starts_with("funcval"),
102        b'n' => name.starts_with("noalg."),
103        b'[' => name.starts_with("[]") || name.starts_with("[]*"),
104        _ => false,
105    };
106
107    // Middle dot check as fallback (Go uses · for unexported identifiers)
108    matches_prefix || name.contains('\u{00B7}')
109}
110
111/// Returns the list of Go internal type prefixes (for testing).
112#[cfg(test)]
113pub(crate) fn go_internal_prefixes() -> &'static [&'static str] {
114    GO_INTERNAL_PREFIXES
115}
116
117pub struct DwarfContext<'a> {
118    dwarf: &'a Dwarf<DwarfSlice<'a>>,
119    address_size: u8,
120    endian: gimli::RunTimeEndian,
121}
122
123impl<'a> DwarfContext<'a> {
124    pub fn new(loaded: &'a LoadedDwarf<'a>) -> Self {
125        Self { dwarf: &loaded.dwarf, address_size: loaded.address_size, endian: loaded.endian }
126    }
127
128    /// Find all structs in the binary.
129    ///
130    /// - `filter`: Optional substring filter for struct names
131    /// - `include_go_runtime`: If false, Go runtime internal types are filtered out
132    pub fn find_structs(
133        &self,
134        filter: Option<&str>,
135        include_go_runtime: bool,
136    ) -> Result<Vec<StructLayout>> {
137        let mut structs = Vec::new();
138        let mut units = self.dwarf.units();
139
140        while let Some(header) =
141            units.next().map_err(|e| Error::Dwarf(format!("Failed to read unit header: {}", e)))?
142        {
143            let unit = self
144                .dwarf
145                .unit(header)
146                .map_err(|e| Error::Dwarf(format!("Failed to parse unit: {}", e)))?;
147
148            self.process_unit(&unit, filter, include_go_runtime, &mut structs)?;
149        }
150
151        // DWARF can contain duplicate identical type entries (e.g., across units or due to
152        // language/compiler quirks). Deduplicate exact duplicates to avoid double-counting in
153        // `check` and unstable matching in `diff`.
154        // Use enumerated index as tiebreaker for stable deduplication (Rust's sort_by is unstable).
155        let mut with_fp: Vec<(StructFingerprint, usize, StructLayout)> =
156            structs.into_iter().enumerate().map(|(i, s)| (struct_fingerprint(&s), i, s)).collect();
157        with_fp.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
158        with_fp.dedup_by(|a, b| a.0 == b.0);
159
160        Ok(with_fp.into_iter().map(|(_, _, s)| s).collect())
161    }
162
163    fn process_unit(
164        &self,
165        unit: &Unit<DwarfSlice<'a>>,
166        filter: Option<&str>,
167        include_go_runtime: bool,
168        structs: &mut Vec<StructLayout>,
169    ) -> Result<()> {
170        let mut type_resolver = TypeResolver::new(self.dwarf, unit, self.address_size);
171        let mut entries = unit.entries();
172
173        while let Some((_, entry)) =
174            entries.next_dfs().map_err(|e| Error::Dwarf(format!("Failed to read DIE: {}", e)))?
175        {
176            if !matches!(entry.tag(), gimli::DW_TAG_structure_type | gimli::DW_TAG_class_type) {
177                continue;
178            }
179
180            if let Some(layout) = self.process_struct_entry(
181                unit,
182                entry,
183                filter,
184                include_go_runtime,
185                &mut type_resolver,
186            )? {
187                structs.push(layout);
188            }
189        }
190
191        Ok(())
192    }
193
194    fn process_struct_entry(
195        &self,
196        unit: &Unit<DwarfSlice<'a>>,
197        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
198        filter: Option<&str>,
199        include_go_runtime: bool,
200        type_resolver: &mut TypeResolver<'a, '_>,
201    ) -> Result<Option<StructLayout>> {
202        // Use consolidated helper for attribute extraction (see read_u64_from_attr).
203        let Some(size) =
204            read_u64_from_attr(entry.attr_value(gimli::DW_AT_byte_size).ok().flatten())
205        else {
206            return Ok(None); // Forward declaration or no size
207        };
208
209        let name = self.get_die_name(unit, entry)?;
210        let name = match name {
211            Some(n) if !n.starts_with("__") => n, // Skip compiler-generated
212            None => return Ok(None),              // Anonymous struct
213            _ => return Ok(None),
214        };
215
216        // Filter Go runtime internal types unless explicitly included
217        if !include_go_runtime && is_go_internal_type(&name) {
218            return Ok(None);
219        }
220
221        if filter.is_some_and(|f| !name.contains(f)) {
222            return Ok(None);
223        }
224
225        let alignment = read_u64_from_attr(entry.attr_value(gimli::DW_AT_alignment).ok().flatten());
226
227        let mut layout = StructLayout::new(name, size, alignment);
228        layout.source_location = self.get_source_location(unit, entry)?;
229        layout.members = self.extract_members(unit, entry, type_resolver)?;
230
231        Ok(Some(layout))
232    }
233
234    fn extract_members(
235        &self,
236        unit: &Unit<DwarfSlice<'a>>,
237        struct_entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
238        type_resolver: &mut TypeResolver<'a, '_>,
239    ) -> Result<Vec<MemberLayout>> {
240        let mut members = Vec::new();
241        let mut tree = unit
242            .entries_tree(Some(struct_entry.offset()))
243            .map_err(|e| Error::Dwarf(format!("Failed to create entries tree: {}", e)))?;
244
245        let root =
246            tree.root().map_err(|e| Error::Dwarf(format!("Failed to get tree root: {}", e)))?;
247
248        let mut children = root.children();
249        while let Some(child) = children
250            .next()
251            .map_err(|e| Error::Dwarf(format!("Failed to iterate children: {}", e)))?
252        {
253            let entry = child.entry();
254            match entry.tag() {
255                gimli::DW_TAG_member => {
256                    if let Some(member) = self.process_member(unit, entry, type_resolver)? {
257                        members.push(member);
258                    }
259                }
260                gimli::DW_TAG_inheritance => {
261                    if let Some(member) = self.process_inheritance(unit, entry, type_resolver)? {
262                        members.push(member);
263                    }
264                }
265                _ => {}
266            }
267        }
268
269        members.sort_by_key(|m| m.offset.unwrap_or(u64::MAX));
270        Ok(members)
271    }
272
273    /// Resolve type information from a DW_AT_type attribute.
274    /// Returns (type_name, size, is_atomic) or a default for unknown types.
275    fn resolve_type_attr(
276        &self,
277        unit: &Unit<DwarfSlice<'a>>,
278        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
279        type_resolver: &mut TypeResolver<'a, '_>,
280    ) -> Result<(String, Option<u64>, bool)> {
281        match entry.attr_value(gimli::DW_AT_type) {
282            Ok(Some(AttributeValue::UnitRef(type_offset))) => {
283                type_resolver.resolve_type(type_offset)
284            }
285            Ok(Some(AttributeValue::DebugInfoRef(debug_info_offset))) => {
286                // Use shared helper for cross-unit reference conversion.
287                if let Some(unit_offset) =
288                    debug_info_ref_to_unit_offset(debug_info_offset, &unit.header)
289                {
290                    type_resolver.resolve_type(unit_offset)
291                } else {
292                    Ok(("unknown".to_string(), None, false))
293                }
294            }
295            _ => Ok(("unknown".to_string(), None, false)),
296        }
297    }
298
299    fn process_inheritance(
300        &self,
301        unit: &Unit<DwarfSlice<'a>>,
302        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
303        type_resolver: &mut TypeResolver<'a, '_>,
304    ) -> Result<Option<MemberLayout>> {
305        let offset = self.get_member_offset(unit, entry)?;
306        let (type_name, size, is_atomic) = self.resolve_type_attr(unit, entry, type_resolver)?;
307
308        let name = format!("<base: {}>", type_name);
309        Ok(Some(MemberLayout::new(name, type_name, offset, size).with_atomic(is_atomic)))
310    }
311
312    fn process_member(
313        &self,
314        unit: &Unit<DwarfSlice<'a>>,
315        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
316        type_resolver: &mut TypeResolver<'a, '_>,
317    ) -> Result<Option<MemberLayout>> {
318        let name = self.get_die_name(unit, entry)?.unwrap_or_else(|| "<anonymous>".to_string());
319        let (type_name, size, is_atomic) = self.resolve_type_attr(unit, entry, type_resolver)?;
320
321        let offset = self.get_member_offset(unit, entry)?;
322
323        let mut member = MemberLayout::new(name, type_name, offset, size).with_atomic(is_atomic);
324
325        let bit_size = read_u64_from_attr(entry.attr_value(gimli::DW_AT_bit_size).ok().flatten());
326        let dwarf5_data_bit_offset =
327            read_u64_from_attr(entry.attr_value(gimli::DW_AT_data_bit_offset).ok().flatten());
328        let dwarf4_bit_offset =
329            read_u64_from_attr(entry.attr_value(gimli::DW_AT_bit_offset).ok().flatten());
330
331        member.bit_size = bit_size;
332
333        if let Some(bit_size) = bit_size
334            && let Some(storage_bytes) = member.size
335            && storage_bytes > 0
336        {
337            let storage_bits = storage_bytes.saturating_mul(8);
338
339            // Determine the containing storage unit byte offset for this bitfield.
340            // Prefer DW_AT_data_member_location when present. If absent, infer the
341            // storage unit start by aligning the absolute DW_AT_data_bit_offset down
342            // to the storage unit size.
343            let container_offset = member.offset.or_else(|| {
344                let data_bit_offset = dwarf5_data_bit_offset?;
345                let start_byte = data_bit_offset.checked_div(8)?;
346                // storage_bytes > 0 is guaranteed by the outer if-let guard
347                start_byte.checked_div(storage_bytes)?.checked_mul(storage_bytes)
348            });
349
350            if member.offset.is_none() {
351                member.offset = container_offset;
352            }
353
354            // Compute bit offset within the containing storage unit.
355            if let Some(container_offset) = container_offset {
356                if let Some(data_bit_offset) = dwarf5_data_bit_offset {
357                    // Use checked_mul to avoid overflow for large container offsets.
358                    if let Some(container_bits) = container_offset.checked_mul(8) {
359                        member.bit_offset = Some(data_bit_offset.saturating_sub(container_bits));
360                    }
361                } else if let Some(raw_bit_offset) = dwarf4_bit_offset {
362                    // Use checked_add to avoid overflow in boundary check.
363                    if let Some(end_bit) = raw_bit_offset.checked_add(bit_size) {
364                        if end_bit <= storage_bits {
365                            let bit_offset = match self.endian {
366                                gimli::RunTimeEndian::Little => {
367                                    storage_bits - raw_bit_offset - bit_size
368                                }
369                                gimli::RunTimeEndian::Big => raw_bit_offset,
370                            };
371                            member.bit_offset = Some(bit_offset);
372                        }
373                    }
374                }
375            }
376        }
377
378        Ok(Some(member))
379    }
380
381    fn get_member_offset(
382        &self,
383        unit: &Unit<DwarfSlice<'a>>,
384        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
385    ) -> Result<Option<u64>> {
386        match entry.attr_value(gimli::DW_AT_data_member_location) {
387            Ok(Some(AttributeValue::Udata(offset))) => Ok(Some(offset)),
388            Ok(Some(AttributeValue::Data1(offset))) => Ok(Some(offset as u64)),
389            Ok(Some(AttributeValue::Data2(offset))) => Ok(Some(offset as u64)),
390            Ok(Some(AttributeValue::Data4(offset))) => Ok(Some(offset as u64)),
391            Ok(Some(AttributeValue::Data8(offset))) => Ok(Some(offset)),
392            Ok(Some(AttributeValue::Sdata(offset))) if offset >= 0 => Ok(Some(offset as u64)),
393            Ok(Some(AttributeValue::Exprloc(expr))) => {
394                // Try simple constant extraction first (fast path)
395                if let Some(offset) = try_simple_offset(expr, unit.encoding()) {
396                    return Ok(Some(offset));
397                }
398                // Fall back to full expression evaluation
399                evaluate_member_offset(expr, unit.encoding())
400            }
401            Ok(None) => Ok(None), // Missing offset - don't assume 0 (bitfields, packed structs)
402            _ => Ok(None),
403        }
404    }
405
406    fn get_die_name(
407        &self,
408        unit: &Unit<DwarfSlice<'a>>,
409        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
410    ) -> Result<Option<String>> {
411        match entry.attr_value(gimli::DW_AT_name) {
412            Ok(Some(attr)) => {
413                let name = self
414                    .dwarf
415                    .attr_string(unit, attr)
416                    .map_err(|e| Error::Dwarf(format!("Failed to read name: {}", e)))?;
417                Ok(Some(name.to_string_lossy().into_owned()))
418            }
419            Ok(None) => Ok(None),
420            Err(e) => Err(Error::Dwarf(format!("Failed to read name attribute: {}", e))),
421        }
422    }
423
424    fn get_source_location(
425        &self,
426        unit: &Unit<DwarfSlice<'a>>,
427        entry: &DebuggingInformationEntry<DwarfSlice<'a>>,
428    ) -> Result<Option<SourceLocation>> {
429        let Some(file_index) =
430            read_u64_from_attr(entry.attr_value(gimli::DW_AT_decl_file).ok().flatten())
431        else {
432            return Ok(None);
433        };
434        let Some(line) =
435            read_u64_from_attr(entry.attr_value(gimli::DW_AT_decl_line).ok().flatten())
436        else {
437            return Ok(None);
438        };
439
440        // Try to resolve the file name from the line program header
441        let file_name = self.resolve_file_name(unit, file_index).unwrap_or_else(|| {
442            // Fall back to file index if resolution fails
443            format!("file#{}", file_index)
444        });
445
446        Ok(Some(SourceLocation { file: file_name, line }))
447    }
448
449    /// Resolve a file index to an actual file path using the .debug_line section.
450    fn resolve_file_name(&self, unit: &Unit<DwarfSlice<'a>>, file_index: u64) -> Option<String> {
451        // Get the line program for this unit (borrow instead of clone for efficiency)
452        let line_program = unit.line_program.as_ref()?;
453
454        let header = line_program.header();
455
456        // File indices in DWARF are 1-based (0 means no file in DWARF 4, or the compilation
457        // directory in DWARF 5). We need to handle both cases.
458        let file = header.file(file_index)?;
459
460        // Get the file name
461        let file_name =
462            self.dwarf.attr_string(unit, file.path_name()).ok()?.to_string_lossy().into_owned();
463
464        // Try to get the directory
465        if let Some(dir) = file.directory(header) {
466            if let Ok(dir_str) = self.dwarf.attr_string(unit, dir) {
467                let dir_name = dir_str.to_string_lossy();
468                if !dir_name.is_empty() {
469                    // Combine directory and file name
470                    return Some(format!("{}/{}", dir_name, file_name));
471                }
472            }
473        }
474
475        Some(file_name)
476    }
477}
478
479#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
480struct StructFingerprint {
481    name: String,
482    size: u64,
483    alignment: Option<u64>,
484    source: Option<(String, u64)>,
485    members: Vec<MemberFingerprint>,
486}
487
488#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
489struct MemberFingerprint {
490    name: String,
491    type_name: String,
492    offset: Option<u64>,
493    size: Option<u64>,
494    bit_offset: Option<u64>,
495    bit_size: Option<u64>,
496    is_atomic: bool,
497}
498
499fn struct_fingerprint(s: &StructLayout) -> StructFingerprint {
500    StructFingerprint {
501        name: s.name.clone(),
502        size: s.size,
503        alignment: s.alignment,
504        source: s.source_location.as_ref().map(|l| (l.file.clone(), l.line)),
505        members: s
506            .members
507            .iter()
508            .map(|m| MemberFingerprint {
509                name: m.name.clone(),
510                type_name: m.type_name.clone(),
511                offset: m.offset,
512                size: m.size,
513                bit_offset: m.bit_offset,
514                bit_size: m.bit_size,
515                is_atomic: m.is_atomic,
516            })
517            .collect(),
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_is_go_internal_type() {
527        // Runtime packages - should be filtered
528        assert!(is_go_internal_type("runtime.g"));
529        assert!(is_go_internal_type("runtime.m"));
530        assert!(is_go_internal_type("runtime.stack"));
531        assert!(is_go_internal_type("runtime/internal/atomic.Uint64"));
532        assert!(is_go_internal_type("internal/abi.Type"));
533        assert!(is_go_internal_type("reflect.Value"));
534        assert!(is_go_internal_type("sync.Mutex"));
535        assert!(is_go_internal_type("sync/atomic.Int64"));
536        assert!(is_go_internal_type("syscall.Stat_t"));
537        assert!(is_go_internal_type("unsafe.Pointer"));
538
539        // Type descriptors - should be filtered
540        assert!(is_go_internal_type("type:main.MyStruct"));
541        assert!(is_go_internal_type("type..hash.main.MyStruct"));
542        assert!(is_go_internal_type("type.*[10]int"));
543
544        // Map/channel internals - should be filtered
545        assert!(is_go_internal_type("hmap"));
546        assert!(is_go_internal_type("hchan"));
547        assert!(is_go_internal_type("hchan<int>"));
548        assert!(is_go_internal_type("waitq<bool>"));
549        assert!(is_go_internal_type("hash<string,int>"));
550        assert!(is_go_internal_type("bucket<string,int>"));
551
552        // Interface dispatch - should be filtered
553        assert!(is_go_internal_type("itab"));
554        assert!(is_go_internal_type("iface"));
555        assert!(is_go_internal_type("eface"));
556
557        // Stack-related runtime types - should be filtered
558        assert!(is_go_internal_type("stackObject"));
559        assert!(is_go_internal_type("stackScan"));
560        assert!(is_go_internal_type("stkframe"));
561
562        // Generic shape types - should be filtered
563        assert!(is_go_internal_type("go.shape.string"));
564        assert!(is_go_internal_type("groupReference<int32,unsafe.Pointer>"));
565
566        // Slice representations - should be filtered
567        assert!(is_go_internal_type("[]int"));
568        assert!(is_go_internal_type("[]*runtime.g"));
569
570        // noalg types - should be filtered
571        assert!(is_go_internal_type("noalg.map.group[string]bool"));
572
573        // Compiler-generated - should be filtered
574        assert!(is_go_internal_type("go:itab.*os.File,io.Reader"));
575
576        // Middle dot (·) - Go unexported identifiers - should be filtered
577        assert!(is_go_internal_type("pkg\u{00B7}unexported"));
578        assert!(is_go_internal_type("(*T)\u{00B7}method"));
579        assert!(is_go_internal_type("main\u{00B7}init"));
580
581        // Should NOT be filtered (user types)
582        assert!(!is_go_internal_type("main.Order"));
583        assert!(!is_go_internal_type("main.Config"));
584        assert!(!is_go_internal_type("main.PoorlyAligned"));
585        assert!(!is_go_internal_type("mypackage.MyStruct"));
586        assert!(!is_go_internal_type("github.com/user/pkg.Type"));
587        assert!(!is_go_internal_type("Order"));
588        assert!(!is_go_internal_type("Config"));
589        // User types that might look like internals but aren't
590        assert!(!is_go_internal_type("MyStack"));
591        assert!(!is_go_internal_type("StackFrame"));
592        assert!(!is_go_internal_type("HashMap"));
593        // False-positive edge cases - contain keywords but missing separators
594        assert!(!is_go_internal_type("runtimeConfig")); // No dot after "runtime"
595        assert!(!is_go_internal_type("syncronizer")); // Contains "sync" but not "sync."
596        assert!(!is_go_internal_type("go_util")); // Underscore, not dot/colon
597        assert!(!is_go_internal_type("MyStackFrame")); // Contains "Stack" but not at start
598        assert!(!is_go_internal_type("reflector")); // Contains "reflect" but not "reflect."
599        assert!(!is_go_internal_type("internalConfig")); // Starts with "internal" but not "internal/"
600
601        // Fixed-size arrays should NOT be filtered (only slices)
602        assert!(!is_go_internal_type("[5]int")); // Fixed array, not slice
603        assert!(!is_go_internal_type("[10]MyStruct")); // User array type
604        assert!(!is_go_internal_type("[3]*MyStruct")); // Array of pointers
605
606        // Generic edge cases - verify exact patterns required
607        assert!(!is_go_internal_type("groupReference")); // No angle bracket (needs "groupReference<")
608
609        // Empty string edge case
610        assert!(!is_go_internal_type("")); // Empty should not crash or filter
611    }
612
613    #[test]
614    fn test_go_internal_prefixes_consistency() {
615        // Verify all prefixes in the constant are covered by the match statement
616        let prefixes = go_internal_prefixes();
617        assert!(prefixes.len() >= 30, "Should have comprehensive prefix list");
618
619        // Each prefix should correctly filter matching types
620        // This catches if a prefix is added to the constant but not the match
621        for prefix in prefixes {
622            let test_name = format!("{}TestType", prefix);
623            assert!(
624                is_go_internal_type(&test_name),
625                "Prefix '{}' should filter '{}' - check match statement coverage",
626                prefix,
627                test_name
628            );
629        }
630    }
631
632    #[test]
633    fn test_go_internal_type_edge_cases() {
634        // Single-character names matching first byte should NOT be filtered
635        assert!(!is_go_internal_type("r")); // Not "runtime."
636        assert!(!is_go_internal_type("s")); // Not "sync."
637        assert!(!is_go_internal_type("i")); // Not "internal/"
638        assert!(!is_go_internal_type("g")); // Not "go."
639        assert!(!is_go_internal_type("t")); // Not "type:"
640        assert!(!is_go_internal_type("[")); // Not "[]"
641
642        // Complex nested types
643        assert!(is_go_internal_type("[][]int")); // Nested slice
644        assert!(is_go_internal_type("[]*[]int")); // Pointer to slice
645
646        // Multiple middle dots
647        assert!(is_go_internal_type("type\u{00B7}inner\u{00B7}func"));
648
649        // Type with middle dot but also matching prefix
650        assert!(is_go_internal_type("runtime\u{00B7}internal"));
651    }
652}