darklua_core/rules/convert_require/
mod.rs1mod 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
31pub enum RequireMode {
32 Path(PathRequireMode),
34 Luau(LuauRequireMode),
36 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#[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}