use std::borrow::Cow;
use crate::{
detection::Confidence,
structures::pclntab::{FuncData, FuncEntryIter, ParsedPclntab},
};
pub fn is_runtime_path(pkg: &str) -> bool {
pkg == "runtime"
|| pkg.starts_with("runtime/")
|| pkg == "internal/runtime"
|| pkg.starts_with("internal/runtime/")
}
pub fn is_internal_path(pkg: &str) -> bool {
is_runtime_path(pkg)
|| pkg.starts_with("internal/")
|| pkg.starts_with("vendor/")
|| pkg.starts_with("type:")
}
pub fn is_stdlib_path(pkg: &str) -> bool {
!is_internal_path(pkg) && !pkg.contains('.')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compiler {
Gc,
TinyGo,
Gccgo,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DepEntry<'a> {
pub path: &'a str,
pub version: Option<&'a str>,
pub sum: Option<&'a str>,
pub replacement: Option<DepReplacement<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepReplacement<'a> {
pub path: &'a str,
pub version: Option<&'a str>,
pub sum: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ObfuscationKind {
None,
Garble {
confidence: Confidence,
},
Other {
reason: String,
},
}
impl ObfuscationKind {
pub fn kind_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Garble { .. } => "garble",
Self::Other { .. } => "other",
}
}
}
impl std::fmt::Display for ObfuscationKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => f.write_str("none"),
Self::Garble { confidence } => write!(f, "garble({confidence})"),
Self::Other { reason } => write!(f, "other({reason})"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FipsInfo<'a> {
pub version: &'a str,
pub enforced_by_default: bool,
pub module_sum: Option<[u8; 32]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitTask<'a> {
pub package: Option<&'a str>,
pub functions: Vec<InitFunc<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InitFunc<'a> {
pub entry_va: u64,
pub name: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiverSpec<'a> {
pub name: &'a str,
pub pointer: bool,
pub generic_args: Option<&'a str>,
}
fn split_receiver_and_method(short: &str) -> Option<(&str, &str)> {
let bytes = short.as_bytes();
if bytes.first() == Some(&b'(') {
let mut depth: i32 = 0;
let mut close = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => depth = depth.checked_add(1)?,
b')' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let close = close?;
let next_idx = close.checked_add(1)?;
if bytes.get(next_idx) != Some(&b'.') {
return None;
}
let method_start = close.checked_add(2)?;
let recv = short.get(..=close)?;
let method = short.get(method_start..)?;
if method.is_empty() {
return None;
}
return Some((recv, method));
}
let mut bracket_depth: i32 = 0;
for (i, ch) in short.char_indices() {
match ch {
'[' => bracket_depth = bracket_depth.checked_add(1)?,
']' => bracket_depth = bracket_depth.checked_sub(1)?,
'.' if bracket_depth == 0 => {
let method_start = i.checked_add(1)?;
let recv = short.get(..i)?;
let method = short.get(method_start..)?;
if recv.is_empty() || method.is_empty() {
return None;
}
if !is_receiver_ident(recv) {
return None;
}
return Some((recv, method));
}
_ => {}
}
}
None
}
fn is_receiver_ident(s: &str) -> bool {
let core = s.split('[').next().unwrap_or(s);
let mut chars = core.chars();
let first = match chars.next() {
Some(c) => c,
None => return false,
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn parse_receiver_spec(recv: &str) -> ReceiverSpec<'_> {
let mut s = recv;
if let Some(stripped) = s.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
s = stripped;
}
let pointer = if let Some(rest) = s.strip_prefix('*') {
s = rest;
true
} else {
false
};
let (name, generic_args) = match s.find('[') {
Some(open) => (s.get(..open).unwrap_or(""), s.get(open..)),
None => (s, None),
};
ReceiverSpec {
name,
pointer,
generic_args,
}
}
fn last_top_level_bracket_segment(s: &str) -> Option<&str> {
let bytes = s.as_bytes();
let mut depth: i32 = 0;
let mut last_open = None;
let mut last_close = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'[' => {
if depth == 0 {
last_open = Some(i);
}
depth = depth.checked_add(1)?;
}
b']' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
last_close = Some(i);
}
}
_ => {}
}
}
match (last_open, last_close) {
(Some(o), Some(c)) if c > o => s.get(o..=c),
_ => None,
}
}
fn has_closure_suffix(name: &str) -> bool {
has_numeric_suffix_after(name, ".func") || has_numeric_suffix_after(name, ".gowrap")
}
fn has_numeric_suffix_after(name: &str, marker: &str) -> bool {
let mut search_from: usize = 0;
while let Some(slice) = name.get(search_from..) {
let rel = match slice.find(marker) {
Some(r) => r,
None => return false,
};
let after = match search_from
.checked_add(rel)
.and_then(|x| x.checked_add(marker.len()))
{
Some(a) => a,
None => return false,
};
let trailing = match name.get(after..) {
Some(t) => t,
None => return false,
};
let digit_end = trailing.bytes().take_while(|b| b.is_ascii_digit()).count();
if digit_end > 0 {
let next_byte = trailing.as_bytes().get(digit_end).copied();
if next_byte.is_none() || next_byte == Some(b'.') {
return true;
}
}
search_from = after;
}
false
}
pub(crate) fn package_of(name: &str) -> Option<&str> {
let boundary = package_boundary(name)?;
name.get(..boundary)
}
fn package_boundary(name: &str) -> Option<usize> {
let last_slash = name.rfind('/').and_then(|p| p.checked_add(1)).unwrap_or(0);
let segment = name.get(last_slash..)?;
let first_dot = segment.find('.')?;
let mut boundary = last_slash.checked_add(first_dot)?;
let after_start = boundary.checked_add(1)?;
if let Some(after) = name.get(after_start..)
&& let Some(rest) = after.strip_prefix('v')
{
let digit_end = rest.bytes().take_while(|b| b.is_ascii_digit()).count();
if digit_end > 0 && rest.as_bytes().get(digit_end) == Some(&b'.') {
boundary = boundary.checked_add(2)?.checked_add(digit_end)?;
}
}
Some(boundary)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildMode {
Exe,
Pie,
CShared,
CArchive,
Plugin,
Archive,
Shared,
Other(String),
}
impl BuildMode {
pub fn parse(value: &str) -> Self {
match value {
"exe" | "" => Self::Exe,
"pie" => Self::Pie,
"c-shared" => Self::CShared,
"c-archive" => Self::CArchive,
"plugin" => Self::Plugin,
"archive" => Self::Archive,
"shared" => Self::Shared,
other => Self::Other(other.to_string()),
}
}
pub fn as_str(&self) -> Cow<'static, str> {
match self {
Self::Exe => Cow::Borrowed("exe"),
Self::Pie => Cow::Borrowed("pie"),
Self::CShared => Cow::Borrowed("c-shared"),
Self::CArchive => Cow::Borrowed("c-archive"),
Self::Plugin => Cow::Borrowed("plugin"),
Self::Archive => Cow::Borrowed("archive"),
Self::Shared => Cow::Borrowed("shared"),
Self::Other(s) => Cow::Owned(s.clone()),
}
}
}
impl std::fmt::Display for BuildMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct BuildInfo<'a> {
pub go_version: Option<&'a str>,
pub main_path: Option<&'a str>,
pub main_module: Option<&'a str>,
pub main_version: Option<&'a str>,
pub main_module_sum: Option<&'a str>,
pub deps: Vec<DepEntry<'a>>,
pub build_settings: Vec<(&'a str, &'a str)>,
}
impl<'a> BuildInfo<'a> {
pub fn setting(&self, key: &str) -> Option<&'a str> {
self.build_settings
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| *v)
}
pub fn goos(&self) -> Option<&'a str> {
self.setting("GOOS")
}
pub fn goarch(&self) -> Option<&'a str> {
self.setting("GOARCH")
}
pub fn cgo_enabled(&self) -> Option<bool> {
self.setting("CGO_ENABLED").map(|v| v == "1")
}
pub fn vcs_revision(&self) -> Option<&'a str> {
self.setting("vcs.revision")
}
pub fn vcs_modified(&self) -> Option<bool> {
self.setting("vcs.modified").map(|v| v == "true")
}
pub fn build_tags(&self) -> impl Iterator<Item = &'a str> + '_ {
self.setting("-tags")
.into_iter()
.flat_map(|v| v.split(',').filter(|s| !s.is_empty()))
}
pub fn dependencies(&self) -> impl Iterator<Item = (&'a str, Option<&'a str>)> + '_ {
self.deps.iter().map(|d| (d.path, d.version))
}
pub fn deps_full(&self) -> impl Iterator<Item = &DepEntry<'a>> + '_ {
self.deps.iter()
}
pub fn module_sum(&self) -> Option<&'a str> {
self.main_module_sum
}
pub fn build_settings_iter(&self) -> impl Iterator<Item = (&'a str, &'a str)> + '_ {
self.build_settings.iter().copied()
}
pub fn build_mode(&self) -> Option<BuildMode> {
if self.build_settings.is_empty() {
return None;
}
Some(BuildMode::parse(self.setting("-buildmode").unwrap_or("")))
}
}
#[derive(Debug, Clone)]
pub struct FunctionInfo<'a> {
pub name: &'a str,
pub entry_offset: u32,
pub args_size: i32,
pub start_line: i32,
pub func_id: u8,
pub flags: u8,
pub deferreturn: u32,
pub pcsp: u32,
pub pcfile: u32,
pub pcln: u32,
pub npcdata: u32,
pub cu_offset: u32,
pub nfuncdata: u8,
pub source_file: Option<&'a str>,
pub end_line: i32,
pub frame_size: i32,
}
impl FunctionInfo<'_> {
pub fn package(&self) -> Option<&str> {
let boundary = package_boundary(self.name)?;
Some(&self.name[..boundary])
}
pub fn is_runtime(&self) -> bool {
self.name.starts_with("runtime.")
}
pub fn short_name(&self) -> &str {
match package_boundary(self.name).and_then(|b| b.checked_add(1)) {
Some(start) => self.name.get(start..).unwrap_or(self.name),
None => self.name,
}
}
pub fn is_method(&self) -> bool {
if self.is_closure() {
return false;
}
split_receiver_and_method(self.short_name()).is_some()
}
pub fn receiver_type(&self) -> Option<ReceiverSpec<'_>> {
if self.is_closure() {
return None;
}
let short = self.short_name();
let (recv, _method) = split_receiver_and_method(short)?;
Some(parse_receiver_spec(recv))
}
pub fn method_name(&self) -> Option<&str> {
if self.is_closure() {
return None;
}
let short = self.short_name();
let (_recv, method) = split_receiver_and_method(short)?;
Some(method)
}
pub fn generic_args(&self) -> Option<&str> {
let short = self.short_name();
last_top_level_bracket_segment(short)
}
pub fn uses_defer(&self) -> bool {
self.deferreturn != 0
}
pub fn func_flags(&self) -> FuncFlags {
FuncFlags(self.flags)
}
pub fn is_top_frame(&self) -> bool {
self.func_flags().is_top_frame()
}
pub fn is_sp_write(&self) -> bool {
self.func_flags().is_sp_write()
}
pub fn is_asm(&self) -> bool {
self.func_flags().is_asm()
}
pub fn is_systemstack(&self) -> bool {
matches!(self.func_id, 98 | 99)
}
pub fn is_closure(&self) -> bool {
if self.is_asm() {
return false;
}
has_closure_suffix(self.name)
}
pub fn is_internal(&self) -> bool {
is_internal_path(self.package().unwrap_or(""))
}
pub fn is_stdlib(&self) -> bool {
self.package().is_some_and(is_stdlib_path)
}
pub fn func_id_name(&self) -> Option<&'static str> {
match self.func_id {
0 => None,
80 => Some("abort"),
81 => Some("asmcgocall"),
82 => Some("asyncPreempt"),
83 => Some("cgocallback"),
84 => Some("debugCallV2"),
85 => Some("gcBgMarkWorker"),
86 => Some("goexit"),
87 => Some("gogo"),
88 => Some("gopanic"),
89 => Some("handleAsyncEvent"),
90 => Some("mcall"),
91 => Some("morestack"),
92 => Some("mstart"),
93 => Some("panicwrap"),
94 => Some("rt0_go"),
95 => Some("runfinq"),
96 => Some("runtime_main"),
97 => Some("sigpanic"),
98 => Some("systemstack"),
99 => Some("systemstack_switch"),
100 => Some("wrapper"),
_ => Some("unknown_special"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FuncFlags(pub u8);
impl FuncFlags {
pub const TOP_FRAME: u8 = 1 << 0;
pub const SP_WRITE: u8 = 1 << 1;
pub const ASM: u8 = 1 << 2;
pub fn bits(self) -> u8 {
self.0
}
pub fn is_top_frame(self) -> bool {
self.0 & Self::TOP_FRAME != 0
}
pub fn is_sp_write(self) -> bool {
self.0 & Self::SP_WRITE != 0
}
pub fn is_asm(self) -> bool {
self.0 & Self::ASM != 0
}
}
#[derive(Debug)]
pub struct FunctionTables<'a> {
pub pcln: &'a [(u32, i32)],
pub pcsp: &'a [(u32, i32)],
pub pcfile: &'a [(u32, u32)],
}
pub fn for_each_function<F>(pclntab: &ParsedPclntab<'_>, mut f: F)
where
F: FnMut(&FunctionInfo<'_>, &FunctionTables<'_>),
{
let mut pcln_buf: Vec<(u32, i32)> = Vec::new();
let mut pcsp_buf: Vec<(u32, i32)> = Vec::new();
let mut pcfile_buf: Vec<(u32, u32)> = Vec::new();
for (_entry_off, func_off) in pclntab.func_entries() {
let func_data: FuncData = match pclntab.parse_func(func_off) {
Some(fd) => fd,
None => continue,
};
let name = pclntab
.func_name(func_data.name_off as u32)
.unwrap_or("<unknown>");
pcln_buf.clear();
pcln_buf.extend(pclntab.decode_pcln(&func_data));
pcsp_buf.clear();
pcsp_buf.extend(pclntab.decode_pcvalue(func_data.pcsp));
pcfile_buf.clear();
pcfile_buf.extend(pclntab.decode_pcfile(&func_data));
let source_file = pcfile_buf
.first()
.and_then(|(_, idx)| pclntab.resolve_file_via_cu(func_data.cu_offset, *idx));
let end_line = pcln_buf.iter().map(|e| e.1).max().unwrap_or(0);
let frame_size = pcsp_buf.iter().map(|e| e.1).max().unwrap_or(0);
let info = FunctionInfo {
name,
entry_offset: func_data.entry_off,
args_size: func_data.args,
start_line: func_data.start_line,
func_id: func_data.func_id,
flags: func_data.flag,
deferreturn: func_data.deferreturn,
pcsp: func_data.pcsp,
pcfile: func_data.pcfile,
pcln: func_data.pcln,
npcdata: func_data.npcdata,
cu_offset: func_data.cu_offset,
nfuncdata: func_data.nfuncdata,
source_file,
end_line,
frame_size,
};
let tables = FunctionTables {
pcln: &pcln_buf,
pcsp: &pcsp_buf,
pcfile: &pcfile_buf,
};
f(&info, &tables);
}
}
pub struct FunctionIter<'a> {
inner: Option<FunctionIterInner<'a>>,
}
struct FunctionIterInner<'a> {
pclntab: ParsedPclntab<'a>,
entries: FuncEntryIter<'a>,
}
impl<'a> FunctionIter<'a> {
pub fn new(pclntab: Option<ParsedPclntab<'a>>) -> Self {
Self {
inner: pclntab.map(|p| FunctionIterInner {
entries: p.func_entries(),
pclntab: p,
}),
}
}
}
impl<'a> Iterator for FunctionIter<'a> {
type Item = FunctionInfo<'a>;
fn next(&mut self) -> Option<Self::Item> {
let inner = self.inner.as_mut()?;
loop {
let (_entry_off, func_off) = inner.entries.next()?;
let func_data = match inner.pclntab.parse_func(func_off) {
Some(fd) => fd,
None => continue,
};
let name = inner
.pclntab
.func_name(func_data.name_off as u32)
.unwrap_or("<unknown>");
let source_file = inner.pclntab.resolve_source_file(&func_data);
let end_line = inner
.pclntab
.line_range(&func_data)
.map(|(_, end)| end)
.unwrap_or(0);
let frame_size = inner.pclntab.max_frame_size(&func_data).unwrap_or(0);
return Some(FunctionInfo {
name,
entry_offset: func_data.entry_off,
args_size: func_data.args,
start_line: func_data.start_line,
func_id: func_data.func_id,
flags: func_data.flag,
deferreturn: func_data.deferreturn,
pcsp: func_data.pcsp,
pcfile: func_data.pcfile,
pcln: func_data.pcln,
npcdata: func_data.npcdata,
cu_offset: func_data.cu_offset,
nfuncdata: func_data.nfuncdata,
source_file,
end_line,
frame_size,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make(name: &'static str) -> FunctionInfo<'static> {
FunctionInfo {
name,
entry_offset: 0,
args_size: 0,
start_line: 0,
func_id: 0,
flags: 0,
deferreturn: 0,
pcsp: 0,
pcfile: 0,
pcln: 0,
npcdata: 0,
cu_offset: 0,
nfuncdata: 0,
source_file: None,
end_line: 0,
frame_size: 0,
}
}
const NAME_CORPUS: &[&str] = &[
"runtime.gcStart",
"runtime.main",
"fmt.Println",
"net/http.(*Client).Do",
"encoding/json.Marshal",
"github.com/spf13/cobra.(*Command).Run",
"github.com/spf13/cobra.OnInitialize",
"golang.org/x/crypto/aes.NewCipher",
"gopkg.in/yaml.v3.Marshal",
"k8s.io/client-go/rest.(*Config).TransportConfig",
"main.main",
"main.main.func1",
"main.run.func2.gowrap1",
"sync.(*Mutex).Lock",
"time.Time.String",
"sort.Slice[...]",
];
#[test]
fn property_package_short_name_roundtrip() {
for &name in NAME_CORPUS {
let f = make(name);
let pkg = f
.package()
.unwrap_or_else(|| panic!("no package for {name}"));
let short = f.short_name();
assert_eq!(
format!("{pkg}.{short}"),
name,
"package + '.' + short_name must round-trip for {name}",
);
}
}
#[test]
fn package_handles_third_party_domains() {
assert_eq!(make("runtime.gcStart").package(), Some("runtime"));
assert_eq!(make("net/http.(*Client).Do").package(), Some("net/http"));
assert_eq!(
make("github.com/spf13/cobra.(*Command).Run").package(),
Some("github.com/spf13/cobra"),
);
assert_eq!(
make("golang.org/x/crypto/aes.NewCipher").package(),
Some("golang.org/x/crypto/aes"),
);
assert_eq!(
make("gopkg.in/yaml.v3.Marshal").package(),
Some("gopkg.in/yaml.v3"),
);
}
#[test]
fn short_name_handles_third_party_domains() {
assert_eq!(make("runtime.gcStart").short_name(), "gcStart");
assert_eq!(make("net/http.(*Client).Do").short_name(), "(*Client).Do");
assert_eq!(
make("github.com/spf13/cobra.(*Command).Run").short_name(),
"(*Command).Run",
);
assert_eq!(
make("golang.org/x/crypto/aes.NewCipher").short_name(),
"NewCipher",
);
assert_eq!(make("gopkg.in/yaml.v3.Marshal").short_name(), "Marshal");
}
#[test]
fn package_returns_none_when_no_dot_after_path() {
assert_eq!(make("noslash").package(), None);
assert_eq!(make("github.com/user/pkg").package(), None);
}
#[test]
fn deps_full_and_module_sum_surface_parsed_detail() {
let info = BuildInfo {
main_module: Some("example.com/app"),
main_version: Some("v1.2.3"),
main_module_sum: Some("h1:mainsum="),
deps: vec![DepEntry {
path: "golang.org/x/crypto",
version: Some("v0.1.0"),
sum: Some("h1:depsum="),
replacement: Some(DepReplacement {
path: "github.com/forked/crypto",
version: Some("v0.1.1"),
sum: Some("h1:repl="),
}),
}],
..Default::default()
};
assert_eq!(info.module_sum(), Some("h1:mainsum="));
let deps: Vec<&DepEntry<'_>> = info.deps_full().collect();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].sum, Some("h1:depsum="));
assert_eq!(
deps[0].replacement.as_ref().map(|r| r.path),
Some("github.com/forked/crypto")
);
let simple: Vec<_> = info.dependencies().collect();
assert_eq!(simple, vec![("golang.org/x/crypto", Some("v0.1.0"))]);
}
#[test]
fn func_flags_bit_accessors() {
assert!(!FuncFlags(0).is_top_frame());
assert!(FuncFlags(FuncFlags::TOP_FRAME).is_top_frame());
assert!(FuncFlags(FuncFlags::SP_WRITE).is_sp_write());
assert!(FuncFlags(FuncFlags::ASM).is_asm());
let all = FuncFlags(FuncFlags::TOP_FRAME | FuncFlags::SP_WRITE | FuncFlags::ASM);
assert!(all.is_top_frame() && all.is_sp_write() && all.is_asm());
assert_eq!(all.bits(), 0b111);
}
#[test]
fn build_mode_parses_known_values() {
assert_eq!(BuildMode::parse("exe"), BuildMode::Exe);
assert_eq!(BuildMode::parse(""), BuildMode::Exe);
assert_eq!(BuildMode::parse("pie"), BuildMode::Pie);
assert_eq!(BuildMode::parse("c-shared"), BuildMode::CShared);
assert_eq!(BuildMode::parse("c-archive"), BuildMode::CArchive);
assert_eq!(BuildMode::parse("plugin"), BuildMode::Plugin);
assert_eq!(
BuildMode::parse("future-mode"),
BuildMode::Other("future-mode".into())
);
}
#[test]
fn build_tags_parses_comma_separated() {
let info = BuildInfo {
build_settings: vec![("-tags", "netgo,osusergo,static_build")],
..Default::default()
};
let tags: Vec<&str> = info.build_tags().collect();
assert_eq!(tags, vec!["netgo", "osusergo", "static_build"]);
}
#[test]
fn build_tags_empty_when_unset() {
let info = BuildInfo {
build_settings: vec![("GOOS", "linux")],
..Default::default()
};
assert_eq!(info.build_tags().count(), 0);
}
#[test]
fn build_mode_defaults_to_exe() {
let info = BuildInfo {
build_settings: vec![("GOOS", "linux")],
..Default::default()
};
assert_eq!(info.build_mode(), Some(BuildMode::Exe));
}
#[test]
fn build_mode_none_for_empty_settings() {
let info = BuildInfo::default();
assert_eq!(info.build_mode(), None);
}
#[test]
fn is_method_handles_value_and_pointer_receivers() {
assert!(make("net/http.(*Client).Do").is_method());
assert!(make("time.Time.String").is_method());
assert!(make("sync.(*Mutex).Lock").is_method());
assert!(make("github.com/spf13/cobra.(*Command).Run").is_method());
}
#[test]
fn is_method_excludes_plain_functions() {
assert!(!make("fmt.Println").is_method());
assert!(!make("runtime.gcStart").is_method());
assert!(!make("encoding/json.Marshal").is_method());
}
#[test]
fn is_method_excludes_closures() {
assert!(!make("main.main.func1").is_method());
assert!(!make("main.run.func2.gowrap1").is_method());
}
#[test]
fn is_method_handles_generic_receivers() {
assert!(make("pkg.(*Map[K, V]).Len").is_method());
assert!(make("pkg.Map[int, string].Get").is_method());
}
#[test]
fn is_closure_strict_requires_numeric_suffix() {
assert!(make("main.main.func1").is_closure());
assert!(make("main.main.func1.func2").is_closure());
assert!(make("main.run.gowrap1").is_closure());
assert!(!make("pkg.Func").is_closure());
assert!(!make("pkg.Func.Method").is_closure());
}
#[test]
fn is_closure_excludes_asm_funcs() {
let mut f = make("runtime.x.func1");
f.flags = FuncFlags::ASM;
assert!(!f.is_closure(), "asm-flagged functions are never closures");
}
#[test]
fn receiver_type_pointer_no_generics() {
let f = make("net/http.(*Client).Do");
let recv = f.receiver_type().unwrap();
assert_eq!(recv.name, "Client");
assert!(recv.pointer);
assert_eq!(recv.generic_args, None);
}
#[test]
fn receiver_type_value_no_generics() {
let f = make("time.Time.String");
let recv = f.receiver_type().unwrap();
assert_eq!(recv.name, "Time");
assert!(!recv.pointer);
assert_eq!(recv.generic_args, None);
}
#[test]
fn receiver_type_pointer_with_generics() {
let f = make("pkg.(*Map[K, V]).Len");
let recv = f.receiver_type().unwrap();
assert_eq!(recv.name, "Map");
assert!(recv.pointer);
assert_eq!(recv.generic_args, Some("[K, V]"));
}
#[test]
fn method_name_returns_method_portion() {
assert_eq!(make("net/http.(*Client).Do").method_name(), Some("Do"));
assert_eq!(make("time.Time.String").method_name(), Some("String"));
assert_eq!(make("fmt.Println").method_name(), None);
assert_eq!(make("main.main.func1").method_name(), None);
}
#[test]
fn generic_args_handles_top_level_brackets() {
assert_eq!(make("sort.Slice[int]").generic_args(), Some("[int]"));
assert_eq!(make("pkg.(*Map[K, V]).Len").generic_args(), Some("[K, V]"),);
assert_eq!(make("fmt.Println").generic_args(), None);
}
}