11use std:: path:: { Path , PathBuf } ;
22use utils:: error:: { Error , Result } ;
33
4- use crate :: types:: { CodeownersEntry , Owner , OwnerType } ;
4+ use crate :: types:: { CodeownersEntry , Owner , OwnerType , Tag } ;
55
66/// Find CODEOWNERS files recursively in the given directory and its subdirectories
77pub fn find_codeowners_files < P : AsRef < Path > > ( base_path : P ) -> Result < Vec < PathBuf > > {
@@ -27,46 +27,59 @@ pub fn find_codeowners_files<P: AsRef<Path>>(base_path: P) -> Result<Vec<PathBuf
2727}
2828
2929/// Parse CODEOWNERS
30- pub fn parse_codeowners ( content : & str , source_path : & Path ) -> Result < Vec < CodeownersEntry > > {
30+ pub fn parse_codeowners ( source_path : & Path ) -> Result < Vec < CodeownersEntry > > {
31+ let content = std:: fs:: read_to_string ( source_path) ?;
32+
3133 content
3234 . lines ( )
3335 . enumerate ( )
34- . filter_map ( |( line_num, line) | parse_line ( line, line_num + 1 , source_path) . transpose ( ) )
36+ . filter_map ( |( line_num, line) | parse_line ( line, line_num, source_path) . transpose ( ) )
3537 . collect ( )
3638}
3739
3840/// Parse a line of CODEOWNERS
3941fn parse_line ( line : & str , line_num : usize , source_path : & Path ) -> Result < Option < CodeownersEntry > > {
40- let clean_line = line. split ( '#' ) . next ( ) . unwrap_or ( "" ) . trim ( ) ;
41- if clean_line. is_empty ( ) {
42+ // Trim the line and check for empty or comment lines
43+ let trimmed = line. trim ( ) ;
44+ if trimmed. is_empty ( ) || trimmed. starts_with ( '#' ) {
45+ return Ok ( None ) ;
46+ }
47+
48+ // Split the line by whitespace into a series of tokens
49+ let tokens: Vec < & str > = trimmed. split_whitespace ( ) . collect ( ) ;
50+ if tokens. is_empty ( ) {
4251 return Ok ( None ) ;
4352 }
4453
45- let mut parts = clean_line. split_whitespace ( ) ;
46- let pattern = parts
47- . next ( )
48- . ok_or_else ( || Error :: new ( "Missing pattern" ) ) ?
49- . to_string ( ) ;
50-
51- let ( owners, tags) = parts. fold ( ( Vec :: new ( ) , Vec :: new ( ) ) , |( mut owners, mut tags) , part| {
52- if part. starts_with ( '@' ) {
53- let owner = parse_owner ( part) . unwrap ( ) ;
54- owners. push ( owner) ;
55- } else if part. starts_with ( '[' ) && part. ends_with ( ']' ) {
56- tags. extend (
57- part[ 1 ..part. len ( ) - 1 ]
58- . split ( ',' )
59- . map ( |t| t. trim ( ) . to_string ( ) ) ,
60- ) ;
54+ // The first token is the pattern
55+ let pattern = tokens[ 0 ] . to_string ( ) ;
56+
57+ let mut owners: Vec < Owner > = Vec :: new ( ) ;
58+ let mut tags: Vec < Tag > = Vec :: new ( ) ;
59+
60+ let mut i = 1 ; // Start after the pattern
61+
62+ // Collect owners until a token starts with '#'
63+ while i < tokens. len ( ) && !tokens[ i] . starts_with ( '#' ) {
64+ owners. push ( parse_owner ( tokens[ i] ) ?) ;
65+ i += 1 ;
66+ }
67+
68+ // Collect tags
69+ while i < tokens. len ( ) {
70+ let token = tokens[ i] ;
71+ if token. starts_with ( '#' ) {
72+ if token == "#" {
73+ // Comment starts, break
74+ break ;
75+ } else {
76+ tags. push ( Tag ( token[ 1 ..] . to_string ( ) ) ) ;
77+ }
6178 } else {
62- let owner = parse_owner ( part) . unwrap ( ) ;
63- owners . push ( owner ) ;
79+ // Non-tag, part of comment
80+ break ;
6481 }
65- ( owners, tags)
66- } ) ;
67-
68- if owners. is_empty ( ) {
69- return Ok ( None ) ;
82+ i += 1 ;
7083 }
7184
7285 Ok ( Some ( CodeownersEntry {
@@ -80,17 +93,20 @@ fn parse_line(line: &str, line_num: usize, source_path: &Path) -> Result<Option<
8093
8194/// Parse an owner string into an Owner struct
8295fn parse_owner ( owner_str : & str ) -> Result < Owner > {
83- let ( identifier, owner_type) = if owner_str. contains ( '@' ) {
84- ( owner_str. to_string ( ) , OwnerType :: Email )
96+ let identifier = owner_str. to_string ( ) ;
97+ let owner_type = if identifier. eq_ignore_ascii_case ( "NOOWNER" ) {
98+ OwnerType :: Unowned
8599 } else if owner_str. starts_with ( '@' ) {
86100 let parts: Vec < & str > = owner_str[ 1 ..] . split ( '/' ) . collect ( ) ;
87101 if parts. len ( ) == 2 {
88- ( owner_str . to_string ( ) , OwnerType :: Team )
102+ OwnerType :: Team
89103 } else {
90- ( owner_str . to_string ( ) , OwnerType :: User )
104+ OwnerType :: User
91105 }
106+ } else if owner_str. contains ( '@' ) {
107+ OwnerType :: Email
92108 } else {
93- ( owner_str . to_string ( ) , OwnerType :: Unknown )
109+ OwnerType :: Unknown
94110 } ;
95111
96112 Ok ( Owner {
@@ -158,4 +174,151 @@ mod tests {
158174 assert ! ( found_files. is_empty( ) ) ;
159175 Ok ( ( ) )
160176 }
177+
178+ #[ test]
179+ fn test_parse_owner_user ( ) -> Result < ( ) > {
180+ let owner = parse_owner ( "@username" ) ?;
181+ assert_eq ! ( owner. identifier, "@username" ) ;
182+ assert ! ( matches!( owner. owner_type, OwnerType :: User ) ) ;
183+
184+ // With hyphens and underscores
185+ let owner = parse_owner ( "@user-name_123" ) ?;
186+ assert_eq ! ( owner. identifier, "@user-name_123" ) ;
187+ assert ! ( matches!( owner. owner_type, OwnerType :: User ) ) ;
188+
189+ // Single character username
190+ let owner = parse_owner ( "@a" ) ?;
191+ assert_eq ! ( owner. identifier, "@a" ) ;
192+ assert ! ( matches!( owner. owner_type, OwnerType :: User ) ) ;
193+
194+ Ok ( ( ) )
195+ }
196+
197+ #[ test]
198+ fn test_parse_owner_team ( ) -> Result < ( ) > {
199+ // Standard team
200+ let owner = parse_owner ( "@org/team-name" ) ?;
201+ assert_eq ! ( owner. identifier, "@org/team-name" ) ;
202+ assert ! ( matches!( owner. owner_type, OwnerType :: Team ) ) ;
203+
204+ // With numbers and special characters
205+ let owner = parse_owner ( "@company123/frontend-team_01" ) ?;
206+ assert_eq ! ( owner. identifier, "@company123/frontend-team_01" ) ;
207+ assert ! ( matches!( owner. owner_type, OwnerType :: Team ) ) ;
208+
209+ // Short names
210+ let owner = parse_owner ( "@o/t" ) ?;
211+ assert_eq ! ( owner. identifier, "@o/t" ) ;
212+ assert ! ( matches!( owner. owner_type, OwnerType :: Team ) ) ;
213+
214+ Ok ( ( ) )
215+ }
216+
217+ #[ test]
218+ fn test_parse_owner_email ( ) -> Result < ( ) > {
219+ // Standard email
220+ let owner = parse_owner ( "user@example.com" ) ?;
221+ assert_eq ! ( owner. identifier, "user@example.com" ) ;
222+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
223+
224+ // With plus addressing
225+ let owner = parse_owner ( "user+tag@example.com" ) ?;
226+ assert_eq ! ( owner. identifier, "user+tag@example.com" ) ;
227+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
228+
229+ // With dots and numbers
230+ let owner = parse_owner ( "user.name123@sub.example.com" ) ?;
231+ assert_eq ! ( owner. identifier, "user.name123@sub.example.com" ) ;
232+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
233+
234+ // Multiple @ symbols - should still be detected as Email
235+ let owner = parse_owner ( "user@example@domain.com" ) ?;
236+ assert_eq ! ( owner. identifier, "user@example@domain.com" ) ;
237+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
238+
239+ // IP address domain
240+ let owner = parse_owner ( "user@[192.168.1.1]" ) ?;
241+ assert_eq ! ( owner. identifier, "user@[192.168.1.1]" ) ;
242+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
243+
244+ Ok ( ( ) )
245+ }
246+
247+ #[ test]
248+ fn test_parse_owner_unowned ( ) -> Result < ( ) > {
249+ let owner = parse_owner ( "NOOWNER" ) ?;
250+ assert_eq ! ( owner. identifier, "NOOWNER" ) ;
251+ assert ! ( matches!( owner. owner_type, OwnerType :: Unowned ) ) ;
252+
253+ // Case insensitive
254+ let owner = parse_owner ( "noowner" ) ?;
255+ assert_eq ! ( owner. identifier, "noowner" ) ;
256+ assert ! ( matches!( owner. owner_type, OwnerType :: Unowned ) ) ;
257+
258+ let owner = parse_owner ( "NoOwNeR" ) ?;
259+ assert_eq ! ( owner. identifier, "NoOwNeR" ) ;
260+ assert ! ( matches!( owner. owner_type, OwnerType :: Unowned ) ) ;
261+
262+ Ok ( ( ) )
263+ }
264+
265+ #[ test]
266+ fn test_parse_owner_unknown ( ) -> Result < ( ) > {
267+ // Random text
268+ let owner = parse_owner ( "plaintext" ) ?;
269+ assert_eq ! ( owner. identifier, "plaintext" ) ;
270+ assert ! ( matches!( owner. owner_type, OwnerType :: Unknown ) ) ;
271+
272+ // Text with special characters (but not @ or email format)
273+ let owner = parse_owner ( "special-text_123" ) ?;
274+ assert_eq ! ( owner. identifier, "special-text_123" ) ;
275+ assert ! ( matches!( owner. owner_type, OwnerType :: Unknown ) ) ;
276+
277+ // URL-like but not an owner
278+ let owner = parse_owner ( "https://example.com" ) ?;
279+ assert_eq ! ( owner. identifier, "https://example.com" ) ;
280+ assert ! ( matches!( owner. owner_type, OwnerType :: Unknown ) ) ;
281+
282+ Ok ( ( ) )
283+ }
284+
285+ #[ test]
286+ fn test_parse_owner_email_edge_cases ( ) -> Result < ( ) > {
287+ // Technically valid by RFC 5322 but unusual emails
288+ let owner = parse_owner ( "\" quoted\" @example.com" ) ?;
289+ assert_eq ! ( owner. identifier, "\" quoted\" @example.com" ) ;
290+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
291+
292+ // Very short email
293+ let owner = parse_owner ( "a@b.c" ) ?;
294+ assert_eq ! ( owner. identifier, "a@b.c" ) ;
295+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
296+
297+ // Email with many subdomains
298+ let owner = parse_owner ( "user@a.b.c.d.example.com" ) ?;
299+ assert_eq ! ( owner. identifier, "user@a.b.c.d.example.com" ) ;
300+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
301+
302+ Ok ( ( ) )
303+ }
304+
305+ #[ test]
306+ fn test_parse_owner_ambiguous_cases ( ) -> Result < ( ) > {
307+ // Contains @ but also has prefix
308+ let owner = parse_owner ( "prefix-user@example.com" ) ?;
309+ assert_eq ! ( owner. identifier, "prefix-user@example.com" ) ;
310+ assert ! ( matches!( owner. owner_type, OwnerType :: Email ) ) ;
311+
312+ // Has team-like structure but without @ prefix
313+ let owner = parse_owner ( "org/team-name" ) ?;
314+ assert_eq ! ( owner. identifier, "org/team-name" ) ;
315+ assert ! ( matches!( owner. owner_type, OwnerType :: Unknown ) ) ;
316+
317+ // Contains "NOOWNER" as substring but isn't exactly NOOWNER
318+ let owner = parse_owner ( "NOOWNER-plus" ) ?;
319+ assert_eq ! ( owner. identifier, "NOOWNER-plus" ) ;
320+ assert ! ( matches!( owner. owner_type, OwnerType :: Unknown ) ) ;
321+
322+ Ok ( ( ) )
323+ }
161324}
0 commit comments