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(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#[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}