use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::{Error, Result};
use crate::error::ConfigErrorKind;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct RuleConfig {
pub general: GeneralConfig,
pub unused_tags: UnusedTagsConfig,
pub undefined_tags: UndefinedTagsConfig,
pub empty_routines: EmptyRoutinesConfig,
pub unused_aois: UnusedAoisConfig,
pub unused_datatypes: UnusedDataTypesConfig,
pub complexity: ComplexityConfig,
pub nesting: NestingConfig,
}
impl RuleConfig {
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| Error::FileRead {
path: path.display().to_string(),
source: e,
})?;
Self::parse(&content)
}
pub fn parse(content: &str) -> Result<Self> {
toml::from_str(content).map_err(|_| Error::ConfigParse {
kind: ConfigErrorKind::TomlSyntax,
})
}
pub fn default_toml() -> String {
r#"# plceye.toml - PLC Code Rule Detector Configuration
[general]
# Minimum severity to report: "info", "warning", "error"
min_severity = "info"
[unused_tags]
# Enable unused tag detection
enabled = true
# Ignore tags matching these patterns (glob-style)
ignore_patterns = [
"_*", # Tags starting with underscore (often internal)
"HMI_*", # HMI interface tags
]
# Ignore tags in these scopes
ignore_scopes = [
# "Program:MainProgram", # Example: ignore MainProgram
]
[undefined_tags]
# Enable undefined tag detection (tags referenced but not declared)
enabled = true
# Ignore undefined tags matching these patterns (useful for I/O)
ignore_patterns = [
"Local:*", # Module I/O references
]
[empty_routines]
# Enable empty routine detection
enabled = true
# Ignore routines matching these patterns
ignore_patterns = [
# "Unused_*", # Example: ignore placeholder routines
]
[unused_aois]
# Enable unused AOI detection
enabled = true
# Ignore AOIs matching these patterns
ignore_patterns = []
[unused_datatypes]
# Enable unused DataType detection
enabled = true
# Ignore DataTypes matching these patterns
ignore_patterns = []
[complexity]
# Enable cyclomatic complexity detection for ST routines
enabled = true
# Maximum allowed cyclomatic complexity
max_complexity = 10
# Ignore routines matching these patterns
ignore_patterns = []
[nesting]
# Enable deep nesting detection for ST routines
enabled = true
# Maximum allowed nesting depth
max_depth = 4
# Ignore routines matching these patterns
ignore_patterns = []
"#
.to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub min_severity: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
min_severity: "info".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UnusedTagsConfig {
pub enabled: bool,
pub ignore_patterns: Vec<String>,
pub ignore_scopes: Vec<String>,
}
impl Default for UnusedTagsConfig {
fn default() -> Self {
Self {
enabled: true,
ignore_patterns: vec!["_*".to_string()],
ignore_scopes: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UndefinedTagsConfig {
pub enabled: bool,
pub ignore_patterns: Vec<String>,
}
impl Default for UndefinedTagsConfig {
fn default() -> Self {
Self {
enabled: true,
ignore_patterns: vec!["Local:*".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EmptyRoutinesConfig {
pub enabled: bool,
pub ignore_patterns: Vec<String>,
}
impl Default for EmptyRoutinesConfig {
fn default() -> Self {
Self {
enabled: true,
ignore_patterns: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UnusedAoisConfig {
pub enabled: bool,
pub ignore_patterns: Vec<String>,
}
impl Default for UnusedAoisConfig {
fn default() -> Self {
Self {
enabled: true,
ignore_patterns: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UnusedDataTypesConfig {
pub enabled: bool,
pub ignore_patterns: Vec<String>,
}
impl Default for UnusedDataTypesConfig {
fn default() -> Self {
Self {
enabled: true,
ignore_patterns: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ComplexityConfig {
pub enabled: bool,
pub max_complexity: usize,
pub ignore_patterns: Vec<String>,
}
impl Default for ComplexityConfig {
fn default() -> Self {
Self {
enabled: true,
max_complexity: 10,
ignore_patterns: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NestingConfig {
pub enabled: bool,
pub max_depth: usize,
pub ignore_patterns: Vec<String>,
}
impl Default for NestingConfig {
fn default() -> Self {
Self {
enabled: true,
max_depth: 4,
ignore_patterns: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = RuleConfig::default();
assert!(config.unused_tags.enabled);
assert_eq!(config.general.min_severity, "info");
}
#[test]
fn test_parse_config() {
let toml = r#"
[general]
min_severity = "warning"
[unused_tags]
enabled = true
ignore_patterns = ["Test_*", "Debug_*"]
ignore_scopes = ["Program:Debug"]
"#;
let config = RuleConfig::parse(toml).unwrap();
assert_eq!(config.general.min_severity, "warning");
assert!(config.unused_tags.enabled);
assert_eq!(config.unused_tags.ignore_patterns.len(), 2);
}
#[test]
fn test_default_toml_parses() {
let toml = RuleConfig::default_toml();
let config = RuleConfig::parse(&toml).unwrap();
assert!(config.unused_tags.enabled);
}
}