use std::collections::HashSet;
use crate::analysis::ProjectAnalysis;
use crate::config::UnusedAoisConfig;
use crate::report::{Report, Severity, Rule, RuleKind};
pub struct UnusedAoisDetector<'a> {
config: &'a UnusedAoisConfig,
}
impl<'a> UnusedAoisDetector<'a> {
pub fn new(config: &'a UnusedAoisConfig) -> Self {
Self { config }
}
pub fn detect(&self, analysis: &ProjectAnalysis, report: &mut Report) {
if !self.config.enabled {
return;
}
let defined_aois: HashSet<&str> = analysis.aoi_definitions.iter().map(|s| s.as_str()).collect();
let called_aois: HashSet<&str> = analysis.aoi_usage.keys().map(|s| s.as_str()).collect();
for aoi_name in &defined_aois {
if called_aois.contains(*aoi_name) {
continue;
}
if self.matches_ignore_pattern(aoi_name) {
continue;
}
report.add(Rule::new(
RuleKind::UnusedAoi,
Severity::Info,
format!("AOI:{}", aoi_name),
aoi_name.to_string(),
format!("AOI '{}' is defined but never called", aoi_name),
));
}
}
fn matches_ignore_pattern(&self, aoi_name: &str) -> bool {
for pattern in &self.config.ignore_patterns {
if glob_match(pattern, aoi_name) {
return true;
}
}
false
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
glob_match_recursive(&pattern_chars, &text_chars, 0, 0)
}
fn glob_match_recursive(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
if pi == pattern.len() {
return ti == text.len();
}
match pattern[pi] {
'*' => {
for i in ti..=text.len() {
if glob_match_recursive(pattern, text, pi + 1, i) {
return true;
}
}
false
}
'?' => {
if ti < text.len() {
glob_match_recursive(pattern, text, pi + 1, ti + 1)
} else {
false
}
}
c => {
if ti < text.len() && c.eq_ignore_ascii_case(&text[ti]) {
glob_match_recursive(pattern, text, pi + 1, ti + 1)
} else {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::{ProjectAnalysis, AoiReference, AoiCallSource, ParseStats};
#[test]
fn test_detects_unused_aoi() {
let config = UnusedAoisConfig {
enabled: true,
ignore_patterns: vec![],
};
let mut analysis = ProjectAnalysis {
rungs: vec![],
st_routines: vec![],
tag_references: vec![],
tag_xref: Default::default(),
routines: vec![],
instruction_usage: Default::default(),
aoi_definitions: vec!["UsedAOI".to_string(), "UnusedAOI".to_string()],
aoi_usage: Default::default(),
stats: ParseStats::default(),
};
analysis.aoi_usage.insert(
"UsedAOI".to_string(),
vec![AoiReference {
aoi_name: "UsedAOI".to_string(),
program: "Main".to_string(),
routine: "Logic".to_string(),
rung_number: Some(0),
source: AoiCallSource::Rll,
}],
);
let detector = UnusedAoisDetector::new(&config);
let mut report = Report::new();
detector.detect(&analysis, &mut report);
assert_eq!(report.rules.len(), 1);
assert_eq!(report.rules[0].kind, RuleKind::UnusedAoi);
assert!(report.rules[0].message.contains("UnusedAOI"));
}
#[test]
fn test_ignores_pattern() {
let config = UnusedAoisConfig {
enabled: true,
ignore_patterns: vec!["Test_*".to_string()],
};
let analysis = ProjectAnalysis {
rungs: vec![],
st_routines: vec![],
tag_references: vec![],
tag_xref: Default::default(),
routines: vec![],
instruction_usage: Default::default(),
aoi_definitions: vec!["Test_AOI".to_string()],
aoi_usage: Default::default(),
stats: ParseStats::default(),
};
let detector = UnusedAoisDetector::new(&config);
let mut report = Report::new();
detector.detect(&analysis, &mut report);
assert_eq!(report.rules.len(), 0);
}
#[test]
fn test_glob_match() {
assert!(glob_match("Test*", "TestAOI"));
assert!(glob_match("*AOI", "MyTestAOI"));
assert!(glob_match("*", "anything"));
assert!(glob_match("Test?", "TestA"));
assert!(!glob_match("Test?", "TestAB"));
}
}