codeowners_validation/
parser.rs1use globset::Glob;
2use std::fs::File;
3use std::io::{self, BufRead};
4
5#[derive(Eq, PartialEq, Clone)]
6pub struct CodeOwnerRule {
7 pub pattern: String,
8 pub owners: Vec<String>,
9 pub original_path: String,
10 pub glob: Glob,
11}
12
13pub struct InvalidLine {
14 pub line_number: usize,
15 pub content: String,
16}
17
18pub fn parse_codeowners_file(
19 file_path: &str,
20) -> io::Result<(Vec<CodeOwnerRule>, Vec<InvalidLine>)> {
21 let file = File::open(file_path)?;
22 let reader = io::BufReader::new(file);
23
24 let mut rules = Vec::new();
25 let mut invalid_lines = Vec::new();
26
27 for (line_number, line_result) in reader.lines().enumerate() {
28 if let Ok(line) = line_result {
29 let trimmed_line = line.trim();
30 if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
31 continue;
33 }
34
35 let parts: Vec<&str> = line.split_whitespace().collect();
36 let pattern = parts[0].trim_matches('/').to_string();
37 let owners = parts[1..].iter().map(|s| s.to_string()).collect();
38 let original_path = parts[0].to_string();
39
40 let glob = match Glob::new(&format!("**{}", pattern)) {
41 Ok(glob) => glob,
42 Err(_) => {
43 let invalid_line = InvalidLine {
44 line_number: line_number + 1,
45 content: line,
46 };
47 invalid_lines.push(invalid_line);
48 continue;
49 }
50 };
51
52 let rule = CodeOwnerRule {
53 pattern,
54 owners,
55 original_path,
56 glob,
57 };
58
59 rules.push(rule);
60 }
61 }
62
63 Ok((rules, invalid_lines))
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::fs::write;
70 use tempfile::NamedTempFile;
71
72 fn with_temp_codeowners(content: &str) -> NamedTempFile {
73 let file = NamedTempFile::new().unwrap();
74 write(file.path(), content).unwrap();
75 file
76 }
77
78 #[test]
79 fn parses_valid_lines() {
80 let file = with_temp_codeowners("src/lib.rs @alice\n");
81 let (rules, invalids) = parse_codeowners_file(file.path().to_str().unwrap()).unwrap();
82 assert_eq!(rules.len(), 1);
83 assert_eq!(rules[0].pattern, "src/lib.rs");
84 assert_eq!(rules[0].owners, vec!["@alice"]);
85 assert!(invalids.is_empty());
86 }
87
88 #[test]
89 fn ignores_comments_and_blanks() {
90 let file = with_temp_codeowners("# comment\n\nsrc/main.rs @bob\n");
91 let (rules, _) = parse_codeowners_file(file.path().to_str().unwrap()).unwrap();
92 assert_eq!(rules.len(), 1);
93 assert_eq!(rules[0].owners, vec!["@bob"]);
94 }
95
96 #[test]
97 fn detects_invalid_glob() {
98 let file = with_temp_codeowners("docs/[ @bad\n");
99 let (_, invalids) = parse_codeowners_file(file.path().to_str().unwrap()).unwrap();
100 assert_eq!(invalids.len(), 1);
101 assert!(invalids[0].content.contains("docs/["));
102 }
103
104 #[test]
105 fn trims_leading_trailing_slashes() {
106 let file = with_temp_codeowners("/foo/ @team\n");
107 let (rules, _) = parse_codeowners_file(file.path().to_str().unwrap()).unwrap();
108 assert_eq!(rules[0].pattern, "foo");
109 assert_eq!(rules[0].original_path, "/foo/");
110 }
111
112 #[test]
113 fn parses_multiple_owners() {
114 let file = with_temp_codeowners("src/ @alice @bob\n");
115 let (rules, _) = parse_codeowners_file(file.path().to_str().unwrap()).unwrap();
116 assert_eq!(rules[0].owners, vec!["@alice", "@bob"]);
117 }
118}