Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/oxc_resolver-11.19.1/src/specifier.rs
Line
Count
Source
1
use std::borrow::Cow;
2
3
use crate::error::SpecifierError;
4
5
#[derive(Debug)]
6
pub struct Specifier<'a> {
7
    path: Cow<'a, str>,
8
    pub query: Option<&'a str>,
9
    pub fragment: Option<&'a str>,
10
}
11
12
impl<'a> Specifier<'a> {
13
0
    pub fn path(&'a self) -> &'a str {
14
0
        self.path.as_ref()
15
0
    }
16
17
0
    pub fn parse(specifier: &'a str) -> Result<Self, SpecifierError> {
18
0
        if specifier.is_empty() {
19
            #[cold]
20
0
            fn empty_specifier_error(specifier: &str) -> SpecifierError {
21
0
                SpecifierError::Empty(specifier.to_string())
22
0
            }
23
0
            return Err(empty_specifier_error(specifier));
24
0
        }
25
0
        let offset = match specifier.as_bytes()[0] {
26
0
            b'/' | b'.' | b'#' => 1,
27
0
            _ => 0,
28
        };
29
0
        let (path, query, fragment) = Self::parse_query_fragment(specifier, offset);
30
0
        if path.is_empty() {
31
            #[cold]
32
0
            fn empty_path_error(specifier: &str) -> SpecifierError {
33
0
                SpecifierError::Empty(specifier.to_string())
34
0
            }
35
0
            return Err(empty_path_error(specifier));
36
0
        }
37
0
        Ok(Self { path, query, fragment })
38
0
    }
39
40
0
    fn parse_query_fragment(
41
0
        specifier: &'a str,
42
0
        skip: usize,
43
0
    ) -> (Cow<'a, str>, Option<&'a str>, Option<&'a str>) {
44
0
        let mut query_start: Option<usize> = None;
45
0
        let mut fragment_start: Option<usize> = None;
46
47
0
        let mut prev = specifier.as_bytes()[0];
48
        // Optimize for the common case: most specifiers don't have escaped characters
49
0
        let mut escaped_indexes: Option<Vec<usize>> = None;
50
0
        for (i, &b) in specifier.as_bytes().iter().enumerate().skip(skip) {
51
0
            if b == b'?' && query_start.is_none() {
52
0
                query_start = Some(i);
53
0
            }
54
0
            if b == b'#' {
55
0
                if prev == b'\0' {
56
0
                    escaped_indexes.get_or_insert_with(Vec::new).push(i - 1);
57
0
                } else {
58
0
                    fragment_start = Some(i);
59
0
                    break;
60
                }
61
0
            }
62
0
            prev = b;
63
        }
64
65
0
        let (path, query, fragment) = match (query_start, fragment_start) {
66
0
            (Some(i), Some(j)) => {
67
0
                debug_assert!(i < j);
68
0
                (&specifier[..i], Some(&specifier[i..j]), Some(&specifier[j..]))
69
            }
70
0
            (Some(i), None) => (&specifier[..i], Some(&specifier[i..]), None),
71
0
            (None, Some(j)) => (&specifier[..j], None, Some(&specifier[j..])),
72
0
            _ => (specifier, None, None),
73
        };
74
75
0
        let path = escaped_indexes.map_or(Cow::Borrowed(path), |escaped_indexes| {
76
0
            let mut s = String::with_capacity(path.len());
77
0
            for (i, &b) in path.as_bytes().iter().enumerate() {
78
0
                if !escaped_indexes.contains(&i) {
79
0
                    s.push(b as char);
80
0
                }
81
            }
82
0
            Cow::Owned(s)
83
0
        });
84
85
0
        (path, query, fragment)
86
0
    }
87
}
88
89
#[cfg(test)]
90
mod tests {
91
    use super::{Specifier, SpecifierError};
92
93
    #[test]
94
    fn debug() {
95
        let specifier = Specifier::parse("/").unwrap();
96
        assert_eq!(
97
            format!("{specifier:?}"),
98
            r#"Specifier { path: "/", query: None, fragment: None }"#
99
        );
100
    }
101
102
    #[test]
103
    fn empty() {
104
        let specifiers = ["", "?"];
105
        for specifier in specifiers {
106
            let error = Specifier::parse(specifier).unwrap_err();
107
            assert_eq!(error, SpecifierError::Empty(specifier.to_string()));
108
        }
109
    }
110
111
    #[test]
112
    fn absolute() -> Result<(), SpecifierError> {
113
        let specifier = "/test?#";
114
        let parsed = Specifier::parse(specifier)?;
115
        assert_eq!(parsed.path, "/test");
116
        assert_eq!(parsed.query, Some("?"));
117
        assert_eq!(parsed.fragment, Some("#"));
118
        Ok(())
119
    }
120
121
    #[test]
122
    fn relative() -> Result<(), SpecifierError> {
123
        let specifiers = ["./test", "../test", "../../test"];
124
        for specifier in specifiers {
125
            let mut r = specifier.to_string();
126
            r.push_str("?#");
127
            let parsed = Specifier::parse(&r)?;
128
            assert_eq!(parsed.path, specifier);
129
            assert_eq!(parsed.query, Some("?"));
130
            assert_eq!(parsed.fragment, Some("#"));
131
        }
132
        Ok(())
133
    }
134
135
    #[test]
136
    fn hash() -> Result<(), SpecifierError> {
137
        let specifiers = ["#", "#path"];
138
        for specifier in specifiers {
139
            let mut r = specifier.to_string();
140
            r.push_str("?#");
141
            let parsed = Specifier::parse(&r)?;
142
            assert_eq!(parsed.path, specifier);
143
            assert_eq!(parsed.query, Some("?"));
144
            assert_eq!(parsed.fragment, Some("#"));
145
        }
146
        Ok(())
147
    }
148
149
    #[test]
150
    fn module() -> Result<(), SpecifierError> {
151
        let specifiers = ["module"];
152
        for specifier in specifiers {
153
            let mut r = specifier.to_string();
154
            r.push_str("?#");
155
            let parsed = Specifier::parse(&r)?;
156
            assert_eq!(parsed.path, specifier);
157
            assert_eq!(parsed.query, Some("?"));
158
            assert_eq!(parsed.fragment, Some("#"));
159
        }
160
        Ok(())
161
    }
162
163
    #[test]
164
    fn query_fragment() -> Result<(), SpecifierError> {
165
        let data = [
166
            ("a?", Some("?"), None),
167
            ("a?query", Some("?query"), None),
168
            ("a?query1?query2", Some("?query1?query2"), None),
169
            ("a?query1?query2?query3", Some("?query1?query2?query3"), None),
170
            ("a#", None, Some("#")),
171
            ("a#b#c", None, Some("#b#c")),
172
            ("a#fragment", None, Some("#fragment")),
173
            ("a?#", Some("?"), Some("#")),
174
            ("a?#fragment", Some("?"), Some("#fragment")),
175
            ("a?query#", Some("?query"), Some("#")),
176
            ("a?query#fragment", Some("?query"), Some("#fragment")),
177
            ("a#fragment?", None, Some("#fragment?")),
178
            ("a#fragment?query", None, Some("#fragment?query")),
179
        ];
180
181
        for (specifier_str, query, fragment) in data {
182
            let specifier = Specifier::parse(specifier_str)?;
183
            assert_eq!(specifier.path, "a", "{specifier_str}");
184
            assert_eq!(specifier.query, query, "{specifier_str}");
185
            assert_eq!(specifier.fragment, fragment, "{specifier_str}");
186
        }
187
188
        Ok(())
189
    }
190
191
    #[test]
192
    // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js
193
    fn enhanced_resolve_edge_cases() -> Result<(), SpecifierError> {
194
        let data = [
195
            ("path/#", "path/", "", "#"),
196
            ("path/as/?", "path/as/", "?", ""),
197
            ("path/#/?", "path/", "", "#/?"),
198
            ("path/#repo#hash", "path/", "", "#repo#hash"),
199
            ("path/#r#hash", "path/", "", "#r#hash"),
200
            ("path/#repo/#repo2#hash", "path/", "", "#repo/#repo2#hash"),
201
            ("path/#r/#r#hash", "path/", "", "#r/#r#hash"),
202
            ("path/#/not/a/hash?not-a-query", "path/", "", "#/not/a/hash?not-a-query"),
203
        ];
204
205
        for (specifier_str, path, query, fragment) in data {
206
            let specifier = Specifier::parse(specifier_str)?;
207
            assert_eq!(specifier.path, path, "{specifier_str}");
208
            assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}");
209
            assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}");
210
        }
211
212
        Ok(())
213
    }
214
215
    // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js
216
    #[test]
217
    fn enhanced_resolve_windows_like() -> Result<(), SpecifierError> {
218
        let data = [
219
            ("path\\#", "path\\", "", "#"),
220
            ("path\\as\\?", "path\\as\\", "?", ""),
221
            ("path\\#\\?", "path\\", "", "#\\?"),
222
            ("path\\#repo#hash", "path\\", "", "#repo#hash"),
223
            ("path\\#r#hash", "path\\", "", "#r#hash"),
224
            ("path\\#repo\\#repo2#hash", "path\\", "", "#repo\\#repo2#hash"),
225
            ("path\\#r\\#r#hash", "path\\", "", "#r\\#r#hash"),
226
            ("path\\#/not/a/hash?not-a-query", "path\\", "", "#/not/a/hash?not-a-query"),
227
        ];
228
229
        for (specifier_str, path, query, fragment) in data {
230
            let specifier = Specifier::parse(specifier_str)?;
231
            assert_eq!(specifier.path, path, "{specifier_str}");
232
            assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}");
233
            assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}");
234
        }
235
236
        Ok(())
237
    }
238
}