gdnative_project_utils/
scan.rs

1//! Scanning of project sources.
2
3use std::collections::HashSet;
4use std::path::Path;
5
6use proc_macro2::TokenTree;
7
8/// Type-alias for a set of classes that were found from the scan.
9pub type Classes = HashSet<String>;
10
11/// Scan the directory at path `dir` for all `*.rs` files and find types which implement `NativeClass`.
12pub fn scan_crate(dir: impl AsRef<Path>) -> Result<Classes, ScanError> {
13    let rs_extension = std::ffi::OsString::from("rs");
14    let mut paths = vec![];
15
16    for file in ignore::Walk::new(dir.as_ref()) {
17        let file = file.map_err(ScanError::WalkDir)?;
18
19        let path = file.into_path();
20
21        if path.extension() == Some(&rs_extension) {
22            rerun_if_changed(&path);
23            paths.push(path);
24        }
25    }
26
27    let classes = paths
28        .into_iter()
29        .map(|path| -> Result<_, ScanError> {
30            let contents = std::fs::read_to_string(&path).map_err(ScanError::ReadFile)?;
31
32            let file = syn::parse_file(&contents).map_err(ScanError::Parse)?;
33
34            find_classes(&file).map_err(ScanError::Parse)
35        })
36        .try_fold(
37            HashSet::new(),
38            |mut acc, classes_res| -> Result<_, ScanError> {
39                let classes = classes_res?;
40                acc.extend(classes.into_iter().map(|x| x.to_string()));
41                Ok(acc)
42            },
43        )?;
44
45    Ok(classes)
46}
47
48/// Error type for errors that can occur during scanning.
49#[derive(Debug)]
50pub enum ScanError {
51    /// An error was encountered when exploring all the files.
52    WalkDir(ignore::Error),
53    /// An error was encountered when reading in a Rust source file.
54    ReadFile(std::io::Error),
55    /// An error was encountered when parsing a Rust source file.
56    Parse(syn::Error),
57}
58
59impl std::fmt::Display for ScanError {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            ScanError::WalkDir(err) => {
63                f.write_fmt(format_args!("Directory walking error: {}", err))
64            }
65            ScanError::ReadFile(err) => f.write_fmt(format_args!("File reading error: {}", err)),
66            ScanError::Parse(err) => f.write_fmt(format_args!("Parsing error: {}", err)),
67        }
68    }
69}
70
71impl std::error::Error for ScanError {
72    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
73        match self {
74            ScanError::WalkDir(err) => Some(err),
75            ScanError::ReadFile(err) => Some(err),
76            ScanError::Parse(err) => Some(err),
77        }
78    }
79}
80
81fn find_classes(file: &syn::File) -> Result<HashSet<syn::Ident>, syn::Error> {
82    fn derives_nativeclass(attrs: &[syn::Attribute]) -> Result<bool, syn::Error> {
83        let mut res = false;
84
85        for attr in attrs {
86            if !attr.path.is_ident("derive") {
87                continue;
88            }
89
90            for t in attr.tokens.clone() {
91                if let TokenTree::Group(g) = &t {
92                    let s = g.stream();
93
94                    for tt in s {
95                        if let TokenTree::Ident(i) = tt {
96                            if i == "NativeClass" {
97                                res = true;
98                                break;
99                            }
100                        }
101                    }
102                } else {
103                    return Err(syn::Error::new(t.span(), "Unexpected #[derive attribute]"));
104                }
105            }
106        }
107
108        Ok(res)
109    }
110
111    struct Visitor {
112        classes: HashSet<syn::Ident>,
113        errors: Vec<syn::Error>,
114    }
115
116    impl<'ast> syn::visit::Visit<'ast> for Visitor {
117        fn visit_item_struct(&mut self, s: &'ast syn::ItemStruct) {
118            match derives_nativeclass(&s.attrs) {
119                Err(err) => {
120                    self.errors.push(err);
121                }
122                Ok(true) => {
123                    self.classes.insert(s.ident.clone());
124                }
125                Ok(false) => {}
126            }
127            syn::visit::visit_item_struct(self, s)
128        }
129
130        fn visit_item_enum(&mut self, i: &'ast syn::ItemEnum) {
131            match derives_nativeclass(&i.attrs) {
132                Err(err) => {
133                    self.errors.push(err);
134                }
135                Ok(true) => {
136                    self.classes.insert(i.ident.clone());
137                }
138                Ok(false) => {}
139            }
140            syn::visit::visit_item_enum(self, i)
141        }
142    }
143
144    let mut vis = Visitor {
145        classes: HashSet::new(),
146        errors: vec![],
147    };
148
149    syn::visit::visit_file(&mut vis, file);
150
151    if vis.errors.is_empty() {
152        Ok(vis.classes)
153    } else {
154        let mut err = vis.errors.pop().unwrap();
155
156        for e in vis.errors {
157            err.combine(e);
158        }
159
160        Err(err)
161    }
162}
163
164fn rerun_if_changed(path: &Path) {
165    if cfg!(feature = "build_script") {
166        println!("cargo:rerun-if-changed={}", path.display());
167    }
168}