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