Skip to main content

mir_analyzer/
file_extensions.rs

1use std::path::Path;
2use std::sync::Arc;
3
4/// File extensions treated as PHP source, sorted and deduplicated. Defaults to `["php"]`.
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct PhpFileExtensions(Arc<[String]>);
7
8impl PhpFileExtensions {
9    /// Strips a leading `.` and lowercases each entry; blank entries are
10    /// dropped, and an empty result falls back to `["php"]`.
11    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    /// Whether `path`'s extension (case-insensitive) is one of these.
31    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}