pub mod architecture;
pub mod init;
pub mod sections;
use globset::GlobSet;
use serde::Deserialize;
use std::path::Path;
pub use architecture::ArchitectureConfig;
pub use init::{generate_default_config, generate_tailored_config};
use sections::DEFAULT_MAX_SUPPRESSION_RATIO;
pub use sections::{
BoilerplateConfig, ComplexityConfig, CouplingConfig, DuplicatesConfig, ReportConfig, SrpConfig,
StructuralConfig, TestConfig, WeightsConfig,
};
#[derive(Debug, Deserialize, Clone)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub ignore_functions: Vec<String>,
pub exclude_files: Vec<String>,
pub strict_closures: bool,
pub strict_iterator_chains: bool,
pub allow_recursion: bool,
pub strict_error_propagation: bool,
pub max_suppression_ratio: f64,
pub fail_on_warnings: bool,
pub complexity: ComplexityConfig,
pub duplicates: DuplicatesConfig,
pub boilerplate: BoilerplateConfig,
pub srp: SrpConfig,
pub coupling: CouplingConfig,
pub structural: StructuralConfig,
pub test_quality: TestConfig,
pub architecture: ArchitectureConfig,
pub weights: WeightsConfig,
pub report: ReportConfig,
#[serde(skip)]
compiled_ignore_fns: Option<GlobSet>,
#[serde(skip)]
compiled_exclude_files: Option<GlobSet>,
}
impl Default for Config {
fn default() -> Self {
Self {
ignore_functions: vec![],
exclude_files: vec![],
strict_closures: false,
strict_iterator_chains: false,
allow_recursion: false,
strict_error_propagation: false,
max_suppression_ratio: DEFAULT_MAX_SUPPRESSION_RATIO,
fail_on_warnings: false,
complexity: ComplexityConfig::default(),
duplicates: DuplicatesConfig::default(),
boilerplate: BoilerplateConfig::default(),
srp: SrpConfig::default(),
coupling: CouplingConfig::default(),
structural: StructuralConfig::default(),
test_quality: TestConfig::default(),
architecture: ArchitectureConfig::default(),
weights: WeightsConfig::default(),
report: ReportConfig::default(),
compiled_ignore_fns: None,
compiled_exclude_files: None,
}
}
}
fn build_globset(patterns: &[String]) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
match globset::Glob::new(pattern) {
Ok(g) => {
builder.add(g);
}
Err(e) => {
eprintln!("Warning: Invalid glob pattern '{pattern}': {e}");
}
}
}
builder
.build()
.unwrap_or_else(|_| globset::GlobSet::empty())
}
fn match_any_pattern(patterns: &[String], compiled: &Option<GlobSet>, target: &str) -> bool {
if let Some(ref gs) = compiled {
return gs.is_match(target);
}
patterns.iter().any(|pattern| {
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
globset::Glob::new(pattern)
.ok()
.and_then(|g| g.compile_matcher().is_match(target).then_some(()))
.is_some()
} else {
target == pattern
}
})
}
const CONFIG_FILE_NAME: &str = "rustqual.toml";
fn find_config_file(project_root: &Path) -> Option<std::path::PathBuf> {
let start = if project_root.is_file() {
project_root.parent().unwrap_or(project_root)
} else {
project_root
};
let mut dir = Some(start);
while let Some(d) = dir {
let candidate = d.join(CONFIG_FILE_NAME);
if candidate.exists() {
return Some(candidate);
}
dir = d.parent();
}
None
}
impl Config {
pub fn compile(&mut self) {
self.compiled_ignore_fns = Some(build_globset(&self.ignore_functions));
self.compiled_exclude_files = Some(build_globset(&self.exclude_files));
}
pub fn load_from_file(config_path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(config_path)
.map_err(|e| format!("Failed to read {}: {e}", config_path.display()))?;
toml::from_str(&content)
.map_err(|e| format!("Failed to parse {}: {e}", config_path.display()))
}
pub fn load(project_root: &Path) -> Result<Self, String> {
find_config_file(project_root)
.map(|p| Self::load_from_file(&p))
.unwrap_or_else(|| Ok(Self::default()))
}
pub fn is_ignored_function(&self, name: &str) -> bool {
match_any_pattern(&self.ignore_functions, &self.compiled_ignore_fns, name)
}
pub fn is_excluded_file(&self, path: &str) -> bool {
match_any_pattern(&self.exclude_files, &self.compiled_exclude_files, path)
}
}
pub fn validate_weights(config: &Config) -> Result<(), String> {
let w = &config.weights;
let sum = w.iosp + w.complexity + w.dry + w.srp + w.coupling + w.test_quality + w.architecture;
if (sum - 1.0).abs() > sections::WEIGHT_SUM_TOLERANCE {
return Err(format!(
"Quality weights must sum to 1.0, but sum is {sum:.4}. \
Check [weights] in rustqual.toml."
));
}
Ok(())
}
#[cfg(test)]
mod tests;