ryo-source 0.1.0

High-speed Rust AST manipulation engine
Documentation
//! Conversion from Pure types back to syn types.
//!
//! This enables code generation from PureFile after parallel modifications.
//! All spans are set to `Span::call_site()` since PureFile doesn't preserve spans.
//!
//! # Module Structure
//!
//! - `helpers` - Common helper functions (ident, parse_path)
//! - `attr` - PureAttribute, PureVis
//! - `item` - PureItem, PureUse, PureUseTree
//! - `function` - PureFn, PureParam, PureGenerics, PureBlock, PureStmt
//! - `expr` - PureExpr, PurePattern, PureMatchArm
//! - `types` - PureType
//! - `structs` - PureStruct, PureFields, PureField, PureEnum, PureVariant
//! - `impls` - PureImpl, PureImplItem
//! - `other` - PureConst, PureStatic, PureTypeAlias, PureMod, PureTrait, PureTraitItem, PureMacro

pub mod helpers;

mod attr;
mod expr;
mod function;
mod impls;
mod item;
mod other;
mod structs;
mod types;

use super::ast::PureFile;

/// Format multiple files in-place using a single rustfmt invocation.
///
/// This is much faster than formatting each file individually, as it avoids
/// spawning a new process for each file.
///
/// Returns Ok(()) if rustfmt succeeds, Err with message otherwise.
pub fn format_files_with_rustfmt<P: AsRef<std::path::Path>>(files: &[P]) -> Result<(), String> {
    use std::process::Command;

    if files.is_empty() {
        return Ok(());
    }

    let output = Command::new("rustfmt")
        .args(["--edition", "2021"])
        .args(files.iter().map(|p| p.as_ref()))
        .output()
        .map_err(|e| format!("Failed to spawn rustfmt: {}", e))?;

    if output.status.success() {
        Ok(())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}

/// Trait for converting Pure types back to syn types.
///
/// All conversions are fallible because they may involve `syn::parse_str`
/// for `Other` variants (expressions, types, patterns stored as strings).
pub trait ToSyn {
    /// `syn` output type produced from `self`.
    type Output;
    /// Convert `self` back into its `syn` representation.
    fn to_syn(&self) -> Result<Self::Output, ToSynError>;
}

/// Error type for ToSyn conversions.
#[derive(Debug, Clone)]
pub enum ToSynError {
    /// Failed to parse a path string (e.g., invalid syntax)
    ParsePath {
        /// Original input string that failed to parse.
        input: String,
        /// Underlying parser message.
        message: String,
    },
    /// Failed to parse an expression
    ParseExpr {
        /// Original input string that failed to parse.
        input: String,
        /// Underlying parser message.
        message: String,
    },
    /// Failed to parse a pattern
    ParsePattern {
        /// Original input string that failed to parse.
        input: String,
        /// Underlying parser message.
        message: String,
    },
    /// Failed to parse a type
    ParseType {
        /// Original input string that failed to parse.
        input: String,
        /// Underlying parser message.
        message: String,
    },
    /// Missing required value
    MissingValue {
        /// Context describing which value was missing.
        context: String,
    },
    /// Other conversion error
    Other {
        /// Free-form error message.
        message: String,
    },
}

impl std::fmt::Display for ToSynError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ParsePath { input, message } => {
                write!(f, "Failed to parse path '{}': {}", input, message)
            }
            Self::ParseExpr { input, message } => {
                write!(f, "Failed to parse expression '{}': {}", input, message)
            }
            Self::ParsePattern { input, message } => {
                write!(f, "Failed to parse pattern '{}': {}", input, message)
            }
            Self::ParseType { input, message } => {
                write!(f, "Failed to parse type '{}': {}", input, message)
            }
            Self::MissingValue { context } => {
                write!(f, "Missing required value: {}", context)
            }
            Self::Other { message } => write!(f, "Conversion error: {}", message),
        }
    }
}

impl std::error::Error for ToSynError {}

impl PureFile {
    /// Convert to syn::File for code generation.
    pub fn to_syn_file(&self) -> Result<syn::File, ToSynError> {
        self.to_syn()
    }

    /// Generate source code string.
    ///
    /// Returns formatted source code using prettyplease. Falls back to
    /// quote-based stringification if prettyplease panics.
    ///
    /// Note: This method does NOT run rustfmt. For final output, use
    /// `format_files_with_rustfmt()` to batch-format written files.
    pub fn to_source(&self) -> Result<String, ToSynError> {
        let file = self.to_syn()?;

        // Try prettyplease first, fall back to quote if it panics
        Ok(
            catch_unwind_silent(|| prettyplease::unparse(&file)).unwrap_or_else(|_| {
                use quote::ToTokens;
                file.to_token_stream().to_string()
            }),
        )
    }
}

/// Execute a closure, catching any panics without printing panic messages.
///
/// This is useful for handling expected panics from third-party libraries
/// (like prettyplease's Expr::Verbatim) without polluting stderr.
fn catch_unwind_silent<F, R>(f: F) -> std::thread::Result<R>
where
    F: FnOnce() -> R + std::panic::UnwindSafe,
{
    let prev_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(|_| {}));
    let result = std::panic::catch_unwind(f);
    std::panic::set_hook(prev_hook);
    result
}

impl ToSyn for PureFile {
    type Output = syn::File;

    fn to_syn(&self) -> Result<syn::File, ToSynError> {
        Ok(syn::File {
            shebang: None,
            attrs: self
                .attrs
                .iter()
                .map(|a| a.to_syn())
                .collect::<Result<Vec<_>, _>>()?,
            items: self
                .items
                .iter()
                .map(|i| i.to_syn())
                .collect::<Result<Vec<_>, _>>()?,
        })
    }
}

#[cfg(test)]
mod tests;