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(mut require_path) = self.current.find_require(call, self.context)? {
134            log::trace!("found require path `{}`", require_path.display());
135
136            let file_loader = self
137                .context
138                .loaders()
139                .get_loader(&require_path)
140                .to_internal_loader();
141
142            if file_loader.outputs_lua()
143                && !matches!(
144                    require_path.extension().and_then(OsStr::to_str),
145                    Some("lua") | Some("luau")
146                )
147            {
148                require_path.set_extension(self.context.preferred_lua_extension());
149            }
150
151            if let Some(new_arguments) =
152                self.target
153                    .generate_require(&require_path, &self.current, self.context)?
154            {
155                call.set_arguments(new_arguments);
156            }
157        }
158        Ok(())
159    }
160}
161
162impl NodeProcessor for RequireConverter<'_> {
163    fn process_function_call(&mut self, call: &mut FunctionCall) {
164        if is_require_call(call, self) {
165            match self.try_require_conversion(call) {
166                Ok(()) => {}
167                Err(err) => {
168                    log::warn!("{}", err);
169                }
170            }
171        }
172    }
173}
174
175pub const CONVERT_REQUIRE_RULE_NAME: &str = "convert_require";
176
177/// A rule that converts require calls between environments
178#[derive(Debug, PartialEq, Eq)]
179pub struct ConvertRequire {
180    metadata: RuleMetadata,
181    current: RequireMode,
182    target: RequireMode,
183}
184
185impl Default for ConvertRequire {
186    fn default() -> Self {
187        Self {
188            metadata: RuleMetadata::default(),
189            current: RequireMode::Path(Default::default()),
190            target: RequireMode::Roblox(Default::default()),
191        }
192    }
193}
194
195impl Rule for ConvertRequire {
196    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
197        let mut current_mode = self.current.clone();
198        current_mode
199            .initialize(context)
200            .map_err(|err| err.to_string())?;
201
202        let mut target_mode = self.target.clone();
203        target_mode
204            .initialize(context)
205            .map_err(|err| err.to_string())?;
206
207        let mut processor = RequireConverter::new(current_mode, target_mode, context);
208        DefaultVisitor::visit_block(block, &mut processor);
209        Ok(())
210    }
211}
212
213impl RuleConfiguration for ConvertRequire {
214    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
215        verify_required_properties(&properties, &["current", "target"])?;
216
217        for (key, value) in properties {
218            match key.as_str() {
219                "current" => {
220                    self.current = value.expect_require_mode(&key)?;
221                }
222                "target" => {
223                    self.target = value.expect_require_mode(&key)?;
224                }
225                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
226            }
227        }
228
229        Ok(())
230    }
231
232    fn get_name(&self) -> &'static str {
233        CONVERT_REQUIRE_RULE_NAME
234    }
235
236    fn serialize_to_properties(&self) -> RuleProperties {
237        RuleProperties::new()
238    }
239
240    fn set_metadata(&mut self, metadata: RuleMetadata) {
241        self.metadata = metadata;
242    }
243
244    fn metadata(&self) -> &RuleMetadata {
245        &self.metadata
246    }
247}
248
249#[cfg(test)]
250mod test {
251    use super::*;
252    use crate::rules::Rule;
253
254    use insta::assert_json_snapshot;
255
256    fn new_rule() -> ConvertRequire {
257        ConvertRequire::default()
258    }
259
260    #[test]
261    fn serialize_default_rule() {
262        let rule: Box<dyn Rule> = Box::new(new_rule());
263
264        assert_json_snapshot!("default_convert_require", rule);
265    }
266
267    #[test]
268    fn configure_with_invalid_require_mode_error() {
269        let result = json5::from_str::<Box<dyn Rule>>(
270            r#"{
271            rule: 'convert_require',
272            current: 'path',
273            target: 'rblox',
274        }"#,
275        );
276        insta::assert_snapshot!(
277            result.unwrap_err().to_string(),
278            @"unexpected value for field 'target': invalid require mode name `rblox` at line 1 column 1"
279        );
280    }
281
282    #[test]
283    fn configure_with_extra_field_error() {
284        let result = json5::from_str::<Box<dyn Rule>>(
285            r#"{
286            rule: 'convert_require',
287            current: 'path',
288            target: 'path',
289            prop: "something",
290        }"#,
291        );
292        insta::assert_snapshot!(result.unwrap_err().to_string(), @"unexpected field 'prop' at line 1 column 1");
293    }
294}