Skip to main content

chaste_types/
module_path.rs

1// SPDX-FileCopyrightText: 2024 The Chaste Authors
2// SPDX-License-Identifier: Apache-2.0 OR BSD-2-Clause
3
4use std::sync::LazyLock;
5
6use crate::error::{Error, Result};
7use crate::name::{package_name, PackageName, PackageNameBorrowed, PackageNamePositions};
8
9pub static ROOT_MODULE_PATH: LazyLock<ModulePath> =
10    LazyLock::new(|| ModulePath::new("".to_string()).unwrap());
11
12#[derive(Debug, PartialEq, Eq, Clone)]
13enum ModulePathSegmentInternal {
14    Arbitrary(usize),
15    NodeModules(usize),
16    PackageName(usize, PackageNamePositions),
17}
18
19impl ModulePathSegmentInternal {
20    fn end_idx(&self) -> usize {
21        match self {
22            Self::Arbitrary(i) => *i,
23            Self::NodeModules(i) => *i,
24            Self::PackageName(i, _) => *i,
25        }
26    }
27}
28
29#[derive(Debug, Clone)]
30pub struct ModulePath {
31    inner: String,
32    segments: Vec<ModulePathSegmentInternal>,
33}
34
35impl ModulePath {
36    pub fn new(value: String) -> Result<Self> {
37        let mut segments = Vec::new();
38        let mut end_idx = 0usize;
39        let mut inside_node_modules = false;
40        let mut expecting_package_name_now = false;
41        let mut inside_scoped = false;
42        for (i, segment) in value.split("/").enumerate() {
43            end_idx += segment.len();
44            if i != 0 {
45                end_idx += 1;
46            }
47            debug_assert!(!expecting_package_name_now || inside_node_modules);
48            match segment {
49                "" => {
50                    // Empty value is a special case for root path
51                    if value.is_empty() {
52                        break;
53                    }
54                    return Err(Error::InvalidModulePath(value.to_string()));
55                }
56                "node_modules" => {
57                    if expecting_package_name_now {
58                        return Err(Error::InvalidModulePath(value.to_string()));
59                    }
60                    inside_node_modules = true;
61                    expecting_package_name_now = true;
62                    segments.push(ModulePathSegmentInternal::NodeModules(end_idx));
63                }
64                _ if !inside_node_modules => {
65                    segments.push(ModulePathSegmentInternal::Arbitrary(end_idx));
66                }
67                seg if expecting_package_name_now && !inside_scoped && seg.starts_with("@") => {
68                    inside_scoped = true;
69                }
70                _ if expecting_package_name_now => {
71                    let last_seg = segments.last().unwrap();
72                    let start_idx = last_seg.end_idx() + 1;
73                    let pn_str = &value[start_idx..end_idx];
74                    let (remaining, pn_positions) = package_name(pn_str)
75                        .map_err(|_| Error::InvalidPackageName(pn_str.to_string()))?;
76                    if !remaining.is_empty() {
77                        return Err(Error::InvalidPackageName(pn_str.to_string()));
78                    }
79                    segments.push(ModulePathSegmentInternal::PackageName(
80                        end_idx,
81                        pn_positions,
82                    ));
83
84                    inside_scoped = false;
85                    expecting_package_name_now = false;
86                }
87                _ => return Err(Error::InvalidModulePath(value.to_string())),
88            }
89        }
90        if inside_scoped
91            || segments
92                .last()
93                .is_some_and(|s| matches!(s, ModulePathSegmentInternal::NodeModules(_)))
94        {
95            return Err(Error::InvalidModulePath(value.to_string()));
96        }
97
98        debug_assert_eq!(
99            segments.last().map(|s| s.end_idx()).unwrap_or(0),
100            value.len()
101        );
102        debug_assert!({
103            let mut segs = segments.iter().peekable();
104            while let Some(ModulePathSegmentInternal::Arbitrary(..)) = segs.peek() {
105                debug_assert!(matches!(
106                    segs.next(),
107                    Some(ModulePathSegmentInternal::Arbitrary(..))
108                ));
109            }
110            while let Some(seg) = segs.next() {
111                debug_assert!(matches!(seg, ModulePathSegmentInternal::NodeModules(..)));
112                debug_assert!(matches!(
113                    segs.next(),
114                    Some(ModulePathSegmentInternal::PackageName(..))
115                ));
116            }
117            segs.next().is_none()
118        });
119
120        Ok(Self {
121            inner: value,
122            segments,
123        })
124    }
125
126    pub fn implied_package_name(&self) -> Option<PackageName> {
127        let iter = self.iter();
128        match iter.last() {
129            Some(ModulePathSegment::PackageName(pn)) => Some(pn.to_owned()),
130            Some(ModulePathSegment::Arbitrary(a)) => match self.segments.len() {
131                1 => PackageName::new(a.to_string()).ok(),
132                2 if self.inner.starts_with("@") => PackageName::new(self.inner.clone()).ok(),
133                0 => unreachable!(),
134                len => {
135                    let scope_start = self.segments.get(len - 2).unwrap().end_idx() + 1;
136                    PackageName::new(self.inner[scope_start..].to_string()).ok()
137                }
138            },
139            Some(ModulePathSegment::NodeModules(_)) => unreachable!(),
140            None => None,
141        }
142    }
143}
144
145impl AsRef<str> for ModulePath {
146    fn as_ref(&self) -> &str {
147        &self.inner
148    }
149}
150
151#[derive(Debug, PartialEq, Eq, Clone)]
152pub enum ModulePathSegment<'a> {
153    Arbitrary(&'a str),
154    NodeModules(&'a str),
155    PackageName(PackageNameBorrowed<'a>),
156}
157impl AsRef<str> for ModulePathSegment<'_> {
158    fn as_ref(&self) -> &str {
159        match self {
160            ModulePathSegment::Arbitrary(i) => i,
161            ModulePathSegment::NodeModules(i) => i,
162            ModulePathSegment::PackageName(package_name_borrowed) => package_name_borrowed.as_ref(),
163        }
164    }
165}
166impl PartialOrd for ModulePathSegment<'_> {
167    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
168        Some(self.cmp(other))
169    }
170}
171impl Ord for ModulePathSegment<'_> {
172    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
173        self.as_ref().cmp(other.as_ref())
174    }
175}
176
177pub struct ModulePathIter<'a> {
178    inner: &'a ModulePath,
179    idx: usize,
180}
181
182impl<'a> Iterator for ModulePathIter<'a> {
183    type Item = ModulePathSegment<'a>;
184
185    fn next(&mut self) -> Option<Self::Item> {
186        let seg_idx = self.idx;
187        self.idx += 1;
188        let seg_int = self.inner.segments.get(seg_idx)?;
189        let start_idx = if seg_idx == 0 {
190            0
191        } else {
192            self.inner
193                .segments
194                .get(seg_idx - 1)
195                .map(|s| s.end_idx() + 1)
196                .unwrap()
197        };
198        let slic = &self.inner.inner[start_idx..seg_int.end_idx()];
199        Some(match seg_int {
200            ModulePathSegmentInternal::Arbitrary(_) => ModulePathSegment::Arbitrary(slic),
201            ModulePathSegmentInternal::NodeModules(_) => ModulePathSegment::NodeModules(slic),
202            ModulePathSegmentInternal::PackageName(_, package_name_positions) => {
203                ModulePathSegment::PackageName(PackageNameBorrowed {
204                    inner: slic,
205                    positions: package_name_positions,
206                })
207            }
208        })
209    }
210}
211
212impl ModulePath {
213    pub fn iter<'a>(&'a self) -> ModulePathIter<'a> {
214        ModulePathIter {
215            inner: self,
216            idx: 0,
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use std::sync::LazyLock;
224
225    use crate::error::Result;
226    use crate::name::PackageName;
227
228    use super::{ModulePath, ModulePathSegment};
229
230    static SEMVER_PN: LazyLock<PackageName> =
231        LazyLock::new(|| PackageName::new("semver".to_string()).unwrap());
232    static TESTCASE_PN: LazyLock<PackageName> =
233        LazyLock::new(|| PackageName::new("@chastelock/testcase".to_string()).unwrap());
234
235    #[test]
236    fn basic_mod() -> Result<()> {
237        let path = ModulePath::new("node_modules/semver".to_string())?;
238        let mut segments = path.iter();
239        assert_eq!(
240            segments.next(),
241            Some(ModulePathSegment::NodeModules("node_modules"))
242        );
243        assert_eq!(
244            segments.next(),
245            Some(ModulePathSegment::PackageName(SEMVER_PN.as_borrowed()))
246        );
247        assert_eq!(segments.next(), None);
248
249        Ok(())
250    }
251
252    #[test]
253    fn basic_mod_scoped() -> Result<()> {
254        let path = ModulePath::new("node_modules/@chastelock/testcase".to_string())?;
255        let mut segments = path.iter();
256        assert_eq!(
257            segments.next(),
258            Some(ModulePathSegment::NodeModules("node_modules"))
259        );
260        assert_eq!(
261            segments.next(),
262            Some(ModulePathSegment::PackageName(TESTCASE_PN.as_borrowed()))
263        );
264        assert_eq!(segments.next(), None);
265
266        Ok(())
267    }
268
269    #[test]
270    fn empty() -> Result<()> {
271        let path = ModulePath::new("".to_string())?;
272        let mut segments = path.iter();
273        assert_eq!(segments.next(), None);
274
275        Ok(())
276    }
277
278    #[test]
279    fn mod_inside_workspace_member() -> Result<()> {
280        let path =
281            ModulePath::new("arbitrary/prefix/node_modules/@chastelock/testcase".to_string())?;
282        let mut segments = path.iter();
283        assert_eq!(
284            segments.next(),
285            Some(ModulePathSegment::Arbitrary("arbitrary"))
286        );
287        assert_eq!(
288            segments.next(),
289            Some(ModulePathSegment::Arbitrary("prefix"))
290        );
291        assert_eq!(
292            segments.next(),
293            Some(ModulePathSegment::NodeModules("node_modules"))
294        );
295        assert_eq!(
296            segments.next(),
297            Some(ModulePathSegment::PackageName(TESTCASE_PN.as_borrowed()))
298        );
299        assert_eq!(segments.next(), None);
300
301        Ok(())
302    }
303
304    #[test]
305    fn nested_modules() -> Result<()> {
306        let path =
307            ModulePath::new("node_modules/@chastelock/testcase/node_modules/semver".to_string())?;
308        let mut segments = path.iter();
309        assert_eq!(
310            segments.next(),
311            Some(ModulePathSegment::NodeModules("node_modules"))
312        );
313        assert_eq!(
314            segments.next(),
315            Some(ModulePathSegment::PackageName(TESTCASE_PN.as_borrowed()))
316        );
317        assert_eq!(
318            segments.next(),
319            Some(ModulePathSegment::NodeModules("node_modules"))
320        );
321        assert_eq!(
322            segments.next(),
323            Some(ModulePathSegment::PackageName(SEMVER_PN.as_borrowed()))
324        );
325        assert_eq!(segments.next(), None);
326
327        Ok(())
328    }
329
330    #[test]
331    fn bs_paths() -> Result<()> {
332        fn invalid(input: &str) {
333            assert!(ModulePath::new(input.to_string()).is_err());
334        }
335        invalid("/");
336        invalid("a/");
337        invalid("node_modules");
338        invalid("node_modules/@chastelock/testcase/something/deeper");
339        invalid("node_modules/@chastelock");
340        invalid("node_modules/node_modules/n");
341        Ok(())
342    }
343}