Skip to content

Commit 45bf8a6

Browse files
committed
wip(core): add tests and update parse_line
1 parent d03aa0a commit 45bf8a6

File tree

3 files changed

+213
-35
lines changed

3 files changed

+213
-35
lines changed

core/src/commands.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ pub fn codeowners_parse(
1919

2020
let codeowners_files = crate::common::find_codeowners_files(path)?;
2121

22-
dbg!(codeowners_files);
22+
dbg!(&codeowners_files);
23+
24+
let parsed_codeowners = codeowners_files
25+
.iter()
26+
.filter_map(|file| {
27+
let parsed = crate::common::parse_codeowners(file).ok()?;
28+
Some((file, parsed))
29+
})
30+
.collect::<Vec<_>>();
31+
32+
dbg!(&parsed_codeowners);
2333

2434
println!("CODEOWNERS parsing completed successfully");
2535
Ok(())

core/src/common.rs

Lines changed: 196 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::{Path, PathBuf};
22
use 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
77
pub 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
3941
fn 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
8295
fn 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
}

core/src/types.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub struct CodeownersEntry {
77
pub line_number: usize,
88
pub pattern: String,
99
pub owners: Vec<Owner>,
10-
pub tags: Vec<String>,
10+
pub tags: Vec<Tag>,
1111
}
1212

1313
/// Detailed owner representation
@@ -23,9 +23,14 @@ pub enum OwnerType {
2323
User,
2424
Team,
2525
Email,
26+
Unowned,
2627
Unknown,
2728
}
2829

30+
/// Tag representation
31+
#[derive(Debug)]
32+
pub struct Tag(pub String);
33+
2934
#[derive(Clone, Debug, PartialEq)]
3035
pub enum OutputFormat {
3136
Text,

0 commit comments

Comments
 (0)