ryo_source/pure/to_syn/mod.rs
1//! Conversion from Pure types back to syn types.
2//!
3//! This enables code generation from PureFile after parallel modifications.
4//! All spans are set to `Span::call_site()` since PureFile doesn't preserve spans.
5//!
6//! # Module Structure
7//!
8//! - `helpers` - Common helper functions (ident, parse_path)
9//! - `attr` - PureAttribute, PureVis
10//! - `item` - PureItem, PureUse, PureUseTree
11//! - `function` - PureFn, PureParam, PureGenerics, PureBlock, PureStmt
12//! - `expr` - PureExpr, PurePattern, PureMatchArm
13//! - `types` - PureType
14//! - `structs` - PureStruct, PureFields, PureField, PureEnum, PureVariant
15//! - `impls` - PureImpl, PureImplItem
16//! - `other` - PureConst, PureStatic, PureTypeAlias, PureMod, PureTrait, PureTraitItem, PureMacro
17
18pub mod helpers;
19
20mod attr;
21mod expr;
22mod function;
23mod impls;
24mod item;
25mod other;
26mod structs;
27mod types;
28
29use super::ast::PureFile;
30
31/// Format multiple files in-place using a single rustfmt invocation.
32///
33/// This is much faster than formatting each file individually, as it avoids
34/// spawning a new process for each file.
35///
36/// Returns Ok(()) if rustfmt succeeds, Err with message otherwise.
37pub fn format_files_with_rustfmt<P: AsRef<std::path::Path>>(files: &[P]) -> Result<(), String> {
38 use std::process::Command;
39
40 if files.is_empty() {
41 return Ok(());
42 }
43
44 let output = Command::new("rustfmt")
45 .args(["--edition", "2021"])
46 .args(files.iter().map(|p| p.as_ref()))
47 .output()
48 .map_err(|e| format!("Failed to spawn rustfmt: {}", e))?;
49
50 if output.status.success() {
51 Ok(())
52 } else {
53 Err(String::from_utf8_lossy(&output.stderr).to_string())
54 }
55}
56
57/// Trait for converting Pure types back to syn types.
58///
59/// All conversions are fallible because they may involve `syn::parse_str`
60/// for `Other` variants (expressions, types, patterns stored as strings).
61pub trait ToSyn {
62 /// `syn` output type produced from `self`.
63 type Output;
64 /// Convert `self` back into its `syn` representation.
65 fn to_syn(&self) -> Result<Self::Output, ToSynError>;
66}
67
68/// Error type for ToSyn conversions.
69#[derive(Debug, Clone)]
70pub enum ToSynError {
71 /// Failed to parse a path string (e.g., invalid syntax)
72 ParsePath {
73 /// Original input string that failed to parse.
74 input: String,
75 /// Underlying parser message.
76 message: String,
77 },
78 /// Failed to parse an expression
79 ParseExpr {
80 /// Original input string that failed to parse.
81 input: String,
82 /// Underlying parser message.
83 message: String,
84 },
85 /// Failed to parse a pattern
86 ParsePattern {
87 /// Original input string that failed to parse.
88 input: String,
89 /// Underlying parser message.
90 message: String,
91 },
92 /// Failed to parse a type
93 ParseType {
94 /// Original input string that failed to parse.
95 input: String,
96 /// Underlying parser message.
97 message: String,
98 },
99 /// Missing required value
100 MissingValue {
101 /// Context describing which value was missing.
102 context: String,
103 },
104 /// Other conversion error
105 Other {
106 /// Free-form error message.
107 message: String,
108 },
109}
110
111impl std::fmt::Display for ToSynError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 Self::ParsePath { input, message } => {
115 write!(f, "Failed to parse path '{}': {}", input, message)
116 }
117 Self::ParseExpr { input, message } => {
118 write!(f, "Failed to parse expression '{}': {}", input, message)
119 }
120 Self::ParsePattern { input, message } => {
121 write!(f, "Failed to parse pattern '{}': {}", input, message)
122 }
123 Self::ParseType { input, message } => {
124 write!(f, "Failed to parse type '{}': {}", input, message)
125 }
126 Self::MissingValue { context } => {
127 write!(f, "Missing required value: {}", context)
128 }
129 Self::Other { message } => write!(f, "Conversion error: {}", message),
130 }
131 }
132}
133
134impl std::error::Error for ToSynError {}
135
136impl PureFile {
137 /// Convert to syn::File for code generation.
138 pub fn to_syn_file(&self) -> Result<syn::File, ToSynError> {
139 self.to_syn()
140 }
141
142 /// Generate source code string.
143 ///
144 /// Returns formatted source code using prettyplease. Falls back to
145 /// quote-based stringification if prettyplease panics.
146 ///
147 /// Note: This method does NOT run rustfmt. For final output, use
148 /// `format_files_with_rustfmt()` to batch-format written files.
149 pub fn to_source(&self) -> Result<String, ToSynError> {
150 let file = self.to_syn()?;
151
152 // Try prettyplease first, fall back to quote if it panics
153 Ok(
154 catch_unwind_silent(|| prettyplease::unparse(&file)).unwrap_or_else(|_| {
155 use quote::ToTokens;
156 file.to_token_stream().to_string()
157 }),
158 )
159 }
160}
161
162/// Execute a closure, catching any panics without printing panic messages.
163///
164/// This is useful for handling expected panics from third-party libraries
165/// (like prettyplease's Expr::Verbatim) without polluting stderr.
166fn catch_unwind_silent<F, R>(f: F) -> std::thread::Result<R>
167where
168 F: FnOnce() -> R + std::panic::UnwindSafe,
169{
170 let prev_hook = std::panic::take_hook();
171 std::panic::set_hook(Box::new(|_| {}));
172 let result = std::panic::catch_unwind(f);
173 std::panic::set_hook(prev_hook);
174 result
175}
176
177impl ToSyn for PureFile {
178 type Output = syn::File;
179
180 fn to_syn(&self) -> Result<syn::File, ToSynError> {
181 Ok(syn::File {
182 shebang: None,
183 attrs: self
184 .attrs
185 .iter()
186 .map(|a| a.to_syn())
187 .collect::<Result<Vec<_>, _>>()?,
188 items: self
189 .items
190 .iter()
191 .map(|i| i.to_syn())
192 .collect::<Result<Vec<_>, _>>()?,
193 })
194 }
195}
196
197#[cfg(test)]
198mod tests;