xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! Language-independent rules over [`crate::domain::model::LanguageModel`].

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;

/// All built-in language-independent rules.
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))
        })
}

/// Helper to attach a synthetic location for unit tests.
#[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);
    }
}