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