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::{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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
29pub enum RequireMode {
30 Path(PathRequireMode),
32 Luau(LuauRequireMode),
34 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#[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}