dx_forge/
patterns.rs

1//! LSP Pattern Detection for DX Tools
2//!
3//! Detects dx-tool patterns in source code:
4//! - dxButton, dxInput, dxCard (dx-ui components)
5//! - dxiHome, dxiUser, dxiSettings (dx-icons)
6//! - dxfRoboto, dxfInter (dx-fonts)
7//! - dxaGoogleLogin (dx-auth)
8
9use anyhow::Result;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15/// Pattern match result
16#[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/// DX Tool type
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum DxToolType {
29    Ui,       // dx-ui
30    Icons,    // dx-icons
31    Fonts,    // dx-fonts
32    Style,    // dx-style
33    I18n,     // dx-i18n
34    Auth,     // dx-auth
35    Check,    // dx-check
36    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
80/// Pattern detector for DX tool references
81pub struct PatternDetector {
82    patterns: HashMap<DxToolType, Regex>,
83}
84
85impl PatternDetector {
86    /// Create a new pattern detector
87    pub fn new() -> Result<Self> {
88        let mut patterns = HashMap::new();
89
90        // dx-ui: dxButton, dxInput, dxCard, etc.
91        patterns.insert(
92            DxToolType::Ui,
93            Regex::new(r"\bdx([A-Z][a-zA-Z0-9]*)\b")?,
94        );
95
96        // dx-icons: dxiHome, dxiUser, dxiSettings, etc.
97        patterns.insert(
98            DxToolType::Icons,
99            Regex::new(r"\bdxi([A-Z][a-zA-Z0-9]*)\b")?,
100        );
101
102        // dx-fonts: dxfRoboto, dxfInter, dxfPoppins, etc.
103        patterns.insert(
104            DxToolType::Fonts,
105            Regex::new(r"\bdxf([A-Z][a-zA-Z0-9]*)\b")?,
106        );
107
108        // dx-style: dxsContainer, dxsFlex, etc.
109        patterns.insert(
110            DxToolType::Style,
111            Regex::new(r"\bdxs([A-Z][a-zA-Z0-9]*)\b")?,
112        );
113
114        // dx-i18n: dxtText, dxtMessage, etc.
115        patterns.insert(
116            DxToolType::I18n,
117            Regex::new(r"\bdxt([A-Z][a-zA-Z0-9]*)\b")?,
118        );
119
120        // dx-auth: dxaGoogleLogin, dxaGithubLogin, etc.
121        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    /// Detect patterns in a file
130    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    /// Detect patterns in multiple files
156    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    /// Group matches by tool type
171    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    /// Check if content contains any dx patterns
185    pub fn has_patterns(&self, content: &str) -> bool {
186        self.patterns
187            .values()
188            .any(|regex| regex.is_match(content))
189    }
190
191    /// Extract unique component names from matches
192    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/// LSP-style position
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Position {
213    pub line: usize,
214    pub character: usize,
215}
216
217/// LSP-style range
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Range {
220    pub start: Position,
221    pub end: Position,
222}
223
224/// Component injection point
225#[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
234/// Analyze file for component injection
235pub fn analyze_for_injection(
236    path: &Path,
237    content: &str,
238    matches: &[PatternMatch],
239) -> Vec<InjectionPoint> {
240    let mut injections = Vec::new();
241
242    // Check if imports already exist
243    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        // Note: Regex patterns also match inside tags, so we may get more matches
291        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}