Skip to main content

factorio_frontend/
discovery.rs

1use std::path::Path;
2
3use syn::{File, Item};
4
5use factorio_ir::stage::Stage;
6
7use crate::{
8    error::{FrontendError, FrontendResult},
9    lower::attrs::{extract_factorio_stage, is_factorio_stage_bang},
10    paths::module_name_from_source,
11};
12
13/// A transpilable module discovered in a source file.
14#[derive(Clone)]
15pub struct DiscoveredModule {
16    pub module_name: String,
17    pub stage: Stage,
18    pub items: Vec<Item>,
19}
20
21/// Discover transpilable modules in a source file.
22///
23/// Modules are found via:
24/// - path-based layout (`src/control/...`, `src/control.rs`, ...)
25/// - `factorio_rs::control_mod! { ... }` block macros (and `shared_mod!` / `data_mod!` /
26///   `settings_updates_mod!` / ...)
27/// - `#[factorio_rs::control] mod name { ... }` inline modules
28///
29/// # Errors
30/// Returns `Err` if parsing the Rust source fails.
31pub fn discover_modules(
32    source_dir: &Path,
33    source_path: &Path,
34    source: &str,
35) -> FrontendResult<Vec<DiscoveredModule>> {
36    let file = syn::parse_file(source)?;
37
38    if let Some(module_name) = module_name_from_source(source_dir, source_path) {
39        // Any file under src/ is transpilable. Files whose names don't match a
40        // known stage prefix (control/settings*/data*/shared) default to Shared so
41        // that helper modules like `adjacent_blacklist.rs` don't need to live
42        // inside a `shared/` subdirectory.
43        let stage = Stage::from_module_name(&module_name).unwrap_or(Stage::Shared);
44        return Ok(vec![DiscoveredModule {
45            module_name,
46            stage,
47            items: file.items,
48        }]);
49    }
50
51    let mut discovered = Vec::new();
52
53    if let Some(stage) = extract_factorio_stage(&file.attrs) {
54        discovered.push(DiscoveredModule {
55            module_name: stage.default_module_name().to_string(),
56            stage,
57            items: file.items,
58        });
59        return Ok(discovered);
60    }
61
62    for item in file.items {
63        match item {
64            Item::Macro(item_macro) => {
65                if let Some(stage) = is_factorio_stage_bang(&item_macro.mac.path) {
66                    let items = parse_macro_items(item_macro.mac.tokens)?;
67                    discovered.push(DiscoveredModule {
68                        module_name: stage.default_module_name().to_string(),
69                        stage,
70                        items,
71                    });
72                }
73            }
74            Item::Mod(item_mod) => {
75                if let Some(stage) = extract_factorio_stage(&item_mod.attrs) {
76                    let Some((_, items)) = item_mod.content else {
77                        continue;
78                    };
79                    discovered.push(DiscoveredModule {
80                        module_name: item_mod.ident.to_string(),
81                        stage,
82                        items,
83                    });
84                }
85            }
86            _ => {}
87        }
88    }
89
90    Ok(discovered)
91}
92
93fn parse_macro_items(tokens: proc_macro2::TokenStream) -> FrontendResult<Vec<Item>> {
94    let file = syn::parse2::<File>(tokens).map_err(FrontendError::from)?;
95    Ok(file.items)
96}
97
98#[cfg(test)]
99mod tests {
100    use std::path::Path;
101
102    use super::discover_modules;
103
104    #[test]
105    fn discovers_control_block_macro_in_lib_rs() {
106        let source_dir = Path::new("/project/src");
107        let source_path = source_dir.join("lib.rs");
108        let source = r"
109            factorio_rs::control_mod! {
110                #[factorio_rs::event(OnSingleplayerInit)]
111                pub fn on_singleplayer_init() {}
112            }
113        ";
114
115        let modules = discover_modules(source_dir, &source_path, source).unwrap();
116        assert_eq!(modules.len(), 1);
117        assert_eq!(modules[0].module_name, "control");
118        assert_eq!(modules[0].stage, factorio_ir::stage::Stage::Control);
119    }
120
121    #[test]
122    fn discovers_inner_control_attribute_on_lib_rs() {
123        let source_dir = Path::new("/project/src");
124        let source_path = source_dir.join("lib.rs");
125        let source = r"
126            #![factorio_rs::control]
127
128            #[factorio_rs::event(OnSingleplayerInit)]
129            pub fn on_singleplayer_init() {}
130        ";
131
132        let modules = discover_modules(source_dir, &source_path, source).unwrap();
133        assert_eq!(modules.len(), 1);
134        assert_eq!(modules[0].module_name, "control");
135        assert_eq!(modules[0].stage, factorio_ir::stage::Stage::Control);
136    }
137
138    #[test]
139    fn discovers_attributed_inline_mod() {
140        let source_dir = Path::new("/project/src");
141        let source_path = source_dir.join("lib.rs");
142        let source = r"
143            #[factorio_rs::control]
144            mod handlers {
145                pub fn on_init() {}
146            }
147        ";
148
149        let modules = discover_modules(source_dir, &source_path, source).unwrap();
150        assert_eq!(modules.len(), 1);
151        assert_eq!(modules[0].module_name, "handlers");
152        assert_eq!(modules[0].stage, factorio_ir::stage::Stage::Control);
153    }
154
155    #[test]
156    fn discovers_settings_phase_modules_by_path() {
157        let source_dir = Path::new("/project/src");
158
159        let updates = discover_modules(
160            source_dir,
161            &source_dir.join("settings_updates.rs"),
162            "pub fn patch() {}",
163        )
164        .unwrap();
165        assert_eq!(updates[0].module_name, "settings_updates");
166        assert_eq!(updates[0].stage, factorio_ir::stage::Stage::SettingsUpdates);
167
168        let finals = discover_modules(
169            source_dir,
170            &source_dir.join("settings_final_fixes.rs"),
171            "pub fn fixup() {}",
172        )
173        .unwrap();
174        assert_eq!(finals[0].module_name, "settings_final_fixes");
175        assert_eq!(
176            finals[0].stage,
177            factorio_ir::stage::Stage::SettingsFinalFixes
178        );
179    }
180
181    #[test]
182    fn discovers_data_phase_attribute() {
183        let source_dir = Path::new("/project/src");
184        let source_path = source_dir.join("lib.rs");
185        let source = r"
186            #![factorio_rs::data_updates]
187
188            pub fn patch() {}
189        ";
190
191        let modules = discover_modules(source_dir, &source_path, source).unwrap();
192        assert_eq!(modules.len(), 1);
193        assert_eq!(modules[0].module_name, "data_updates");
194        assert_eq!(modules[0].stage, factorio_ir::stage::Stage::DataUpdates);
195    }
196}