xbp_analysis/application/rules/
mod.rs1mod 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
34pub 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#[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}