deps_composer/
formatter.rs1use deps_core::lsp_helpers::EcosystemFormatter;
2
3pub struct ComposerFormatter;
10
11impl EcosystemFormatter for ComposerFormatter {
12 fn normalize_package_name(&self, name: &str) -> String {
13 name.to_lowercase()
14 }
15
16 fn format_version_for_text_edit(&self, version: &str) -> String {
17 version.to_string()
18 }
19
20 fn package_url(&self, name: &str) -> String {
21 format!("https://packagist.org/packages/{name}")
22 }
23
24 fn yanked_message(&self) -> &'static str {
25 "This package is abandoned"
26 }
27
28 fn yanked_label(&self) -> &'static str {
29 "*(abandoned)*"
30 }
31
32 fn version_satisfies_requirement(&self, version: &str, requirement: &str) -> bool {
42 let version = version.strip_prefix('v').unwrap_or(version);
43 let requirement = requirement.trim();
44
45 if requirement.is_empty() || requirement == "*" {
46 return true;
47 }
48
49 if requirement.contains("||") {
51 return requirement
52 .split("||")
53 .any(|part| self.version_satisfies_requirement(version, part.trim()));
54 }
55
56 let parts: Vec<&str> = requirement.split_whitespace().collect();
59 if parts.len() > 1
60 && parts
61 .iter()
62 .any(|p| p.starts_with('>') || p.starts_with('<'))
63 {
64 return parts
65 .iter()
66 .all(|part| self.version_satisfies_requirement(version, part));
67 }
68
69 if let Some(req) = requirement.strip_prefix('^') {
71 return satisfies_caret(version, req);
72 }
73
74 if let Some(req) = requirement.strip_prefix('~') {
76 return satisfies_tilde_composer(version, req);
77 }
78
79 if let Some(req) = requirement.strip_prefix(">=") {
81 return compare_versions(version, req.trim()) >= 0;
82 }
83 if let Some(req) = requirement.strip_prefix("<=") {
84 return compare_versions(version, req.trim()) <= 0;
85 }
86 if let Some(req) = requirement.strip_prefix('>') {
87 return compare_versions(version, req.trim()) > 0;
88 }
89 if let Some(req) = requirement.strip_prefix('<') {
90 return compare_versions(version, req.trim()) < 0;
91 }
92 if let Some(req) = requirement.strip_prefix('=') {
93 return compare_versions(version, req.trim()) == 0;
94 }
95 if let Some(req) = requirement.strip_prefix("!=") {
96 return compare_versions(version, req.trim()) != 0;
97 }
98
99 if requirement.ends_with(".*") {
101 let prefix = requirement.trim_end_matches(".*");
102 return version.starts_with(prefix) && version[prefix.len()..].starts_with('.');
103 }
104
105 let req_parts: Vec<&str> = requirement.split('.').collect();
107 let ver_parts: Vec<&str> = version.split('.').collect();
108
109 if req_parts.len() == ver_parts.len() {
110 return version == requirement;
111 }
112
113 if req_parts.len() < ver_parts.len() {
115 return ver_parts.starts_with(&req_parts);
116 }
117
118 false
119 }
120}
121
122fn satisfies_tilde_composer(version: &str, req: &str) -> bool {
128 let req_parts: Vec<&str> = req.split('.').collect();
129 let ver_parts: Vec<&str> = version.split('.').collect();
130
131 if req_parts.len() >= 3 {
132 if req_parts.first() != ver_parts.first() {
135 return false;
136 }
137 if req_parts.get(1) != ver_parts.get(1) {
138 return false;
139 }
140 let req_patch: u64 = req_parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
142 let ver_patch: u64 = ver_parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
143 ver_patch >= req_patch
144 } else if req_parts.len() == 2 {
145 let req_major: u64 = req_parts.first().and_then(|p| p.parse().ok()).unwrap_or(0);
147 let req_minor: u64 = req_parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
148 let ver_major: u64 = ver_parts.first().and_then(|p| p.parse().ok()).unwrap_or(0);
149 let ver_minor: u64 = ver_parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
150
151 if ver_major != req_major {
152 return false;
153 }
154 ver_minor >= req_minor
156 } else {
157 req_parts.first() == ver_parts.first()
159 }
160}
161
162fn satisfies_caret(version: &str, req: &str) -> bool {
164 let req_parts: Vec<&str> = req.split('.').collect();
165 let ver_parts: Vec<&str> = version.split('.').collect();
166
167 if req_parts.first() != ver_parts.first() {
168 return false;
169 }
170
171 if req_parts.first().is_some_and(|m| *m != "0") {
172 return true;
173 }
174
175 if req_parts.len() >= 2 && ver_parts.len() >= 2 {
176 return req_parts[1] == ver_parts[1];
177 }
178
179 true
180}
181
182fn compare_versions(a: &str, b: &str) -> i32 {
186 fn parse_segment(s: &str) -> u64 {
187 let digits: String = s
188 .chars()
189 .skip_while(|c| !c.is_ascii_digit())
190 .take_while(|c| c.is_ascii_digit())
191 .collect();
192 digits.parse().unwrap_or(0)
193 }
194 let a_trimmed = a.trim_start_matches(|c: char| !c.is_ascii_digit());
195 let b_trimmed = b.trim_start_matches(|c: char| !c.is_ascii_digit());
196 let a_parts: Vec<u64> = a_trimmed.split('.').map(parse_segment).collect();
197 let b_parts: Vec<u64> = b_trimmed.split('.').map(parse_segment).collect();
198
199 let len = a_parts.len().max(b_parts.len());
200 for i in 0..len {
201 let av = a_parts.get(i).copied().unwrap_or(0);
202 let bv = b_parts.get(i).copied().unwrap_or(0);
203 if av < bv {
204 return -1;
205 }
206 if av > bv {
207 return 1;
208 }
209 }
210 0
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_normalize_package_name() {
219 let f = ComposerFormatter;
220 assert_eq!(f.normalize_package_name("Vendor/Package"), "vendor/package");
221 assert_eq!(
222 f.normalize_package_name("symfony/console"),
223 "symfony/console"
224 );
225 }
226
227 #[test]
228 fn test_package_url() {
229 let f = ComposerFormatter;
230 assert_eq!(
231 f.package_url("symfony/console"),
232 "https://packagist.org/packages/symfony/console"
233 );
234 }
235
236 #[test]
237 fn test_wildcard() {
238 let f = ComposerFormatter;
239 assert!(f.version_satisfies_requirement("1.2.3", "*"));
240 assert!(f.version_satisfies_requirement("99.0.0", "*"));
241 }
242
243 #[test]
244 fn test_caret_operator() {
245 let f = ComposerFormatter;
246 assert!(f.version_satisfies_requirement("1.2.3", "^1.2"));
247 assert!(f.version_satisfies_requirement("1.5.0", "^1.0"));
248 assert!(!f.version_satisfies_requirement("2.0.0", "^1.2"));
249 assert!(!f.version_satisfies_requirement("0.3.0", "^1.0"));
250 }
251
252 #[test]
253 fn test_tilde_with_three_segments() {
254 let f = ComposerFormatter;
255 assert!(f.version_satisfies_requirement("1.2.3", "~1.2.3"));
257 assert!(f.version_satisfies_requirement("1.2.9", "~1.2.3"));
258 assert!(!f.version_satisfies_requirement("1.3.0", "~1.2.3"));
259 assert!(!f.version_satisfies_requirement("1.2.2", "~1.2.3"));
260 }
261
262 #[test]
263 fn test_tilde_with_two_segments_composer_specific() {
264 let f = ComposerFormatter;
265 assert!(f.version_satisfies_requirement("1.2.0", "~1.2"));
267 assert!(f.version_satisfies_requirement("1.9.9", "~1.2"));
268 assert!(!f.version_satisfies_requirement("2.0.0", "~1.2")); assert!(!f.version_satisfies_requirement("1.1.9", "~1.2")); assert!(!f.version_satisfies_requirement("0.9.0", "~1.2")); }
272
273 #[test]
274 fn test_wildcard_version() {
275 let f = ComposerFormatter;
276 assert!(f.version_satisfies_requirement("1.0.5", "1.0.*"));
277 assert!(!f.version_satisfies_requirement("1.1.0", "1.0.*"));
278 }
279
280 #[test]
281 fn test_or_combinator() {
282 let f = ComposerFormatter;
283 assert!(f.version_satisfies_requirement("1.0.0", "1.0.0 || 2.0.0"));
284 assert!(f.version_satisfies_requirement("2.0.0", "1.0.0 || 2.0.0"));
285 assert!(!f.version_satisfies_requirement("3.0.0", "1.0.0 || 2.0.0"));
286 }
287
288 #[test]
289 fn test_range_constraint() {
290 let f = ComposerFormatter;
291 assert!(f.version_satisfies_requirement("1.5.0", ">=1.0 <2.0"));
292 assert!(!f.version_satisfies_requirement("2.0.0", ">=1.0 <2.0"));
293 assert!(!f.version_satisfies_requirement("0.9.0", ">=1.0 <2.0"));
294 }
295
296 #[test]
297 fn test_comparison_operators() {
298 let f = ComposerFormatter;
299 assert!(f.version_satisfies_requirement("2.0.0", ">=2.0.0"));
300 assert!(f.version_satisfies_requirement("2.0.1", ">=2.0.0"));
301 assert!(!f.version_satisfies_requirement("1.9.9", ">=2.0.0"));
302
303 assert!(f.version_satisfies_requirement("1.9.9", "<2.0.0"));
304 assert!(!f.version_satisfies_requirement("2.0.0", "<2.0.0"));
305
306 assert!(f.version_satisfies_requirement("1.0.0", "=1.0.0"));
307 assert!(!f.version_satisfies_requirement("1.0.1", "=1.0.0"));
308 }
309
310 #[test]
311 fn test_exact_version() {
312 let f = ComposerFormatter;
313 assert!(f.version_satisfies_requirement("1.2.3", "1.2.3"));
314 assert!(!f.version_satisfies_requirement("1.2.4", "1.2.3"));
315 }
316
317 #[test]
318 fn test_partial_version() {
319 let f = ComposerFormatter;
320 assert!(f.version_satisfies_requirement("1.2.3", "1"));
321 assert!(f.version_satisfies_requirement("1.2.3", "1.2"));
322 assert!(!f.version_satisfies_requirement("2.0.0", "1.2"));
323 }
324
325 #[test]
326 fn test_v_prefix_stripped() {
327 let f = ComposerFormatter;
328 assert!(f.version_satisfies_requirement("v1.24.1", "^1.24"));
329 assert!(f.version_satisfies_requirement("v1.2.3", "~1.2.3"));
330 assert!(f.version_satisfies_requirement("v2.0.0", ">=2.0.0"));
331 assert!(f.version_satisfies_requirement("v1.0.5", "1.0.*"));
332 assert!(f.version_satisfies_requirement("v1.2.3", "1.2.3"));
333 assert!(!f.version_satisfies_requirement("v2.0.0", "^1.0"));
334 }
335}