Skip to content

Commit 75dc984

Browse files
committed
Merge parse_file
1 parent 658c2d0 commit 75dc984

6 files changed

Lines changed: 43 additions & 55 deletions

File tree

crates/vespera_macro/src/collector.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,6 @@ pub fn collect_metadata(
5050
continue;
5151
}
5252

53-
let content = std::fs::read_to_string(&file).map_err(|e| {
54-
err_call_site(format!(
55-
"vespera! macro: failed to read route file '{}': {}. Check file permissions.",
56-
file.display(),
57-
e
58-
))
59-
})?;
60-
6153
let file_path = file.display().to_string();
6254

6355
// Get module path (cheap — no parsing needed)
@@ -118,7 +110,9 @@ pub fn collect_metadata(
118110
// into SCHEMA_STORAGE.field_defaults (Priority 0 in process_default_functions)
119111
} else {
120112
// Slow path: full parsing (fallback for files not in ROUTE_STORAGE)
121-
let file_ast = syn::parse_file(&content).map_err(|e| err_call_site(format!("vespera! macro: syntax error in '{}': {}. Fix the Rust syntax errors in this file.", file.display(), e)))?;
113+
// Uses get_parsed_file: single syn::parse_file entry point + content cache
114+
let file_ast = crate::schema_macro::file_cache::get_parsed_file(&file)
115+
.ok_or_else(|| err_call_site(format!("vespera! macro: cannot read or parse '{}'. Fix the Rust syntax errors in this file.", file.display())))?;
122116

123117
// Store file AST for downstream reuse
124118
file_asts.insert(file_path.clone(), file_ast);

crates/vespera_macro/src/file_utils.rs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,6 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6-
/// Read and parse a Rust source file, printing warnings on error.
7-
#[allow(clippy::similar_names)]
8-
pub fn read_and_parse_file_warn(path: &Path, context: &str) -> Option<syn::File> {
9-
let content = match std::fs::read_to_string(path) {
10-
Ok(c) => c,
11-
Err(e) => {
12-
eprintln!(
13-
"Warning: {}: Cannot read '{}': {}",
14-
context,
15-
path.display(),
16-
e
17-
);
18-
return None;
19-
}
20-
};
21-
match syn::parse_file(&content) {
22-
Ok(ast) => Some(ast),
23-
Err(e) => {
24-
eprintln!(
25-
"Warning: {}: Parse error in '{}': {}",
26-
context,
27-
path.display(),
28-
e
29-
);
30-
None
31-
}
32-
}
33-
}
34-
356
pub fn collect_files(folder_path: &Path) -> io::Result<Vec<PathBuf>> {
367
let mut files = Vec::new();
378
for entry in std::fs::read_dir(folder_path)? {

crates/vespera_macro/src/openapi_generator.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use vespera_core::{
1010
};
1111

1212
use crate::{
13-
file_utils::read_and_parse_file_warn,
1413
metadata::CollectedMetadata,
1514
parser::{
1615
build_operation_from_function, extract_default, extract_field_rename, extract_rename_all,
@@ -129,7 +128,7 @@ fn build_file_cache(metadata: &CollectedMetadata) -> HashMap<String, syn::File>
129128
.collect();
130129
let mut cache = HashMap::with_capacity(unique_paths.len());
131130
for path in unique_paths {
132-
if let Some(ast) = read_and_parse_file_warn(Path::new(path), "OpenAPI generation") {
131+
if let Some(ast) = crate::schema_macro::file_cache::get_parsed_file(Path::new(path)) {
133132
cache.insert(path.to_string(), ast);
134133
}
135134
}

crates/vespera_macro/src/schema_impl.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,8 @@ fn extract_field_defaults(input: &syn::DeriveInput) -> BTreeMap<String, serde_js
121121
return defaults;
122122
};
123123

124-
// Read and parse the file
125-
let Some(file_ast) = crate::file_utils::read_and_parse_file_warn(
126-
&file_path,
127-
"derive(Schema) default extraction",
128-
) else {
124+
// Read and parse the file (cached via FileCache parsed_file_asts)
125+
let Some(file_ast) = crate::schema_macro::file_cache::get_parsed_file(&file_path) else {
129126
return defaults;
130127
};
131128

crates/vespera_macro/src/schema_macro/file_cache.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ struct FileCache {
3333
/// Built from cheap `String::contains` search, not full parsing.
3434
struct_candidates: HashMap<(PathBuf, String), Vec<PathBuf>>,
3535

36-
// NOTE: We do NOT cache `syn::ItemStruct` directly because `syn` types contain
37-
// `proc_macro::Span` handles tied to a specific macro invocation context.
36+
// NOTE: We CANNOT cache `syn::File` or `syn::ItemStruct` across proc-macro
37+
// invocations. Both `syn` and `proc_macro2` types contain `proc_macro::Span`
38+
// and `proc_macro::TokenStream` bridge handles allocated in the current
39+
// invocation's bridge context. Cloning them in a later invocation panics with
40+
// "use-after-free in `proc_macro` handle".
41+
//
3842
// Instead, `struct_definitions` caches extracted definition *strings* which have
39-
// no Span handles and are safe to reuse across invocations.
43+
// no bridge handles and are safe to reuse. For callers needing `syn::File`,
44+
// `get_parsed_file()` caches the file *content* (safe string) and re-parses
45+
// per invocation, avoiding redundant disk I/O while staying safe.
4046

4147
// --- Profiling counters (zero-cost when VESPERA_PROFILE is not set) ---
4248
/// Number of file content reads from disk (cache miss).
@@ -112,6 +118,30 @@ pub fn get_manifest_dir() -> Option<String> {
112118
})
113119
}
114120

121+
/// Get a parsed `syn::File` for the given path.
122+
///
123+
/// Uses the file content cache to avoid redundant disk I/O, then parses with
124+
/// `syn::parse_file` each time. We CANNOT cache `syn::File` across proc-macro
125+
/// invocations because `proc_macro2`/`syn` types contain `proc_macro::TokenStream`
126+
/// bridge handles that become invalid when the invocation that created them ends.
127+
pub fn get_parsed_file(path: &Path) -> Option<syn::File> {
128+
FILE_CACHE.with(|cache| {
129+
let mut cache = cache.borrow_mut();
130+
parse_file_cached(&mut cache, path)
131+
})
132+
}
133+
134+
/// **Single call site for `syn::parse_file`.**
135+
///
136+
/// Reads file content from the mtime-validated content cache (avoids redundant
137+
/// disk I/O), then calls `syn::parse_file`. The resulting `syn::File` is NOT
138+
/// cached — it must be used and dropped within the current proc-macro invocation.
139+
fn parse_file_cached(cache: &mut FileCache, path: &Path) -> Option<syn::File> {
140+
let content = get_file_content_inner(cache, path)?;
141+
cache.ast_parses += 1;
142+
syn::parse_file(&content).ok()
143+
}
144+
115145
/// Get candidate files that likely contain `struct_name`, using cache when available.
116146
///
117147
/// Performs a cheap text-based search (`String::contains`) on file contents.
@@ -165,12 +195,9 @@ fn ensure_struct_definitions(cache: &mut FileCache, path: &Path) -> bool {
165195
return true;
166196
}
167197

168-
// Cache miss — parse file and extract all struct definitions
169-
let Some(content) = get_file_content_inner(cache, path) else {
170-
return false;
171-
};
172-
cache.ast_parses += 1;
173-
let Ok(file_ast) = syn::parse_file(&content) else {
198+
// Cache miss — parse file and extract all struct definitions.
199+
// Uses parse_file_cached: single syn::parse_file entry point.
200+
let Some(file_ast) = parse_file_cached(cache, path) else {
174201
return false;
175202
};
176203

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
mod circular;
88
mod codegen;
9-
mod file_cache;
9+
pub mod file_cache;
1010
mod file_lookup;
1111
mod from_model;
1212
mod inline_types;

0 commit comments

Comments
 (0)