1use deps_core::VersionReq;
2use deps_core::lsp_helpers::{
3 DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
4 RequirementMatcher, RequirementResolution, SourcePolicy, compile_requirement_unless,
5};
6use deps_core::{ConcreteVersion, Dependency, DepsError, InvalidPackageName, PackageName};
7
8use crate::types::{GoDependency, GoDirective};
9
10fn go_version_matches(version: &str, requirement: &str) -> bool {
18 if version == requirement {
19 return true;
20 }
21
22 if let Some(suffix) = version.strip_prefix(requirement) {
26 return suffix.is_empty()
27 || suffix.starts_with('.')
28 || suffix.starts_with('-')
29 || suffix.starts_with('+');
30 }
31
32 false
33}
34
35struct ExactMatcher(String);
40
41impl RequirementMatcher for ExactMatcher {
42 fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
43 let version = version.as_str();
44 Some(go_version_matches(version, &self.0))
45 }
46}
47
48pub struct GoFormatter;
55
56impl PackageNaming for GoFormatter {
57 fn validate_package_name(&self, name: &str) -> Result<(), InvalidPackageName> {
67 let Err(err) = crate::registry::validate_module_path(name) else {
68 return Ok(());
69 };
70 let DepsError::InvalidVersionReq(reason) = err else {
77 unreachable!("validate_module_path only ever returns DepsError::InvalidVersionReq")
78 };
79 Err(InvalidPackageName::new(reason))
80 }
81}
82
83impl PackageRendering for GoFormatter {
84 fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
85 let version = version.as_str();
86 version.to_string()
89 }
90
91 fn package_url(&self, name: &PackageName) -> String {
92 crate::registry::package_url(name.as_str())
93 }
94
95 fn suppress_package_url(&self, source: &deps_core::parser::DependencySource) -> bool {
105 !self.source_is_public_registry_content(source)
106 }
107}
108
109impl RequirementResolution for GoFormatter {
110 fn version_satisfies_requirement(&self, version: &ConcreteVersion, requirement: &str) -> bool {
111 let version = version.as_str();
112 go_version_matches(version, requirement)
113 }
114
115 fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
128 compile_requirement_unless(
129 requirement.as_str(),
130 crate::version::is_pseudo_version,
131 ExactMatcher,
132 )
133 }
134
135 fn manifest_requirement_is_resolved_version(&self, dep: &dyn Dependency) -> bool {
136 dep.as_any()
150 .downcast_ref::<GoDependency>()
151 .is_some_and(|go_dep| go_dep.directive == GoDirective::Require)
152 }
153}
154
155impl DiagnosticMessages for GoFormatter {}
156
157impl DiagnosticPolicy for GoFormatter {}
158
159impl SourcePolicy for GoFormatter {
160 fn can_resolve_source(&self, source: &deps_core::parser::DependencySource) -> bool {
166 matches!(
167 source,
168 deps_core::parser::DependencySource::Registry
169 | deps_core::parser::DependencySource::AlternateRegistry { .. }
170 )
171 }
172}
173
174impl OsvNaming for GoFormatter {
175 fn osv_version_to_native(&self, version: &str) -> String {
176 if version.starts_with('v') {
179 version.to_string()
180 } else {
181 format!("v{version}")
182 }
183 }
184
185 fn osv_version(&self, version: &str) -> String {
186 version.strip_prefix('v').unwrap_or(version).to_string()
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use tower_lsp_server::ls_types::{Position, Range};
197
198 fn go_dep(directive: GoDirective, version: &str) -> GoDependency {
199 GoDependency {
200 module_path: PackageName::new("github.com/gorilla/mux"),
201 module_path_range: Range::new(Position::new(0, 0), Position::new(0, 1)),
202 version: Some(deps_core::VersionReq::new(version)),
203 version_range: Some(Range::new(Position::new(0, 0), Position::new(0, 1))),
204 directive,
205 indirect: false,
206 source: deps_core::parser::DependencySource::Registry,
207 }
208 }
209
210 #[test]
216 fn test_manifest_requirement_is_resolved_version_only_for_require_directive() {
217 let formatter = GoFormatter;
218
219 let require_dep = go_dep(GoDirective::Require, "v1.8.0");
220 assert!(formatter.manifest_requirement_is_resolved_version(&require_dep));
221
222 let exclude_dep = go_dep(GoDirective::Exclude, "v0.1.0");
223 assert!(!formatter.manifest_requirement_is_resolved_version(&exclude_dep));
224
225 let replace_dep = go_dep(GoDirective::Replace, "v1.0.0");
226 assert!(!formatter.manifest_requirement_is_resolved_version(&replace_dep));
227
228 let retract_dep = go_dep(GoDirective::Retract, "v1.0.0");
229 assert!(!formatter.manifest_requirement_is_resolved_version(&retract_dep));
230 }
231
232 #[test]
233 fn test_format_version_for_text_edit() {
234 let formatter = GoFormatter;
235
236 assert_eq!(
238 formatter.format_version_for_text_edit(&ConcreteVersion::new("v1.2.3")),
239 "v1.2.3"
240 );
241
242 assert_eq!(
244 formatter.format_version_for_text_edit(&ConcreteVersion::new(
245 "v0.0.0-20191109021931-daa7c04131f5"
246 )),
247 "v0.0.0-20191109021931-daa7c04131f5"
248 );
249
250 assert_eq!(
252 formatter.format_version_for_text_edit(&ConcreteVersion::new("v2.0.0+incompatible")),
253 "v2.0.0+incompatible"
254 );
255 }
256
257 #[test]
258 fn test_package_url() {
259 let formatter = GoFormatter;
260
261 assert_eq!(
263 formatter.package_url(&PackageName::new("github.com/gin-gonic/gin")),
264 "https://pkg.go.dev/github.com/gin-gonic/gin"
265 );
266
267 assert_eq!(
269 formatter.package_url(&PackageName::new("github.com/go-redis/redis/v8")),
270 "https://pkg.go.dev/github.com/go-redis/redis/v8"
271 );
272
273 assert_eq!(
275 formatter.package_url(&PackageName::new("fmt")),
276 "https://pkg.go.dev/fmt"
277 );
278
279 assert_eq!(
281 formatter.package_url(&PackageName::new("github.com/user@org/package")),
282 "https://pkg.go.dev/github.com/user%40org/package"
283 );
284
285 assert_eq!(
287 formatter.package_url(&PackageName::new("github.com/user/pkg name")),
288 "https://pkg.go.dev/github.com/user/pkg%20name"
289 );
290 }
291
292 #[test]
293 fn test_version_satisfies_requirement_exact_match() {
294 let formatter = GoFormatter;
295
296 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.3"), "v1.2.3"));
298 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("v0.1.0"), "v0.1.0"));
299 }
300
301 #[test]
302 fn test_version_satisfies_requirement_pseudo_version() {
303 let formatter = GoFormatter;
304
305 assert!(formatter.version_satisfies_requirement(
307 &ConcreteVersion::new("v0.0.0-20191109021931-daa7c04131f5"),
308 "v0.0.0"
309 ));
310
311 assert!(formatter.version_satisfies_requirement(
313 &ConcreteVersion::new("v0.0.0-20191109021931-daa7c04131f5"),
314 "v0.0.0-20191109021931-daa7c04131f5"
315 ));
316 }
317
318 #[test]
319 fn test_version_satisfies_requirement_incompatible() {
320 let formatter = GoFormatter;
321
322 assert!(
324 formatter.version_satisfies_requirement(
325 &ConcreteVersion::new("v2.0.0+incompatible"),
326 "v2.0.0"
327 )
328 );
329
330 assert!(formatter.version_satisfies_requirement(
332 &ConcreteVersion::new("v2.0.0+incompatible"),
333 "v2.0.0+incompatible"
334 ));
335 }
336
337 #[test]
338 fn test_version_does_not_satisfy_requirement() {
339 let formatter = GoFormatter;
340
341 assert!(
343 !formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.3"), "v1.2.4")
344 );
345 assert!(
346 !formatter.version_satisfies_requirement(&ConcreteVersion::new("v2.0.0"), "v1.0.0")
347 );
348
349 assert!(
351 !formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.3"), "v1.2.3.4")
352 );
353 }
354
355 #[test]
356 fn test_version_satisfies_requirement_prefix_scenarios() {
357 let formatter = GoFormatter;
358
359 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2"), "v1.2.3"));
361
362 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.3"), "v1.2"));
364
365 assert!(
367 !formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.30"), "v1.2.3")
368 );
369
370 assert!(
372 formatter.version_satisfies_requirement(&ConcreteVersion::new("v1.2.3.1"), "v1.2.3")
373 );
374 }
375
376 #[test]
377 fn test_osv_version_to_native_prepends_v_prefix() {
378 let formatter = GoFormatter;
379
380 assert_eq!(formatter.osv_version_to_native("0.3.7"), "v0.3.7");
381 assert_eq!(formatter.osv_version_to_native("v0.3.7"), "v0.3.7");
384 }
385
386 #[test]
387 fn test_osv_version_strips_v_prefix() {
388 let formatter = GoFormatter;
389
390 assert_eq!(formatter.osv_version("v1.2.3"), "1.2.3");
391 assert_eq!(
392 formatter.osv_version("v0.0.0-20191109021931-daa7c04131f5"),
393 "0.0.0-20191109021931-daa7c04131f5"
394 );
395 assert_eq!(
396 formatter.osv_version("v2.0.0+incompatible"),
397 "2.0.0+incompatible"
398 );
399 }
400
401 #[test]
402 fn test_osv_version_unprefixed_is_unaffected() {
403 let formatter = GoFormatter;
404
405 assert_eq!(formatter.osv_version("1.2.3"), "1.2.3");
408 }
409
410 #[test]
411 fn test_compile_requirement_satisfiable() {
412 let formatter = GoFormatter;
413 let matcher = formatter
414 .compile_requirement(&VersionReq::new("v1.9.1"))
415 .expect("an ordinary tagged requirement compiles");
416 assert_eq!(matcher.matches(&ConcreteVersion::new("v1.9.1")), Some(true));
417 assert_eq!(
418 matcher.matches(&ConcreteVersion::new("v1.9.2")),
419 Some(false)
420 );
421 }
422
423 #[test]
424 fn test_compile_requirement_never_skips_a_candidate() {
425 let formatter = GoFormatter;
428 let matcher = formatter
429 .compile_requirement(&VersionReq::new("v1.9.1"))
430 .unwrap();
431 assert_eq!(
432 matcher.matches(&ConcreteVersion::new("not-a-version-at-all")),
433 Some(false)
434 );
435 }
436
437 #[test]
441 fn test_compile_requirement_pseudo_version_requirement_returns_none() {
442 let formatter = GoFormatter;
443 assert!(
444 formatter
445 .compile_requirement(&VersionReq::new("v0.0.0-20191109021931-daa7c04131f5"))
446 .is_none()
447 );
448 }
449
450 #[test]
453 fn test_compile_requirement_incompatible_tag_still_compiles() {
454 let formatter = GoFormatter;
455 let matcher = formatter
456 .compile_requirement(&VersionReq::new("v2.0.0+incompatible"))
457 .expect("a +incompatible tag is not a pseudo-version");
458 assert_eq!(
459 matcher.matches(&ConcreteVersion::new("v2.0.0+incompatible")),
460 Some(true)
461 );
462 }
463
464 #[test]
465 fn test_validate_package_name_accepts_valid_module_path() {
466 let formatter = GoFormatter;
467 assert!(
468 formatter
469 .validate_package_name("github.com/gin-gonic/gin")
470 .is_ok()
471 );
472 assert!(formatter.validate_package_name("golang.org/x/mod").is_ok());
473 }
474
475 #[test]
476 fn test_validate_package_name_rejects_empty() {
477 let formatter = GoFormatter;
478 assert!(formatter.validate_package_name("").is_err());
479 }
480
481 #[test]
484 fn test_validate_package_name_rejects_dot_segment() {
485 let formatter = GoFormatter;
486 assert!(
487 formatter
488 .validate_package_name("github.com/user/..")
489 .is_err()
490 );
491 assert!(formatter.validate_package_name("./evil").is_err());
492 }
493
494 #[test]
497 fn test_can_resolve_source() {
498 let formatter = GoFormatter;
499 assert!(formatter.can_resolve_source(&deps_core::parser::DependencySource::Registry));
500 assert!(formatter.can_resolve_source(
501 &deps_core::parser::DependencySource::AlternateRegistry {
502 index: "go-proxy:deadbeef".to_string(),
503 mirrors_crates_io: false,
504 }
505 ));
506 assert!(!formatter.can_resolve_source(
507 &deps_core::parser::DependencySource::CustomRegistry {
508 url: "not-a-valid-url".to_string(),
509 }
510 ));
511 }
512
513 #[test]
517 fn test_suppress_package_url() {
518 let formatter = GoFormatter;
519 assert!(!formatter.suppress_package_url(&deps_core::parser::DependencySource::Registry));
520 assert!(formatter.suppress_package_url(
521 &deps_core::parser::DependencySource::AlternateRegistry {
522 index: "go-proxy:deadbeef".to_string(),
523 mirrors_crates_io: false,
524 }
525 ));
526 assert!(formatter.suppress_package_url(
527 &deps_core::parser::DependencySource::CustomRegistry {
528 url: "not-a-valid-url".to_string(),
529 }
530 ));
531 }
532
533 #[test]
534 fn test_validate_package_name_rejects_too_long() {
535 let formatter = GoFormatter;
536 let too_long = "a".repeat(501);
537 assert!(formatter.validate_package_name(&too_long).is_err());
538 }
539}