Skip to main content

deps_go/
types.rs

1//! Types for Go module dependency management.
2
3use deps_core::parser::DependencySource;
4use std::any::Any;
5use tower_lsp_server::ls_types::Range;
6
7/// A dependency from a go.mod file.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GoDependency {
10    /// Module path (e.g., "github.com/gin-gonic/gin")
11    pub module_path: deps_core::PackageName,
12    /// LSP range of the module path in source
13    pub module_path_range: Range,
14    /// Version requirement (e.g., "v1.9.1", "v0.0.0-20191109021931-daa7c04131f5")
15    pub version: Option<deps_core::VersionReq>,
16    /// LSP range of version in source
17    pub version_range: Option<Range>,
18    /// Dependency directive type
19    pub directive: GoDirective,
20    /// Whether this is an indirect dependency (// indirect comment)
21    pub indirect: bool,
22    /// Resolved source (spec 034): `Registry` unless `$GOENV` declares a `GOPROXY`/`GOPRIVATE`
23    /// override applicable to this module path.
24    pub source: DependencySource,
25}
26
27/// Go module directive types.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum GoDirective {
30    /// Direct dependency in require block
31    Require,
32    /// Replacement directive
33    Replace,
34    /// Exclusion directive
35    Exclude,
36    /// Retraction directive
37    Retract,
38}
39
40/// Version information from proxy.golang.org.
41#[derive(Debug, Clone)]
42pub struct GoVersion {
43    /// Version string (e.g., "v1.9.1")
44    pub version: deps_core::ConcreteVersion,
45    /// Publish timestamp, parsed eagerly from the `/@latest` and
46    /// `/@v/{version}.info` endpoints' `Time` field.
47    ///
48    /// Always `None` for versions from `/@v/list` (the `Ch2` path), which
49    /// carries no dates — a documented Go-specific limitation, not a bug.
50    /// `None` also when the value fails to parse as RFC 3339, degrading
51    /// gracefully per [US-003](https://github.com/bug-ops/deps-lsp/issues/145).
52    pub published_at: Option<deps_core::PublishTime>,
53    /// Whether this is a pseudo-version
54    pub is_pseudo: bool,
55    /// Whether this version is retracted
56    pub retracted: bool,
57}
58
59/// Package metadata from proxy.golang.org.
60#[derive(Debug, Clone)]
61pub struct GoMetadata {
62    /// Module path
63    pub module_path: deps_core::PackageName,
64    /// Latest stable version
65    pub latest_version: deps_core::ConcreteVersion,
66    /// Description (if available from go.mod or README)
67    pub description: Option<String>,
68    /// Repository URL (inferred from module path)
69    pub repository: Option<String>,
70    /// Documentation URL (pkg.go.dev)
71    pub documentation: Option<String>,
72}
73
74// NOTE: Cannot use deps_core::impl_dependency! macro because we need to provide custom
75// features() implementation (Go modules don't have features like Cargo).
76// The macro would provide features() but we need to override it anyway.
77impl deps_core::ecosystem::Dependency for GoDependency {
78    fn name(&self) -> &deps_core::PackageName {
79        &self.module_path
80    }
81
82    fn name_range(&self) -> Range {
83        self.module_path_range
84    }
85
86    fn version_requirement(&self) -> Option<&deps_core::VersionReq> {
87        self.version.as_ref()
88    }
89
90    fn version_range(&self) -> Option<Range> {
91        self.version_range
92    }
93
94    fn source(&self) -> DependencySource {
95        self.source.clone()
96    }
97
98    fn features(&self) -> &[String] {
99        &[]
100    }
101
102    fn as_any(&self) -> &dyn Any {
103        self
104    }
105}
106
107// NOTE: Cannot use impl_version! macro because GoVersion has custom is_prerelease() logic.
108// Go considers pseudo-versions as pre-releases, and has special handling for +incompatible suffix.
109impl deps_core::registry::Version for GoVersion {
110    fn version_string(&self) -> &deps_core::ConcreteVersion {
111        &self.version
112    }
113
114    fn removal_status(&self) -> deps_core::RemovalStatus {
115        deps_core::RemovalStatus::from_yanked(self.retracted)
116    }
117
118    fn published_at(&self) -> Option<deps_core::PublishTime> {
119        self.published_at
120    }
121
122    fn is_prerelease(&self) -> bool {
123        // Go considers pseudo-versions as pre-releases (they're commit-based).
124        // Regular pre-releases contain '-' (e.g., v1.0.0-beta.1).
125        // BUT: +incompatible suffix is NOT a pre-release indicator.
126        self.is_pseudo
127            || (self.version.as_str().contains('-')
128                && !self.version.as_str().contains("+incompatible"))
129    }
130
131    fn features(&self) -> Vec<String> {
132        vec![]
133    }
134
135    fn as_any(&self) -> &dyn Any {
136        self
137    }
138}
139
140deps_core::impl_metadata!(GoMetadata {
141    name: module_path,
142    description: description,
143    repository: repository,
144    documentation: documentation,
145    latest_version: latest_version,
146});
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use deps_core::ecosystem::Dependency;
152    use deps_core::registry::{Metadata, Version};
153    use std::assert_matches;
154    use tower_lsp_server::ls_types::Position;
155
156    #[test]
157    fn test_go_dependency_trait() {
158        let dep = GoDependency {
159            module_path: "github.com/gin-gonic/gin".into(),
160            module_path_range: Range::new(Position::new(0, 0), Position::new(0, 10)),
161            version: Some("v1.9.1".into()),
162            version_range: Some(Range::new(Position::new(0, 11), Position::new(0, 17))),
163            directive: GoDirective::Require,
164            indirect: false,
165            source: DependencySource::Registry,
166        };
167
168        assert_eq!(dep.name(), "github.com/gin-gonic/gin");
169        assert_eq!(
170            dep.version_requirement().map(deps_core::VersionReq::as_str),
171            Some("v1.9.1")
172        );
173        assert_matches!(dep.source(), DependencySource::Registry);
174        assert_eq!(dep.features().len(), 0);
175    }
176
177    #[test]
178    fn test_go_version_trait() {
179        let version = GoVersion {
180            version: "v1.9.1".into(),
181            published_at: deps_core::PublishTime::parse_rfc3339("2023-01-01T00:00:00Z"),
182            is_pseudo: false,
183            retracted: false,
184        };
185
186        assert_eq!(version.version_string().as_str(), "v1.9.1");
187        assert!(!version.removal_status().blocks_resolution());
188        assert!(!version.is_prerelease());
189        assert!(version.is_stable());
190        assert_eq!(
191            version.published_at(),
192            deps_core::PublishTime::parse_rfc3339("2023-01-01T00:00:00Z")
193        );
194    }
195
196    #[test]
197    fn test_pseudo_version_is_prerelease() {
198        let version = GoVersion {
199            version: "v0.0.0-20191109021931-daa7c04131f5".into(),
200            published_at: None,
201            is_pseudo: true,
202            retracted: false,
203        };
204
205        assert!(version.is_prerelease());
206        assert!(!version.is_stable());
207    }
208
209    #[test]
210    fn test_retracted_version_is_yanked() {
211        let version = GoVersion {
212            version: "v1.0.0".into(),
213            published_at: None,
214            is_pseudo: false,
215            retracted: true,
216        };
217
218        assert!(version.removal_status().blocks_resolution());
219        assert!(!version.is_stable());
220    }
221
222    #[test]
223    fn test_go_metadata_trait() {
224        let metadata = GoMetadata {
225            module_path: "github.com/gin-gonic/gin".into(),
226            latest_version: "v1.9.1".into(),
227            description: Some("Gin is a HTTP web framework".to_string()),
228            repository: Some("https://github.com/gin-gonic/gin".to_string()),
229            documentation: Some("https://pkg.go.dev/github.com/gin-gonic/gin".to_string()),
230        };
231
232        assert_eq!(metadata.name(), "github.com/gin-gonic/gin");
233        assert_eq!(metadata.latest_version(), "v1.9.1");
234        assert_eq!(metadata.description(), Some("Gin is a HTTP web framework"));
235        assert_eq!(
236            metadata.repository(),
237            Some("https://github.com/gin-gonic/gin")
238        );
239        assert_eq!(
240            metadata.documentation(),
241            Some("https://pkg.go.dev/github.com/gin-gonic/gin")
242        );
243    }
244
245    #[test]
246    fn test_go_directive_equality() {
247        assert_eq!(GoDirective::Require, GoDirective::Require);
248        assert_ne!(GoDirective::Require, GoDirective::Replace);
249    }
250}