From 90d1605e28d913484228c044156799e6df016e37 Mon Sep 17 00:00:00 2001 From: Jochen Hoenle Date: Wed, 23 Sep 2026 16:05:26 +0200 Subject: [PATCH] [plantuml tooling] some fixes - allow hide/show_empty_members - fix anonymous element panic --- .../parser/skinparam_and_legend/output.json | 32 +++++++++++ .../skinparam_and_legend.puml | 27 ++++++++++ .../anonymous_frame/anonymous_frame.puml | 25 +++++++++ .../anonymous_frame/output.json | 54 +++++++++++++++++++ .../hide_show_empty_members.puml | 23 ++++++++ .../hide_show_empty_members/output.json | 39 ++++++++++++++ .../integration_test/src/test_error_view.rs | 5 ++ .../activity_diagram/test/integration_test.rs | 5 ++ .../component_diagram/src/component_parser.rs | 6 +++ .../test/component_integration_test.rs | 5 ++ .../puml_parser/src/grammar/activity.pest | 2 +- .../puml_parser/src/grammar/component.pest | 6 ++- .../src/component_resolver.rs | 8 ++- .../tests/component_resolver_test.rs | 5 ++ 14 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/output.json create mode 100644 plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/skinparam_and_legend.puml create mode 100644 plantuml/parser/integration_test/component_diagram/anonymous_frame/anonymous_frame.puml create mode 100644 plantuml/parser/integration_test/component_diagram/anonymous_frame/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/hide_show_empty_members.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/output.json diff --git a/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/output.json b/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/output.json new file mode 100644 index 00000000..3677c20a --- /dev/null +++ b/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/output.json @@ -0,0 +1,32 @@ +{ + "skinparam_and_legend.puml": { + "name": "skinparam_and_legend", + "statements": [ + { + "Start": { + "source": { + "start_line": 19, + "start_column": 1 + } + } + }, + { + "Action": { + "label": "do something", + "source": { + "start_line": 20, + "start_column": 1 + } + } + }, + { + "Stop": { + "source": { + "start_line": 21, + "start_column": 1 + } + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/skinparam_and_legend.puml b/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/skinparam_and_legend.puml new file mode 100644 index 00000000..5bdd1589 --- /dev/null +++ b/plantuml/parser/integration_test/activity_diagram/parser/skinparam_and_legend/skinparam_and_legend.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml skinparam_and_legend + +skinparam activity { + BackgroundColor Red +} + +start +:do something; +stop + +legend + hi +endlegend + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/anonymous_frame/anonymous_frame.puml b/plantuml/parser/integration_test/component_diagram/anonymous_frame/anonymous_frame.puml new file mode 100644 index 00000000..e27670d5 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/anonymous_frame/anonymous_frame.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml anonymous_frame + +component ExternalCaller + +frame { + node AdaptiveApplication { + component ApplicationLogic + } +} + +ExternalCaller --> ApplicationLogic : call + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/anonymous_frame/output.json b/plantuml/parser/integration_test/component_diagram/anonymous_frame/output.json new file mode 100644 index 00000000..d2ee59bd --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/anonymous_frame/output.json @@ -0,0 +1,54 @@ +{ + "anonymous_frame.puml": { + "ExternalCaller": { + "id": "ExternalCaller", + "name": "ExternalCaller", + "alias": null, + "parent_id": null, + "element_type": "Component", + "stereotype": null, + "relations": [ + { + "target": "AdaptiveApplication.ApplicationLogic", + "annotation": "call", + "relation_type": "Dependency", + "source_role": "None", + "source_location": { + "file": "", + "line": 23 + } + } + ], + "source_location": { + "file": "", + "line": 15 + } + }, + "AdaptiveApplication": { + "id": "AdaptiveApplication", + "name": "AdaptiveApplication", + "alias": null, + "parent_id": null, + "element_type": "Node", + "stereotype": null, + "relations": [], + "source_location": { + "file": "", + "line": 18 + } + }, + "AdaptiveApplication.ApplicationLogic": { + "id": "AdaptiveApplication.ApplicationLogic", + "name": "ApplicationLogic", + "alias": null, + "parent_id": "AdaptiveApplication", + "element_type": "Component", + "stereotype": null, + "relations": [], + "source_location": { + "file": "", + "line": 19 + } + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/hide_show_empty_members.puml b/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/hide_show_empty_members.puml new file mode 100644 index 00000000..1bf78b84 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/hide_show_empty_members.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml + +hide empty members +show empty methods +hide empty attributes +show empty fields + +component A +component B + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/output.json new file mode 100644 index 00000000..cea915df --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/hide_show_empty_members/output.json @@ -0,0 +1,39 @@ +{ + "hide_show_empty_members.puml": { + "name": null, + "statements": [ + { + "Element": { + "identity": { + "name": "A", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 20 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "B", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 21 + } + }, + "style": null, + "statements": [] + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/src/test_error_view.rs b/plantuml/parser/integration_test/src/test_error_view.rs index d262ea9e..3d56fd8b 100644 --- a/plantuml/parser/integration_test/src/test_error_view.rs +++ b/plantuml/parser/integration_test/src/test_error_view.rs @@ -266,6 +266,11 @@ impl ErrorView for ComponentResolverError { } => ProjectedError::new("AmbiguousReference") .with_field("reference", reference.clone()) .with_field("candidates", candidates.join(", ")), + + ComponentResolverError::MissingElementIdentity { source_location } => { + ProjectedError::new("MissingElementIdentity") + .with_field("source_location", format!("{:?}", source_location)) + } } } } diff --git a/plantuml/parser/puml_parser/src/activity_diagram/test/integration_test.rs b/plantuml/parser/puml_parser/src/activity_diagram/test/integration_test.rs index dcb03636..78ac90f1 100644 --- a/plantuml/parser/puml_parser/src/activity_diagram/test/integration_test.rs +++ b/plantuml/parser/puml_parser/src/activity_diagram/test/integration_test.rs @@ -74,6 +74,11 @@ fn test_start_stop() { run_activity_diagram_parser_case("start_stop"); } +#[test] +fn test_skinparam_and_legend() { + run_activity_diagram_parser_case("skinparam_and_legend"); +} + #[test] fn test_if() { run_activity_diagram_parser_case("if"); diff --git a/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs b/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs index feb0ec04..cfa1aaac 100644 --- a/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs +++ b/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs @@ -75,6 +75,12 @@ impl PumlComponentParser { match inner.as_rule() { Rule::element => { let element = Self::parse_element(inner, source_file, ignored_notes)?; + if element.identity.name.is_none() && element.identity.alias.is_none() { + // Anonymous container (e.g. bare `frame { ... }`) has no identity to + // resolve against; flatten it like `together` instead of keeping a + // nameless wrapper the resolver can't build an FQN for. + return Ok(element.statements); + } return Ok(vec![Statement::Element(element)]); } Rule::relation => { diff --git a/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs index ed1f41b4..f95470ab 100644 --- a/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs +++ b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs @@ -145,3 +145,8 @@ fn test_relation_quoted_name() { fn test_component_note_alias_hidden_relation() { run_component_diagram_parser_case("component_note_alias_hidden_relation"); } + +#[test] +fn test_hide_show_empty_members() { + run_component_diagram_parser_case("hide_show_empty_members"); +} diff --git a/plantuml/parser/puml_parser/src/grammar/activity.pest b/plantuml/parser/puml_parser/src/grammar/activity.pest index 33d5e8d2..eef3a36f 100644 --- a/plantuml/parser/puml_parser/src/grammar/activity.pest +++ b/plantuml/parser/puml_parser/src/grammar/activity.pest @@ -14,7 +14,7 @@ activity_diagram = { SOI ~ empty_line* ~ startuml - ~ (statement | empty_line)* + ~ (ignored_block | ignored_stmt | statement | empty_line)* ~ activity_enduml ~ EOI } diff --git a/plantuml/parser/puml_parser/src/grammar/component.pest b/plantuml/parser/puml_parser/src/grammar/component.pest index 651644d0..3c7652e3 100644 --- a/plantuml/parser/puml_parser/src/grammar/component.pest +++ b/plantuml/parser/puml_parser/src/grammar/component.pest @@ -15,7 +15,11 @@ diagram_start = { empty_line* ~ startuml ~ (ignored_block | diagram_statement | empty_line)* ~ enduml } -diagram_statement = { ((ignored_stmt | relation | together_block | port_declaration | element | footer_line) ~ EOL) } +diagram_statement = { ((ignored_stmt | component_ignored_stmt | relation | together_block | port_declaration | element | footer_line) ~ EOL) } + +component_ignored_stmt = _{ + shared_hide_show_empty_member_core +} port_declaration = { port_keyword ~ port_name ~ alias_clause? } port_keyword = @{ "portin" | "portout" | "port" } diff --git a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs index 24a4233e..3fa4b977 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs @@ -16,6 +16,7 @@ use std::collections::{BTreeSet, HashMap}; use component_diagram::{ ComponentRelationType, ComponentType, EndpointRole, LogicComponent, LogicRelation, + SourceLocation, }; use component_parser::{Arrow, CompPumlDocument, Element, Port, PortType, Relation, Statement}; use resolver_traits::DiagramResolver; @@ -37,6 +38,9 @@ pub enum ComponentResolverError { #[error("Unknown element type: {element_type}")] UnknownElementType { element_type: String }, + #[error("Element at {source_location:?} has neither a name nor an alias")] + MissingElementIdentity { source_location: SourceLocation }, + #[error("Invalid relationship: {from} -> {to}: {reason}")] InvalidRelationship { from: String, @@ -796,7 +800,9 @@ impl ComponentResolver { .alias .as_deref() .or(element.identity.name.as_deref()) - .expect("Element must have name or alias (guaranteed by grammar)"); + .ok_or_else(|| ComponentResolverError::MissingElementIdentity { + source_location: element.identity.source_location.clone(), + })?; let fqn = self.make_fqn(local_id); if self.elements.contains_key(&fqn) { diff --git a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs index 58424c9f..7c65bb84 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs @@ -109,6 +109,11 @@ fn test_relation_quoted_name() { run_component_resolver_case("relation_quoted_name"); } +#[test] +fn test_anonymous_frame() { + run_component_resolver_case("anonymous_frame"); +} + #[test] fn test_invalid_unresolved_reference() { run_component_resolver_case("invalid_unresolved_reference");