1use deps_core::lsp_helpers::{
2 DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
3 RequirementMatcher, RequirementResolution, SourcePolicy,
4};
5use deps_core::parser::DependencySource;
6use deps_core::{ConcreteVersion, InvalidPackageName, PackageName, VersionReq};
7
8const MAX_NAME_LENGTH: usize = 64;
17
18struct SemverMatcher(semver::VersionReq);
21
22impl RequirementMatcher for SemverMatcher {
23 fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
24 let version = version.as_str();
25 version
26 .parse::<semver::Version>()
27 .ok()
28 .map(|v| self.0.matches(&v))
29 }
30}
31
32pub struct CargoFormatter;
33
34impl PackageNaming for CargoFormatter {
35 fn validate_package_name(&self, name: &str) -> Result<(), InvalidPackageName> {
71 if name.is_empty() {
72 return Err(InvalidPackageName::new("name cannot be empty"));
73 }
74 if !crate::sparse::is_safe_crate_name_charset(name) {
75 return Err(InvalidPackageName::new(
76 "name must contain only ASCII letters, digits, '-', or '_'",
77 ));
78 }
79 if !name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
80 return Err(InvalidPackageName::new(
81 "name must start with an ASCII letter or '_'",
82 ));
83 }
84 if name.chars().count() > MAX_NAME_LENGTH {
85 return Err(InvalidPackageName::new(format!(
86 "name cannot exceed {MAX_NAME_LENGTH} characters"
87 )));
88 }
89 Ok(())
90 }
91}
92
93impl PackageRendering for CargoFormatter {
94 fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
95 let version = version.as_str();
96 version.to_string()
97 }
98
99 fn package_url(&self, name: &PackageName) -> String {
100 crate::registry::crate_url(name.as_str())
101 }
102
103 fn suppress_package_url(&self, source: &DependencySource) -> bool {
111 !self.source_is_public_registry_content(source)
112 }
113}
114
115impl RequirementResolution for CargoFormatter {
116 fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
121 requirement
122 .as_str()
123 .parse::<semver::VersionReq>()
124 .ok()
125 .map(|req| Box::new(SemverMatcher(req)) as Box<dyn RequirementMatcher>)
126 }
127}
128
129impl DiagnosticMessages for CargoFormatter {}
130
131impl DiagnosticPolicy for CargoFormatter {
132 fn strict_semver_prerelease_exclusion(&self) -> bool {
135 true
136 }
137}
138
139impl SourcePolicy for CargoFormatter {
140 fn can_resolve_source(&self, source: &DependencySource) -> bool {
147 matches!(
148 source,
149 DependencySource::Registry | DependencySource::AlternateRegistry { .. }
150 )
151 }
152
153 fn source_is_public_registry_content(&self, source: &DependencySource) -> bool {
160 matches!(
161 source,
162 DependencySource::Registry
163 | DependencySource::AlternateRegistry {
164 mirrors_crates_io: true,
165 ..
166 }
167 )
168 }
169}
170
171impl OsvNaming for CargoFormatter {}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_source_is_public_registry_content_plain_registry() {
179 let formatter = CargoFormatter;
180 assert!(formatter.source_is_public_registry_content(&DependencySource::Registry));
181 }
182
183 #[test]
184 fn test_source_is_public_registry_content_crates_io_mirror() {
185 let formatter = CargoFormatter;
186 assert!(formatter.source_is_public_registry_content(
187 &DependencySource::AlternateRegistry {
188 index: "https://mirror.example".into(),
189 mirrors_crates_io: true,
190 }
191 ));
192 }
193
194 #[test]
195 fn test_source_is_public_registry_content_non_mirror_alternate_is_false() {
196 let formatter = CargoFormatter;
197 assert!(!formatter.source_is_public_registry_content(
198 &DependencySource::AlternateRegistry {
199 index: "https://index.mycorp.dev".into(),
200 mirrors_crates_io: false,
201 }
202 ));
203 }
204
205 #[test]
206 fn test_suppress_package_url_mirror_not_suppressed() {
207 let formatter = CargoFormatter;
208 assert!(
209 !formatter.suppress_package_url(&DependencySource::AlternateRegistry {
210 index: "https://mirror.example".into(),
211 mirrors_crates_io: true,
212 })
213 );
214 }
215
216 #[test]
217 fn test_suppress_package_url_non_mirror_alternate_suppressed() {
218 let formatter = CargoFormatter;
219 assert!(
220 formatter.suppress_package_url(&DependencySource::AlternateRegistry {
221 index: "https://index.mycorp.dev".into(),
222 mirrors_crates_io: false,
223 })
224 );
225 }
226
227 #[test]
228 fn test_format_version() {
229 let formatter = CargoFormatter;
230 assert_eq!(
231 formatter.format_version_for_text_edit(&ConcreteVersion::new("1.0.214")),
232 "1.0.214"
233 );
234 assert_eq!(
235 formatter.format_version_for_text_edit(&ConcreteVersion::new("0.1.0")),
236 "0.1.0"
237 );
238 }
239
240 #[test]
241 fn test_package_url() {
242 let formatter = CargoFormatter;
243 assert_eq!(
244 formatter.package_url(&PackageName::new("serde")),
245 "https://crates.io/crates/serde"
246 );
247 assert_eq!(
248 formatter.package_url(&PackageName::new("tokio-util")),
249 "https://crates.io/crates/tokio-util"
250 );
251 }
252
253 #[test]
254 fn test_validate_package_name_accepts_valid_names() {
255 let formatter = CargoFormatter;
256 for name in [
257 "serde",
258 "tokio-util",
259 "my_crate",
260 "a",
261 "a".repeat(64).as_str(),
262 ] {
263 assert!(
264 formatter.validate_package_name(name).is_ok(),
265 "expected {name:?} to be accepted"
266 );
267 }
268 }
269
270 #[test]
271 fn test_validate_package_name_rejects_empty() {
272 let formatter = CargoFormatter;
273 assert!(formatter.validate_package_name("").is_err());
274 }
275
276 #[test]
277 fn test_validate_package_name_rejects_too_long() {
278 let formatter = CargoFormatter;
279 let too_long = "a".repeat(65);
280 assert!(formatter.validate_package_name(&too_long).is_err());
281 }
282
283 #[test]
286 fn test_validate_package_name_rejects_non_ascii() {
287 let formatter = CargoFormatter;
288 assert!(formatter.validate_package_name("日本語").is_err());
289 }
290
291 #[test]
292 fn test_validate_package_name_rejects_disallowed_punctuation() {
293 let formatter = CargoFormatter;
294 for name in ["serde.rs", "serde/util", "serde@1.0", "serde util"] {
295 assert!(
296 formatter.validate_package_name(name).is_err(),
297 "expected {name:?} to be rejected"
298 );
299 }
300 }
301
302 #[test]
306 fn test_validate_package_name_rejects_leading_digit_or_hyphen() {
307 let formatter = CargoFormatter;
308 for name in ["1abc", "9serde", "-abc"] {
309 assert!(
310 formatter.validate_package_name(name).is_err(),
311 "expected {name:?} to be rejected"
312 );
313 }
314 }
315
316 #[test]
318 fn test_validate_package_name_accepts_leading_underscore() {
319 let formatter = CargoFormatter;
320 assert!(formatter.validate_package_name("_private").is_ok());
321 }
322
323 #[test]
328 fn test_validate_package_name_long_non_ascii_reports_charset_error() {
329 let formatter = CargoFormatter;
330 let name = "日".repeat(30); let err = formatter
332 .validate_package_name(&name)
333 .expect_err("non-ASCII name must be rejected");
334 assert!(
335 err.reason().contains("ASCII"),
336 "expected a charset error, got: {}",
337 err.reason()
338 );
339 }
340
341 #[test]
348 fn test_validate_package_name_over_128_bytes_reports_length_error_not_charset() {
349 let formatter = CargoFormatter;
350 let name = "a".repeat(130);
351 let err = formatter
352 .validate_package_name(&name)
353 .expect_err("over-length name must be rejected");
354 assert!(
355 err.reason().contains("exceed"),
356 "expected a length error, got: {}",
357 err.reason()
358 );
359 }
360
361 #[test]
362 fn test_default_normalize_is_identity() {
363 let formatter = CargoFormatter;
364 assert_eq!(
365 formatter.normalize_package_name(&PackageName::new("serde")),
366 "serde"
367 );
368 assert_eq!(
369 formatter.normalize_package_name(&PackageName::new("tokio-util")),
370 "tokio-util"
371 );
372 }
373
374 #[test]
375 fn test_default_yanked_message() {
376 let formatter = CargoFormatter;
377 assert_eq!(formatter.yanked_message(), "This version has been yanked");
378 assert_eq!(formatter.yanked_label(), "*(yanked)*");
379 }
380
381 #[test]
382 fn test_version_satisfies_requirement() {
383 let formatter = CargoFormatter;
384
385 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "1.2.3"));
386 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "^1.2"));
387 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "~1.2"));
388 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "1"));
389 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "1.2"));
390
391 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "2.0.0"));
392 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.3"), "1.3"));
393 }
394
395 #[test]
396 fn test_compile_requirement_satisfiable() {
397 let formatter = CargoFormatter;
398 let matcher = formatter
399 .compile_requirement(&VersionReq::new("^1.0"))
400 .expect("valid semver requirement must compile");
401 assert_eq!(matcher.matches(&ConcreteVersion::new("1.5.0")), Some(true));
402 assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(false));
403 }
404
405 #[test]
406 fn test_compile_requirement_unparseable_requirement_returns_none() {
407 let formatter = CargoFormatter;
408 assert!(
409 formatter
410 .compile_requirement(&VersionReq::new("not a semver req"))
411 .is_none()
412 );
413 }
414
415 #[test]
416 fn test_compile_requirement_unparseable_candidate_is_skipped() {
417 let formatter = CargoFormatter;
418 let matcher = formatter
419 .compile_requirement(&VersionReq::new("^1.0"))
420 .unwrap();
421 assert_eq!(
422 matcher.matches(&ConcreteVersion::new("not-a-version")),
423 None
424 );
425 }
426
427 #[test]
432 fn test_compile_requirement_comparator_list_satisfiable() {
433 let formatter = CargoFormatter;
434 let matcher = formatter
435 .compile_requirement(&VersionReq::new(">=1.0, <2.0"))
436 .unwrap();
437 assert_eq!(matcher.matches(&ConcreteVersion::new("1.5.0")), Some(true));
438 }
439
440 #[test]
445 fn test_compile_requirement_tilde_mistyped_patch_is_unsatisfiable() {
446 let formatter = CargoFormatter;
447 let matcher = formatter
448 .compile_requirement(&VersionReq::new("~1.0.999"))
449 .unwrap();
450 assert_eq!(
451 matcher.matches(&ConcreteVersion::new("1.0.214")),
452 Some(false)
453 );
454 }
455}