Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion internal/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,97 @@ func (si StackInfo) isFuncAnchor(s string) bool {
if si.PackageName == "" && si.FuncName == "" {
return true // cannot calculate anchor, calling algorithm set it zero
}
return strings.Contains(s, si.fullName())
fullName := si.fullName()

// If only package name is provided (no function name), match the package name as-is
if si.FuncName == "" {
return strings.Contains(s, fullName)
}

// Build the needle to search for
var needle string
if strings.HasSuffix(fullName, "(") {
// FuncName already includes "(", use as-is
needle = fullName
} else {
// Add "(" to match function calls
needle = fullName + "("
}

// If there's no package name (only function name like "Handle"), we need to ensure
// we match at a function boundary (preceded by .) to prevent matching user functions
// like "FirstHandle" when searching for "Handle".
// This is critical because "FirstHandle(" contains "Handle(" as a substring.
if si.PackageName == "" && !strings.HasSuffix(si.FuncName, "(") {
searchFrom := 0
for {
idx := strings.Index(s[searchFrom:], needle)
if idx == -1 {
return false
}
// Adjust idx to absolute position in the original string
absIdx := searchFrom + idx

// Only allow a '.' boundary and require "err2" immediately before it.
// Note: Go stack lines never start with a bare function name, so we don't check absIdx == 0.
if absIdx > 0 && isErr2BeforeDot(s, absIdx) {
return true
}

// Continue search after this match
searchFrom = absIdx + 1
}
}

return strings.Contains(s, needle)
}

// isErr2BeforeDot returns true if the token immediately before the dot is "err2",
// or a major version token "vN" whose previous path segment is "err2".
// absIdx points to the start of the function name; absIdx-1 must be '.'.
// Examples:
// - "github.com/lainio/err2.Handle(...)" -> true (package token is "err2")
// - "github.com/lainio/err2/v2.Handle(...)" -> true (versioned module)
// - "err2.Handle(...)" -> true (package token is "err2")
// - "main.Handle(...)" -> false (package token is "main")
// - "mypackage.Handle(...)" -> false (package token is "mypackage")
func isErr2BeforeDot(s string, absIdx int) bool {
dot := absIdx - 1
if dot < 0 || s[dot] != '.' {
return false
}

// Find the last '/' before the dot to isolate the package token.
start := strings.LastIndex(s[:dot], "/")
if start == -1 {
start = 0
} else {
start++ // exclude '/'
}
pkg := s[start:dot]
if pkg == "err2" {
return true
}

// Support versioned imports like ".../err2/v2.Handle".
// If pkg == "vN" and the previous path token == "err2", accept.
if len(pkg) > 1 && pkg[0] == 'v' {
allDigits := true
for i := 1; i < len(pkg); i++ {
if pkg[i] < '0' || pkg[i] > '9' {
allDigits = false
break
}
}
if allDigits && start > 0 {
prev := strings.LastIndex(s[:start-1], "/")
if prev != -1 && s[prev+1:start-1] == "err2" {
return true
}
}
}

return false
}

func (si StackInfo) needToCalcFnNameAnchor() bool {
Expand Down
30 changes: 30 additions & 0 deletions internal/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,36 @@ func TestIsFuncAnchor(t *testing.T) {
{"package name only", args{
"github.com/lainio/err2/try.To1[...](...)",
StackInfo{"lainio/err2", "", 0, nil, nil, false}}, true},
{"user function containing Handle should not match", args{
"main.FirstHandle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, false},
{"user function containing Handle matches err2.Handle", args{
"github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, true},
{"user function named exactly Handle should not match", args{
"main.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, false},
{"user package function named Handle should not match", args{
"mypackage.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, false},
{"err2.Handle should match", args{
"err2.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, true},
{"lainio/err2.Handle should match", args{
"github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, true},
{"user package with err2 in path should not match", args{
"github.com/mycompany/err2/mypackage.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, false},
{"versioned err2/v2.Handle should match", args{
"github.com/lainio/err2/v2.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, true},
{"versioned err2/v10.Handle should match", args{
"github.com/lainio/err2/v10.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, true},
{"non-err2 versioned package should not match", args{
"github.com/someone/otherpkg/v2.Handle(0x40000b3ed8, 0x40000b3ef8)",
StackInfo{"", "Handle", 0, nil, nil, false}}, false},
}
for _, ttv := range tests {
tt := ttv
Expand Down