use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
#[serde(default)]
pub root: Option<PathBuf>,
#[serde(default)]
pub entry: EntryConfig,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub analysis: AnalysisConfig,
#[serde(default)]
pub plugins: PluginsConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
root: None,
entry: EntryConfig::default(),
include: vec![
"**/*.ts".to_string(),
"**/*.tsx".to_string(),
"**/*.js".to_string(),
"**/*.jsx".to_string(),
"**/*.mts".to_string(),
"**/*.cts".to_string(),
],
exclude: vec![
"**/node_modules/**".to_string(),
"**/dist/**".to_string(),
"**/build/**".to_string(),
"**/*.d.ts".to_string(),
"**/*.test.*".to_string(),
"**/*.spec.*".to_string(),
"**/__tests__/**".to_string(),
],
output: OutputConfig::default(),
analysis: AnalysisConfig::default(),
plugins: PluginsConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct EntryConfig {
#[serde(default)]
pub files: Vec<PathBuf>,
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default = "default_true")]
pub auto_detect: bool,
#[serde(default)]
pub exports: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputConfig {
#[serde(default)]
pub format: OutputFormat,
#[serde(default)]
pub min_confidence: ConfidenceLevel,
#[serde(default = "default_true")]
pub show_chains: bool,
#[serde(default = "default_chain_length")]
pub max_chain_length: usize,
#[serde(default = "default_true")]
pub group_by_file: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
format: OutputFormat::Table,
min_confidence: ConfidenceLevel::High,
show_chains: true,
max_chain_length: 5,
group_by_file: true,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Table,
Json,
Compact,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum ConfidenceLevel {
Low,
Medium,
#[default]
High,
}
impl ConfidenceLevel {
pub fn to_confidence(&self) -> crate::core::Confidence {
match self {
ConfidenceLevel::Low => crate::core::Confidence::Low,
ConfidenceLevel::Medium => crate::core::Confidence::Medium,
ConfidenceLevel::High => crate::core::Confidence::High,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisConfig {
#[serde(default = "default_true")]
pub include_types: bool,
#[serde(default)]
pub analyze_tests: bool,
#[serde(default)]
pub report_test_only: bool,
#[serde(default = "default_true")]
pub follow_reexports: bool,
#[serde(default = "default_max_depth")]
pub max_transitive_depth: usize,
#[serde(default)]
pub ignore_symbols: HashSet<String>,
#[serde(default)]
pub ignore_patterns: Vec<String>,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
include_types: true,
analyze_tests: false,
report_test_only: false,
follow_reexports: true,
max_transitive_depth: 50,
ignore_symbols: HashSet::new(),
ignore_patterns: vec![
"^_".to_string(), ],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PluginsConfig {
#[serde(default)]
pub enabled: Vec<String>,
#[serde(default)]
pub disabled: Vec<String>,
#[serde(default = "default_true")]
pub auto_detect: bool,
#[serde(default)]
pub nextjs: NextJsConfig,
#[serde(default)]
pub jest: JestConfig,
#[serde(default)]
pub express: ExpressConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NextJsConfig {
#[serde(default)]
pub page_dirs: Vec<PathBuf>,
#[serde(default)]
pub app_dirs: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct JestConfig {
#[serde(default)]
pub test_patterns: Vec<String>,
#[serde(default)]
pub setup_files: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ExpressConfig {
#[serde(default)]
pub middleware_patterns: Vec<String>,
}
fn default_true() -> bool {
true
}
fn default_chain_length() -> usize {
5
}
fn default_max_depth() -> usize {
50
}
impl Config {
pub fn minimal() -> Self {
Self {
exclude: vec!["**/node_modules/**".to_string()],
..Default::default()
}
}
pub fn should_include(&self, path: &std::path::Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.exclude {
if glob::Pattern::new(pattern)
.map(|p| p.matches(&path_str))
.unwrap_or(false)
{
return false;
}
}
if self.include.is_empty() {
return true;
}
for pattern in &self.include {
if glob::Pattern::new(pattern)
.map(|p| p.matches(&path_str))
.unwrap_or(false)
{
return true;
}
}
false
}
pub fn should_ignore_symbol(&self, name: &str) -> bool {
if self.analysis.ignore_symbols.contains(name) {
return true;
}
for pattern in &self.analysis.ignore_patterns {
if let Ok(re) = regex_lite::Regex::new(pattern) {
if re.is_match(name) {
return true;
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.entry.auto_detect);
assert!(config.output.show_chains);
assert_eq!(config.output.min_confidence, ConfidenceLevel::High);
}
#[test]
fn test_should_include() {
let config = Config::default();
assert!(config.should_include(std::path::Path::new("src/foo.ts")));
assert!(!config.should_include(std::path::Path::new("node_modules/foo.ts")));
assert!(!config.should_include(std::path::Path::new("src/foo.test.ts")));
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let toml_str = toml::to_string_pretty(&config).unwrap();
assert!(toml_str.contains("include"));
assert!(toml_str.contains("exclude"));
}
}