Skip to main content

perl_lsp_uri/
lib.rs

1#![warn(missing_docs)]
2//! Typed URI parsing helpers for LSP components.
3
4use lsp_types::Uri;
5
6fn fallback_uri() -> Uri {
7    for candidate in ["file:///unknown", "file:///", "about:blank", "urn:perl-lsp:unknown"] {
8        if let Ok(uri) = candidate.parse::<Uri>() {
9            return uri;
10        }
11    }
12
13    // Last-resort fallback that avoids panicking if URI parser behavior changes unexpectedly.
14    let mut suffix = 0usize;
15    loop {
16        let candidate = format!("http://localhost/{suffix}");
17        if let Ok(uri) = candidate.parse::<Uri>() {
18            return uri;
19        }
20        suffix = suffix.saturating_add(1);
21    }
22}
23
24/// Parse a URI string into [`lsp_types::Uri`].
25///
26/// Falls back to a guaranteed-valid URI if parsing fails.
27#[must_use]
28pub fn parse_uri(s: &str) -> Uri {
29    match s.parse::<Uri>() {
30        Ok(uri) => uri,
31        Err(_) => fallback_uri(),
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::{fallback_uri, parse_uri};
38
39    // ── File URI parsing ──────────────────────────────────────────
40
41    #[test]
42    fn parse_uri_returns_original_for_valid_uri() {
43        let uri = parse_uri("file:///tmp/test.pl");
44        assert_eq!(uri.as_str(), "file:///tmp/test.pl");
45    }
46
47    #[test]
48    fn parse_uri_unix_absolute_path() {
49        let uri = parse_uri("file:///home/user/project/lib/Module.pm");
50        assert_eq!(uri.as_str(), "file:///home/user/project/lib/Module.pm");
51    }
52
53    #[test]
54    fn parse_uri_deeply_nested_path() {
55        let uri = parse_uri("file:///a/b/c/d/e/f/g.pl");
56        assert_eq!(uri.as_str(), "file:///a/b/c/d/e/f/g.pl");
57    }
58
59    #[test]
60    fn parse_uri_file_root() {
61        let uri = parse_uri("file:///");
62        assert_eq!(uri.as_str(), "file:///");
63    }
64
65    // ── Windows paths ─────────────────────────────────────────────
66
67    #[test]
68    fn parse_uri_windows_drive_path() {
69        let uri = parse_uri("file:///C:/Users/dev/project/file.pm");
70        assert_eq!(uri.as_str(), "file:///C:/Users/dev/project/file.pm");
71    }
72
73    #[test]
74    fn parse_uri_windows_lowercase_drive() {
75        let uri = parse_uri("file:///c:/perl/lib/Module.pm");
76        assert_eq!(uri.as_str(), "file:///c:/perl/lib/Module.pm");
77    }
78
79    #[test]
80    fn parse_uri_windows_drive_root() {
81        let uri = parse_uri("file:///D:/");
82        assert_eq!(uri.as_str(), "file:///D:/");
83    }
84
85    // ── Percent-encoded URIs (spaces and special chars) ───────────
86
87    #[test]
88    fn parse_uri_percent_encoded_space() {
89        let uri = parse_uri("file:///path/to/my%20module/Foo.pm");
90        assert_eq!(uri.as_str(), "file:///path/to/my%20module/Foo.pm");
91    }
92
93    #[test]
94    fn parse_uri_percent_encoded_special_chars() {
95        let uri = parse_uri("file:///tmp/%E2%9C%93check.pl");
96        assert_eq!(uri.as_str(), "file:///tmp/%E2%9C%93check.pl");
97    }
98
99    #[test]
100    fn parse_uri_percent_encoded_hash() {
101        // '#' must be encoded in paths
102        let uri = parse_uri("file:///tmp/file%23name.pl");
103        assert_eq!(uri.as_str(), "file:///tmp/file%23name.pl");
104    }
105
106    #[test]
107    fn parse_uri_percent_encoded_windows_space() {
108        let uri = parse_uri("file:///C:/My%20Documents/script.pl");
109        assert_eq!(uri.as_str(), "file:///C:/My%20Documents/script.pl");
110    }
111
112    // ── Invalid URI fallback chain ────────────────────────────────
113
114    #[test]
115    fn parse_uri_falls_back_for_invalid_uri() {
116        let uri = parse_uri("not a uri");
117        assert!(!uri.as_str().is_empty());
118    }
119
120    #[test]
121    fn parse_uri_empty_string_does_not_panic() {
122        // An empty string may parse as a valid (empty) URI depending on the
123        // URI library. The key invariant is that parse_uri never panics.
124        let _uri = parse_uri("");
125    }
126
127    #[test]
128    fn parse_uri_fallback_for_bare_path() {
129        // A bare filesystem path (no scheme) may or may not parse depending on
130        // the URI library. Either way parse_uri must not panic.
131        let uri = parse_uri("/usr/local/lib/perl5/Foo.pm");
132        assert!(!uri.as_str().is_empty());
133    }
134
135    #[test]
136    fn parse_uri_fallback_for_whitespace_only() {
137        let uri = parse_uri("   ");
138        assert!(!uri.as_str().is_empty());
139    }
140
141    #[test]
142    fn parse_uri_fallback_for_control_chars() {
143        let uri = parse_uri("\x00\x01\x02");
144        assert!(!uri.as_str().is_empty());
145    }
146
147    #[test]
148    fn parse_uri_fallback_returns_valid_uri_string() {
149        // The fallback must produce a string that starts with a known scheme
150        let uri = parse_uri("definitely not valid %%% uri");
151        let s = uri.as_str();
152        assert!(
153            s.starts_with("file:")
154                || s.starts_with("about:")
155                || s.starts_with("urn:")
156                || s.starts_with("http:"),
157            "fallback URI should have a recognized scheme, got: {s}"
158        );
159    }
160
161    #[test]
162    fn fallback_uri_returns_known_scheme() {
163        let uri = fallback_uri();
164        let s = uri.as_str();
165        assert!(
166            s.starts_with("file:")
167                || s.starts_with("about:")
168                || s.starts_with("urn:")
169                || s.starts_with("http:"),
170            "fallback_uri should produce a recognized scheme, got: {s}"
171        );
172    }
173
174    #[test]
175    fn fallback_uri_is_deterministic() {
176        let a = fallback_uri();
177        let b = fallback_uri();
178        assert_eq!(a.as_str(), b.as_str());
179    }
180
181    // ── URI scheme preservation ───────────────────────────────────
182
183    #[test]
184    fn parse_uri_preserves_https_scheme() {
185        let uri = parse_uri("https://example.com/docs/perl");
186        assert_eq!(uri.as_str(), "https://example.com/docs/perl");
187    }
188
189    #[test]
190    fn parse_uri_preserves_untitled_scheme() {
191        // VS Code uses untitled: for unsaved buffers
192        let uri = parse_uri("untitled:Untitled-1");
193        assert_eq!(uri.as_str(), "untitled:Untitled-1");
194    }
195
196    // ── Round-trip: parse then as_str ─────────────────────────────
197
198    #[test]
199    fn parse_uri_round_trip_preserves_string() {
200        let inputs = [
201            "file:///tmp/test.pl",
202            "file:///C:/Users/file.pm",
203            "file:///path/with%20space/lib.pm",
204            "https://example.com/resource",
205        ];
206        for input in inputs {
207            let uri = parse_uri(input);
208            assert_eq!(uri.as_str(), input, "round-trip failed for: {input}");
209        }
210    }
211
212    // ── Edge cases ────────────────────────────────────────────────
213
214    #[test]
215    fn parse_uri_with_query_and_fragment() {
216        let uri = parse_uri("file:///path/to/file.pm?line=10#L10");
217        assert_eq!(uri.as_str(), "file:///path/to/file.pm?line=10#L10");
218    }
219
220    #[test]
221    fn parse_uri_with_port() {
222        let uri = parse_uri("http://localhost:8080/path");
223        assert_eq!(uri.as_str(), "http://localhost:8080/path");
224    }
225
226    #[test]
227    fn parse_uri_very_long_path() {
228        let long_segment = "a".repeat(200);
229        let input = format!("file:///{long_segment}/{long_segment}.pm");
230        let uri = parse_uri(&input);
231        assert_eq!(uri.as_str(), input);
232    }
233}