Skip to main content

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