pytest_language_server/fixtures/
mod.rs

1//! Fixture database and analysis module.
2//!
3//! This module provides the core functionality for managing pytest fixtures:
4//! - Scanning workspaces for fixture definitions
5//! - Analyzing Python files for fixtures and their usages
6//! - Resolving fixture definitions based on pytest's priority rules
7//! - Providing completion context for fixture suggestions
8
9mod analyzer;
10pub(crate) mod cli;
11pub mod decorators; // Public for testing
12mod resolver;
13mod scanner;
14mod string_utils;
15pub mod types;
16
17#[allow(unused_imports)] // ParamInsertionInfo re-exported for public API via lib.rs
18pub use types::{
19    CompletionContext, FixtureDefinition, FixtureUsage, ParamInsertionInfo, UndeclaredFixture,
20};
21
22use dashmap::DashMap;
23use std::collections::hash_map::DefaultHasher;
24use std::collections::HashSet;
25use std::hash::{Hash, Hasher};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use tracing::debug;
29
30/// Cache entry for line indices: (content_hash, line_index).
31/// The content hash is used to invalidate the cache when file content changes.
32type LineIndexCacheEntry = (u64, Arc<Vec<usize>>);
33
34/// The central database for fixture definitions and usages.
35///
36/// Uses `DashMap` for lock-free concurrent access during workspace scanning.
37#[derive(Debug)]
38pub struct FixtureDatabase {
39    /// Map from fixture name to all its definitions (can be in multiple conftest.py files).
40    pub definitions: Arc<DashMap<String, Vec<FixtureDefinition>>>,
41    /// Map from file path to fixtures used in that file.
42    pub usages: Arc<DashMap<PathBuf, Vec<FixtureUsage>>>,
43    /// Cache of file contents for analyzed files (uses Arc for efficient sharing).
44    pub file_cache: Arc<DashMap<PathBuf, Arc<String>>>,
45    /// Map from file path to undeclared fixtures used in function bodies.
46    pub undeclared_fixtures: Arc<DashMap<PathBuf, Vec<UndeclaredFixture>>>,
47    /// Map from file path to imported names in that file.
48    pub imports: Arc<DashMap<PathBuf, HashSet<String>>>,
49    /// Cache of canonical paths to avoid repeated filesystem calls.
50    pub canonical_path_cache: Arc<DashMap<PathBuf, PathBuf>>,
51    /// Cache of line indices (byte offsets) for files to avoid recomputation.
52    /// Stores (content_hash, line_index) to invalidate when content changes.
53    pub line_index_cache: Arc<DashMap<PathBuf, LineIndexCacheEntry>>,
54}
55
56impl Default for FixtureDatabase {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl FixtureDatabase {
63    /// Create a new empty fixture database.
64    pub fn new() -> Self {
65        Self {
66            definitions: Arc::new(DashMap::new()),
67            usages: Arc::new(DashMap::new()),
68            file_cache: Arc::new(DashMap::new()),
69            undeclared_fixtures: Arc::new(DashMap::new()),
70            imports: Arc::new(DashMap::new()),
71            canonical_path_cache: Arc::new(DashMap::new()),
72            line_index_cache: Arc::new(DashMap::new()),
73        }
74    }
75
76    /// Get canonical path with caching to avoid repeated filesystem calls.
77    /// Falls back to original path if canonicalization fails.
78    pub(crate) fn get_canonical_path(&self, path: PathBuf) -> PathBuf {
79        // Check cache first
80        if let Some(cached) = self.canonical_path_cache.get(&path) {
81            return cached.value().clone();
82        }
83
84        // Attempt canonicalization
85        let canonical = path.canonicalize().unwrap_or_else(|_| {
86            debug!("Could not canonicalize path {:?}, using as-is", path);
87            path.clone()
88        });
89
90        // Store in cache for future lookups
91        self.canonical_path_cache.insert(path, canonical.clone());
92        canonical
93    }
94
95    /// Get file content from cache or read from filesystem.
96    /// Returns None if file cannot be read.
97    pub(crate) fn get_file_content(&self, file_path: &Path) -> Option<Arc<String>> {
98        if let Some(cached) = self.file_cache.get(file_path) {
99            Some(Arc::clone(cached.value()))
100        } else {
101            std::fs::read_to_string(file_path).ok().map(Arc::new)
102        }
103    }
104
105    /// Get or compute line index for a file, with content-hash-based caching.
106    /// Returns Arc to avoid cloning the potentially large Vec.
107    /// The cache is invalidated when the content hash changes.
108    pub(crate) fn get_line_index(&self, file_path: &Path, content: &str) -> Arc<Vec<usize>> {
109        let content_hash = Self::hash_content(content);
110
111        // Check cache first - only use if content hash matches
112        if let Some(cached) = self.line_index_cache.get(file_path) {
113            let (cached_hash, cached_index) = cached.value();
114            if *cached_hash == content_hash {
115                return Arc::clone(cached_index);
116            }
117        }
118
119        // Build line index
120        let line_index = Self::build_line_index(content);
121        let arc_index = Arc::new(line_index);
122
123        // Store in cache with content hash
124        self.line_index_cache.insert(
125            file_path.to_path_buf(),
126            (content_hash, Arc::clone(&arc_index)),
127        );
128
129        arc_index
130    }
131
132    /// Compute a hash of the content for cache invalidation.
133    fn hash_content(content: &str) -> u64 {
134        let mut hasher = DefaultHasher::new();
135        content.hash(&mut hasher);
136        hasher.finish()
137    }
138}