ryo-source 0.1.0

High-speed Rust AST manipulation engine
Documentation
//! Helper functions for Pure → syn conversion.

use proc_macro2::{Ident, Span};
use syn::token;

/// Create a syn::Abi from an ABI string (e.g., "C" -> extern "C")
pub fn make_abi(abi: &str) -> syn::Abi {
    syn::Abi {
        extern_token: token::Extern::default(),
        name: Some(syn::LitStr::new(abi, Span::call_site())),
    }
}

/// Create an identifier, handling raw identifiers (r#async, r#type, etc.)
/// Invalid identifiers are sanitized to prevent panics.
///
/// # Panics (debug mode only)
/// Panics if the name contains pattern syntax like `(`, `)`, `{`, `}`.
/// This indicates a programming error where a pattern was passed instead of an identifier.
pub fn ident(name: &str) -> Ident {
    // Debug assertion: catch obvious pattern syntax errors early
    debug_assert!(
        !name.contains('(') && !name.contains('{'),
        "ident received pattern syntax '{}'. This is likely a bug - patterns should not be passed to ident().",
        name
    );

    // Handle raw identifiers (e.g., r#async, r#type)
    if let Some(raw_name) = name.strip_prefix("r#") {
        Ident::new_raw(raw_name, Span::call_site())
    } else {
        // Sanitize invalid identifiers
        let sanitized = sanitize_ident(name);
        Ident::new(&sanitized, Span::call_site())
    }
}

/// Sanitize a string to be a valid Rust identifier.
/// - Empty strings become "_empty"
/// - Invalid characters are replaced with "_"
/// - If starts with a digit, prefix with "_"
fn sanitize_ident(name: &str) -> String {
    if name.is_empty() {
        return "_empty".to_string();
    }

    // Check if the name is a valid identifier
    if is_valid_ident(name) {
        return name.to_string();
    }

    // Sanitize: replace invalid characters, handle leading digits
    let mut result = String::with_capacity(name.len() + 1);
    let mut chars = name.chars().peekable();

    // Handle first character
    if let Some(&first) = chars.peek() {
        if first.is_ascii_digit() {
            result.push('_');
        }
    }

    for c in chars {
        if c.is_alphanumeric() || c == '_' {
            result.push(c);
        } else {
            result.push('_');
        }
    }

    // If result is empty or only underscores after sanitization
    if result.is_empty() || result.chars().all(|c| c == '_') {
        return "_param".to_string();
    }

    result
}

/// Check if a string is a valid Rust identifier.
fn is_valid_ident(s: &str) -> bool {
    if s.is_empty() {
        return false;
    }

    let mut chars = s.chars();

    // First character must be letter or underscore
    match chars.next() {
        Some(c) if c.is_alphabetic() || c == '_' => {}
        _ => return false,
    }

    // Rest must be alphanumeric or underscore
    chars.all(|c| c.is_alphanumeric() || c == '_')
}

use super::ToSynError;

/// Parse a path string into syn::Path, returning an error on failure.
///
/// This is the fallible version of `parse_path()`. Use this when you need
/// proper error handling instead of silent fallbacks.
pub fn try_parse_path(path: &str) -> Result<syn::Path, ToSynError> {
    syn::parse_str(path).map_err(|e| ToSynError::ParsePath {
        input: path.to_string(),
        message: e.to_string(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ident_simple() {
        let id = ident("foo");
        assert_eq!(id.to_string(), "foo");
    }

    #[test]
    fn test_ident_raw() {
        let id = ident("r#async");
        assert_eq!(id.to_string(), "r#async");
    }

    #[test]
    fn test_try_parse_path_simple() {
        let path = try_parse_path("std::io::Result").unwrap();
        assert_eq!(path.segments.len(), 3);
    }

    #[test]
    fn test_try_parse_path_with_generics() {
        let path = try_parse_path("Vec<String>").unwrap();
        assert_eq!(path.segments.len(), 1);
    }

    #[test]
    fn test_try_parse_path_complex_generics() {
        let path = try_parse_path("HashMap<String, Vec<i32>>").unwrap();
        assert_eq!(path.segments.len(), 1);
    }

    #[test]
    fn test_try_parse_path_rejects_pattern_syntax() {
        // Tuple pattern and struct pattern are not valid paths
        assert!(try_parse_path("(crate_name, path)").is_err());
        assert!(try_parse_path("Foo { x }").is_err());
    }

    #[test]
    fn test_try_parse_path_tuple_in_generics() {
        // Tuple type inside generic arguments is a valid path
        let path = try_parse_path("Vec<(String, String)>").unwrap();
        assert_eq!(path.segments.len(), 1);
        assert_eq!(path.segments[0].ident.to_string(), "Vec");
    }

    #[test]
    #[cfg_attr(
        debug_assertions,
        should_panic(expected = "ident received pattern syntax")
    )]
    fn test_sanitize_tuple_pattern() {
        let id = ident("(crate_name , path)");
        assert!(!id.to_string().contains('('));
        assert!(!id.to_string().contains(')'));
    }

    #[test]
    fn test_sanitize_empty() {
        let id = ident("");
        assert_eq!(id.to_string(), "_empty");
    }

    #[test]
    fn test_sanitize_starting_with_digit() {
        let id = ident("123abc");
        assert!(id.to_string().starts_with('_'));
    }
}