From 9422217e9420c1ea623d26b8d95a1a221a0d310d Mon Sep 17 00:00:00 2001 From: Jochen Hoenle <173445474+hoe-jo@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:44:44 +0200 Subject: [PATCH] update plantuml parser --- .../class_diagram_positive/output.json | 2 - .../integration_test/component_diagram/BUILD | 8 +- .../invalid_duplicate_component.puml | 20 + .../invalid_duplicate_component/output.yaml | 17 + .../invalid_unresolved_reference.puml | 18 + .../invalid_unresolved_reference/output.yaml | 17 + .../plantuml/basic_example.puml | 18 + .../plantuml/changing_arrows_direction.puml | 16 + .../plantuml/changing_arrows_direction2.puml | 16 + .../plantuml/changing_arrows_direction3.puml | 18 + .../plantuml/changing_arrows_direction4.puml | 19 + .../component_diagram/plantuml/component.puml | 20 + .../plantuml/grouping_components.puml | 20 + .../hide_or_remove_unlinked_component | 0 .../hide_or_remove_unlinked_component.puml | 18 + .../hide_or_remove_unlinked_component2.puml | 20 + .../plantuml/individual_colors.puml | 15 + .../plantuml/interfaces.puml | 22 + .../plantuml/long_description.puml | 19 + .../plantuml/naming_exception.puml | 20 + .../component_diagram/plantuml/skinparam.puml | 39 ++ .../plantuml/skinparam2.puml | 35 ++ .../plantuml/specific_skin_parameter.puml | 30 ++ .../plantuml/specific_skin_parameter2.puml | 30 ++ .../plantuml/use_rectangle_notation.puml | 21 + .../plantuml/use_uml1_notation.puml | 21 + .../plantuml/use_uml2_notation.puml | 20 + .../plantuml/using_notes.puml | 29 ++ .../plantuml/using_notes2.puml | 22 + .../plantuml/using_notes3.puml | 22 + .../plantuml/using_sprite_in_stereotype.puml | 39 ++ .../relation_absolute_fqn/output.json | 46 ++ .../relation_absolute_fqn.puml | 27 ++ .../relation_simple_name/output.json | 5 + .../relation_simple_name.puml | 4 +- .../relation_simple_name_alias/output.json | 46 ++ .../relation_simple_name_alias.puml | 27 ++ .../integration_test/src/test_error_view.rs | 4 + plantuml/parser/puml_cli/BUILD | 1 + plantuml/parser/puml_cli/src/main.rs | 40 +- plantuml/parser/puml_lobster/BUILD | 1 + plantuml/parser/puml_lobster/src/lib.rs | 123 ++++- .../puml_parser/src/class_diagram/BUILD | 5 + .../src/class_diagram/src/class_ast.rs | 439 ++++++++++++++++++ .../src/class_diagram/src/class_parser.rs | 108 +++++ .../puml_parser/src/class_diagram/src/lib.rs | 15 - .../class_diagram/test/integration_test.rs | 5 + .../puml_parser/src/component_diagram/BUILD | 19 +- .../component_diagram/src/component_parser.rs | 90 ++-- .../preprocessor/src/include/include_ast.rs | 34 +- .../src/include/include_expander.rs | 31 +- .../src/include/include_parser.rs | 35 +- .../src/procedure/procedure_expander.rs | 106 +++-- .../src/procedure/procedure_parser.rs | 13 + .../src/preprocessor/tests/include_tests.rs | 14 +- .../src/preprocessor/tests/procedure_tests.rs | 20 + .../full_features/full_features.puml | 43 ++ .../class_diagram/full_features/output.json | 173 +++++++ .../namespace_1/namespace_1.puml | 5 + .../class_diagram/namespace_1/output.json | 23 + .../includesub_in_subblock/common.puml | 22 + .../includesub_in_subblock/output.yaml | 37 ++ .../include/includesub_in_subblock/sub.puml | 21 + .../include/includesub_in_subblock/user.puml | 18 + .../common.puml | 0 .../output.yaml | 0 .../user.puml | 0 .../invalid_include_number_sub/common.puml | 19 + .../invalid_include_number_sub/output.yaml | 18 + .../invalid_include_number_sub/user.puml | 18 + .../include/simple_includesub/common.puml | 5 + .../include/simple_includesub/output.yaml | 4 + .../include/simple_includesub/user.puml | 1 + .../preprocessor/procedure/empty/empty.puml | 4 +- .../macro_nested_args/macro_nested_args.puml | 28 ++ .../procedure/macro_nested_args/output.yaml | 19 + .../max_depth_exceeded.puml | 54 +++ .../procedure/max_depth_exceeded/output.yaml | 15 + .../procedure/mix_call/mix_call.puml | 6 +- .../procedure/unknown_var_in_args/output.yaml | 23 + .../unknown_var_in_args.puml | 28 ++ .../procedure/unknown_variable/output.yaml | 17 + .../unknown_variable/unknown_variable.puml | 26 ++ .../src/class_diagram/src/class_resolver.rs | 151 ++++-- .../src/component_resolver.rs | 7 +- .../tests/component_resolver_test.rs | 20 + plantuml/parser/puml_serializer/BUILD | 7 + plantuml/parser/puml_serializer/src/fbs/BUILD | 32 ++ plantuml/parser/puml_serializer/src/lib.rs | 1 + .../puml_serializer/src/serialize/BUILD | 15 + .../src/serialize/class_serializer.rs | 386 +++++++++++++++ 91 files changed, 2904 insertions(+), 231 deletions(-) create mode 100644 plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/invalid_duplicate_component.puml create mode 100644 plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/output.yaml create mode 100644 plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/invalid_unresolved_reference.puml create mode 100644 plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/output.yaml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/basic_example.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction2.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction3.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction4.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/component.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/grouping_components.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component2.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/individual_colors.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/interfaces.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/long_description.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/naming_exception.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/skinparam.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/skinparam2.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter2.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/use_rectangle_notation.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/use_uml1_notation.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/use_uml2_notation.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/using_notes.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/using_notes2.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/using_notes3.puml create mode 100644 plantuml/parser/integration_test/component_diagram/plantuml/using_sprite_in_stereotype.puml create mode 100644 plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/relation_absolute_fqn.puml create mode 100644 plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/relation_simple_name_alias.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/full_features/full_features.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/full_features/output.json create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/sub.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/user.puml rename plantuml/parser/puml_parser/tests/preprocessor/include/{invalid_include_unknow_sub => invalid_include_label_sub}/common.puml (100%) rename plantuml/parser/puml_parser/tests/preprocessor/include/{invalid_include_unknow_sub => invalid_include_label_sub}/output.yaml (100%) rename plantuml/parser/puml_parser/tests/preprocessor/include/{invalid_include_unknow_sub => invalid_include_label_sub}/user.puml (100%) create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/macro_nested_args.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/max_depth_exceeded.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/unknown_var_in_args.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/unknown_variable.puml create mode 100644 plantuml/parser/puml_serializer/src/serialize/class_serializer.rs diff --git a/plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json b/plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json index 769b65a..f6453b1 100644 --- a/plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json +++ b/plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json @@ -1,6 +1,5 @@ {"class_diagram_positive.puml": { - "class_positive_test": { "name": "class_positive_test", "entities": [ { @@ -619,6 +618,5 @@ "class_positive_test" ], "version": null - } } } diff --git a/plantuml/parser/integration_test/component_diagram/BUILD b/plantuml/parser/integration_test/component_diagram/BUILD index f7eba97..8373e0f 100644 --- a/plantuml/parser/integration_test/component_diagram/BUILD +++ b/plantuml/parser/integration_test/component_diagram/BUILD @@ -14,8 +14,14 @@ filegroup( name = "component_diagram_files", srcs = glob([ "**/*.puml", - # "**/*.yaml", + "**/*.yaml", "**/*.json", ]), visibility = ["//visibility:public"], ) + +filegroup( + name = "component_integration_test_files", + srcs = glob(["plantuml/*.puml"]), + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/invalid_duplicate_component.puml b/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/invalid_duplicate_component.puml new file mode 100644 index 0000000..51563c1 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/invalid_duplicate_component.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 "Invalid Duplicate Component" + +package "Pkg" as Pkg { + component "Component A" as X <> + component "Component B" as X <> +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/output.yaml new file mode 100644 index 0000000..03473a6 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_duplicate_component/output.yaml @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +invalid_duplicate_component.puml: + error: + type: "DuplicateComponent" + fields: + component_id: "Pkg.X" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/invalid_unresolved_reference.puml b/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/invalid_unresolved_reference.puml new file mode 100644 index 0000000..3672a67 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/invalid_unresolved_reference.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 "Invalid Unresolved Reference" + +component "Component A" as ComponentA <> +ComponentA --> MissingComponent : uses + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/output.yaml new file mode 100644 index 0000000..bb02425 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_unresolved_reference/output.yaml @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +invalid_unresolved_reference.puml: + error: + type: "UnresolvedReference" + fields: + reference: "MissingComponent" diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/basic_example.puml b/plantuml/parser/integration_test/component_diagram/plantuml/basic_example.puml new file mode 100644 index 0000000..af482b3 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/basic_example.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 + +DataAccess - [First Component] +[First Component] ..> HTTP : use + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction.puml b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction.puml new file mode 100644 index 0000000..278f83e --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction.puml @@ -0,0 +1,16 @@ +' ******************************************************************************* +' 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 +[Component] --> Interface1 +[Component] -> Interface2 +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction2.puml b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction2.puml new file mode 100644 index 0000000..4ab9b4f --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction2.puml @@ -0,0 +1,16 @@ +' ******************************************************************************* +' 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 +Interface1 <-- [Component] +Interface2 <- [Component] +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction3.puml b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction3.puml new file mode 100644 index 0000000..a9c13fc --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction3.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 +[Component] -left-> left +[Component] -right-> right +[Component] -up-> up +[Component] -down-> down +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction4.puml b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction4.puml new file mode 100644 index 0000000..cd4fa6e --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction4.puml @@ -0,0 +1,19 @@ +' ******************************************************************************* +' 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 +left to right direction +[Component] -left-> left +[Component] -right-> right +[Component] -up-> up +[Component] -down-> down +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/component.puml b/plantuml/parser/integration_test/component_diagram/plantuml/component.puml new file mode 100644 index 0000000..7ee2e94 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/component.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 + +[First component] +[Another component] as Comp2 +component Comp3 +component [Last\ncomponent] as Comp4 + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/grouping_components.puml b/plantuml/parser/integration_test/component_diagram/plantuml/grouping_components.puml new file mode 100644 index 0000000..7ee2e94 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/grouping_components.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 + +[First component] +[Another component] as Comp2 +component Comp3 +component [Last\ncomponent] as Comp4 + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component b/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component new file mode 100644 index 0000000..e69de29 diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component.puml b/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component.puml new file mode 100644 index 0000000..609c422 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 +component C1 +component C2 +component C3 +C1 -- C2 +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component2.puml b/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component2.puml new file mode 100644 index 0000000..472394b --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component2.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 +component C1 +component C2 +component C3 +C1 -- C2 + +remove @unlinked +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/individual_colors.puml b/plantuml/parser/integration_test/component_diagram/plantuml/individual_colors.puml new file mode 100644 index 0000000..18f982f --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/individual_colors.puml @@ -0,0 +1,15 @@ +' ******************************************************************************* +' 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 +component [Web Server] #Yellow +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/interfaces.puml b/plantuml/parser/integration_test/component_diagram/plantuml/interfaces.puml new file mode 100644 index 0000000..454c7a9 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/interfaces.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 + +() "First Interface" +() "Another interface" as Interf2 +interface Interf3 +interface "Last\ninterface" as Interf4 + +[component] +footer //Adding "component" to force diagram to be a **component diagram**// +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/long_description.puml b/plantuml/parser/integration_test/component_diagram/plantuml/long_description.puml new file mode 100644 index 0000000..be21269 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/long_description.puml @@ -0,0 +1,19 @@ +' ******************************************************************************* +' 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 +component comp1 [ +This component +has a long comment +on several lines +] +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/naming_exception.puml b/plantuml/parser/integration_test/component_diagram/plantuml/naming_exception.puml new file mode 100644 index 0000000..58021e1 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/naming_exception.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 +component [$C1] +component [$C2] $C2 +component [$C2] as dollarC2 +remove $C1 +remove $C2 +remove dollarC2 +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/skinparam.puml b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam.puml new file mode 100644 index 0000000..201d5bf --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam.puml @@ -0,0 +1,39 @@ +' ******************************************************************************* +' 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 interface { + backgroundColor RosyBrown + borderColor orange +} + +skinparam component { + FontSize 13 + BackgroundColor<> Pink + BorderColor<> #FF6655 + FontName Courier + BorderColor black + BackgroundColor gold + ArrowFontName Impact + ArrowColor #FF6655 + ArrowFontColor #777777 +} + +() "Data Access" as DA +Component "Web Server" as WS << Apache >> + +DA - [First Component] +[First Component] ..> () HTTP : use +HTTP - WS + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/skinparam2.puml b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam2.puml new file mode 100644 index 0000000..8eaf1f7 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam2.puml @@ -0,0 +1,35 @@ +' ******************************************************************************* +' 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 component { + backgroundColor<> DarkKhaki + backgroundColor<> Green +} + +skinparam node { + borderColor Green + backgroundColor Yellow + backgroundColor<> Magenta +} +skinparam databaseBackgroundColor Aqua + +[AA] <> +[BB] <> +[CC] <> + +node node1 +node node2 <> +database Production + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter.puml b/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter.puml new file mode 100644 index 0000000..14aea8b --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter.puml @@ -0,0 +1,30 @@ +' ******************************************************************************* +' 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 BackgroundColor transparent +skinparam componentStyle uml2 +component A { + component "A.1" { +} + component A.44 { + [A4.1] +} + component "A.2" + [A.3] + component A.5 [ +A.5] + component A.6 [ +] +} +[a]->[b] +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter2.puml b/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter2.puml new file mode 100644 index 0000000..d761e6f --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/specific_skin_parameter2.puml @@ -0,0 +1,30 @@ +' ******************************************************************************* +' 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 BackgroundColor transparent +skinparam componentStyle rectangle +component A { + component "A.1" { +} + component A.44 { + [A4.1] +} + component "A.2" + [A.3] + component A.5 [ +A.5] + component A.6 [ +] +} +[a]->[b] +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/use_rectangle_notation.puml b/plantuml/parser/integration_test/component_diagram/plantuml/use_rectangle_notation.puml new file mode 100644 index 0000000..944f18b --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/use_rectangle_notation.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' 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 componentStyle rectangle + +interface "Data Access" as DA + +DA - [First Component] +[First Component] ..> HTTP : use + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/use_uml1_notation.puml b/plantuml/parser/integration_test/component_diagram/plantuml/use_uml1_notation.puml new file mode 100644 index 0000000..ac1ab5b --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/use_uml1_notation.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' 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 componentStyle uml1 + +interface "Data Access" as DA + +DA - [First Component] +[First Component] ..> HTTP : use + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/use_uml2_notation.puml b/plantuml/parser/integration_test/component_diagram/plantuml/use_uml2_notation.puml new file mode 100644 index 0000000..a996d8d --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/use_uml2_notation.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 + +interface "Data Access" as DA + +DA - [First Component] +[First Component] ..> HTTP : use + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/using_notes.puml b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes.puml new file mode 100644 index 0000000..244685a --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes.puml @@ -0,0 +1,29 @@ +' ******************************************************************************* +' 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 +[Component] as C + +note top of C: A top note + +note bottom of C + A bottom note can also + be on several lines +end note + +note left of C + A left note can also + be on several lines +end note + +note right of C: A right note +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/using_notes2.puml b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes2.puml new file mode 100644 index 0000000..074e565 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes2.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 +[Component] as C + +note as N + A floating note can also + be on several lines +end note + +C .. N +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/using_notes3.puml b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes3.puml new file mode 100644 index 0000000..074e565 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/using_notes3.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 +[Component] as C + +note as N + A floating note can also + be on several lines +end note + +C .. N +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/using_sprite_in_stereotype.puml b/plantuml/parser/integration_test/component_diagram/plantuml/using_sprite_in_stereotype.puml new file mode 100644 index 0000000..7c19ca5 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/using_sprite_in_stereotype.puml @@ -0,0 +1,39 @@ +' ******************************************************************************* +' 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 +sprite $businessProcess [16x16/16] { +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFF0FFFFF +FFFFFFFFFF00FFFF +FF00000000000FFF +FF000000000000FF +FF00000000000FFF +FFFFFFFFFF00FFFF +FFFFFFFFFF0FFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +FFFFFFFFFFFFFFFF +} + + +rectangle " End to End\nbusiness process" <<$businessProcess>> { + rectangle "inner process 1" <<$businessProcess>> as src + rectangle "inner process 2" <<$businessProcess>> as tgt + src -> tgt +} +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/output.json b/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/output.json new file mode 100644 index 0000000..306c9ce --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/output.json @@ -0,0 +1,46 @@ +{ + "relation_absolute_fqn.puml": { + "PkgA": { + "id": "PkgA", + "name": "Package A", + "alias": "PkgA", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "PkgA.ComponentA": { + "id": "PkgA.ComponentA", + "name": "Component A", + "alias": "ComponentA", + "parent_id": "PkgA", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + }, + "PkgB": { + "id": "PkgB", + "name": "Package B", + "alias": "PkgB", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "PkgB.ComponentB": { + "id": "PkgB.ComponentB", + "name": "Component B", + "alias": "ComponentB", + "parent_id": "PkgB", + "comp_type": "Component", + "stereotype": "component", + "relations": [ + { + "target": "PkgA.ComponentA", + "annotation": "uses", + "relation_type": "None" + } + ] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/relation_absolute_fqn.puml b/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/relation_absolute_fqn.puml new file mode 100644 index 0000000..3a25f5a --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_absolute_fqn/relation_absolute_fqn.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 "Relation with Absolute FQN" + +package "Package A" as PkgA { + component "Component A" as ComponentA <> { + } +} + +package "Package B" as PkgB { + component "Component B" as ComponentB <> { + } + + ComponentB --> PkgA.ComponentA : uses +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json b/plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json index 09ea7c1..8a6ee5f 100644 --- a/plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json @@ -26,6 +26,11 @@ "comp_type": "Component", "stereotype": "component", "relations": [ + { + "target": "SampleSEooC.ComponentA.UnitA", + "annotation": "uses", + "relation_type": "None" + }, { "target": "SampleSEooC.ComponentB", "annotation": "uses", diff --git a/plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.puml b/plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.puml index adff2ba..02b7675 100644 --- a/plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.puml +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.puml @@ -10,7 +10,7 @@ ' ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml +@startuml "Relation with Simple Name" package "Sample SEooC" as SampleSEooC #LightBlue { component "Component A" as ComponentA <> { @@ -21,7 +21,7 @@ package "Sample SEooC" as SampleSEooC #LightBlue { component "Unit B" as UnitB <> { } } - + ComponentA --> UnitA : uses ComponentA --> ComponentB : uses } diff --git a/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/output.json b/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/output.json new file mode 100644 index 0000000..795edb4 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/output.json @@ -0,0 +1,46 @@ +{ + "relation_simple_name_alias.puml": { + "PkgA": { + "id": "PkgA", + "name": "Pkg A", + "alias": "PkgA", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "PkgA.AliasA": { + "id": "PkgA.AliasA", + "name": "Component A", + "alias": "AliasA", + "parent_id": "PkgA", + "comp_type": "Component", + "stereotype": "component", + "relations": [ + { + "target": "PkgB.AliasB", + "annotation": "uses", + "relation_type": "None" + } + ] + }, + "PkgB": { + "id": "PkgB", + "name": "Pkg B", + "alias": "PkgB", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "PkgB.AliasB": { + "id": "PkgB.AliasB", + "name": "Component B", + "alias": "AliasB", + "parent_id": "PkgB", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/relation_simple_name_alias.puml b/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/relation_simple_name_alias.puml new file mode 100644 index 0000000..37d341a --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name_alias/relation_simple_name_alias.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 "Relation with Simple Name Alias" + +package "Pkg A" as PkgA { + component "Component A" as AliasA <> { + } +} + +package "Pkg B" as PkgB { + component "Component B" as AliasB <> { + } +} + +PkgA.AliasA --> AliasB : uses + +@enduml diff --git a/plantuml/parser/integration_test/src/test_error_view.rs b/plantuml/parser/integration_test/src/test_error_view.rs index 44fb07e..b8cb580 100644 --- a/plantuml/parser/integration_test/src/test_error_view.rs +++ b/plantuml/parser/integration_test/src/test_error_view.rs @@ -157,6 +157,10 @@ impl ErrorView for ProcedureExpandError { .with_field("expected", expected.to_string()) .with_field("actual", actual.to_string()), + ProcedureExpandError::UnknownVariable { name } => { + ProjectedError::new("UnknownVariable").with_field("name", name.clone()) + } + ProcedureExpandError::RecursiveMacro { chain, name } => { let chain_str = chain.join(" -> "); ProjectedError::new("RecursiveMacro") diff --git a/plantuml/parser/puml_cli/BUILD b/plantuml/parser/puml_cli/BUILD index d121227..4a71a49 100644 --- a/plantuml/parser/puml_cli/BUILD +++ b/plantuml/parser/puml_cli/BUILD @@ -23,6 +23,7 @@ rust_binary( "//plantuml/parser/puml_resolver", "//plantuml/parser/puml_serializer", "//plantuml/parser/puml_utils", + "//tools/metamodel:class_diagram", "@crates//:clap", "@crates//:env_logger", "@crates//:log", diff --git a/plantuml/parser/puml_cli/src/main.rs b/plantuml/parser/puml_cli/src/main.rs index 649b1fa..816b153 100644 --- a/plantuml/parser/puml_cli/src/main.rs +++ b/plantuml/parser/puml_cli/src/main.rs @@ -21,12 +21,12 @@ use std::fs; use std::path::{Path, PathBuf}; use std::rc::Rc; -use puml_lobster::write_lobster_to_file; +use puml_lobster::{write_lobster_to_file, LobsterModel}; use puml_parser::{ DiagramParser, Preprocessor, PumlClassParser, PumlComponentParser, PumlSequenceParser, }; -use puml_resolver::{ComponentResolver, DiagramResolver}; -use puml_serializer::ComponentSerializer; +use puml_resolver::{ClassResolver, ComponentResolver, DiagramResolver}; +use puml_serializer::{ClassSerializer, ComponentSerializer}; use puml_utils::{write_fbs_to_file, write_json_to_file, write_placeholder_file, LogLevel}; /// CLI wrapper for LogLevel that implements ValueEnum @@ -86,9 +86,9 @@ struct Args { /// Output directory for generated lobster files (optional). /// When set, a .lobster is written for each diagram that resolves - /// to a Component model (independent of --fbs-output-dir). On resolve - /// errors a placeholder empty .lobster is written so the build output - /// set is always complete. + /// to a Component or Class model (independent of --fbs-output-dir). + /// On resolve errors a placeholder empty .lobster is written so the + /// build output set is always complete. #[arg(long)] lobster_output_dir: Option, } @@ -181,10 +181,12 @@ fn main() -> Result<(), Box> { write_fbs_to_file(&fbs_buffer, path, dir)?; } - if let (ResolvedDiagram::Component(ref model), Some(ldir)) = - (&logic_result, &lobster_output_dir) - { - write_lobster_to_file(model, path, ldir)?; + if let Some(ldir) = &lobster_output_dir { + let lobster_model = match &logic_result { + ResolvedDiagram::Component(model) => LobsterModel::Component(model), + ResolvedDiagram::Class(model) => LobsterModel::Class(model), + }; + write_lobster_to_file(lobster_model, path, ldir)?; } } Err(e) => { @@ -198,7 +200,7 @@ fn main() -> Result<(), Box> { write_placeholder_file(path, dir)?; } if let Some(ref ldir) = lobster_output_dir { - write_lobster_to_file(&HashMap::new(), path, ldir)?; + write_lobster_to_file(LobsterModel::Empty, path, ldir)?; } } } @@ -212,10 +214,10 @@ fn serialize_resolved_diagram(resolved_content: &ResolvedDiagram, source_file: & match resolved_content { ResolvedDiagram::Component(resolved_content) => { ComponentSerializer::serialize(resolved_content, source_file) - } // ResolvedDiagram::Class(_) => { // placeholder - // /* class serializer */ - // } - // ResolvedDiagram::Sequence(_) => { // placeholder + } + ResolvedDiagram::Class(resolved_content) => { + ClassSerializer::serialize(resolved_content, source_file) + } // ResolvedDiagram::Sequence(_) => { // placeholder // /* sequence serializer */ // } } @@ -224,7 +226,7 @@ fn serialize_resolved_diagram(resolved_content: &ResolvedDiagram, source_file: & #[derive(Debug, Serialize)] pub enum ResolvedDiagram { Component(HashMap), - // Class(ClassLogic), // placeholder + Class(class_diagram::ClassDiagram), // Sequence(SequenceLogic), // placeholder } @@ -236,9 +238,9 @@ fn resolve_parsed_diagram( let mut resolver = ComponentResolver::new(); puml_resolver(&mut resolver, &parsed_content).map(ResolvedDiagram::Component) } - ParsedDiagram::Class(_) => { - /* class resolver */ - Err("Class diagrams not implemented".into()) + ParsedDiagram::Class(parsed_content) => { + let mut resolver = ClassResolver::new(); + puml_resolver(&mut resolver, &parsed_content).map(ResolvedDiagram::Class) } ParsedDiagram::Sequence(_) => { /* sequence resolver */ diff --git a/plantuml/parser/puml_lobster/BUILD b/plantuml/parser/puml_lobster/BUILD index 4fa3fe2..251cce9 100644 --- a/plantuml/parser/puml_lobster/BUILD +++ b/plantuml/parser/puml_lobster/BUILD @@ -19,6 +19,7 @@ rust_library( visibility = ["//plantuml/parser:__subpackages__"], deps = [ "//plantuml/parser/puml_resolver", + "//tools/metamodel:class_diagram", "@crates//:serde_json", ], ) diff --git a/plantuml/parser/puml_lobster/src/lib.rs b/plantuml/parser/puml_lobster/src/lib.rs index 55e9b2d..a7cbaf4 100644 --- a/plantuml/parser/puml_lobster/src/lib.rs +++ b/plantuml/parser/puml_lobster/src/lib.rs @@ -16,6 +16,7 @@ //! //! Only [`ComponentType::Interface`] elements are emitted +use class_diagram::{ClassDiagram, EntityType}; use puml_resolver::{ComponentType, LogicComponent}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -24,36 +25,53 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; +pub enum LobsterModel<'a> { + Component(&'a HashMap), + Class(&'a ClassDiagram), + Empty, +} + /// Convert an in-memory resolved component model to a `lobster-imp-trace` /// JSON [`Value`]. /// /// `source_path` is embedded in the `location.file` field of every emitted /// item so that LOBSTER can trace items back to their source diagram. -pub fn model_to_lobster(model: &HashMap, source_path: &str) -> Value { - let mut items: Vec = model +fn comp_model_to_lobster(model: &HashMap, source_path: &str) -> Value { + let items: Vec = model .values() .filter(|c| c.comp_type == ComponentType::Interface) - .map(|c| { - json!({ - "tag": format!("req {}", c.id), - "location": { - "kind": "file", - "file": source_path, - "line": 1, - "column": null, - }, - "name": c.id, - "messages": [], - "just_up": [], - "just_down": [], - "just_global": [], - "refs": [], - "language": "Architecture", - "kind": "Interface", - }) + .map(|c| build_lobster_item(&c.id, source_path, None, "Interface")) + .collect(); + + lobster_document_from_items(items) +} + +/// Convert an in-memory resolved class model to a `lobster-imp-trace` +/// JSON [`Value`]. +/// +/// Every class entity becomes one lobster item. If an entity carries explicit +/// source location metadata that is used; otherwise `source_path` is used and +/// the line is emitted as `null` because LOBSTER does not accept `0`. +fn class_model_to_lobster(model: &ClassDiagram, source_path: &str) -> Value { + let items: Vec = model + .entities + .iter() + .map(|entity| { + let source_file = entity.source_file.as_deref().unwrap_or(source_path); + + build_lobster_item( + &entity.id, + source_file, + entity.source_line, + map_entity_type_to_kind(entity.entity_type), + ) }) .collect(); + lobster_document_from_items(items) +} + +fn lobster_document_from_items(mut items: Vec) -> Value { // Sort by tag for deterministic output items.sort_by(|a, b| { a["tag"] @@ -70,12 +88,72 @@ pub fn model_to_lobster(model: &HashMap, source_path: &s }) } +fn empty_lobster_document() -> Value { + lobster_document_from_items(Vec::new()) +} + +fn build_lobster_item( + name: &str, + source_file: &str, + source_line: Option, + kind: &str, +) -> Value { + json!({ + "tag": format!("req {}", name), + "location": { + "kind": "file", + "file": source_file, + "line": source_line, + "column": null, + }, + "name": name, + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "refs": [], + "language": "Architecture", + "kind": kind, + }) +} + +fn map_entity_type_to_kind(entity_type: EntityType) -> &'static str { + match entity_type { + EntityType::Class => "Class", + EntityType::Struct => "Struct", + EntityType::Interface => "Interface", + EntityType::Enum => "Enum", + EntityType::AbstractClass => "AbstractClass", + EntityType::Annotation => "Annotation", + } +} + /// Write a `lobster-imp-trace` JSON file derived from `model` into `output_dir`. /// /// The output filename is `.lobster` where `` is the file stem of /// `input_path` (the original `.puml` source file). pub fn write_lobster_to_file( - model: &HashMap, + model: LobsterModel<'_>, + input_path: &Path, + output_dir: &Path, +) -> io::Result { + let lobster = match model { + LobsterModel::Component(component_model) => { + let source_str = input_path.to_string_lossy().into_owned(); + comp_model_to_lobster(component_model, &source_str) + } + LobsterModel::Class(class_model) => { + let source_str = input_path.to_string_lossy().into_owned(); + class_model_to_lobster(class_model, &source_str) + } + LobsterModel::Empty => empty_lobster_document(), + }; + + write_lobster_value_to_file(&lobster, input_path, output_dir) +} + +fn write_lobster_value_to_file( + lobster: &Value, input_path: &Path, output_dir: &Path, ) -> io::Result { @@ -85,9 +163,6 @@ pub fn write_lobster_to_file( let output_path = output_dir.join(file_stem).with_extension("lobster"); - let source_str = input_path.to_string_lossy().into_owned(); - let lobster = model_to_lobster(model, &source_str); - let content = serde_json::to_string_pretty(&lobster) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; diff --git a/plantuml/parser/puml_parser/src/class_diagram/BUILD b/plantuml/parser/puml_parser/src/class_diagram/BUILD index 9ba95f1..4cbd7fd 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/BUILD +++ b/plantuml/parser/puml_parser/src/class_diagram/BUILD @@ -44,6 +44,11 @@ rust_library( ], ) +rust_test( + name = "puml_parser_class_unit_test", + crate = ":puml_parser_class", +) + rust_test( name = "puml_parser_class_integration_test", srcs = ["test/integration_test.rs"], diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs b/plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs index dfae8d2..e400ee9 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs @@ -240,3 +240,442 @@ impl AsRef for ClassUmlFile { &self.name } } + +#[cfg(test)] +mod tests { + use super::*; + use parser_core::common_ast::ArrowLine; + + #[test] + fn test_element_set_namespace_and_package() { + let mut class = ClassDef::default(); + class.name.internal = "TestClass".into(); + + let mut element = Element::ClassDef(class); + + element.set_namespace("ns1".into()); + element.set_package("pkg1".into()); + + let Element::ClassDef(def) = element else { + unreachable!(); + }; + assert_eq!(def.namespace, "ns1"); + assert_eq!(def.package, "pkg1"); + } + + #[test] + fn test_name_write() { + let mut name = Name::default(); + + name.write_name("InternalName", Some("DisplayName")); + + assert_eq!(name.internal, "InternalName"); + assert_eq!(name.display, Some("DisplayName".to_string())); + } + + #[test] + fn test_name_write_without_display() { + let mut name = Name::default(); + + name.write_name("OnlyInternal", None::); + + assert_eq!(name.internal, "OnlyInternal"); + assert_eq!(name.display, None); + } + + #[test] + fn test_attribute_default() { + let attr = Attribute::default(); + + assert_eq!(attr.visibility, Visibility::Public); + assert_eq!(attr.name, ""); + assert_eq!(attr.r#type, None); + } + + #[test] + fn test_method_default() { + let method = Method::default(); + + assert_eq!(method.visibility, Visibility::Public); + assert_eq!(method.name, ""); + assert!(method.generic_params.is_empty()); + assert!(method.params.is_empty()); + assert_eq!(method.r#type, None); + } + + #[test] + fn test_class_uml_file_is_empty() { + let file = ClassUmlFile::default(); + assert!(file.is_empty()); + } + + #[test] + fn test_class_uml_file_not_empty_elements() { + let mut file = ClassUmlFile::default(); + + file.elements + .push(ClassUmlTopLevel::Namespace(Namespace::default())); + + assert!(!file.is_empty()); + } + + #[test] + fn test_class_uml_file_not_empty_relationships() { + let mut file = ClassUmlFile::default(); + + file.relationships.push(Relationship { + left: "A".into(), + right: "B".into(), + arrow: Arrow { + left: None, + line: ArrowLine { raw: "-->".into() }, + middle: None, + right: None, + }, + label: None, + }); + + assert!(!file.is_empty()); + } + + #[test] + fn test_enum_def() { + let mut enum_def = EnumDef::default(); + + enum_def.name.internal = "Color".into(); + enum_def.items.push(EnumItem { + visibility: Some(Visibility::Public), + name: "RED".into(), + value: Some(EnumValue::Literal("1".into())), + }); + + assert_eq!(enum_def.name.internal, "Color"); + assert_eq!(enum_def.items.len(), 1); + } + + #[test] + fn test_namespace_nested() { + let mut root = Namespace::default(); + root.name.internal = "root".into(); + + let mut child = Namespace::default(); + child.name.internal = "child".into(); + + root.namespaces.push(child); + + assert_eq!(root.namespaces.len(), 1); + assert_eq!(root.namespaces[0].name.internal, "child"); + } + + #[test] + fn test_package_relationships() { + let mut pkg = Package::default(); + + pkg.relationships.push(Relationship { + left: "A".into(), + right: "B".into(), + arrow: Arrow { + left: None, + line: ArrowLine { raw: "-->".into() }, + middle: None, + right: None, + }, + label: Some("uses".into()), + }); + + assert_eq!(pkg.relationships.len(), 1); + } + + #[test] + fn test_element_all_variants_set_namespace() { + let mut elements = vec![ + Element::ClassDef(ClassDef::default()), + Element::StructDef(StructDef::default()), + Element::EnumDef(EnumDef::default()), + Element::InterfaceDef(InterfaceDef::default()), + ]; + + for el in elements.iter_mut() { + el.set_namespace("test_ns".into()); + } + + for el in elements { + match el { + Element::ClassDef(d) => assert_eq!(d.namespace, "test_ns"), + Element::StructDef(d) => assert_eq!(d.namespace, "test_ns"), + Element::EnumDef(d) => assert_eq!(d.namespace, "test_ns"), + Element::InterfaceDef(d) => assert_eq!(d.namespace, "test_ns"), + } + } + } + + #[test] + fn test_methods_mut() { + let mut class = ClassDef::default(); + + class.methods_mut().push(Method { + name: "foo".into(), + ..Default::default() + }); + + assert_eq!(class.methods.len(), 1); + assert_eq!(class.methods[0].name, "foo"); + } + + #[test] + fn test_attributes_mut() { + let mut class = ClassDef::default(); + + class.attributes_mut().push(Attribute { + name: "field".into(), + ..Default::default() + }); + + assert_eq!(class.attributes.len(), 1); + } + + #[test] + fn test_name_mut() { + let mut class = ClassDef::default(); + + class.name_mut().internal = "MyClass".into(); + + assert_eq!(class.name.internal, "MyClass"); + } + + #[test] + fn test_as_ref() { + let file = ClassUmlFile { + name: "test_file".into(), + ..Default::default() + }; + + let name: &str = file.as_ref(); + + assert_eq!(name, "test_file"); + } + + #[test] + fn test_struct_methods_mut() { + let mut s = StructDef::default(); + + s.methods_mut().push(Method::default()); + + assert_eq!(s.methods.len(), 1); + } + + #[test] + fn test_interface_methods_mut() { + let mut i = InterfaceDef::default(); + + i.methods_mut().push(Method::default()); + + assert_eq!(i.methods.len(), 1); + } + + #[test] + fn test_typedef_trait_object_calls_for_class_def() { + use crate::class_traits::TypeDef; + + let mut c = ClassDef::default(); + + { + let obj: &mut dyn TypeDef = &mut c; + + obj.name_mut().internal = "ClassViaTrait".into(); + obj.attributes_mut().push(Attribute { + name: "field".into(), + ..Default::default() + }); + obj.methods_mut().push(Method { + name: "method".into(), + ..Default::default() + }); + } + + assert_eq!(c.name.internal, "ClassViaTrait"); + assert_eq!(c.attributes.len(), 1); + assert_eq!(c.attributes[0].name, "field"); + assert_eq!(c.methods.len(), 1); + assert_eq!(c.methods[0].name, "method"); + } + + #[test] + fn test_typedef_trait_object_calls_for_struct_def() { + use crate::class_traits::TypeDef; + + let mut s = StructDef::default(); + + { + let obj: &mut dyn TypeDef = &mut s; + + obj.name_mut().internal = "StructViaTrait".into(); + obj.attributes_mut().push(Attribute { + name: "field".into(), + ..Default::default() + }); + obj.methods_mut().push(Method { + name: "method".into(), + ..Default::default() + }); + } + + assert_eq!(s.name.internal, "StructViaTrait"); + assert_eq!(s.attributes.len(), 1); + assert_eq!(s.attributes[0].name, "field"); + assert_eq!(s.methods.len(), 1); + assert_eq!(s.methods[0].name, "method"); + } + + #[test] + fn test_typedef_trait_object_calls_for_interface_def() { + use crate::class_traits::TypeDef; + + let mut i = InterfaceDef::default(); + + { + let obj: &mut dyn TypeDef = &mut i; + + obj.name_mut().internal = "InterfaceViaTrait".into(); + obj.attributes_mut().push(Attribute { + name: "field".into(), + ..Default::default() + }); + obj.methods_mut().push(Method { + name: "method".into(), + ..Default::default() + }); + } + + assert_eq!(i.name.internal, "InterfaceViaTrait"); + assert_eq!(i.attributes.len(), 1); + assert_eq!(i.attributes[0].name, "field"); + assert_eq!(i.methods.len(), 1); + assert_eq!(i.methods[0].name, "method"); + } + + #[test] + fn test_class_uml_file_supports_generic_as_ref_usage() { + fn use_as_ref>(v: T) -> String { + v.as_ref().to_string() + } + + let file = ClassUmlFile { + name: "abc".into(), + ..Default::default() + }; + + let s = use_as_ref(file); + + assert_eq!(s, "abc"); + } + + #[test] + fn test_class_uml_file_is_empty_for_empty_named_file() { + let file = ClassUmlFile { + name: "x".into(), + elements: vec![], + relationships: vec![], + }; + + assert!(file.is_empty()); + } + + #[test] + fn test_name_supports_generic_write_name_usage() { + fn call_write(mut t: T) -> T { + t.write_name("a", Some("b")); + t + } + + let name = Name::default(); + let name = call_write(name); + + assert_eq!(name.internal, "a"); + } + + #[test] + fn test_name_write_accepts_owned_and_borrowed_variants() { + let mut name = Name { + internal: "abc".into(), + ..Default::default() + }; + + name.write_name("abc", None::); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, None); + + name.write_name("abc", Some("ABC")); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, Some("ABC".to_string())); + + let internal = String::from("xyz"); + let display = String::from("XYZ"); + + name.write_name(internal, Some(display)); + + assert_eq!(name.internal, "xyz"); + assert_eq!(name.display, Some("XYZ".to_string())); + } + + #[test] + fn test_struct_methods_mut_returns_mutable_reference() { + let mut s = StructDef::default(); + + s.methods_mut().push(Method { + name: "struct_method".into(), + ..Default::default() + }); + + assert_eq!(s.methods.len(), 1); + assert_eq!(s.methods[0].name, "struct_method"); + } + + #[test] + fn test_class_uml_file_default_supports_as_ref_and_is_empty() { + let file1 = ClassUmlFile::default(); + assert_eq!(file1.as_ref(), ""); + assert!(file1.is_empty()); + + let file2 = ClassUmlFile::default(); + assert_eq!(file2.as_ref(), ""); + assert!(file2.is_empty()); + } + + #[test] + fn test_generic_trait_calls_support_mixed_input_variants() { + let mut name = Name::default(); + + name.write_name("abc", None::); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, None); + + name.write_name("abc", Some(String::from("x"))); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, Some("x".to_string())); + + name.write_name("abc", None::<&str>); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, None); + + name.write_name("abc", Some("x")); + assert_eq!(name.internal, "abc"); + assert_eq!(name.display, Some("x".to_string())); + + let mut s = StructDef::default(); + s.methods_mut().push(Method { + name: "mixed_method".into(), + ..Default::default() + }); + assert_eq!(s.methods.len(), 1); + assert_eq!(s.methods[0].name, "mixed_method"); + + let file1 = ClassUmlFile::default(); + assert_eq!(file1.as_ref(), ""); + assert!(file1.is_empty()); + + let file2 = ClassUmlFile::default(); + assert_eq!(file2.as_ref(), ""); + assert!(file2.is_empty()); + } +} diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs b/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs index ebc2d87..83e0211 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs @@ -491,3 +491,111 @@ impl DiagramParser for PumlClassParser { Ok(uml_file) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_visibility_none() { + let vis = super::parse_visibility(None); + assert_eq!(vis, Visibility::Public); + } + + #[test] + fn test_parse_visibility_unknown_symbol() { + let pair = PlantUmlCommonParser::parse(Rule::identifier, "abc") + .unwrap() + .next() + .unwrap(); + + let vis = super::parse_visibility(Some(pair)); + + assert_eq!(vis, Visibility::Public); + } + + #[test] + fn test_parse_param_unnamed_varargs() { + let input = "int..."; + let pair = PlantUmlCommonParser::parse(Rule::param, input) + .unwrap() + .next() + .unwrap(); + + let param = super::parse_param(pair); + + assert_eq!(param.name, None); + assert_eq!(param.param_type, "int"); + assert!(param.varargs); + } + + #[test] + fn test_parse_file_error() { + let mut parser = PumlClassParser; + + let result = parser.parse_file( + &std::rc::Rc::new(std::path::PathBuf::from("test.puml")), + "invalid syntax !!!", + LogLevel::Info, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_attribute_without_name() { + let input = r#"@startuml + class A { + +a + } + @enduml + "#; + + let mut parser = PumlClassParser; + let result = parser + .parse_file( + &std::rc::Rc::new(std::path::PathBuf::from("test.puml")), + input, + LogLevel::Info, + ) + .unwrap(); + + assert!(!result.elements.is_empty()); + } + + #[test] + fn test_parse_relationship_minimal() { + let pair = PlantUmlCommonParser::parse(Rule::relationship, "A --> B") + .unwrap() + .next() + .unwrap(); + + let rel = super::parse_relationship(pair); + + assert_eq!(rel.left, "A"); + assert_eq!(rel.right, "B"); + } + + #[test] + fn test_enum_value_all_cases() { + // literal + let pair = PlantUmlCommonParser::parse(Rule::enum_value, "= 1") + .unwrap() + .next() + .unwrap(); + match super::parse_enum_value(pair) { + EnumValue::Literal(v) => assert_eq!(v, "1"), + _ => panic!(), + } + + // description + let pair = PlantUmlCommonParser::parse(Rule::enum_value, ": ok") + .unwrap() + .next() + .unwrap(); + match super::parse_enum_value(pair) { + EnumValue::Description(v) => assert_eq!(v, "ok"), + _ => panic!(), + } + } +} diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs index fd8d1e6..d8f9830 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs @@ -19,18 +19,3 @@ pub use class_ast::{ Method, Name, Namespace, Package, Param, Relationship, Visibility, }; pub use class_parser::{ClassError, PumlClassParser}; - -/// Parse a PlantUML class diagram and return the parsed structure -/// This is a convenience function for backwards compatibility with tests -pub fn parse_class_diagram(input: &str) -> Result> { - use parser_core::DiagramParser; - use puml_utils::LogLevel; - use std::path::PathBuf; - use std::rc::Rc; - - let mut parser = PumlClassParser; - let dummy_path = Rc::new(PathBuf::from("")); - let document = parser.parse_file(&dummy_path, input, LogLevel::Error)?; - - Ok(document) -} diff --git a/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs b/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs index 6097770..a1d145e 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs @@ -199,3 +199,8 @@ fn test_stereotype_relationship() { fn test_struct() { run_class_diagram_parser_case("struct"); } + +#[test] +fn test_full_features() { + run_class_diagram_parser_case("full_features"); +} diff --git a/plantuml/parser/puml_parser/src/component_diagram/BUILD b/plantuml/parser/puml_parser/src/component_diagram/BUILD index f28042b..fb4f722 100644 --- a/plantuml/parser/puml_parser/src/component_diagram/BUILD +++ b/plantuml/parser/puml_parser/src/component_diagram/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") filegroup( name = "puml_parser_component_files", @@ -42,3 +42,20 @@ rust_library( "@crates//:serde", ], ) + +rust_test( + name = "component_integration_test", + srcs = ["test/component_integration_test.rs"], + args = [ + "--nocapture", + ], + data = ["//plantuml/parser/integration_test/component_diagram:component_integration_test_files"], + proc_macro_deps = [ + ], + deps = [ + ":puml_parser_component", + "//plantuml/parser/integration_test:test_framework", + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + ], +) 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 266e496..29e475b 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 @@ -32,6 +32,8 @@ pub struct PumlComponentParser; // lobster-trace: Tools.ArchitectureModelingComponentHierarchyComponent // lobster-trace: Tools.ArchitectureModelingComponentInteract impl PumlComponentParser { + // Debug-only, excluded to keep coverage focused on parser logic. + #[cfg(not(coverage))] fn format_parse_tree(pairs: pest::iterators::Pairs, indent: usize, output: &mut String) { for pair in pairs { let indent_str = " ".repeat(indent); @@ -107,16 +109,6 @@ impl PumlComponentParser { component.name = Some(Self::extract_interface_name(inner)); component.component_type = "interface".to_string(); } - Rule::default_component => { - let (ctype, name_opt) = Self::parse_default_component(inner)?; - component.component_type = ctype; - component.name = name_opt; - } - Rule::bracket_component => { - let name_opt = Self::parse_bracket_component(inner)?; - component.component_type = "component".to_string(); - component.name = name_opt; - } Rule::alias_clause => { component.alias = Self::extract_alias(inner); } @@ -189,39 +181,29 @@ impl PumlComponentParser { // Helper methods fn extract_component_name(pair: pest::iterators::Pair) -> String { - for inner in pair.into_inner() { - if let Rule::component_old_name = inner.as_rule() { - return inner.as_str().to_string(); - } - } - String::new() + pair.into_inner() + .find(|inner| inner.as_rule() == Rule::component_old_name) + .map(|inner| inner.as_str().to_string()) + .unwrap_or_default() } fn extract_interface_name(pair: pest::iterators::Pair) -> String { - for inner in pair.into_inner() { - if let Rule::interface_old_name = inner.as_rule() { - return inner.as_str().to_string(); - } - } - String::new() + pair.into_inner() + .find(|inner| inner.as_rule() == Rule::interface_old_name) + .map(|inner| inner.as_str().to_string()) + .unwrap_or_default() } fn extract_alias(pair: pest::iterators::Pair) -> Option { - for inner in pair.into_inner() { - if let Rule::ALIAS_ID = inner.as_rule() { - return Some(inner.as_str().to_string()); - } - } - None + pair.into_inner() + .find(|inner| inner.as_rule() == Rule::ALIAS_ID) + .map(|inner| inner.as_str().to_string()) } fn extract_stereotype(pair: pest::iterators::Pair) -> Option { - for inner in pair.into_inner() { - if let Rule::STEREOTYPE_NAME = inner.as_rule() { - return Some(inner.as_str().to_string()); - } - } - None + pair.into_inner() + .find(|inner| inner.as_rule() == Rule::STEREOTYPE_NAME) + .map(|inner| inner.as_str().to_string()) } fn parse_default_component( @@ -288,11 +270,8 @@ impl PumlComponentParser { statements.push(stmt); } } - Rule::EOL => { - // Skip empty lines - } _ => { - // Skip other rules like braces + // Skip empty lines and other rules like braces } } } @@ -316,7 +295,8 @@ impl DiagramParser for PumlComponentParser { let pairs = PlantUmlCommonParser::parse(Rule::component_start, content) .map_err(|e| pest_to_syntax_error(e, path.as_ref().clone(), content))?; - // Show raw parse tree at debug level + // Debug-only, excluded to keep coverage focused on parser logic. + #[cfg(not(coverage))] if matches!(log_level, LogLevel::Debug | LogLevel::Trace) { let mut tree_output = String::new(); @@ -335,25 +315,23 @@ impl DiagramParser for PumlComponentParser { }; for pair in pairs { - if pair.as_rule() == Rule::component_start { - for inner_pair in pair.into_inner() { - match inner_pair.as_rule() { - Rule::startuml => { - for start_inner in inner_pair.into_inner() { - if let Rule::puml_name = start_inner.as_rule() { - document.name = Some(start_inner.as_str().to_string()); - } - } - } - Rule::component_statement => { - if let Ok(stmt) = Self::parse_statement(inner_pair) { - document.statements.push(stmt); - } + for inner_pair in pair.into_inner() { + match inner_pair.as_rule() { + Rule::startuml => { + if let Some(start_inner) = inner_pair + .into_inner() + .find(|p| p.as_rule() == Rule::puml_name) + { + document.name = Some(start_inner.as_str().to_string()); } - Rule::empty_line => { - // Skip empty lines + } + Rule::component_statement => { + if let Ok(stmt) = Self::parse_statement(inner_pair) { + document.statements.push(stmt); } - _ => {} + } + _ => { + // Skip empty lines and other rules like enduml } } } diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs index c11058d..fcda17b 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs @@ -32,21 +32,21 @@ pub enum IncludeStmt { #[derive(Debug, Clone, PartialEq)] pub struct SubBlock { pub name: IncludeSuffix, - pub content: Vec, + pub content: Vec, } #[derive(Debug, Clone, PartialEq)] -pub enum PreprocessStmt { +pub enum IncludeFile { Include(IncludeStmt), Text(String), SubBlock(SubBlock), } -impl PreprocessStmt { +impl IncludeFile { pub fn render(&self, out: &mut String) { match self { - PreprocessStmt::Text(text) => out.push_str(text), - PreprocessStmt::SubBlock(sub) => { + IncludeFile::Text(text) => out.push_str(text), + IncludeFile::SubBlock(sub) => { for stmt in &sub.content { stmt.render(out); } @@ -62,7 +62,7 @@ mod tests { #[test] fn render_text_node_outputs_text() { - let stmt = PreprocessStmt::Text("hello".into()); + let stmt = IncludeFile::Text("hello".into()); let mut out = String::new(); stmt.render(&mut out); assert_eq!(out, "hello"); @@ -73,11 +73,11 @@ mod tests { let sub = SubBlock { name: IncludeSuffix::Label("sub".into()), content: vec![ - PreprocessStmt::Text("a\n".into()), - PreprocessStmt::Text("b\n".into()), + IncludeFile::Text("a\n".into()), + IncludeFile::Text("b\n".into()), ], }; - let stmt = PreprocessStmt::SubBlock(sub); + let stmt = IncludeFile::SubBlock(sub); let mut out = String::new(); stmt.render(&mut out); assert_eq!(out, "a\nb\n"); @@ -89,7 +89,7 @@ mod tests { name: IncludeSuffix::Label("empty".into()), content: vec![], }; - let stmt = PreprocessStmt::SubBlock(sub); + let stmt = IncludeFile::SubBlock(sub); let mut out = String::new(); stmt.render(&mut out); assert_eq!(out, ""); @@ -99,17 +99,17 @@ mod tests { fn render_nested_subblocks() { let inner_sub = SubBlock { name: IncludeSuffix::Label("inner".into()), - content: vec![PreprocessStmt::Text("inner\n".into())], + content: vec![IncludeFile::Text("inner\n".into())], }; let outer_sub = SubBlock { name: IncludeSuffix::Label("outer".into()), content: vec![ - PreprocessStmt::Text("start\n".into()), - PreprocessStmt::SubBlock(inner_sub), - PreprocessStmt::Text("end\n".into()), + IncludeFile::Text("start\n".into()), + IncludeFile::SubBlock(inner_sub), + IncludeFile::Text("end\n".into()), ], }; - let stmt = PreprocessStmt::SubBlock(outer_sub); + let stmt = IncludeFile::SubBlock(outer_sub); let mut out = String::new(); stmt.render(&mut out); assert_eq!(out, "start\ninner\nend\n"); @@ -117,7 +117,7 @@ mod tests { #[test] fn render_include_stmt_does_not_panic() { - let include = PreprocessStmt::Include(IncludeStmt::Include { + let include = IncludeFile::Include(IncludeStmt::Include { kind: IncludeKind::Include, path: "file.puml".into(), }); @@ -128,7 +128,7 @@ mod tests { #[test] fn render_include_sub_stmt_does_not_panic() { - let include_sub = PreprocessStmt::Include(IncludeStmt::IncludeSub { + let include_sub = IncludeFile::Include(IncludeStmt::IncludeSub { path: "file.puml".into(), suffix: IncludeSuffix::Label("sub".into()), }); diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs index a28c990..4445da5 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs @@ -35,7 +35,7 @@ use std::path::PathBuf; use std::rc::Rc; use thiserror::Error; -use crate::include_ast::{IncludeKind, IncludeStmt, IncludeSuffix, PreprocessStmt, SubBlock}; +use crate::include_ast::{IncludeFile, IncludeKind, IncludeStmt, IncludeSuffix, SubBlock}; use crate::include_parser::{IncludeParseError, IncludeParserService}; use crate::utils::{normalize_path, strip_start_end}; @@ -43,7 +43,7 @@ use crate::utils::{normalize_path, strip_start_end}; // Type Aliases // ---------------------- type GlobalSubRegistry = HashMap, FileSubRegistry>; -type AstCache = HashMap, Rc>>; +type AstCache = HashMap, Rc>>; type FileList = HashSet>; /// Stores all subblocks in a file, indexed by label or numeric index. @@ -54,9 +54,9 @@ pub struct FileSubRegistry { } impl FileSubRegistry { - fn collect_from_file(&mut self, stmts: &[PreprocessStmt]) { + fn collect_from_file(&mut self, stmts: &[IncludeFile]) { for stmt in stmts { - if let PreprocessStmt::SubBlock(sub) = stmt { + if let IncludeFile::SubBlock(sub) = stmt { let rc_sub = Rc::new(sub.clone()); match &sub.name { IncludeSuffix::Label(name) => { @@ -243,10 +243,7 @@ impl IncludeExpander { /// /// # Errors /// - `IncludeExpandError::ParseFailed`: read or parse errors - fn load_ast( - &mut self, - file: &Rc, - ) -> Result>, IncludeExpandError> { + fn load_ast(&mut self, file: &Rc) -> Result>, IncludeExpandError> { if let Some(ast) = self.ast_cache.get(file) { return Ok(Rc::clone(ast)); } @@ -279,26 +276,26 @@ impl IncludeExpander { /// - Propagates errors from include expansion fn expand_stmts( &mut self, - stmts: &[PreprocessStmt], + stmts: &[IncludeFile], current_file: &Rc, ctx: &mut IncludeContext, file_list: &FileList, - ) -> Result, IncludeExpandError> { + ) -> Result, IncludeExpandError> { let mut result = Vec::new(); for stmt in stmts { match stmt { - PreprocessStmt::Text(text) => { - result.push(PreprocessStmt::Text(text.clone())); + IncludeFile::Text(text) => { + result.push(IncludeFile::Text(text.clone())); } - PreprocessStmt::Include(inc) => { + IncludeFile::Include(inc) => { let expanded = self.expand_include(inc, current_file, ctx, file_list)?; - result.push(PreprocessStmt::Text(expanded)); + result.push(IncludeFile::Text(expanded)); } - PreprocessStmt::SubBlock(sub) => { + IncludeFile::SubBlock(sub) => { let expanded_content = self.expand_stmts(&sub.content, current_file, ctx, file_list)?; - result.push(PreprocessStmt::SubBlock(SubBlock { + result.push(IncludeFile::SubBlock(SubBlock { name: sub.name.clone(), content: expanded_content, })); @@ -376,7 +373,7 @@ impl IncludeExpander { /// /// # Returns /// - `String` containing PlantUML text -pub fn render_stmts(stmts: Vec) -> String { +pub fn render_stmts(stmts: Vec) -> String { let mut out = String::new(); for stmt in stmts { diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs index d123fbb..9a31f60 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs @@ -16,14 +16,14 @@ use pest_derive::Parser; use std::fs; use std::path::{Path, PathBuf}; -use crate::include_ast::{IncludeKind, IncludeStmt, IncludeSuffix, PreprocessStmt, SubBlock}; +use crate::include_ast::{IncludeFile, IncludeKind, IncludeStmt, IncludeSuffix, SubBlock}; use parser_core::{pest_to_syntax_error, BaseParseError}; #[derive(Parser)] #[grammar = "../../../grammar/include.pest"] pub struct IncludeParser; -const PREPROCESS_KW: &[&str] = &["!include", "!include_once", "!include_many", "!includesub"]; +const INCLUDE_KW: &[&str] = &["!include", "!include_once", "!include_many", "!includesub"]; #[derive(Debug, thiserror::Error)] pub enum IncludeParseError { @@ -39,7 +39,7 @@ pub struct IncludeParserService; impl IncludeParserService { // -------------------- Parser Entry -------------------- - pub fn parse_file(&mut self, file: &Path) -> Result, IncludeParseError> { + pub fn parse_file(&mut self, file: &Path) -> Result, IncludeParseError> { let content = fs::read_to_string(file).map_err(|e| { IncludeParseError::Base(BaseParseError::IoError { path: file.to_path_buf(), @@ -56,27 +56,27 @@ impl IncludeParserService { match line.as_rule() { Rule::include_line => { let include = self.parse_include_line(line); - stmts.push(PreprocessStmt::Include(include)); + stmts.push(IncludeFile::Include(include)); } Rule::includesub_line => { let include_sub = self.parse_includesub_line(line); - stmts.push(PreprocessStmt::Include(include_sub)); + stmts.push(IncludeFile::Include(include_sub)); } Rule::sub_block => { let sub_block = self.parse_sub_block(line); - stmts.push(PreprocessStmt::SubBlock(sub_block)); + stmts.push(IncludeFile::SubBlock(sub_block)); } Rule::text_line => { let text = line.as_str().to_string(); let trimmed = text.trim_start(); - if PREPROCESS_KW.iter().any(|kw| trimmed.starts_with(kw)) { + if INCLUDE_KW.iter().any(|kw| trimmed.starts_with(kw)) { return Err(IncludeParseError::InvalidTextLine { line: text, file: file.to_path_buf(), }); } if !text.trim().is_empty() { - stmts.push(PreprocessStmt::Text(text)); + stmts.push(IncludeFile::Text(text)); } } _ => {} @@ -171,21 +171,21 @@ impl IncludeParserService { let startsub_directive = inner.next().unwrap(); let name = self.extract_suffix(startsub_directive); - let mut content: Vec = Vec::new(); + let mut content: Vec = Vec::new(); for line in inner { match line.as_rule() { Rule::include_line => { let include = self.parse_include_line(line); - content.push(PreprocessStmt::Include(include)); + content.push(IncludeFile::Include(include)); } Rule::includesub_line => { let include_sub = self.parse_includesub_line(line); - content.push(PreprocessStmt::Include(include_sub)); + content.push(IncludeFile::Include(include_sub)); } Rule::text_line => { let text = line.as_str().to_string(); if !text.trim().is_empty() { - content.push(PreprocessStmt::Text(text)); + content.push(IncludeFile::Text(text)); } } _ => {} @@ -256,4 +256,15 @@ mod tests { assert_eq!(path, "path/to/file"); assert!(matches!(suffix, IncludeSuffix::Index(2))); } + + #[test] + fn test_parse_file_io_error() { + let mut service = IncludeParserService; + let missing = PathBuf::from("does-not-exist-include-test.puml"); + let result = service.parse_file(&missing); + assert!(matches!( + result, + Err(IncludeParseError::Base(BaseParseError::IoError { .. })) + )); + } } diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs index a3557e7..b70a8d8 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs @@ -43,6 +43,9 @@ pub enum ProcedureExpandError { actual: usize, }, + #[error("variable not defined: {name}")] + UnknownVariable { name: String }, + #[error("recursive macro detected: {chain:?} -> {name}")] RecursiveMacro { chain: Vec, name: String }, @@ -136,27 +139,12 @@ impl ProcedureExpander { }); } - let proc_opt = self.procedures.get(&call.name); - let proc = if call.name.starts_with('$') { - proc_opt.ok_or_else(|| ProcedureExpandError::MacroNotDefined(call.name.clone()))? - } else if stack.is_empty() { - proc_opt.ok_or_else(|| ProcedureExpandError::MacroNotDefined(call.name.clone()))? - } else if let Some(p) = proc_opt { - p - } else { - // Not found, Keep the original text, including parameters - let args_text = call - .args - .iter() - .map(|arg| match arg { - Arg::Variable(v) => format!("${}", v), - Arg::String(s) => format!("\"{}\"", s), - Arg::Number(n) => n.to_string(), - Arg::Identifier(id) => id.clone(), - }) - .collect::>() - .join(", "); - return Ok(format!("{}({})\n", call.name, args_text)); + let proc = match self.resolve_proc(call, stack)? { + Some(proc) => proc, + None => { + // Not found: keep the call text, but substitute any known $variables in arguments. + return Ok(self.render_unknown_call(call, parent_params)); + } }; if proc.params.len() != call.args.len() { @@ -166,12 +154,81 @@ impl ProcedureExpander { actual: call.args.len(), }); } + stack.push(call.name.clone()); + let mut new_params = self.build_params(proc, call, parent_params)?; + let result = self.expand_body(&proc.body, &mut new_params, stack, depth + 1)?; + stack.pop(); + + Ok(result) + } + + fn resolve_proc( + &self, + call: &MacroCallDef, + stack: &[String], + ) -> Result, ProcedureExpandError> { + let proc_opt = self.procedures.get(&call.name); + if call.name.starts_with('$') { + return proc_opt + .ok_or_else(|| ProcedureExpandError::MacroNotDefined(call.name.clone())) + .map(Some); + } + + if stack.is_empty() { + return proc_opt + .ok_or_else(|| ProcedureExpandError::MacroNotDefined(call.name.clone())) + .map(Some); + } + Ok(proc_opt) + } + + // Literal render for unresolved non-$ calls inside a macro body (stack non-empty). + fn render_unknown_call( + &self, + call: &MacroCallDef, + parent_params: &HashMap, + ) -> String { + let args_text = call + .args + .iter() + .map(|arg| match arg { + Arg::Variable(v) => parent_params + .get(v) + .cloned() + .unwrap_or_else(|| v.to_string()), + Arg::String(s) => { + if s.starts_with('$') { + parent_params + .get(s) + .map(|value| format!("\"{}\"", value)) + .unwrap_or_else(|| format!("\"{}\"", s)) + } else { + format!("\"{}\"", s) + } + } + Arg::Number(n) => n.to_string(), + Arg::Identifier(id) => id.clone(), + }) + .collect::>() + .join(", "); + format!("{}({})\n", call.name, args_text) + } + + fn build_params( + &self, + proc: &ProcedureDef, + call: &MacroCallDef, + parent_params: &HashMap, + ) -> Result, ProcedureExpandError> { let mut new_params = HashMap::new(); for (param, arg) in proc.params.iter().zip(&call.args) { let value = match arg { - Arg::Variable(v) => parent_params.get(v).cloned().unwrap_or(v.clone()), + Arg::Variable(v) => parent_params + .get(v) + .cloned() + .ok_or_else(|| ProcedureExpandError::UnknownVariable { name: v.clone() })?, Arg::String(s) => s.clone(), Arg::Number(n) => n.to_string(), Arg::Identifier(id) => id.clone(), @@ -179,10 +236,7 @@ impl ProcedureExpander { new_params.insert(param.clone(), value); } - let result = self.expand_body(&proc.body, &mut new_params, stack, depth + 1)?; - stack.pop(); - - Ok(result) + Ok(new_params) } fn expand_body( diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs index c8ea71f..2964524 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs @@ -156,14 +156,27 @@ fn parse_text_line(pair: Pair) -> Vec { let mut current = String::new(); let mut chars = text.chars().peekable(); + // Walk the raw text to split it into literal spans and $variable tokens. while let Some(c) = chars.next() { if c == '$' { + // Treat '$' as literal when it appears inside an identifier (e.g., "123$val"). + let prev_is_ident = current + .chars() + .last() + .is_some_and(|ch| ch.is_alphanumeric() || ch == '_'); + if prev_is_ident { + current.push(c); + continue; + } + if !current.is_empty() { + // Flush the accumulated literal text before starting a $variable token. parts.push(TextPart::Literal(current.clone())); current.clear(); } let mut var = String::from("$"); + // Collect the variable name following '$' to build the full $variable token. while let Some(&ch) = chars.peek() { if ch.is_alphanumeric() || ch == '_' { var.push(ch); diff --git a/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs b/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs index 06055f5..5d7a33f 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs @@ -62,6 +62,11 @@ fn test_includesub_with_serveral_subblock() { run_include_preprocess_case("several_subblock"); } +#[test] +fn test_includesub_in_subblock() { + run_include_preprocess_case("includesub_in_subblock"); +} + #[test] fn test_includesub_with_invalid_suffix() { run_include_preprocess_case("invalid_suffix_for_includesub"); @@ -73,8 +78,13 @@ fn test_invalid_nested_subblock() { } #[test] -fn test_invalid_include_unknow_sub() { - run_include_preprocess_case("invalid_include_unknow_sub"); +fn test_invalid_include_label_sub() { + run_include_preprocess_case("invalid_include_label_sub"); +} + +#[test] +fn test_invalid_include_number_sub() { + run_include_preprocess_case("invalid_include_number_sub"); } // --------- test for include_once --------- diff --git a/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs b/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs index 85e432c..2422335 100644 --- a/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs +++ b/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs @@ -66,6 +66,16 @@ fn test_recursive_macro() { run_procedure_preprocess_case("recursive_macro"); } +#[test] +fn test_macro_nested_args() { + run_procedure_preprocess_case("macro_nested_args"); +} + +#[test] +fn test_max_depth_exceeded() { + run_procedure_preprocess_case("max_depth_exceeded"); +} + #[test] fn test_noise_item() { run_procedure_preprocess_case("noise_item"); @@ -75,3 +85,13 @@ fn test_noise_item() { fn test_empty() { run_procedure_preprocess_case("empty"); } + +#[test] +fn test_unknown_var_in_args() { + run_procedure_preprocess_case("unknown_var_in_args"); +} + +#[test] +fn test_unknown_variable() { + run_procedure_preprocess_case("unknown_variable"); +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/full_features/full_features.puml b/plantuml/parser/puml_parser/tests/class_diagram/full_features/full_features.puml new file mode 100644 index 0000000..d8e1eb3 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/full_features/full_features.puml @@ -0,0 +1,43 @@ +' ******************************************************************************* +' 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 full_features + +package "pkg1" { + class "AInternal" as A { + +id: int + -name: String + #flag: bool + ~pkgField: float + + +foo(a: int, b: String...): bool + } + + struct B { + +value: int + } + + interface I { + +bar(): void + } + + enum E { + +A = 1 + B : desc + C + } + + A --> B : uses + B --> I +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/full_features/output.json b/plantuml/parser/puml_parser/tests/class_diagram/full_features/output.json new file mode 100644 index 0000000..f75b9b6 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/full_features/output.json @@ -0,0 +1,173 @@ +{ + "full_features.puml": { + "name": "full_features", + "elements": [ + { + "Package": { + "name": { + "internal": "pkg1", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "AInternal", + "display": "A" + }, + "namespace": "", + "package": "pkg1", + "attributes": [ + { + "visibility": "Public", + "name": "id", + "type": "int" + }, + { + "visibility": "Private", + "name": "name", + "type": "String" + }, + { + "visibility": "Protected", + "name": "flag", + "type": "bool" + }, + { + "visibility": "Package", + "name": "pkgField", + "type": "float" + } + ], + "methods": [ + { + "visibility": "Public", + "name": "foo", + "generic_params": ["T", "U"], + "params": [ + { + "name": "a", + "param_type": "int", + "varargs": false + }, + { + "name": "b", + "param_type": "String", + "varargs": true + } + ], + "type": "bool" + } + ] + } + }, + { + "StructDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "", + "package": "pkg1", + "attributes": [ + { + "visibility": "Public", + "name": "value", + "type": "int" + } + ], + "methods": [] + } + }, + { + "InterfaceDef": { + "name": { + "internal": "I", + "display": null + }, + "namespace": "", + "package": "pkg1", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "bar", + "generic_params": [], + "params": [], + "type": "void" + } + ] + } + }, + { + "EnumDef": { + "name": { + "internal": "E", + "display": null + }, + "namespace": "", + "package": "pkg1", + "stereotypes": [], + "items": [ + { + "visibility": "Public", + "name": "A", + "value": { + "Literal": "1" + } + }, + { + "visibility": null, + "name": "B", + "value": { + "Description": "desc" + } + }, + { + "visibility": null, + "name": "C", + "value": null + } + ] + } + } + ], + "relationships": [ + { + "left": "A", + "right": "B", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": "uses" + }, + { + "left": "B", + "right": "I", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + } + ], + "packages": [] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.puml b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.puml index 38777c8..eac9ae7 100644 --- a/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.puml +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.puml @@ -15,6 +15,11 @@ namespace Core { class A class B + + enum C { + a + b + } } @enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json index ac4c543..d73bf45 100644 --- a/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json @@ -32,6 +32,29 @@ "attributes": [], "methods": [] } + }, + { + "EnumDef": { + "name": { + "internal": "C", + "display": null + }, + "namespace": "Core", + "package": "", + "stereotypes": [], + "items": [ + { + "visibility": null, + "name": "a", + "value": null + }, + { + "visibility": null, + "name": "b", + "value": null + } + ] + } } ], "namespaces": [] diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/common.puml new file mode 100644 index 0000000..f5a4fba --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/common.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 common + +!startsub Block +!includesub sub.puml!Sub +class Common { + +id: int +} +!endsub + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/output.yaml new file mode 100644 index 0000000..b2ca38e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/output.yaml @@ -0,0 +1,37 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +common.puml: | + @startuml common + class SubThing { + +name: String + } + class Common { + +id: int + } + @enduml +sub.puml: | + @startuml sub + class SubThing { + +name: String + } + @enduml +user.puml: | + @startuml user + class SubThing { + +name: String + } + class Common { + +id: int + } + class User {} + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/sub.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/sub.puml new file mode 100644 index 0000000..5e53961 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/sub.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' 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 sub + +!startsub Sub +class SubThing { + +name: String +} +!endsub + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/user.puml new file mode 100644 index 0000000..516b61c --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/includesub_in_subblock/user.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 user + +!includesub common.puml!Block +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/common.puml similarity index 100% rename from plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/common.puml rename to plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/common.puml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/output.yaml similarity index 100% rename from plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/output.yaml rename to plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/output.yaml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/user.puml similarity index 100% rename from plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/user.puml rename to plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_label_sub/user.puml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/common.puml new file mode 100644 index 0000000..7f0e116 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/common.puml @@ -0,0 +1,19 @@ +' ******************************************************************************* +' 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 common + +class Common { + +id: int +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/output.yaml new file mode 100644 index 0000000..0d0f3c1 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/output.yaml @@ -0,0 +1,18 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +user.puml: + error: + type: "UnknownSub" + path: "user.puml" + suffix: "1" + file: "common.puml" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/user.puml new file mode 100644 index 0000000..6edb7ee --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_number_sub/user.puml @@ -0,0 +1,18 @@ +' ******************************************************************************* +' 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 + +!includesub common.puml!1 +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.puml index 0b0d385..d8d5b13 100644 --- a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.puml +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.puml @@ -19,6 +19,11 @@ class Common { } !endsub +!startsub 1 +class A { +} +!endsub + class Student { +name: String } diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml index db5f85b..de6b8c5 100644 --- a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml @@ -16,6 +16,8 @@ common.puml: | +id: int +login() } + class A { + } class Student { +name: String } @@ -27,4 +29,6 @@ user.puml: | +login() } class User {} + class A { + } @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/user.puml index 59205dc..3b2968e 100644 --- a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/user.puml +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/user.puml @@ -14,5 +14,6 @@ !includesub common.puml!CommonClass class User {} +!includesub common.puml!1 @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml index 582acdb..3aea335 100644 --- a/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml @@ -18,8 +18,8 @@ !procedure init_class($name) class $name { - $addCommonMethod() -} + $addCommonMethod() + } !endprocedure !procedure $addCommonMethod() diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/macro_nested_args.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/macro_nested_args.puml new file mode 100644 index 0000000..5d63537 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/macro_nested_args.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 macro_nested_args + +!procedure $Inner($num, $id) + rectangle "$num-$id" as $id +!endprocedure + +!procedure $Outer($val) + class test { + UnknownMacro($val, Bar) + $Inner(123, Foo) + } +!endprocedure + +$Outer("Hello") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/output.yaml new file mode 100644 index 0000000..be5469f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_nested_args/output.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +macro_nested_args.puml: | + @startuml macro_nested_args + class test { + UnknownMacro(Hello, Bar) + rectangle "123-Foo" as Foo + } + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/max_depth_exceeded.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/max_depth_exceeded.puml new file mode 100644 index 0000000..9e9707b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/max_depth_exceeded.puml @@ -0,0 +1,54 @@ +' ******************************************************************************* +' 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 max_depth_exceeded + +!procedure $M1() + $M2() +!endprocedure +!procedure $M2() + $M3() +!endprocedure +!procedure $M3() + $M4() +!endprocedure +!procedure $M4() + $M5() +!endprocedure +!procedure $M5() + $M6() +!endprocedure +!procedure $M6() + $M7() +!endprocedure +!procedure $M7() + $M8() +!endprocedure +!procedure $M8() + $M9() +!endprocedure +!procedure $M9() + $M10() +!endprocedure +!procedure $M10() + $M11() +!endprocedure +!procedure $M11() + $M12() +!endprocedure +!procedure $M12() + rectangle "Done" as Done +!endprocedure + +$M1() + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/output.yaml new file mode 100644 index 0000000..475d29a --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/max_depth_exceeded/output.yaml @@ -0,0 +1,15 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +max_depth_exceeded.puml: + error: + type: "MaxDepthExceeded" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.puml index 64103d9..7be7980 100644 --- a/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.puml +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.puml @@ -21,6 +21,10 @@ $alias -u-> $connection !endprocedure -BasicEvent("No More Cookies", "SampleLibrary.NoMoreCookies", "AG2") +!procedure $Outer() + BasicEvent("No More Cookies", "SampleLibrary.NoMoreCookies", "AG2") +!endprocedure + +$Outer() @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/output.yaml new file mode 100644 index 0000000..14e84d3 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/output.yaml @@ -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 +# ******************************************************************************* +unknown_var_in_args.puml: | + @startuml + class test { + UnknownMacro(Hello) + UnknownMacro(qwert$val) + UnknownMacro($valqwert) + UnknownMacro("Hello") + UnknownMacro("qwert$val") + UnknownMacro("$valqwert") + } + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/unknown_var_in_args.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/unknown_var_in_args.puml new file mode 100644 index 0000000..962f0b1 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_var_in_args/unknown_var_in_args.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 + +!procedure $Outer($val) + class test { + UnknownMacro($val) + UnknownMacro(qwert$val) + UnknownMacro($valqwert) + UnknownMacro("$val") + UnknownMacro("qwert$val") + UnknownMacro("$valqwert") + } +!endprocedure + +$Outer("Hello") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/output.yaml new file mode 100644 index 0000000..4c7b7d3 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/output.yaml @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +unknown_variable.puml: + error: + type: "UnknownVariable" + fields: + name: "$val123" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/unknown_variable.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/unknown_variable.puml new file mode 100644 index 0000000..527aaea --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/unknown_variable/unknown_variable.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' 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 unknown_variable + +!procedure $Outer($val) + $Inner($val123) +!endprocedure + +!procedure $Inner($val) + class $val { + } +!endprocedure + +$Outer("World") + +@enduml diff --git a/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs b/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs index f429504..c10f9b2 100644 --- a/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs +++ b/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs @@ -471,14 +471,12 @@ impl ClassResolver { impl DiagramResolver for ClassResolver { type Document = ClassUmlFile; type Statement = (); - type Output = HashMap; + type Output = ClassDiagram; type Error = ClassResolverError; fn visit_document(&mut self, document: &Self::Document) -> Result { self.name_map.clear(); - let mut result = HashMap::new(); - self.logic.name = document.name.clone(); self.logic.source_files.push(document.name.clone()); @@ -496,9 +494,7 @@ impl DiagramResolver for ClassResolver { }, ); - result.insert(logic_class.name.clone(), logic_class); - - Ok(result) + Ok(logic_class) } } @@ -629,33 +625,89 @@ mod tests { // convert_arrow // ---------------------------- #[test] - fn test_arrow_inheritance_reversed() { + fn test_convert_arrow_cases() { let resolver = ClassResolver::new(); - let arrow = make_arrow(Some("<|"), "--", None); - let (ty, reversed) = resolver.convert_arrow(&arrow).unwrap(); - assert_eq!(ty, RelationType::Inheritance); - assert!(reversed); - } + struct Case { + arrow: Arrow, + expected_ty: RelationType, + expected_reversed: bool, + } - #[test] - fn test_arrow_inheritance_normal() { - let resolver = ClassResolver::new(); - let arrow = make_arrow(None, "--", Some("|>")); + let cases = vec![ + Case { + arrow: make_arrow(Some("<|"), "--", None), + expected_ty: RelationType::Inheritance, + expected_reversed: true, + }, + Case { + arrow: make_arrow(None, "--", Some("|>")), + expected_ty: RelationType::Inheritance, + expected_reversed: false, + }, + Case { + arrow: make_arrow(None, "--", Some(">")), + expected_ty: RelationType::Association, + expected_reversed: false, + }, + Case { + arrow: make_arrow(None, "..", Some("|>")), + expected_ty: RelationType::Implementation, + expected_reversed: false, + }, + Case { + arrow: make_arrow(None, "--", Some("*")), + expected_ty: RelationType::Composition, + expected_reversed: false, + }, + Case { + arrow: make_arrow(None, "--", Some("o")), + expected_ty: RelationType::Aggregation, + expected_reversed: false, + }, + Case { + arrow: make_arrow(Some("<"), "--", None), + expected_ty: RelationType::Association, + expected_reversed: true, + }, + Case { + arrow: make_arrow(Some("<"), "..", None), + expected_ty: RelationType::Dependency, + expected_reversed: true, + }, + Case { + arrow: make_arrow(None, "--", None), + expected_ty: RelationType::Link, + expected_reversed: false, + }, + Case { + arrow: make_arrow(None, "..", None), + expected_ty: RelationType::DashedLink, + expected_reversed: false, + }, + ]; - let (ty, reversed) = resolver.convert_arrow(&arrow).unwrap(); - assert_eq!(ty, RelationType::Inheritance); - assert!(!reversed); + for (i, case) in cases.into_iter().enumerate() { + let (ty, reversed) = resolver.convert_arrow(&case.arrow).unwrap(); + + assert_eq!(ty, case.expected_ty, "case {} failed: type mismatch", i); + assert_eq!( + reversed, case.expected_reversed, + "case {} failed: reversed mismatch", + i + ); + } } #[test] - fn test_arrow_association() { + fn test_convert_arrow_invalid() { let resolver = ClassResolver::new(); - let arrow = make_arrow(None, "--", Some(">")); - let (ty, reversed) = resolver.convert_arrow(&arrow).unwrap(); - assert_eq!(ty, RelationType::Association); - assert!(!reversed); + let arrow = make_arrow(Some("?"), "~~", Some("?")); + + let result = resolver.convert_arrow(&arrow); + + assert!(result.is_err()); } // ---------------------------- @@ -687,6 +739,25 @@ mod tests { assert_eq!(r.stereotype, Some("label".to_string())); } + #[test] + fn test_process_relationship_unresolved_left() { + let mut resolver = ClassResolver::new(); + + let rel = Relationship { + left: "UnknownA".to_string(), + right: "KnownB".to_string(), + arrow: make_arrow(None, "--", Some(">")), + label: None, + }; + + let result = resolver.process_relationship(&rel, None); + + assert!(matches!( + result, + Err(ClassResolverError::UnresolvedReference { ref reference }) if reference == "UnknownA" + )); + } + // ---------------------------- // namespace // ---------------------------- @@ -722,11 +793,35 @@ mod tests { relationships: vec![], }; - let result = resolver.visit_document(&file).unwrap(); - assert!(result.contains_key("test")); - - let logic = result.get("test").unwrap(); + let logic = resolver.visit_document(&file).unwrap(); + assert_eq!(logic.name, "test"); assert_eq!(logic.entities.len(), 1); assert_eq!(logic.entities[0].id, "User"); } + + // ---------------------------- + // top_level + // ---------------------------- + #[test] + fn test_process_top_level_enum_and_namespace() { + let cases = vec![ + ClassUmlTopLevel::Enum(EnumDef { + name: make_name("MyEnum"), + namespace: "".to_string(), + package: "".to_string(), + items: vec![], + stereotypes: vec![], + }), + ClassUmlTopLevel::Namespace(Namespace { + name: make_name("ns"), + types: vec![], + namespaces: vec![], + }), + ]; + + for case in cases { + let mut resolver = ClassResolver::new(); + assert!(resolver.process_top_level(&case, None).is_ok()); + } + } } 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 25ed4b9..2904f07 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 @@ -20,17 +20,12 @@ use crate::component_logic::{ use component_parser::{CompPumlDocument, Component, Statement}; use resolver_traits::DiagramResolver; +#[derive(Default)] pub struct ComponentResolver { pub scope: Vec, // component id stack pub components: HashMap, // FQN -> LogicComponent } -impl Default for ComponentResolver { - fn default() -> Self { - Self::new() - } -} - impl ComponentResolver { pub fn new() -> Self { Self { 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 026b8ef..a28a33a 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 @@ -73,3 +73,23 @@ fn test_relation_fqn() { fn test_relation_relative_name() { run_component_resolver_case("relation_relative_name"); } + +#[test] +fn test_relation_simple_name_alias() { + run_component_resolver_case("relation_simple_name_alias"); +} + +#[test] +fn test_relation_absolute_fqn() { + run_component_resolver_case("relation_absolute_fqn"); +} + +#[test] +fn test_invalid_unresolved_reference() { + run_component_resolver_case("invalid_unresolved_reference"); +} + +#[test] +fn test_invalid_duplicate_component() { + run_component_resolver_case("invalid_duplicate_component"); +} diff --git a/plantuml/parser/puml_serializer/BUILD b/plantuml/parser/puml_serializer/BUILD index 6f06633..4d185d5 100644 --- a/plantuml/parser/puml_serializer/BUILD +++ b/plantuml/parser/puml_serializer/BUILD @@ -17,6 +17,7 @@ rust_library( srcs = ["src/lib.rs"], visibility = ["//plantuml/parser:__subpackages__"], deps = [ + ":class_serializer", ":component_serializer", ], ) @@ -26,3 +27,9 @@ alias( actual = "//plantuml/parser/puml_serializer/src/serialize:puml_serialize_component", visibility = ["//plantuml/parser:__subpackages__"], ) + +alias( + name = "class_serializer", + actual = "//plantuml/parser/puml_serializer/src/serialize:puml_serialize_class", + visibility = ["//plantuml/parser:__subpackages__"], +) diff --git a/plantuml/parser/puml_serializer/src/fbs/BUILD b/plantuml/parser/puml_serializer/src/fbs/BUILD index 264785c..f9d2b37 100644 --- a/plantuml/parser/puml_serializer/src/fbs/BUILD +++ b/plantuml/parser/puml_serializer/src/fbs/BUILD @@ -46,3 +46,35 @@ rust_library( "@crates//:flatbuffers", ], ) + +flatbuffer_library_public( + name = "class_fbs_codegen", + srcs = [ + "//tools/metamodel:schemas", + ], + outs = [ + "class_diagram_generated.rs", + ], + flatc_args = [], + language_flag = "--rust", + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "class_fbs", + srcs = [ + ":class_fbs_codegen", + ], + rustc_flags = [ + "--allow=unused_imports", + "--allow=clippy::extra-unused-lifetimes", + "--allow=clippy::missing-safety-doc", + "--allow=clippy::needless-lifetimes", + ], + visibility = [ + "//plantuml/parser:__subpackages__", + ], + deps = [ + "@crates//:flatbuffers", + ], +) diff --git a/plantuml/parser/puml_serializer/src/lib.rs b/plantuml/parser/puml_serializer/src/lib.rs index 8f31add..e61f58a 100644 --- a/plantuml/parser/puml_serializer/src/lib.rs +++ b/plantuml/parser/puml_serializer/src/lib.rs @@ -11,4 +11,5 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +pub use class_serializer::ClassSerializer; pub use component_serializer::ComponentSerializer; diff --git a/plantuml/parser/puml_serializer/src/serialize/BUILD b/plantuml/parser/puml_serializer/src/serialize/BUILD index 7fd8f06..af91a9d 100644 --- a/plantuml/parser/puml_serializer/src/serialize/BUILD +++ b/plantuml/parser/puml_serializer/src/serialize/BUILD @@ -26,3 +26,18 @@ rust_library( "@crates//:flatbuffers", ], ) + +rust_library( + name = "puml_serialize_class", + srcs = [ + "class_serializer.rs", + ], + crate_name = "class_serializer", + crate_root = "class_serializer.rs", + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_serializer/src/fbs:class_fbs", + "//tools/metamodel:class_diagram", + "@crates//:flatbuffers", + ], +) diff --git a/plantuml/parser/puml_serializer/src/serialize/class_serializer.rs b/plantuml/parser/puml_serializer/src/serialize/class_serializer.rs new file mode 100644 index 0000000..6b08337 --- /dev/null +++ b/plantuml/parser/puml_serializer/src/serialize/class_serializer.rs @@ -0,0 +1,386 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use class_diagram::{ + ClassDiagram, ContainerType, EntityType, LogicAttribute, LogicContainer, LogicEntity, + LogicEnumLiteral, LogicMethod, LogicParameter, LogicRelationship, RelationType, Visibility, +}; +use class_fbs::class_metamodel as fb; +use flatbuffers::FlatBufferBuilder; + +const UNKNOWN_SOURCE_LINE: u32 = 0; + +pub struct ClassSerializer; + +impl ClassSerializer { + pub fn serialize(diagram: &ClassDiagram, _source_file: &str) -> Vec { + let mut builder = FlatBufferBuilder::new(); + + let name_offset = builder.create_string(&diagram.name); + + let entity_offsets: Vec<_> = diagram + .entities + .iter() + .map(|entity| Self::serialize_entity(&mut builder, entity)) + .collect(); + let entities_offset = builder.create_vector(&entity_offsets); + + let container_offsets: Vec<_> = diagram + .containers + .iter() + .map(|container| Self::serialize_container(&mut builder, container)) + .collect(); + let containers_offset = builder.create_vector(&container_offsets); + + let relationship_offsets: Vec<_> = diagram + .relationships + .iter() + .map(|relationship| Self::serialize_relationship(&mut builder, relationship)) + .collect(); + let relationships_offset = builder.create_vector(&relationship_offsets); + + let source_offsets: Vec<_> = diagram + .source_files + .iter() + .map(|source| builder.create_string(source)) + .collect(); + let source_files_offset = builder.create_vector(&source_offsets); + + let version_offset = diagram.version.as_ref().map(|v| builder.create_string(v)); + + let root = fb::ClassDiagram::create( + &mut builder, + &fb::ClassDiagramArgs { + name: Some(name_offset), + entities: Some(entities_offset), + containers: Some(containers_offset), + relationships: Some(relationships_offset), + source_files: Some(source_files_offset), + version: version_offset, + }, + ); + + builder.finish(root, Some("CLSD")); + builder.finished_data().to_vec() + } + + fn serialize_entity<'a>( + builder: &mut FlatBufferBuilder<'a>, + entity: &LogicEntity, + ) -> flatbuffers::WIPOffset> { + let id_offset = builder.create_string(&entity.id); + let name_offset = entity.name.as_ref().map(|name| builder.create_string(name)); + let alias_offset = entity + .alias + .as_ref() + .map(|alias| builder.create_string(alias)); + let parent_offset = entity + .parent_id + .as_ref() + .map(|parent| builder.create_string(parent)); + + let stereotype_offsets: Vec<_> = entity + .stereotypes + .iter() + .map(|st| builder.create_string(st)) + .collect(); + let stereotypes_offset = builder.create_vector(&stereotype_offsets); + + let attribute_offsets: Vec<_> = entity + .attributes + .iter() + .map(|attr| Self::serialize_attribute(builder, attr)) + .collect(); + let attributes_offset = builder.create_vector(&attribute_offsets); + + let method_offsets: Vec<_> = entity + .methods + .iter() + .map(|method| Self::serialize_method(builder, method)) + .collect(); + let methods_offset = builder.create_vector(&method_offsets); + + let template_offsets: Vec<_> = entity + .template_params + .iter() + .map(|param| builder.create_string(param)) + .collect(); + let template_params_offset = builder.create_vector(&template_offsets); + + let enum_literal_offsets: Vec<_> = entity + .enum_literals + .iter() + .map(|literal| Self::serialize_enum_literal(builder, literal)) + .collect(); + let enum_literals_offset = builder.create_vector(&enum_literal_offsets); + + let source_file_offset = entity + .source_file + .as_ref() + .map(|source| builder.create_string(source)); + + fb::Entity::create( + builder, + &fb::EntityArgs { + id: Some(id_offset), + name: name_offset, + alias: alias_offset, + parent_id: parent_offset, + entity_type: Self::map_entity_type(entity.entity_type), + stereotypes: Some(stereotypes_offset), + attributes: Some(attributes_offset), + methods: Some(methods_offset), + template_params: Some(template_params_offset), + enum_literals: Some(enum_literals_offset), + source_file: source_file_offset, + source_line: entity.source_line.unwrap_or(UNKNOWN_SOURCE_LINE), + }, + ) + } + + fn serialize_attribute<'a>( + builder: &mut FlatBufferBuilder<'a>, + attr: &LogicAttribute, + ) -> flatbuffers::WIPOffset> { + let name_offset = builder.create_string(&attr.name); + let data_type_offset = attr + .data_type + .as_ref() + .map(|data_type| builder.create_string(data_type)); + let default_value_offset = attr + .default_value + .as_ref() + .map(|value| builder.create_string(value)); + let description_offset = attr + .description + .as_ref() + .map(|description| builder.create_string(description)); + + fb::Attribute::create( + builder, + &fb::AttributeArgs { + name: Some(name_offset), + data_type: data_type_offset, + visibility: Self::map_visibility(attr.visibility), + default_value: default_value_offset, + is_static: attr.is_static, + is_const: attr.is_const, + description: description_offset, + }, + ) + } + + fn serialize_method<'a>( + builder: &mut FlatBufferBuilder<'a>, + method: &LogicMethod, + ) -> flatbuffers::WIPOffset> { + let name_offset = builder.create_string(&method.name); + let return_type_offset = method + .return_type + .as_ref() + .map(|return_type| builder.create_string(return_type)); + + let parameter_offsets: Vec<_> = method + .parameters + .iter() + .map(|param| Self::serialize_parameter(builder, param)) + .collect(); + let parameters_offset = builder.create_vector(¶meter_offsets); + + let template_offsets: Vec<_> = method + .template_params + .iter() + .map(|param| builder.create_string(param)) + .collect(); + let template_params_offset = builder.create_vector(&template_offsets); + + fb::Method::create( + builder, + &fb::MethodArgs { + name: Some(name_offset), + return_type: return_type_offset, + visibility: Self::map_visibility(method.visibility), + parameters: Some(parameters_offset), + template_params: Some(template_params_offset), + is_static: method.is_static, + is_const: method.is_const, + is_virtual: method.is_virtual, + is_abstract: method.is_abstract, + is_override: method.is_override, + is_constructor: method.is_constructor, + is_destructor: method.is_destructor, + }, + ) + } + + fn serialize_parameter<'a>( + builder: &mut FlatBufferBuilder<'a>, + param: &LogicParameter, + ) -> flatbuffers::WIPOffset> { + let name_offset = builder.create_string(¶m.name); + let param_type_offset = param + .param_type + .as_ref() + .map(|param_type| builder.create_string(param_type)); + let default_value_offset = param + .default_value + .as_ref() + .map(|value| builder.create_string(value)); + + fb::Parameter::create( + builder, + &fb::ParameterArgs { + name: Some(name_offset), + param_type: param_type_offset, + default_value: default_value_offset, + is_reference: param.is_reference, + is_const: param.is_const, + is_variadic: param.is_variadic, + }, + ) + } + + fn serialize_enum_literal<'a>( + builder: &mut FlatBufferBuilder<'a>, + literal: &LogicEnumLiteral, + ) -> flatbuffers::WIPOffset> { + let name_offset = builder.create_string(&literal.name); + let value_offset = literal + .value + .as_ref() + .map(|value| builder.create_string(value)); + let description_offset = literal + .description + .as_ref() + .map(|description| builder.create_string(description)); + + fb::EnumLiteral::create( + builder, + &fb::EnumLiteralArgs { + name: Some(name_offset), + visibility: Self::map_visibility(literal.visibility), + value: value_offset, + description: description_offset, + }, + ) + } + + fn serialize_container<'a>( + builder: &mut FlatBufferBuilder<'a>, + container: &LogicContainer, + ) -> flatbuffers::WIPOffset> { + let id_offset = builder.create_string(&container.id); + let name_offset = builder.create_string(&container.name); + let parent_offset = container + .parent_id + .as_ref() + .map(|parent| builder.create_string(parent)); + + fb::Container::create( + builder, + &fb::ContainerArgs { + id: Some(id_offset), + name: Some(name_offset), + parent_id: parent_offset, + container_type: Self::map_container_type(container.container_type), + }, + ) + } + + fn serialize_relationship<'a>( + builder: &mut FlatBufferBuilder<'a>, + relationship: &LogicRelationship, + ) -> flatbuffers::WIPOffset> { + let source_offset = builder.create_string(&relationship.source); + let target_offset = builder.create_string(&relationship.target); + let label_offset = relationship + .label + .as_ref() + .map(|label| builder.create_string(label)); + let stereotype_offset = relationship + .stereotype + .as_ref() + .map(|stereotype| builder.create_string(stereotype)); + let source_multiplicity_offset = relationship + .source_multiplicity + .as_ref() + .map(|multiplicity| builder.create_string(multiplicity)); + let target_multiplicity_offset = relationship + .target_multiplicity + .as_ref() + .map(|multiplicity| builder.create_string(multiplicity)); + let source_role_offset = relationship + .source_role + .as_ref() + .map(|role| builder.create_string(role)); + let target_role_offset = relationship + .target_role + .as_ref() + .map(|role| builder.create_string(role)); + + fb::Relationship::create( + builder, + &fb::RelationshipArgs { + source: Some(source_offset), + target: Some(target_offset), + relation_type: Self::map_relation_type(relationship.relation_type), + label: label_offset, + stereotype: stereotype_offset, + source_multiplicity: source_multiplicity_offset, + target_multiplicity: target_multiplicity_offset, + source_role: source_role_offset, + target_role: target_role_offset, + }, + ) + } + + fn map_visibility(v: Visibility) -> fb::Visibility { + match v { + Visibility::Public => fb::Visibility::Public, + Visibility::Private => fb::Visibility::Private, + Visibility::Protected => fb::Visibility::Protected, + Visibility::Package => fb::Visibility::Package, + } + } + + fn map_entity_type(t: EntityType) -> fb::EntityType { + match t { + EntityType::Class => fb::EntityType::Class, + EntityType::Struct => fb::EntityType::Struct, + EntityType::Interface => fb::EntityType::Interface, + EntityType::Enum => fb::EntityType::Enum, + EntityType::AbstractClass => fb::EntityType::AbstractClass, + EntityType::Annotation => fb::EntityType::Annotation, + } + } + + fn map_container_type(t: ContainerType) -> fb::ContainerType { + match t { + ContainerType::Namespace => fb::ContainerType::Namespace, + ContainerType::Package => fb::ContainerType::Package, + } + } + + fn map_relation_type(t: RelationType) -> fb::RelationType { + match t { + RelationType::Inheritance => fb::RelationType::Inheritance, + RelationType::Implementation => fb::RelationType::Implementation, + RelationType::Composition => fb::RelationType::Composition, + RelationType::Aggregation => fb::RelationType::Aggregation, + RelationType::Association => fb::RelationType::Association, + RelationType::Dependency => fb::RelationType::Dependency, + RelationType::Link => fb::RelationType::Link, + RelationType::DashedLink => fb::RelationType::DashedLink, + } + } +}