tree-type-proc-macro 0.4.5

Procedural macros for tree-type crate
Documentation
//! Parser for `tree_type` macro syntax

use crate::core::Attribute;
use crate::core::Child;
use crate::core::DefaultValue;
use crate::core::TreeDef;
use syn::Error;
use syn::Expr;
use syn::Ident;
use syn::LitStr;
use syn::Result;
use syn::Token;
use syn::Type;
use syn::braced;
use syn::bracketed;
use syn::parse::Parse;
use syn::parse::ParseStream;

impl Parse for TreeDef {
    fn parse(input: ParseStream) -> Result<Self> {
        // Parse attributes FIRST (before root name)
        let attributes = parse_attributes(input)?;

        let name: Ident = input.parse()?;

        // Reject old => syntax with helpful error
        if input.peek(Token![=>]) {
            return Err(Error::new(
                input.span(),
                "the `=>` token is no longer supported\nhelp: use `TreeName { ... }` instead of `TreeName => { ... }`",
            ));
        }

        let content;
        braced!(content in input);

        let children = parse_children_with_parent(&content, Some(&name))?;

        Ok(TreeDef {
            name,
            attributes,
            children,
        })
    }
}

fn parse_children_with_parent(
    input: ParseStream,
    _parent_type: Option<&Ident>,
) -> Result<Vec<Child>> {
    let mut children = Vec::new();

    while !input.is_empty() {
        children.push(parse_child_with_parent(input, None)?);

        if input.peek(Token![,]) {
            input.parse::<Token![,]>()?;
        }
    }

    Ok(children)
}

