Skip to main content

typewriter_engine/
scan.rs

1//! Source scanning for `#[derive(TypeWriter)]` items.
2
3use 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}