codeowners_validation/validators/
duplicate_patterns.rs

1use crate::parser::CodeOwnerRule;
2use rustc_hash::FxHashSet;
3
4pub fn validate_duplicates(rules: &[CodeOwnerRule]) -> Vec<CodeOwnerRule> {
5    let mut pattern_set = FxHashSet::default();
6    let mut original_path_set = FxHashSet::default();
7
8    pattern_set.reserve(rules.len());
9    original_path_set.reserve(rules.len());
10
11    let mut duplicates = Vec::new();
12
13    for rule in rules {
14        let is_original_path_duplicate = !original_path_set.insert(&rule.original_path);
15        let is_pattern_duplicate = !pattern_set.insert(&rule.pattern);
16
17        if is_original_path_duplicate || is_pattern_duplicate {
18            duplicates.push(rule.clone());
19            // Only print a warning if it is the normalized duplicate.
20            if !is_original_path_duplicate && is_pattern_duplicate {
21                println!("Warning: Duplicate pattern found in normalized pattern");
22                println!("Please raise an issue if this seems incorrect.");
23                println!("Pattern: {}", &rule.pattern);
24                println!("Original: {}", &rule.original_path);
25            }
26        }
27    }
28
29    duplicates
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::parser::CodeOwnerRule;
36
37    fn rule(pattern: &str, original: &str) -> CodeOwnerRule {
38        CodeOwnerRule {
39            pattern: pattern.trim_matches('/').to_string(),
40            original_path: original.to_string(),
41            owners: vec!["@owner".to_string()],
42        }
43    }
44
45    #[test]
46    fn no_duplicates() {
47        let rules = vec![rule("a.txt", "a.txt"), rule("b.txt", "b.txt")];
48        let result = validate_duplicates(&rules);
49        assert!(result.is_empty());
50    }
51
52    #[test]
53    fn detects_exact_duplicates() {
54        let rules = vec![rule("src", "src/"), rule("src", "src/")];
55        let result = validate_duplicates(&rules);
56        assert_eq!(result.len(), 1);
57        assert_eq!(result[0].pattern, "src");
58    }
59
60    #[test]
61    fn normalized_duplicates_detected() {
62        // /docs and docs normalize to the same pattern
63        let rules = vec![rule("docs", "/docs"), rule("docs", "docs")];
64        let result = validate_duplicates(&rules);
65        assert_eq!(result.len(), 1);
66        assert_eq!(result[0].pattern, "docs");
67    }
68
69    #[test]
70    fn different_slash_variations() {
71        // These all normalize to "src/lib"
72        let rules = vec![
73            rule("src/lib", "/src/lib/"),
74            rule("src/lib", "src/lib"),
75            rule("src/lib", "/src/lib"),
76        ];
77        let result = validate_duplicates(&rules);
78        // First one is not a duplicate, second and third are
79        assert_eq!(result.len(), 2);
80    }
81
82    #[test]
83    fn original_path_duplicates() {
84        // Same original path is always a duplicate
85        let rules = vec![rule("docs", "docs/"), rule("docs", "docs/")];
86        let result = validate_duplicates(&rules);
87        assert_eq!(result.len(), 1);
88    }
89
90    #[test]
91    fn wildcard_duplicates() {
92        let rules = vec![rule("*.md", "*.md"), rule("*.md", "*.md")];
93        let result = validate_duplicates(&rules);
94        assert_eq!(result.len(), 1);
95    }
96
97    #[test]
98    fn complex_pattern_duplicates() {
99        let rules = vec![
100            rule("**/*.test.js", "**/*.test.js"),
101            rule("src/*.rs", "src/*.rs"),
102            rule("**/*.test.js", "**/*.test.js"), // duplicate
103        ];
104        let result = validate_duplicates(&rules);
105        assert_eq!(result.len(), 1);
106        assert_eq!(result[0].pattern, "**/*.test.js");
107    }
108}