tree-type-proc-macro 0.4.5

Procedural macros for tree-type crate
Documentation
//! Validation logic with comprehensive error messages

use crate::core::Attribute;
use crate::core::Child;
use crate::core::TreeDef;
use proc_macro2::Span;
use std::collections::HashMap;
use syn::Error;
use syn::Result;

pub fn validate_tree(tree: &TreeDef) -> Result<()> {
    validate_unique_names(&tree.children)?;
    validate_symlink_targets(&tree.children, &tree.children)?;
    validate_attribute_combinations(&tree.children)?;
    Ok(())
}

fn validate_unique_names(children: &[Child]) -> Result<()> {
    let mut seen: HashMap<String, Span> = HashMap::new();

    for child in children {
        let name = child.name();
        let name_str = name.to_string();

        if let Some(_first_span) = seen.get(&name_str) {
            return Err(Error::new(
                name.span(),
                format!("duplicate field name `{name_str}`"),
            ));
        }

        seen.insert(name_str, name.span());

        // Recursively validate children
        if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
            validate_unique_names(children)?;
        }
    }

    Ok(())
}

fn validate_symlink_targets(children: &[Child], root_children: &[Child]) -> Result<()> {
    for child in children {
        if let Some(target) = child.get_symlink_target() {
            let target_str = target.value();
            if target_str.is_empty() {
                return Err(Error::new(target.span(), "symlink target cannot be empty"));
            }

            // Enhanced validation: check if target will exist at runtime
            validate_symlink_target_existence(child, target, root_children, children)?;
        }

        // Recursively validate children
        if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
            validate_symlink_targets(children, root_children)?;
        }
    }

    Ok(())
}

fn validate_symlink_target_existence(
    _symlink_child: &Child,
    target: &syn::LitStr,
    root_children: &[Child],
    current_children: &[Child],
) -> Result<()> {
    let target_str = target.value();

    // Parse the target path to find the referenced child
    let target_child = if let Some(stripped) = target_str.strip_prefix('/') {
        // Absolute path: search from root
        find_child_by_path(stripped, root_children)
    } else {
        // Relative path: search in current directory first, then recursively in tree
        find_child_by_name(&target_str, current_children)
            .or_else(|| find_child_recursive(&target_str, root_children))
    };

    if let Some(target_child) = target_child {
        // Check if target has default content or is required
        if !target_has_default_content(target_child) && !target_child.is_required() {
            // Generate compile-time error with helpful message
            return Err(Error::new(
                target.span(),
                format!(
                    "symlink target '{}' may not exist at runtime\n\
                    help: add #[default(...)] attribute to '{}' to ensure it exists during setup()\n\
                    help: or add #[required] attribute if the file should be created manually",
                    target_str,
                    target_child.name()
                ),
            ));
        }
    } else {
        // Target identifier not found - this should already be caught by Rust's type system
        // but provide a helpful error message anyway
        return Err(Error::new(
            target.span(),
            format!(
                "symlink target '{target_str}' not found in tree structure\n\
                help: ensure the target identifier exists in the same directory or use an absolute path"
            ),
        ));
    }

    Ok(())
}

fn find_child_recursive<'a>(name: &str, children: &'a [Child]) -> Option<&'a Child> {
    // First check direct children
    if let Some(child) = find_child_by_name(name, children) {
        return Some(child);
    }

    // Then recursively search in subdirectories
    for child in children {
        if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
            if let Some(found) = find_child_recursive(name, children) {
                return Some(found);
            }
        }
    }

    None
}

fn target_has_default_content(child: &Child) -> bool {
    child
        .attributes()
        .iter()
        .any(|attr| matches!(attr, Attribute::Default(_)))
}

fn find_child_by_path<'a>(path: &str, children: &'a [Child]) -> Option<&'a Child> {
    let parts: Vec<&str> = path.split('/').collect();
    if parts.is_empty() {
        return None;
    }

    // Find the first part
    let first_part = parts[0];
    let child = find_child_by_name(first_part, children)?;

    if parts.len() == 1 {
        // Found the target
        Some(child)
    } else {
        // Need to recurse into child directory
        match child {
            Child::Directory { children, .. } | Child::DynamicId { children, .. } => {
                let remaining_path = parts[1..].join("/");
                find_child_by_path(&remaining_path, children)
            }
            Child::File { .. } => None, // Can't recurse into a file
        }
    }
}

fn find_child_by_name<'a>(name: &str, children: &'a [Child]) -> Option<&'a Child> {
    children.iter().find(|child| *child.name() == name)
}

fn validate_attribute_combinations(children: &[Child]) -> Result<()> {
    for child in children {
        let attrs = child.attributes();

        // Check for conflicting attributes
        let has_required = attrs.iter().any(|a| matches!(a, Attribute::Required));
        let has_optional = attrs.iter().any(|a| matches!(a, Attribute::Optional));

        if has_required && has_optional {
            return Err(Error::new(
                child.span(),
                "conflicting attributes: cannot be both `required` and `optional`",
            ));
        }

        // Note: required + default is allowed (legacy macro compatibility)

        // Validate pattern attribute only with feature flag
        let has_pattern = attrs.iter().any(|a| matches!(a, Attribute::Pattern(_)));
        if has_pattern {
            #[cfg(not(feature = "pattern-validation"))]
            {
                return Err(Error::new(
                    child.span(),
                    "pattern validation requires the `pattern-validation` feature\nhelp: enable the feature in Cargo.toml",
                ));
            }
        }

        // Recursively validate children
        if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
            validate_attribute_combinations(children)?;
        }
    }

    Ok(())
}