mod empty_handler;
mod error_context_loss;
mod ignored_failure;
mod logging_only;
mod missing_cleanup;
mod non_idempotent_retry;
mod partial_commit;
mod placeholder;
mod unbounded_retry;
mod unobserved_task;
mod unsafe_termination;
use crate::domain::capabilities::Capability;
use crate::domain::capabilities::CapabilitySet;
use crate::domain::concepts::{ConceptInstance, ConceptKind};
use crate::domain::model::LanguageModel;
use crate::domain::rule::Rule;
use crate::domain::types::{Confidence, Finding, LanguageId, Severity};
pub use empty_handler::EmptyHandlerRule;
pub use error_context_loss::ErrorContextLossRule;
pub use ignored_failure::IgnoredFailureRule;
pub use logging_only::LoggingOnlyHandlerRule;
pub use missing_cleanup::MissingCleanupRule;
pub use non_idempotent_retry::NonIdempotentRetryRule;
pub use partial_commit::PartialCommitRule;
pub use placeholder::PlaceholderHandlerRule;
pub use unbounded_retry::UnboundedRetryRule;
pub use unobserved_task::UnobservedTaskFailureRule;
pub use unsafe_termination::UnsafeTerminationRule;
pub fn catalog() -> Vec<Box<dyn Rule>> {
vec![
Box::new(IgnoredFailureRule::default()),
Box::new(UnsafeTerminationRule::default()),
Box::new(EmptyHandlerRule::default()),
Box::new(LoggingOnlyHandlerRule::default()),
Box::new(PlaceholderHandlerRule::default()),
Box::new(MissingCleanupRule::default()),
Box::new(UnboundedRetryRule::default()),
Box::new(NonIdempotentRetryRule::default()),
Box::new(ErrorContextLossRule::default()),
Box::new(PartialCommitRule::default()),
Box::new(UnobservedTaskFailureRule::default()),
]
}
pub(crate) fn base_caps() -> CapabilitySet {
CapabilitySet::new([Capability::ErrorFlow, Capability::ExceptionOrResult])
}
pub(crate) fn async_caps() -> CapabilitySet {
CapabilitySet::new([
Capability::ErrorFlow,
Capability::ExceptionOrResult,
Capability::AsyncConcurrency,
])
}
pub(crate) fn finding_from_concept(
rule_id: &str,
language: &LanguageId,
c: &ConceptInstance,
severity: Severity,
confidence: Confidence,
explanation: impl Into<String>,
failure: impl Into<String>,
remediation: impl Into<String>,
) -> Finding {
Finding::builder(rule_id, language.clone())
.severity(severity)
.confidence(confidence)
.location(c.location.clone())
.explanation(explanation)
.failure_scenario(failure)
.remediation(remediation)
.evidence("concept", format!("{:?}:{}", c.kind, c.tag))
.evidence("surface", c.surface.clone())
.build()
}
pub(crate) fn concepts_of<'a>(
model: &'a LanguageModel,
kind: ConceptKind,
) -> impl Iterator<Item = (&'a LanguageId, &'a ConceptInstance)> + 'a {
model
.files
.iter()
.flat_map(move |f| {
f.concepts
.iter()
.filter(move |c| c.kind == kind)
.map(move |c| (&f.language, c))
})
}
#[cfg(test)]
pub fn test_concept(
kind: ConceptKind,
tag: &str,
line: u32,
) -> ConceptInstance {
use crate::domain::types::SourceLocation;
ConceptInstance::new(
kind,
SourceLocation::single_line("test.rs", line, 1),
tag,
format!("{tag} @ {line}"),
)
}
#[cfg(test)]
mod domain_rule_tests {
use super::*;
use crate::domain::model::FileModel;
use std::path::PathBuf;
fn model_with(concepts: Vec<ConceptInstance>) -> LanguageModel {
LanguageModel {
language: LanguageId::rust(),
files: vec![FileModel {
path: PathBuf::from("test.rs"),
language: LanguageId::rust(),
concepts,
control_flow: Default::default(),
error_paths: vec![],
generated: false,
}],
}
}
#[test]
fn ignored_failure_fires() {
let m = model_with(vec![test_concept(ConceptKind::IgnoredFailure, "let_underscore", 1)]);
let findings = IgnoredFailureRule::default().evaluate(&m);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].rule_id, "ignored-failure");
}
#[test]
fn unsafe_termination_fires() {
let m = model_with(vec![test_concept(ConceptKind::Termination, "unwrap", 2)]);
let findings = UnsafeTerminationRule::default().evaluate(&m);
assert!(!findings.is_empty());
}
#[test]
fn empty_catalog_rules_have_unique_ids() {
let rules = catalog();
let mut ids: Vec<_> = rules.iter().map(|r| r.meta().id.clone()).collect();
ids.sort();
let mut dedup = ids.clone();
dedup.dedup();
assert_eq!(ids, dedup);
}
}