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!(
72 "Failed to parse Rust code: {}",
73 content
74 )))
75 }
76
77 fn merge_visitor_results(&mut self, visitor: ClassVisitor) {
79 self.classes.extend(visitor.classes);
80 for (breakpoint, classes) in visitor.responsive_classes {
81 self.responsive_classes
82 .entry(breakpoint)
83 .or_default()
84 .extend(classes);
85 }
86 for (condition, classes) in visitor.conditional_classes {
87 self.conditional_classes
88 .entry(condition)
89 .or_default()
90 .extend(classes);
91 }
92 }
93
94 pub fn get_classes(&self) -> &HashSet<String> {
96 &self.classes
97 }
98
99 pub fn get_responsive_classes(&self, breakpoint: &str) -> Option<&HashSet<String>> {
101 self.responsive_classes.get(breakpoint)
102 }
103
104 pub fn get_conditional_classes(&self, condition: &str) -> Option<&HashSet<String>> {
106 self.conditional_classes.get(condition)
107 }
108
109 pub fn get_all_responsive_classes(
111 &self,
112 ) -> &std::collections::HashMap<String, HashSet<String>> {
113 &self.responsive_classes
114 }
115
116 pub fn get_all_conditional_classes(
118 &self,
119 ) -> &std::collections::HashMap<String, HashSet<String>> {
120 &self.conditional_classes
121 }
122
123 pub fn parsed_file_count(&self) -> usize {
125 self.parsed_files.len()
126 }
127
128 pub fn class_count(&self) -> usize {
130 self.classes.len()
131 }
132
133 pub fn has_parsed_file(&self, path: &str) -> bool {
135 self.parsed_files.contains(path)
136 }
137
138 pub fn clear(&mut self) {
140 self.classes.clear();
141 self.responsive_classes.clear();
142 self.conditional_classes.clear();
143 self.parsed_files.clear();
144 }
145}
146
147impl Default for AstParser {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[derive(Debug, Clone)]
155struct ClassVisitor {
156 classes: HashSet<String>,
157 responsive_classes: std::collections::HashMap<String, HashSet<String>>,
158 conditional_classes: std::collections::HashMap<String, HashSet<String>>,
159}
160
161impl ClassVisitor {
162 fn new() -> Self {
163 Self {
164 classes: HashSet::new(),
165 responsive_classes: std::collections::HashMap::new(),
166 conditional_classes: std::collections::HashMap::new(),
167 }
168 }
169
170 fn visit_file(&mut self, file: &syn::File) {
171 for item in &file.items {
172 self.visit_item(item);
173 }
174 }
175
176 fn visit_item(&mut self, item: &syn::Item) {
177 match item {
178 syn::Item::Fn(func) => {
179 for stmt in &func.block.stmts {
180 self.visit_stmt(stmt);
181 }
182 }
183 syn::Item::Impl(impl_item) => {
184 for item in &impl_item.items {
185 self.visit_impl_item(item);
186 }
187 }
188 _ => {}
189 }
190 }
191
192 fn visit_impl_item(&mut self, item: &syn::ImplItem) {
193 if let syn::ImplItem::Fn(method) = item {
194 for stmt in &method.block.stmts {
195 self.visit_stmt(stmt);
196 }
197 }
198 }
199
200 fn visit_stmt(&mut self, stmt: &syn::Stmt) {
201 match stmt {
202 syn::Stmt::Expr(expr, _) => self.visit_expr(expr),
203 syn::Stmt::Local(local) => {
204 if let Some(init) = &local.init {
205 self.visit_expr(&init.expr);
206 }
207 }
208 _ => {}
209 }
210 }
211
212 fn visit_expr(&mut self, expr: &syn::Expr) {
213 match expr {
214 syn::Expr::MethodCall(method_call) => self.visit_method_call(method_call),
215 syn::Expr::Call(call) => self.visit_call(call),
216 syn::Expr::Block(block) => {
217 for stmt in &block.block.stmts {
218 self.visit_stmt(stmt);
219 }
220 }
221 syn::Expr::If(if_expr) => {
222 self.visit_expr(&if_expr.cond);
223 self.visit_block(&if_expr.then_branch);
224 if let Some(else_branch) = &if_expr.else_branch {
225 self.visit_expr(&else_branch.1);
226 }
227 }
228 syn::Expr::Match(match_expr) => {
229 self.visit_expr(&match_expr.expr);
230 for arm in &match_expr.arms {
231 self.visit_expr(&arm.body);
232 }
233 }
234 syn::Expr::Return(return_expr) => {
235 if let Some(expr) = &return_expr.expr {
236 self.visit_expr(expr);
237 }
238 }
239 syn::Expr::Assign(assign_expr) => {
240 self.visit_expr(&assign_expr.right);
241 }
242 _ => {}
243 }
244 }
245
246 fn visit_block(&mut self, block: &syn::Block) {
247 for stmt in &block.stmts {
248 self.visit_stmt(stmt);
249 }
250 }
251
252 fn visit_method_call(&mut self, method_call: &syn::ExprMethodCall) {
253 let method_name = method_call.method.to_string();
254
255 if self.is_class_builder_method(&method_name) {
257 self.extract_class_from_method_call(method_call, &method_name);
258 }
259
260 self.visit_expr(&method_call.receiver);
263
264 for arg in &method_call.args {
266 self.visit_expr(arg);
267 }
268 }
269
270 fn visit_call(&mut self, call: &syn::ExprCall) {
271 if let syn::Expr::Path(path) = &*call.func {
273 if let Some(ident) = path.path.get_ident() {
274 if ident == "class" {
275 self.extract_direct_class_call(call);
276 }
277 }
278 }
279
280 for arg in &call.args {
282 self.visit_expr(arg);
283 }
284 }
285
286 fn is_class_builder_method(&self, method_name: &str) -> bool {
287 matches!(
288 method_name,
289 "class"
290 | "padding"
291 | "margin"
292 | "background_color"
293 | "text_color"
294 | "border_color"
295 | "ring_color"
296 | "width"
297 | "height"
298 | "display"
299 | "flex"
300 | "grid"
301 | "responsive"
302 | "conditional"
303 | "custom"
304 )
305 }
306
307 fn extract_class_from_method_call(
308 &mut self,
309 method_call: &syn::ExprMethodCall,
310 method_name: &str,
311 ) {
312 if let Some(arg) = method_call.args.first() {
313 match method_name {
314 "class" => {
315 if let Ok(class_name) = self.extract_string_literal(arg) {
316 self.classes.insert(class_name);
317 }
318 }
319 "padding" => {
320 if let Ok(spacing_value) = self.extract_spacing_value(arg) {
321 self.classes.insert(format!("p-{}", spacing_value));
322 }
323 }
324 "margin" => {
325 if let Ok(spacing_value) = self.extract_spacing_value(arg) {
326 self.classes.insert(format!("m-{}", spacing_value));
327 }
328 }
329 "background_color" => {
330 if let Ok(color_value) = self.extract_color_value(arg) {
331 self.classes.insert(format!("bg-{}", color_value));
332 }
333 }
334 "text_color" => {
335 if let Ok(color_value) = self.extract_color_value(arg) {
336 self.classes.insert(format!("text-{}", color_value));
337 }
338 }
339 "responsive" => {
340 self.extract_responsive_classes(method_call);
341 }
342 "conditional" => {
343 self.extract_conditional_classes(method_call);
344 }
345 _ => {}
346 }
347 }
348 }
349
350 fn extract_direct_class_call(&mut self, call: &syn::ExprCall) {
351 if let Some(arg) = call.args.first() {
352 if let Ok(class_name) = self.extract_string_literal(arg) {
353 self.classes.insert(class_name);
354 }
355 }
356 }
357
358 fn extract_string_literal(&self, expr: &syn::Expr) -> Result<String> {
359 match expr {
360 syn::Expr::Lit(syn::ExprLit {
361 lit: syn::Lit::Str(lit_str),
362 ..
363 }) => Ok(lit_str.value()),
364 _ => Err(TailwindError::build("Expected string literal".to_string())),
365 }
366 }
367
368 fn extract_spacing_value(&self, expr: &syn::Expr) -> Result<String> {
369 match expr {
370 syn::Expr::Lit(syn::ExprLit {
371 lit: syn::Lit::Int(lit_int),
372 ..
373 }) => Ok(lit_int.to_string()),
374 syn::Expr::Path(path) => {
375 if let Some(ident) = path.path.get_ident() {
376 Ok(ident.to_string().to_lowercase())
377 } else {
378 Err(TailwindError::build("Expected identifier".to_string()))
379 }
380 }
381 _ => Err(TailwindError::build("Expected spacing value".to_string())),
382 }
383 }
384
385 fn extract_color_value(&self, expr: &syn::Expr) -> Result<String> {
386 match expr {
387 syn::Expr::Path(path) => {
388 if let Some(ident) = path.path.get_ident() {
389 Ok(ident.to_string().to_lowercase())
390 } else {
391 Err(TailwindError::build(
392 "Expected color identifier".to_string(),
393 ))
394 }
395 }
396 _ => Err(TailwindError::build("Expected color value".to_string())),
397 }
398 }
399
400 fn extract_responsive_classes(&mut self, method_call: &syn::ExprMethodCall) {
401 if let Some(arg) = method_call.args.first() {
404 if let Ok(breakpoint) = self.extract_string_literal(arg) {
405 self.responsive_classes
407 .entry(breakpoint)
408 .or_default()
409 .insert("responsive-class".to_string());
410 }
411 }
412 }
413
414 fn extract_conditional_classes(&mut self, method_call: &syn::ExprMethodCall) {
415 if let Some(arg) = method_call.args.first() {
418 if let Ok(condition) = self.extract_string_literal(arg) {
419 self.conditional_classes
421 .entry(condition)
422 .or_default()
423 .insert("conditional-class".to_string());
424 }
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_ast_parser_creation() {
435 let parser = AstParser::new();
436 assert_eq!(parser.class_count(), 0);
437 assert_eq!(parser.parsed_file_count(), 0);
438 }
439
440 #[test]
441 fn test_parse_content() {
442 let mut parser = AstParser::new();
443 let content = r#"
444 use tailwind_rs_core::ClassBuilder;
445
446 fn create_button() -> String {
447 ClassBuilder::new()
448 .class("px-4")
449 .class("py-2")
450 .class("bg-blue-500")
451 .build_string()
452 }
453 "#;
454
455 parser.parse_content(content).unwrap();
456
457 println!("Extracted classes: {:?}", parser.get_classes());
459
460 assert!(parser.get_classes().contains("px-4"));
462 assert!(parser.get_classes().contains("py-2"));
463 assert!(parser.get_classes().contains("bg-blue-500"));
464 }
465
466 #[test]
467 fn test_parse_file() {
468 let mut parser = AstParser::new();
469 let temp_file = std::env::temp_dir().join("test_rust_file.rs");
470
471 let content = r#"
472 use tailwind_rs_core::ClassBuilder;
473
474 fn test() -> String {
475 ClassBuilder::new().class("test-class").build_string()
476 }
477 "#;
478
479 std::fs::write(&temp_file, content).unwrap();
480
481 parser.parse_file(&temp_file).unwrap();
482
483 assert!(parser.get_classes().contains("test-class"));
485 assert_eq!(parser.parsed_file_count(), 1);
486
487 std::fs::remove_file(&temp_file).unwrap();
489 }
490
491 #[test]
492 fn test_clear() {
493 let mut parser = AstParser::new();
494 let content = r#"
495 ClassBuilder::new().class("test-class").to_string()
496 "#;
497
498 parser.parse_content(content).unwrap();
499 parser.clear();
503 assert_eq!(parser.class_count(), 0);
504 assert_eq!(parser.parsed_file_count(), 0);
505 }
506
507 #[test]
508 fn test_invalid_rust_code() {
509 let mut parser = AstParser::new();
510 let content = "invalid rust code {";
511
512 let result = parser.parse_content(content);
513 assert!(result.is_err());
514 }
515}