fn parse_child_with_parent(input: ParseStream, _parent_type: Option<&Ident>) -> Result<Child> {
    // Parse attributes FIRST (before item)
    let attributes = parse_attributes(input)?;

    // Check for dynamic ID: [id: Type] => ChildType => { ... }
    if input.peek(syn::token::Bracket) {
        return parse_dynamic_id_with_parent(input, attributes, None);
    }

    // Parse name
    let name: Ident = input.parse()?;

    // Block reserved method names
    if name == "parent" {
        return Err(syn::Error::new(
            name.span(),
            "Directory name 'parent' is reserved for the parent() method",
        ));
    }

    // Check for directory marker (/)
    let is_directory = input.peek(Token![/]);
    if is_directory {
        input.parse::<Token![/]>()?;
    }

    // Parse custom filename: name("filename")
    let custom_filename = if input.peek(syn::token::Paren) {
        let content;
        syn::parenthesized!(content in input);
        Some(content.parse::<LitStr>()?)
    } else {
        None
    };

    // Parse custom type: name as Type
    let custom_type = if input.peek(Token![as]) {
        input.parse::<Token![as]>()?;
        Some(input.parse::<Ident>()?)
    } else {
        None
    };

    // Reject old trailing attribute syntax
    if input.peek(Token![#]) {
        return Err(Error::new(
            input.span(),
            "attributes must appear before the item, not after\nhelp: move `#[...]` before the item name",
        ));
    }

    // Parse children for directories
    if is_directory {
        let children = if input.peek(syn::token::Brace) {
            let content;
            braced!(content in input);
            // Pass the current directory type as parent for its children
            let current_type = custom_type.as_ref().unwrap_or(&name);
            parse_children_with_parent(&content, Some(current_type))?
        } else {
            Vec::new()
        };

        Ok(Child::Directory {
            name,
            custom_filename,
            custom_type,
            attributes,
            children,
        })
    } else {
        Ok(Child::File {
            name,
            custom_filename,
            custom_type,
            attributes,
        })
    }
}

fn parse_dynamic_id_with_parent(
    input: ParseStream,
    attributes: Vec<Attribute>,
    _parent_type: Option<&Ident>,
) -> Result<Child> {
    // Parse [id: Type]
    let content;
    bracketed!(content in input);

    let id_name: Ident = content.parse()?;
    content.parse::<Token![:]>()?;
    let id_type: Type = content.parse()?;

    // Check for directory marker (/)
    let is_directory = input.peek(Token![/]);
    if is_directory {
        input.parse::<Token![/]>()?;
    }

    // Parse optional pattern: ("pattern-{placeholder}.ext")
    let pattern = if input.peek(syn::token::Paren) {
        let paren_content;
        syn::parenthesized!(paren_content in input);
        let pattern_lit: LitStr = paren_content.parse()?;

        // Validate pattern has exactly one {placeholder}
        let pattern_str = pattern_lit.value();
        let open_count = pattern_str.matches('{').count();
        let close_count = pattern_str.matches('}').count();

        if open_count != 1 || close_count != 1 {
            return Err(Error::new(
                pattern_lit.span(),
                "pattern must contain exactly one {placeholder}",
            ));
        }

        // Validate placeholder name is a valid identifier
        if let Some(start) = pattern_str.find('{') {
            if let Some(end) = pattern_str.find('}') {
                let placeholder = &pattern_str[start + 1..end];
                if placeholder.is_empty()
                    || !placeholder.chars().all(|c| c.is_alphanumeric() || c == '_')
                {
                    return Err(Error::new(
                        pattern_lit.span(),
                        "placeholder name must be a valid identifier (alphanumeric and underscores)",
                    ));
                }
            }
        }

        Some(pattern_lit)
    } else {
        None
    };

    // Parse optional custom type: as ChildType
    let child_type = if input.peek(Token![as]) {
        input.parse::<Token![as]>()?;
        input.parse::<Ident>()?
    } else {
        // Generate default type name from id_name
        let default_name = format!(
            "{}Type",
            id_name
                .to_string()
                .chars()
                .next()
                .unwrap()
                .to_uppercase()
                .collect::<String>()
                + &id_name.to_string()[1..]
        );
        Ident::new(&default_name, id_name.span())
    };

    // Parse children for directories
    let children = if is_directory && input.peek(syn::token::Brace) {
        let content;
        braced!(content in input);
        // Pass the child_type as parent for its children
        parse_children_with_parent(&content, Some(&child_type))?
    } else {
        Vec::new()
    };

    Ok(Child::DynamicId {
        id_name,
        id_type,
        child_type,
        attributes,
        children,
        is_directory,
        pattern,
    })
}

fn parse_attributes(input: ParseStream) -> Result<Vec<Attribute>> {
    let mut attributes = Vec::new();

    while input.peek(Token![#]) {
        input.parse::<Token![#]>()?;

        let content;
        bracketed!(content in input);

        // Parse single attribute (no comma-separated lists)
        let attr = parse_attribute(&content)?;
        attributes.push(attr);

        // Reject grouped attributes with helpful error
        if content.peek(Token![,]) {
            return Err(Error::new(
                content.span(),
                "grouped attributes are no longer supported\nhelp: use separate `#[...]` for each attribute instead of `#[a, b]`",
            ));
        }

        // Ensure attribute is fully consumed
        if !content.is_empty() {
            return Err(Error::new(content.span(), "unexpected tokens in attribute"));
        }
    }

    Ok(attributes)
}

fn parse_attribute(input: ParseStream) -> Result<Attribute> {
    let name: Ident = input.parse()?;
    let name_str = name.to_string();

    match name_str.as_str() {
        "required" => Ok(Attribute::Required),
        "optional" => Ok(Attribute::Optional),
        "transparent" => Ok(Attribute::Transparent),
        "default" => {
            // New function call syntax: #[default(expr)]
            if input.peek(syn::token::Paren) {
                let content;
                syn::parenthesized!(content in input);

                // Try to parse as literal string first
                if let Ok(lit) = content.parse::<LitStr>() {
                    Ok(Attribute::Default(DefaultValue::Literal(lit)))
                } else {
                    // Parse as expression (function call)
                    let expr: Expr = content.parse()?;
                    Ok(Attribute::Default(DefaultValue::Function(expr)))
                }
            } else if input.peek(Token![=]) {
                // Old assignment syntax - emit helpful error
                Err(Error::new(
                    input.span(),
                    "assignment syntax is no longer supported\nhelp: use `#[default(function_name)]` instead of `#[default = function_name]`",
                ))
            } else {
                Ok(Attribute::Default(DefaultValue::DefaultTrait))
            }
        }
        "validate" => {
            let content;
            syn::parenthesized!(content in input);
            let expr: Expr = content.parse()?;
            Ok(Attribute::Validate(expr))
        }
        "pattern" => {
            let content;
            syn::parenthesized!(content in input);
            let pattern: LitStr = content.parse()?;
            Ok(Attribute::Pattern(pattern))
        }
        "symlink" => {
            // New function call syntax: #[symlink(target)]
            if input.peek(syn::token::Paren) {
                let content;
                syn::parenthesized!(content in input);

                // Try to parse as string literal first
                if let Ok(target) = content.parse::<LitStr>() {
                    return Ok(Attribute::Symlink(target));
                }

                // Parse identity path (e.g., src/main, ../other/file, /config/main)
                let mut path_parts = Vec::new();
                let mut is_absolute = false;

                // Handle leading / for absolute paths
                if content.peek(Token![/]) {
                    content.parse::<Token![/]>()?;
                    is_absolute = true;
                }

                // Handle leading .. for relative paths
                while content.peek(Token![..]) {
                    content.parse::<Token![..]>()?;
                    path_parts.push("..".to_string());
                    if content.peek(Token![/]) {
                        content.parse::<Token![/]>()?;
                    }
                }

                // Parse path components separated by /
                while let Ok(ident) = content.parse::<Ident>() {
                    path_parts.push(ident.to_string());

                    if content.peek(Token![/]) {
                        content.parse::<Token![/]>()?;
                    } else {
                        break;
                    }
                }

                if path_parts.is_empty() {
                    return Err(Error::new(content.span(), "symlink target cannot be empty"));
                }

                // Join path parts and create string literal
                let path_str = if is_absolute {
                    format!("/{}", path_parts.join("/"))
                } else {
                    path_parts.join("/")
                };
                let target_str = LitStr::new(&path_str, content.span());
                Ok(Attribute::Symlink(target_str))
            } else if input.peek(Token![=]) {
                // Old assignment syntax - emit helpful error
                Err(Error::new(
                    input.span(),
                    "assignment syntax is no longer supported\nhelp: use `#[symlink(target)]` instead of `#[symlink = target]`",
                ))
            } else {
                Err(Error::new(
                    input.span(),
                    "symlink attribute requires a target\nhelp: use `#[symlink(target)]`",
                ))
            }
        }
        _ => Err(Error::new(
            name.span(),
            format!(
                "unknown attribute `{name_str}`\nhelp: valid attributes are: required, optional, default, validate, pattern, symlink"
            ),
        )),
    }
}