plceye/
detector.rs

1//! Main rule detector that coordinates all individual detectors.
2
3use std::path::Path;
4
5use l5x::Controller;
6
7use crate::analysis::{analyze_controller, analyze_plcopen_project, ParseStats, PlcopenStats};
8use crate::config::RuleConfig;
9use crate::loader::LoadedProject;
10use crate::report::{Report, Severity};
11use crate::rules::{
12    ComplexityDetector, EmptyRoutinesDetector, NestingDetector,
13    UndefinedTagsDetector, UnusedTagsDetector,
14    PlcopenUnusedVarsDetector, PlcopenUndefinedVarsDetector, PlcopenEmptyPousDetector,
15};
16use crate::Result;
17
18/// Main rule detector that runs all enabled detectors.
19pub struct RuleDetector {
20    config: RuleConfig,
21}
22
23impl RuleDetector {
24    /// Create a new rule detector with default configuration.
25    pub fn new() -> Self {
26        Self {
27            config: RuleConfig::default(),
28        }
29    }
30
31    /// Create a new rule detector with the given configuration.
32    pub fn with_config(config: RuleConfig) -> Self {
33        Self { config }
34    }
35
36    /// Load configuration from a file.
37    pub fn from_config_file(path: &Path) -> Result<Self> {
38        let config = RuleConfig::from_file(path)?;
39        Ok(Self { config })
40    }
41
42    /// Get the current configuration.
43    pub fn config(&self) -> &RuleConfig {
44        &self.config
45    }
46
47    /// Get minimum severity from config.
48    pub fn min_severity(&self) -> Severity {
49        Severity::parse(&self.config.general.min_severity).unwrap_or(Severity::Info)
50    }
51
52    /// Analyze a file (L5X or PLCopen) and return a report.
53    pub fn analyze_file(&self, path: &Path) -> Result<Report> {
54        let project = LoadedProject::from_file(path)?;
55        let mut report = self.analyze(&project)?;
56        report.source_file = project.source_path;
57        Ok(report)
58    }
59
60    /// Analyze a loaded project.
61    pub fn analyze(&self, project: &LoadedProject) -> Result<Report> {
62        if let Some(ref controller) = project.l5x_controller {
63            return self.analyze_controller(controller);
64        }
65        
66        if let Some(ref plcopen) = project.plcopen_project {
67            return self.analyze_plcopen(plcopen, project.source_path.clone());
68        }
69        
70        // Unknown format
71        Ok(Report::new())
72    }
73    
74    /// Analyze a PLCopen project.
75    fn analyze_plcopen(&self, project: &plcopen::Project, source_path: Option<String>) -> Result<Report> {
76        let analysis = analyze_plcopen_project(project);
77        
78        let mut report = Report::new();
79        report.source_file = source_path;
80        
81        // Run PLCopen-specific detectors
82        let unused_detector = PlcopenUnusedVarsDetector::new(&self.config.unused_tags);
83        unused_detector.detect(&analysis, &mut report);
84        
85        let undefined_detector = PlcopenUndefinedVarsDetector::new(&self.config.undefined_tags);
86        undefined_detector.detect(&analysis, &mut report);
87        
88        let empty_detector = PlcopenEmptyPousDetector::new(&self.config.empty_routines);
89        empty_detector.detect(&analysis, &mut report);
90        
91        Ok(report)
92    }
93
94    /// Analyze a parsed L5X controller.
95    pub fn analyze_controller(&self, controller: &Controller) -> Result<Report> {
96        // Run the L5X analysis to get tag references, etc.
97        let analysis = analyze_controller(controller);
98
99        let mut report = Report::new();
100
101        // Run unused tags detector
102        let unused_tags_detector = UnusedTagsDetector::new(&self.config.unused_tags);
103        unused_tags_detector.detect(controller, &analysis, &mut report);
104
105        // Run undefined tags detector
106        let undefined_tags_detector = UndefinedTagsDetector::new(&self.config.undefined_tags);
107        undefined_tags_detector.detect(controller, &analysis, &mut report);
108
109        // Run empty routines detector
110        let empty_routines_detector = EmptyRoutinesDetector::new(&self.config.empty_routines);
111        empty_routines_detector.detect(controller, &analysis, &mut report);
112
113        // Run cyclomatic complexity detector on ST routines
114        let complexity_detector = ComplexityDetector::new(&self.config.complexity);
115        complexity_detector.detect(&analysis, &mut report);
116
117        // Run deep nesting detector on ST routines
118        let nesting_detector = NestingDetector::new(&self.config.nesting);
119        nesting_detector.detect(&analysis, &mut report);
120
121        Ok(report)
122    }
123
124    /// Get statistics for a file without running rule detection.
125    pub fn get_stats_file(&self, path: &Path) -> Result<ParseStats> {
126        let project = LoadedProject::from_file(path)?;
127        self.get_stats(&project)
128    }
129
130    /// Get statistics for a loaded project (L5X format).
131    pub fn get_stats(&self, project: &LoadedProject) -> Result<ParseStats> {
132        if let Some(ref controller) = project.l5x_controller {
133            let analysis = analyze_controller(controller);
134            return Ok(analysis.stats);
135        }
136        
137        // For non-L5X, return empty stats
138        Ok(ParseStats::default())
139    }
140    
141    /// Get PLCopen statistics for a loaded project.
142    pub fn get_plcopen_stats(&self, project: &LoadedProject) -> Result<PlcopenStats> {
143        if let Some(ref plcopen) = project.plcopen_project {
144            let analysis = analyze_plcopen_project(plcopen);
145            return Ok(analysis.stats);
146        }
147        
148        Ok(PlcopenStats::default())
149    }
150}
151
152impl Default for RuleDetector {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_detector_default() {
164        let detector = RuleDetector::new();
165        assert!(detector.config().unused_tags.enabled);
166    }
167
168    #[test]
169    fn test_detector_with_config() {
170        let mut config = RuleConfig::default();
171        config.unused_tags.enabled = false;
172        let detector = RuleDetector::with_config(config);
173        assert!(!detector.config().unused_tags.enabled);
174    }
175
176    #[test]
177    fn test_analyze_l5x() {
178        let xml = r#"<?xml version="1.0"?>
179        <RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="32.00">
180            <Controller Name="TestController">
181                <Tags>
182                    <Tag Name="Unused" DataType="BOOL"/>
183                </Tags>
184                <Programs>
185                    <Program Name="MainProgram">
186                        <Routines>
187                            <Routine Name="MainRoutine" Type="RLL">
188                                <RLLContent>
189                                    <Rung Number="0">
190                                        <Text>NOP();</Text>
191                                    </Rung>
192                                </RLLContent>
193                            </Routine>
194                        </Routines>
195                    </Program>
196                </Programs>
197            </Controller>
198        </RSLogix5000Content>"#;
199        
200        let project = LoadedProject::from_str(xml, None).expect("Should parse");
201        let detector = RuleDetector::new();
202        let report = detector.analyze(&project).expect("Should analyze");
203        
204        // Should detect unused tag
205        assert!(!report.rules.is_empty());
206    }
207
208    #[test]
209    fn test_analyze_plcopen() {
210        let xml = r#"<?xml version="1.0"?>
211        <project xmlns="http://www.plcopen.org/xml/tc6_0200">
212            <fileHeader companyName="Test" productName="TestProject" productVersion="1.0" creationDateTime="2024-01-01T00:00:00"/>
213            <contentHeader name="Test"/>
214            <types>
215                <pous>
216                    <pou name="Main" pouType="program"/>
217                </pous>
218            </types>
219        </project>"#;
220        
221        let project = LoadedProject::from_str(xml, None).expect("Should parse");
222        let detector = RuleDetector::new();
223        let report = detector.analyze(&project).expect("Should analyze");
224        
225        // Should detect empty POU
226        assert!(report.rules.iter().any(|s| s.identifier == "Main"));
227    }
228}