use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::json;
use super::decision::Questions;
use super::report::Finding;
pub const CONFLICT_ABOVE: f64 = 0.7;
const MAX_SOURCE_CHARS: usize = 20_000;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Conflict {
pub requirement: String,
pub probability: f64,
}
pub struct Other<'a> {
pub id: &'a str,
pub must: &'a str,
pub why: &'a str,
}
pub fn questions(
finding: &Finding,
must: &str,
source: Option<&str>,
others: &[Other],
) -> Questions {
let mut state = json!({
"file": finding.file,
"finding": {
"requirement": finding.requirement,
"must": must,
"line": finding.line,
"message": finding.message,
"fix": finding.fix_hint,
},
});
if let Some(source) = source {
state["source"] = json!(cut(source));
}
let questions = others
.iter()
.map(|other| {
let question = json!({
"type": "noul",
"instructions": {
"subject": "the change `finding.fix` asks for in `file`",
"question": format!(
"Would making this change break this other requirement? {}: {} (why: {})",
other.id, other.must, other.why
),
},
"criteria": {
"true": "Making the change the way the finding asks would leave the file, or the code around it, violating the other requirement, so the two requirements cannot both hold here.",
"false": "The change can be made while still meeting the other requirement, or the other requirement does not concern this change.",
},
});
(other.id.to_string(), question)
})
.collect();
Questions { state, questions }
}
fn cut(source: &str) -> String {
match source.char_indices().nth(MAX_SOURCE_CHARS) {
None => source.to_string(),
Some((at, _)) => format!("{}\n… (cut)", &source[..at]),
}
}
pub fn conflicts(answers: impl IntoIterator<Item = (String, f64)>) -> Vec<Conflict> {
let mut found: Vec<Conflict> = answers
.into_iter()
.filter(|(_, p)| *p > CONFLICT_ABOVE)
.map(|(requirement, probability)| Conflict {
requirement,
probability,
})
.collect();
found.sort_by(|a, b| b.probability.total_cmp(&a.probability));
found
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Decision {
pub requirement: String,
pub breaks: String,
pub files: Vec<String>,
pub probability: f64,
}
pub fn decisions(findings: &[Finding]) -> Vec<Decision> {
let mut pairs: BTreeMap<(String, String), Decision> = BTreeMap::new();
for finding in findings {
for conflict in &finding.conflicts {
let decision = pairs
.entry((finding.requirement.clone(), conflict.requirement.clone()))
.or_insert_with(|| Decision {
requirement: finding.requirement.clone(),
breaks: conflict.requirement.clone(),
files: Vec::new(),
probability: 0.0,
});
if let Some(file) = &finding.file
&& !decision.files.contains(file)
{
decision.files.push(file.clone());
}
decision.probability = decision.probability.max(conflict.probability);
}
}
pairs.into_values().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::report::FindingKind;
fn finding(requirement: &str, file: &str, conflicts: &[(&str, f64)]) -> Finding {
Finding {
requirement: requirement.into(),
kind: FindingKind::Violation,
file: Some(file.into()),
line: Some(1),
message: "m".into(),
fix_hint: "f".into(),
probability: None,
model: None,
block: None,
source: None,
conflicts: conflicts
.iter()
.map(|(r, p)| Conflict {
requirement: (*r).into(),
probability: *p,
})
.collect(),
}
}
#[test]
fn only_answers_above_the_threshold_are_conflicts() {
let found = conflicts([("A".into(), 0.9), ("B".into(), 0.7), ("C".into(), 0.95)]);
assert_eq!(
found
.iter()
.map(|c| c.requirement.as_str())
.collect::<Vec<_>>(),
["C", "A"]
);
}
#[test]
fn decisions_group_files_by_pair_and_keep_the_highest_probability() {
let findings = [
finding("COLOCATION", "core/a.rs", &[("CORE", 0.8)]),
finding("COLOCATION", "core/b.rs", &[("CORE", 0.9)]),
finding("OTHER", "x.rs", &[]),
];
assert_eq!(
decisions(&findings),
[Decision {
requirement: "COLOCATION".into(),
breaks: "CORE".into(),
files: vec!["core/a.rs".into(), "core/b.rs".into()],
probability: 0.9,
}]
);
}
}