use super::super::rust_index::RustIndex;
use super::family::family_for_probe_shape;
use crate::domain::ProbeFamily;
use std::path::Path;
pub fn classify_changed_syntax(
index: &RustIndex,
file: &Path,
line: usize,
changed_text: &str,
) -> Option<Vec<ProbeFamily>> {
let facts = index.files.get(file)?;
let mut families = facts
.probe_shapes
.iter()
.filter(|shape| {
shape.start_line <= line
&& line <= shape.end_line
&& shape_contains_changed_text(&shape.text, changed_text)
})
.filter_map(|shape| family_for_probe_shape(&shape.kind))
.collect::<Vec<_>>();
if families.is_empty() {
return None;
}
families.sort_by(|a, b| a.as_str().cmp(b.as_str()));
families.dedup_by(|a, b| a.as_str() == b.as_str());
Some(families)
}
fn shape_contains_changed_text(shape_text: &str, changed_text: &str) -> bool {
let changed = changed_text
.trim()
.trim_end_matches(';')
.trim_end_matches(',');
if changed.is_empty() {
return false;
}
let shape = shape_text.trim();
shape.contains(changed) || changed.contains(shape)
}
pub fn should_ignore_changed_line(text: &str) -> bool {
text.is_empty()
|| text.starts_with("//")
|| text.starts_with("use ")
|| text.starts_with("pub use ")
|| text.starts_with("mod ")
|| text.starts_with("#")
}
#[cfg(test)]
mod tests {
use super::super::super::rust_index::{
FileFacts, PROBE_SHAPE_ERROR_PATH, PROBE_SHAPE_PREDICATE, ProbeShapeFact, RustIndex,
};
use super::*;
use std::collections::BTreeMap;
use std::path::PathBuf;
#[test]
fn classify_functions_are_callable() {
assert!(should_ignore_changed_line("// comment"));
assert!(!should_ignore_changed_line("let x = 5;"));
}
#[test]
fn classify_changed_syntax_uses_matching_probe_shape() {
let path = PathBuf::from("src/lib.rs");
let index = RustIndex {
files: BTreeMap::from([(
path.clone(),
FileFacts {
path: path.clone(),
probe_shapes: vec![
ProbeShapeFact {
start_line: 3,
end_line: 3,
start_byte: 0,
kind: PROBE_SHAPE_PREDICATE.to_string(),
text: "if amount >= threshold {".to_string(),
},
ProbeShapeFact {
start_line: 7,
end_line: 7,
start_byte: 20,
kind: PROBE_SHAPE_ERROR_PATH.to_string(),
text: "Err(AuthError::Revoked)".to_string(),
},
],
..FileFacts::default()
},
)]),
..RustIndex::default()
};
let families = classify_changed_syntax(&index, &path, 3, "amount >= threshold;");
assert_eq!(families, Some(vec![ProbeFamily::Predicate]));
}
#[test]
fn classify_changed_syntax_returns_none_without_matching_shape() {
let path = PathBuf::from("src/lib.rs");
let index = RustIndex {
files: BTreeMap::from([(
path.clone(),
FileFacts {
path: path.clone(),
probe_shapes: vec![ProbeShapeFact {
start_line: 3,
end_line: 3,
start_byte: 0,
kind: PROBE_SHAPE_PREDICATE.to_string(),
text: "if amount >= threshold {".to_string(),
}],
..FileFacts::default()
},
)]),
..RustIndex::default()
};
let families = classify_changed_syntax(&index, &path, 4, "return total");
assert_eq!(families, None);
}
}