1use syn::{Attribute, ExprMethodCall, Macro, Type};
2
3pub fn matches_pattern(text: &str, pattern: &str) -> bool {
5 if !pattern.contains('*') {
6 return text == pattern;
7 }
8 let star_count = pattern.matches('*').count();
9 match star_count {
10 1 => match_single_wildcard(text, pattern),
11 _ => match_multiple_wildcards(text, pattern, star_count),
12 }
13}
14
15pub(crate) fn match_single_wildcard(text: &str, pattern: &str) -> bool {
16 let Some((prefix, suffix)) = pattern.split_once('*') else {
17 return false;
18 };
19 text.starts_with(prefix) && text.ends_with(suffix)
20}
21
22fn match_multiple_wildcards(text: &str, pattern: &str, star_count: usize) -> bool {
23 let total = star_count + 1;
24 let mut pos = 0;
25
26 for (index, part) in pattern.split('*').enumerate() {
27 match try_match_part(text, &mut pos, part, index, total) {
28 PartMatch::Continue => continue,
29 PartMatch::Fail => return false,
30 PartMatch::Ok => {}
31 }
32 }
33 true
34}
35
36enum PartMatch {
37 Continue,
38 Fail,
39 Ok,
40}
41
42fn try_match_part(
43 text: &str,
44 pos: &mut usize,
45 part: &str,
46 index: usize,
47 total: usize,
48) -> PartMatch {
49 match (part.is_empty(), text[*pos..].find(part)) {
50 (true, _) => PartMatch::Continue,
51 (false, None) => PartMatch::Fail,
52 (false, Some(found)) => check_position_constraints(text, pos, part, found, index, total),
53 }
54}
55
56fn check_position_constraints(
57 text: &str,
58 pos: &mut usize,
59 part: &str,
60 found: usize,
61 index: usize,
62 total: usize,
63) -> PartMatch {
64 let is_first = index == 0;
65 let is_last = index == total - 1;
66 let first_mismatch = is_first && found != 0;
67 let last_mismatch = is_last && *pos + found + part.len() != text.len();
68
69 match (first_mismatch, last_mismatch) {
70 (true, _) | (_, true) => PartMatch::Fail,
71 _ => {
72 *pos += found + part.len();
73 PartMatch::Ok
74 }
75 }
76}
77
78pub fn matches_glob(pattern: &str, path: &str) -> bool {
84 let path = path.strip_prefix("./").unwrap_or(path);
85 let pat_segs: Vec<&str> = pattern.split('/').collect();
86 let path_segs: Vec<&str> = path.split('/').collect();
87
88 if matches_glob_at(&pat_segs, &path_segs) {
89 return true;
90 }
91
92 let pattern_is_relative =
95 !pattern.starts_with('/') && pat_segs.first().is_none_or(|s| *s != "**");
96 match pattern_is_relative && path_segs.len() > pat_segs.len() {
97 true => (1..=path_segs.len() - pat_segs.len())
98 .any(|offset| matches_glob_at(&pat_segs, &path_segs[offset..])),
99 false => false,
100 }
101}
102
103fn matches_glob_at(pat: &[&str], path: &[&str]) -> bool {
104 let p = match pat.first() {
105 Some(seg) => *seg,
106 None => return path.is_empty(),
107 };
108
109 if p == "**" {
110 return matches_double_star_at(&pat[1..], path);
111 }
112
113 let s = match path.first() {
114 Some(seg) => *seg,
115 None => return false,
116 };
117
118 matches_segment(p, s) && matches_glob_at(&pat[1..], &path[1..])
119}
120
121fn matches_double_star_at(pat: &[&str], path: &[&str]) -> bool {
122 match pat.is_empty() {
123 true => true,
124 false => (0..=path.len()).any(|i| matches_glob_at(pat, &path[i..])),
125 }
126}
127
128fn matches_segment(pattern: &str, segment: &str) -> bool {
129 match pattern {
130 "*" => true,
131 p if p.contains('*') => match_single_wildcard(segment, p),
132 _ => pattern == segment,
133 }
134}
135
136pub fn extract_attribute_text(attr: &Attribute) -> Box<str> {
138 let tokens = &attr.meta;
139 quote::quote!(#tokens)
140 .to_string()
141 .replace(' ', "")
142 .into_boxed_str()
143}
144
145pub fn extract_type_text(ty: &Type) -> Box<str> {
147 quote::quote!(#ty)
148 .to_string()
149 .replace(' ', "")
150 .into_boxed_str()
151}
152
153pub fn extract_method_call_text(call: &ExprMethodCall) -> Box<str> {
155 format!(".{}()", call.method).into_boxed_str()
156}
157
158pub fn extract_macro_text(mac: &Macro) -> Box<str> {
160 let path = &mac.path;
161 format!("{}!", quote::quote!(#path).to_string().replace(' ', "")).into_boxed_str()
162}