darklua_core/rules/convert_require/
roblox_require_mode.rs

1use 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            // if we are about to make a require to a path like `./x/y/z/init.lua`
127            // we can pop the last component from the path
128            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}