tailwind_rs_core/
ast_parser.rs1use crate::error::{Result, TailwindError};
7use std::collections::HashSet;
8use std::path::Path;
9
10#[derive(Debug, Clone)]
12pub struct AstParser {
13 classes: HashSet<String>,
15 responsive_classes: std::collections::HashMap<String, HashSet<String>>,
17 conditional_classes: std::collections::HashMap<String, HashSet<String>>,
19 parsed_files: HashSet<String>,
21}
22
23impl AstParser {
24 pub fn new() -> Self {
26 Self {
27 classes: HashSet::new(),
28 responsive_classes: std::collections::HashMap::new(),
29 conditional_classes: std::collections::HashMap::new(),
30 parsed_files: HashSet::new(),
31 }
32 }
33
34 pub fn parse_file(&mut self, path: &Path) -> Result<()> {
36 let content = std::fs::read_to_string(path)
37 .map_err(|e| TailwindError::build(format!("Failed to read file {:?}: {}", path, e)))?;
38
39 self.parse_content(&content)?;
40 self.parsed_files.insert(path.to_string_lossy().to_string());
41
42 Ok(())
43 }
44
45 pub fn parse_content(&mut self, content: &str) -> Result<()> {
47 if let Ok(syntax_tree) = syn::parse_file(content) {
49 let mut visitor = ClassVisitor::new();
50 visitor.visit_file(&syntax_tree);
51 self.merge_visitor_results(visitor);
52 return Ok(());
53 }
54
55 if let Ok(expr) = syn::parse_str::<syn::Expr>(content) {
57 let mut visitor = ClassVisitor::new();
58 visitor.visit_expr(&expr);
59 self.merge_visitor_results(visitor);
60 return Ok(());
61 }
62
63 if let Ok(stmt) = syn::parse_str::<syn::Stmt>(content) {
65 let mut visitor = ClassVisitor::new();
66 visitor.visit_stmt(&stmt);
67 self.merge_visitor_results(visitor);
68 return Ok(());
69 }
70
71 Err(TailwindError::build(format!("Failed to parse Rust code: {}", content)))
72 }
73
74 fn merge_visitor_results(&mut self, visitor: ClassVisitor) {
76 self.classes.extend(visitor.classes);
77 for (breakpoint, classes) in visitor.responsive_classes {
78 self.responsive_classes.entry(breakpoint).or_default().extend(classes);
79 }
80 for (condition, classes) in visitor.conditional_classes {
81 self.conditional_classes.entry(condition).or_default().extend(classes);
82 }
83 }
84
85 pub fn get_classes(&self) -> &HashSet<String> {
87 &self.classes
88 }
89
90 pub fn get_responsive_classes(&self, breakpoint: &str) -> Option<&HashSet<String>> {
92 self.responsive_classes.get(breakpoint)
93 }
94
95 pub fn get_conditional_classes(&self, condition: &str) -> Option<&HashSet<String>> {
97 self.conditional_classes.get(condition)
98 }
99
100 pub fn get_all_responsive_classes(&self) -> &std::collections::HashMap<String, HashSet<String>> {
102 &self.responsive_classes
103 }
104
105 pub fn get_all_conditional_classes(&self) -> &std::collections::HashMap<String, HashSet<String>> {
107 &self.conditional_classes
108 }
109
110 pub fn parsed_file_count(&self) -> usize {
112 self.parsed_files.len()
113 }
114
115 pub fn class_count(&self) -> usize {
117 self.classes.len()
118 }
119
120 pub fn has_parsed_file(&self, path: &str) -> bool {
122 self.parsed_files.contains(path)
123 }
124
125 pub fn clear(&mut self) {
127 self.classes.clear();
128 self.responsive_classes.clear();
129 self.conditional_classes.clear();
130 self.parsed_files.clear();
131 }
132}
133
134impl Default for AstParser {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140#[derive(Debug, Clone)]
142struct ClassVisitor {
143 classes: HashSet<String>,
144 responsive_classes: std::collections::HashMap<String, HashSet<String>>,
145 conditional_classes: std::collections::HashMap<String, HashSet<String>>,
146}
147
148impl ClassVisitor {
149 fn new() -> Self {
150 Self {
151 classes: HashSet::new(),
152 responsive_classes: std::collections::HashMap::new(),
153 conditional_classes: std::collections::HashMap::new(),
154 }
155 }
156
157 fn visit_file(&mut self, file: &syn::File) {
158 for item in &file.items {
159 self.visit_item(item);
160 }
161 }
162
163 fn visit_item(&mut self, item: &syn::Item) {
164 match item {
165 syn::Item::Fn(func) => {
166 for stmt in &func.block.stmts {
167 self.visit_stmt(stmt);
168 }
169 }
170 syn::Item::Impl(impl_item) => {
171 for item in &impl_item.items {
172 self.visit_impl_item(item);
173 }
174 }
175 _ => {}
176 }
177 }
178
179 fn visit_impl_item(&mut self, item: &syn::ImplItem) {
180 match item {
181 syn::ImplItem::Fn(method) => {
182 for stmt in &method.block.stmts {
183 self.visit_stmt(stmt);
184 }
185 }
186 _ => {}
187 }
188 }
189
190 fn visit_stmt(&mut self, stmt: &syn::Stmt) {
191 match stmt {
192 syn::Stmt::Expr(expr, _) => self.visit_expr(expr),
193 syn::Stmt::Local(local) => {
194 if let Some(init) = &local.init {
195 self.visit_expr(&init.expr);
196 }
197 }
198 _ => {}
199 }
200 }
201
202 fn visit_expr(&mut self, expr: &syn::Expr) {
203 match expr {
204 syn::Expr::MethodCall(method_call) => self.visit_method_call(method_call),
205 syn::Expr::Call(call) => self.visit_call(call),
206 syn::Expr::Block(block) => {
207 for stmt in &block.block.stmts {
208 self.visit_stmt(stmt);
209 }
210 }
211 syn::Expr::If(if_expr) => {
212 self.visit_expr(&if_expr.cond);
213 self.visit_block(&if_expr.then_branch);
214 if let Some(else_branch) = &if_expr.else_branch {
215 self.visit_expr(&else_branch.1);
216 }
217 }
218 syn::Expr::Match(match_expr) => {
219 self.visit_expr(&match_expr.expr);
220 for arm in &match_expr.arms {
221 self.visit_expr(&arm.body);
222 }
223 }
224 syn::Expr::Return(return_expr) => {
225 if let Some(expr) = &return_expr.expr {
226 self.visit_expr(expr);
227 }
228 }
229 syn::Expr::Assign(assign_expr) => {
230 self.visit_expr(&assign_expr.right);
231 }
232 _ => {}
233 }
234 }
235
236 fn visit_block(&mut self, block: &syn::Block) {
237 for stmt in &block.stmts {
238 self.visit_stmt(stmt);
239 }
240 }
241
242 fn visit_method_call(&mut self, method_call: &syn::ExprMethodCall) {
243 let method_name = method_call.method.to_string();
244
245 if self.is_class_builder_method(&method_name) {
247 self.extract_class_from_method_call(method_call, &method_name);
248 }
249
250 self.visit_expr(&method_call.receiver);
253
254 for arg in &method_call.args {
256 self.visit_expr(arg);
257 }
258 }
259
260 fn visit_call(&mut self, call: &syn::ExprCall) {
261 if let syn::Expr::Path(path) = &*call.func {
263 if let Some(ident) = path.path.get_ident() {
264 if ident == "class" {
265 self.extract_direct_class_call(call);
266 }
267 }
268 }
269
270 for arg in &call.args {
272 self.visit_expr(arg);
273 }
274 }
275
276 fn is_class_builder_method(&self, method_name: &str) -> bool {
277 matches!(method_name,
278 "class" | "padding" | "margin" | "background_color" | "text_color" |
279 "border_color" | "ring_color" | "width" | "height" | "display" |
280 "flex" | "grid" | "responsive" | "conditional" | "custom"
281 )
282 }
283
284 fn extract_class_from_method_call(&mut self, method_call: &syn::ExprMethodCall, method_name: &str) {
285 if let Some(arg) = method_call.args.first() {
286 match method_name {
287 "class" => {
288 if let Ok(class_name) = self.extract_string_literal(arg) {
289 self.classes.insert(class_name);
290 }
291 }
292 "padding" => {
293 if let Ok(spacing_value) = self.extract_spacing_value(arg) {
294 self.classes.insert(format!("p-{}", spacing_value));
295 }
296 }
297 "margin" => {
298 if let Ok(spacing_value) = self.extract_spacing_value(arg) {
299 self.classes.insert(format!("m-{}", spacing_value));
300 }
301 }
302 "background_color" => {
303 if let Ok(color_value) = self.extract_color_value(arg) {
304 self.classes.insert(format!("bg-{}", color_value));
305 }
306 }
307 "text_color" => {
308 if let Ok(color_value) = self.extract_color_value(arg) {
309 self.classes.insert(format!("text-{}", color_value));
310 }
311 }
312 "responsive" => {
313 self.extract_responsive_classes(method_call);
314 }
315 "conditional" => {
316 self.extract_conditional_classes(method_call);
317 }
318 _ => {}
319 }
320 }
321 }
322
323 fn extract_direct_class_call(&mut self, call: &syn::ExprCall) {
324 if let Some(arg) = call.args.first() {
325 if let Ok(class_name) = self.extract_string_literal(arg) {
326 self.classes.insert(class_name);
327 }
328 }
329 }
330
331 fn extract_string_literal(&self, expr: &syn::Expr) -> Result<String> {
332 match expr {
333 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) => {
334 Ok(lit_str.value())
335 }
336 _ => Err(TailwindError::build("Expected string literal".to_string()))
337 }
338 }
339
340 fn extract_spacing_value(&self, expr: &syn::Expr) -> Result<String> {
341 match expr {
342 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(lit_int), .. }) => {
343 Ok(lit_int.to_string())
344 }
345 syn::Expr::Path(path) => {
346 if let Some(ident) = path.path.get_ident() {
347 Ok(ident.to_string().to_lowercase())
348 } else {
349 Err(TailwindError::build("Expected identifier".to_string()))
350 }
351 }
352 _ => Err(TailwindError::build("Expected spacing value".to_string()))
353 }
354 }
355
356 fn extract_color_value(&self, expr: &syn::Expr) -> Result<String> {
357 match expr {
358 syn::Expr::Path(path) => {
359 if let Some(ident) = path.path.get_ident() {
360 Ok(ident.to_string().to_lowercase())
361 } else {
362 Err(TailwindError::build("Expected color identifier".to_string()))
363 }
364 }
365 _ => Err(TailwindError::build("Expected color value".to_string()))
366 }
367 }
368
369 fn extract_responsive_classes(&mut self, method_call: &syn::ExprMethodCall) {
370 if let Some(arg) = method_call.args.first() {
373 if let Ok(breakpoint) = self.extract_string_literal(arg) {
374 self.responsive_classes.entry(breakpoint).or_default().insert("responsive-class".to_string());
376 }
377 }
378 }
379
380 fn extract_conditional_classes(&mut self, method_call: &syn::ExprMethodCall) {
381 if let Some(arg) = method_call.args.first() {
384 if let Ok(condition) = self.extract_string_literal(arg) {
385 self.conditional_classes.entry(condition).or_default().insert("conditional-class".to_string());
387 }
388 }
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_ast_parser_creation() {
398 let parser = AstParser::new();
399 assert_eq!(parser.class_count(), 0);
400 assert_eq!(parser.parsed_file_count(), 0);
401 }
402
403 #[test]
404 fn test_parse_content() {
405 let mut parser = AstParser::new();
406 let content = r#"
407 use tailwind_rs_core::ClassBuilder;
408
409 fn create_button() -> String {
410 ClassBuilder::new()
411 .class("px-4")
412 .class("py-2")
413 .class("bg-blue-500")
414 .build_string()
415 }
416 "#;
417
418 parser.parse_content(content).unwrap();
419
420 println!("Extracted classes: {:?}", parser.get_classes());
422
423 assert!(parser.get_classes().contains("px-4"));
425 assert!(parser.get_classes().contains("py-2"));
426 assert!(parser.get_classes().contains("bg-blue-500"));
427 }
428
429 #[test]
430 fn test_parse_file() {
431 let mut parser = AstParser::new();
432 let temp_file = std::env::temp_dir().join("test_rust_file.rs");
433
434 let content = r#"
435 use tailwind_rs_core::ClassBuilder;
436
437 fn test() -> String {
438 ClassBuilder::new().class("test-class").build_string()
439 }
440 "#;
441
442 std::fs::write(&temp_file, content).unwrap();
443
444 parser.parse_file(&temp_file).unwrap();
445
446 assert!(parser.get_classes().contains("test-class"));
448 assert_eq!(parser.parsed_file_count(), 1);
449
450 std::fs::remove_file(&temp_file).unwrap();
452 }
453
454 #[test]
455 fn test_clear() {
456 let mut parser = AstParser::new();
457 let content = r#"
458 ClassBuilder::new().class("test-class").to_string()
459 "#;
460
461 parser.parse_content(content).unwrap();
462 parser.clear();
466 assert_eq!(parser.class_count(), 0);
467 assert_eq!(parser.parsed_file_count(), 0);
468 }
469
470 #[test]
471 fn test_invalid_rust_code() {
472 let mut parser = AstParser::new();
473 let content = "invalid rust code {";
474
475 let result = parser.parse_content(content);
476 assert!(result.is_err());
477 }
478}