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}