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::{
7        convert_require::rojo_sourcemap::RojoSourcemap,
8        require::path_utils::{get_relative_parent_path, get_relative_path},
9        Context,
10    },
11    utils, DarkluaError,
12};
13
14use std::path::{Component, Path, PathBuf};
15
16use super::{
17    instance_path::{get_parent_instance, script_identifier},
18    RequireMode, RobloxIndexStyle,
19};
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(deny_unknown_fields, rename_all = "snake_case")]
23pub struct RobloxRequireMode {
24    rojo_sourcemap: Option<PathBuf>,
25    #[serde(default, deserialize_with = "crate::utils::string_or_struct")]
26    indexing_style: RobloxIndexStyle,
27    #[serde(skip)]
28    cached_sourcemap: Option<RojoSourcemap>,
29}
30
31impl RobloxRequireMode {
32    pub(crate) fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
33        if let Some(ref rojo_sourcemap_path) = self
34            .rojo_sourcemap
35            .as_ref()
36            .map(|rojo_sourcemap_path| context.project_location().join(rojo_sourcemap_path))
37        {
38            context.add_file_dependency(rojo_sourcemap_path.clone());
39
40            let sourcemap_parent_location = get_relative_parent_path(rojo_sourcemap_path);
41            let sourcemap = RojoSourcemap::parse(
42                &context
43                    .resources()
44                    .get(rojo_sourcemap_path)
45                    .map_err(|err| {
46                        DarkluaError::from(err).context("while initializing Roblox require mode")
47                    })?,
48                sourcemap_parent_location,
49            )
50            .map_err(|err| {
51                err.context(format!(
52                    "unable to parse Rojo sourcemap at `{}`",
53                    rojo_sourcemap_path.display()
54                ))
55            })?;
56            self.cached_sourcemap = Some(sourcemap);
57        }
58        Ok(())
59    }
60
61    pub(crate) fn find_require(
62        &self,
63        _call: &FunctionCall,
64        _context: &Context,
65    ) -> DarkluaResult<Option<PathBuf>> {
66        Err(DarkluaError::custom("unsupported initial require mode")
67            .context("Roblox require mode cannot be used as the current require mode"))
68    }
69
70    pub(crate) fn generate_require(
71        &self,
72        require_path: &Path,
73        current: &RequireMode,
74        context: &Context,
75    ) -> DarkluaResult<Option<Arguments>> {
76        let source_path = utils::normalize_path(context.current_path());
77        log::trace!(
78            "generate Roblox require for `{}` from `{}`",
79            require_path.display(),
80            source_path.display(),
81        );
82
83        if let Some((sourcemap, sourcemap_path)) = self
84            .cached_sourcemap
85            .as_ref()
86            .zip(self.rojo_sourcemap.as_ref())
87        {
88            if let Some(require_relative_to_sourcemap) = get_relative_path(
89                require_path,
90                get_relative_parent_path(sourcemap_path),
91                false,
92            )? {
93                log::trace!(
94                    "  ⨽ use sourcemap at `{}` to find `{}`",
95                    sourcemap_path.display(),
96                    require_relative_to_sourcemap.display()
97                );
98
99                if let Some(instance_path) =
100                    sourcemap.get_instance_path(&source_path, &require_relative_to_sourcemap)
101                {
102                    Ok(Some(Arguments::default().with_argument(
103                        instance_path.convert(&self.indexing_style),
104                    )))
105                } else {
106                    match (
107                        sourcemap.exists(&source_path),
108                        sourcemap.exists(&require_relative_to_sourcemap),
109                    ) {
110                        (true, true) => {
111                            log::warn!(
112                                "unable to get relative path to `{}` in sourcemap (from `{}`)",
113                                require_relative_to_sourcemap.display(),
114                                source_path.display()
115                            );
116                        }
117                        (false, _) => {
118                            log::warn!(
119                                "unable to find source path `{}` in sourcemap",
120                                source_path.display()
121                            );
122                        }
123                        (true, false) => {
124                            log::warn!(
125                                "unable to find path `{}` in sourcemap (from `{}`)",
126                                require_relative_to_sourcemap.display(),
127                                source_path.display()
128                            );
129                        }
130                    }
131                    Ok(None)
132                }
133            } else {
134                log::debug!(
135                    "unable to get relative path from sourcemap for `{}`",
136                    require_path.display()
137                );
138                Ok(None)
139            }
140        } else if let Some(relative_require_path) =
141            get_relative_path(require_path, &source_path, true)?
142        {
143            log::trace!(
144                "make require path relative to source: `{}`",
145                relative_require_path.display()
146            );
147
148            let require_is_module_folder_name =
149                current.is_module_folder_name(&relative_require_path);
150            // if we are about to make a require to a path like `./x/y/z/init.lua`
151            // we can pop the last component from the path
152            let take_components = relative_require_path
153                .components()
154                .count()
155                .saturating_sub(if require_is_module_folder_name { 1 } else { 0 });
156            let mut path_components = relative_require_path.components().take(take_components);
157
158            if let Some(first_component) = path_components.next() {
159                let source_is_module_folder_name = current.is_module_folder_name(&source_path);
160
161                let instance_path = path_components.try_fold(
162                    match first_component {
163                        Component::CurDir => {
164                            if source_is_module_folder_name {
165                                script_identifier().into()
166                            } else {
167                                get_parent_instance(script_identifier())
168                            }
169                        }
170                        Component::ParentDir => {
171                            if source_is_module_folder_name {
172                                get_parent_instance(script_identifier())
173                            } else {
174                                get_parent_instance(get_parent_instance(script_identifier()))
175                            }
176                        }
177                        Component::Normal(_) => {
178                            return Err(DarkluaError::custom(format!(
179                                concat!(
180                                    "unable to convert path `{}`: the require path should be ",
181                                    "relative and start with `.` or `..` (got `{}`)"
182                                ),
183                                require_path.display(),
184                                relative_require_path.display(),
185                            )))
186                        }
187                        Component::Prefix(_) | Component::RootDir => {
188                            return Err(DarkluaError::custom(format!(
189                                concat!(
190                                    "unable to convert absolute path `{}`: ",
191                                    "without a provided Rojo sourcemap, ",
192                                    "darklua can only convert relative paths ",
193                                    "(starting with `.` or `..`)"
194                                ),
195                                require_path.display(),
196                            )))
197                        }
198                    },
199                    |instance: Prefix, component| match component {
200                        Component::CurDir => Ok(instance),
201                        Component::ParentDir => Ok(get_parent_instance(instance)),
202                        Component::Normal(name) => utils::convert_os_string(name)
203                            .map(|child_name| self.indexing_style.index(instance, child_name)),
204                        Component::Prefix(_) | Component::RootDir => {
205                            Err(DarkluaError::custom(format!(
206                                "unable to convert path `{}`: unexpected component in relative path `{}`",
207                                require_path.display(),
208                                relative_require_path.display(),
209                            )))
210                        },
211                    },
212                )?;
213
214                Ok(Some(Arguments::default().with_argument(instance_path)))
215            } else {
216                Err(DarkluaError::custom(format!(
217                    "unable to convert path `{}` from `{}` without a sourcemap: the relative path is empty `{}`",
218                    require_path.display(),
219                    source_path.display(),
220                    relative_require_path.display(),
221                )))
222            }
223        } else {
224            Err(DarkluaError::custom(format!(
225                concat!(
226                    "unable to convert path `{}` from `{}` without a sourcemap: unable to ",
227                    "make the require path relative to the source file"
228                ),
229                require_path.display(),
230                source_path.display(),
231            )))
232        }
233    }
234}