mod analyzer;
pub(crate) mod cli;
pub mod decorators; mod docstring;
pub mod import_analysis;
mod imports;
mod resolver;
mod scanner;
pub(crate) mod string_utils; pub mod types;
mod undeclared;
#[allow(unused_imports)] pub use types::{
CompletionContext, FixtureCycle, FixtureDefinition, FixtureScope, FixtureUsage,
ParamInsertionInfo, ScopeMismatch, TypeImportSpec, UndeclaredFixture,
};
use dashmap::DashMap;
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::debug;
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct EditableInstall {
pub package_name: String,
pub raw_package_name: String,
pub source_root: PathBuf,
pub site_packages: PathBuf,
}
type LineIndexCacheEntry = (u64, Arc<Vec<usize>>);
type AstCacheEntry = (u64, Arc<rustpython_parser::ast::Mod>);
type CycleCacheEntry = (u64, Arc<Vec<types::FixtureCycle>>);
type AvailableFixturesCacheEntry = (u64, Arc<Vec<FixtureDefinition>>);
type ImportedFixturesCacheEntry = (u64, u64, Arc<HashSet<String>>);
type NameImportMapCacheEntry = (
u64,
Arc<HashMap<String, crate::fixtures::types::TypeImportSpec>>,
);
const MAX_FILE_CACHE_SIZE: usize = 2000;
#[derive(Debug)]
pub struct FixtureDatabase {
pub definitions: Arc<DashMap<String, Vec<FixtureDefinition>>>,
pub file_definitions: Arc<DashMap<PathBuf, HashSet<String>>>,
pub usages: Arc<DashMap<PathBuf, Vec<FixtureUsage>>>,
pub usage_by_fixture: Arc<DashMap<String, Vec<(PathBuf, FixtureUsage)>>>,
pub file_cache: Arc<DashMap<PathBuf, Arc<String>>>,
pub undeclared_fixtures: Arc<DashMap<PathBuf, Vec<UndeclaredFixture>>>,
pub imports: Arc<DashMap<PathBuf, HashSet<String>>>,
pub canonical_path_cache: Arc<DashMap<PathBuf, PathBuf>>,
pub line_index_cache: Arc<DashMap<PathBuf, LineIndexCacheEntry>>,
pub ast_cache: Arc<DashMap<PathBuf, AstCacheEntry>>,
pub definitions_version: Arc<std::sync::atomic::AtomicU64>,
pub cycle_cache: Arc<DashMap<(), CycleCacheEntry>>,
pub available_fixtures_cache: Arc<DashMap<PathBuf, AvailableFixturesCacheEntry>>,
pub imported_fixtures_cache: Arc<DashMap<PathBuf, ImportedFixturesCacheEntry>>,
pub site_packages_paths: Arc<std::sync::Mutex<Vec<PathBuf>>>,
pub editable_install_roots: Arc<std::sync::Mutex<Vec<EditableInstall>>>,
pub workspace_root: Arc<std::sync::Mutex<Option<PathBuf>>>,
pub plugin_fixture_files: Arc<DashMap<PathBuf, ()>>,
pub name_import_map_cache: Arc<DashMap<PathBuf, NameImportMapCacheEntry>>,
}
impl Default for FixtureDatabase {
fn default() -> Self {
Self::new()
}
}
impl FixtureDatabase {
pub fn new() -> Self {
Self {
definitions: Arc::new(DashMap::new()),
file_definitions: Arc::new(DashMap::new()),
usages: Arc::new(DashMap::new()),
usage_by_fixture: Arc::new(DashMap::new()),
file_cache: Arc::new(DashMap::new()),
undeclared_fixtures: Arc::new(DashMap::new()),
imports: Arc::new(DashMap::new()),
canonical_path_cache: Arc::new(DashMap::new()),
line_index_cache: Arc::new(DashMap::new()),
ast_cache: Arc::new(DashMap::new()),
definitions_version: Arc::new(std::sync::atomic::AtomicU64::new(0)),
cycle_cache: Arc::new(DashMap::new()),
available_fixtures_cache: Arc::new(DashMap::new()),
imported_fixtures_cache: Arc::new(DashMap::new()),
site_packages_paths: Arc::new(std::sync::Mutex::new(Vec::new())),
editable_install_roots: Arc::new(std::sync::Mutex::new(Vec::new())),
workspace_root: Arc::new(std::sync::Mutex::new(None)),
plugin_fixture_files: Arc::new(DashMap::new()),
name_import_map_cache: Arc::new(DashMap::new()),
}
}
pub(crate) fn invalidate_cycle_cache(&self) {
self.definitions_version
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
pub(crate) fn get_canonical_path(&self, path: PathBuf) -> PathBuf {
if let Some(cached) = self.canonical_path_cache.get(&path) {
return cached.value().clone();
}
let canonical = path.canonicalize().unwrap_or_else(|_| {
debug!("Could not canonicalize path {:?}, using as-is", path);
path.clone()
});
self.canonical_path_cache.insert(path, canonical.clone());
canonical
}
pub(crate) fn get_file_content(&self, file_path: &Path) -> Option<Arc<String>> {
if let Some(cached) = self.file_cache.get(file_path) {
Some(Arc::clone(cached.value()))
} else {
std::fs::read_to_string(file_path).ok().map(Arc::new)
}
}
pub(crate) fn get_line_index(&self, file_path: &Path, content: &str) -> Arc<Vec<usize>> {
let content_hash = Self::hash_content(content);
if let Some(cached) = self.line_index_cache.get(file_path) {
let (cached_hash, cached_index) = cached.value();
if *cached_hash == content_hash {
return Arc::clone(cached_index);
}
}
let line_index = Self::build_line_index(content);
let arc_index = Arc::new(line_index);
self.line_index_cache.insert(
file_path.to_path_buf(),
(content_hash, Arc::clone(&arc_index)),
);
arc_index
}
pub(crate) fn get_parsed_ast(
&self,
file_path: &Path,
content: &str,
) -> Option<Arc<rustpython_parser::ast::Mod>> {
let content_hash = Self::hash_content(content);
if let Some(cached) = self.ast_cache.get(file_path) {
let (cached_hash, cached_ast) = cached.value();
if *cached_hash == content_hash {
return Some(Arc::clone(cached_ast));
}
}
let parsed = rustpython_parser::parse(content, rustpython_parser::Mode::Module, "").ok()?;
let arc_ast = Arc::new(parsed);
self.ast_cache.insert(
file_path.to_path_buf(),
(content_hash, Arc::clone(&arc_ast)),
);
Some(arc_ast)
}
pub fn get_name_to_import_map(
&self,
file_path: &Path,
content: &str,
) -> Arc<HashMap<String, crate::fixtures::types::TypeImportSpec>> {
let hash = Self::hash_content(content);
if let Some(entry) = self.name_import_map_cache.get(file_path) {
let (cached_hash, arc_map) = entry.value();
if *cached_hash == hash {
return Arc::clone(arc_map);
}
}
let map = match self.get_parsed_ast(file_path, content) {
Some(ast) => {
if let rustpython_parser::ast::Mod::Module(module) = ast.as_ref() {
self.build_name_to_import_map(&module.body, file_path)
} else {
HashMap::new()
}
}
None => HashMap::new(),
};
let arc_map = Arc::new(map);
self.name_import_map_cache
.insert(file_path.to_path_buf(), (hash, Arc::clone(&arc_map)));
arc_map
}
fn hash_content(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn is_editable_install_third_party(&self, file_path: &Path) -> bool {
let installs = self.editable_install_roots.lock().unwrap();
let workspace = self.workspace_root.lock().unwrap();
for install in installs.iter() {
if file_path.starts_with(&install.source_root) {
if let Some(ref ws) = *workspace {
if install.source_root.starts_with(ws) {
return false;
}
if ws.starts_with(&install.source_root) {
return false;
}
}
return true;
}
}
false
}
pub fn cleanup_file_cache(&self, file_path: &Path) {
let canonical = file_path
.canonicalize()
.unwrap_or_else(|_| file_path.to_path_buf());
debug!("Cleaning up cache for file: {:?}", canonical);
self.line_index_cache.remove(&canonical);
self.ast_cache.remove(&canonical);
self.name_import_map_cache.remove(&canonical);
self.file_cache.remove(&canonical);
self.available_fixtures_cache.remove(&canonical);
self.imported_fixtures_cache.remove(&canonical);
}
pub(crate) fn evict_cache_if_needed(&self) {
if self.file_cache.len() > MAX_FILE_CACHE_SIZE {
debug!(
"File cache size ({}) exceeds limit ({}), evicting entries",
self.file_cache.len(),
MAX_FILE_CACHE_SIZE
);
let to_remove_count = self.file_cache.len() / 4;
let to_remove: Vec<PathBuf> = self
.file_cache
.iter()
.take(to_remove_count)
.map(|entry| entry.key().clone())
.collect();
for path in to_remove {
self.file_cache.remove(&path);
self.line_index_cache.remove(&path);
self.ast_cache.remove(&path);
self.available_fixtures_cache.remove(&path);
self.imported_fixtures_cache.remove(&path);
self.name_import_map_cache.remove(&path);
}
debug!(
"Cache eviction complete, new size: {}",
self.file_cache.len()
);
}
}
}