deps_go/
formatter.rs

1use deps_core::lsp_helpers::EcosystemFormatter;
2
3/// Formatter for Go module version strings and package URLs.
4///
5/// Handles Go-specific version formatting:
6/// - Versions are unquoted in go.mod (v1.2.3)
7/// - Pseudo-versions (v0.0.0-20191109021931-daa7c04131f5)
8/// - +incompatible suffix for v2+ modules without /v2 path
9pub struct GoFormatter;
10
11impl EcosystemFormatter for GoFormatter {
12    fn format_version_for_code_action(&self, version: &str) -> String {
13        // Go versions in go.mod are unquoted: v1.2.3
14        // Return version as-is since it should already have "v" prefix from registry
15        version.to_string()
16    }
17
18    fn package_url(&self, name: &str) -> String {
19        // Use pkg.go.dev for package documentation
20        // URL encode special characters (@ and space)
21        let encoded = name.replace('@', "%40").replace(' ', "%20");
22        format!("https://pkg.go.dev/{encoded}")
23    }
24
25    fn version_satisfies_requirement(&self, version: &str, requirement: &str) -> bool {
26        // For Go modules, version matching is typically exact
27        // However, we need to handle:
28        // 1. Exact match: v1.2.3 == v1.2.3
29        // 2. Prefix match for pseudo-versions: v0.0.0-20191109021931-daa7c04131f5 starts with v0.0.0
30        // 3. Prefix match for +incompatible: v2.0.0+incompatible starts with v2.0.0
31
32        if version == requirement {
33            return true;
34        }
35
36        // Handle pseudo-versions and +incompatible suffix
37        // Check if version starts with requirement followed by a dot, hyphen, plus, or end
38        // This prevents false positives like v1.2.30 matching v1.2.3
39        if let Some(suffix) = version.strip_prefix(requirement) {
40            return suffix.is_empty()
41                || suffix.starts_with('.')
42                || suffix.starts_with('-')
43                || suffix.starts_with('+');
44        }
45
46        false
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_format_version_for_code_action() {
56        let formatter = GoFormatter;
57
58        // Standard semantic version
59        assert_eq!(formatter.format_version_for_code_action("v1.2.3"), "v1.2.3");
60
61        // Pseudo-version
62        assert_eq!(
63            formatter.format_version_for_code_action("v0.0.0-20191109021931-daa7c04131f5"),
64            "v0.0.0-20191109021931-daa7c04131f5"
65        );
66
67        // Version with +incompatible
68        assert_eq!(
69            formatter.format_version_for_code_action("v2.0.0+incompatible"),
70            "v2.0.0+incompatible"
71        );
72    }
73
74    #[test]
75    fn test_package_url() {
76        let formatter = GoFormatter;
77
78        // Standard package
79        assert_eq!(
80            formatter.package_url("github.com/gin-gonic/gin"),
81            "https://pkg.go.dev/github.com/gin-gonic/gin"
82        );
83
84        // Package with version path
85        assert_eq!(
86            formatter.package_url("github.com/go-redis/redis/v8"),
87            "https://pkg.go.dev/github.com/go-redis/redis/v8"
88        );
89
90        // Standard library package
91        assert_eq!(formatter.package_url("fmt"), "https://pkg.go.dev/fmt");
92
93        // Package with @ character (should be URL encoded)
94        assert_eq!(
95            formatter.package_url("github.com/user@org/package"),
96            "https://pkg.go.dev/github.com/user%40org/package"
97        );
98
99        // Package with space (should be URL encoded)
100        assert_eq!(
101            formatter.package_url("github.com/user/pkg name"),
102            "https://pkg.go.dev/github.com/user/pkg%20name"
103        );
104    }
105
106    #[test]
107    fn test_version_satisfies_requirement_exact_match() {
108        let formatter = GoFormatter;
109
110        // Exact version match
111        assert!(formatter.version_satisfies_requirement("v1.2.3", "v1.2.3"));
112        assert!(formatter.version_satisfies_requirement("v0.1.0", "v0.1.0"));
113    }
114
115    #[test]
116    fn test_version_satisfies_requirement_pseudo_version() {
117        let formatter = GoFormatter;
118
119        // Pseudo-version prefix match
120        assert!(
121            formatter.version_satisfies_requirement("v0.0.0-20191109021931-daa7c04131f5", "v0.0.0")
122        );
123
124        // Full pseudo-version match
125        assert!(formatter.version_satisfies_requirement(
126            "v0.0.0-20191109021931-daa7c04131f5",
127            "v0.0.0-20191109021931-daa7c04131f5"
128        ));
129    }
130
131    #[test]
132    fn test_version_satisfies_requirement_incompatible() {
133        let formatter = GoFormatter;
134
135        // +incompatible suffix handling
136        assert!(formatter.version_satisfies_requirement("v2.0.0+incompatible", "v2.0.0"));
137
138        // Exact match with +incompatible
139        assert!(
140            formatter.version_satisfies_requirement("v2.0.0+incompatible", "v2.0.0+incompatible")
141        );
142    }
143
144    #[test]
145    fn test_version_does_not_satisfy_requirement() {
146        let formatter = GoFormatter;
147
148        // Different versions
149        assert!(!formatter.version_satisfies_requirement("v1.2.3", "v1.2.4"));
150        assert!(!formatter.version_satisfies_requirement("v2.0.0", "v1.0.0"));
151
152        // Partial match that doesn't start with requirement
153        assert!(!formatter.version_satisfies_requirement("v1.2.3", "v1.2.3.4"));
154    }
155
156    #[test]
157    fn test_version_satisfies_requirement_prefix_scenarios() {
158        let formatter = GoFormatter;
159
160        // Version is prefix of requirement (should NOT match)
161        assert!(!formatter.version_satisfies_requirement("v1.2", "v1.2.3"));
162
163        // Requirement is prefix of version with dot boundary (should match)
164        assert!(formatter.version_satisfies_requirement("v1.2.3", "v1.2"));
165
166        // False positive prevention: v1.2.30 should NOT match v1.2.3
167        assert!(!formatter.version_satisfies_requirement("v1.2.30", "v1.2.3"));
168
169        // But v1.2.3.1 SHOULD match v1.2.3 (if it has dot boundary)
170        assert!(formatter.version_satisfies_requirement("v1.2.3.1", "v1.2.3"));
171    }
172}