factorio_frontend/
discovery.rs1use 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#[derive(Clone)]
15pub struct DiscoveredModule {
16 pub module_name: String,
17 pub stage: Stage,
18 pub items: Vec<Item>,
19}
20
21pub 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 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}