1use 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
18pub struct RuleDetector {
20 config: RuleConfig,
21}
22
23impl RuleDetector {
24 pub fn new() -> Self {
26 Self {
27 config: RuleConfig::default(),
28 }
29 }
30
31 pub fn with_config(config: RuleConfig) -> Self {
33 Self { config }
34 }
35
36 pub fn from_config_file(path: &Path) -> Result<Self> {
38 let config = RuleConfig::from_file(path)?;
39 Ok(Self { config })
40 }
41
42 pub fn config(&self) -> &RuleConfig {
44 &self.config
45 }
46
47 pub fn min_severity(&self) -> Severity {
49 Severity::parse(&self.config.general.min_severity).unwrap_or(Severity::Info)
50 }
51
52 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 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 Ok(Report::new())
72 }
73
74 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 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 pub fn analyze_controller(&self, controller: &Controller) -> Result<Report> {
96 let analysis = analyze_controller(controller);
98
99 let mut report = Report::new();
100
101 let unused_tags_detector = UnusedTagsDetector::new(&self.config.unused_tags);
103 unused_tags_detector.detect(controller, &analysis, &mut report);
104
105 let undefined_tags_detector = UndefinedTagsDetector::new(&self.config.undefined_tags);
107 undefined_tags_detector.detect(controller, &analysis, &mut report);
108
109 let empty_routines_detector = EmptyRoutinesDetector::new(&self.config.empty_routines);
111 empty_routines_detector.detect(controller, &analysis, &mut report);
112
113 let complexity_detector = ComplexityDetector::new(&self.config.complexity);
115 complexity_detector.detect(&analysis, &mut report);
116
117 let nesting_detector = NestingDetector::new(&self.config.nesting);
119 nesting_detector.detect(&analysis, &mut report);
120
121 Ok(report)
122 }
123
124 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 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 Ok(ParseStats::default())
139 }
140
141 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 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 assert!(report.rules.iter().any(|s| s.identifier == "Main"));
227 }
228}