@@ -40,6 +40,15 @@ func matcherMatches(m config.Matcher, p Proc) bool {
4040 return true
4141 }
4242 }
43+ for _ , s := range m .NameContains {
44+ needle := normalizeProcName (s )
45+ if needle == "" {
46+ continue
47+ }
48+ if fuzzyNameContainsMatch (p .NameNorm , needle ) {
49+ return true
50+ }
51+ }
4352 for _ , s := range m .ExeContains {
4453 needle := strings .ToLower (strings .TrimSpace (s ))
4554 if needle == "" {
@@ -57,6 +66,93 @@ func shouldUseExactNameMatch(needle string) bool {
5766 return len (needle ) <= 5 && ! strings .Contains (needle , " " )
5867}
5968
69+ func fuzzyNameContainsMatch (nameNorm string , needle string ) bool {
70+ if nameNorm == needle {
71+ return true
72+ }
73+
74+ if trimmed := trimVersionLikeSuffix (nameNorm ); trimmed != "" && trimmed != nameNorm {
75+ if trimmed == needle {
76+ return true
77+ }
78+ if ! shouldUseExactNameMatch (needle ) && strings .Contains (trimmed , needle ) {
79+ return true
80+ }
81+ }
82+
83+ if shouldUseExactNameMatch (needle ) {
84+ return hasDelimitedAffix (nameNorm , needle )
85+ }
86+ return false
87+ }
88+
89+ func hasDelimitedAffix (nameNorm string , needle string ) bool {
90+ if len (nameNorm ) <= len (needle ) {
91+ return false
92+ }
93+ if strings .HasPrefix (nameNorm , needle ) && isNameDelimiter (rune (nameNorm [len (needle )])) {
94+ return true
95+ }
96+ if strings .HasSuffix (nameNorm , needle ) && isNameDelimiter (rune (nameNorm [len (nameNorm )- len (needle )- 1 ])) {
97+ return true
98+ }
99+ return false
100+ }
101+
102+ func trimVersionLikeSuffix (nameNorm string ) string {
103+ trimmed := strings .TrimSpace (nameNorm )
104+ for {
105+ base , suffix , ok := splitTrailingSegment (trimmed )
106+ if ! ok || ! looksVersionLikeSegment (suffix ) {
107+ return trimmed
108+ }
109+ trimmed = strings .TrimSpace (base )
110+ }
111+ }
112+
113+ func splitTrailingSegment (s string ) (string , string , bool ) {
114+ if s == "" {
115+ return "" , "" , false
116+ }
117+ for i := len (s ) - 1 ; i >= 0 ; i -- {
118+ if isNameDelimiter (rune (s [i ])) {
119+ if i == len (s )- 1 {
120+ return "" , "" , false
121+ }
122+ return s [:i ], s [i + 1 :], true
123+ }
124+ }
125+ return "" , "" , false
126+ }
127+
128+ func looksVersionLikeSegment (seg string ) bool {
129+ seg = strings .TrimSpace (seg )
130+ if seg == "" {
131+ return false
132+ }
133+ hasDigit := false
134+ for _ , r := range seg {
135+ if r >= '0' && r <= '9' {
136+ hasDigit = true
137+ continue
138+ }
139+ if (r >= 'a' && r <= 'z' ) || r == '.' {
140+ continue
141+ }
142+ return false
143+ }
144+ return hasDigit
145+ }
146+
147+ func isNameDelimiter (r rune ) bool {
148+ switch r {
149+ case ' ' , '-' , '_' , '.' , '(' , ')' , '[' , ']' , '{' , '}' :
150+ return true
151+ default :
152+ return false
153+ }
154+ }
155+
60156func groupRootMatch (g config.GroupSpec , p Proc ) bool {
61157 for _ , m := range g .RootMatchers {
62158 if matcherMatches (m , p ) {
0 commit comments