Skip to main content

tailwind_rs_scanner/
tree_sitter_parser.rs

1//! Tree-sitter parser implementation
2//!
3//! This module provides tree-sitter integration for
4//! accurate AST-based parsing and class extraction.
5
6use crate::error::{Result, ScannerError};
7
8/// Tree-sitter parser for different languages
9#[derive(Debug)]
10pub struct TreeSitterParser {
11    /// Supported languages
12    languages: Vec<LanguageSupport>,
13}
14
15/// Language support information
16#[derive(Debug, Clone)]
17pub struct LanguageSupport {
18    /// Language name
19    pub name: String,
20    /// File extensions
21    pub extensions: Vec<String>,
22    /// Whether language is supported
23    pub supported: bool,
24}
25
26/// Parse result
27#[derive(Debug, Clone)]
28pub struct ParseResult {
29    /// Parsed AST
30    pub ast: String,
31    /// Parse errors
32    pub errors: Vec<String>,
33    /// Language used
34    pub language: String,
35}
36
37impl TreeSitterParser {
38    /// Create a new tree-sitter parser
39    pub fn new() -> Self {
40        let mut languages = Vec::new();
41
42        // Initialize supported languages
43        languages.push(LanguageSupport {
44            name: "rust".to_string(),
45            extensions: vec!["rs".to_string()],
46            supported: true,
47        });
48
49        languages.push(LanguageSupport {
50            name: "javascript".to_string(),
51            extensions: vec!["js".to_string(), "jsx".to_string()],
52            supported: true,
53        });
54
55        languages.push(LanguageSupport {
56            name: "typescript".to_string(),
57            extensions: vec!["ts".to_string(), "tsx".to_string()],
58            supported: true,
59        });
60
61        languages.push(LanguageSupport {
62            name: "html".to_string(),
63            extensions: vec!["html".to_string(), "htm".to_string()],
64            supported: true,
65        });
66
67        Self { languages }
68    }
69
70    /// Parse content with tree-sitter
71    pub fn parse(&self, content: &str, language: &str) -> Result<ParseResult> {
72        if !self.is_language_supported(language) {
73            return Err(ScannerError::UnsupportedLanguage(language.to_string()));
74        }
75
76        // For now, implement a basic parser that extracts class names
77        // In a full implementation, this would use the tree-sitter crate
78        let errors = Vec::new();
79        let ast = self.generate_ast(content, language);
80
81        Ok(ParseResult {
82            ast,
83            errors,
84            language: language.to_string(),
85        })
86    }
87
88    /// Generate a basic AST representation
89    fn generate_ast(&self, content: &str, language: &str) -> String {
90        match language {
91            "html" => self.parse_html_ast(content),
92            "javascript" | "js" => self.parse_js_ast(content),
93            "typescript" | "ts" => self.parse_ts_ast(content),
94            "rust" => self.parse_rust_ast(content),
95            _ => format!("Basic AST for {}: {}", language, content),
96        }
97    }
98
99    /// Parse HTML and extract class attributes
100    fn parse_html_ast(&self, content: &str) -> String {
101        let mut ast_nodes = Vec::new();
102        let mut in_class_attr = false;
103        let mut current_class = String::new();
104
105        for (i, c) in content.char_indices() {
106            if content[i..].starts_with("class=") {
107                in_class_attr = true;
108                continue;
109            }
110
111            if in_class_attr {
112                if c == '"' || c == '\'' {
113                    if !current_class.is_empty() {
114                        ast_nodes.push(format!("class: {}", current_class));
115                        current_class.clear();
116                    }
117                    in_class_attr = false;
118                } else if c != ' ' {
119                    current_class.push(c);
120                }
121            }
122        }
123
124        if ast_nodes.is_empty() {
125            "HTML AST: No class attributes found".to_string()
126        } else {
127            format!("HTML AST: {}", ast_nodes.join(", "))
128        }
129    }
130
131    /// Parse JavaScript and extract string literals that might contain classes
132    fn parse_js_ast(&self, content: &str) -> String {
133        let mut ast_nodes = Vec::new();
134        let mut in_string = false;
135        let mut current_string = String::new();
136        let mut string_delimiter = '"';
137
138        for c in content.chars() {
139            if !in_string && (c == '"' || c == '\'') {
140                in_string = true;
141                string_delimiter = c;
142                current_string.clear();
143            } else if in_string {
144                if c == string_delimiter {
145                    // Check if this string contains potential Tailwind classes
146                    if self.looks_like_tailwind_classes(&current_string) {
147                        ast_nodes.push(format!("string: {}", current_string));
148                    }
149                    in_string = false;
150                } else {
151                    current_string.push(c);
152                }
153            }
154        }
155
156        if ast_nodes.is_empty() {
157            "JS AST: No class strings found".to_string()
158        } else {
159            format!("JS AST: {}", ast_nodes.join(", "))
160        }
161    }
162
163    /// Parse TypeScript (similar to JavaScript)
164    fn parse_ts_ast(&self, content: &str) -> String {
165        // TypeScript parsing is similar to JavaScript
166        self.parse_js_ast(content)
167    }
168
169    /// Parse Rust and extract string literals
170    fn parse_rust_ast(&self, content: &str) -> String {
171        let mut ast_nodes = Vec::new();
172        let mut in_string = false;
173        let mut current_string = String::new();
174        let mut string_delimiter = '"';
175
176        for c in content.chars() {
177            if !in_string && c == '"' {
178                in_string = true;
179                current_string.clear();
180            } else if in_string {
181                if c == string_delimiter {
182                    // Check if this string contains potential Tailwind classes
183                    if self.looks_like_tailwind_classes(&current_string) {
184                        ast_nodes.push(format!("string: {}", current_string));
185                    }
186                    in_string = false;
187                } else {
188                    current_string.push(c);
189                }
190            }
191        }
192
193        if ast_nodes.is_empty() {
194            "Rust AST: No class strings found".to_string()
195        } else {
196            format!("Rust AST: {}", ast_nodes.join(", "))
197        }
198    }
199
200    /// Check if a string looks like it contains Tailwind classes
201    fn looks_like_tailwind_classes(&self, s: &str) -> bool {
202        // Simple heuristic: check for common Tailwind patterns
203        s.contains("bg-")
204            || s.contains("text-")
205            || s.contains("p-")
206            || s.contains("m-")
207            || s.contains("w-")
208            || s.contains("h-")
209            || s.contains("flex")
210            || s.contains("grid")
211            || s.contains("hidden")
212            || s.contains("block")
213    }
214
215    /// Get supported languages
216    pub fn get_supported_languages(&self) -> &[LanguageSupport] {
217        &self.languages
218    }
219
220    /// Check if language is supported
221    pub fn is_language_supported(&self, language: &str) -> bool {
222        self.languages.iter().any(|lang| lang.name == language)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_tree_sitter_parser_creation() {
232        let parser = TreeSitterParser::new();
233        assert!(!parser.languages.is_empty());
234    }
235
236    #[test]
237    fn test_language_support() {
238        let parser = TreeSitterParser::new();
239        assert!(parser.is_language_supported("rust"));
240        assert!(parser.is_language_supported("javascript"));
241        assert!(!parser.is_language_supported("unknown"));
242    }
243
244    #[test]
245    fn test_parse_content() {
246        let parser = TreeSitterParser::new();
247        let content = "let class = 'p-4';";
248        let result = parser.parse(content, "javascript");
249
250        assert!(result.is_ok());
251        let result = result.unwrap();
252        assert_eq!(result.language, "javascript");
253        assert!(result.errors.is_empty());
254    }
255}