1use anyhow::Result;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PatternMatch {
18 pub file: PathBuf,
19 pub line: usize,
20 pub column: usize,
21 pub pattern: String,
22 pub tool: DxToolType,
23 pub component_name: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum DxToolType {
29 Ui, Icons, Fonts, Style, I18n, Auth, Check, Custom(String),
37}
38
39impl DxToolType {
40 pub fn prefix(&self) -> &str {
41 match self {
42 DxToolType::Ui => "dx",
43 DxToolType::Icons => "dxi",
44 DxToolType::Fonts => "dxf",
45 DxToolType::Style => "dxs",
46 DxToolType::I18n => "dxt",
47 DxToolType::Auth => "dxa",
48 DxToolType::Check => "dxc",
49 DxToolType::Custom(prefix) => prefix,
50 }
51 }
52
53 pub fn tool_name(&self) -> &str {
54 match self {
55 DxToolType::Ui => "dx-ui",
56 DxToolType::Icons => "dx-icons",
57 DxToolType::Fonts => "dx-fonts",
58 DxToolType::Style => "dx-style",
59 DxToolType::I18n => "dx-i18n",
60 DxToolType::Auth => "dx-auth",
61 DxToolType::Check => "dx-check",
62 DxToolType::Custom(name) => name,
63 }
64 }
65
66 pub fn from_prefix(prefix: &str) -> Self {
67 match prefix {
68 "dx" => DxToolType::Ui,
69 "dxi" => DxToolType::Icons,
70 "dxf" => DxToolType::Fonts,
71 "dxs" => DxToolType::Style,
72 "dxt" => DxToolType::I18n,
73 "dxa" => DxToolType::Auth,
74 "dxc" => DxToolType::Check,
75 other => DxToolType::Custom(other.to_string()),
76 }
77 }
78}
79
80pub struct PatternDetector {
82 patterns: HashMap<DxToolType, Regex>,
83}
84
85impl PatternDetector {
86 pub fn new() -> Result<Self> {
88 let mut patterns = HashMap::new();
89
90 patterns.insert(
92 DxToolType::Ui,
93 Regex::new(r"\bdx([A-Z][a-zA-Z0-9]*)\b")?,
94 );
95
96 patterns.insert(
98 DxToolType::Icons,
99 Regex::new(r"\bdxi([A-Z][a-zA-Z0-9]*)\b")?,
100 );
101
102 patterns.insert(
104 DxToolType::Fonts,
105 Regex::new(r"\bdxf([A-Z][a-zA-Z0-9]*)\b")?,
106 );
107
108 patterns.insert(
110 DxToolType::Style,
111 Regex::new(r"\bdxs([A-Z][a-zA-Z0-9]*)\b")?,
112 );
113
114 patterns.insert(
116 DxToolType::I18n,
117 Regex::new(r"\bdxt([A-Z][a-zA-Z0-9]*)\b")?,
118 );
119
120 patterns.insert(
122 DxToolType::Auth,
123 Regex::new(r"\bdxa([A-Z][a-zA-Z0-9]*)\b")?,
124 );
125
126 Ok(Self { patterns })
127 }
128
129 pub fn detect_in_file(&self, path: &Path, content: &str) -> Result<Vec<PatternMatch>> {
131 let mut matches = Vec::new();
132
133 for (line_idx, line) in content.lines().enumerate() {
134 for (tool, regex) in &self.patterns {
135 for cap in regex.captures_iter(line) {
136 if let Some(m) = cap.get(0) {
137 let component_name = cap.get(1).map(|c| c.as_str()).unwrap_or("");
138
139 matches.push(PatternMatch {
140 file: path.to_path_buf(),
141 line: line_idx + 1,
142 column: m.start() + 1,
143 pattern: m.as_str().to_string(),
144 tool: tool.clone(),
145 component_name: component_name.to_string(),
146 });
147 }
148 }
149 }
150 }
151
152 Ok(matches)
153 }
154
155 pub fn detect_in_files(
157 &self,
158 files: &[(PathBuf, String)],
159 ) -> Result<Vec<PatternMatch>> {
160 let mut all_matches = Vec::new();
161
162 for (path, content) in files {
163 let matches = self.detect_in_file(path, content)?;
164 all_matches.extend(matches);
165 }
166
167 Ok(all_matches)
168 }
169
170 pub fn group_by_tool(
172 &self,
173 matches: Vec<PatternMatch>,
174 ) -> HashMap<DxToolType, Vec<PatternMatch>> {
175 let mut grouped: HashMap<DxToolType, Vec<PatternMatch>> = HashMap::new();
176
177 for m in matches {
178 grouped.entry(m.tool.clone()).or_default().push(m);
179 }
180
181 grouped
182 }
183
184 pub fn has_patterns(&self, content: &str) -> bool {
186 self.patterns
187 .values()
188 .any(|regex| regex.is_match(content))
189 }
190
191 pub fn extract_components(&self, matches: &[PatternMatch]) -> Vec<String> {
193 let mut components: Vec<String> = matches
194 .iter()
195 .map(|m| m.component_name.clone())
196 .collect();
197
198 components.sort();
199 components.dedup();
200 components
201 }
202}
203
204impl Default for PatternDetector {
205 fn default() -> Self {
206 Self::new().expect("Failed to initialize pattern detector")
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Position {
213 pub line: usize,
214 pub character: usize,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Range {
220 pub start: Position,
221 pub end: Position,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct InjectionPoint {
227 pub file: PathBuf,
228 pub range: Range,
229 pub component: String,
230 pub tool: DxToolType,
231 pub import_needed: bool,
232}
233
234pub fn analyze_for_injection(
236 path: &Path,
237 content: &str,
238 matches: &[PatternMatch],
239) -> Vec<InjectionPoint> {
240 let mut injections = Vec::new();
241
242 let has_imports = content.contains("import") || content.contains("require");
244
245 for m in matches {
246 injections.push(InjectionPoint {
247 file: path.to_path_buf(),
248 range: Range {
249 start: Position {
250 line: m.line - 1,
251 character: m.column - 1,
252 },
253 end: Position {
254 line: m.line - 1,
255 character: m.column + m.pattern.len() - 1,
256 },
257 },
258 component: m.component_name.clone(),
259 tool: m.tool.clone(),
260 import_needed: !has_imports,
261 });
262 }
263
264 injections
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_pattern_detection() {
273 let detector = PatternDetector::new().unwrap();
274 let content = r#"
275 const MyComponent = () => {
276 return (
277 <div>
278 <dxButton>Click</dxButton>
279 <dxiHome size={24} />
280 <dxfRoboto>Hello</dxfRoboto>
281 </div>
282 );
283 };
284 "#;
285
286 let matches = detector
287 .detect_in_file(Path::new("test.tsx"), content)
288 .unwrap();
289
290 assert!(matches.len() >= 3, "Expected at least 3 matches, got {}", matches.len());
292 assert!(matches.iter().any(|m| m.tool == DxToolType::Ui));
293 assert!(matches.iter().any(|m| m.tool == DxToolType::Icons));
294 assert!(matches.iter().any(|m| m.tool == DxToolType::Fonts));
295 }
296
297 #[test]
298 fn test_component_extraction() {
299 let detector = PatternDetector::new().unwrap();
300 let content = "dxButton dxButton dxInput dxCard";
301
302 let matches = detector
303 .detect_in_file(Path::new("test.tsx"), content)
304 .unwrap();
305 let components = detector.extract_components(&matches);
306
307 assert_eq!(components.len(), 3);
308 assert!(components.contains(&"Button".to_string()));
309 assert!(components.contains(&"Input".to_string()));
310 assert!(components.contains(&"Card".to_string()));
311 }
312
313 #[test]
314 fn test_tool_prefix() {
315 assert_eq!(DxToolType::Ui.prefix(), "dx");
316 assert_eq!(DxToolType::Icons.prefix(), "dxi");
317 assert_eq!(DxToolType::Fonts.prefix(), "dxf");
318 }
319
320 #[test]
321 fn test_has_patterns() {
322 let detector = PatternDetector::new().unwrap();
323
324 assert!(detector.has_patterns("const x = dxButton;"));
325 assert!(detector.has_patterns("<dxiHome />"));
326 assert!(!detector.has_patterns("const x = regular;"));
327 }
328}