#![allow(dead_code)]
use rustc_hash::FxHashMap;
use std::path::PathBuf;
use super::file_usage::FileUsageInfoOwned;
#[derive(Debug, Default)]
pub struct ProjectIndex {
files: FxHashMap<PathBuf, FileUsageInfoOwned>,
provide_index: FxHashMap<String, Vec<PathBuf>>,
inject_index: FxHashMap<String, Vec<PathBuf>>,
component_graph: FxHashMap<PathBuf, Vec<ComponentEdge>>,
component_reverse_index: FxHashMap<String, Vec<PathBuf>>,
}
#[derive(Debug, Clone)]
pub struct ComponentEdge {
pub component_name: String,
pub is_dynamic: bool,
pub start: u32,
pub end: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InjectValidation {
Valid {
providers: Vec<PathBuf>,
},
NoProvider,
DynamicKey,
KeyNotFound,
}
#[derive(Debug, Clone, Default)]
pub struct FileInjectValidation {
pub valid: Vec<InjectValidationEntry>,
pub missing_providers: Vec<InjectValidationEntry>,
pub dynamic_keys: Vec<DynamicInjectEntry>,
}
#[derive(Debug, Clone)]
pub struct InjectValidationEntry {
pub key: String,
pub providers: Vec<PathBuf>,
pub start: u32,
pub end: u32,
}
#[derive(Debug, Clone)]
pub struct DynamicInjectEntry {
pub start: u32,
pub end: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ProvideInjectSummary {
pub provide_keys: Vec<String>,
pub inject_keys: Vec<String>,
pub unused_provides: Vec<String>,
pub missing_provides: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ComponentUsageSummary {
pub component_names: Vec<String>,
pub external_components: Vec<String>,
pub usage_counts: FxHashMap<String, usize>,
}
#[derive(Debug, Clone, Default)]
pub struct ProjectStats {
pub file_count: usize,
pub files_with_provide: usize,
pub files_with_inject: usize,
pub files_with_props: usize,
pub files_with_emits: usize,
pub files_with_async_setup: usize,
pub unique_provide_keys: usize,
pub unique_inject_keys: usize,
}
impl ProjectIndex {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(file_count: usize) -> Self {
Self {
files: FxHashMap::with_capacity_and_hasher(file_count, Default::default()),
provide_index: FxHashMap::default(),
inject_index: FxHashMap::default(),
component_graph: FxHashMap::with_capacity_and_hasher(file_count, Default::default()),
component_reverse_index: FxHashMap::default(),
}
}
pub fn add_file(&mut self, path: PathBuf, info: FileUsageInfoOwned) {
if self.files.contains_key(&path) {
self.remove_file(&path);
}
for provide in &info.provides {
if let Some(key) = &provide.key {
self.provide_index
.entry(key.clone())
.or_default()
.push(path.clone());
}
}
for inject in &info.injects {
if let Some(key) = &inject.key {
self.inject_index
.entry(key.clone())
.or_default()
.push(path.clone());
}
}
let edges: Vec<ComponentEdge> = info
.components
.iter()
.filter_map(|c| {
c.name.as_ref().map(|name| ComponentEdge {
component_name: name.clone(),
is_dynamic: c.is_dynamic,
start: c.start,
end: c.end,
})
})
.collect();
for edge in &edges {
self.component_reverse_index
.entry(edge.component_name.clone())
.or_default()
.push(path.clone());
}
self.component_graph.insert(path.clone(), edges);
self.files.insert(path, info);
}
pub fn remove_file(&mut self, path: &PathBuf) -> Option<FileUsageInfoOwned> {
let info = self.files.remove(path)?;
for provide in &info.provides {
if let Some(key) = &provide.key {
if let Some(providers) = self.provide_index.get_mut(key) {
providers.retain(|p| p != path);
if providers.is_empty() {
self.provide_index.remove(key);
}
}
}
}
for inject in &info.injects {
if let Some(key) = &inject.key {
if let Some(injectors) = self.inject_index.get_mut(key) {
injectors.retain(|p| p != path);
if injectors.is_empty() {
self.inject_index.remove(key);
}
}
}
}
if let Some(edges) = self.component_graph.remove(path) {
for edge in edges {
if let Some(users) = self.component_reverse_index.get_mut(&edge.component_name) {
users.retain(|p| p != path);
if users.is_empty() {
self.component_reverse_index.remove(&edge.component_name);
}
}
}
}
Some(info)
}
pub fn get_file(&self, path: &PathBuf) -> Option<&FileUsageInfoOwned> {
self.files.get(path)
}
pub fn contains_file(&self, path: &PathBuf) -> bool {
self.files.contains_key(path)
}
pub fn file_paths(&self) -> impl Iterator<Item = &PathBuf> {
self.files.keys()
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn files_providing(&self, key: &str) -> &[PathBuf] {
self.provide_index
.get(key)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn files_injecting(&self, key: &str) -> &[PathBuf] {
self.inject_index
.get(key)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn all_provide_keys(&self) -> impl Iterator<Item = &String> {
self.provide_index.keys()
}
pub fn all_inject_keys(&self) -> impl Iterator<Item = &String> {
self.inject_index.keys()
}
pub fn components_used_by(&self, path: &PathBuf) -> &[ComponentEdge] {
self.component_graph
.get(path)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn files_using_component(&self, component_name: &str) -> &[PathBuf] {
self.component_reverse_index
.get(component_name)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn all_component_names(&self) -> impl Iterator<Item = &String> {
self.component_reverse_index.keys()
}
pub fn validate_inject(&self, file: &PathBuf, key: &str) -> InjectValidation {
let Some(info) = self.files.get(file) else {
return InjectValidation::KeyNotFound;
};
let inject = info.injects.iter().find(|i| i.key.as_deref() == Some(key));
match inject {
Some(i) if i.is_dynamic_key => InjectValidation::DynamicKey,
Some(_) => {
let providers: Vec<PathBuf> = self.files_providing(key).to_vec();
if providers.is_empty() {
InjectValidation::NoProvider
} else {
InjectValidation::Valid { providers }
}
}
None => InjectValidation::KeyNotFound,
}
}
pub fn validate_file_injects(&self, file: &PathBuf) -> FileInjectValidation {
let Some(info) = self.files.get(file) else {
return FileInjectValidation::default();
};
let mut result = FileInjectValidation::default();
for inject in &info.injects {
if inject.is_dynamic_key {
result.dynamic_keys.push(DynamicInjectEntry {
start: inject.start,
end: inject.end,
});
} else if let Some(key) = &inject.key {
let providers: Vec<PathBuf> = self.files_providing(key).to_vec();
let entry = InjectValidationEntry {
key: key.clone(),
providers: providers.clone(),
start: inject.start,
end: inject.end,
};
if providers.is_empty() {
result.missing_providers.push(entry);
} else {
result.valid.push(entry);
}
}
}
result
}
pub fn provide_inject_summary(&self) -> ProvideInjectSummary {
let provide_keys: Vec<String> = self.provide_index.keys().cloned().collect();
let inject_keys: Vec<String> = self.inject_index.keys().cloned().collect();
let unused_provides = provide_keys
.iter()
.filter(|k| !self.inject_index.contains_key(*k))
.cloned()
.collect();
let missing_provides = inject_keys
.iter()
.filter(|k| !self.provide_index.contains_key(*k))
.cloned()
.collect();
ProvideInjectSummary {
provide_keys,
inject_keys,
unused_provides,
missing_provides,
}
}
pub fn component_usage_summary(&self) -> ComponentUsageSummary {
let component_names: Vec<String> = self.component_reverse_index.keys().cloned().collect();
let usage_counts: FxHashMap<String, usize> = self
.component_reverse_index
.iter()
.map(|(name, files)| (name.clone(), files.len()))
.collect();
let external_components = Vec::new();
ComponentUsageSummary {
component_names,
external_components,
usage_counts,
}
}
pub fn stats(&self) -> ProjectStats {
use super::file_usage::FileUsageFlags;
let mut stats = ProjectStats {
file_count: self.files.len(),
unique_provide_keys: self.provide_index.len(),
unique_inject_keys: self.inject_index.len(),
..Default::default()
};
for info in self.files.values() {
if info.has_flag(FileUsageFlags::HAS_PROVIDE) {
stats.files_with_provide += 1;
}
if info.has_flag(FileUsageFlags::HAS_INJECT) {
stats.files_with_inject += 1;
}
if info.has_flag(FileUsageFlags::HAS_DEFINE_PROPS) {
stats.files_with_props += 1;
}
if info.has_flag(FileUsageFlags::HAS_DEFINE_EMITS) {
stats.files_with_emits += 1;
}
if info.has_flag(FileUsageFlags::IS_ASYNC_SETUP) {
stats.files_with_async_setup += 1;
}
}
stats
}
pub fn clear(&mut self) {
self.files.clear();
self.provide_index.clear();
self.inject_index.clear();
self.component_graph.clear();
self.component_reverse_index.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::file_usage::{
ComponentUsageOwned, FileUsageFlags, InjectUsageOwned, ProvideUsageOwned,
};
fn make_file_info() -> FileUsageInfoOwned {
FileUsageInfoOwned {
imports: Vec::new(),
macros: Vec::new(),
provides: Vec::new(),
injects: Vec::new(),
components: Vec::new(),
flags: 0,
}
}
fn make_file_with_provide(key: &str) -> FileUsageInfoOwned {
let mut info = make_file_info();
info.provides.push(ProvideUsageOwned {
key: Some(key.to_string()),
is_dynamic_key: false,
start: 0,
end: 10,
});
info.flags |= FileUsageFlags::HAS_PROVIDE;
info
}
fn make_file_with_inject(key: &str) -> FileUsageInfoOwned {
let mut info = make_file_info();
info.injects.push(InjectUsageOwned {
key: Some(key.to_string()),
is_dynamic_key: false,
has_default: false,
binding_name: None,
start: 0,
end: 10,
});
info.flags |= FileUsageFlags::HAS_INJECT;
info
}
fn make_file_with_dynamic_inject() -> FileUsageInfoOwned {
let mut info = make_file_info();
info.injects.push(InjectUsageOwned {
key: None,
is_dynamic_key: true,
has_default: false,
binding_name: None,
start: 0,
end: 10,
});
info.flags |= FileUsageFlags::HAS_INJECT;
info
}
fn make_file_with_component(name: &str) -> FileUsageInfoOwned {
let mut info = make_file_info();
info.components.push(ComponentUsageOwned {
name: Some(name.to_string()),
is_dynamic: false,
start: 0,
end: 10,
});
info.flags |= FileUsageFlags::HAS_COMPONENT_USAGE;
info
}
#[test]
fn test_new_index_is_empty() {
let index = ProjectIndex::new();
assert_eq!(index.file_count(), 0);
assert_eq!(index.all_provide_keys().count(), 0);
assert_eq!(index.all_inject_keys().count(), 0);
}
#[test]
fn test_add_and_get_file() {
let mut index = ProjectIndex::new();
let path = PathBuf::from("src/App.vue");
let info = make_file_info();
index.add_file(path.clone(), info);
assert!(index.contains_file(&path));
assert_eq!(index.file_count(), 1);
assert!(index.get_file(&path).is_some());
}
#[test]
fn test_remove_file() {
let mut index = ProjectIndex::new();
let path = PathBuf::from("src/App.vue");
let info = make_file_with_provide("theme");
index.add_file(path.clone(), info);
assert_eq!(index.files_providing("theme").len(), 1);
let removed = index.remove_file(&path);
assert!(removed.is_some());
assert!(!index.contains_file(&path));
assert_eq!(index.files_providing("theme").len(), 0);
}
#[test]
fn test_provide_index() {
let mut index = ProjectIndex::new();
let app_path = PathBuf::from("src/App.vue");
let child_path = PathBuf::from("src/Child.vue");
index.add_file(app_path.clone(), make_file_with_provide("theme"));
index.add_file(child_path.clone(), make_file_with_provide("theme"));
let providers = index.files_providing("theme");
assert_eq!(providers.len(), 2);
assert!(providers.contains(&app_path));
assert!(providers.contains(&child_path));
}
#[test]
fn test_inject_index() {
let mut index = ProjectIndex::new();
let child1_path = PathBuf::from("src/Child1.vue");
let child2_path = PathBuf::from("src/Child2.vue");
index.add_file(child1_path.clone(), make_file_with_inject("theme"));
index.add_file(child2_path.clone(), make_file_with_inject("theme"));
let injectors = index.files_injecting("theme");
assert_eq!(injectors.len(), 2);
assert!(injectors.contains(&child1_path));
assert!(injectors.contains(&child2_path));
}
#[test]
fn test_validate_inject_valid() {
let mut index = ProjectIndex::new();
let provider_path = PathBuf::from("src/Provider.vue");
let consumer_path = PathBuf::from("src/Consumer.vue");
index.add_file(provider_path.clone(), make_file_with_provide("config"));
index.add_file(consumer_path.clone(), make_file_with_inject("config"));
let validation = index.validate_inject(&consumer_path, "config");
match validation {
InjectValidation::Valid { providers } => {
assert_eq!(providers.len(), 1);
assert!(providers.contains(&provider_path));
}
_ => panic!("Expected Valid validation"),
}
}
#[test]
fn test_validate_inject_no_provider() {
let mut index = ProjectIndex::new();
let consumer_path = PathBuf::from("src/Consumer.vue");
index.add_file(consumer_path.clone(), make_file_with_inject("missing"));
let validation = index.validate_inject(&consumer_path, "missing");
assert_eq!(validation, InjectValidation::NoProvider);
}
#[test]
fn test_validate_inject_dynamic_key() {
let mut index = ProjectIndex::new();
let consumer_path = PathBuf::from("src/Consumer.vue");
let mut info = make_file_with_dynamic_inject();
info.injects.push(InjectUsageOwned {
key: Some("static".to_string()),
is_dynamic_key: false,
has_default: false,
binding_name: None,
start: 20,
end: 30,
});
index.add_file(consumer_path.clone(), info);
let file_validation = index.validate_file_injects(&consumer_path);
assert_eq!(file_validation.dynamic_keys.len(), 1);
}
#[test]
fn test_validate_inject_key_not_found() {
let mut index = ProjectIndex::new();
let consumer_path = PathBuf::from("src/Consumer.vue");
index.add_file(consumer_path.clone(), make_file_with_inject("exists"));
let validation = index.validate_inject(&consumer_path, "nonexistent");
assert_eq!(validation, InjectValidation::KeyNotFound);
}
#[test]
fn test_validate_file_injects() {
let mut index = ProjectIndex::new();
let provider_path = PathBuf::from("src/Provider.vue");
let consumer_path = PathBuf::from("src/Consumer.vue");
index.add_file(provider_path.clone(), make_file_with_provide("provided"));
let mut consumer_info = make_file_info();
consumer_info.injects.push(InjectUsageOwned {
key: Some("provided".to_string()),
is_dynamic_key: false,
has_default: false,
binding_name: None,
start: 0,
end: 10,
});
consumer_info.injects.push(InjectUsageOwned {
key: Some("missing".to_string()),
is_dynamic_key: false,
has_default: false,
binding_name: None,
start: 20,
end: 30,
});
consumer_info.injects.push(InjectUsageOwned {
key: None,
is_dynamic_key: true,
has_default: false,
binding_name: None,
start: 40,
end: 50,
});
consumer_info.flags |= FileUsageFlags::HAS_INJECT;
index.add_file(consumer_path.clone(), consumer_info);
let validation = index.validate_file_injects(&consumer_path);
assert_eq!(validation.valid.len(), 1);
assert_eq!(validation.valid[0].key, "provided");
assert_eq!(validation.missing_providers.len(), 1);
assert_eq!(validation.missing_providers[0].key, "missing");
assert_eq!(validation.dynamic_keys.len(), 1);
}
#[test]
fn test_component_graph() {
let mut index = ProjectIndex::new();
let app_path = PathBuf::from("src/App.vue");
let mut app_info = make_file_info();
app_info.components.push(ComponentUsageOwned {
name: Some("Header".to_string()),
is_dynamic: false,
start: 0,
end: 10,
});
app_info.components.push(ComponentUsageOwned {
name: Some("Footer".to_string()),
is_dynamic: false,
start: 20,
end: 30,
});
index.add_file(app_path.clone(), app_info);
let components = index.components_used_by(&app_path);
assert_eq!(components.len(), 2);
let files_using_header = index.files_using_component("Header");
assert_eq!(files_using_header.len(), 1);
assert!(files_using_header.contains(&app_path));
}
#[test]
fn test_provide_inject_summary() {
let mut index = ProjectIndex::new();
index.add_file(
PathBuf::from("src/Provider.vue"),
make_file_with_provide("theme"),
);
index.add_file(
PathBuf::from("src/Provider2.vue"),
make_file_with_provide("config"),
);
index.add_file(
PathBuf::from("src/Consumer.vue"),
make_file_with_inject("theme"),
);
index.add_file(
PathBuf::from("src/Consumer2.vue"),
make_file_with_inject("missing"),
);
let summary = index.provide_inject_summary();
assert_eq!(summary.provide_keys.len(), 2); assert_eq!(summary.inject_keys.len(), 2); assert_eq!(summary.unused_provides.len(), 1); assert_eq!(summary.missing_provides.len(), 1); }
#[test]
fn test_project_stats() {
let mut index = ProjectIndex::new();
let mut info1 = make_file_with_provide("theme");
info1.flags |= FileUsageFlags::HAS_DEFINE_PROPS;
index.add_file(PathBuf::from("src/App.vue"), info1);
let mut info2 = make_file_with_inject("theme");
info2.flags |= FileUsageFlags::HAS_DEFINE_EMITS | FileUsageFlags::IS_ASYNC_SETUP;
index.add_file(PathBuf::from("src/Child.vue"), info2);
let stats = index.stats();
assert_eq!(stats.file_count, 2);
assert_eq!(stats.files_with_provide, 1);
assert_eq!(stats.files_with_inject, 1);
assert_eq!(stats.files_with_props, 1);
assert_eq!(stats.files_with_emits, 1);
assert_eq!(stats.files_with_async_setup, 1);
assert_eq!(stats.unique_provide_keys, 1);
assert_eq!(stats.unique_inject_keys, 1);
}
#[test]
fn test_update_file_reindexes() {
let mut index = ProjectIndex::new();
let path = PathBuf::from("src/App.vue");
index.add_file(path.clone(), make_file_with_provide("old"));
assert_eq!(index.files_providing("old").len(), 1);
assert_eq!(index.files_providing("new").len(), 0);
index.add_file(path.clone(), make_file_with_provide("new"));
assert_eq!(index.files_providing("old").len(), 0);
assert_eq!(index.files_providing("new").len(), 1);
}
#[test]
fn test_clear() {
let mut index = ProjectIndex::new();
index.add_file(
PathBuf::from("src/App.vue"),
make_file_with_provide("theme"),
);
assert_eq!(index.file_count(), 1);
index.clear();
assert_eq!(index.file_count(), 0);
assert_eq!(index.all_provide_keys().count(), 0);
}
#[test]
fn test_component_usage_summary() {
let mut index = ProjectIndex::new();
index.add_file(
PathBuf::from("src/App.vue"),
make_file_with_component("Header"),
);
index.add_file(
PathBuf::from("src/Page.vue"),
make_file_with_component("Header"),
);
index.add_file(
PathBuf::from("src/Other.vue"),
make_file_with_component("Footer"),
);
let summary = index.component_usage_summary();
assert_eq!(summary.component_names.len(), 2); assert_eq!(summary.usage_counts.get("Header"), Some(&2));
assert_eq!(summary.usage_counts.get("Footer"), Some(&1));
}
#[test]
fn test_file_paths_iterator() {
let mut index = ProjectIndex::new();
let path1 = PathBuf::from("src/App.vue");
let path2 = PathBuf::from("src/Child.vue");
index.add_file(path1.clone(), make_file_info());
index.add_file(path2.clone(), make_file_info());
let paths: Vec<_> = index.file_paths().collect();
assert_eq!(paths.len(), 2);
assert!(paths.contains(&&path1));
assert!(paths.contains(&&path2));
}
}