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 location (shared across all ecosystems).
77///
78/// Covers the union of all source types across Cargo, npm, PyPI, Go,
79/// Dart, Bundler, Maven, and Gradle ecosystems.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum DependencySource {
83 /// Default package registry (crates.io, npm, PyPI, pub.dev, rubygems.org, Maven Central).
84 Registry,
85
86 /// Git repository dependency.
87 Git {
88 url: String,
89 /// Git ref: commit SHA, tag, or branch name (ecosystem-specific semantics).
90 rev: Option<String>,
91 },
92
93 /// Local filesystem path dependency.
94 Path { path: String },
95
96 /// Direct URL to artifact (PyPI wheels, npm tarballs).
97 Url { url: String },
98
99 /// SDK-provided dependency (Dart: `sdk: flutter`).
100 Sdk { sdk: String },
101
102 /// Workspace-inherited dependency (Cargo: `workspace = true`).
103 Workspace,
104
105 /// Custom/alternative registry (Bundler custom sources, private registries).
106 CustomRegistry { url: String },
107}
108
109impl DependencySource {
110 /// Returns true if this dependency comes from any registry (default or custom).
111 ///
112 /// Registry dependencies support version fetching and update checks.
113 /// Git, Path, Url, Sdk, and Workspace dependencies do not.
114 pub fn is_registry(&self) -> bool {
115 matches!(self, Self::Registry | Self::CustomRegistry { .. })
116 }
117
118 /// Returns true if version resolution is possible for this source.
119 ///
120 /// Currently equivalent to `is_registry()`, but semantically distinct
121 /// for future extensibility (e.g., Git tags could support version listing).
122 pub fn is_version_resolvable(&self) -> bool {
123 self.is_registry()
124 }
125}
126
127/// Loading state for registry data fetching.
128///
129/// Tracks the current state of background registry operations to provide
130/// user feedback about data availability.
131///
132/// # State Transitions
133///
134/// Complete state machine diagram showing all valid transitions:
135///
136/// ```text
137/// ┌─────┐
138/// │Idle │ (Initial state: no data loaded, not loading)
139/// └──┬──┘
140/// │
141/// │ didOpen/didChange
142/// │ (start fetching)
143/// ▼
144/// ┌────────┐
145/// │Loading │ (Fetching registry data)
146/// └───┬────┘
147/// │
148/// ├─────── Success ──────┐
149/// │ ▼
150/// │ ┌────────┐
151/// │ │Loaded │ (Data cached and ready)
152/// │ └───┬────┘
153/// │ │
154/// │ │ didChange/refresh
155/// │ │ (re-fetch)
156/// │ │
157/// │ ▼
158/// │ ┌────────┐
159/// │ │Loading │
160/// │ └────────┘
161/// │
162/// └─────── Error ─────────┐
163/// ▼
164/// ┌────────┐
165/// │Failed │ (Fetch failed, old cache may exist)
166/// └───┬────┘
167/// │
168/// │ didChange/retry
169/// │ (try again)
170/// │
171/// ▼
172/// ┌────────┐
173/// │Loading │
174/// └────────┘
175/// ```
176///
177/// # Key Behaviors
178///
179/// - **Idle**: Initial state when no data has been fetched yet
180/// - **Loading**: Actively fetching from registry (may show loading indicator)
181/// - **Loaded**: Successfully fetched and cached data
182/// - **Failed**: Network/registry error occurred (falls back to old cache if available)
183///
184/// # Thread Safety
185///
186/// This enum is `Copy` for efficient passing across thread boundaries in async contexts.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub enum LoadingState {
189 /// No data loaded, not currently loading
190 #[default]
191 Idle,
192 /// Currently fetching registry data
193 Loading,
194 /// Data fetched and cached
195 Loaded,
196 /// Fetch failed (old cached data may still be available)
197 Failed,
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_dependency_source_registry() {
206 let source = DependencySource::Registry;
207 assert_eq!(source, DependencySource::Registry);
208 assert!(source.is_registry());
209 assert!(source.is_version_resolvable());
210 }
211
212 #[test]
213 fn test_dependency_source_git() {
214 let source = DependencySource::Git {
215 url: "https://github.com/user/repo".into(),
216 rev: Some("main".into()),
217 };
218
219 assert!(!source.is_registry());
220 assert!(!source.is_version_resolvable());
221
222 match source {
223 DependencySource::Git { url, rev } => {
224 assert_eq!(url, "https://github.com/user/repo");
225 assert_eq!(rev, Some("main".into()));
226 }
227 _ => panic!("Expected Git source"),
228 }
229 }
230
231 #[test]
232 fn test_dependency_source_git_no_rev() {
233 let source = DependencySource::Git {
234 url: "https://github.com/user/repo".into(),
235 rev: None,
236 };
237
238 match source {
239 DependencySource::Git { url, rev } => {
240 assert_eq!(url, "https://github.com/user/repo");
241 assert!(rev.is_none());
242 }
243 _ => panic!("Expected Git source"),
244 }
245 }
246
247 #[test]
248 fn test_dependency_source_path() {
249 let source = DependencySource::Path {
250 path: "../local-crate".into(),
251 };
252
253 assert!(!source.is_registry());
254
255 match source {
256 DependencySource::Path { path } => {
257 assert_eq!(path, "../local-crate");
258 }
259 _ => panic!("Expected Path source"),
260 }
261 }
262
263 #[test]
264 fn test_dependency_source_url() {
265 let source = DependencySource::Url {
266 url: "https://example.com/package.whl".into(),
267 };
268 assert!(!source.is_registry());
269 assert!(!source.is_version_resolvable());
270 }
271
272 #[test]
273 fn test_dependency_source_sdk() {
274 let source = DependencySource::Sdk {
275 sdk: "flutter".into(),
276 };
277 assert!(!source.is_registry());
278 }
279
280 #[test]
281 fn test_dependency_source_workspace() {
282 let source = DependencySource::Workspace;
283 assert!(!source.is_registry());
284 assert!(!source.is_version_resolvable());
285 }
286
287 #[test]
288 fn test_dependency_source_custom_registry() {
289 let source = DependencySource::CustomRegistry {
290 url: "https://gems.example.com".into(),
291 };
292 assert!(source.is_registry());
293 assert!(source.is_version_resolvable());
294 }
295
296 #[test]
297 fn test_dependency_source_clone() {
298 let source1 = DependencySource::Git {
299 url: "https://example.com/repo".into(),
300 rev: Some("v1.0".into()),
301 };
302 let source2 = source1.clone();
303
304 assert_eq!(source1, source2);
305 }
306
307 #[test]
308 fn test_dependency_source_equality() {
309 let reg1 = DependencySource::Registry;
310 let reg2 = DependencySource::Registry;
311 assert_eq!(reg1, reg2);
312
313 let git1 = DependencySource::Git {
314 url: "https://example.com".into(),
315 rev: None,
316 };
317 let git2 = DependencySource::Git {
318 url: "https://example.com".into(),
319 rev: None,
320 };
321 assert_eq!(git1, git2);
322
323 let git3 = DependencySource::Git {
324 url: "https://different.com".into(),
325 rev: None,
326 };
327 assert_ne!(git1, git3);
328 }
329
330 #[test]
331 fn test_dependency_source_debug() {
332 let source = DependencySource::Registry;
333 let debug = format!("{:?}", source);
334 assert_eq!(debug, "Registry");
335
336 let git = DependencySource::Git {
337 url: "https://example.com".into(),
338 rev: Some("main".into()),
339 };
340 let git_debug = format!("{:?}", git);
341 assert!(git_debug.contains("https://example.com"));
342 assert!(git_debug.contains("main"));
343 }
344
345 #[test]
346 fn test_loading_state_default() {
347 assert_eq!(LoadingState::default(), LoadingState::Idle);
348 }
349
350 #[test]
351 fn test_loading_state_copy() {
352 let state = LoadingState::Loading;
353 let copied = state;
354 assert_eq!(state, copied);
355 }
356
357 #[test]
358 fn test_loading_state_debug() {
359 let debug_str = format!("{:?}", LoadingState::Loading);
360 assert_eq!(debug_str, "Loading");
361 }
362
363 #[test]
364 fn test_loading_state_all_variants() {
365 let variants = [
366 LoadingState::Idle,
367 LoadingState::Loading,
368 LoadingState::Loaded,
369 LoadingState::Failed,
370 ];
371 for (i, v1) in variants.iter().enumerate() {
372 for (j, v2) in variants.iter().enumerate() {
373 if i == j {
374 assert_eq!(v1, v2);
375 } else {
376 assert_ne!(v1, v2);
377 }
378 }
379 }
380 }
381}