use std::collections::{HashMap, HashSet};
use l5x::rll::{Rung as ParsedRung, TagReference, ErrorContext, ParseError};
use l5x::{
Controller,
UDIDefinition, UDIDefinitionContent,
};
use super::iec61131_adapter::Pou;
use super::rll_parsing::parse_routine;
use super::st_parsing::{
parse_st_routines_from_program, parse_st_routines_from_aoi,
extract_st_call_names,
};
#[derive(Debug, Clone, PartialEq)]
pub struct RungLocation {
pub program: String,
pub routine: String,
pub rung_number: u32,
}
impl RungLocation {
pub fn new(program: impl Into<String>, routine: impl Into<String>, rung_number: u32) -> Self {
Self {
program: program.into(),
routine: routine.into(),
rung_number,
}
}
pub fn path(&self) -> String {
format!("{}/{}/Rung#{}", self.program, self.routine, self.rung_number)
}
}
#[derive(Debug, Clone)]
pub struct LocatedRung {
pub location: RungLocation,
pub parsed: ParsedRung,
}
impl LocatedRung {
pub fn tag_references(&self) -> Vec<LocatedTagReference> {
self.parsed
.tag_references()
.into_iter()
.map(|r| LocatedTagReference {
location: self.location.clone(),
reference: r,
})
.collect()
}
pub fn parse_error(&self) -> Option<ParseError> {
self.parsed.error.as_ref().map(|err| {
ParseError::new(err.clone(), &self.parsed.raw_text).with_context(ErrorContext::new(
&self.location.program,
&self.location.routine,
self.location.rung_number,
))
})
}
pub fn has_error(&self) -> bool {
self.parsed.error.is_some()
}
}
#[derive(Debug, Clone)]
pub struct LocatedTagReference {
pub location: RungLocation,
pub reference: TagReference,
}
impl LocatedTagReference {
pub fn tag_name(&self) -> &str {
&self.reference.name
}
pub fn full_operand(&self) -> &str {
&self.reference.full_operand
}
pub fn instruction(&self) -> &str {
&self.reference.instruction
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct STLocation {
pub program: String,
pub routine: String,
}
impl STLocation {
pub fn new(program: impl Into<String>, routine: impl Into<String>) -> Self {
Self {
program: program.into(),
routine: routine.into(),
}
}
pub fn path(&self) -> String {
format!("{}/{}", self.program, self.routine)
}
}
#[derive(Debug)]
pub struct ParsedSTRoutine {
pub location: STLocation,
pub source: String,
pub pou: Option<Pou>,
pub parse_error: Option<super::iec61131_adapter::ParseError>,
}
impl ParsedSTRoutine {
pub fn is_parsed(&self) -> bool {
self.pou.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AoiReference {
pub aoi_name: String,
pub program: String,
pub routine: String,
pub rung_number: Option<u32>,
pub source: AoiCallSource,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AoiCallSource {
Rll,
St,
}
impl AoiReference {
pub fn from_rll(aoi_name: &str, location: &RungLocation) -> Self {
Self {
aoi_name: aoi_name.to_string(),
program: location.program.clone(),
routine: location.routine.clone(),
rung_number: Some(location.rung_number),
source: AoiCallSource::Rll,
}
}
pub fn from_st(aoi_name: &str, location: &STLocation) -> Self {
Self {
aoi_name: aoi_name.to_string(),
program: location.program.clone(),
routine: location.routine.clone(),
rung_number: None,
source: AoiCallSource::St,
}
}
pub fn path(&self) -> String {
match self.rung_number {
Some(n) => format!("{}/{}/Rung#{}", self.program, self.routine, n),
None => format!("{}/{}", self.program, self.routine),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ParseStats {
pub programs: usize,
pub aois: usize,
pub routines: usize,
pub rungs: usize,
pub rll_rungs_programs: usize,
pub rll_rungs_aois: usize,
pub parsed_ok: usize,
pub parsed_err: usize,
pub tag_references: usize,
pub unique_tags: usize,
pub st_routines: usize,
pub st_routines_programs: usize,
pub st_routines_aois: usize,
pub st_parsed_ok: usize,
pub st_parsed_err: usize,
pub st_max_complexity: usize,
pub st_avg_complexity: f32,
pub st_max_nesting: usize,
pub st_avg_nesting: f32,
}
#[derive(Debug, Clone)]
pub struct RoutineSummary {
pub program: String,
pub routine: String,
pub routine_type: String,
pub rung_count: usize,
pub rung_indices: Vec<usize>,
pub parse_errors: usize,
pub tags_used: Vec<String>,
pub instructions: HashMap<String, usize>,
}
#[derive(Debug)]
pub struct ProjectAnalysis {
pub rungs: Vec<LocatedRung>,
pub st_routines: Vec<ParsedSTRoutine>,
pub tag_references: Vec<LocatedTagReference>,
pub tag_xref: HashMap<String, Vec<usize>>,
pub routines: Vec<RoutineSummary>,
pub instruction_usage: HashMap<String, usize>,
pub aoi_definitions: Vec<String>,
pub aoi_usage: HashMap<String, Vec<AoiReference>>,
pub stats: ParseStats,
}
impl ProjectAnalysis {
pub fn references_to(&self, tag_name: &str) -> Vec<&LocatedTagReference> {
if let Some(indices) = self.tag_xref.get(tag_name) {
indices.iter().filter_map(|&i| self.tag_references.get(i)).collect()
} else {
Vec::new()
}
}
pub fn unique_tags(&self) -> Vec<&str> {
let mut tags: Vec<&str> = self.tag_xref.keys().map(|s| s.as_str()).collect();
tags.sort();
tags
}
pub fn get_routine(&self, program: &str, routine: &str) -> Option<&RoutineSummary> {
self.routines.iter().find(|r| r.program == program && r.routine == routine)
}
pub fn parse_errors(&self) -> Vec<ParseError> {
self.rungs.iter().filter_map(|rung| rung.parse_error()).collect()
}
pub fn unused_aois(&self) -> Vec<&str> {
self.aoi_definitions
.iter()
.filter(|name| {
self.aoi_usage.get(*name).map(|refs| refs.is_empty()).unwrap_or(true)
})
.map(|s| s.as_str())
.collect()
}
pub fn aoi_references(&self, aoi_name: &str) -> Vec<&AoiReference> {
self.aoi_usage
.get(aoi_name)
.map(|refs| refs.iter().collect())
.unwrap_or_default()
}
}
fn parse_rll_from_aoi(aoi: &UDIDefinition) -> Vec<LocatedRung> {
let mut results = Vec::new();
let aoi_name = format!("AOI:{}", aoi.name);
for content in &aoi.content {
if let UDIDefinitionContent::Routines(routine_collection) = content {
for routine in &routine_collection.routine {
results.extend(parse_routine(routine, &aoi_name));
}
}
}
results
}
pub fn analyze_controller(controller: &Controller) -> ProjectAnalysis {
let mut rungs = Vec::new();
let mut routine_summaries = Vec::new();
let mut stats = ParseStats::default();
if let Some(programs) = &controller.programs {
for program in &programs.program {
stats.programs += 1;
if let Some(routines) = &program.routines {
for routine in &routines.routine {
stats.routines += 1;
let start_idx = rungs.len();
let routine_rungs = parse_routine(routine, &program.name);
let rung_count = routine_rungs.len();
stats.rll_rungs_programs += rung_count;
let rung_indices: Vec<usize> = (start_idx..start_idx + rung_count).collect();
let mut parse_errors = 0;
let mut routine_tags: Vec<String> = Vec::new();
let mut routine_instructions: HashMap<String, usize> = HashMap::new();
for rung in &routine_rungs {
if rung.parsed.is_parsed() {
for tag_ref in rung.tag_references() {
routine_tags.push(tag_ref.reference.name.clone());
*routine_instructions.entry(tag_ref.reference.instruction.clone()).or_insert(0) += 1;
}
} else {
parse_errors += 1;
}
}
routine_tags.sort();
routine_tags.dedup();
routine_summaries.push(RoutineSummary {
program: program.name.clone(),
routine: routine.name.clone(),
routine_type: routine.r#type.clone(),
rung_count,
rung_indices,
parse_errors,
tags_used: routine_tags,
instructions: routine_instructions,
});
rungs.extend(routine_rungs);
}
}
}
}
let mut aoi_definitions: Vec<String> = Vec::new();
if let Some(aois) = &controller.add_on_instruction_definitions {
stats.aois = aois.add_on_instruction_definition.len();
for aoi in &aois.add_on_instruction_definition {
aoi_definitions.push(aoi.name.clone());
let aoi_rungs = parse_rll_from_aoi(aoi);
stats.rll_rungs_aois += aoi_rungs.len();
rungs.extend(aoi_rungs);
}
}
let aoi_name_set: HashSet<&str> = aoi_definitions.iter().map(|s| s.as_str()).collect();
stats.rungs = rungs.len();
let mut all_refs = Vec::new();
let mut tag_xref: HashMap<String, Vec<usize>> = HashMap::new();
let mut instruction_usage: HashMap<String, usize> = HashMap::new();
let mut aoi_usage: HashMap<String, Vec<AoiReference>> = HashMap::new();
for aoi_name in &aoi_definitions {
aoi_usage.insert(aoi_name.clone(), Vec::new());
}
for rung in &rungs {
if rung.parsed.is_parsed() {
stats.parsed_ok += 1;
let mut rung_aoi_calls: HashSet<String> = HashSet::new();
for tag_ref in rung.tag_references() {
let ref_idx = all_refs.len();
let tag_name = tag_ref.reference.name.clone();
let instruction = tag_ref.reference.instruction.clone();
if aoi_name_set.contains(instruction.as_str()) && !rung_aoi_calls.contains(&instruction) {
rung_aoi_calls.insert(instruction.clone());
aoi_usage
.entry(instruction.clone())
.or_default()
.push(AoiReference::from_rll(&instruction, &rung.location));
}
tag_xref.entry(tag_name).or_default().push(ref_idx);
*instruction_usage.entry(instruction).or_insert(0) += 1;
all_refs.push(tag_ref);
}
} else {
stats.parsed_err += 1;
}
}
let mut st_routines = Vec::new();
if let Some(programs) = &controller.programs {
for program in &programs.program {
let program_st = parse_st_routines_from_program(program);
stats.st_routines_programs += program_st.len();
st_routines.extend(program_st);
}
}
if let Some(aois) = &controller.add_on_instruction_definitions {
for aoi in &aois.add_on_instruction_definition {
let aoi_st = parse_st_routines_from_aoi(aoi);
stats.st_routines_aois += aoi_st.len();
st_routines.extend(aoi_st);
}
}
for st_routine in &st_routines {
if let Some(ref pou) = st_routine.pou {
let call_names = extract_st_call_names(pou);
for call_name in call_names {
if aoi_name_set.contains(call_name.as_str()) {
aoi_usage
.entry(call_name.clone())
.or_default()
.push(AoiReference::from_st(&call_name, &st_routine.location));
}
}
}
}
stats.st_routines = st_routines.len();
for st_routine in &st_routines {
if st_routine.is_parsed() {
stats.st_parsed_ok += 1;
} else {
stats.st_parsed_err += 1;
}
}
let mut complexities: Vec<usize> = Vec::new();
let mut nestings: Vec<usize> = Vec::new();
for st_routine in &st_routines {
if let Some(ref pou) = st_routine.pou {
let cfg = iec61131::analysis::CfgBuilder::new().build(&pou.body);
let complexity = cfg.cyclomatic_complexity();
let nesting = iec61131::analysis::max_nesting_depth(&pou.body);
complexities.push(complexity);
nestings.push(nesting);
}
}
if !complexities.is_empty() {
stats.st_max_complexity = *complexities.iter().max().unwrap_or(&0);
stats.st_avg_complexity = complexities.iter().sum::<usize>() as f32 / complexities.len() as f32;
}
if !nestings.is_empty() {
stats.st_max_nesting = *nestings.iter().max().unwrap_or(&0);
stats.st_avg_nesting = nestings.iter().sum::<usize>() as f32 / nestings.len() as f32;
}
stats.tag_references = all_refs.len();
stats.unique_tags = tag_xref.len();
ProjectAnalysis {
rungs,
st_routines,
tag_references: all_refs,
tag_xref,
routines: routine_summaries,
instruction_usage,
aoi_definitions,
aoi_usage,
stats,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rung_location_path() {
let loc = RungLocation::new("MainProgram", "MainRoutine", 5);
assert_eq!(loc.path(), "MainProgram/MainRoutine/Rung#5");
}
#[test]
fn test_st_location_path() {
let loc = STLocation::new("MainProgram", "Logic");
assert_eq!(loc.path(), "MainProgram/Logic");
}
#[test]
fn test_parse_stats_default() {
let stats = ParseStats::default();
assert_eq!(stats.st_max_complexity, 0);
assert_eq!(stats.st_avg_complexity, 0.0);
assert_eq!(stats.st_max_nesting, 0);
assert_eq!(stats.st_avg_nesting, 0.0);
}
#[test]
fn test_complexity_stats_calculation() {
let code = r#"
IF a THEN
IF b THEN
x := 1;
ELSIF c THEN
x := 2;
END_IF;
END_IF;
"#;
let wrapped = format!("PROGRAM Test\nVAR\nEND_VAR\n{}\nEND_PROGRAM", code);
let pou = crate::analysis::parse_pou(&wrapped).unwrap();
let cfg = iec61131::analysis::CfgBuilder::new().build(&pou.body);
let complexity = cfg.cyclomatic_complexity();
let nesting = iec61131::analysis::max_nesting_depth(&pou.body);
assert!(complexity >= 3, "Expected complexity >= 3, got {}", complexity);
assert_eq!(nesting, 2, "Expected nesting depth 2, got {}", nesting);
}
}