use super::super::analyzers;
use super::super::graph::{DependencyEdge, DependencyGraph, ModuleNode};
use super::super::registry::{FileId, ModuleRegistry};
use super::types::{CrossFileOptions, CrossFileResult, CrossFileStats};
use crate::{Analyzer, AnalyzerOptions, Croquis};
use std::path::Path;
pub struct CrossFileAnalyzer {
options: CrossFileOptions,
registry: ModuleRegistry,
graph: DependencyGraph,
single_file_options: AnalyzerOptions,
}
impl CrossFileAnalyzer {
pub fn new(options: CrossFileOptions) -> Self {
Self {
options,
registry: ModuleRegistry::new(),
graph: DependencyGraph::new(),
single_file_options: AnalyzerOptions::full(),
}
}
pub fn with_project_root(options: CrossFileOptions, root: impl AsRef<Path>) -> Self {
Self {
options,
registry: ModuleRegistry::with_project_root(root.as_ref()),
graph: DependencyGraph::new(),
single_file_options: AnalyzerOptions::full(),
}
}
pub fn set_single_file_options(&mut self, options: AnalyzerOptions) {
self.single_file_options = options;
}
pub fn add_file(&mut self, path: impl AsRef<Path>, source: &str) -> FileId {
let path = path.as_ref();
let analysis = self.analyze_single_file(source, path);
let (file_id, is_new) = self.registry.register(path, source, analysis);
if is_new {
let mut node = ModuleNode::new(file_id, path.to_string_lossy().as_ref());
if let Some(entry) = self.registry.get(file_id) {
node.component_name = entry.component_name.clone();
}
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename == "App.vue"
|| filename == "main.ts"
|| filename == "main.js"
|| filename == "index.vue"
{
node.is_entry = true;
}
self.graph.add_node(node);
}
if let Some(entry) = self.registry.get(file_id) {
let imports_data: Vec<_> = entry
.analysis
.scopes
.iter()
.filter(|s| s.kind == crate::scope::ScopeKind::ExternalModule)
.filter_map(|s| {
if let crate::scope::ScopeData::ExternalModule(data) = s.data() {
Some((data.source.clone(), data.is_type_only))
} else {
None
}
})
.collect();
let used_components: Vec<_> = entry.analysis.used_components.iter().cloned().collect();
for (source, is_type_only) in imports_data {
if let Some(target_id) = self.resolve_import(&source) {
let edge_type = if is_type_only {
DependencyEdge::TypeImport
} else {
DependencyEdge::Import
};
self.graph.add_edge(file_id, target_id, edge_type);
}
}
for component in used_components {
if let Some(target_id) = self.graph.find_by_component(component.as_str()) {
self.graph
.add_edge(file_id, target_id, DependencyEdge::ComponentUsage);
}
}
}
file_id
}
pub fn add_files(&mut self, files: &[(&Path, &str)]) {
for (path, source) in files {
self.add_file(path, source);
}
}
pub fn add_file_with_analysis(
&mut self,
path: impl AsRef<Path>,
source: &str,
analysis: Croquis,
) -> FileId {
let path = path.as_ref();
let (file_id, is_new) = self.registry.register(path, source, analysis);
if is_new {
let mut node = ModuleNode::new(file_id, path.to_string_lossy().as_ref());
if let Some(entry) = self.registry.get(file_id) {
node.component_name = entry.component_name.clone();
}
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename == "App.vue"
|| filename == "main.ts"
|| filename == "main.js"
|| filename == "index.vue"
{
node.is_entry = true;
}
self.graph.add_node(node);
}
if let Some(entry) = self.registry.get(file_id) {
let imports_data: Vec<_> = entry
.analysis
.scopes
.iter()
.filter(|s| s.kind == crate::scope::ScopeKind::ExternalModule)
.filter_map(|s| {
if let crate::scope::ScopeData::ExternalModule(data) = s.data() {
Some((data.source.clone(), data.is_type_only))
} else {
None
}
})
.collect();
let used_components: Vec<_> = entry.analysis.used_components.iter().cloned().collect();
for (source, is_type_only) in imports_data {
if let Some(target_id) = self.resolve_import(&source) {
let edge_type = if is_type_only {
DependencyEdge::TypeImport
} else {
DependencyEdge::Import
};
self.graph.add_edge(file_id, target_id, edge_type);
}
}
for component in used_components {
if let Some(target_id) = self.graph.find_by_component(component.as_str()) {
self.graph
.add_edge(file_id, target_id, DependencyEdge::ComponentUsage);
}
}
}
file_id
}
pub fn rebuild_component_edges(&mut self) {
let component_data: Vec<_> = self
.registry
.iter()
.map(|entry| {
let components: Vec<_> = entry.analysis.used_components.iter().cloned().collect();
(entry.id, components)
})
.collect();
for (file_id, used_components) in component_data {
for component in used_components {
if let Some(target_id) = self.graph.find_by_component(component.as_str()) {
self.graph
.add_edge(file_id, target_id, DependencyEdge::ComponentUsage);
}
}
}
}
pub fn analyze(&mut self) -> CrossFileResult {
#[cfg(not(target_arch = "wasm32"))]
let start_time = std::time::Instant::now();
let mut result = CrossFileResult::default();
if self.options.circular_dependencies {
self.graph.detect_circular_dependencies();
result.circular_deps = self.graph.circular_dependencies().to_vec();
}
if self.options.fallthrough_attrs {
let (info, diags) = analyzers::analyze_fallthrough(&self.registry, &self.graph);
result.fallthrough_info = info;
result.diagnostics.extend(diags);
}
if self.options.component_emits {
let (flows, diags) = analyzers::analyze_emits(&self.registry, &self.graph);
result.emit_flows = flows;
result.diagnostics.extend(diags);
}
if self.options.event_bubbling {
let (bubbles, diags) = analyzers::analyze_event_bubbling(&self.registry, &self.graph);
result.event_bubbles = bubbles;
result.diagnostics.extend(diags);
}
if self.options.provide_inject {
let (matches, diags) = analyzers::analyze_provide_inject(&self.registry, &self.graph);
result.provide_inject_matches = matches;
result.diagnostics.extend(diags);
}
if self.options.unique_ids {
let (issues, diags) = analyzers::analyze_element_ids(&self.registry);
result.unique_id_issues = issues;
result.diagnostics.extend(diags);
}
if self.options.server_client_boundary || self.options.error_suspense_boundary {
let (boundaries, diags) = analyzers::analyze_boundaries(&self.registry, &self.graph);
result.boundaries = boundaries;
result.diagnostics.extend(diags);
}
if self.options.reactivity_tracking {
let (issues, diags) = analyzers::analyze_reactivity(&self.registry, &self.graph);
result.reactivity_issues = issues;
result.diagnostics.extend(diags);
let (cross_issues, cross_diags) =
analyzers::analyze_cross_file_reactivity(&self.registry, &self.graph);
result.cross_file_reactivity_issues = cross_issues;
result.diagnostics.extend(cross_diags);
}
if self.options.setup_context {
let (issues, diags) = analyzers::analyze_setup_context(&self.registry, &self.graph);
result.setup_context_issues = issues;
result.diagnostics.extend(diags);
}
if self.options.component_resolution {
let (issues, diags) =
analyzers::analyze_component_resolution(&self.registry, &self.graph);
result.component_resolution_issues = issues;
result.diagnostics.extend(diags);
}
if self.options.props_validation {
let (issues, diags) = analyzers::analyze_props_validation(&self.registry, &self.graph);
result.props_validation_issues = issues;
result.diagnostics.extend(diags);
}
let error_count = result.diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = result.diagnostics.iter().filter(|d| d.is_warning()).count();
#[cfg(not(target_arch = "wasm32"))]
let analysis_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
#[cfg(target_arch = "wasm32")]
let analysis_time_ms = 0.0;
result.stats = CrossFileStats {
files_analyzed: self.registry.len(),
vue_components: self.registry.vue_components().count(),
dependency_edges: self.count_edges(),
error_count,
warning_count,
info_count: result.diagnostics.len() - error_count - warning_count,
analysis_time_ms,
};
result
}
#[inline]
pub fn registry(&self) -> &ModuleRegistry {
&self.registry
}
#[inline]
pub fn graph(&self) -> &DependencyGraph {
&self.graph
}
pub fn get_analysis(&self, file_id: FileId) -> Option<&Croquis> {
self.registry.get(file_id).map(|e| &e.analysis)
}
pub fn get_file_path(&self, file_id: FileId) -> Option<&Path> {
self.registry.get(file_id).map(|e| e.path.as_path())
}
pub fn clear(&mut self) {
self.registry.clear();
self.graph = DependencyGraph::new();
}
fn analyze_single_file(&self, source: &str, path: &Path) -> Croquis {
let mut analyzer = Analyzer::with_options(self.single_file_options);
let is_vue = path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("vue"));
if is_vue {
analyzer.analyze_script_setup(source);
} else {
analyzer.analyze_script_plain(source);
}
analyzer.finish()
}
fn resolve_import(&self, specifier: &str) -> Option<FileId> {
if specifier.starts_with('.') {
return None;
}
for entry in self.registry.iter() {
if entry.filename.as_str() == specifier || {
#[allow(clippy::disallowed_macros)]
let vue_name = format!("{}.vue", specifier);
entry.filename.as_str() == vue_name
} {
return Some(entry.id);
}
}
None
}
fn count_edges(&self) -> usize {
self.graph.nodes().map(|n| n.imports.len()).sum()
}
}
impl Default for CrossFileAnalyzer {
fn default() -> Self {
Self::new(CrossFileOptions::default())
}
}