deps_lsp/document/
state.rs

1use dashmap::DashMap;
2use deps_core::HttpCache;
3use deps_core::lockfile::LockFileCache;
4use deps_core::{EcosystemRegistry, ParseResult};
5use std::collections::HashMap;
6
7#[cfg(feature = "cargo")]
8use deps_cargo::{CargoVersion, ParsedDependency};
9#[cfg(feature = "go")]
10use deps_go::{GoDependency, GoVersion};
11#[cfg(feature = "npm")]
12use deps_npm::{NpmDependency, NpmVersion};
13#[cfg(feature = "pypi")]
14use deps_pypi::{PypiDependency, PypiVersion};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::task::JoinHandle;
18use tower_lsp_server::ls_types::Uri;
19
20/// Unified dependency enum for multi-ecosystem support.
21///
22/// Wraps ecosystem-specific dependency types to allow storing
23/// dependencies from different ecosystems in the same document state.
24#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub enum UnifiedDependency {
27    #[cfg(feature = "cargo")]
28    Cargo(ParsedDependency),
29    #[cfg(feature = "npm")]
30    Npm(NpmDependency),
31    #[cfg(feature = "pypi")]
32    Pypi(PypiDependency),
33    #[cfg(feature = "go")]
34    Go(GoDependency),
35}
36
37impl UnifiedDependency {
38    /// Returns the dependency name.
39    #[allow(unreachable_patterns)]
40    pub fn name(&self) -> &str {
41        match self {
42            #[cfg(feature = "cargo")]
43            Self::Cargo(dep) => &dep.name,
44            #[cfg(feature = "npm")]
45            Self::Npm(dep) => &dep.name,
46            #[cfg(feature = "pypi")]
47            Self::Pypi(dep) => &dep.name,
48            #[cfg(feature = "go")]
49            Self::Go(dep) => &dep.module_path,
50            _ => unreachable!("no ecosystem features enabled"),
51        }
52    }
53
54    /// Returns the name range for LSP operations.
55    #[allow(unreachable_patterns)]
56    pub fn name_range(&self) -> tower_lsp_server::ls_types::Range {
57        match self {
58            #[cfg(feature = "cargo")]
59            Self::Cargo(dep) => dep.name_range,
60            #[cfg(feature = "npm")]
61            Self::Npm(dep) => dep.name_range,
62            #[cfg(feature = "pypi")]
63            Self::Pypi(dep) => dep.name_range,
64            #[cfg(feature = "go")]
65            Self::Go(dep) => dep.module_path_range,
66            _ => unreachable!("no ecosystem features enabled"),
67        }
68    }
69
70    /// Returns the version requirement string if present.
71    #[allow(unreachable_patterns)]
72    pub fn version_req(&self) -> Option<&str> {
73        match self {
74            #[cfg(feature = "cargo")]
75            Self::Cargo(dep) => dep.version_req.as_deref(),
76            #[cfg(feature = "npm")]
77            Self::Npm(dep) => dep.version_req.as_deref(),
78            #[cfg(feature = "pypi")]
79            Self::Pypi(dep) => dep.version_req.as_deref(),
80            #[cfg(feature = "go")]
81            Self::Go(dep) => dep.version.as_deref(),
82            _ => unreachable!("no ecosystem features enabled"),
83        }
84    }
85
86    /// Returns the version range for LSP operations if present.
87    #[allow(unreachable_patterns)]
88    pub fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
89        match self {
90            #[cfg(feature = "cargo")]
91            Self::Cargo(dep) => dep.version_range,
92            #[cfg(feature = "npm")]
93            Self::Npm(dep) => dep.version_range,
94            #[cfg(feature = "pypi")]
95            Self::Pypi(dep) => dep.version_range,
96            #[cfg(feature = "go")]
97            Self::Go(dep) => dep.version_range,
98            _ => unreachable!("no ecosystem features enabled"),
99        }
100    }
101
102    /// Returns true if this is a registry dependency (not Git/Path).
103    #[allow(unreachable_patterns)]
104    pub fn is_registry(&self) -> bool {
105        match self {
106            #[cfg(feature = "cargo")]
107            Self::Cargo(dep) => {
108                matches!(dep.source, deps_cargo::DependencySource::Registry)
109            }
110            #[cfg(feature = "npm")]
111            Self::Npm(_) => true,
112            #[cfg(feature = "pypi")]
113            Self::Pypi(dep) => {
114                matches!(dep.source, deps_pypi::PypiDependencySource::PyPI)
115            }
116            #[cfg(feature = "go")]
117            Self::Go(_) => true,
118            _ => unreachable!("no ecosystem features enabled"),
119        }
120    }
121}
122
123/// Unified version information enum for multi-ecosystem support.
124///
125/// Wraps ecosystem-specific version types.
126#[derive(Debug, Clone)]
127#[non_exhaustive]
128pub enum UnifiedVersion {
129    #[cfg(feature = "cargo")]
130    Cargo(CargoVersion),
131    #[cfg(feature = "npm")]
132    Npm(NpmVersion),
133    #[cfg(feature = "pypi")]
134    Pypi(PypiVersion),
135    #[cfg(feature = "go")]
136    Go(GoVersion),
137}
138
139impl UnifiedVersion {
140    /// Returns the version number as a string.
141    #[allow(unreachable_patterns)]
142    pub fn version_string(&self) -> &str {
143        match self {
144            #[cfg(feature = "cargo")]
145            Self::Cargo(v) => &v.num,
146            #[cfg(feature = "npm")]
147            Self::Npm(v) => &v.version,
148            #[cfg(feature = "pypi")]
149            Self::Pypi(v) => &v.version,
150            #[cfg(feature = "go")]
151            Self::Go(v) => &v.version,
152            _ => unreachable!("no ecosystem features enabled"),
153        }
154    }
155
156    /// Returns true if this version is yanked/deprecated.
157    #[allow(unreachable_patterns)]
158    pub fn is_yanked(&self) -> bool {
159        match self {
160            #[cfg(feature = "cargo")]
161            Self::Cargo(v) => v.yanked,
162            #[cfg(feature = "npm")]
163            Self::Npm(v) => v.deprecated,
164            #[cfg(feature = "pypi")]
165            Self::Pypi(v) => v.yanked,
166            #[cfg(feature = "go")]
167            Self::Go(v) => v.retracted,
168            _ => unreachable!("no ecosystem features enabled"),
169        }
170    }
171}
172
173// Implement helper traits from deps-core for generic handler support
174impl deps_core::VersionStringGetter for UnifiedVersion {
175    fn version_string(&self) -> &str {
176        self.version_string()
177    }
178}
179
180impl deps_core::YankedChecker for UnifiedVersion {
181    fn is_yanked(&self) -> bool {
182        self.is_yanked()
183    }
184}
185
186// Re-export LoadingState from deps-core for convenience
187pub use deps_core::LoadingState;
188
189/// Package ecosystem type.
190///
191/// Identifies which package manager and manifest file format
192/// a document belongs to. Used for routing LSP operations to
193/// the appropriate parser and registry.
194///
195/// # Examples
196///
197/// ```
198/// use deps_lsp::document::Ecosystem;
199///
200/// let cargo = Ecosystem::from_filename("Cargo.toml");
201/// assert_eq!(cargo, Some(Ecosystem::Cargo));
202///
203/// let npm = Ecosystem::from_filename("package.json");
204/// assert_eq!(npm, Some(Ecosystem::Npm));
205///
206/// let pypi = Ecosystem::from_filename("pyproject.toml");
207/// assert_eq!(pypi, Some(Ecosystem::Pypi));
208///
209/// let unknown = Ecosystem::from_filename("requirements.txt");
210/// assert_eq!(unknown, None);
211/// ```
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213#[non_exhaustive]
214pub enum Ecosystem {
215    /// Rust Cargo ecosystem (Cargo.toml)
216    Cargo,
217    /// JavaScript/TypeScript npm ecosystem (package.json)
218    Npm,
219    /// Python PyPI ecosystem (pyproject.toml)
220    Pypi,
221    /// Go modules ecosystem (go.mod)
222    Go,
223}
224
225impl Ecosystem {
226    /// Detects ecosystem from filename.
227    ///
228    /// Returns `Some(Ecosystem)` if the filename matches a known manifest file,
229    /// or `None` if the file is not recognized.
230    pub fn from_filename(filename: &str) -> Option<Self> {
231        match filename {
232            "Cargo.toml" => Some(Self::Cargo),
233            "package.json" => Some(Self::Npm),
234            "pyproject.toml" => Some(Self::Pypi),
235            "go.mod" => Some(Self::Go),
236            _ => None,
237        }
238    }
239
240    /// Detects ecosystem from full URI path.
241    ///
242    /// Extracts the filename from the URI and checks if it matches a known manifest.
243    pub fn from_uri(uri: &Uri) -> Option<Self> {
244        let path = uri.path();
245        let filename = path.as_str().split('/').next_back()?;
246        Self::from_filename(filename)
247    }
248}
249
250/// State for a single open document.
251///
252/// Stores the document content, parsed dependency information, and cached
253/// version data for a single file. The state is updated when the document
254/// changes or when version information is fetched from the registry.
255///
256/// Supports multiple package ecosystems (Cargo, npm) with unified dependency
257/// and version storage.
258///
259/// # Examples
260///
261/// ```no_run
262/// use deps_lsp::document::{DocumentState, Ecosystem, UnifiedDependency};
263/// use deps_lsp::ParsedDependency;
264/// use deps_cargo::{DependencySection, DependencySource};
265/// use tower_lsp_server::ls_types::{Position, Range};
266///
267/// let dep = ParsedDependency {
268///     name: "serde".into(),
269///     name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
270///     version_req: Some("1.0".into()),
271///     version_range: Some(Range::new(Position::new(0, 8), Position::new(0, 12))),
272///     features: vec![],
273///     features_range: None,
274///     source: DependencySource::Registry,
275///     workspace_inherited: false,
276///     section: DependencySection::Dependencies,
277/// };
278///
279/// let state = DocumentState::new(
280///     Ecosystem::Cargo,
281///     "[dependencies]\nserde = \"1.0\"".into(),
282///     vec![UnifiedDependency::Cargo(dep)],
283/// );
284///
285/// assert!(state.versions.is_empty());
286/// assert_eq!(state.dependencies.len(), 1);
287/// ```
288pub struct DocumentState {
289    /// Package ecosystem type (deprecated, use ecosystem_id)
290    pub ecosystem: Ecosystem,
291    /// Ecosystem identifier ("cargo", "npm", "pypi")
292    pub ecosystem_id: &'static str,
293    /// Original document content
294    pub content: String,
295    /// Parsed dependencies with positions (legacy)
296    pub dependencies: Vec<UnifiedDependency>,
297    /// Parsed result as trait object (new architecture)
298    /// Note: This is not cloned when DocumentState is cloned
299    #[allow(dead_code)]
300    parse_result: Option<Box<dyn ParseResult>>,
301    /// Cached latest version information from registry
302    pub versions: HashMap<String, UnifiedVersion>,
303    /// Simplified cached versions (just strings) for new architecture
304    pub cached_versions: HashMap<String, String>,
305    /// Resolved versions from lock file
306    pub resolved_versions: HashMap<String, String>,
307    /// Last successful parse time
308    pub parsed_at: Instant,
309    /// Current loading state for registry data
310    pub loading_state: LoadingState,
311    /// When the current loading operation started (for timeout/metrics)
312    pub loading_started_at: Option<Instant>,
313}
314
315impl Clone for DocumentState {
316    fn clone(&self) -> Self {
317        Self {
318            ecosystem: self.ecosystem,
319            ecosystem_id: self.ecosystem_id,
320            content: self.content.clone(),
321            dependencies: self.dependencies.clone(),
322            parse_result: None, // Don't clone trait object
323            versions: self.versions.clone(),
324            cached_versions: self.cached_versions.clone(),
325            resolved_versions: self.resolved_versions.clone(),
326            parsed_at: self.parsed_at,
327            loading_state: self.loading_state,
328            // Note: Instant is Copy. Clones share the same loading start time.
329            loading_started_at: self.loading_started_at,
330        }
331    }
332}
333
334/// Tracks recent cold start attempts per URI to prevent DOS.
335///
336/// Uses rate limiting with a configurable minimum interval between
337/// cold start attempts for the same URI. This prevents malicious or
338/// buggy clients from overwhelming the server with rapid file loading
339/// requests.
340///
341/// # Examples
342///
343/// ```
344/// use deps_lsp::document::ColdStartLimiter;
345/// use tower_lsp_server::ls_types::Uri;
346/// use std::time::Duration;
347///
348/// let limiter = ColdStartLimiter::new(Duration::from_millis(100));
349/// let uri = Uri::from_file_path("/test.toml").unwrap();
350///
351/// assert!(limiter.allow_cold_start(&uri));
352/// assert!(!limiter.allow_cold_start(&uri)); // Rate limited
353/// ```
354#[derive(Debug)]
355pub struct ColdStartLimiter {
356    /// Maps URI to last cold start attempt time.
357    last_attempts: DashMap<Uri, Instant>,
358    /// Minimum interval between cold start attempts for the same URI.
359    min_interval: Duration,
360}
361
362impl ColdStartLimiter {
363    /// Creates a new cold start limiter with the specified minimum interval.
364    pub fn new(min_interval: Duration) -> Self {
365        Self {
366            last_attempts: DashMap::new(),
367            min_interval,
368        }
369    }
370
371    /// Returns true if cold start is allowed, false if rate limited.
372    ///
373    /// Updates the last attempt time if the cold start is allowed.
374    pub fn allow_cold_start(&self, uri: &Uri) -> bool {
375        let now = Instant::now();
376
377        // Check last attempt time
378        if let Some(mut entry) = self.last_attempts.get_mut(uri) {
379            let elapsed = now.duration_since(*entry);
380            if elapsed < self.min_interval {
381                let retry_after = self.min_interval.checked_sub(elapsed).unwrap();
382                tracing::warn!(
383                    "Cold start rate limited for {:?} (retry after {:?})",
384                    uri,
385                    retry_after
386                );
387                return false;
388            }
389            *entry = now;
390        } else {
391            self.last_attempts.insert(uri.clone(), now);
392        }
393
394        true
395    }
396
397    /// Cleans up old entries periodically.
398    ///
399    /// Removes entries older than `max_age` to prevent unbounded memory growth.
400    /// Should be called from a background task.
401    pub fn cleanup_old_entries(&self, max_age: Duration) {
402        let now = Instant::now();
403        self.last_attempts
404            .retain(|_, instant| now.duration_since(*instant) < max_age);
405    }
406
407    /// Returns the number of tracked URIs.
408    #[cfg(test)]
409    pub fn tracked_count(&self) -> usize {
410        self.last_attempts.len()
411    }
412}
413
414impl std::fmt::Debug for DocumentState {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        f.debug_struct("DocumentState")
417            .field("ecosystem", &self.ecosystem)
418            .field("ecosystem_id", &self.ecosystem_id)
419            .field("content_len", &self.content.len())
420            .field("dependencies_count", &self.dependencies.len())
421            .field("has_parse_result", &self.parse_result.is_some())
422            .field("versions_count", &self.versions.len())
423            .field("cached_versions_count", &self.cached_versions.len())
424            .field("resolved_versions_count", &self.resolved_versions.len())
425            .field("parsed_at", &self.parsed_at)
426            .field("loading_state", &self.loading_state)
427            .field("loading_started_at", &self.loading_started_at)
428            .finish()
429    }
430}
431
432impl DocumentState {
433    /// Creates a new document state (legacy constructor).
434    ///
435    /// Initializes with the given ecosystem, content, and parsed dependencies.
436    /// Version information starts empty and is populated asynchronously.
437    pub fn new(
438        ecosystem: Ecosystem,
439        content: String,
440        dependencies: Vec<UnifiedDependency>,
441    ) -> Self {
442        let ecosystem_id = match ecosystem {
443            Ecosystem::Cargo => "cargo",
444            Ecosystem::Npm => "npm",
445            Ecosystem::Pypi => "pypi",
446            Ecosystem::Go => "go",
447        };
448
449        Self {
450            ecosystem,
451            ecosystem_id,
452            content,
453            dependencies,
454            parse_result: None,
455            versions: HashMap::new(),
456            cached_versions: HashMap::new(),
457            resolved_versions: HashMap::new(),
458            parsed_at: Instant::now(),
459            loading_state: LoadingState::Idle,
460            loading_started_at: None,
461        }
462    }
463
464    /// Creates a new document state using trait objects (new architecture).
465    ///
466    /// This is the preferred constructor for Phase 3+ implementations.
467    pub fn new_from_parse_result(
468        ecosystem_id: &'static str,
469        content: String,
470        parse_result: Box<dyn ParseResult>,
471    ) -> Self {
472        let ecosystem = match ecosystem_id {
473            "cargo" => Ecosystem::Cargo,
474            "npm" => Ecosystem::Npm,
475            "pypi" => Ecosystem::Pypi,
476            "go" => Ecosystem::Go,
477            _ => Ecosystem::Cargo, // Default fallback
478        };
479
480        Self {
481            ecosystem,
482            ecosystem_id,
483            content,
484            dependencies: vec![],
485            parse_result: Some(parse_result),
486            versions: HashMap::new(),
487            cached_versions: HashMap::new(),
488            resolved_versions: HashMap::new(),
489            parsed_at: Instant::now(),
490            loading_state: LoadingState::Idle,
491            loading_started_at: None,
492        }
493    }
494
495    /// Creates a new document state without a parse result.
496    ///
497    /// Used when parsing fails but the document should still be stored
498    /// to enable fallback completion and other LSP features.
499    pub fn new_without_parse_result(ecosystem_id: &'static str, content: String) -> Self {
500        let ecosystem = match ecosystem_id {
501            "cargo" => Ecosystem::Cargo,
502            "npm" => Ecosystem::Npm,
503            "pypi" => Ecosystem::Pypi,
504            "go" => Ecosystem::Go,
505            _ => Ecosystem::Cargo, // Default fallback
506        };
507
508        Self {
509            ecosystem,
510            ecosystem_id,
511            content,
512            dependencies: vec![],
513            parse_result: None,
514            versions: HashMap::new(),
515            cached_versions: HashMap::new(),
516            resolved_versions: HashMap::new(),
517            parsed_at: Instant::now(),
518            loading_state: LoadingState::Idle,
519            loading_started_at: None,
520        }
521    }
522
523    /// Gets a reference to the parse result if available.
524    pub fn parse_result(&self) -> Option<&dyn ParseResult> {
525        self.parse_result.as_ref().map(std::convert::AsRef::as_ref)
526    }
527
528    /// Updates the cached latest version information for dependencies.
529    pub fn update_versions(&mut self, versions: HashMap<String, UnifiedVersion>) {
530        self.versions = versions;
531    }
532
533    /// Updates the simplified cached versions (new architecture).
534    pub fn update_cached_versions(&mut self, versions: HashMap<String, String>) {
535        self.cached_versions = versions;
536    }
537
538    /// Updates the resolved versions from lock file.
539    pub fn update_resolved_versions(&mut self, versions: HashMap<String, String>) {
540        self.resolved_versions = versions;
541    }
542
543    /// Mark document as loading registry data.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use deps_lsp::document::DocumentState;
549    ///
550    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
551    /// doc.set_loading();
552    /// assert!(doc.loading_started_at.is_some());
553    /// ```
554    ///
555    /// # Thread Safety
556    ///
557    /// This method requires exclusive access (`&mut self`). When used with
558    /// `DashMap::get_mut()`, thread safety is guaranteed by the lock.
559    /// Calling while already `Loading` resets the timer.
560    pub fn set_loading(&mut self) {
561        self.loading_state = LoadingState::Loading;
562        self.loading_started_at = Some(Instant::now());
563    }
564
565    /// Mark document as loaded with fresh data.
566    ///
567    /// # Examples
568    ///
569    /// ```
570    /// use deps_lsp::document::{DocumentState, LoadingState};
571    ///
572    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
573    /// doc.set_loading();
574    /// doc.set_loaded();
575    /// assert_eq!(doc.loading_state, LoadingState::Loaded);
576    /// assert!(doc.loading_started_at.is_none());
577    /// ```
578    pub fn set_loaded(&mut self) {
579        self.loading_state = LoadingState::Loaded;
580        self.loading_started_at = None;
581    }
582
583    /// Mark document as failed to load (keeps old cached data).
584    ///
585    /// # Examples
586    ///
587    /// ```
588    /// use deps_lsp::document::{DocumentState, LoadingState};
589    ///
590    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
591    /// doc.set_loading();
592    /// doc.set_failed();
593    /// assert_eq!(doc.loading_state, LoadingState::Failed);
594    /// assert!(doc.loading_started_at.is_none());
595    /// ```
596    pub fn set_failed(&mut self) {
597        self.loading_state = LoadingState::Failed;
598        self.loading_started_at = None;
599    }
600
601    /// Get current loading duration if loading.
602    ///
603    /// Returns `None` if not currently loading, or `Some(Duration)` representing
604    /// how long the current loading operation has been running.
605    ///
606    /// # Examples
607    ///
608    /// ```
609    /// use deps_lsp::document::DocumentState;
610    ///
611    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
612    /// assert!(doc.loading_duration().is_none());
613    ///
614    /// doc.set_loading();
615    /// assert!(doc.loading_duration().is_some());
616    /// ```
617    #[must_use]
618    pub fn loading_duration(&self) -> Option<Duration> {
619        self.loading_started_at
620            .map(|start| Instant::now().duration_since(start))
621    }
622}
623
624/// Global LSP server state.
625///
626/// Manages all open documents, HTTP cache, lock file cache, and background
627/// tasks for the server. This state is shared across all LSP handlers via
628/// `Arc` and uses concurrent data structures (`DashMap`, `RwLock`) for
629/// thread-safe access.
630///
631/// # Examples
632///
633/// ```
634/// use deps_lsp::document::ServerState;
635/// use tower_lsp_server::ls_types::Uri;
636///
637/// let state = ServerState::new();
638/// assert_eq!(state.document_count(), 0);
639/// ```
640pub struct ServerState {
641    /// Open documents by URI
642    pub documents: DashMap<Uri, DocumentState>,
643    /// HTTP cache for registry requests
644    pub cache: Arc<HttpCache>,
645    /// Lock file cache for parsed lock files
646    pub lockfile_cache: Arc<LockFileCache>,
647    /// Ecosystem registry for trait-based architecture
648    pub ecosystem_registry: Arc<EcosystemRegistry>,
649    /// Cold start rate limiter
650    pub cold_start_limiter: ColdStartLimiter,
651    /// Background task handles
652    tasks: tokio::sync::RwLock<HashMap<Uri, JoinHandle<()>>>,
653}
654
655impl ServerState {
656    /// Creates a new server state with default configuration.
657    pub fn new() -> Self {
658        let cache = Arc::new(HttpCache::new());
659        let lockfile_cache = Arc::new(LockFileCache::new());
660        let ecosystem_registry = Arc::new(EcosystemRegistry::new());
661
662        // Register ecosystems based on enabled features
663        crate::register_ecosystems(&ecosystem_registry, Arc::clone(&cache));
664
665        // Create cold start limiter with default 100ms interval (10 req/sec per URI)
666        let cold_start_limiter = ColdStartLimiter::new(Duration::from_millis(100));
667
668        Self {
669            documents: DashMap::new(),
670            cache,
671            lockfile_cache,
672            ecosystem_registry,
673            cold_start_limiter,
674            tasks: tokio::sync::RwLock::new(HashMap::new()),
675        }
676    }
677
678    /// Retrieves document state by URI.
679    ///
680    /// Returns a read-only reference to the document state if it exists.
681    /// The reference holds a lock on the internal map, so it should be
682    /// dropped as soon as possible.
683    pub fn get_document(
684        &self,
685        uri: &Uri,
686    ) -> Option<dashmap::mapref::one::Ref<'_, Uri, DocumentState>> {
687        self.documents.get(uri)
688    }
689
690    /// Retrieves a cloned copy of document state by URI.
691    ///
692    /// This method clones the document state immediately and releases
693    /// the DashMap lock, allowing concurrent access to the map while
694    /// the document is being processed. Use this in hot paths where
695    /// async operations are performed with the document data.
696    ///
697    /// # Performance
698    ///
699    /// Cloning `DocumentState` is relatively cheap as it only clones
700    /// `String` and `HashMap` metadata, not the underlying parse result
701    /// trait object.
702    ///
703    /// # Examples
704    ///
705    /// ```no_run
706    /// # use deps_lsp::document::ServerState;
707    /// # use tower_lsp_server::ls_types::Uri;
708    /// # async fn example(state: &ServerState, uri: &Uri) {
709    /// // Lock released immediately after clone
710    /// let doc = state.get_document_clone(uri);
711    ///
712    /// if let Some(doc) = doc {
713    ///     // Perform async operations without holding lock
714    ///     let result = process_async(&doc).await;
715    /// }
716    /// # }
717    /// # async fn process_async(doc: &deps_lsp::document::DocumentState) {}
718    /// ```
719    pub fn get_document_clone(&self, uri: &Uri) -> Option<DocumentState> {
720        self.documents.get(uri).map(|doc| doc.clone())
721    }
722
723    /// Updates or inserts document state.
724    ///
725    /// If a document already exists at the given URI, it is replaced.
726    /// Otherwise, a new entry is created.
727    pub fn update_document(&self, uri: Uri, state: DocumentState) {
728        self.documents.insert(uri, state);
729    }
730
731    /// Removes document state and returns the removed entry.
732    ///
733    /// Returns `None` if no document exists at the given URI.
734    pub fn remove_document(&self, uri: &Uri) -> Option<(Uri, DocumentState)> {
735        self.documents.remove(uri)
736    }
737
738    /// Spawns a background task for a document.
739    ///
740    /// If a task already exists for the given URI, it is aborted before
741    /// the new task is registered. This ensures only one background task
742    /// runs per document.
743    ///
744    /// Typical use case: fetching version data asynchronously after
745    /// document open or change.
746    pub async fn spawn_background_task(&self, uri: Uri, task: JoinHandle<()>) {
747        let mut tasks = self.tasks.write().await;
748
749        // Cancel existing task if any
750        if let Some(old_task) = tasks.remove(&uri) {
751            old_task.abort();
752        }
753
754        tasks.insert(uri, task);
755    }
756
757    /// Cancels the background task for a document.
758    ///
759    /// If no task exists, this is a no-op.
760    pub async fn cancel_background_task(&self, uri: &Uri) {
761        let mut tasks = self.tasks.write().await;
762        if let Some(task) = tasks.remove(uri) {
763            task.abort();
764        }
765    }
766
767    /// Returns the number of open documents.
768    pub fn document_count(&self) -> usize {
769        self.documents.len()
770    }
771}
772
773impl Default for ServerState {
774    fn default() -> Self {
775        Self::new()
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    // =========================================================================
784    // Generic tests (no feature flag required)
785    // =========================================================================
786
787    // =========================================================================
788    // LoadingState tests
789    // =========================================================================
790
791    mod loading_state_tests {
792        use super::*;
793
794        #[test]
795        fn test_loading_state_default() {
796            let state = LoadingState::default();
797            assert_eq!(state, LoadingState::Idle);
798        }
799
800        #[test]
801        fn test_loading_state_transitions() {
802            use std::time::Duration;
803
804            let content = "[dependencies]\nserde = \"1.0\"".to_string();
805            let mut doc = DocumentState::new_without_parse_result("cargo", content);
806
807            // Initial state
808            assert_eq!(doc.loading_state, LoadingState::Idle);
809            assert!(doc.loading_started_at.is_none());
810
811            // Transition to Loading
812            doc.set_loading();
813            assert_eq!(doc.loading_state, LoadingState::Loading);
814            assert!(doc.loading_started_at.is_some());
815
816            // Small sleep to ensure duration is non-zero
817            std::thread::sleep(Duration::from_millis(10));
818
819            // Check loading duration
820            let duration = doc.loading_duration();
821            assert!(duration.is_some());
822            assert!(duration.unwrap() >= Duration::from_millis(10));
823
824            // Transition to Loaded
825            doc.set_loaded();
826            assert_eq!(doc.loading_state, LoadingState::Loaded);
827            assert!(doc.loading_started_at.is_none());
828            assert!(doc.loading_duration().is_none());
829        }
830
831        #[test]
832        fn test_loading_state_failed_transition() {
833            let content = "[dependencies]\nserde = \"1.0\"".to_string();
834            let mut doc = DocumentState::new_without_parse_result("cargo", content);
835
836            doc.set_loading();
837            assert_eq!(doc.loading_state, LoadingState::Loading);
838
839            doc.set_failed();
840            assert_eq!(doc.loading_state, LoadingState::Failed);
841            assert!(doc.loading_started_at.is_none());
842        }
843
844        #[test]
845        fn test_loading_state_clone() {
846            let content = "[dependencies]\nserde = \"1.0\"".to_string();
847            let mut doc = DocumentState::new_without_parse_result("cargo", content);
848
849            doc.set_loading();
850            let cloned = doc.clone();
851
852            assert_eq!(cloned.loading_state, LoadingState::Loading);
853            assert!(cloned.loading_started_at.is_some());
854        }
855
856        #[test]
857        fn test_loading_state_debug() {
858            let content = "[dependencies]\nserde = \"1.0\"".to_string();
859            let mut doc = DocumentState::new_without_parse_result("cargo", content);
860            doc.set_loading();
861
862            let debug_str = format!("{:?}", doc);
863            assert!(debug_str.contains("loading_state"));
864            assert!(debug_str.contains("Loading"));
865        }
866
867        #[test]
868        fn test_loading_duration_none_when_idle() {
869            let content = "[dependencies]\nserde = \"1.0\"".to_string();
870            let doc = DocumentState::new_without_parse_result("cargo", content);
871
872            assert_eq!(doc.loading_state, LoadingState::Idle);
873            assert!(doc.loading_duration().is_none());
874        }
875
876        #[test]
877        fn test_loading_state_equality() {
878            assert_eq!(LoadingState::Idle, LoadingState::Idle);
879            assert_eq!(LoadingState::Loading, LoadingState::Loading);
880            assert_eq!(LoadingState::Loaded, LoadingState::Loaded);
881            assert_eq!(LoadingState::Failed, LoadingState::Failed);
882
883            assert_ne!(LoadingState::Idle, LoadingState::Loading);
884            assert_ne!(LoadingState::Loading, LoadingState::Loaded);
885        }
886
887        #[test]
888        fn test_loading_duration_tracks_time_correctly() {
889            use std::time::Duration;
890
891            let content = "[dependencies]\nserde = \"1.0\"".to_string();
892            let mut doc = DocumentState::new_without_parse_result("cargo", content);
893
894            doc.set_loading();
895
896            // Check duration increases over time
897            let duration1 = doc.loading_duration().unwrap();
898            std::thread::sleep(Duration::from_millis(20));
899            let duration2 = doc.loading_duration().unwrap();
900
901            assert!(duration2 > duration1, "Duration should increase over time");
902        }
903
904        #[tokio::test]
905        async fn test_concurrent_loading_state_mutations() {
906            use std::sync::Arc;
907            use tokio::sync::Barrier;
908
909            let state = Arc::new(ServerState::new());
910            let uri = Uri::from_file_path("/concurrent-loading-test.toml").unwrap();
911
912            let doc = DocumentState::new_without_parse_result("cargo", String::new());
913            state.update_document(uri.clone(), doc);
914
915            let barrier = Arc::new(Barrier::new(10));
916            let mut handles = vec![];
917
918            for i in 0..10 {
919                let state_clone = Arc::clone(&state);
920                let uri_clone = uri.clone();
921                let barrier_clone = Arc::clone(&barrier);
922
923                handles.push(tokio::spawn(async move {
924                    barrier_clone.wait().await;
925                    if let Some(mut doc) = state_clone.documents.get_mut(&uri_clone) {
926                        if i % 3 == 0 {
927                            doc.set_loading();
928                        } else if i % 3 == 1 {
929                            doc.set_loaded();
930                        } else {
931                            doc.set_failed();
932                        }
933                    }
934                }));
935            }
936
937            for handle in handles {
938                handle.await.unwrap();
939            }
940
941            let doc = state.get_document(&uri).unwrap();
942            assert!(matches!(
943                doc.loading_state,
944                LoadingState::Idle
945                    | LoadingState::Loading
946                    | LoadingState::Loaded
947                    | LoadingState::Failed
948            ));
949        }
950
951        #[test]
952        fn test_set_loaded_idempotent() {
953            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
954
955            doc.set_loading();
956            doc.set_loaded();
957
958            // Call again - should be safe
959            doc.set_loaded();
960
961            assert_eq!(doc.loading_state, LoadingState::Loaded);
962            assert!(doc.loading_started_at.is_none());
963        }
964
965        #[test]
966        fn test_set_loading_resets_timer() {
967            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
968
969            doc.set_loading();
970            let first_start = doc.loading_started_at.unwrap();
971
972            std::thread::sleep(std::time::Duration::from_millis(10));
973
974            // Call set_loading again - should reset timer
975            doc.set_loading();
976            let second_start = doc.loading_started_at.unwrap();
977
978            assert!(second_start > first_start, "Timer should be reset");
979            assert_eq!(doc.loading_state, LoadingState::Loading);
980        }
981
982        #[test]
983        fn test_retry_after_failure() {
984            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
985
986            doc.set_loading();
987            doc.set_failed();
988            assert_eq!(doc.loading_state, LoadingState::Failed);
989            assert!(doc.loading_started_at.is_none());
990
991            // Retry
992            doc.set_loading();
993            assert_eq!(doc.loading_state, LoadingState::Loading);
994            assert!(doc.loading_started_at.is_some());
995
996            doc.set_loaded();
997            assert_eq!(doc.loading_state, LoadingState::Loaded);
998        }
999
1000        #[test]
1001        fn test_refresh_after_loaded() {
1002            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
1003
1004            doc.set_loading();
1005            doc.set_loaded();
1006            assert_eq!(doc.loading_state, LoadingState::Loaded);
1007
1008            // Refresh
1009            doc.set_loading();
1010            assert_eq!(doc.loading_state, LoadingState::Loading);
1011            assert!(doc.loading_started_at.is_some());
1012
1013            doc.set_loaded();
1014            assert_eq!(doc.loading_state, LoadingState::Loaded);
1015        }
1016    }
1017
1018    #[test]
1019    fn test_ecosystem_from_filename() {
1020        #[cfg(feature = "cargo")]
1021        assert_eq!(
1022            Ecosystem::from_filename("Cargo.toml"),
1023            Some(Ecosystem::Cargo)
1024        );
1025        #[cfg(feature = "npm")]
1026        assert_eq!(
1027            Ecosystem::from_filename("package.json"),
1028            Some(Ecosystem::Npm)
1029        );
1030        #[cfg(feature = "pypi")]
1031        assert_eq!(
1032            Ecosystem::from_filename("pyproject.toml"),
1033            Some(Ecosystem::Pypi)
1034        );
1035        #[cfg(feature = "go")]
1036        assert_eq!(Ecosystem::from_filename("go.mod"), Some(Ecosystem::Go));
1037        assert_eq!(Ecosystem::from_filename("unknown.txt"), None);
1038    }
1039
1040    #[test]
1041    fn test_ecosystem_from_uri() {
1042        #[cfg(feature = "cargo")]
1043        {
1044            let cargo_uri = Uri::from_file_path("/path/to/Cargo.toml").unwrap();
1045            assert_eq!(Ecosystem::from_uri(&cargo_uri), Some(Ecosystem::Cargo));
1046        }
1047        #[cfg(feature = "npm")]
1048        {
1049            let npm_uri = Uri::from_file_path("/path/to/package.json").unwrap();
1050            assert_eq!(Ecosystem::from_uri(&npm_uri), Some(Ecosystem::Npm));
1051        }
1052        #[cfg(feature = "pypi")]
1053        {
1054            let pypi_uri = Uri::from_file_path("/path/to/pyproject.toml").unwrap();
1055            assert_eq!(Ecosystem::from_uri(&pypi_uri), Some(Ecosystem::Pypi));
1056        }
1057        #[cfg(feature = "go")]
1058        {
1059            let go_uri = Uri::from_file_path("/path/to/go.mod").unwrap();
1060            assert_eq!(Ecosystem::from_uri(&go_uri), Some(Ecosystem::Go));
1061        }
1062        let unknown_uri = Uri::from_file_path("/path/to/README.md").unwrap();
1063        assert_eq!(Ecosystem::from_uri(&unknown_uri), None);
1064    }
1065
1066    #[test]
1067    fn test_ecosystem_from_filename_edge_cases() {
1068        assert_eq!(Ecosystem::from_filename(""), None);
1069        assert_eq!(Ecosystem::from_filename("cargo.toml"), None);
1070        assert_eq!(Ecosystem::from_filename("CARGO.TOML"), None);
1071        assert_eq!(Ecosystem::from_filename("requirements.txt"), None);
1072    }
1073
1074    #[test]
1075    fn test_server_state_creation() {
1076        let state = ServerState::new();
1077        assert_eq!(state.document_count(), 0);
1078        assert!(state.cache.is_empty(), "Cache should start empty");
1079    }
1080
1081    #[test]
1082    fn test_server_state_default() {
1083        let state = ServerState::default();
1084        assert_eq!(state.document_count(), 0);
1085    }
1086
1087    #[tokio::test]
1088    async fn test_server_state_background_tasks() {
1089        let state = ServerState::new();
1090        let uri = Uri::from_file_path("/test.toml").unwrap();
1091
1092        let task = tokio::spawn(async {
1093            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1094        });
1095
1096        state.spawn_background_task(uri.clone(), task).await;
1097        state.cancel_background_task(&uri).await;
1098    }
1099
1100    #[tokio::test]
1101    async fn test_spawn_background_task_cancels_previous() {
1102        let state = ServerState::new();
1103        let uri = Uri::from_file_path("/test.toml").unwrap();
1104
1105        let task1 = tokio::spawn(async {
1106            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1107        });
1108        state.spawn_background_task(uri.clone(), task1).await;
1109
1110        let task2 = tokio::spawn(async {
1111            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1112        });
1113        state.spawn_background_task(uri.clone(), task2).await;
1114        state.cancel_background_task(&uri).await;
1115    }
1116
1117    #[tokio::test]
1118    async fn test_cancel_background_task_nonexistent() {
1119        let state = ServerState::new();
1120        let uri = Uri::from_file_path("/test.toml").unwrap();
1121        state.cancel_background_task(&uri).await;
1122    }
1123
1124    // =========================================================================
1125    // ColdStartLimiter tests
1126    // =========================================================================
1127
1128    mod cold_start_limiter {
1129        use super::*;
1130        use std::time::Duration;
1131
1132        #[test]
1133        fn test_allows_first_request() {
1134            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1135            let uri = Uri::from_file_path("/test.toml").unwrap();
1136            assert!(
1137                limiter.allow_cold_start(&uri),
1138                "First request should be allowed"
1139            );
1140        }
1141
1142        #[test]
1143        fn test_blocks_rapid_requests() {
1144            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1145            let uri = Uri::from_file_path("/test.toml").unwrap();
1146
1147            assert!(limiter.allow_cold_start(&uri), "First request allowed");
1148            assert!(
1149                !limiter.allow_cold_start(&uri),
1150                "Second immediate request should be blocked"
1151            );
1152        }
1153
1154        #[tokio::test]
1155        async fn test_allows_after_interval() {
1156            let limiter = ColdStartLimiter::new(Duration::from_millis(50));
1157            let uri = Uri::from_file_path("/test.toml").unwrap();
1158
1159            assert!(limiter.allow_cold_start(&uri), "First request allowed");
1160            tokio::time::sleep(Duration::from_millis(60)).await;
1161            assert!(
1162                limiter.allow_cold_start(&uri),
1163                "Request after interval should be allowed"
1164            );
1165        }
1166
1167        #[test]
1168        fn test_different_uris_independent() {
1169            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1170            let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1171            let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1172
1173            assert!(limiter.allow_cold_start(&uri1), "URI 1 first request");
1174            assert!(limiter.allow_cold_start(&uri2), "URI 2 first request");
1175            assert!(
1176                !limiter.allow_cold_start(&uri1),
1177                "URI 1 second request blocked"
1178            );
1179            assert!(
1180                !limiter.allow_cold_start(&uri2),
1181                "URI 2 second request blocked"
1182            );
1183        }
1184
1185        #[test]
1186        fn test_cleanup() {
1187            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1188            let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1189            let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1190
1191            limiter.allow_cold_start(&uri1);
1192            limiter.allow_cold_start(&uri2);
1193            assert_eq!(limiter.tracked_count(), 2, "Should track 2 URIs");
1194
1195            limiter.cleanup_old_entries(Duration::from_millis(0));
1196            assert_eq!(
1197                limiter.tracked_count(),
1198                0,
1199                "All entries should be cleaned up"
1200            );
1201        }
1202
1203        #[tokio::test]
1204        async fn test_concurrent_access() {
1205            use std::sync::Arc;
1206
1207            let limiter = Arc::new(ColdStartLimiter::new(Duration::from_millis(100)));
1208            let uri = Uri::from_file_path("/concurrent-test.toml").unwrap();
1209
1210            let mut handles = vec![];
1211            const CONCURRENT_TASKS: usize = 10;
1212
1213            for _ in 0..CONCURRENT_TASKS {
1214                let limiter_clone = Arc::clone(&limiter);
1215                let uri_clone = uri.clone();
1216                let handle =
1217                    tokio::spawn(async move { limiter_clone.allow_cold_start(&uri_clone) });
1218                handles.push(handle);
1219            }
1220
1221            let mut results = vec![];
1222            for handle in handles {
1223                results.push(handle.await.unwrap());
1224            }
1225
1226            let allowed_count = results.iter().filter(|&&allowed| allowed).count();
1227            assert_eq!(allowed_count, 1, "Exactly one concurrent request allowed");
1228
1229            let blocked_count = results.iter().filter(|&&allowed| !allowed).count();
1230            assert_eq!(
1231                blocked_count,
1232                CONCURRENT_TASKS - 1,
1233                "Rest should be blocked"
1234            );
1235        }
1236    }
1237
1238    // =========================================================================
1239    // Cargo ecosystem tests
1240    // =========================================================================
1241
1242    #[cfg(feature = "cargo")]
1243    mod cargo_tests {
1244        use super::*;
1245        use deps_cargo::{DependencySection, DependencySource};
1246        use tower_lsp_server::ls_types::{Position, Range};
1247
1248        fn create_test_dependency() -> UnifiedDependency {
1249            UnifiedDependency::Cargo(ParsedDependency {
1250                name: "serde".into(),
1251                name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1252                version_req: Some("1.0".into()),
1253                version_range: Some(Range::new(Position::new(0, 9), Position::new(0, 14))),
1254                features: vec![],
1255                features_range: None,
1256                source: DependencySource::Registry,
1257                workspace_inherited: false,
1258                section: DependencySection::Dependencies,
1259            })
1260        }
1261
1262        #[test]
1263        fn test_document_state_creation() {
1264            let deps = vec![create_test_dependency()];
1265            let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1266
1267            assert_eq!(state.ecosystem, Ecosystem::Cargo);
1268            assert_eq!(state.content, "test content");
1269            assert_eq!(state.dependencies.len(), 1);
1270            assert!(state.versions.is_empty());
1271        }
1272
1273        #[test]
1274        fn test_document_state_update_versions() {
1275            let deps = vec![create_test_dependency()];
1276            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1277
1278            let mut versions = HashMap::new();
1279            versions.insert(
1280                "serde".into(),
1281                UnifiedVersion::Cargo(CargoVersion {
1282                    num: "1.0.0".into(),
1283                    yanked: false,
1284                    features: HashMap::new(),
1285                }),
1286            );
1287
1288            state.update_versions(versions);
1289            assert_eq!(state.versions.len(), 1);
1290            assert!(state.versions.contains_key("serde"));
1291        }
1292
1293        #[test]
1294        fn test_server_state_document_operations() {
1295            let state = ServerState::new();
1296            let uri = Uri::from_file_path("/test.toml").unwrap();
1297            let deps = vec![create_test_dependency()];
1298            let doc_state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1299
1300            state.update_document(uri.clone(), doc_state);
1301            assert_eq!(state.document_count(), 1);
1302
1303            let retrieved = state.get_document(&uri);
1304            assert!(retrieved.is_some());
1305            assert_eq!(retrieved.unwrap().content, "test");
1306
1307            let removed = state.remove_document(&uri);
1308            assert!(removed.is_some());
1309            assert_eq!(state.document_count(), 0);
1310        }
1311
1312        #[test]
1313        fn test_unified_dependency_name() {
1314            let cargo_dep = create_test_dependency();
1315            assert_eq!(cargo_dep.name(), "serde");
1316            assert_eq!(cargo_dep.version_req(), Some("1.0"));
1317            assert!(cargo_dep.is_registry());
1318        }
1319
1320        #[test]
1321        fn test_unified_dependency_git_source() {
1322            let git_dep = UnifiedDependency::Cargo(ParsedDependency {
1323                name: "custom".into(),
1324                name_range: Range::new(Position::new(0, 0), Position::new(0, 6)),
1325                version_req: None,
1326                version_range: None,
1327                features: vec![],
1328                features_range: None,
1329                source: DependencySource::Git {
1330                    url: "https://github.com/user/repo".into(),
1331                    rev: None,
1332                },
1333                workspace_inherited: false,
1334                section: DependencySection::Dependencies,
1335            });
1336            assert!(!git_dep.is_registry());
1337        }
1338
1339        #[test]
1340        fn test_unified_version() {
1341            let version = UnifiedVersion::Cargo(CargoVersion {
1342                num: "1.0.0".into(),
1343                yanked: false,
1344                features: HashMap::new(),
1345            });
1346            assert_eq!(version.version_string(), "1.0.0");
1347            assert!(!version.is_yanked());
1348        }
1349
1350        #[test]
1351        fn test_document_state_new_from_parse_result() {
1352            let state = ServerState::new();
1353            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1354            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1355            let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1356
1357            let parse_result = tokio::runtime::Runtime::new()
1358                .unwrap()
1359                .block_on(ecosystem.parse_manifest(&content, &uri))
1360                .unwrap();
1361
1362            let doc_state =
1363                DocumentState::new_from_parse_result("cargo", content.clone(), parse_result);
1364
1365            assert_eq!(doc_state.ecosystem_id, "cargo");
1366            assert_eq!(doc_state.content, content);
1367            assert!(doc_state.parse_result.is_some());
1368        }
1369
1370        #[test]
1371        fn test_document_state_new_without_parse_result() {
1372            let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1373            let doc_state = DocumentState::new_without_parse_result("cargo", content);
1374
1375            assert_eq!(doc_state.ecosystem_id, "cargo");
1376            assert_eq!(doc_state.ecosystem, Ecosystem::Cargo);
1377            assert!(doc_state.parse_result.is_none());
1378            assert!(doc_state.dependencies.is_empty());
1379        }
1380
1381        #[test]
1382        fn test_document_state_update_resolved_versions() {
1383            let deps = vec![create_test_dependency()];
1384            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1385
1386            let mut resolved = HashMap::new();
1387            resolved.insert("serde".into(), "1.0.195".into());
1388
1389            state.update_resolved_versions(resolved);
1390            assert_eq!(state.resolved_versions.len(), 1);
1391            assert_eq!(
1392                state.resolved_versions.get("serde"),
1393                Some(&"1.0.195".into())
1394            );
1395        }
1396
1397        #[test]
1398        fn test_document_state_update_cached_versions() {
1399            let deps = vec![create_test_dependency()];
1400            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1401
1402            let mut cached = HashMap::new();
1403            cached.insert("serde".into(), "1.0.210".into());
1404
1405            state.update_cached_versions(cached);
1406            assert_eq!(state.cached_versions.len(), 1);
1407        }
1408
1409        #[test]
1410        fn test_document_state_parse_result_accessor() {
1411            let deps = vec![create_test_dependency()];
1412            let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1413            assert!(state.parse_result().is_none());
1414        }
1415
1416        #[test]
1417        fn test_document_state_clone() {
1418            let deps = vec![create_test_dependency()];
1419            let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1420            let cloned = state.clone();
1421
1422            assert_eq!(cloned.ecosystem, state.ecosystem);
1423            assert_eq!(cloned.content, state.content);
1424            assert_eq!(cloned.dependencies.len(), state.dependencies.len());
1425            assert!(cloned.parse_result.is_none());
1426        }
1427
1428        #[test]
1429        fn test_document_state_debug() {
1430            let deps = vec![create_test_dependency()];
1431            let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1432            let debug_str = format!("{state:?}");
1433            assert!(debug_str.contains("DocumentState"));
1434        }
1435    }
1436
1437    // =========================================================================
1438    // npm ecosystem tests
1439    // =========================================================================
1440
1441    #[cfg(feature = "npm")]
1442    mod npm_tests {
1443        use super::*;
1444        use deps_npm::{NpmDependency, NpmDependencySection};
1445        use tower_lsp_server::ls_types::{Position, Range};
1446
1447        #[test]
1448        fn test_unified_dependency() {
1449            let npm_dep = UnifiedDependency::Npm(NpmDependency {
1450                name: "express".into(),
1451                name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
1452                version_req: Some("^4.0.0".into()),
1453                version_range: Some(Range::new(Position::new(0, 11), Position::new(0, 18))),
1454                section: NpmDependencySection::Dependencies,
1455            });
1456
1457            assert_eq!(npm_dep.name(), "express");
1458            assert_eq!(npm_dep.version_req(), Some("^4.0.0"));
1459            assert!(npm_dep.is_registry());
1460        }
1461
1462        #[test]
1463        fn test_unified_version() {
1464            let version = UnifiedVersion::Npm(deps_npm::NpmVersion {
1465                version: "4.18.2".into(),
1466                deprecated: false,
1467            });
1468            assert_eq!(version.version_string(), "4.18.2");
1469            assert!(!version.is_yanked());
1470        }
1471
1472        #[test]
1473        fn test_document_state_new_without_parse_result() {
1474            let content = r#"{"dependencies": {"express": "^4.18.0"}}"#.to_string();
1475            let doc_state = DocumentState::new_without_parse_result("npm", content);
1476
1477            assert_eq!(doc_state.ecosystem_id, "npm");
1478            assert_eq!(doc_state.ecosystem, Ecosystem::Npm);
1479            assert!(doc_state.parse_result.is_none());
1480        }
1481    }
1482
1483    // =========================================================================
1484    // PyPI ecosystem tests
1485    // =========================================================================
1486
1487    #[cfg(feature = "pypi")]
1488    mod pypi_tests {
1489        use super::*;
1490        use deps_pypi::{PypiDependency, PypiDependencySection, PypiDependencySource};
1491        use tower_lsp_server::ls_types::{Position, Range};
1492
1493        #[test]
1494        fn test_unified_dependency() {
1495            let pypi_dep = UnifiedDependency::Pypi(PypiDependency {
1496                name: "requests".into(),
1497                name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1498                version_req: Some(">=2.0.0".into()),
1499                version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 18))),
1500                extras: vec![],
1501                extras_range: None,
1502                markers: None,
1503                markers_range: None,
1504                source: PypiDependencySource::PyPI,
1505                section: PypiDependencySection::Dependencies,
1506            });
1507
1508            assert_eq!(pypi_dep.name(), "requests");
1509            assert_eq!(pypi_dep.version_req(), Some(">=2.0.0"));
1510            assert!(pypi_dep.is_registry());
1511        }
1512
1513        #[test]
1514        fn test_unified_version() {
1515            let version = UnifiedVersion::Pypi(deps_pypi::PypiVersion {
1516                version: "2.31.0".into(),
1517                yanked: true,
1518            });
1519            assert_eq!(version.version_string(), "2.31.0");
1520            assert!(version.is_yanked());
1521        }
1522
1523        #[test]
1524        fn test_document_state_new_without_parse_result() {
1525            let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n".to_string();
1526            let doc_state = DocumentState::new_without_parse_result("pypi", content);
1527
1528            assert_eq!(doc_state.ecosystem_id, "pypi");
1529            assert_eq!(doc_state.ecosystem, Ecosystem::Pypi);
1530            assert!(doc_state.parse_result.is_none());
1531        }
1532    }
1533
1534    // =========================================================================
1535    // Go ecosystem tests
1536    // =========================================================================
1537
1538    #[cfg(feature = "go")]
1539    mod go_tests {
1540        use super::*;
1541        use deps_go::{GoDependency, GoDirective, GoVersion};
1542        use tower_lsp_server::ls_types::{Position, Range};
1543
1544        fn create_test_dependency() -> UnifiedDependency {
1545            UnifiedDependency::Go(GoDependency {
1546                module_path: "github.com/gin-gonic/gin".into(),
1547                module_path_range: Range::new(Position::new(0, 0), Position::new(0, 25)),
1548                version: Some("v1.9.1".into()),
1549                version_range: Some(Range::new(Position::new(0, 26), Position::new(0, 32))),
1550                directive: GoDirective::Require,
1551                indirect: false,
1552            })
1553        }
1554
1555        #[test]
1556        fn test_unified_dependency() {
1557            let go_dep = create_test_dependency();
1558            assert_eq!(go_dep.name(), "github.com/gin-gonic/gin");
1559            assert_eq!(go_dep.version_req(), Some("v1.9.1"));
1560            assert!(go_dep.is_registry());
1561        }
1562
1563        #[test]
1564        fn test_unified_dependency_name_range() {
1565            let range = Range::new(Position::new(5, 10), Position::new(5, 35));
1566            let go_dep = UnifiedDependency::Go(GoDependency {
1567                module_path: "github.com/example/pkg".into(),
1568                module_path_range: range,
1569                version: Some("v1.0.0".into()),
1570                version_range: Some(Range::new(Position::new(5, 36), Position::new(5, 42))),
1571                directive: GoDirective::Require,
1572                indirect: false,
1573            });
1574            assert_eq!(go_dep.name_range(), range);
1575        }
1576
1577        #[test]
1578        fn test_unified_dependency_version_range() {
1579            let version_range = Range::new(Position::new(5, 36), Position::new(5, 42));
1580            let go_dep = UnifiedDependency::Go(GoDependency {
1581                module_path: "github.com/example/pkg".into(),
1582                module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1583                version: Some("v1.0.0".into()),
1584                version_range: Some(version_range),
1585                directive: GoDirective::Require,
1586                indirect: false,
1587            });
1588            assert_eq!(go_dep.version_range(), Some(version_range));
1589        }
1590
1591        #[test]
1592        fn test_unified_dependency_no_version() {
1593            let go_dep = UnifiedDependency::Go(GoDependency {
1594                module_path: "github.com/example/pkg".into(),
1595                module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1596                version: None,
1597                version_range: None,
1598                directive: GoDirective::Require,
1599                indirect: false,
1600            });
1601            assert_eq!(go_dep.version_req(), None);
1602            assert_eq!(go_dep.version_range(), None);
1603        }
1604
1605        #[test]
1606        fn test_unified_version() {
1607            let version = UnifiedVersion::Go(GoVersion {
1608                version: "v1.9.1".into(),
1609                time: Some("2023-07-18T14:30:00Z".into()),
1610                is_pseudo: false,
1611                retracted: false,
1612            });
1613            assert_eq!(version.version_string(), "v1.9.1");
1614            assert!(!version.is_yanked());
1615        }
1616
1617        #[test]
1618        fn test_unified_version_retracted() {
1619            let version = UnifiedVersion::Go(GoVersion {
1620                version: "v1.0.0".into(),
1621                time: None,
1622                is_pseudo: false,
1623                retracted: true,
1624            });
1625            assert_eq!(version.version_string(), "v1.0.0");
1626            assert!(version.is_yanked());
1627        }
1628
1629        #[test]
1630        fn test_unified_version_pseudo() {
1631            let version = UnifiedVersion::Go(GoVersion {
1632                version: "v0.0.0-20191109021931-daa7c04131f5".into(),
1633                time: Some("2019-11-09T02:19:31Z".into()),
1634                is_pseudo: true,
1635                retracted: false,
1636            });
1637            assert_eq!(
1638                version.version_string(),
1639                "v0.0.0-20191109021931-daa7c04131f5"
1640            );
1641            assert!(!version.is_yanked());
1642        }
1643
1644        #[test]
1645        fn test_document_state_new() {
1646            let deps = vec![create_test_dependency()];
1647            let state = DocumentState::new(Ecosystem::Go, "test content".into(), deps);
1648
1649            assert_eq!(state.ecosystem, Ecosystem::Go);
1650            assert_eq!(state.ecosystem_id, "go");
1651            assert_eq!(state.dependencies.len(), 1);
1652        }
1653
1654        #[test]
1655        fn test_document_state_new_without_parse_result() {
1656            let content =
1657                "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1658                    .to_string();
1659            let doc_state = DocumentState::new_without_parse_result("go", content);
1660
1661            assert_eq!(doc_state.ecosystem_id, "go");
1662            assert_eq!(doc_state.ecosystem, Ecosystem::Go);
1663            assert!(doc_state.parse_result.is_none());
1664        }
1665
1666        #[test]
1667        fn test_document_state_new_from_parse_result() {
1668            let state = ServerState::new();
1669            let uri = Uri::from_file_path("/test/go.mod").unwrap();
1670            let ecosystem = state.ecosystem_registry.get("go").unwrap();
1671            let content =
1672                "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1673                    .to_string();
1674
1675            let parse_result = tokio::runtime::Runtime::new()
1676                .unwrap()
1677                .block_on(ecosystem.parse_manifest(&content, &uri))
1678                .unwrap();
1679
1680            let doc_state =
1681                DocumentState::new_from_parse_result("go", content.clone(), parse_result);
1682
1683            assert_eq!(doc_state.ecosystem_id, "go");
1684            assert!(doc_state.parse_result.is_some());
1685        }
1686    }
1687}