darklua_core/rules/convert_require/
rojo_sourcemap.rs1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{utils, DarkluaError};
6
7use super::InstancePath;
8
9type NodeId = usize;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase")]
13struct RojoSourcemapNode {
14 name: String,
15 class_name: String,
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
17 file_paths: Vec<PathBuf>,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
19 children: Vec<RojoSourcemapNode>,
20 #[serde(skip)]
21 id: NodeId,
22 #[serde(skip)]
23 parent_id: NodeId,
24}
25
26impl RojoSourcemapNode {
27 fn initialize(mut self, relative_to: &Path) -> Self {
28 let mut queue = vec![&mut self];
29 let mut index = 0;
30
31 while let Some(node) = queue.pop() {
32 node.id = index;
33 for file_path in &mut node.file_paths {
34 *file_path = utils::normalize_path(relative_to.join(&file_path));
35 }
36 for child in &mut node.children {
37 child.parent_id = index;
38 queue.push(child);
39 }
40 index += 1;
41 }
42
43 self
44 }
45
46 fn id(&self) -> NodeId {
47 self.id
48 }
49
50 fn parent_id(&self) -> NodeId {
51 self.parent_id
52 }
53
54 fn iter(&self) -> impl Iterator<Item = &Self> {
55 RojoSourcemapNodeIterator::new(self)
56 }
57
58 fn get_child(&self, id: NodeId) -> Option<&RojoSourcemapNode> {
59 self.children.iter().find(|node| node.id == id)
60 }
61
62 fn get_descendant(&self, id: NodeId) -> Option<&RojoSourcemapNode> {
63 self.iter().find(|node| node.id == id)
64 }
65
66 fn is_root(&self) -> bool {
67 self.id == self.parent_id
68 }
69}
70
71struct RojoSourcemapNodeIterator<'a> {
72 queue: Vec<&'a RojoSourcemapNode>,
73}
74
75impl<'a> RojoSourcemapNodeIterator<'a> {
76 fn new(root_node: &'a RojoSourcemapNode) -> Self {
77 Self {
78 queue: vec![root_node],
79 }
80 }
81}
82
83impl<'a> Iterator for RojoSourcemapNodeIterator<'a> {
84 type Item = &'a RojoSourcemapNode;
85
86 fn next(&mut self) -> Option<Self::Item> {
87 if let Some(next_node) = self.queue.pop() {
88 for child in &next_node.children {
89 self.queue.push(child);
90 }
91 Some(next_node)
92 } else {
93 None
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub(crate) struct RojoSourcemap {
100 root_node: RojoSourcemapNode,
101 is_datamodel: bool,
102}
103
104impl RojoSourcemap {
105 pub(crate) fn parse(
106 content: &str,
107 relative_to: impl AsRef<Path>,
108 ) -> Result<Self, DarkluaError> {
109 let root_node =
110 serde_json::from_str::<RojoSourcemapNode>(content)?.initialize(relative_to.as_ref());
111
112 let is_datamodel = root_node.class_name == "DataModel";
113 Ok(Self {
114 root_node,
115 is_datamodel,
116 })
117 }
118
119 pub(crate) fn get_instance_path(
120 &self,
121 from_file: impl AsRef<Path>,
122 target_file: impl AsRef<Path>,
123 ) -> Option<InstancePath> {
124 let from_file = from_file.as_ref();
125 let target_file = target_file.as_ref();
126
127 let from_node = self.find_node(from_file)?;
128 let target_node = self.find_node(target_file)?;
129
130 let from_ancestors = self.hierarchy(from_node);
131 let target_ancestors = self.hierarchy(target_node);
132
133 let (parents, descendants, common_ancestor_id) = from_ancestors
134 .iter()
135 .enumerate()
136 .find_map(|(index, ancestor_id)| {
137 if let Some((target_index, common_ancestor_id)) = target_ancestors
138 .iter()
139 .enumerate()
140 .find(|(_, id)| *id == ancestor_id)
141 {
142 Some((index, target_index, *common_ancestor_id))
143 } else {
144 None
145 }
146 })
147 .map(
148 |(from_ancestor_split, target_ancestor_split, common_ancestor_id)| {
149 (
150 from_ancestors.split_at(from_ancestor_split).0,
151 target_ancestors.split_at(target_ancestor_split).0,
152 common_ancestor_id,
153 )
154 },
155 )?;
156
157 let relative_path_length = parents.len().saturating_add(descendants.len());
158
159 if !self.is_datamodel || relative_path_length <= target_ancestors.len() {
160 log::trace!(" ⨽ use Roblox path from script instance");
161
162 let mut instance_path = InstancePath::from_script();
163
164 for _ in 0..parents.len() {
165 instance_path.parent();
166 }
167
168 self.index_descendants(
169 instance_path,
170 self.root_node.get_descendant(common_ancestor_id)?,
171 descendants.iter().rev(),
172 )
173 } else {
174 log::trace!(" ⨽ use Roblox path from DataModel instance");
175
176 self.index_descendants(
177 InstancePath::from_root(),
178 &self.root_node,
179 target_ancestors.iter().rev().skip(1),
180 )
181 }
182 }
183
184 fn index_descendants<'a>(
185 &self,
186 mut instance_path: InstancePath,
187 mut node: &RojoSourcemapNode,
188 descendants: impl Iterator<Item = &'a usize>,
189 ) -> Option<InstancePath> {
190 for descendant_id in descendants {
191 node = node.get_child(*descendant_id)?;
192 instance_path.child(&node.name);
193 }
194 Some(instance_path)
195 }
196
197 fn hierarchy(&self, node: &RojoSourcemapNode) -> Vec<NodeId> {
199 let mut ids = vec![node.id()];
200
201 if node.is_root() {
202 return ids;
203 }
204
205 let mut parent_id = node.parent_id();
206
207 while let Some(parent) = self.root_node.get_descendant(parent_id) {
208 ids.push(parent_id);
209 if parent.is_root() {
210 break;
211 }
212 parent_id = parent.parent_id();
213 }
214
215 ids
216 }
217
218 fn find_node(&self, path: &Path) -> Option<&RojoSourcemapNode> {
219 self.root_node
220 .iter()
221 .find(|node| node.file_paths.iter().any(|file_path| file_path == path))
222 }
223}
224
225#[cfg(test)]
226mod test {
227 use super::*;
228
229 fn new_sourcemap(content: &str) -> RojoSourcemap {
230 RojoSourcemap::parse(content, "").expect("unable to parse sourcemap")
231 }
232
233 mod instance_paths {
234 use super::*;
235
236 fn script_path(components: &[&'static str]) -> InstancePath {
237 components
238 .iter()
239 .fold(InstancePath::from_script(), |mut path, component| {
240 match *component {
241 "parent" => {
242 path.parent();
243 }
244 child_name => {
245 path.child(child_name);
246 }
247 }
248 path
249 })
250 }
251
252 #[test]
253 fn from_init_to_sibling_module() {
254 let sourcemap = new_sourcemap(
255 r#"{
256 "name": "Project",
257 "className": "ModuleScript",
258 "filePaths": ["src/init.lua", "default.project.json"],
259 "children": [
260 {
261 "name": "value",
262 "className": "ModuleScript",
263 "filePaths": ["src/value.lua"]
264 }
265 ]
266 }"#,
267 );
268 pretty_assertions::assert_eq!(
269 sourcemap
270 .get_instance_path("src/init.lua", "src/value.lua")
271 .unwrap(),
272 script_path(&["value"])
273 );
274 }
275
276 #[test]
277 fn from_sibling_to_sibling_module() {
278 let sourcemap = new_sourcemap(
279 r#"{
280 "name": "Project",
281 "className": "ModuleScript",
282 "filePaths": ["src/init.lua", "default.project.json"],
283 "children": [
284 {
285 "name": "main",
286 "className": "ModuleScript",
287 "filePaths": ["src/main.lua"]
288 },
289 {
290 "name": "value",
291 "className": "ModuleScript",
292 "filePaths": ["src/value.lua"]
293 }
294 ]
295 }"#,
296 );
297 pretty_assertions::assert_eq!(
298 sourcemap
299 .get_instance_path("src/main.lua", "src/value.lua")
300 .unwrap(),
301 script_path(&["parent", "value"])
302 );
303 }
304
305 #[test]
306 fn from_sibling_to_nested_sibling_module() {
307 let sourcemap = new_sourcemap(
308 r#"{
309 "name": "Project",
310 "className": "ModuleScript",
311 "filePaths": ["src/init.lua", "default.project.json"],
312 "children": [
313 {
314 "name": "main",
315 "className": "ModuleScript",
316 "filePaths": ["src/main.lua"]
317 },
318 {
319 "name": "Lib",
320 "className": "Folder",
321 "children": [
322 {
323 "name": "format",
324 "className": "ModuleScript",
325 "filePaths": ["src/Lib/format.lua"]
326 }
327 ]
328 }
329 ]
330 }"#,
331 );
332 pretty_assertions::assert_eq!(
333 sourcemap
334 .get_instance_path("src/main.lua", "src/Lib/format.lua")
335 .unwrap(),
336 script_path(&["parent", "Lib", "format"])
337 );
338 }
339
340 #[test]
341 fn from_child_require_parent() {
342 let sourcemap = new_sourcemap(
343 r#"{
344 "name": "Project",
345 "className": "ModuleScript",
346 "filePaths": ["src/init.lua", "default.project.json"],
347 "children": [
348 {
349 "name": "main",
350 "className": "ModuleScript",
351 "filePaths": ["src/main.lua"]
352 }
353 ]
354 }"#,
355 );
356 pretty_assertions::assert_eq!(
357 sourcemap
358 .get_instance_path("src/main.lua", "src/init.lua")
359 .unwrap(),
360 script_path(&["parent"])
361 );
362 }
363
364 #[test]
365 fn from_child_require_parent_nested() {
366 let sourcemap = new_sourcemap(
367 r#"{
368 "name": "Project",
369 "className": "ModuleScript",
370 "filePaths": ["src/init.lua", "default.project.json"],
371 "children": [
372 {
373 "name": "Sub",
374 "className": "ModuleScript",
375 "filePaths": ["src/Sub/init.lua"],
376 "children": [
377 {
378 "name": "test",
379 "className": "ModuleScript",
380 "filePaths": ["src/Sub/test.lua"]
381 }
382 ]
383 }
384 ]
385 }"#,
386 );
387 pretty_assertions::assert_eq!(
388 sourcemap
389 .get_instance_path("src/Sub/test.lua", "src/Sub/init.lua")
390 .unwrap(),
391 script_path(&["parent"])
392 );
393 }
394 }
395}