deps_core/parser.rs
1use crate::error::Result;
2use tower_lsp_server::ls_types::{Range, Uri};
3
4/// Generic manifest parser interface.
5///
6/// Implementors parse ecosystem-specific manifest files (Cargo.toml, package.json, etc.)
7/// and extract dependency information with precise LSP positions.
8///
9/// # Note
10///
11/// This trait is being phased out in favor of the `Ecosystem` trait.
12/// New implementations should use `Ecosystem::parse_manifest()` instead.
13pub trait ManifestParser: Send + Sync {
14 /// Parsed dependency type for this ecosystem.
15 type Dependency: DependencyInfo + Clone + Send + Sync;
16
17 /// Parse result containing dependencies and optional workspace information.
18 type ParseResult: ParseResultInfo<Dependency = Self::Dependency> + Send;
19
20 /// Parses a manifest file and extracts all dependencies with positions.
21 ///
22 /// # Errors
23 ///
24 /// Returns error if:
25 /// - Manifest syntax is invalid
26 /// - File path cannot be determined from URL
27 fn parse(&self, content: &str, doc_uri: &Uri) -> Result<Self::ParseResult>;
28}
29
30/// Dependency information trait.
31///
32/// All parsed dependencies must implement this for generic handler access.
33///
34/// # Note
35///
36/// The new `Ecosystem` trait uses `crate::ecosystem::Dependency` instead.
37/// This trait is kept for backward compatibility during migration.
38pub trait DependencyInfo {
39 /// Dependency name (package/crate name).
40 fn name(&self) -> &str;
41
42 /// LSP range of the dependency name in the source file.
43 fn name_range(&self) -> Range;
44
45 /// Version requirement string (e.g., "^1.0", "~2.3.4").
46 fn version_requirement(&self) -> Option<&str>;
47
48 /// LSP range of the version string (for inlay hints positioning).
49 fn version_range(&self) -> Option<Range>;
50
51 /// Dependency source (registry, git, path).
52 fn source(&self) -> DependencySource;
53
54 /// Feature flags requested (Cargo-specific, empty for npm).
55 fn features(&self) -> &[String] {
56 &[]
57 }
58}
59
60/// Parse result information trait.
61///
62/// # Note
63///
64/// The new `Ecosystem` trait uses `crate::ecosystem::ParseResult` instead.
65/// This trait is kept for backward compatibility during migration.
66pub trait ParseResultInfo {
67 type Dependency: DependencyInfo;
68
69 /// All dependencies found in the manifest.
70 fn dependencies(&self) -> &[Self::Dependency];
71
72 /// Workspace root path (for monorepo support).
73 fn workspace_root(&self) -> Option<&std::path::Path>;
74}
75
76/// Dependency source (shared across ecosystems).
77#[derive(Debug, Clone, PartialEq)]
78pub enum DependencySource {
79 /// Dependency from default registry (crates.io, npm, PyPI).
80 Registry,
81 /// Dependency from Git repository.
82 Git { url: String, rev: Option<String> },
83 /// Dependency from local filesystem path.
84 Path { path: String },
85}
86
87/// Loading state for registry data fetching.
88///
89/// Tracks the current state of background registry operations to provide
90/// user feedback about data availability.
91///
92/// # State Transitions
93///
94/// Complete state machine diagram showing all valid transitions:
95///
96/// ```text
97/// ┌─────┐
98/// │Idle │ (Initial state: no data loaded, not loading)
99/// └──┬──┘
100/// │
101/// │ didOpen/didChange
102/// │ (start fetching)
103/// ▼
104/// ┌────────┐
105/// │Loading │ (Fetching registry data)
106/// └───┬────┘
107/// │
108/// ├─────── Success ──────┐
109/// │ ▼
110/// │ ┌────────┐
111/// │ │Loaded │ (Data cached and ready)
112/// │ └───┬────┘
113/// │ │
114/// │ │ didChange/refresh
115/// │ │ (re-fetch)
116/// │ │
117/// │ ▼
118/// │ ┌────────┐
119/// │ │Loading │
120/// │ └────────┘
121/// │
122/// └─────── Error ─────────┐
123/// ▼
124/// ┌────────┐
125/// │Failed │ (Fetch failed, old cache may exist)
126/// └───┬────┘
127/// │
128/// │ didChange/retry
129/// │ (try again)
130/// │
131/// ▼
132/// ┌────────┐
133/// │Loading │
134/// └────────┘
135/// ```
136///
137/// # Key Behaviors
138///
139/// - **Idle**: Initial state when no data has been fetched yet
140/// - **Loading**: Actively fetching from registry (may show loading indicator)
141/// - **Loaded**: Successfully fetched and cached data
142/// - **Failed**: Network/registry error occurred (falls back to old cache if available)
143///
144/// # Thread Safety
145///
146/// This enum is `Copy` for efficient passing across thread boundaries in async contexts.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub enum LoadingState {
149 /// No data loaded, not currently loading
150 #[default]
151 Idle,
152 /// Currently fetching registry data
153 Loading,
154 /// Data fetched and cached
155 Loaded,
156 /// Fetch failed (old cached data may still be available)
157 Failed,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_dependency_source_registry() {
166 let source = DependencySource::Registry;
167 assert_eq!(source, DependencySource::Registry);
168 }
169
170 #[test]
171 fn test_dependency_source_git() {
172 let source = DependencySource::Git {
173 url: "https://github.com/user/repo".into(),
174 rev: Some("main".into()),
175 };
176
177 match source {
178 DependencySource::Git { url, rev } => {
179 assert_eq!(url, "https://github.com/user/repo");
180 assert_eq!(rev, Some("main".into()));
181 }
182 _ => panic!("Expected Git source"),
183 }
184 }
185
186 #[test]
187 fn test_dependency_source_git_no_rev() {
188 let source = DependencySource::Git {
189 url: "https://github.com/user/repo".into(),
190 rev: None,
191 };
192
193 match source {
194 DependencySource::Git { url, rev } => {
195 assert_eq!(url, "https://github.com/user/repo");
196 assert!(rev.is_none());
197 }
198 _ => panic!("Expected Git source"),
199 }
200 }
201
202 #[test]
203 fn test_dependency_source_path() {
204 let source = DependencySource::Path {
205 path: "../local-crate".into(),
206 };
207
208 match source {
209 DependencySource::Path { path } => {
210 assert_eq!(path, "../local-crate");
211 }
212 _ => panic!("Expected Path source"),
213 }
214 }
215
216 #[test]
217 fn test_dependency_source_clone() {
218 let source1 = DependencySource::Git {
219 url: "https://example.com/repo".into(),
220 rev: Some("v1.0".into()),
221 };
222 let source2 = source1.clone();
223
224 assert_eq!(source1, source2);
225 }
226
227 #[test]
228 fn test_dependency_source_equality() {
229 let reg1 = DependencySource::Registry;
230 let reg2 = DependencySource::Registry;
231 assert_eq!(reg1, reg2);
232
233 let git1 = DependencySource::Git {
234 url: "https://example.com".into(),
235 rev: None,
236 };
237 let git2 = DependencySource::Git {
238 url: "https://example.com".into(),
239 rev: None,
240 };
241 assert_eq!(git1, git2);
242
243 let git3 = DependencySource::Git {
244 url: "https://different.com".into(),
245 rev: None,
246 };
247 assert_ne!(git1, git3);
248 }
249
250 #[test]
251 fn test_dependency_source_debug() {
252 let source = DependencySource::Registry;
253 let debug = format!("{:?}", source);
254 assert_eq!(debug, "Registry");
255
256 let git = DependencySource::Git {
257 url: "https://example.com".into(),
258 rev: Some("main".into()),
259 };
260 let git_debug = format!("{:?}", git);
261 assert!(git_debug.contains("https://example.com"));
262 assert!(git_debug.contains("main"));
263 }
264
265 #[test]
266 fn test_loading_state_default() {
267 assert_eq!(LoadingState::default(), LoadingState::Idle);
268 }
269
270 #[test]
271 fn test_loading_state_copy() {
272 let state = LoadingState::Loading;
273 let copied = state;
274 assert_eq!(state, copied);
275 }
276
277 #[test]
278 fn test_loading_state_debug() {
279 let debug_str = format!("{:?}", LoadingState::Loading);
280 assert_eq!(debug_str, "Loading");
281 }
282
283 #[test]
284 fn test_loading_state_all_variants() {
285 let variants = [
286 LoadingState::Idle,
287 LoadingState::Loading,
288 LoadingState::Loaded,
289 LoadingState::Failed,
290 ];
291 for (i, v1) in variants.iter().enumerate() {
292 for (j, v2) in variants.iter().enumerate() {
293 if i == j {
294 assert_eq!(v1, v2);
295 } else {
296 assert_ne!(v1, v2);
297 }
298 }
299 }
300 }
301}