use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use plceye::{RuleConfig, RuleDetector, Report, ParseStats};
#[derive(Parser)]
#[command(name = "plceye")]
#[command(version, about = "PLC code rule detector for L5X files", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(value_name = "FILE")]
files: Vec<PathBuf>,
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(short, long, value_name = "LEVEL", default_value = "info")]
severity: String,
#[arg(long)]
stats: bool,
}
#[derive(Subcommand)]
enum Commands {
Init,
}
fn main() -> ExitCode {
let cli = Cli::parse();
if let Some(Commands::Init) = cli.command {
return init_config();
}
if cli.files.is_empty() {
eprintln!("Error: No input files specified");
eprintln!("Usage: plceye <FILE>...");
eprintln!("Try 'plceye --help' for more information.");
return ExitCode::from(1);
}
if cli.stats {
return show_stats(&cli.files);
}
let mut config = if let Some(ref path) = cli.config {
match RuleConfig::from_file(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading config: {}", e);
return ExitCode::from(1);
}
}
} else if Path::new("plceye.toml").exists() {
match RuleConfig::from_file(Path::new("plceye.toml")) {
Ok(c) => c,
Err(e) => {
eprintln!("Warning: Failed to load plceye.toml: {}", e);
RuleConfig::default()
}
}
} else {
RuleConfig::default()
};
config.general.min_severity = cli.severity.clone();
let detector = RuleDetector::with_config(config);
let min_severity = detector.min_severity();
let mut all_reports: Vec<(String, Report)> = Vec::new();
let mut has_errors = false;
for file in &cli.files {
match detector.analyze_file(file) {
Ok(report) => {
all_reports.push((file.display().to_string(), report));
}
Err(e) => {
eprintln!("Error analyzing {}: {}", file.display(), e);
has_errors = true;
}
}
}
let total_issues: usize = all_reports
.iter()
.map(|(_, r)| r.filter_by_severity(min_severity).len())
.sum();
for (file, report) in &all_reports {
let filtered = report.filter_by_severity(min_severity);
if !filtered.is_empty() {
println!("\n=== {} ===", file);
for rule in filtered {
println!("{}", rule);
}
}
}
println!();
if total_issues == 0 {
println!("No issues found in {} file(s).", cli.files.len());
} else {
println!("Found {} issue(s) in {} file(s).", total_issues, cli.files.len());
}
if has_errors {
ExitCode::from(2)
} else if total_issues > 0 {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn init_config() -> ExitCode {
let path = Path::new("plceye.toml");
if path.exists() {
eprintln!("Error: plceye.toml already exists");
return ExitCode::from(1);
}
let content = RuleConfig::default_toml();
match std::fs::write(path, content) {
Ok(_) => {
println!("Created plceye.toml with default configuration");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error writing plceye.toml: {}", e);
ExitCode::from(1)
}
}
}
fn show_stats(files: &[PathBuf]) -> ExitCode {
let detector = RuleDetector::new();
let mut has_errors = false;
for file in files {
println!("=== {} ===", file.display());
match plceye::LoadedProject::from_file(file) {
Ok(project) => {
if project.format == plceye::FileFormat::PlcOpen {
match detector.get_plcopen_stats(&project) {
Ok(stats) => {
print_plcopen_stats(&stats);
}
Err(e) => {
eprintln!("Error: {}", e);
has_errors = true;
}
}
} else {
match detector.get_stats(&project) {
Ok(stats) => {
print_stats(&stats);
}
Err(e) => {
eprintln!("Error: {}", e);
has_errors = true;
}
}
}
}
Err(e) => {
eprintln!("Error: {}", e);
has_errors = true;
}
}
println!();
}
if has_errors {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn print_stats(stats: &ParseStats) {
println!("Programs: {:>6}", stats.programs);
println!("AOIs: {:>6}", stats.aois);
println!("Routines: {:>6}", stats.routines);
println!();
println!("RLL Rungs (total): {:>6}", stats.rungs);
println!(" In programs: {:>6}", stats.rll_rungs_programs);
println!(" In AOIs: {:>6}", stats.rll_rungs_aois);
println!(" Parsed OK: {:>6}", stats.parsed_ok);
println!(" Parse errors: {:>6}", stats.parsed_err);
println!();
println!("ST Routines: {:>6}", stats.st_routines);
println!(" In programs: {:>6}", stats.st_routines_programs);
println!(" In AOIs: {:>6}", stats.st_routines_aois);
println!(" Parsed OK: {:>6}", stats.st_parsed_ok);
println!(" Parse errors: {:>6}", stats.st_parsed_err);
println!();
println!("Tag references: {:>6}", stats.tag_references);
println!("Unique tags: {:>6}", stats.unique_tags);
if stats.st_parsed_ok > 0 {
println!();
println!("ST Complexity:");
println!(" Max complexity: {:>6}", stats.st_max_complexity);
println!(" Avg complexity: {:>6.1}", stats.st_avg_complexity);
println!(" Max nesting: {:>6}", stats.st_max_nesting);
println!(" Avg nesting: {:>6.1}", stats.st_avg_nesting);
}
}
fn print_plcopen_stats(stats: &plceye::PlcopenStats) {
println!("POUs (total): {:>6}", stats.pous);
println!(" Functions: {:>6}", stats.functions);
println!(" Function Blocks: {:>6}", stats.function_blocks);
println!(" Programs: {:>6}", stats.programs);
println!(" Empty POUs: {:>6}", stats.empty_pous);
println!();
println!("Language Usage:");
println!(" ST (Structured Text): {:>6}", stats.st_bodies);
println!(" IL (Instruction List): {:>6}", stats.il_bodies);
println!(" FBD (Function Block): {:>6}", stats.fbd_bodies);
println!(" LD (Ladder Diagram): {:>6}", stats.ld_bodies);
println!(" SFC (Sequential Chart): {:>6}", stats.sfc_bodies);
println!();
println!("Variables: {:>6}", stats.variables);
}