impl DriftDetector {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(registry: CommandRegistry) -> Self {
Self {
registry,
command_regex: Regex::new(r"pmat\s+([\w\-]+(?:\s+[\w\-]+)*)").expect("internal error"),
code_block_regex: Regex::new(r"(?:```(?:bash|shell|sh)?\n|\$\s*)(pmat[^\n`]+)")
.expect("internal error"),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_in_file(&self, path: &Path) -> Result<Vec<DriftError>, std::io::Error> {
let content = std::fs::read_to_string(path)?;
let file_name = path.to_string_lossy().to_string();
Ok(self.detect_in_content(&content, &file_name))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn detect_in_content(&self, content: &str, file_name: &str) -> Vec<DriftError> {
let mut errors = Vec::new();
let lines: Vec<&str> = content.lines().collect();
for (line_idx, line) in lines.iter().enumerate() {
let line_num = line_idx + 1;
for cap in self.command_regex.captures_iter(line) {
let cmd_path = cap.get(1).expect("internal error").as_str();
let is_deprecated_context = line.to_lowercase().contains("deprecated");
if !self.command_exists(cmd_path) {
errors.push(DriftError::NonExistentCommand {
mentioned: cmd_path.to_string(),
file: file_name.to_string(),
line: line_num,
suggestion: self.find_similar_command(cmd_path),
});
} else if let Some(cmd) = self.registry.find_command(cmd_path) {
if cmd.deprecated.is_some() && !is_deprecated_context {
errors.push(DriftError::DeprecatedWithoutWarning {
command: cmd_path.to_string(),
file: file_name.to_string(),
line: line_num,
});
}
}
}
}
for cap in self.code_block_regex.captures_iter(content) {
let example = cap.get(1).expect("internal error").as_str().trim();
if let Some(error) = self.validate_example(example, file_name) {
errors.push(error);
}
}
errors
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn generate_report(&self, paths: &[&Path]) -> DriftReport {
let mut all_errors = Vec::new();
let mut documented_commands = HashSet::new();
for path in paths {
if let Ok(content) = std::fs::read_to_string(path) {
let file_name = path.to_string_lossy().to_string();
let errors = self.detect_in_content(&content, &file_name);
all_errors.extend(errors);
for cap in self.command_regex.captures_iter(&content) {
let cmd_path = cap.get(1).expect("internal error").as_str();
if self.command_exists(cmd_path) {
documented_commands.insert(cmd_path.to_string());
}
}
}
}
let all_commands: HashSet<_> = self.registry.all_command_paths().into_iter().collect();
let undocumented: HashSet<_> = all_commands
.difference(&documented_commands)
.filter(|cmd| self.is_user_facing(cmd))
.cloned()
.collect();
let total = all_commands.len();
let coverage = if total > 0 {
(documented_commands.len() as f64 / total as f64) * 100.0
} else {
100.0
};
DriftReport {
errors: all_errors,
documented_commands,
undocumented_commands: undocumented,
total_commands: total,
coverage,
}
}
fn command_exists(&self, path: &str) -> bool {
self.registry.find_command(path).is_some()
}
fn find_similar_command(&self, query: &str) -> Option<String> {
let all_commands = self.registry.all_command_paths();
all_commands
.into_iter()
.min_by_key(|cmd| levenshtein(&cmd.to_lowercase(), &query.to_lowercase()))
.filter(|cmd| levenshtein(&cmd.to_lowercase(), &query.to_lowercase()) <= 3)
}
fn is_user_facing(&self, command: &str) -> bool {
if let Some(cmd) = self.registry.find_command(command) {
!cmd.category.to_lowercase().contains("internal")
} else {
false
}
}
fn validate_example(&self, example: &str, file_name: &str) -> Option<DriftError> {
let parts: Vec<&str> = example.split_whitespace().collect();
if parts.len() < 2 || parts[0] != "pmat" {
return None;
}
let mut cmd_path = parts[1].to_string();
if parts.len() > 2 && !parts[2].starts_with('-') {
let extended_path = format!("{} {}", cmd_path, parts[2]);
if self.command_exists(&extended_path) {
cmd_path = extended_path;
}
}
if !self.command_exists(&cmd_path) {
return Some(DriftError::InvalidExample {
example: example.to_string(),
file: file_name.to_string(),
line: 0, reason: format!("command '{}' doesn't exist", cmd_path),
});
}
None
}
}
fn levenshtein(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let a_len = a_chars.len();
let b_len = b_chars.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut matrix = vec![vec![0usize; b_len + 1]; a_len + 1];
for i in 0..=a_len {
matrix[i][0] = i;
}
for j in 0..=b_len {
matrix[0][j] = j;
}
for i in 1..=a_len {
for j in 1..=b_len {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
matrix[i][j] = std::cmp::min(
std::cmp::min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1),
matrix[i - 1][j - 1] + cost,
);
}
}
matrix[a_len][b_len]
}