Skip to content
Merged
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
87 changes: 59 additions & 28 deletions pkg/diff/topology_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package diff

import (
"sort"
"strings"

"github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/topology"
)
Expand Down Expand Up @@ -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: ( ) [ ] * , <space> { }
// 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
}
94 changes: 94 additions & 0 deletions pkg/diff/topology_match_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading