mir_analyzer/
file_extensions.rs1use std::path::Path;
2use std::sync::Arc;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct PhpFileExtensions(Arc<[String]>);
7
8impl PhpFileExtensions {
9 pub fn new<I, S>(extensions: I) -> Self
12 where
13 I: IntoIterator<Item = S>,
14 S: AsRef<str>,
15 {
16 let mut normalized: Vec<String> = Vec::new();
17 for ext in extensions {
18 let ext = ext.as_ref().trim().trim_start_matches('.').to_lowercase();
19 if !ext.is_empty() && !normalized.contains(&ext) {
20 normalized.push(ext);
21 }
22 }
23 if normalized.is_empty() {
24 return Self::default();
25 }
26 normalized.sort();
27 Self(normalized.into())
28 }
29
30 pub fn is_php_source(&self, path: &Path) -> bool {
32 path.extension()
33 .and_then(|e| e.to_str())
34 .is_some_and(|e| self.0.iter().any(|x| x.eq_ignore_ascii_case(e)))
35 }
36
37 pub fn as_slice(&self) -> &[String] {
38 &self.0
39 }
40}
41
42impl Default for PhpFileExtensions {
43 fn default() -> Self {
44 Self(Arc::from(vec!["php".to_string()]))
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn normalizes_dots_case_and_blanks() {
54 let exts = PhpFileExtensions::new([".INC", "Module", " ", "inc"]);
55 assert_eq!(exts.as_slice(), ["inc", "module"]);
56 }
57
58 #[test]
59 fn empty_input_falls_back_to_php() {
60 assert_eq!(
61 PhpFileExtensions::new(Vec::<String>::new()),
62 PhpFileExtensions::default()
63 );
64 assert_eq!(PhpFileExtensions::new(["", "."]).as_slice(), ["php"]);
65 }
66
67 #[test]
68 fn equality_ignores_order() {
69 assert_eq!(
70 PhpFileExtensions::new(["php", "inc"]),
71 PhpFileExtensions::new(["inc", "php"])
72 );
73 }
74
75 #[test]
76 fn matches_case_insensitively() {
77 let exts = PhpFileExtensions::new(["php", "inc"]);
78 assert!(exts.is_php_source(Path::new("a/B.INC")));
79 assert!(exts.is_php_source(Path::new("a.php")));
80 assert!(!exts.is_php_source(Path::new("a.module")));
81 assert!(!exts.is_php_source(Path::new("noext")));
82 }
83}