fallow_graph/resolve/
path_info.rs1#[must_use]
10pub fn is_path_alias(specifier: &str) -> bool {
11 if specifier.starts_with('#') {
13 return true;
14 }
15 if specifier.starts_with("~/") || specifier.starts_with("~~/") {
17 return true;
18 }
19 if specifier.starts_with("@/") {
21 return true;
22 }
23 if specifier.starts_with('@') {
27 let scope = specifier.split('/').next().unwrap_or(specifier);
28 if scope.len() > 1 && scope.chars().nth(1).is_some_and(|c| c.is_ascii_uppercase()) {
29 return true;
30 }
31 }
32
33 false
34}
35
36#[must_use]
38pub fn is_bare_specifier(specifier: &str) -> bool {
39 !specifier.starts_with('.')
40 && !specifier.starts_with('/')
41 && !specifier.contains("://")
42 && !specifier.starts_with("data:")
43}
44
45#[must_use]
49pub fn extract_package_name(specifier: &str) -> String {
50 if specifier.starts_with('@') {
51 let parts: Vec<&str> = specifier.splitn(3, '/').collect();
52 if parts.len() >= 2 {
53 format!("{}/{}", parts[0], parts[1])
54 } else {
55 specifier.to_string()
56 }
57 } else {
58 specifier.split('/').next().unwrap_or(specifier).to_string()
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn test_extract_package_name() {
68 assert_eq!(extract_package_name("react"), "react");
69 assert_eq!(extract_package_name("lodash/merge"), "lodash");
70 assert_eq!(extract_package_name("@scope/pkg"), "@scope/pkg");
71 assert_eq!(extract_package_name("@scope/pkg/foo"), "@scope/pkg");
72 }
73
74 #[test]
75 fn test_is_bare_specifier() {
76 assert!(is_bare_specifier("react"));
77 assert!(is_bare_specifier("@scope/pkg"));
78 assert!(is_bare_specifier("#internal/module"));
79 assert!(!is_bare_specifier("./utils"));
80 assert!(!is_bare_specifier("../lib"));
81 assert!(!is_bare_specifier("/absolute"));
82 }
83
84 #[test]
85 fn test_is_bare_specifier_url_specifiers() {
86 assert!(!is_bare_specifier("https://cdn.example.com/lib.js"));
87 assert!(!is_bare_specifier("http://example.com/module"));
88 assert!(!is_bare_specifier("data:text/javascript,export default 42"));
89 }
90
91 #[test]
94 fn path_alias_hash_prefix() {
95 assert!(is_path_alias("#internal/module"));
96 assert!(is_path_alias("#shared"));
97 }
98
99 #[test]
100 fn path_alias_tilde_prefix() {
101 assert!(is_path_alias("~/components/Button"));
102 assert!(is_path_alias("~~/utils/helpers"));
103 }
104
105 #[test]
106 fn path_alias_at_slash_prefix() {
107 assert!(is_path_alias("@/components/Button"));
108 assert!(is_path_alias("@/lib"));
109 }
110
111 #[test]
112 fn path_alias_pascal_case_scope() {
113 assert!(is_path_alias("@Components/Button"));
115 assert!(is_path_alias("@Hooks/useApi"));
116 assert!(is_path_alias("@Services/auth"));
117 }
118
119 #[test]
120 fn path_alias_lowercase_scope_is_not_alias() {
121 assert!(!is_path_alias("@babel/core"));
123 assert!(!is_path_alias("@types/react"));
124 assert!(!is_path_alias("@scope/pkg"));
125 }
126
127 #[test]
128 fn path_alias_plain_specifier_is_not_alias() {
129 assert!(!is_path_alias("react"));
130 assert!(!is_path_alias("lodash/merge"));
131 assert!(!is_path_alias("my-utils"));
132 }
133
134 #[test]
135 fn path_alias_tilde_without_slash_is_not_alias() {
136 assert!(!is_path_alias("~something"));
138 }
139
140 #[test]
143 fn extract_package_name_bare_scope_only() {
144 assert_eq!(extract_package_name("@scope"), "@scope");
146 }
147
148 #[test]
149 fn extract_package_name_deep_subpath() {
150 assert_eq!(
151 extract_package_name("@scope/pkg/deep/nested/path"),
152 "@scope/pkg"
153 );
154 }
155
156 #[test]
157 fn extract_package_name_single_name() {
158 assert_eq!(extract_package_name("react"), "react");
159 }
160
161 mod proptests {
162 use super::*;
163 use proptest::prelude::*;
164
165 proptest! {
166 #[test]
168 fn relative_paths_are_not_bare(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
169 let dot = format!(".{suffix}");
170 let slash = format!("/{suffix}");
171 prop_assert!(!is_bare_specifier(&dot), "'.{suffix}' was classified as bare");
172 prop_assert!(!is_bare_specifier(&slash), "'/{suffix}' was classified as bare");
173 }
174
175 #[test]
177 fn scoped_package_name_has_two_segments(
178 scope in "[a-z][a-z0-9-]{0,20}",
179 pkg in "[a-z][a-z0-9-]{0,20}",
180 subpath in "(/[a-z0-9-]{1,20}){0,3}",
181 ) {
182 let specifier = format!("@{scope}/{pkg}{subpath}");
183 let extracted = extract_package_name(&specifier);
184 let expected = format!("@{scope}/{pkg}");
185 prop_assert_eq!(extracted, expected);
186 }
187
188 #[test]
190 fn unscoped_package_name_is_first_segment(
191 pkg in "[a-z][a-z0-9-]{0,30}",
192 subpath in "(/[a-z0-9-]{1,20}){0,3}",
193 ) {
194 let specifier = format!("{pkg}{subpath}");
195 let extracted = extract_package_name(&specifier);
196 prop_assert_eq!(extracted, pkg);
197 }
198
199 #[test]
201 fn bare_specifier_and_path_alias_no_panic(s in "[a-zA-Z0-9@#~/._-]{1,100}") {
202 let _ = is_bare_specifier(&s);
203 let _ = is_path_alias(&s);
204 }
205
206 #[test]
208 fn at_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
209 let specifier = format!("@/{suffix}");
210 prop_assert!(is_path_alias(&specifier));
211 }
212
213 #[test]
215 fn tilde_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
216 let specifier = format!("~/{suffix}");
217 prop_assert!(is_path_alias(&specifier));
218 }
219
220 #[test]
222 fn hash_prefix_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
223 let specifier = format!("#{suffix}");
224 prop_assert!(is_path_alias(&specifier));
225 }
226
227 #[test]
229 fn node_modules_package_name_never_empty(
230 pkg in "[a-z][a-z0-9-]{0,20}",
231 file in "[a-z]{1,10}\\.(js|ts|mjs)",
232 ) {
233 let path = std::path::PathBuf::from(format!("/project/node_modules/{pkg}/{file}"));
234 if let Some(name) = crate::resolve::fallbacks::extract_package_name_from_node_modules_path(&path) {
235 prop_assert!(!name.is_empty());
236 }
237 }
238 }
239 }
240}