use crate::context::TemplateContext;
use crate::error::{Result, TemplateError};
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct DebugInfo {
pub template_name: String,
pub source: String,
pub variables_used: HashSet<String>,
pub functions_used: HashSet<String>,
pub blocks_defined: HashSet<String>,
pub extends_templates: Vec<String>,
pub includes_templates: Vec<String>,
pub syntax_errors: Vec<String>,
pub render_time_ms: Option<u64>,
pub memory_usage: Option<usize>,
}
#[derive(Debug)]
pub struct TemplateDebugger {
verbose: bool,
track_variables: bool,
track_functions: bool,
validate_syntax: bool,
profile_performance: bool,
}
impl Default for TemplateDebugger {
fn default() -> Self {
Self {
verbose: false,
track_variables: true,
track_functions: true,
validate_syntax: true,
profile_performance: false,
}
}
}
impl TemplateDebugger {
pub fn new() -> Self {
Self::default()
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn track_variables(mut self, track: bool) -> Self {
self.track_variables = track;
self
}
pub fn track_functions(mut self, track: bool) -> Self {
self.track_functions = track;
self
}
pub fn validate_syntax(mut self, validate: bool) -> Self {
self.validate_syntax = validate;
self
}
pub fn profile_performance(mut self, profile: bool) -> Self {
self.profile_performance = profile;
self
}
pub fn analyze(&self, template_content: &str, template_name: &str) -> Result<DebugInfo> {
let mut info = DebugInfo {
template_name: template_name.to_string(),
source: template_content.to_string(),
variables_used: HashSet::new(),
functions_used: HashSet::new(),
blocks_defined: HashSet::new(),
extends_templates: Vec::new(),
includes_templates: Vec::new(),
syntax_errors: Vec::new(),
render_time_ms: None,
memory_usage: None,
};
if self.validate_syntax {
self.validate_template_syntax(template_content, &mut info)?;
}
if self.track_variables {
self.extract_variables(template_content, &mut info);
}
if self.track_functions {
self.extract_functions(template_content, &mut info);
}
self.extract_composition_info(template_content, &mut info);
Ok(info)
}
fn validate_template_syntax(&self, content: &str, info: &mut DebugInfo) -> Result<()> {
let mut errors = Vec::new();
self.check_brace_matching(content, &mut errors);
self.check_variable_syntax(content, &mut errors);
self.check_function_syntax(content, &mut errors);
info.syntax_errors = errors;
Ok(())
}
fn check_brace_matching(&self, content: &str, errors: &mut Vec<String>) {
let mut stack = Vec::new();
for (i, ch) in content.char_indices() {
match ch {
'{' => {
if let Some(next) = content.chars().nth(i + 1) {
if next == '{' || next == '%' || next == '#' {
stack.push((ch, i));
}
}
}
'%' | '#' => {
if i > 0 && content.chars().nth(i - 1) == Some('{') {
stack.push((ch, i));
}
}
'}' => {
if let Some(next) = content.chars().nth(i + 1) {
if next == '}' {
if let Some((open, _open_pos)) = stack.pop() {
if !matches!((open, ch), ('{', '}')) {
errors.push(format!(
"Unmatched braces at position {}: found '{}' but expected matching '{}'",
i, ch, open
));
}
} else {
errors.push(format!("Unmatched closing brace at position {}", i));
}
}
}
}
_ => {}
}
}
for (open, pos) in stack {
errors.push(format!("Unclosed '{}' at position {}", open, pos));
}
}
fn check_variable_syntax(&self, content: &str, errors: &mut Vec<String>) {
let var_regex = regex::Regex::new(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)").unwrap();
for cap in var_regex.captures_iter(content) {
if let Some(var_name) = cap.get(1) {
let var = var_name.as_str();
if var.contains(" ") {
errors.push(format!("Invalid variable name '{}' contains spaces", var));
}
}
}
}
fn check_function_syntax(&self, content: &str, _errors: &mut [String]) {
let func_regex = regex::Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap();
for cap in func_regex.captures_iter(content) {
if let Some(func_name) = cap.get(1) {
let func = func_name.as_str();
let known_functions = [
"env",
"now_rfc3339",
"sha256",
"toml_encode",
"fake_name",
"fake_email",
"uuid_v4",
"include",
"extends",
];
if !known_functions.contains(&func) && !func.starts_with("fake_") {
if self.verbose {
eprintln!("Warning: Unknown function '{}' in template", func);
}
}
}
}
}
fn extract_variables(&self, content: &str, info: &mut DebugInfo) {
let var_regex = regex::Regex::new(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)").unwrap();
for cap in var_regex.captures_iter(content) {
if let Some(var_name) = cap.get(1) {
info.variables_used.insert(var_name.as_str().to_string());
}
}
}
fn extract_functions(&self, content: &str, info: &mut DebugInfo) {
let func_regex = regex::Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap();
for cap in func_regex.captures_iter(content) {
if let Some(func_name) = cap.get(1) {
info.functions_used.insert(func_name.as_str().to_string());
}
}
}
fn extract_composition_info(&self, content: &str, info: &mut DebugInfo) {
let extends_regex = regex::Regex::new(r#"extends\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
for cap in extends_regex.captures_iter(content) {
if let Some(template) = cap.get(1) {
info.extends_templates.push(template.as_str().to_string());
}
}
let include_regex = regex::Regex::new(r#"include\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
for cap in include_regex.captures_iter(content) {
if let Some(template) = cap.get(1) {
info.includes_templates.push(template.as_str().to_string());
}
}
let block_regex = regex::Regex::new(r#"block\s*\(\s*["']([^"']+)["']\s*\)"#).unwrap();
for cap in block_regex.captures_iter(content) {
if let Some(block_name) = cap.get(1) {
info.blocks_defined.insert(block_name.as_str().to_string());
}
}
}
pub fn debug_render(
&self,
template_content: &str,
context: &TemplateContext,
template_name: &str,
) -> Result<DebugInfo> {
let mut info = self.analyze(template_content, template_name)?;
if self.profile_performance {
let start = std::time::Instant::now();
let result = crate::render_with_context(template_content, context);
let elapsed = start.elapsed();
info.render_time_ms = Some(elapsed.as_millis() as u64);
if let Err(e) = result {
info.syntax_errors.push(e.to_string());
}
}
if self.verbose {
self.print_debug_info(&info);
}
Ok(info)
}
fn print_debug_info(&self, info: &DebugInfo) {
eprintln!("=== Template Debug Info ===");
eprintln!("Template: {}", info.template_name);
eprintln!("Variables used: {:?}", info.variables_used);
eprintln!("Functions used: {:?}", info.functions_used);
eprintln!("Blocks defined: {:?}", info.blocks_defined);
eprintln!("Extends: {:?}", info.extends_templates);
eprintln!("Includes: {:?}", info.includes_templates);
if !info.syntax_errors.is_empty() {
eprintln!("Syntax errors:");
for error in &info.syntax_errors {
eprintln!(" - {}", error);
}
}
if let Some(time) = info.render_time_ms {
eprintln!("Render time: {}ms", time);
}
}
pub fn find_unused_variables(
&self,
debug_info: &DebugInfo,
context: &TemplateContext,
) -> Vec<String> {
let mut unused = Vec::new();
for var_name in context.vars.keys() {
if !debug_info.variables_used.contains(var_name) {
unused.push(var_name.clone());
}
}
unused
}
pub fn find_missing_variables(
&self,
debug_info: &DebugInfo,
context: &TemplateContext,
) -> Vec<String> {
let mut missing = Vec::new();
for var_name in &debug_info.variables_used {
if !context.vars.contains_key(var_name) {
missing.push(var_name.clone());
}
}
missing
}
}
pub struct TemplateAnalyzer {
debugger: TemplateDebugger,
}
impl TemplateAnalyzer {
pub fn new() -> Self {
Self {
debugger: TemplateDebugger::new(),
}
}
pub fn analyze_file<P: AsRef<Path>>(&self, file_path: P) -> Result<DebugInfo> {
let content = std::fs::read_to_string(&file_path)
.map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
let file_name = file_path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
self.debugger.analyze(&content, file_name)
}
pub fn analyze_directory<P: AsRef<Path>>(
&self,
dir_path: P,
) -> Result<HashMap<String, DebugInfo>> {
use walkdir::WalkDir;
let mut results = HashMap::new();
for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let path = entry.path();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
if matches!(ext, "toml" | "tera" | "tpl" | "template") {
if let Ok(info) = self.analyze_file(path) {
let name = info.template_name.clone();
results.insert(name, info);
}
}
}
}
}
Ok(results)
}
pub fn find_unused_variables(
&self,
template_info: &DebugInfo,
context: &TemplateContext,
) -> Vec<String> {
let used_vars: HashSet<String> = template_info.variables_used.iter().cloned().collect();
let context_vars: HashSet<String> = context.vars.keys().cloned().collect();
context_vars.difference(&used_vars).cloned().collect()
}
pub fn find_missing_variables(
&self,
template_info: &DebugInfo,
context: &TemplateContext,
) -> Vec<String> {
let used_vars: HashSet<String> = template_info.variables_used.iter().cloned().collect();
let context_vars: HashSet<String> = context.vars.keys().cloned().collect();
used_vars.difference(&context_vars).cloned().collect()
}
}
pub mod lint {
use super::*;
pub trait LintRule {
fn check(&self, info: &DebugInfo) -> Vec<String>;
}
pub struct UnusedVariablesRule;
impl LintRule for UnusedVariablesRule {
fn check(&self, _info: &DebugInfo) -> Vec<String> {
Vec::new()
}
}
pub struct DeprecatedFunctionsRule;
impl LintRule for DeprecatedFunctionsRule {
fn check(&self, info: &DebugInfo) -> Vec<String> {
let deprecated = ["old_function", "deprecated_helper"];
let mut violations = Vec::new();
for func in &info.functions_used {
if deprecated.contains(&func.as_str()) {
violations.push(format!("Deprecated function '{}' used", func));
}
}
violations
}
}
pub struct ComplexityRule {
max_complexity: usize,
}
impl ComplexityRule {
pub fn new(max_complexity: usize) -> Self {
Self { max_complexity }
}
}
impl LintRule for ComplexityRule {
fn check(&self, info: &DebugInfo) -> Vec<String> {
let mut violations = Vec::new();
let complexity =
info.functions_used.len() + info.variables_used.len() + info.blocks_defined.len();
if complexity > self.max_complexity {
violations.push(format!(
"Template complexity {} exceeds maximum {}",
complexity, self.max_complexity
));
}
violations
}
}
pub struct UndocumentedVariablesRule;
impl LintRule for UndocumentedVariablesRule {
fn check(&self, info: &DebugInfo) -> Vec<String> {
let mut violations = Vec::new();
for var in &info.variables_used {
let doc_pattern = format!("{{# {} #}}", var);
if !info.source.contains(&doc_pattern) {
violations.push(format!("Variable '{}' is not documented", var));
}
}
violations
}
}
}
pub struct TemplateLinter {
rules: Vec<Box<dyn lint::LintRule>>,
debugger: TemplateDebugger,
}
impl Default for TemplateLinter {
fn default() -> Self {
Self {
rules: Vec::new(),
debugger: TemplateDebugger::new(),
}
}
}
impl TemplateLinter {
pub fn new() -> Self {
Self::default()
}
pub fn with_rule<R: lint::LintRule + 'static>(mut self, rule: R) -> Self {
self.rules.push(Box::new(rule));
self
}
pub fn with_production_rules(mut self) -> Self {
self.rules.push(Box::new(lint::DeprecatedFunctionsRule));
self.rules.push(Box::new(lint::ComplexityRule::new(50))); self.rules.push(Box::new(lint::UndocumentedVariablesRule));
self
}
pub fn lint(&self, template_content: &str, template_name: &str) -> Result<Vec<String>> {
let mut violations = Vec::new();
let info = self.debugger.analyze(template_content, template_name)?;
for rule in &self.rules {
violations.extend(rule.check(&info));
}
Ok(violations)
}
pub fn lint_file<P: AsRef<Path>>(&self, file_path: P) -> Result<Vec<String>> {
let content = std::fs::read_to_string(&file_path)
.map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
let file_name = file_path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
self.lint(&content, file_name)
}
pub fn lint_directory<P: AsRef<Path>>(
&self,
dir_path: P,
) -> Result<HashMap<String, Vec<String>>> {
use walkdir::WalkDir;
let mut results = HashMap::new();
for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let path = entry.path();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
if matches!(ext, "toml" | "tera" | "tpl" | "template") {
match self.lint_file(path) {
Ok(violations) => {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
results.insert(name, violations);
}
Err(e) => {
eprintln!("Warning: Failed to lint template {:?}: {}", path, e);
}
}
}
}
}
}
Ok(results)
}
}
pub struct DebugTemplateValidator {
validator: crate::validation::TemplateValidator,
linter: TemplateLinter,
debugger: TemplateDebugger,
}
impl Default for DebugTemplateValidator {
fn default() -> Self {
Self::new()
}
}
impl DebugTemplateValidator {
pub fn new() -> Self {
Self {
validator: crate::validation::TemplateValidator::new(),
linter: TemplateLinter::new(),
debugger: TemplateDebugger::new(),
}
}
pub fn with_validator<F>(mut self, f: F) -> Self
where
F: FnOnce(crate::validation::TemplateValidator) -> crate::validation::TemplateValidator,
{
self.validator = f(self.validator);
self
}
pub fn with_linter<F>(mut self, f: F) -> Self
where
F: FnOnce(TemplateLinter) -> TemplateLinter,
{
self.linter = f(self.linter);
self
}
pub fn with_debugger<F>(mut self, f: F) -> Self
where
F: FnOnce(TemplateDebugger) -> TemplateDebugger,
{
self.debugger = f(self.debugger);
self
}
pub fn validate_template(
&self,
template: &str,
context: &TemplateContext,
name: &str,
) -> Result<ValidationReport> {
let mut report = ValidationReport {
template_name: name.to_string(),
syntax_valid: true,
syntax_errors: Vec::new(),
lint_violations: Vec::new(),
unused_variables: Vec::new(),
missing_variables: Vec::new(),
performance_metrics: None,
};
let debug_info = self.debugger.analyze(template, name)?;
report.syntax_errors = debug_info.syntax_errors.clone();
if !report.syntax_errors.is_empty() {
report.syntax_valid = false;
}
report.lint_violations = self.linter.lint(template, name)?;
report.unused_variables = self.debugger.find_unused_variables(&debug_info, context);
report.missing_variables = self.debugger.find_missing_variables(&debug_info, context);
if self.debugger.profile_performance {
let render_result = crate::render_with_context(template, context);
if let Ok(rendered) = render_result {
report.performance_metrics = Some(PerformanceMetrics {
render_time_ms: 0, template_size: template.len(),
output_size: rendered.len(),
});
}
}
Ok(report)
}
pub fn validate_file<P: AsRef<Path>>(
&self,
file_path: P,
context: &TemplateContext,
) -> Result<ValidationReport> {
let content = std::fs::read_to_string(&file_path)
.map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
let file_name = file_path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
self.validate_template(&content, context, file_name)
}
}
#[derive(Debug, Clone)]
pub struct ValidationReport {
pub template_name: String,
pub syntax_valid: bool,
pub syntax_errors: Vec<String>,
pub lint_violations: Vec<String>,
pub unused_variables: Vec<String>,
pub missing_variables: Vec<String>,
pub performance_metrics: Option<PerformanceMetrics>,
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub render_time_ms: u64,
pub template_size: usize,
pub output_size: usize,
}
impl Default for TemplateAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_analysis() {
let debugger = TemplateDebugger::new();
let template = r#"
{{ service.name }}
{{ fake_name() }}
{% block content %}
Hello {{ user }}
{% endblock %}
"#;
let info = debugger.analyze(template, "test").unwrap();
assert!(info.variables_used.contains("service.name"));
assert!(info.variables_used.contains("user"));
assert!(info.functions_used.contains("fake_name"));
assert!(info.blocks_defined.contains("content"));
}
#[test]
fn test_syntax_validation() {
let debugger = TemplateDebugger::new();
let invalid_template = r#"{{ unclosed_variable"#;
let info = debugger.analyze(invalid_template, "test").unwrap();
assert!(!info.syntax_errors.is_empty());
}
#[test]
fn test_lint_rules() {
let debugger = TemplateDebugger::new();
let template = r#"
{{ deprecated_function() }}
{{ another_old_func() }}
"#;
let info = debugger.analyze(template, "test").unwrap();
let deprecated_rule = lint::DeprecatedFunctionsRule;
let violations = deprecated_rule.check(&info);
assert!(!violations.is_empty());
}
#[test]
fn test_template_linter() {
let linter = TemplateLinter::new().with_production_rules();
let template = r#"
{{ deprecated_function() }}
Very complex template with many variables and functions
"#;
let violations = linter.lint(template, "test").unwrap();
assert!(!violations.is_empty()); }
}