@@ -26,6 +26,22 @@ pub fn find_codeowners_files<P: AsRef<Path>>(base_path: P) -> Result<Vec<PathBuf
2626 Ok ( result)
2727}
2828
29+ // Find all files in the given directory and its subdirectories
30+ pub fn find_files < P : AsRef < Path > > ( base_path : P ) -> Result < Vec < PathBuf > > {
31+ let mut result = Vec :: new ( ) ;
32+ if let Ok ( entries) = std:: fs:: read_dir ( base_path) {
33+ for entry in entries. flatten ( ) {
34+ let path = entry. path ( ) ;
35+ if path. is_file ( ) {
36+ result. push ( path) ;
37+ } else if path. is_dir ( ) {
38+ result. extend ( find_files ( path) ?) ;
39+ }
40+ }
41+ }
42+ Ok ( result)
43+ }
44+
2945/// Parse CODEOWNERS
3046pub fn parse_codeowners ( source_path : & Path ) -> Result < Vec < CodeownersEntry > > {
3147 let content = std:: fs:: read_to_string ( source_path) ?;
@@ -65,21 +81,27 @@ fn parse_line(line: &str, line_num: usize, source_path: &Path) -> Result<Option<
6581 i += 1 ;
6682 }
6783
68- // Collect tags
84+ // Collect tags with lookahead to check for comments
6985 while i < tokens. len ( ) {
7086 let token = tokens[ i] ;
7187 if token. starts_with ( '#' ) {
7288 if token == "#" {
7389 // Comment starts, break
7490 break ;
7591 } else {
92+ // Check if the next token is not a tag (doesn't start with '#')
93+ let next_is_non_tag = i + 1 < tokens. len ( ) && !tokens[ i + 1 ] . starts_with ( '#' ) ;
94+ if next_is_non_tag {
95+ // This token is part of the comment, break
96+ break ;
97+ }
7698 tags. push ( Tag ( token[ 1 ..] . to_string ( ) ) ) ;
99+ i += 1 ;
77100 }
78101 } else {
79102 // Non-tag, part of comment
80103 break ;
81104 }
82- i += 1 ;
83105 }
84106
85107 Ok ( Some ( CodeownersEntry {
@@ -321,4 +343,117 @@ mod tests {
321343
322344 Ok ( ( ) )
323345 }
346+
347+ #[ test]
348+ fn test_parse_line_pattern_with_owners ( ) -> Result < ( ) > {
349+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
350+ let result = parse_line ( "*.js @qa-team @bob #test" , 1 , source_path) ?;
351+
352+ assert ! ( result. is_some( ) ) ;
353+ let entry = result. unwrap ( ) ;
354+ assert_eq ! ( entry. pattern, "*.js" ) ;
355+ assert_eq ! ( entry. owners. len( ) , 2 ) ;
356+ assert_eq ! ( entry. owners[ 0 ] . identifier, "@qa-team" ) ;
357+ assert_eq ! ( entry. owners[ 1 ] . identifier, "@bob" ) ;
358+ assert_eq ! ( entry. tags. len( ) , 1 ) ;
359+ assert_eq ! ( entry. tags[ 0 ] . 0 , "test" ) ;
360+ assert_eq ! ( entry. line_number, 1 ) ;
361+ assert_eq ! ( entry. source_file, source_path) ;
362+
363+ Ok ( ( ) )
364+ }
365+
366+ #[ test]
367+ fn test_parse_line_with_path_pattern ( ) -> Result < ( ) > {
368+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
369+ let result = parse_line ( "/fixtures/ @alice @dave" , 2 , source_path) ?;
370+
371+ assert ! ( result. is_some( ) ) ;
372+ let entry = result. unwrap ( ) ;
373+ assert_eq ! ( entry. pattern, "/fixtures/" ) ;
374+ assert_eq ! ( entry. owners. len( ) , 2 ) ;
375+ assert_eq ! ( entry. owners[ 0 ] . identifier, "@alice" ) ;
376+ assert_eq ! ( entry. owners[ 1 ] . identifier, "@dave" ) ;
377+ assert_eq ! ( entry. tags. len( ) , 0 ) ;
378+
379+ Ok ( ( ) )
380+ }
381+
382+ #[ test]
383+ fn test_parse_line_comment ( ) -> Result < ( ) > {
384+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
385+ let result = parse_line ( "# this is a comment line" , 3 , source_path) ?;
386+
387+ assert ! ( result. is_none( ) ) ;
388+
389+ Ok ( ( ) )
390+ }
391+
392+ #[ test]
393+ fn test_parse_line_with_multiple_tags_and_comment ( ) -> Result < ( ) > {
394+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
395+ let result = parse_line (
396+ "/hooks.ts @org/frontend #test #core # this is a comment" ,
397+ 4 ,
398+ source_path,
399+ ) ?;
400+
401+ assert ! ( result. is_some( ) ) ;
402+ let entry = result. unwrap ( ) ;
403+ assert_eq ! ( entry. pattern, "/hooks.ts" ) ;
404+ assert_eq ! ( entry. owners. len( ) , 1 ) ;
405+ assert_eq ! ( entry. owners[ 0 ] . identifier, "@org/frontend" ) ;
406+ assert_eq ! ( entry. tags. len( ) , 2 ) ;
407+ assert_eq ! ( entry. tags[ 0 ] . 0 , "test" ) ;
408+ assert_eq ! ( entry. tags[ 1 ] . 0 , "core" ) ;
409+
410+ Ok ( ( ) )
411+ }
412+
413+ #[ test]
414+ fn test_parse_line_empty ( ) -> Result < ( ) > {
415+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
416+ let result = parse_line ( "" , 5 , source_path) ?;
417+
418+ assert ! ( result. is_none( ) ) ;
419+
420+ let result = parse_line ( " " , 6 , source_path) ?;
421+ assert ! ( result. is_none( ) ) ;
422+
423+ Ok ( ( ) )
424+ }
425+
426+ #[ test]
427+ fn test_parse_line_security_tag ( ) -> Result < ( ) > {
428+ let source_path = Path :: new ( "/test/.husky/CODEOWNERS" ) ;
429+ let result = parse_line ( "pre-commit @org/security @frank #security" , 2 , source_path) ?;
430+
431+ assert ! ( result. is_some( ) ) ;
432+ let entry = result. unwrap ( ) ;
433+ assert_eq ! ( entry. pattern, "pre-commit" ) ;
434+ assert_eq ! ( entry. owners. len( ) , 2 ) ;
435+ assert_eq ! ( entry. owners[ 0 ] . identifier, "@org/security" ) ;
436+ assert_eq ! ( entry. owners[ 1 ] . identifier, "@frank" ) ;
437+ assert_eq ! ( entry. tags. len( ) , 1 ) ;
438+ assert_eq ! ( entry. tags[ 0 ] . 0 , "security" ) ;
439+
440+ Ok ( ( ) )
441+ }
442+
443+ #[ test]
444+ fn test_parse_line_with_pound_tag_edge_case ( ) -> Result < ( ) > {
445+ let source_path = Path :: new ( "/test/CODEOWNERS" ) ;
446+
447+ // Test edge case where # is followed by a space (comment marker)
448+ let result = parse_line ( "*.md @docs-team #not a tag" , 7 , source_path) ?;
449+
450+ assert ! ( result. is_some( ) ) ;
451+ let entry = result. unwrap ( ) ;
452+ assert_eq ! ( entry. pattern, "*.md" ) ;
453+ assert_eq ! ( entry. owners. len( ) , 1 ) ;
454+ assert_eq ! ( entry. owners[ 0 ] . identifier, "@docs-team" ) ;
455+ assert_eq ! ( entry. tags. len( ) , 0 ) ; // No tags, just a comment
456+
457+ Ok ( ( ) )
458+ }
324459}
0 commit comments