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}