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