#![allow(unused)]
#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
pub mod entropy_calculator;
pub mod pattern_extractor;
pub mod violation_detector;
pub use entropy_calculator::{EntropyCalculator, EntropyMetrics, EntropyReport};
pub use pattern_extractor::{
AstPattern, Location, PatternCollection, PatternExtractor, PatternType,
};
pub use violation_detector::{ActionableViolation, PatternSummary, Severity, ViolationDetector};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntropyConfig {
pub max_pattern_repetition: usize,
pub min_pattern_diversity: f64,
pub max_cross_file_similarity: f64,
pub max_inconsistency_score: f64,
pub min_severity: Severity,
pub pattern_types: Vec<PatternType>,
pub exclude_paths: Vec<String>,
}
impl Default for EntropyConfig {
fn default() -> Self {
Self {
max_pattern_repetition: 5,
min_pattern_diversity: 0.3,
max_cross_file_similarity: 0.7,
max_inconsistency_score: 0.8,
min_severity: Severity::Medium,
pattern_types: vec![
PatternType::ErrorHandling,
PatternType::DataValidation,
PatternType::ResourceManagement,
PatternType::ControlFlow,
PatternType::DataTransformation,
PatternType::ApiCall,
],
exclude_paths: Self::analysis_excludes(false),
}
}
}
const NEVER_ANALYZED: [&str; 3] = ["**/target/**", "**/node_modules/**", "**/.git/**"];
const TEST_AND_EXAMPLE_PATHS: [&str; 8] = [
"tests/**",
"**/tests/**",
"examples/**",
"**/examples/**",
"benches/**",
"**/benches/**",
"**/*test*.rs",
"**/*.test.rs",
];
impl EntropyConfig {
#[must_use]
pub fn analysis_excludes(include_tests: bool) -> Vec<String> {
let mut excludes: Vec<String> = NEVER_ANALYZED.iter().map(|s| (*s).to_string()).collect();
if !include_tests {
excludes.extend(TEST_AND_EXAMPLE_PATHS.iter().map(|s| (*s).to_string()));
}
excludes
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_project_ignores(mut self, project_path: &std::path::Path) -> Self {
for ignore_file in &[".pmatignore", ".paimlignore"] {
let path = project_path.join(ignore_file);
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
self.exclude_paths.push(trimmed.to_string());
}
}
}
}
self
}
}
pub struct EntropyAnalyzer {
config: EntropyConfig,
pattern_extractor: PatternExtractor,
violation_detector: ViolationDetector,
entropy_calculator: EntropyCalculator,
}
impl Default for EntropyAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl EntropyAnalyzer {
#[must_use]
pub fn new() -> Self {
Self::with_config(EntropyConfig::default())
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_config(config: EntropyConfig) -> Self {
Self {
config: config.clone(),
pattern_extractor: PatternExtractor::new(config.clone()),
violation_detector: ViolationDetector::new(config.clone()),
entropy_calculator: EntropyCalculator::new(config),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze(&self, project_path: &Path) -> Result<EntropyReport> {
let patterns = self
.pattern_extractor
.extract_patterns(project_path)
.await?;
let entropy_metrics = self.entropy_calculator.calculate(&patterns)?;
let violations = self
.violation_detector
.detect_violations(&patterns, &entropy_metrics)?;
let measurement_note = Self::measurement_note(&entropy_metrics, patterns.file_count());
Ok(EntropyReport {
total_files_analyzed: patterns.file_count(),
actionable_violations: violations,
pattern_summary: patterns.summary(),
entropy_metrics,
measurement_note,
})
}
fn measurement_note(metrics: &EntropyMetrics, files: usize) -> Option<String> {
if metrics.pattern_diversity.is_some() {
return None;
}
Some(format!(
"entropy not measured: no repeated pattern was detected in {files} file(s) \
/ {loc} source line(s). A construct is only counted once it recurs, \
structurally identical, within a single file at least this many times: \
{thresholds}. Small inputs therefore legitimately yield no distribution \
to take the entropy of.",
loc = metrics.total_loc,
thresholds = Self::threshold_sentence()
))
}
fn threshold_sentence() -> String {
use crate::entropy::pattern_extractor::RUST_PATTERN_THRESHOLDS;
let mut entries: Vec<(usize, &'static str)> = RUST_PATTERN_THRESHOLDS
.iter()
.map(|t| (t.effective_minimum(), t.name))
.collect();
entries.sort_unstable();
entries
.iter()
.map(|(n, name)| format!("{name} {n}"))
.collect::<Vec<_>>()
.join(", ")
}
}
#[cfg(test)]
#[path = "determinism_tests.rs"]
mod determinism_tests;
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = EntropyConfig::default();
assert_eq!(config.max_pattern_repetition, 5);
assert_eq!(config.min_pattern_diversity, 0.3);
assert_eq!(config.max_cross_file_similarity, 0.7);
}
#[tokio::test]
async fn test_analyzer_creation() {
let analyzer = EntropyAnalyzer::new();
assert!(!analyzer.config.pattern_types.is_empty());
}
}