Skip to main content

ktrs_cli/ktlint/
java_glob.rs

1//! Java's `glob:` `PathMatcher` (`sun.nio.fs.Globs.toUnixRegexPattern`), matched against a path string.
2
3use regex::Regex;
4
5const REGEX_META_CHARS: &str = ".^$+{[]|()";
6const GLOB_META_CHARS: &str = "\\*?[{";
7
8fn is_regex_meta(c: char) -> bool {
9    REGEX_META_CHARS.contains(c)
10}
11
12fn is_glob_meta(c: char) -> bool {
13    GLOB_META_CHARS.contains(c)
14}
15
16/// `Globs.toRegexPattern(glob, isDos = false)`; `Err` is the `PatternSyntaxException` message.
17pub fn to_unix_regex_pattern(glob: &str) -> Result<String, String> {
18    let chars: Vec<char> = glob.chars().collect();
19    let next = |i: usize| chars.get(i).copied();
20    let mut in_group = false;
21    let mut regex = String::from("^");
22    let mut i = 0;
23    while i < chars.len() {
24        let c = chars[i];
25        i += 1;
26        match c {
27            '\\' => {
28                let Some(next) = next(i) else { return Err("No character to escape".to_owned()) };
29                if is_glob_meta(next) || is_regex_meta(next) {
30                    regex.push('\\');
31                }
32                regex.push(next);
33                i += 1;
34            }
35            '/' => regex.push(c),
36            '[' => {
37                regex.push_str("[[^/]&&[");
38                if next(i) == Some('^') {
39                    regex.push_str("\\^");
40                    i += 1;
41                } else {
42                    if next(i) == Some('!') {
43                        regex.push('^');
44                        i += 1;
45                    }
46                    if next(i) == Some('-') {
47                        regex.push('-');
48                        i += 1;
49                    }
50                }
51                let mut has_range_start = false;
52                let mut last = '\0';
53                let mut c = '\0';
54                while i < chars.len() {
55                    c = chars[i];
56                    i += 1;
57                    if c == ']' {
58                        break;
59                    }
60                    if c == '/' {
61                        return Err("Explicit 'name separator' in class".to_owned());
62                    }
63                    if c == '\\' || c == '[' || c == '&' && next(i) == Some('&') {
64                        regex.push('\\');
65                    }
66                    regex.push(c);
67                    if c == '-' {
68                        if !has_range_start {
69                            return Err("Invalid range".to_owned());
70                        }
71                        let end = next(i);
72                        i += 1;
73                        match end {
74                            None | Some(']') => {
75                                c = end.unwrap_or('\0');
76                                break;
77                            }
78                            Some(end) if end < last => return Err("Invalid range".to_owned()),
79                            Some(end) => {
80                                c = end;
81                                regex.push(end);
82                            }
83                        }
84                        has_range_start = false;
85                    } else {
86                        has_range_start = true;
87                        last = c;
88                    }
89                }
90                if c != ']' {
91                    return Err("Missing ']".to_owned());
92                }
93                regex.push_str("]]");
94            }
95            '{' => {
96                if in_group {
97                    return Err("Cannot nest groups".to_owned());
98                }
99                regex.push_str("(?:(?:");
100                in_group = true;
101            }
102            '}' if in_group => {
103                regex.push_str("))");
104                in_group = false;
105            }
106            ',' if in_group => regex.push_str(")|(?:"),
107            '*' if next(i) == Some('*') => {
108                regex.push_str(".*");
109                i += 1;
110            }
111            '*' => regex.push_str("[^/]*"),
112            '?' => regex.push_str("[^/]"),
113            c => {
114                if is_regex_meta(c) {
115                    regex.push('\\');
116                }
117                regex.push(c);
118            }
119        }
120    }
121    if in_group {
122        return Err("Missing '}".to_owned());
123    }
124    regex.push('$');
125    Ok(regex)
126}
127
128/// A compiled `glob:` matcher. Java regexes run in DOTALL-less mode, so `.` does not match line breaks.
129#[derive(Clone, Debug)]
130pub struct PathMatcher {
131    glob: String,
132    regex: Regex,
133}
134
135impl PathMatcher {
136    pub fn new(glob: &str, case_insensitive: bool) -> Result<PathMatcher, String> {
137        let pattern = to_unix_regex_pattern(glob)?;
138        let pattern = if case_insensitive { format!("(?i){pattern}") } else { pattern };
139        let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
140        Ok(PathMatcher { glob: glob.to_owned(), regex })
141    }
142
143    pub fn matches(&self, path: &str) -> bool {
144        self.regex.is_match(path)
145    }
146
147    /// `toString()` of the JDK's matcher lambda is opaque; the glob is what a trace log can usefully show.
148    pub fn glob(&self) -> &str {
149        &self.glob
150    }
151}