darklua_core/rules/convert_require/
mod.rs

1mod instance_path;
2mod roblox_index_style;
3mod roblox_require_mode;
4mod rojo_sourcemap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::frontend::DarkluaResult;
9use crate::nodes::{Arguments, Block, FunctionCall};
10use crate::process::{DefaultVisitor, IdentifierTracker, NodeProcessor, NodeVisitor};
11use crate::rules::require::{is_require_call, PathRequireMode};
12use crate::rules::{Context, RuleConfiguration, RuleConfigurationError, RuleProperties};
13
14use instance_path::InstancePath;
15pub use roblox_index_style::RobloxIndexStyle;
16pub use roblox_require_mode::RobloxRequireMode;
17
18use super::{verify_required_properties, Rule, RuleProcessResult};
19
20use std::ffi::OsStr;
21use std::ops::{Deref, DerefMut};
22use std::path::{Path, PathBuf};
23use std::str::FromStr;
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
27pub enum RequireMode {
28    Path(PathRequireMode),
29    Roblox(RobloxRequireMode),
30}
31
32impl RequireMode {
33    fn find_require(
34        &self,
35        call: &FunctionCall,
36        context: &Context,
37    ) -> DarkluaResult<Option<PathBuf>> {
38        match self {
39            RequireMode::Path(path_mode) => path_mode.find_require(call, context),
40            RequireMode::Roblox(roblox_mode) => roblox_mode.find_require(call, context),
41        }
42    }
43
44    fn generate_require(
45        &self,
46        path: &Path,
47        current_mode: &Self,
48        context: &Context,
49    ) -> DarkluaResult<Option<Arguments>> {
50        match self {
51            RequireMode::Path(path_mode) => path_mode.generate_require(path, current_mode, context),
52            RequireMode::Roblox(roblox_mode) => {
53                roblox_mode.generate_require(path, current_mode, context)
54            }
55        }
56    }
57
58    fn is_module_folder_name(&self, path: &Path) -> bool {
59        match self {
60            RequireMode::Path(path_mode) => path_mode.is_module_folder_name(path),
61            RequireMode::Roblox(_roblox_mode) => {
62                matches!(path.file_stem().and_then(OsStr::to_str), Some("init"))
63            }
64        }
65    }
66
67    fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
68        match self {
69            RequireMode::Roblox(roblox_mode) => roblox_mode.initialize(context),
70            RequireMode::Path(_) => Ok(()),
71        }
72    }
73}
74
75impl FromStr for RequireMode {
76    type Err = String;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        Ok(match s {
80            "path" => Self::Path(Default::default()),
81            "roblox" => Self::Roblox(Default::default()),
82            _ => return Err(format!("invalid require mode name `{}`", s)),
83        })
84    }
85}
86
87#[derive(Debug, Clone)]
88struct RequireConverter<'a> {
89    identifier_tracker: IdentifierTracker,
90    current: RequireMode,
91    target: RequireMode,
92    context: &'a Context<'a, 'a, 'a>,
93}
94
95impl<'a> Deref for RequireConverter<'a> {
96    type Target = IdentifierTracker;
97
98    fn deref(&self) -> &Self::Target {
99        &self.identifier_tracker
100    }
101}
102
103impl<'a> DerefMut for RequireConverter<'a> {
104    fn deref_mut(&mut self) -> &mut Self::Target {
105        &mut self.identifier_tracker
106    }
107}
108
109impl<'a> RequireConverter<'a> {
110    fn new(current: RequireMode, target: RequireMode, context: &'a Context) -> Self {
111        Self {
112            identifier_tracker: IdentifierTracker::new(),
113            current,
114            target,
115            context,
116        }
117    }
118
119    fn try_require_conversion(&mut self, call: &mut FunctionCall) -> DarkluaResult<()> {
120        if let Some(require_path) = self.current.find_require(call, self.context)? {
121            log::trace!("found require path `{}`", require_path.display());
122
123            if let Some(new_arguments) =
124                self.target
125                    .generate_require(&require_path, &self.current, self.context)?
126            {
127                call.set_arguments(new_arguments);
128            }
129        }
130        Ok(())
131    }
132}
133
134impl<'a> NodeProcessor for RequireConverter<'a> {
135    fn process_function_call(&mut self, call: &mut FunctionCall) {
136        if is_require_call(call, self) {
137            match self.try_require_conversion(call) {
138                Ok(()) => {}
139                Err(err) => {
140                    log::warn!("{}", err);
141                }
142            }
143        }
144    }
145}
146
147pub const CONVERT_REQUIRE_RULE_NAME: &str = "convert_require";
148
149/// A rule that converts require calls between environments
150#[derive(Debug, PartialEq, Eq)]
151pub struct ConvertRequire {
152    current: RequireMode,
153    target: RequireMode,
154}
155
156impl Default for ConvertRequire {
157    fn default() -> Self {
158        Self {
159            current: RequireMode::Path(Default::default()),
160            target: RequireMode::Roblox(Default::default()),
161        }
162    }
163}
164
165impl Rule for ConvertRequire {
166    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
167        let mut current_mode = self.current.clone();
168        current_mode
169            .initialize(context)
170            .map_err(|err| err.to_string())?;
171
172        let mut target_mode = self.target.clone();
173        target_mode
174            .initialize(context)
175            .map_err(|err| err.to_string())?;
176
177        let mut processor = RequireConverter::new(current_mode, target_mode, context);
178        DefaultVisitor::visit_block(block, &mut processor);
179        Ok(())
180    }
181}
182
183impl RuleConfiguration for ConvertRequire {
184    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
185        verify_required_properties(&properties, &["current", "target"])?;
186
187        for (key, value) in properties {
188            match key.as_str() {
189                "current" => {
190                    self.current = value.expect_require_mode(&key)?;
191                }
192                "target" => {
193                    self.target = value.expect_require_mode(&key)?;
194                }
195                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
196            }
197        }
198
199        Ok(())
200    }
201
202    fn get_name(&self) -> &'static str {
203        CONVERT_REQUIRE_RULE_NAME
204    }
205
206    fn serialize_to_properties(&self) -> RuleProperties {
207        RuleProperties::new()
208    }
209}
210
211#[cfg(test)]
212mod test {
213    use super::*;
214    use crate::rules::Rule;
215
216    use insta::assert_json_snapshot;
217
218    fn new_rule() -> ConvertRequire {
219        ConvertRequire::default()
220    }
221
222    #[test]
223    fn serialize_default_rule() {
224        let rule: Box<dyn Rule> = Box::new(new_rule());
225
226        assert_json_snapshot!("default_convert_require", rule);
227    }
228
229    #[test]
230    fn configure_with_invalid_require_mode_error() {
231        let result = json5::from_str::<Box<dyn Rule>>(
232            r#"{
233            rule: 'convert_require',
234            current: 'path',
235            target: 'rblox',
236        }"#,
237        );
238        pretty_assertions::assert_eq!(
239            result.unwrap_err().to_string(),
240            "unexpected value for field 'target': invalid require mode name `rblox`"
241        );
242    }
243
244    #[test]
245    fn configure_with_extra_field_error() {
246        let result = json5::from_str::<Box<dyn Rule>>(
247            r#"{
248            rule: 'convert_require',
249            current: 'path',
250            target: 'path',
251            prop: "something",
252        }"#,
253        );
254        pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
255    }
256}