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 mod proptests {
82 use super::*;
83 use proptest::prelude::*;
84
85 proptest! {
86 #[test]
88 fn relative_paths_are_not_bare(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
89 let dot = format!(".{suffix}");
90 let slash = format!("/{suffix}");
91 prop_assert!(!is_bare_specifier(&dot), "'.{suffix}' was classified as bare");
92 prop_assert!(!is_bare_specifier(&slash), "'/{suffix}' was classified as bare");
93 }
94
95 #[test]
97 fn scoped_package_name_has_two_segments(
98 scope in "[a-z][a-z0-9-]{0,20}",
99 pkg in "[a-z][a-z0-9-]{0,20}",
100 subpath in "(/[a-z0-9-]{1,20}){0,3}",
101 ) {
102 let specifier = format!("@{scope}/{pkg}{subpath}");
103 let extracted = extract_package_name(&specifier);
104 let expected = format!("@{scope}/{pkg}");
105 prop_assert_eq!(extracted, expected);
106 }
107
108 #[test]
110 fn unscoped_package_name_is_first_segment(
111 pkg in "[a-z][a-z0-9-]{0,30}",
112 subpath in "(/[a-z0-9-]{1,20}){0,3}",
113 ) {
114 let specifier = format!("{pkg}{subpath}");
115 let extracted = extract_package_name(&specifier);
116 prop_assert_eq!(extracted, pkg);
117 }
118
119 #[test]
121 fn bare_specifier_and_path_alias_no_panic(s in "[a-zA-Z0-9@#~/._-]{1,100}") {
122 let _ = is_bare_specifier(&s);
123 let _ = is_path_alias(&s);
124 }
125
126 #[test]
128 fn at_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
129 let specifier = format!("@/{suffix}");
130 prop_assert!(is_path_alias(&specifier));
131 }
132
133 #[test]
135 fn tilde_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
136 let specifier = format!("~/{suffix}");
137 prop_assert!(is_path_alias(&specifier));
138 }
139
140 #[test]
142 fn hash_prefix_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
143 let specifier = format!("#{suffix}");
144 prop_assert!(is_path_alias(&specifier));
145 }
146
147 #[test]
149 fn node_modules_package_name_never_empty(
150 pkg in "[a-z][a-z0-9-]{0,20}",
151 file in "[a-z]{1,10}\\.(js|ts|mjs)",
152 ) {
153 let path = std::path::PathBuf::from(format!("/project/node_modules/{pkg}/{file}"));
154 if let Some(name) = crate::resolve::fallbacks::extract_package_name_from_node_modules_path(&path) {
155 prop_assert!(!name.is_empty());
156 }
157 }
158 }
159 }
160}