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