Skip to main content

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}