plceye/
report.rs

1//! Report types for rule detection results.
2
3use std::fmt;
4
5/// Severity level of a detected rule.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum Severity {
8    /// Informational - potential issue worth reviewing
9    Info,
10    /// Warning - likely a problem
11    Warning,
12    /// Error - definite problem
13    Error,
14}
15
16impl fmt::Display for Severity {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Severity::Info => write!(f, "info"),
20            Severity::Warning => write!(f, "warning"),
21            Severity::Error => write!(f, "error"),
22        }
23    }
24}
25
26impl Severity {
27    /// Parse severity from string.
28    pub fn parse(s: &str) -> Option<Self> {
29        match s.to_lowercase().as_str() {
30            "info" => Some(Severity::Info),
31            "warning" | "warn" => Some(Severity::Warning),
32            "error" | "err" => Some(Severity::Error),
33            _ => None,
34        }
35    }
36}
37
38/// Kind of code rule detected.
39///
40/// This enum contains all rule types. The open-source `plceye` detects the first 5 rules.
41/// Additional rules are detected by `plceye-pro` (commercial license).
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum RuleKind {
44    // =========================================================================
45    // OPEN SOURCE RULES (detected by plceye)
46    // =========================================================================
47
48    /// S0001: Tag is defined but never used
49    UnusedTag,
50    /// S0002: Tag is used but never defined (might be external/aliased)
51    UndefinedTag,
52    /// S0003: Empty routine or POU
53    EmptyBlock,
54    /// S0004: AOI is defined but never called
55    UnusedAoi,
56    /// S0005: DataType is defined but never used
57    UnusedDataType,
58    /// M0001: Cyclomatic complexity too high
59    CyclomaticComplexity,
60    /// M0003: Deep nesting (> 4 levels)
61    DeepNesting,
62
63    // =========================================================================
64    // PRO RULES (detected by plceye-pro - commercial license)
65    // For licensing information, contact: []
66    // =========================================================================
67
68    // --- Coding Practice (C) ---
69    /// C0010: Floating-point comparison with = or <>
70    FloatEquality,
71    /// C0011: TIME comparison with = or <>
72    TimeEquality,
73    /// C0014: Possible division by zero
74    DivisionByZero,
75    /// C0015: Magic number (hardcoded literal that should be a constant)
76    MagicNumber,
77    /// C0016: Timer/counter without reset path
78    TimerNoReset,
79    /// C0031: POU calls itself recursively
80    RecursiveCall,
81    /// C0032: FOR loop variable modified inside loop
82    LoopVarModified,
83    /// C0050: POU has too many parameters (>7)
84    TooManyParameters,
85    /// C0060: Too many global variables
86    ExcessiveGlobals,
87
88    // --- Style (S) ---
89    /// S0020: CONTINUE statement used
90    ContinueUsed,
91    /// S0021: EXIT statement used
92    ExitUsed,
93    /// S0022: IF without ELSE clause
94    IfWithoutElse,
95    /// S0023: CASE without ELSE clause
96    CaseWithoutElse,
97    /// S0025: RETURN not at end of POU
98    ReturnInMiddle,
99
100    // --- Naming (N) ---
101    /// N0006: Name length < 3 characters
102    NameTooShort,
103    /// N0007: Name length > 30 characters
104    NameTooLong,
105
106    // --- Vendor-Specific L5X (X) ---
107    /// X0001: AOI without description
108    AoiNoDescription,
109    /// X0002: Tag without description
110    TagNoDescription,
111    /// X0003: Routine without description
112    RoutineNoDescription,
113    /// X0004: Program without description
114    ProgramNoDescription,
115    /// X0006: Task watchdog disabled
116    TaskWatchdogDisabled,
117    /// X0007: Excessive task rate (<1ms)
118    ExcessiveTaskRate,
119    /// X0009: Alias chain (alias pointing to alias)
120    AliasChain,
121    /// X0010: Large array (>10000 elements)
122    LargeArray,
123}
124
125impl RuleKind {
126    /// Get the rule code (e.g., "S0001").
127    pub fn code(&self) -> &'static str {
128        match self {
129            // Open Source rules
130            RuleKind::UnusedTag => "S0001",
131            RuleKind::UndefinedTag => "S0002",
132            RuleKind::EmptyBlock => "S0003",
133            RuleKind::UnusedAoi => "S0004",
134            RuleKind::UnusedDataType => "S0005",
135            // Pro: Coding Practice
136            RuleKind::FloatEquality => "C0010",
137            RuleKind::TimeEquality => "C0011",
138            RuleKind::DivisionByZero => "C0014",
139            RuleKind::MagicNumber => "C0015",
140            RuleKind::TimerNoReset => "C0016",
141            RuleKind::RecursiveCall => "C0031",
142            RuleKind::LoopVarModified => "C0032",
143            RuleKind::TooManyParameters => "C0050",
144            RuleKind::ExcessiveGlobals => "C0060",
145            // Pro: Style
146            RuleKind::ContinueUsed => "S0020",
147            RuleKind::ExitUsed => "S0021",
148            RuleKind::IfWithoutElse => "S0022",
149            RuleKind::CaseWithoutElse => "S0023",
150            RuleKind::ReturnInMiddle => "S0025",
151            // Pro: Metrics
152            RuleKind::CyclomaticComplexity => "M0001",
153            RuleKind::DeepNesting => "M0003",
154            // Pro: Naming
155            RuleKind::NameTooShort => "N0006",
156            RuleKind::NameTooLong => "N0007",
157            // Pro: Vendor-Specific L5X
158            RuleKind::AoiNoDescription => "X0001",
159            RuleKind::TagNoDescription => "X0002",
160            RuleKind::RoutineNoDescription => "X0003",
161            RuleKind::ProgramNoDescription => "X0004",
162            RuleKind::TaskWatchdogDisabled => "X0006",
163            RuleKind::ExcessiveTaskRate => "X0007",
164            RuleKind::AliasChain => "X0009",
165            RuleKind::LargeArray => "X0010",
166        }
167    }
168    
169    /// Get the rule name (e.g., "unused-tag").
170    pub fn name(&self) -> &'static str {
171        match self {
172            // Coding Practice
173            RuleKind::UnusedTag => "unused-tag",
174            RuleKind::UndefinedTag => "undefined-tag",
175            RuleKind::EmptyBlock => "empty-block",
176            RuleKind::UnusedAoi => "unused-aoi",
177            RuleKind::UnusedDataType => "unused-datatype",
178            RuleKind::FloatEquality => "float-equality",
179            RuleKind::TimeEquality => "time-equality",
180            RuleKind::DivisionByZero => "division-by-zero",
181            RuleKind::MagicNumber => "magic-number",
182            RuleKind::TimerNoReset => "timer-no-reset",
183            RuleKind::RecursiveCall => "recursive-call",
184            RuleKind::LoopVarModified => "loop-var-modified",
185            RuleKind::TooManyParameters => "too-many-parameters",
186            RuleKind::ExcessiveGlobals => "excessive-globals",
187            // Style
188            RuleKind::ContinueUsed => "continue-used",
189            RuleKind::ExitUsed => "exit-used",
190            RuleKind::IfWithoutElse => "if-without-else",
191            RuleKind::CaseWithoutElse => "case-without-else",
192            RuleKind::ReturnInMiddle => "return-in-middle",
193            // Metrics
194            RuleKind::CyclomaticComplexity => "cyclomatic-complexity",
195            RuleKind::DeepNesting => "deep-nesting",
196            // Naming
197            RuleKind::NameTooShort => "name-too-short",
198            RuleKind::NameTooLong => "name-too-long",
199            // Vendor-Specific L5X
200            RuleKind::AoiNoDescription => "aoi-no-description",
201            RuleKind::TagNoDescription => "tag-no-description",
202            RuleKind::RoutineNoDescription => "routine-no-description",
203            RuleKind::ProgramNoDescription => "program-no-description",
204            RuleKind::TaskWatchdogDisabled => "task-watchdog-disabled",
205            RuleKind::ExcessiveTaskRate => "excessive-task-rate",
206            RuleKind::AliasChain => "alias-chain",
207            RuleKind::LargeArray => "large-array",
208        }
209    }
210}
211
212impl fmt::Display for RuleKind {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "{}", self.code())
215    }
216}
217
218/// A single detected code rule.
219#[derive(Debug, Clone)]
220pub struct Rule {
221    /// Kind of rule
222    pub kind: RuleKind,
223    /// Severity level
224    pub severity: Severity,
225    /// Location in the project (e.g., "Program:Main")
226    pub location: String,
227    /// The identifier involved (tag name, routine name, etc.)
228    pub identifier: String,
229    /// Human-readable message
230    pub message: String,
231}
232
233impl Rule {
234    /// Create a new rule.
235    pub fn new(
236        kind: RuleKind,
237        severity: Severity,
238        location: impl Into<String>,
239        identifier: impl Into<String>,
240        message: impl Into<String>,
241    ) -> Self {
242        Self {
243            kind,
244            severity,
245            location: location.into(),
246            identifier: identifier.into(),
247            message: message.into(),
248        }
249    }
250}
251
252impl fmt::Display for Rule {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        write!(
255            f,
256            "[{}] {}: {} - {} ({})",
257            self.severity, self.kind, self.location, self.message, self.identifier
258        )
259    }
260}
261
262/// Report containing all detected rules.
263#[derive(Debug, Clone, Default)]
264pub struct Report {
265    /// All detected rules
266    pub rules: Vec<Rule>,
267    /// Source file that was analyzed
268    pub source_file: Option<String>,
269}
270
271impl Report {
272    /// Create a new empty report.
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// Add a rule to the report.
278    pub fn add(&mut self, rule: Rule) {
279        self.rules.push(rule);
280    }
281
282    /// Filter rules by minimum severity.
283    pub fn filter_by_severity(&self, min_severity: Severity) -> Vec<&Rule> {
284        self.rules
285            .iter()
286            .filter(|s| s.severity >= min_severity)
287            .collect()
288    }
289
290    /// Check if report has any rules.
291    pub fn is_empty(&self) -> bool {
292        self.rules.is_empty()
293    }
294
295    /// Get total number of rules.
296    pub fn len(&self) -> usize {
297        self.rules.len()
298    }
299}