fob_graph/analysis/lib.rs
1//! # Analysis Module
2//!
3//! Graph analysis with I/O and traversal capabilities for JavaScript/TypeScript module graphs.
4//!
5//! This module provides the `Analyzer` API and related analysis functionality
6//! that operates on top of the `fob-graph` data structures. It enables fast,
7//! standalone analysis of module dependency graphs without requiring full bundling.
8//!
9//! ## Architecture
10//!
11//! ```text
12//! ┌─────────────────────────────────────────────────────────────┐
13//! │ Analyzer API │
14//! │ (Typestate pattern: Unconfigured → Configured → Analysis) │
15//! └────────────────────┬────────────────────────────────────────┘
16//! │
17//! ▼
18//! ┌─────────────────────────────────────────────────────────────┐
19//! │ GraphWalker │
20//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
21//! │ │ Traversal │ │ Parser │ │ Validation │ │
22//! │ │ (BFS) │→ │ (Extract) │→ │ (Security) │ │
23//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
24//! └────────────────────┬────────────────────────────────────────┘
25//! │
26//! ▼
27//! ┌─────────────────────────────────────────────────────────────┐
28//! │ ModuleResolver │
29//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
30//! │ │ Algorithm │ │ Aliases │ │ Extensions │ │
31//! │ │ (Resolution) │→ │ (Path maps) │→ │ (.ts, .js) │ │
32//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
33//! └────────────────────┬────────────────────────────────────────┘
34//! │
35//! ▼
36//! ┌─────────────────────────────────────────────────────────────┐
37//! │ ModuleGraph │
38//! │ (from fob-graph crate) │
39//! └─────────────────────────────────────────────────────────────┘
40//! ```
41//!
42//! ## Features
43//!
44//! - **Type-safe API**: Typestate pattern ensures analysis can only be performed
45//! after configuration is complete
46//! - **Security**: Path traversal protection and DoS limits (max depth, max modules, file size)
47//! - **Framework Support**: Extracts JavaScript/TypeScript from Astro, Svelte, and Vue components
48//! - **Path Aliases**: Supports path alias resolution (e.g., `@` → `./src`)
49//! - **External Packages**: Mark npm packages as external to skip analysis
50//! - **Usage Analysis**: Compute export usage counts across the module graph
51//!
52//! ## Quick Start
53//!
54//! ```rust,no_run
55//! use super::Analyzer;
56//!
57//! # async fn example() -> crate::Result<()> {
58//! // Create analyzer and configure entry points
59//! let analysis = Analyzer::new()
60//! .entry("src/index.ts") // Required: transitions to Configured state
61//! .external(vec!["react", "lodash"]) // Mark as external
62//! .path_alias("@", "./src") // Configure path aliases
63//! .max_depth(Some(100)) // Set DoS protection limits
64//! .analyze() // Only available on Configured
65//! .await?;
66//!
67//! // Use analysis results
68//! let unused = analysis.unused_exports()?;
69//! println!("Found {} unused exports", unused.len());
70//!
71//! let circular = analysis.find_circular_dependencies()?;
72//! println!("Found {} circular dependencies", circular.len());
73//! # Ok(())
74//! # }
75//! ```
76//!
77//! ## Module Organization
78//!
79//! - [`analyzer`] - Main `Analyzer` API with typestate pattern
80//! - [`config`] - Configuration types and constants
81//! - [`walker`] - Graph traversal and module parsing
82//! - [`walker::traversal`] - BFS traversal logic
83//! - [`walker::parser`] - Module parsing and script extraction
84//! - [`walker::validation`] - Path security validation
85//! - [`resolver`] - Module resolution algorithm
86//! - [`resolver::algorithm`] - Core resolution logic
87//! - [`resolver::aliases`] - Path alias handling
88//! - [`resolver::extensions`] - File extension resolution
89//! - [`extractors`] - Framework-specific script extractors
90//! - [`result`] - Analysis result types
91//!
92//! ## Security Considerations
93//!
94//! The analyzer includes several security features:
95//!
96//! - **Path Traversal Protection**: All paths are validated to prevent escaping
97//! the current working directory
98//! - **DoS Protection**: Limits on maximum depth, module count, and file size
99//! prevent resource exhaustion attacks
100//! - **File Size Limits**: Files larger than `MAX_FILE_SIZE` (10MB) are rejected
101//!
102//! ## Examples
103//!
104//! See the `examples/` directory for more detailed usage examples:
105//!
106//! - `basic_analysis.rs` - Simple analysis workflow
107//! - `path_aliases.rs` - Configuring and using path aliases
108//! - `circular_detection.rs` - Detecting circular dependencies
109//! - `framework_components.rs` - Analyzing framework-specific components
110
111use std::path::Path;
112use thiserror::Error;
113
114pub mod analyzer;
115pub mod cache;
116pub mod config;
117pub mod extractors;
118pub mod resolver;
119pub mod result;
120pub mod stats;
121pub mod trace;
122pub mod walker;
123
124#[cfg(test)]
125mod tests;
126
127pub use analyzer::{Analyzer, Configured, Unconfigured};
128pub use cache::{CacheAnalysis, CacheEffectiveness};
129pub use config::{AnalyzerConfig, ResolveResult};
130pub use result::AnalysisResult;
131pub use trace::{ImportOutcome, ImportResolution, RenameEvent, RenamePhase, TransformationTrace};
132
133/// Error that can occur during analysis.
134#[derive(Debug, Error)]
135pub enum AnalyzeError {
136 /// Failed to determine current working directory.
137 #[error("failed to determine current directory: {0}")]
138 CurrentDir(#[from] std::io::Error),
139
140 /// Analysis operation failed with a specific reason.
141 #[error("analysis failed: {message}")]
142 AnalysisFailed {
143 /// Human-readable error message describing what went wrong.
144 message: String,
145 /// Optional context about where the error occurred.
146 context: Option<String>,
147 },
148
149 /// A specific analysis operation is not yet implemented.
150 #[error("analysis not implemented: {0}")]
151 NotImplemented(String),
152}
153
154impl AnalyzeError {
155 /// Create an analysis failed error with a message.
156 pub fn analysis_failed(message: impl Into<String>) -> Self {
157 Self::AnalysisFailed {
158 message: message.into(),
159 context: None,
160 }
161 }
162
163 /// Create an analysis failed error with a message and context.
164 pub fn analysis_failed_with_context(
165 message: impl Into<String>,
166 context: impl Into<String>,
167 ) -> Self {
168 Self::AnalysisFailed {
169 message: message.into(),
170 context: Some(context.into()),
171 }
172 }
173}
174
175/// Options for the analyze() function.
176#[derive(Clone)]
177pub struct AnalyzeOptions {
178 /// Framework rules to apply during analysis.
179 ///
180 /// Joy does not provide any default framework rules. External tools
181 /// (like Danny) should provide framework-specific detection logic.
182 pub framework_rules: Vec<Box<dyn crate::FrameworkRule>>,
183
184 /// Whether to compute usage counts for exports.
185 ///
186 /// When enabled, each export will have its `usage_count` field populated
187 /// with the number of times it's imported across the module graph.
188 ///
189 /// Default: true
190 pub compute_usage_counts: bool,
191}
192
193impl Default for AnalyzeOptions {
194 fn default() -> Self {
195 Self {
196 framework_rules: Vec::new(),
197 compute_usage_counts: true,
198 }
199 }
200}
201
202impl std::fmt::Debug for AnalyzeOptions {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("AnalyzeOptions")
205 .field("framework_rules_count", &self.framework_rules.len())
206 .finish()
207 }
208}
209
210/// Analyze module graph with custom options.
211///
212/// # Example
213///
214/// ```rust,ignore
215/// use super::{analyze_with_options, AnalyzeOptions};
216/// use fob_graph::FrameworkRule;
217///
218/// // Define your own framework rules
219/// let options = AnalyzeOptions {
220/// framework_rules: vec![Box::new(MyCustomRule)],
221/// compute_usage_counts: true,
222/// };
223///
224/// let result = analyze_with_options(["src/index.tsx"], options).await?;
225/// ```
226pub async fn analyze_with_options<P>(
227 entries: impl IntoIterator<Item = P>,
228 options: AnalyzeOptions,
229) -> Result<AnalysisResult, AnalyzeError>
230where
231 P: AsRef<Path>,
232{
233 // Collect entries first
234 let entries: Vec<_> = entries.into_iter().collect();
235 if entries.is_empty() {
236 return Err(AnalyzeError::analysis_failed(
237 "At least one entry point is required".to_string(),
238 ));
239 }
240
241 // Build analyzer with first entry (transitions to Configured)
242 let mut analyzer: Analyzer<Configured> = Analyzer::new().entry(entries[0].as_ref());
243
244 // Add remaining entries
245 for entry in entries.into_iter().skip(1) {
246 analyzer = analyzer.entries([entry.as_ref()]);
247 }
248
249 // Analyze with options
250 analyzer
251 .analyze_with_options(options)
252 .await
253 .map_err(|e| AnalyzeError::analysis_failed(format!("{}", e)))
254}
255
256/// Convenience function using default options.
257///
258/// This analyzes the module graph without applying any framework rules.
259/// For framework-aware analysis, use `analyze_with_options` and provide
260/// framework rules explicitly.
261///
262/// # Example
263///
264/// ```rust,ignore
265/// use super::analyze;
266///
267/// let result = analyze(["src/index.tsx"]).await?;
268/// // No framework rules are applied - pure infrastructure analysis
269/// let unused = result.graph.unused_exports();
270/// ```
271pub async fn analyze<P>(
272 entries: impl IntoIterator<Item = P>,
273) -> Result<AnalysisResult, AnalyzeError>
274where
275 P: AsRef<Path>,
276{
277 analyze_with_options(entries, AnalyzeOptions::default()).await
278}