use crate::utils::error::{Error, Result};
use std::path::Path;
use std::io::{BufRead, BufReader};
use std::fs::File;
use super::types::{InlineCodeownersEntry, Owner, Tag};
use super::parser::parse_owner;
pub fn detect_inline_codeowners(file_path: &Path) -> Result<Option<InlineCodeownersEntry>> {
let file = match File::open(file_path) {
Ok(f) => f,
Err(_) => return Ok(None), };
let reader = BufReader::new(file);
let lines = reader.lines().take(50);
for (line_num, line_result) in lines.enumerate() {
let line = match line_result {
Ok(l) => l,
Err(_) => continue, };
if let Some(entry) = parse_inline_codeowners_line(&line, line_num + 1, file_path)? {
return Ok(Some(entry));
}
}
Ok(None)
}
fn parse_inline_codeowners_line(
line: &str,
line_number: usize,
file_path: &Path,
) -> Result<Option<InlineCodeownersEntry>> {
if let Some(marker_pos) = line.find("!!!CODEOWNERS") {
let after_marker = &line[marker_pos + "!!!CODEOWNERS".len()..];
let tokens: Vec<&str> = after_marker.split_whitespace().collect();
if tokens.is_empty() {
return Ok(None);
}
let mut owners: Vec<Owner> = Vec::new();
let mut tags: Vec<Tag> = Vec::new();
let mut i = 0;
while i < tokens.len() && !tokens[i].starts_with('#') {
owners.push(parse_owner(tokens[i])?);
i += 1;
}
while i < tokens.len() {
let token = tokens[i];
if token.starts_with('#') {
if token == "#" {
break;
} else {
let tag_part = &token[1..];
if tag_part.is_empty() {
break;
}
let next_token = if i + 1 < tokens.len() { Some(tokens[i + 1]) } else { None };
match next_token {
Some("-->") | Some("*/") => {
tags.push(Tag(tag_part.to_string()));
i += 1;
break; }
Some(next) if next.starts_with('#') => {
tags.push(Tag(tag_part.to_string()));
i += 1;
}
Some(_) => {
if tag_part.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
tags.push(Tag(tag_part.to_string()));
i += 1;
break; } else {
break; }
}
None => {
tags.push(Tag(tag_part.to_string()));
i += 1;
}
}
}
} else {
break;
}
}
if !owners.is_empty() {
return Ok(Some(InlineCodeownersEntry {
file_path: file_path.to_path_buf(),
line_number,
owners,
tags,
}));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{Owner, OwnerType, Tag};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_inline_codeowners_rust_comment() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let content = r#"// This is a Rust file
// !!!CODEOWNERS @user1 @org/team2 #tag1 #tag2
fn main() {
println!("Hello world");
}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.file_path, file_path);
assert_eq!(entry.line_number, 2);
assert_eq!(entry.owners.len(), 2);
assert_eq!(entry.owners[0].identifier, "@user1");
assert_eq!(entry.owners[1].identifier, "@org/team2");
assert_eq!(entry.tags.len(), 2);
assert_eq!(entry.tags[0].0, "tag1");
assert_eq!(entry.tags[1].0, "tag2");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_javascript_comment() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.js");
let content = r#"/*
* !!!CODEOWNERS @frontend-team #javascript
*/
function hello() {
console.log("Hello");
}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.owners.len(), 1);
assert_eq!(entry.owners[0].identifier, "@frontend-team");
assert_eq!(entry.tags.len(), 1);
assert_eq!(entry.tags[0].0, "javascript");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_python_comment() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.py");
let content = r#"#!/usr/bin/env python3
# !!!CODEOWNERS @python-team @user1 #backend #critical
"""
This is a Python module
"""
def main():
pass
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.line_number, 2);
assert_eq!(entry.owners.len(), 2);
assert_eq!(entry.owners[0].identifier, "@python-team");
assert_eq!(entry.owners[1].identifier, "@user1");
assert_eq!(entry.tags.len(), 2);
assert_eq!(entry.tags[0].0, "backend");
assert_eq!(entry.tags[1].0, "critical");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_html_comment() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.html");
let content = r#"<!DOCTYPE html>
<html>
<!-- !!!CODEOWNERS @web-team #frontend -->
<head>
<title>Test</title>
</head>
</html>
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.owners.len(), 1);
assert_eq!(entry.owners[0].identifier, "@web-team");
assert_eq!(entry.tags.len(), 1);
assert_eq!(entry.tags[0].0, "frontend");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_no_marker() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let content = r#"// This is a regular file
fn main() {
println!("No CODEOWNERS marker here");
}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_none());
Ok(())
}
#[test]
fn test_detect_inline_codeowners_no_owners() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let content = r#"// !!!CODEOWNERS #just-tags
fn main() {
println!("Only tags, no owners");
}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_none());
Ok(())
}
#[test]
fn test_detect_inline_codeowners_first_occurrence_only() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let content = r#"// !!!CODEOWNERS @first-owner #first-tag
fn main() {
// !!!CODEOWNERS @second-owner #second-tag
println!("Should only detect first occurrence");
}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.line_number, 1);
assert_eq!(entry.owners[0].identifier, "@first-owner");
assert_eq!(entry.tags[0].0, "first-tag");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_beyond_50_lines() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let mut content = String::new();
for i in 1..=50 {
content.push_str(&format!("// Line {}\n", i));
}
content.push_str("// !!!CODEOWNERS @should-not-be-found #beyond-limit\n");
content.push_str("fn main() {}\n");
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_none());
Ok(())
}
#[test]
fn test_detect_inline_codeowners_with_comment_after() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let content = r#"// !!!CODEOWNERS @user1 #tag1 # this is a comment after
fn main() {}
"#;
fs::write(&file_path, content).unwrap();
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_some());
let entry = result.unwrap();
assert_eq!(entry.owners.len(), 1);
assert_eq!(entry.owners[0].identifier, "@user1");
assert_eq!(entry.tags.len(), 1);
assert_eq!(entry.tags[0].0, "tag1");
Ok(())
}
#[test]
fn test_detect_inline_codeowners_nonexistent_file() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("nonexistent.rs");
let result = detect_inline_codeowners(&file_path)?;
assert!(result.is_none());
Ok(())
}
}