Repository: seaofvoices/darklua (rust)

Issue:
Make Rojo sourcemap paths relative
When using a rojo sourcemap to convert requires, the paths should be relative to the location of the sourcemap itself.

Pull request:
 Normalize Rojo sourcemap paths relative to its location
Closes #116 

This makes the sourcemap paths relative to the sourcemap location itself. 

- [x] add entry to the changelog


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa0beb0..6eede26 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+* make rojo sourcemap paths relative to its file location ([#117](https://github.com/seaofvoices/darklua/pull/117))
+
 ## 0.10.0
 
 * convert configuration to snake case (this renames the `retain-lines` generator to `retain_lines`) ([#114](https://github.com/seaofvoices/darklua/pull/114))
diff --git a/src/rules/convert_require/mod.rs b/src/rules/convert_require/mod.rs
index 146e9bf..c4ba6b2 100644
--- a/src/rules/convert_require/mod.rs
+++ b/src/rules/convert_require/mod.rs
@@ -10,7 +10,6 @@ use crate::nodes::{Arguments, Block, FunctionCall};
 use crate::process::{DefaultVisitor, IdentifierTracker, NodeProcessor, NodeVisitor};
 use crate::rules::require::{is_require_call, PathRequireMode};
 use crate::rules::{Context, RuleConfiguration, RuleConfigurationError, RuleProperties};
-use crate::Resources;
 
 use instance_path::InstancePath;
 pub use roblox_index_style::RobloxIndexStyle;
@@ -65,9 +64,9 @@ impl RequireMode {
         }
     }
 
-    fn initialize(&mut self, resources: &Resources) -> DarkluaResult<()> {
+    fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
         match self {
-            RequireMode::Roblox(roblox_mode) => roblox_mode.initialize(resources),
+            RequireMode::Roblox(roblox_mode) => roblox_mode.initialize(context),
             RequireMode::Path(_) => Ok(()),
         }
     }
@@ -167,12 +166,12 @@ impl Rule for ConvertRequire {
     fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
         let mut current_mode = self.current.clone();
         current_mode
-            .initialize(context.resources())
+            .initialize(context)
             .map_err(|err| err.to_string())?;
 
         let mut target_mode = self.target.clone();
         target_mode
-            .initialize(context.resources())
+            .initialize(context)
             .map_err(|err| err.to_string())?;
 
         let mut processor = RequireConverter::new(current_mode, target_mode, context);
diff --git a/src/rules/convert_require/roblox_require_mode.rs b/src/rules/convert_require/roblox_require_mode.rs
index 6408c50..39921ca 100644
--- a/src/rules/convert_require/roblox_require_mode.rs
+++ b/src/rules/convert_require/roblox_require_mode.rs
@@ -4,7 +4,7 @@ use crate::{
     frontend::DarkluaResult,
     nodes::{Arguments, FunctionCall, Prefix},
     rules::{convert_require::rojo_sourcemap::RojoSourcemap, Context},
-    utils, DarkluaError, Resources,
+    utils, DarkluaError,
 };
 
 use std::path::{Component, Path, PathBuf};
@@ -25,15 +25,30 @@ pub struct RobloxRequireMode {
 }
 
 impl RobloxRequireMode {
-    pub(crate) fn initialize(&mut self, resources: &Resources) -> DarkluaResult<()> {
-        if let Some(rojo_sourcemap_path) = &self.rojo_sourcemap {
-            let sourcemap =
-                RojoSourcemap::parse(&resources.get(rojo_sourcemap_path)?).map_err(|err| {
-                    err.context(format!(
-                        "unable to parse Rojo sourcemap at `{}`",
-                        rojo_sourcemap_path.display()
-                    ))
-                })?;
+    pub(crate) fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
+        if let Some(ref rojo_sourcemap_path) = self
+            .rojo_sourcemap
+            .as_ref()
+            .map(|rojo_sourcemap_path| context.project_location().join(rojo_sourcemap_path))
+        {
+            let sourcemap_parent_location = rojo_sourcemap_path
+                .parent()
+                .unwrap_or_else(|| Path::new("."));
+            let sourcemap = RojoSourcemap::parse(
+                &context
+                    .resources()
+                    .get(rojo_sourcemap_path)
+                    .map_err(|err| {
+                        DarkluaError::from(err).context("while initializing Roblox require mode")
+                    })?,
+                sourcemap_parent_location,
+            )
+            .map_err(|err| {
+                err.context(format!(
+                    "unable to parse Rojo sourcemap at `{}`",
+                    rojo_sourcemap_path.display()
+                ))
+            })?;
             self.cached_sourcemap = Some(sourcemap);
         }
         Ok(())
@@ -54,7 +69,7 @@ impl RobloxRequireMode {
         current: &RequireMode,
         context: &Context,
     ) -> DarkluaResult<Option<Arguments>> {
-        let source_path = context.current_path();
+        let source_path = utils::normalize_path(context.current_path());
         log::trace!(
             "generate Roblox require for `{}` from `{}`",
             require_path.display(),
@@ -78,7 +93,7 @@ impl RobloxRequireMode {
                 );
 
                 if let Some(instance_path) =
-                    sourcemap.get_instance_path(source_path, &require_relative_to_sourcemap)
+                    sourcemap.get_instance_path(&source_path, &require_relative_to_sourcemap)
                 {
                     Ok(Some(Arguments::default().with_argument(
                         instance_path.convert(&self.indexing_style),
@@ -99,7 +114,7 @@ impl RobloxRequireMode {
                 Ok(None)
             }
         } else if let Some(relative_require_path) =
-            get_relative_path(require_path, source_path, true)?
+            get_relative_path(require_path, &source_path, true)?
         {
             log::trace!(
                 "make require path relative to source: `{}`",
@@ -117,7 +132,7 @@ impl RobloxRequireMode {
             let mut path_components = relative_require_path.components().take(take_components);
 
             if let Some(first_component) = path_components.next() {
-                let source_is_module_folder_name = current.is_module_folder_name(source_path);
+                let source_is_module_folder_name = current.is_module_folder_name(&source_path);
 
                 let instance_path = path_components.try_fold(
                     match first_component {
diff --git a/src/rules/convert_require/rojo_sourcemap.rs b/src/rules/convert_require/rojo_sourcemap.rs
index 4e2f7cc..3fda9f9 100644
--- a/src/rules/convert_require/rojo_sourcemap.rs
+++ b/src/rules/convert_require/rojo_sourcemap.rs
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
 
 use serde::{Deserialize, Serialize};
 
-use crate::DarkluaError;
+use crate::{utils, DarkluaError};
 
 use super::InstancePath;
 
@@ -24,12 +24,15 @@ struct RojoSourcemapNode {
 }
 
 impl RojoSourcemapNode {
-    fn assign_ids(mut self) -> Self {
+    fn initialize(mut self, relative_to: &Path) -> Self {
         let mut queue = vec![&mut self];
         let mut index = 0;
 
         while let Some(node) = queue.pop() {
             node.id = index;
+            for file_path in &mut node.file_paths {
+                *file_path = utils::normalize_path(relative_to.join(&file_path));
+            }
             for child in &mut node.children {
                 child.parent_id = index;
                 queue.push(child);
@@ -99,8 +102,12 @@ pub(crate) struct RojoSourcemap {
 }
 
 impl RojoSourcemap {
-    pub(crate) fn parse(content: &str) -> Result<Self, DarkluaError> {
-        let root_node = serde_json::from_str::<RojoSourcemapNode>(content)?.assign_ids();
+    pub(crate) fn parse(
+        content: &str,
+        relative_to: impl AsRef<Path>,
+    ) -> Result<Self, DarkluaError> {
+        let root_node =
+            serde_json::from_str::<RojoSourcemapNode>(content)?.initialize(relative_to.as_ref());
 
         let is_datamodel = root_node.class_name == "DataModel";
         Ok(Self {
@@ -220,7 +227,7 @@ mod test {
     use super::*;
 
     fn new_sourcemap(content: &str) -> RojoSourcemap {
-        RojoSourcemap::parse(content).expect("unable to parse sourcemap")
+        RojoSourcemap::parse(content, "").expect("unable to parse sourcemap")
     }
 
     mod instance_paths {


Test patch:
diff --git a/tests/rule_tests/convert_require.rs b/tests/rule_tests/convert_require.rs
index 02f9402..d904a10 100644
--- a/tests/rule_tests/convert_require.rs
+++ b/tests/rule_tests/convert_require.rs
@@ -269,6 +269,35 @@ mod sourcemap {
         );
     }
 
+    #[test]
+    fn convert_sibling_module_from_init_module_in_nested_sourcemap() {
+        let resources = memory_resources!(
+            "parent/src/d/init.lua" => include_str!("../test_cases/sourcemap/src/d/init.lua"),
+            "parent/src/d/d1.lua" => include_str!("../test_cases/sourcemap/src/d/d1.lua"),
+            "parent/src/d/d2.lua" => include_str!("../test_cases/sourcemap/src/d/d2.lua"),
+
+            ".darklua.json" => r#"{
+                generator: 'retain_lines',
+                rules: [
+                    {
+                        rule: 'convert_require',
+                        current: 'path',
+                        target: {
+                            name: 'roblox',
+                            rojo_sourcemap: './parent/sourcemap.json',
+                        }
+                    }
+                ]
+            }"#,
+            "parent/sourcemap.json" => include_str!("../test_cases/sourcemap/sourcemap.json"),
+        );
+        snapshot_file_process(
+            &resources,
+            "parent/src/d/init.lua",
+            "convert_sibling_module_from_init_module",
+        );
+    }
+
     #[test]
     fn in_datamodel_convert_sibling_module_from_init_module() {
         snapshot_file_process(