1use crate::context::TemplateContext;
11use crate::error::{Result, TemplateError};
12use std::collections::{HashMap, HashSet};
13use std::path::Path;
14
15#[derive(Debug, Clone)]
17pub struct DebugInfo {
18 pub template_name: String,
20 pub source: String,
22 pub variables_used: HashSet<String>,
24 pub functions_used: HashSet<String>,
26 pub blocks_defined: HashSet<String>,
28 pub extends_templates: Vec<String>,
30 pub includes_templates: Vec<String>,
32 pub syntax_errors: Vec<String>,
34 pub render_time_ms: Option<u64>,
36 pub memory_usage: Option<usize>,
38}
39
40#[derive(Debug)]
42pub struct TemplateDebugger {
43 verbose: bool,
45 track_variables: bool,
47 track_functions: bool,
49 validate_syntax: bool,
51 profile_performance: bool,
53}
54
55impl Default for TemplateDebugger {
56 fn default() -> Self {
57 Self {
58 verbose: false,
59 track_variables: true,
60 track_functions: true,
61 validate_syntax: true,
62 profile_performance: false,
63 }
64 }
65}
66
67impl TemplateDebugger {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn verbose(mut self, verbose: bool) -> Self {
75 self.verbose = verbose;
76 self
77 }
78
79 pub fn track_variables(mut self, track: bool) -> Self {
81 self.track_variables = track;
82 self
83 }
84
85 pub fn track_functions(mut self, track: bool) -> Self {
87 self.track_functions = track;
88 self
89 }
90
91 pub fn validate_syntax(mut self, validate: bool) -> Self {
93 self.validate_syntax = validate;
94 self
95 }
96
97 pub fn profile_performance(mut self, profile: bool) -> Self {
99 self.profile_performance = profile;
100 self
101 }
102
103 pub fn analyze(&self, template_content: &str, template_name: &str) -> Result<DebugInfo> {
109 let mut info = DebugInfo {
110 template_name: template_name.to_string(),
111 source: template_content.to_string(),
112 variables_used: HashSet::new(),
113 functions_used: HashSet::new(),
114 blocks_defined: HashSet::new(),
115 extends_templates: Vec::new(),
116 includes_templates: Vec::new(),
117 syntax_errors: Vec::new(),
118 render_time_ms: None,
119 memory_usage: None,
120 };
121
122 if self.validate_syntax {
124 self.validate_template_syntax(template_content, &mut info)?;
125 }
126
127 if self.track_variables {
129 self.extract_variables(template_content, &mut info);
130 }
131
132 if self.track_functions {
134 self.extract_functions(template_content, &mut info);
135 }
136
137 self.extract_composition_info(template_content, &mut info);
139
140 Ok(info)
141 }
142
143 fn validate_template_syntax(&self, content: &str, info: &mut DebugInfo) -> Result<()> {
145 let mut errors = Vec::new();
147
148 self.check_brace_matching(content, &mut errors);
150
151 self.check_variable_syntax(content, &mut errors);
153
154 self.check_function_syntax(content, &mut errors);
156
157 info.syntax_errors = errors;
158 Ok(())
159 }
160
161 fn check_brace_matching(&self, content: &str, errors: &mut Vec<String>) {
163 let mut stack = Vec::new();
164
165 for (i, ch) in content.char_indices() {
166 match ch {
167 '{' => {
168 if let Some(next) = content.chars().nth(i + 1) {
169 if next == '{' || next == '%' || next == '#' {
170 stack.push((ch, i));
171 }
172 }
173 }
174 '%' | '#' => {
175 if i > 0 && content.chars().nth(i - 1) == Some('{') {
177 stack.push((ch, i));
178 }
179 }
180 '}' => {
181 if let Some(next) = content.chars().nth(i + 1) {
182 if next == '}' {
183 if let Some((open, _open_pos)) = stack.pop() {
184 if !matches!((open, ch), ('{', '}')) {
185 errors.push(format!(
186 "Unmatched braces at position {}: found '{}' but expected matching '{}'",
187 i, ch, open
188 ));
189 }
190 } else {
191 errors.push(format!("Unmatched closing brace at position {}", i));
192 }
193 }
194 }
195 }
196 _ => {}
197 }
198 }
199
200 for (open, pos) in stack {
202 errors.push(format!("Unclosed '{}' at position {}", open, pos));
203 }
204 }
205
206 fn check_variable_syntax(&self, content: &str, errors: &mut Vec<String>) {
208 let var_regex = regex::Regex::new(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)").unwrap();
210
211 for cap in var_regex.captures_iter(content) {
212 if let Some(var_name) = cap.get(1) {
213 let var = var_name.as_str();
214 if var.contains(" ") {
216 errors.push(format!("Invalid variable name '{}' contains spaces", var));
217 }
218 }
219 }
220 }
221
222 fn check_function_syntax(&self, content: &str, _errors: &mut [String]) {
224 let func_regex = regex::Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap();
226
227 for cap in func_regex.captures_iter(content) {
228 if let Some(func_name) = cap.get(1) {
229 let func = func_name.as_str();
230 let known_functions = [
232 "env",
233 "now_rfc3339",
234 "sha256",
235 "toml_encode",
236 "fake_name",
237 "fake_email",
238 "uuid_v4",
239 "include",
240 "extends",
241 ];
242
243 if !known_functions.contains(&func) && !func.starts_with("fake_") {
244 if self.verbose {
246 eprintln!("Warning: Unknown function '{}' in template", func);
247 }
248 }
249 }
250 }
251 }
252
253 fn extract_variables(&self, content: &str, info: &mut DebugInfo) {
255 let var_regex = regex::Regex::new(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)").unwrap();
257
258 for cap in var_regex.captures_iter(content) {
259 if let Some(var_name) = cap.get(1) {
260 info.variables_used.insert(var_name.as_str().to_string());
261 }
262 }
263 }
264
265 fn extract_functions(&self, content: &str, info: &mut DebugInfo) {
267 let func_regex = regex::Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap();
269
270 for cap in func_regex.captures_iter(content) {
271 if let Some(func_name) = cap.get(1) {
272 info.functions_used.insert(func_name.as_str().to_string());
273 }
274 }
275 }
276
277 fn extract_composition_info(&self, content: &str, info: &mut DebugInfo) {
279 let extends_regex = regex::Regex::new(r#"extends\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
281
282 for cap in extends_regex.captures_iter(content) {
283 if let Some(template) = cap.get(1) {
284 info.extends_templates.push(template.as_str().to_string());
285 }
286 }
287
288 let include_regex = regex::Regex::new(r#"include\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
290
291 for cap in include_regex.captures_iter(content) {
292 if let Some(template) = cap.get(1) {
293 info.includes_templates.push(template.as_str().to_string());
294 }
295 }
296
297 let block_regex = regex::Regex::new(r#"block\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
299
300 for cap in block_regex.captures_iter(content) {
301 if let Some(block_name) = cap.get(1) {
302 info.blocks_defined.insert(block_name.as_str().to_string());
303 }
304 }
305 }
306
307 pub fn debug_render(
314 &self,
315 template_content: &str,
316 context: &TemplateContext,
317 template_name: &str,
318 ) -> Result<DebugInfo> {
319 let mut info = self.analyze(template_content, template_name)?;
320
321 if self.profile_performance {
322 let start = std::time::Instant::now();
323
324 let result = crate::render_with_context(template_content, context);
326
327 let elapsed = start.elapsed();
328 info.render_time_ms = Some(elapsed.as_millis() as u64);
329
330 if let Err(e) = result {
331 info.syntax_errors.push(e.to_string());
332 }
333 }
334
335 if self.verbose {
336 self.print_debug_info(&info);
337 }
338
339 Ok(info)
340 }
341
342 fn print_debug_info(&self, info: &DebugInfo) {
344 eprintln!("=== Template Debug Info ===");
345 eprintln!("Template: {}", info.template_name);
346 eprintln!("Variables used: {:?}", info.variables_used);
347 eprintln!("Functions used: {:?}", info.functions_used);
348 eprintln!("Blocks defined: {:?}", info.blocks_defined);
349 eprintln!("Extends: {:?}", info.extends_templates);
350 eprintln!("Includes: {:?}", info.includes_templates);
351
352 if !info.syntax_errors.is_empty() {
353 eprintln!("Syntax errors:");
354 for error in &info.syntax_errors {
355 eprintln!(" - {}", error);
356 }
357 }
358
359 if let Some(time) = info.render_time_ms {
360 eprintln!("Render time: {}ms", time);
361 }
362 }
363
364 pub fn find_unused_variables(
370 &self,
371 debug_info: &DebugInfo,
372 context: &TemplateContext,
373 ) -> Vec<String> {
374 let mut unused = Vec::new();
375 for var_name in context.vars.keys() {
376 if !debug_info.variables_used.contains(var_name) {
377 unused.push(var_name.clone());
378 }
379 }
380 unused
381 }
382
383 pub fn find_missing_variables(
389 &self,
390 debug_info: &DebugInfo,
391 context: &TemplateContext,
392 ) -> Vec<String> {
393 let mut missing = Vec::new();
394 for var_name in &debug_info.variables_used {
395 if !context.vars.contains_key(var_name) {
396 missing.push(var_name.clone());
397 }
398 }
399 missing
400 }
401}
402
403pub struct TemplateAnalyzer {
405 debugger: TemplateDebugger,
406}
407
408impl TemplateAnalyzer {
409 pub fn new() -> Self {
411 Self {
412 debugger: TemplateDebugger::new(),
413 }
414 }
415
416 pub fn analyze_file<P: AsRef<Path>>(&self, file_path: P) -> Result<DebugInfo> {
421 let content = std::fs::read_to_string(&file_path)
422 .map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
423
424 let file_name = file_path
425 .as_ref()
426 .file_stem()
427 .and_then(|s| s.to_str())
428 .unwrap_or("unknown");
429
430 self.debugger.analyze(&content, file_name)
431 }
432
433 pub fn analyze_directory<P: AsRef<Path>>(
438 &self,
439 dir_path: P,
440 ) -> Result<HashMap<String, DebugInfo>> {
441 use walkdir::WalkDir;
442
443 let mut results = HashMap::new();
444
445 for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
446 if entry.file_type().is_file() {
447 let path = entry.path();
448 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
449 if matches!(ext, "toml" | "tera" | "tpl" | "template") {
450 if let Ok(info) = self.analyze_file(path) {
451 let name = info.template_name.clone();
452 results.insert(name, info);
453 }
454 }
455 }
456 }
457 }
458
459 Ok(results)
460 }
461
462 pub fn find_unused_variables(
468 &self,
469 template_info: &DebugInfo,
470 context: &TemplateContext,
471 ) -> Vec<String> {
472 let used_vars: HashSet<String> = template_info.variables_used.iter().cloned().collect();
473 let context_vars: HashSet<String> = context.vars.keys().cloned().collect();
474
475 context_vars.difference(&used_vars).cloned().collect()
476 }
477
478 pub fn find_missing_variables(
484 &self,
485 template_info: &DebugInfo,
486 context: &TemplateContext,
487 ) -> Vec<String> {
488 let used_vars: HashSet<String> = template_info.variables_used.iter().cloned().collect();
489 let context_vars: HashSet<String> = context.vars.keys().cloned().collect();
490
491 used_vars.difference(&context_vars).cloned().collect()
492 }
493}
494
495pub mod lint {
497 use super::*;
498
499 pub trait LintRule {
501 fn check(&self, info: &DebugInfo) -> Vec<String>;
506 }
507
508 pub struct UnusedVariablesRule;
510
511 impl LintRule for UnusedVariablesRule {
512 fn check(&self, _info: &DebugInfo) -> Vec<String> {
513 Vec::new()
516 }
517 }
518
519 pub struct DeprecatedFunctionsRule;
521
522 impl LintRule for DeprecatedFunctionsRule {
523 fn check(&self, info: &DebugInfo) -> Vec<String> {
524 let deprecated = ["old_function", "deprecated_helper"];
525 let mut violations = Vec::new();
526
527 for func in &info.functions_used {
528 if deprecated.contains(&func.as_str()) {
529 violations.push(format!("Deprecated function '{}' used", func));
530 }
531 }
532
533 violations
534 }
535 }
536
537 pub struct ComplexityRule {
539 max_complexity: usize,
540 }
541
542 impl ComplexityRule {
543 pub fn new(max_complexity: usize) -> Self {
544 Self { max_complexity }
545 }
546 }
547
548 impl LintRule for ComplexityRule {
549 fn check(&self, info: &DebugInfo) -> Vec<String> {
550 let mut violations = Vec::new();
551
552 let complexity =
554 info.functions_used.len() + info.variables_used.len() + info.blocks_defined.len();
555
556 if complexity > self.max_complexity {
557 violations.push(format!(
558 "Template complexity {} exceeds maximum {}",
559 complexity, self.max_complexity
560 ));
561 }
562
563 violations
564 }
565 }
566
567 pub struct UndocumentedVariablesRule;
569
570 impl LintRule for UndocumentedVariablesRule {
571 fn check(&self, info: &DebugInfo) -> Vec<String> {
572 let mut violations = Vec::new();
574
575 for var in &info.variables_used {
576 let doc_pattern = format!("{{# {} #}}", var);
578 if !info.source.contains(&doc_pattern) {
579 violations.push(format!("Variable '{}' is not documented", var));
580 }
581 }
582
583 violations
584 }
585 }
586}
587
588pub struct TemplateLinter {
590 rules: Vec<Box<dyn lint::LintRule>>,
592 debugger: TemplateDebugger,
594}
595
596impl Default for TemplateLinter {
597 fn default() -> Self {
598 Self {
599 rules: Vec::new(),
600 debugger: TemplateDebugger::new(),
601 }
602 }
603}
604
605impl TemplateLinter {
606 pub fn new() -> Self {
608 Self::default()
609 }
610
611 pub fn with_rule<R: lint::LintRule + 'static>(mut self, rule: R) -> Self {
613 self.rules.push(Box::new(rule));
614 self
615 }
616
617 pub fn with_production_rules(mut self) -> Self {
619 self.rules.push(Box::new(lint::DeprecatedFunctionsRule));
620 self.rules.push(Box::new(lint::ComplexityRule::new(50))); self.rules.push(Box::new(lint::UndocumentedVariablesRule));
622 self
623 }
624
625 pub fn lint(&self, template_content: &str, template_name: &str) -> Result<Vec<String>> {
631 let mut violations = Vec::new();
632
633 let info = self.debugger.analyze(template_content, template_name)?;
635
636 for rule in &self.rules {
638 violations.extend(rule.check(&info));
639 }
640
641 Ok(violations)
642 }
643
644 pub fn lint_file<P: AsRef<Path>>(&self, file_path: P) -> Result<Vec<String>> {
649 let content = std::fs::read_to_string(&file_path)
650 .map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
651
652 let file_name = file_path
653 .as_ref()
654 .file_stem()
655 .and_then(|s| s.to_str())
656 .unwrap_or("unknown");
657
658 self.lint(&content, file_name)
659 }
660
661 pub fn lint_directory<P: AsRef<Path>>(
666 &self,
667 dir_path: P,
668 ) -> Result<HashMap<String, Vec<String>>> {
669 use walkdir::WalkDir;
670
671 let mut results = HashMap::new();
672
673 for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
674 if entry.file_type().is_file() {
675 let path = entry.path();
676 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
677 if matches!(ext, "toml" | "tera" | "tpl" | "template") {
678 match self.lint_file(path) {
679 Ok(violations) => {
680 let name = path
681 .file_stem()
682 .and_then(|s| s.to_str())
683 .unwrap_or("unknown")
684 .to_string();
685 results.insert(name, violations);
686 }
687 Err(e) => {
688 eprintln!("Warning: Failed to lint template {:?}: {}", path, e);
689 }
690 }
691 }
692 }
693 }
694 }
695
696 Ok(results)
697 }
698}
699
700pub struct DebugTemplateValidator {
702 validator: crate::validation::TemplateValidator,
704 linter: TemplateLinter,
706 debugger: TemplateDebugger,
708}
709
710impl Default for DebugTemplateValidator {
711 fn default() -> Self {
712 Self::new()
713 }
714}
715
716impl DebugTemplateValidator {
717 pub fn new() -> Self {
719 Self {
720 validator: crate::validation::TemplateValidator::new(),
721 linter: TemplateLinter::new(),
722 debugger: TemplateDebugger::new(),
723 }
724 }
725
726 pub fn with_validator<F>(mut self, f: F) -> Self
728 where
729 F: FnOnce(crate::validation::TemplateValidator) -> crate::validation::TemplateValidator,
730 {
731 self.validator = f(self.validator);
732 self
733 }
734
735 pub fn with_linter<F>(mut self, f: F) -> Self
737 where
738 F: FnOnce(TemplateLinter) -> TemplateLinter,
739 {
740 self.linter = f(self.linter);
741 self
742 }
743
744 pub fn with_debugger<F>(mut self, f: F) -> Self
746 where
747 F: FnOnce(TemplateDebugger) -> TemplateDebugger,
748 {
749 self.debugger = f(self.debugger);
750 self
751 }
752
753 pub fn validate_template(
760 &self,
761 template: &str,
762 context: &TemplateContext,
763 name: &str,
764 ) -> Result<ValidationReport> {
765 let mut report = ValidationReport {
766 template_name: name.to_string(),
767 syntax_valid: true,
768 syntax_errors: Vec::new(),
769 lint_violations: Vec::new(),
770 unused_variables: Vec::new(),
771 missing_variables: Vec::new(),
772 performance_metrics: None,
773 };
774
775 let debug_info = self.debugger.analyze(template, name)?;
777 report.syntax_errors = debug_info.syntax_errors.clone();
778
779 if !report.syntax_errors.is_empty() {
780 report.syntax_valid = false;
781 }
782
783 report.lint_violations = self.linter.lint(template, name)?;
785
786 report.unused_variables = self.debugger.find_unused_variables(&debug_info, context);
788 report.missing_variables = self.debugger.find_missing_variables(&debug_info, context);
789
790 if self.debugger.profile_performance {
792 let render_result = crate::render_with_context(template, context);
793 if let Ok(rendered) = render_result {
794 report.performance_metrics = Some(PerformanceMetrics {
795 render_time_ms: 0, template_size: template.len(),
797 output_size: rendered.len(),
798 });
799 }
800 }
801
802 Ok(report)
803 }
804
805 pub fn validate_file<P: AsRef<Path>>(
811 &self,
812 file_path: P,
813 context: &TemplateContext,
814 ) -> Result<ValidationReport> {
815 let content = std::fs::read_to_string(&file_path)
816 .map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
817
818 let file_name = file_path
819 .as_ref()
820 .file_stem()
821 .and_then(|s| s.to_str())
822 .unwrap_or("unknown");
823
824 self.validate_template(&content, context, file_name)
825 }
826}
827
828#[derive(Debug, Clone)]
830pub struct ValidationReport {
831 pub template_name: String,
833 pub syntax_valid: bool,
835 pub syntax_errors: Vec<String>,
837 pub lint_violations: Vec<String>,
839 pub unused_variables: Vec<String>,
841 pub missing_variables: Vec<String>,
843 pub performance_metrics: Option<PerformanceMetrics>,
845}
846
847#[derive(Debug, Clone)]
849pub struct PerformanceMetrics {
850 pub render_time_ms: u64,
852 pub template_size: usize,
854 pub output_size: usize,
856}
857
858impl Default for TemplateAnalyzer {
859 fn default() -> Self {
860 Self::new()
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
869 fn test_template_analysis() {
870 let debugger = TemplateDebugger::new();
871 let template = r#"
872{{ service.name }}
873{{ fake_name() }}
874{% block content %}
875Hello {{ user }}
876{% endblock %}
877 "#;
878
879 let info = debugger.analyze(template, "test").unwrap();
880
881 assert!(info.variables_used.contains("service.name"));
882 assert!(info.variables_used.contains("user"));
883 assert!(info.functions_used.contains("fake_name"));
884 assert!(info.blocks_defined.contains("content"));
885 }
886
887 #[test]
888 fn test_syntax_validation() {
889 let debugger = TemplateDebugger::new();
890 let invalid_template = r#"{{ unclosed_variable"#;
891
892 let info = debugger.analyze(invalid_template, "test").unwrap();
893 assert!(!info.syntax_errors.is_empty());
894 }
895
896 #[test]
897 fn test_lint_rules() {
898 let debugger = TemplateDebugger::new();
899 let template = r#"
900{{ deprecated_function() }}
901{{ another_old_func() }}
902 "#;
903
904 let info = debugger.analyze(template, "test").unwrap();
905
906 let deprecated_rule = lint::DeprecatedFunctionsRule;
907 let violations = deprecated_rule.check(&info);
908
909 assert!(!violations.is_empty());
911 }
912
913 #[test]
914 fn test_template_linter() {
915 let linter = TemplateLinter::new().with_production_rules();
916
917 let template = r#"
918{{ deprecated_function() }}
919Very complex template with many variables and functions
920 "#;
921
922 let violations = linter.lint(template, "test").unwrap();
923 assert!(!violations.is_empty()); }
925}