darklua_core/rules/convert_require/
roblox_require_mode.rs1use serde::{Deserialize, Serialize};
2
3use crate::{
4 frontend::DarkluaResult,
5 nodes::{Arguments, FunctionCall, Prefix},
6 rules::{convert_require::rojo_sourcemap::RojoSourcemap, Context},
7 utils, DarkluaError,
8};
9
10use std::path::{Component, Path, PathBuf};
11
12use super::{
13 instance_path::{get_parent_instance, script_identifier},
14 RequireMode, RobloxIndexStyle,
15};
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(deny_unknown_fields, rename_all = "snake_case")]
19pub struct RobloxRequireMode {
20 rojo_sourcemap: Option<PathBuf>,
21 #[serde(default, deserialize_with = "crate::utils::string_or_struct")]
22 indexing_style: RobloxIndexStyle,
23 #[serde(skip)]
24 cached_sourcemap: Option<RojoSourcemap>,
25}
26
27impl RobloxRequireMode {
28 pub(crate) fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
29 if let Some(ref rojo_sourcemap_path) = self
30 .rojo_sourcemap
31 .as_ref()
32 .map(|rojo_sourcemap_path| context.project_location().join(rojo_sourcemap_path))
33 {
34 context.add_file_dependency(rojo_sourcemap_path.clone());
35
36 let sourcemap_parent_location = get_relative_parent_path(rojo_sourcemap_path);
37 let sourcemap = RojoSourcemap::parse(
38 &context
39 .resources()
40 .get(rojo_sourcemap_path)
41 .map_err(|err| {
42 DarkluaError::from(err).context("while initializing Roblox require mode")
43 })?,
44 sourcemap_parent_location,
45 )
46 .map_err(|err| {
47 err.context(format!(
48 "unable to parse Rojo sourcemap at `{}`",
49 rojo_sourcemap_path.display()
50 ))
51 })?;
52 self.cached_sourcemap = Some(sourcemap);
53 }
54 Ok(())
55 }
56
57 pub(crate) fn find_require(
58 &self,
59 _call: &FunctionCall,
60 _context: &Context,
61 ) -> DarkluaResult<Option<PathBuf>> {
62 Err(DarkluaError::custom("unsupported initial require mode")
63 .context("Roblox require mode cannot be used as the current require mode"))
64 }
65
66 pub(crate) fn generate_require(
67 &self,
68 require_path: &Path,
69 current: &RequireMode,
70 context: &Context,
71 ) -> DarkluaResult<Option<Arguments>> {
72 let source_path = utils::normalize_path(context.current_path());
73 log::trace!(
74 "generate Roblox require for `{}` from `{}`",
75 require_path.display(),
76 source_path.display(),
77 );
78
79 if let Some((sourcemap, sourcemap_path)) = self
80 .cached_sourcemap
81 .as_ref()
82 .zip(self.rojo_sourcemap.as_ref())
83 {
84 if let Some(require_relative_to_sourcemap) = get_relative_path(
85 require_path,
86 get_relative_parent_path(sourcemap_path),
87 false,
88 )? {
89 log::trace!(
90 " ⨽ use sourcemap at `{}` to find `{}`",
91 sourcemap_path.display(),
92 require_relative_to_sourcemap.display()
93 );
94
95 if let Some(instance_path) =
96 sourcemap.get_instance_path(&source_path, &require_relative_to_sourcemap)
97 {
98 Ok(Some(Arguments::default().with_argument(
99 instance_path.convert(&self.indexing_style),
100 )))
101 } else {
102 log::warn!(
103 "unable to find path `{}` in sourcemap (from `{}`)",
104 require_relative_to_sourcemap.display(),
105 source_path.display()
106 );
107 Ok(None)
108 }
109 } else {
110 log::debug!(
111 "unable to get relative path from sourcemap for `{}`",
112 require_path.display()
113 );
114 Ok(None)
115 }
116 } else if let Some(relative_require_path) =
117 get_relative_path(require_path, &source_path, true)?
118 {
119 log::trace!(
120 "make require path relative to source: `{}`",
121 relative_require_path.display()
122 );
123
124 let require_is_module_folder_name =
125 current.is_module_folder_name(&relative_require_path);
126 let take_components = relative_require_path
129 .components()
130 .count()
131 .saturating_sub(if require_is_module_folder_name { 1 } else { 0 });
132 let mut path_components = relative_require_path.components().take(take_components);
133
134 if let Some(first_component) = path_components.next() {
135 let source_is_module_folder_name = current.is_module_folder_name(&source_path);
136
137 let instance_path = path_components.try_fold(
138 match first_component {
139 Component::CurDir => {
140 if source_is_module_folder_name {
141 script_identifier().into()
142 } else {
143 get_parent_instance(script_identifier())
144 }
145 }
146 Component::ParentDir => {
147 if source_is_module_folder_name {
148 get_parent_instance(script_identifier())
149 } else {
150 get_parent_instance(get_parent_instance(script_identifier()))
151 }
152 }
153 Component::Normal(_) => {
154 return Err(DarkluaError::custom(format!(
155 concat!(
156 "unable to convert path `{}`: the require path should be ",
157 "relative and start with `.` or `..` (got `{}`)"
158 ),
159 require_path.display(),
160 relative_require_path.display(),
161 )))
162 }
163 Component::Prefix(_) | Component::RootDir => {
164 return Err(DarkluaError::custom(format!(
165 concat!(
166 "unable to convert absolute path `{}`: ",
167 "without a provided Rojo sourcemap, ",
168 "darklua can only convert relative paths ",
169 "(starting with `.` or `..`)"
170 ),
171 require_path.display(),
172 )))
173 }
174 },
175 |instance: Prefix, component| match component {
176 Component::CurDir => Ok(instance),
177 Component::ParentDir => Ok(get_parent_instance(instance)),
178 Component::Normal(name) => utils::convert_os_string(name)
179 .map(|child_name| self.indexing_style.index(instance, child_name)),
180 Component::Prefix(_) | Component::RootDir => {
181 Err(DarkluaError::custom(format!(
182 "unable to convert path `{}`: unexpected component in relative path `{}`",
183 require_path.display(),
184 relative_require_path.display(),
185 )))
186 },
187 },
188 )?;
189
190 Ok(Some(Arguments::default().with_argument(instance_path)))
191 } else {
192 Err(DarkluaError::custom(format!(
193 "unable to convert path `{}` from `{}` without a sourcemap: the relative path is empty `{}`",
194 require_path.display(),
195 source_path.display(),
196 relative_require_path.display(),
197 )))
198 }
199 } else {
200 Err(DarkluaError::custom(format!(
201 concat!(
202 "unable to convert path `{}` from `{}` without a sourcemap: unable to ",
203 "make the require path relative to the source file"
204 ),
205 require_path.display(),
206 source_path.display(),
207 )))
208 }
209 }
210}
211
212fn get_relative_path(
213 require_path: &Path,
214 source_path: &Path,
215 use_current_dir_prefix: bool,
216) -> Result<Option<PathBuf>, DarkluaError> {
217 Ok(
218 pathdiff::diff_paths(require_path, get_relative_parent_path(source_path))
219 .map(|path| {
220 if use_current_dir_prefix && !path.starts_with(".") && !path.starts_with("..") {
221 Path::new(".").join(path)
222 } else if !use_current_dir_prefix && path.starts_with(".") {
223 path.strip_prefix(".")
224 .map(Path::to_path_buf)
225 .ok()
226 .unwrap_or(path)
227 } else {
228 path
229 }
230 })
231 .map(utils::normalize_path_with_current_dir),
232 )
233}
234
235fn get_relative_parent_path(path: &Path) -> &Path {
236 match path.parent() {
237 Some(parent) => {
238 if parent == Path::new("") {
239 Path::new(".")
240 } else {
241 parent
242 }
243 }
244 None => Path::new(".."),
245 }
246}