Skip to main content

xbp_analysis/application/rules/
mod.rs

1//! Language-independent rules over [`crate::domain::model::LanguageModel`].
2
3mod empty_handler;
4mod error_context_loss;
5mod ignored_failure;
6mod logging_only;
7mod missing_cleanup;
8mod non_idempotent_retry;
9mod partial_commit;
10mod placeholder;
11mod unbounded_retry;
12mod unobserved_task;
13mod unsafe_termination;
14
15use crate::domain::capabilities::Capability;
16use crate::domain::capabilities::CapabilitySet;
17use crate::domain::concepts::{ConceptInstance, ConceptKind};
18use crate::domain::model::LanguageModel;
19use crate::domain::rule::Rule;
20use crate::domain::types::{Confidence, Finding, LanguageId, Severity};
21
22pub use empty_handler::EmptyHandlerRule;
23pub use error_context_loss::ErrorContextLossRule;
24pub use ignored_failure::IgnoredFailureRule;
25pub use logging_only::LoggingOnlyHandlerRule;
26pub use missing_cleanup::MissingCleanupRule;
27pub use non_idempotent_retry::NonIdempotentRetryRule;
28pub use partial_commit::PartialCommitRule;
29pub use placeholder::PlaceholderHandlerRule;
30pub use unbounded_retry::UnboundedRetryRule;
31pub use unobserved_task::UnobservedTaskFailureRule;
32pub use unsafe_termination::UnsafeTerminationRule;
33
34/// All built-in language-independent rules.
35pub fn catalog() -> Vec<Box<dyn Rule>> {
36    vec![
37        Box::new(IgnoredFailureRule::default()),
38        Box::new(UnsafeTerminationRule::default()),
39        Box::new(EmptyHandlerRule::default()),
40        Box::new(LoggingOnlyHandlerRule::default()),
41        Box::new(PlaceholderHandlerRule::default()),
42        Box::new(MissingCleanupRule::default()),
43        Box::new(UnboundedRetryRule::default()),
44        Box::new(NonIdempotentRetryRule::default()),
45        Box::new(ErrorContextLossRule::default()),
46        Box::new(PartialCommitRule::default()),
47        Box::new(UnobservedTaskFailureRule::default()),
48    ]
49}
50
51pub(crate) fn base_caps() -> CapabilitySet {
52    CapabilitySet::new([Capability::ErrorFlow, Capability::ExceptionOrResult])
53}
54
55pub(crate) fn async_caps() -> CapabilitySet {
56    CapabilitySet::new([
57        Capability::ErrorFlow,
58        Capability::ExceptionOrResult,
59        Capability::AsyncConcurrency,
60    ])
61}
62
63pub(crate) fn finding_from_concept(
64    rule_id: &str,
65    language: &LanguageId,
66    c: &ConceptInstance,
67    severity: Severity,
68    confidence: Confidence,
69    explanation: impl Into<String>,
70    failure: impl Into<String>,
71    remediation: impl Into<String>,
72) -> Finding {
73    Finding::builder(rule_id, language.clone())
74        .severity(severity)
75        .confidence(confidence)
76        .location(c.location.clone())
77        .explanation(explanation)
78        .failure_scenario(failure)
79        .remediation(remediation)
80        .evidence("concept", format!("{:?}:{}", c.kind, c.tag))
81        .evidence("surface", c.surface.clone())
82        .build()
83}
84
85pub(crate) fn concepts_of<'a>(
86    model: &'a LanguageModel,
87    kind: ConceptKind,
88) -> impl Iterator<Item = (&'a LanguageId, &'a ConceptInstance)> + 'a {
89    model
90        .files
91        .iter()
92        .flat_map(move |f| {
93            f.concepts
94                .iter()
95                .filter(move |c| c.kind == kind)
96                .map(move |c| (&f.language, c))
97        })
98}
99
100/// Helper to attach a synthetic location for unit tests.
101#[cfg(test)]
102pub fn test_concept(
103    kind: ConceptKind,
104    tag: &str,
105    line: u32,
106) -> ConceptInstance {
107    use crate::domain::types::SourceLocation;
108    ConceptInstance::new(
109        kind,
110        SourceLocation::single_line("test.rs", line, 1),
111        tag,
112        format!("{tag} @ {line}"),
113    )
114}
115
116#[cfg(test)]
117mod domain_rule_tests {
118    use super::*;
119    use crate::domain::model::FileModel;
120    use std::path::PathBuf;
121
122    fn model_with(concepts: Vec<ConceptInstance>) -> LanguageModel {
123        LanguageModel {
124            language: LanguageId::rust(),
125            files: vec![FileModel {
126                path: PathBuf::from("test.rs"),
127                language: LanguageId::rust(),
128                concepts,
129                control_flow: Default::default(),
130                error_paths: vec![],
131                generated: false,
132            }],
133        }
134    }
135
136    #[test]
137    fn ignored_failure_fires() {
138        let m = model_with(vec![test_concept(ConceptKind::IgnoredFailure, "let_underscore", 1)]);
139        let findings = IgnoredFailureRule::default().evaluate(&m);
140        assert_eq!(findings.len(), 1);
141        assert_eq!(findings[0].rule_id, "ignored-failure");
142    }
143
144    #[test]
145    fn unsafe_termination_fires() {
146        let m = model_with(vec![test_concept(ConceptKind::Termination, "unwrap", 2)]);
147        let findings = UnsafeTerminationRule::default().evaluate(&m);
148        assert!(!findings.is_empty());
149    }
150
151    #[test]
152    fn empty_catalog_rules_have_unique_ids() {
153        let rules = catalog();
154        let mut ids: Vec<_> = rules.iter().map(|r| r.meta().id.clone()).collect();
155        ids.sort();
156        let mut dedup = ids.clone();
157        dedup.dedup();
158        assert_eq!(ids, dedup);
159    }
160}