diff --git a/pkg/diff/topology_match.go b/pkg/diff/topology_match.go index 5724302..19af373 100644 --- a/pkg/diff/topology_match.go +++ b/pkg/diff/topology_match.go @@ -2,6 +2,7 @@ package diff import ( "sort" + "strings" "github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/topology" ) @@ -181,37 +182,67 @@ func MatchFunctionsByTopology(oldResults, newResults []FingerprintResult, thresh } func ShortFuncName(fullName string) string { - // FIX: Handle nested parenthesis and brackets (generics) - // Scan backwards to find the package separator '/' at depth 0. - depth := 0 - start := 0 - for i := len(fullName) - 1; i >= 0; i-- { - ch := fullName[i] - if ch == ')' || ch == ']' { - depth++ - } else if ch == '(' || ch == '[' { - depth-- - } else if ch == '/' && depth == 0 { - start = i + 1 - break + var sb strings.Builder + // We want to process "words". + // A word is a sequence of non-separator characters. + // Separators: ( ) [ ] * , { } + // Note: '.' and '/' are part of the word. + + lastSep := -1 + for i := 0; i < len(fullName); i++ { + c := fullName[i] + isSep := false + switch c { + case '(', ')', '[', ']', '*', ',', ' ', '{', '}': + isSep = true } - } - name := fullName[start:] - - // Scan forward to find the first dot at depth 0. - depth = 0 - for i, ch := range name { - switch ch { - case '(', '[': - depth++ - case ')', ']': - depth-- - case '.': - if depth == 0 { - return name[i+1:] + if isSep { + // Process the previous word + if i > lastSep+1 { + word := fullName[lastSep+1 : i] + sb.WriteString(shortenWord(word)) } + sb.WriteByte(c) + lastSep = i } } - return name + // Process last word + if len(fullName) > lastSep+1 { + word := fullName[lastSep+1:] + sb.WriteString(shortenWord(word)) + } + + return sb.String() +} + +func shortenWord(w string) string { + if strings.HasPrefix(w, "...") { + return "..." + shortenWord(w[3:]) + } + if strings.HasPrefix(w, ".") { + // Preserve leading dot (e.g. .Method) + return "." + shortenWord(w[1:]) + } + + lastSlash := strings.LastIndexByte(w, '/') + + // Search for last dot after lastSlash + lastDot := strings.LastIndexByte(w, '.') + + if lastDot > lastSlash { + // Dot is in the name part + return w[lastDot+1:] + } + + // No dot in the name part. Return the name part (after slash). + if lastSlash != -1 { + return w[lastSlash+1:] + } + + // No slash, no dot (or dot was before slash - impossible if lastDot checked properly? + // Wait, LastIndexByte finds the very last one. + // If lastDot < lastSlash, it means there are dots in path, but none in name. + + return w } diff --git a/pkg/diff/topology_match_test.go b/pkg/diff/topology_match_test.go new file mode 100644 index 0000000..93f4bb1 --- /dev/null +++ b/pkg/diff/topology_match_test.go @@ -0,0 +1,94 @@ +package diff + +import ( + "testing" +) + +func TestShortFuncName(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + input: "github.com/pkg/repo/pkg.Function", + expected: "Function", + }, + { + input: "github.com/pkg/repo/pkg.Function[int]", + expected: "Function[int]", + }, + { + input: "github.com/pkg/repo/pkg.Function[github.com/other/pkg.Type]", + expected: "Function[Type]", + }, + { + input: "github.com/pkg/repo/pkg.(*github.com/other/pkg.Type).Method", + expected: "(*Type).Method", + }, + { + input: "github.com/pkg/repo/pkg.Function[func(int) int]", + expected: "Function[func(int) int]", + }, + { + input: "github.com/pkg/repo/pkg.Type[int].Method", + expected: "Type[int].Method", + }, + { + input: "github.com/pkg/repo/pkg.Function[github.com/pkg/repo/pkg.List[int]]", + expected: "Function[List[int]]", + }, + { + input: "github.com/pkg/repo/pkg.Function[github.com/pkg/repo/pkg.List[github.com/other/pkg.Val]]", + expected: "Function[List[Val]]", + }, + // Variadic + { + input: "github.com/pkg/repo/pkg.Func[...int]", + expected: "Func[...int]", + }, + // Map + { + input: "github.com/pkg/repo/pkg.Func[map[github.com/pkg.Key]github.com/pkg.Val]", + expected: "Func[map[Key]Val]", + }, + // Anonymous struct + { + input: "github.com/pkg/repo/pkg.Func[struct{F github.com/pkg.Type}]", + expected: "Func[struct{F Type}]", + }, + // Pointer in generics + { + input: "github.com/pkg/repo/pkg.Func[*github.com/pkg.Type]", + expected: "Func[*Type]", + }, + // Dot method + { + input: "(*github.com/pkg.Type).Method", + expected: "(*Type).Method", + }, + // No path + { + input: "int", + expected: "int", + }, + { + input: "pkg.Type", + expected: "Type", + }, + { + input: "Type", + expected: "Type", + }, + { + input: "vendor/github.com/pkg/name", + expected: "name", + }, + } + + for _, tt := range tests { + got := ShortFuncName(tt.input) + if got != tt.expected { + t.Errorf("ShortFuncName(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +}