code-moniker-check 0.5.0

Rules engine for code-moniker: DSL, rule config/profiles, evaluation over the symbol graph, and suppression.
Documentation
use std::collections::hash_map::Entry;
use std::path::PathBuf;

use code_moniker_core::lang::Lang;
use rustc_hash::FxHashMap;

use crate::check;
use code_moniker_workspace::environment::{
	self, IdentityResolver, IndexedSourceMaterial, ResourceCache,
};
use code_moniker_workspace::snapshot::{CodeIndex, LinkageSnapshot, ResourceGeneration, SymbolId};

use crate::{RuleSetRequest, RuleSeverity};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceCheckRunnerOptions {
	pub rules: PathBuf,
	pub profile: Option<String>,
	pub scheme: String,
}

impl WorkspaceCheckRunnerOptions {
	pub fn new(rules: PathBuf, profile: Option<String>, scheme: impl Into<String>) -> Self {
		Self {
			rules,
			profile,
			scheme: scheme.into(),
		}
	}
}

pub struct WorkspaceCheckRunner {
	options: WorkspaceCheckRunnerOptions,
	cache: ResourceCache,
}

impl WorkspaceCheckRunner {
	pub fn new(options: WorkspaceCheckRunnerOptions, cache: ResourceCache) -> Self {
		Self { options, cache }
	}

	pub fn run_check(
		&mut self,
		index: &CodeIndex,
		_linkage: &LinkageSnapshot,
	) -> anyhow::Result<WorkspaceRuleDiagnostics> {
		let material = environment::cached_index_material(&self.cache, index.generation)
			.ok_or_else(|| anyhow::anyhow!("code index material is unavailable"))?;
		let generation = environment::next_resource_generation(&self.cache);
		let diagnostics = collect_diagnostics(&material, &self.options)?;
		Ok(WorkspaceRuleDiagnostics::with_diagnostics(
			generation,
			index.generation,
			diagnostics,
		))
	}
}

fn collect_diagnostics(
	material: &IndexedSourceMaterial,
	options: &WorkspaceCheckRunnerOptions,
) -> anyhow::Result<Vec<WorkspaceRuleDiagnostic>> {
	let cfg = load_config(options)?;
	let excludes = check::UriExclusionMatcher::new(&cfg.exclude.uris);
	let identity = IdentityResolver::new(options.scheme.clone());
	let symbol_by_identity = material
		.symbols()
		.map(|(id, moniker)| (identity.moniker_uri(moniker), id))
		.collect::<std::collections::BTreeMap<_, _>>();
	let mut compiled: FxHashMap<Lang, check::CompiledRules> = FxHashMap::default();
	let mut diagnostics = Vec::new();
	for file in material
		.files
		.iter()
		.filter(|file| !excludes.matches_path(&file.path))
	{
		let rules = match compiled.entry(file.lang) {
			Entry::Occupied(entry) => entry.into_mut(),
			Entry::Vacant(entry) => entry.insert(
				check::compile_rules(&cfg, file.lang, &options.scheme)
					.map_err(|err| anyhow::anyhow!(err.to_string()))?,
			),
		};
		let raw =
			check::evaluate_compiled(&file.graph, &file.source, file.lang, &options.scheme, rules);
		let violations = check::apply_suppressions(&file.graph, &file.source, raw);
		diagnostics.extend(
			violations
				.into_iter()
				.map(|violation| diagnostic_from_violation(violation, &symbol_by_identity)),
		);
	}
	Ok(diagnostics)
}

fn load_config(options: &WorkspaceCheckRunnerOptions) -> anyhow::Result<check::Config> {
	RuleSetRequest::with_rules(options.rules.clone(), options.scheme.clone())
		.with_profile(options.profile.clone())
		.load_config()
}

fn diagnostic_from_violation(
	violation: check::Violation,
	symbol_by_identity: &std::collections::BTreeMap<String, SymbolId>,
) -> WorkspaceRuleDiagnostic {
	WorkspaceRuleDiagnostic::new(
		violation.rule_id,
		violation.severity,
		symbol_by_identity.get(&violation.moniker).cloned(),
		violation.message,
	)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceRuleDiagnostics {
	pub generation: ResourceGeneration,
	pub index_generation: ResourceGeneration,
	pub errors: usize,
	pub warnings: usize,
	pub diagnostics: Vec<WorkspaceRuleDiagnostic>,
}

impl WorkspaceRuleDiagnostics {
	pub fn with_diagnostics(
		generation: ResourceGeneration,
		index_generation: ResourceGeneration,
		diagnostics: Vec<WorkspaceRuleDiagnostic>,
	) -> Self {
		let errors = diagnostics
			.iter()
			.filter(|diagnostic| diagnostic.severity.is_error())
			.count();
		let warnings = diagnostics
			.iter()
			.filter(|diagnostic| diagnostic.severity.is_warn())
			.count();
		Self {
			generation,
			index_generation,
			errors,
			warnings,
			diagnostics,
		}
	}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceRuleDiagnostic {
	pub rule_id: String,
	pub severity: RuleSeverity,
	pub symbol: Option<SymbolId>,
	pub message: String,
}

impl WorkspaceRuleDiagnostic {
	pub fn new(
		rule_id: impl Into<String>,
		severity: RuleSeverity,
		symbol: Option<SymbolId>,
		message: impl Into<String>,
	) -> Self {
		Self {
			rule_id: rule_id.into(),
			severity,
			symbol,
			message: message.into(),
		}
	}
}