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 framework 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 fob_graph::{Analyzer, Result};
56//!
57//! # async fn example() -> 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,no_run
215/// use fob_graph::analysis::{analyze_with_options, AnalyzeOptions};
216/// use fob_graph::{FrameworkRule, ModuleGraph, Result};
217/// use async_trait::async_trait;
218///
219/// // Define your own framework rules
220/// #[derive(Clone)]
221/// struct MyCustomRule;
222///
223/// #[async_trait]
224/// impl FrameworkRule for MyCustomRule {
225/// async fn apply(&self, _graph: &ModuleGraph) -> Result<()> { Ok(()) }
226/// fn name(&self) -> &'static str { "my-rule" }
227/// fn description(&self) -> &'static str { "My Custom Rule" }
228/// fn clone_box(&self) -> Box<dyn FrameworkRule> { Box::new(self.clone()) }
229/// }
230///
231/// # async fn run() -> Result<()> {
232/// let options = AnalyzeOptions {
233/// framework_rules: vec![Box::new(MyCustomRule)],
234/// compute_usage_counts: true,
235/// };
236///
237/// let result = analyze_with_options(["src/index.tsx"], options).await
238/// .map_err(|e| fob_graph::Error::Operation(e.to_string()))?;
239/// # Ok(())
240/// # }
241/// ```
242pub async fn analyze_with_options<P>(
243 entries: impl IntoIterator<Item = P>,
244 options: AnalyzeOptions,
245) -> Result<AnalysisResult, AnalyzeError>
246where
247 P: AsRef<Path>,
248{
249 // Collect entries first
250 let entries: Vec<_> = entries.into_iter().collect();
251 if entries.is_empty() {
252 return Err(AnalyzeError::analysis_failed(
253 "At least one entry point is required".to_string(),
254 ));
255 }
256
257 // Build analyzer with first entry (transitions to Configured)
258 let mut analyzer: Analyzer<Configured> = Analyzer::new().entry(entries[0].as_ref());
259
260 // Add remaining entries
261 for entry in entries.into_iter().skip(1) {
262 analyzer = analyzer.entries([entry.as_ref()]);
263 }
264
265 // Analyze with options
266 analyzer
267 .analyze_with_options(options)
268 .await
269 .map_err(|e| AnalyzeError::analysis_failed(format!("{}", e)))
270}
271
272/// Convenience function using default options.
273///
274/// This analyzes the module graph without applying any framework rules.
275/// For framework-aware analysis, use `analyze_with_options` and provide
276/// framework rules explicitly.
277///
278/// # Example
279///
280/// ```rust,no_run
281/// use fob_graph::analysis::analyze;
282/// use fob_graph::Result;
283///
284/// # async fn run() -> Result<()> {
285/// let result = analyze(["src/index.tsx"]).await
286/// .map_err(|e| fob_graph::Error::Operation(e.to_string()))?;
287/// // No framework rules are applied - pure infrastructure analysis
288/// let unused = result.graph.unused_exports();
289/// # Ok(())
290/// # }
291/// ```
292pub async fn analyze<P>(
293 entries: impl IntoIterator<Item = P>,
294) -> Result<AnalysisResult, AnalyzeError>
295where
296 P: AsRef<Path>,
297{
298 analyze_with_options(entries, AnalyzeOptions::default()).await
299}