typewriter_engine/
scan.rs1use anyhow::{Context, Result};
4use std::path::{Path, PathBuf};
5use syn::{Data, DeriveInput, Item, ItemEnum, ItemStruct, ItemUnion};
6use walkdir::WalkDir;
7
8use crate::{parser, TypeSpec};
9
10pub fn scan_project(project_root: &Path) -> Result<Vec<TypeSpec>> {
11 let mut specs = Vec::new();
12 for file in discover_rust_files(project_root) {
13 specs.extend(scan_file(&file)?);
14 }
15 Ok(specs)
16}
17
18pub fn scan_file(path: &Path) -> Result<Vec<TypeSpec>> {
19 let content = std::fs::read_to_string(path)
20 .with_context(|| format!("failed to read source file {}", path.display()))?;
21 let parsed = syn::parse_file(&content)
22 .with_context(|| format!("failed to parse Rust source {}", path.display()))?;
23
24 let mut specs = Vec::new();
25 collect_items(&parsed.items, path, &mut specs)?;
26 Ok(specs)
27}
28
29pub fn discover_rust_files(project_root: &Path) -> Vec<PathBuf> {
30 WalkDir::new(project_root)
31 .into_iter()
32 .filter_entry(|entry| {
33 let name = entry.file_name().to_string_lossy();
34 !(name == ".git" || name == "target")
35 })
36 .filter_map(|entry| entry.ok())
37 .filter(|entry| {
38 entry.file_type().is_file()
39 && entry
40 .path()
41 .extension()
42 .map(|ext| ext == "rs")
43 .unwrap_or(false)
44 })
45 .map(|entry| entry.into_path())
46 .collect()
47}
48
49fn collect_items(items: &[Item], source_path: &Path, specs: &mut Vec<TypeSpec>) -> Result<()> {
50 for item in items {
51 match item {
52 Item::Struct(item_struct) => {
53 maybe_collect_from_derive_input(
54 item_struct_to_derive(item_struct),
55 source_path,
56 specs,
57 )?;
58 }
59 Item::Enum(item_enum) => {
60 maybe_collect_from_derive_input(
61 item_enum_to_derive(item_enum),
62 source_path,
63 specs,
64 )?;
65 }
66 Item::Union(item_union) => {
67 maybe_collect_from_derive_input(
68 item_union_to_derive(item_union),
69 source_path,
70 specs,
71 )?;
72 }
73 Item::Mod(item_mod) => {
74 if let Some((_, inline_items)) = &item_mod.content {
75 collect_items(inline_items, source_path, specs)?;
76 }
77 }
78 _ => {}
79 }
80 }
81
82 Ok(())
83}
84
85fn maybe_collect_from_derive_input(
86 input: DeriveInput,
87 source_path: &Path,
88 specs: &mut Vec<TypeSpec>,
89) -> Result<()> {
90 if !parser::has_typewriter_derive(&input.attrs) {
91 return Ok(());
92 }
93
94 let type_def = parser::parse_type_def(&input)
95 .map_err(|err| anyhow::anyhow!("{} ({})", err, source_path.display()))?;
96 let targets = parser::parse_sync_to_attr(&input)
97 .map_err(|err| anyhow::anyhow!("{} ({})", err, source_path.display()))?;
98
99 if targets.is_empty() {
100 return Err(anyhow::anyhow!(
101 "typewriter: #[sync_to(...)] attribute is required. Example: #[sync_to(typescript, python)] ({})",
102 source_path.display()
103 ));
104 }
105
106 specs.push(TypeSpec {
107 type_def,
108 targets,
109 source_path: source_path.to_path_buf(),
110 });
111
112 Ok(())
113}
114
115fn item_struct_to_derive(item: &ItemStruct) -> DeriveInput {
116 DeriveInput {
117 attrs: item.attrs.clone(),
118 vis: item.vis.clone(),
119 ident: item.ident.clone(),
120 generics: item.generics.clone(),
121 data: Data::Struct(syn::DataStruct {
122 struct_token: item.struct_token,
123 fields: item.fields.clone(),
124 semi_token: item.semi_token,
125 }),
126 }
127}
128
129fn item_enum_to_derive(item: &ItemEnum) -> DeriveInput {
130 DeriveInput {
131 attrs: item.attrs.clone(),
132 vis: item.vis.clone(),
133 ident: item.ident.clone(),
134 generics: item.generics.clone(),
135 data: Data::Enum(syn::DataEnum {
136 enum_token: item.enum_token,
137 brace_token: item.brace_token,
138 variants: item.variants.clone(),
139 }),
140 }
141}
142
143fn item_union_to_derive(item: &ItemUnion) -> DeriveInput {
144 DeriveInput {
145 attrs: item.attrs.clone(),
146 vis: item.vis.clone(),
147 ident: item.ident.clone(),
148 generics: item.generics.clone(),
149 data: Data::Union(syn::DataUnion {
150 union_token: item.union_token,
151 fields: item.fields.clone(),
152 }),
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn scans_typewriter_items_from_file() {
162 let temp = tempfile::tempdir().unwrap();
163 let file = temp.path().join("mod.rs");
164 std::fs::write(
165 &file,
166 r#"
167 #[derive(TypeWriter)]
168 #[sync_to(typescript, python)]
169 struct User {
170 id: String,
171 }
172 "#,
173 )
174 .unwrap();
175
176 let specs = scan_file(&file).unwrap();
177 assert_eq!(specs.len(), 1);
178 assert_eq!(specs[0].type_def.name(), "User");
179 }
180}