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