darklua_core/rules/convert_require/
rojo_sourcemap.rs

1use 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    /// returns the ids of each ancestor of the given node and itself
198    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}