From 1cc6ab1e161534453dd5a31a11da409054039dc8 Mon Sep 17 00:00:00 2001 From: Jochen Hoenle Date: Tue, 17 Mar 2026 16:09:19 +0100 Subject: [PATCH] parser for checking plantuml diagrams This PR contains a parser to parse the following plantuml diagrams - component diagram - class diagram - sequence diagram. The resulting represenation is suitable for further checking of the content against requirements and implementation --- .github/CODEOWNERS | 20 + BUILD | 1 + MODULE.bazel | 63 + REUSE.toml | 10 + plantuml/parser/BUILD | 17 + plantuml/parser/README.md | 89 + plantuml/parser/docs/README.md | 386 +++ .../parser/docs/assets/preprocessor_seq.puml | 111 + plantuml/parser/docs/assets/puml_parser.puml | 97 + plantuml/parser/docs/assets/puml_parser.svg | 1 + .../parser/docs/assets/puml_parser_class.puml | 91 + .../parser/docs/assets/puml_parser_class.svg | 1 + .../docs/assets/puml_parser_deployment.puml | 58 + .../docs/assets/puml_parser_deployment.svg | 1 + plantuml/parser/docs/tool_requirements.trlc | 94 + plantuml/parser/integration_test/BUILD | 33 + .../integration_test/class_diagram/BUILD | 21 + .../class_diagram_negative.puml | 30 + .../class_diagram_negative/output.yaml | 17 + .../class_diagram_positive.puml | 128 + .../class_diagram_positive/output.json | 624 +++++ .../integration_test/component_diagram/BUILD | 21 + .../component_diagram/n_pkg_comp.puml | 19 + .../relation_fqn/output.json | 56 + .../relation_fqn/relation_fqn.puml | 28 + .../relation_relative_name/output.json | 55 + .../relation_relative_name.puml | 28 + .../relation_simple_name/output.json | 55 + .../relation_simple_name.puml | 28 + .../component_diagram/simple_component.puml | 28 + .../integration_test/sequence_diagram/BUILD | 35 + .../comprehensive_sequence_test.json | 2289 +++++++++++++++++ .../comprehensive_sequence_test.puml | 387 +++ .../sequence_diagram/simple_sequence.puml | 32 + plantuml/parser/integration_test/src/lib.rs | 19 + .../integration_test/src/test_error_view.rs | 247 ++ .../integration_test/src/test_framework.rs | 264 ++ plantuml/parser/puml_cli/BUILD | 31 + plantuml/parser/puml_cli/src/main.rs | 416 +++ plantuml/parser/puml_lobster/BUILD | 24 + plantuml/parser/puml_lobster/src/lib.rs | 96 + plantuml/parser/puml_parser/BUILD | 56 + .../puml_parser/src/class_diagram/BUILD | 60 + .../detail_design/class_ast.puml | 139 + .../class_diagram/detail_design/parser.puml | 64 + .../src/class_diagram/detail_design/test.puml | 83 + .../src/class_diagram/src/class_ast.rs | 242 ++ .../src/class_diagram/src/class_parser.rs | 493 ++++ .../src/class_diagram/src/class_traits.rs | 23 + .../puml_parser/src/class_diagram/src/lib.rs | 36 + .../class_diagram/test/integration_test.rs | 201 ++ .../puml_parser/src/component_diagram/BUILD | 44 + .../component_diagram/src/component_ast.rs | 51 + .../component_diagram/src/component_parser.rs | 364 +++ .../src/component_diagram/src/lib.rs | 18 + .../test/component_integration_test.rs | 648 +++++ plantuml/parser/puml_parser/src/grammar/BUILD | 72 + .../parser/puml_parser/src/grammar/class.pest | 156 ++ .../puml_parser/src/grammar/common.pest | 275 ++ .../puml_parser/src/grammar/component.pest | 90 + .../puml_parser/src/grammar/include.pest | 85 + .../puml_parser/src/grammar/procedure.pest | 70 + .../puml_parser/src/grammar/sequence.pest | 252 ++ plantuml/parser/puml_parser/src/lib.rs | 22 + .../parser/puml_parser/src/parser_core/BUILD | 43 + .../src/parser_core/src/common_ast.rs | 63 + .../src/parser_core/src/common_detail_design | 60 + .../src/parser_core/src/common_parser.rs | 147 ++ .../puml_parser/src/parser_core/src/error.rs | 80 + .../puml_parser/src/parser_core/src/lib.rs | 31 + .../parser/puml_parser/src/preprocessor/BUILD | 72 + .../src/preprocessor/src/include/BUILD | 57 + .../preprocessor/src/include/include_ast.rs | 139 + .../src/include/include_expander.rs | 387 +++ .../src/include/include_parser.rs | 259 ++ .../src/preprocessor/src/include/lib.rs | 20 + .../src/preprocessor/src/include/utils.rs | 102 + .../puml_parser/src/preprocessor/src/lib.rs | 18 + .../src/preprocessor/src/preprocessor.rs | 90 + .../src/preprocessor/src/procedure/BUILD | 48 + .../src/preprocessor/src/procedure/lib.rs | 19 + .../src/procedure/procedure_ast.rs | 64 + .../src/procedure/procedure_expander.rs | 224 ++ .../src/procedure/procedure_parser.rs | 186 ++ .../src/preprocessor/tests/include_tests.rs | 106 + .../preprocessor/tests/preprocess_runner.rs | 33 + .../src/preprocessor/tests/procedure_tests.rs | 77 + .../puml_parser/src/sequence_diagram/BUILD | 48 + .../src/sequence_diagram/src/lib.rs | 40 + .../src/sequence_diagram/src/syntax_ast.rs | 133 + .../sequence_diagram/src/syntax_parse_main.rs | 69 + .../src/sequence_diagram/src/syntax_parser.rs | 389 +++ .../src/sequence_diagram/tests/BUILD | 29 + .../tests/syntax_parse_test.rs | 71 + .../puml_parser/tests/class_diagram/BUILD | 21 + .../alias_class/alias_class.puml | 22 + .../class_diagram/alias_class/output.json | 79 + .../alias_package/alias_package.puml | 30 + .../class_diagram/alias_package/output.json | 94 + .../attr_method/attr_method.puml | 21 + .../class_diagram/attr_method/output.json | 41 + .../class_merge/class_merge.puml | 23 + .../class_diagram/class_merge/output.json | 48 + .../tests/class_diagram/color/color.puml | 21 + .../tests/class_diagram/color/output.json | 39 + .../class_diagram/cpp_style/cpp_style.puml | 21 + .../tests/class_diagram/cpp_style/output.json | 50 + .../ctrl_instruct/ctrl_instruct.puml | 28 + .../class_diagram/ctrl_instruct/output.json | 52 + .../tests/class_diagram/empty/empty.puml | 23 + .../tests/class_diagram/empty/output.json | 7 + .../tests/class_diagram/enum/enum.puml | 34 + .../tests/class_diagram/enum/output.json | 109 + .../class_diagram/interface/interface.puml | 19 + .../tests/class_diagram/interface/output.json | 28 + .../namespace_1/namespace_1.puml | 20 + .../class_diagram/namespace_1/output.json | 43 + .../namespace_2/namespace_2.puml | 21 + .../class_diagram/namespace_2/output.json | 40 + .../namespace_3/namespace_3.puml | 21 + .../class_diagram/namespace_3/output.json | 45 + .../negative_pkg_comp/negative_pkg_comp.puml | 19 + .../negative_pkg_comp/output.yaml | 22 + .../class_diagram/one_class/one_class.puml | 17 + .../tests/class_diagram/one_class/output.json | 22 + .../only_attribute/only_attribute.puml | 19 + .../class_diagram/only_attribute/output.json | 28 + .../only_method/only_method.puml | 19 + .../class_diagram/only_method/output.json | 30 + .../tests/class_diagram/package/output.json | 78 + .../tests/class_diagram/package/package.puml | 26 + .../tests/class_diagram/param/output.json | 41 + .../tests/class_diagram/param/param.puml | 19 + .../class_diagram/param_templete/output.json | 36 + .../param_templete/param_templete.puml | 19 + .../relationship_arrows/output.json | 68 + .../relationship_arrows.puml | 20 + .../relationship_inheritance/output.json | 38 + .../relationship_inheritance.puml | 19 + .../relationship_mix_inher/output.json | 38 + .../relationship_mix_inher.puml | 18 + .../relationship_normal/output.json | 21 + .../relationship_normal.puml | 17 + .../relationship_qualified_id/output.json | 23 + .../relationship_qualified_id.puml | 17 + .../stereotype_definition/output.json | 101 + .../stereotype_definition.puml | 33 + .../stereotype_relationship/output.json | 52 + .../stereotype_relationship.puml | 20 + .../tests/class_diagram/struct/output.json | 81 + .../tests/class_diagram/struct/struct.puml | 29 + .../tests/preprocessor/include/BUILD | 20 + .../common.puml | 20 + .../output.yaml | 43 + .../user.puml | 22 + .../user2.puml | 22 + .../include/complex_include/common.puml | 20 + .../include/complex_include/feature.puml | 27 + .../include/complex_include/output.yaml | 43 + .../include/complex_include/user.puml | 18 + .../invalid_cycle_include/feature.puml | 18 + .../include/invalid_cycle_include/output.yaml | 18 + .../include/invalid_cycle_include/user.puml | 18 + .../include/invalid_include_path/output.yaml | 16 + .../include/invalid_include_path/user.puml | 18 + .../invalid_include_unknow_sub/common.puml | 19 + .../invalid_include_unknow_sub/output.yaml | 18 + .../invalid_include_unknow_sub/user.puml | 18 + .../invalid_nested_subblock/common.puml | 28 + .../invalid_nested_subblock/output.yaml | 24 + .../invalid_repeat_include_once/common.puml | 20 + .../invalid_repeat_include_once/output.yaml | 17 + .../invalid_repeat_include_once/user.puml | 19 + .../invalid_suffix_for_include/output.yaml | 21 + .../invalid_suffix_for_include/user.puml | 17 + .../invalid_suffix_for_includesub/output.yaml | 24 + .../invalid_suffix_for_includesub/user.puml | 17 + .../include/repeat_include/common.puml | 20 + .../include/repeat_include/output.yaml | 29 + .../include/repeat_include/user.puml | 21 + .../include/several_subblock/common.puml | 30 + .../include/several_subblock/output.yaml | 25 + .../include/several_subblock/user.puml | 19 + .../include/simple_include/common.puml | 20 + .../include/simple_include/output.yaml | 27 + .../include/simple_include/user.puml | 18 + .../include/simple_include_many/common.puml | 20 + .../include/simple_include_many/output.yaml | 33 + .../include/simple_include_many/user.puml | 22 + .../include/simple_include_once/common.puml | 20 + .../include/simple_include_once/output.yaml | 27 + .../include/simple_include_once/user.puml | 18 + .../include/simple_includesub/common.puml | 26 + .../include/simple_includesub/output.yaml | 30 + .../include/simple_includesub/user.puml | 18 + .../tests/preprocessor/procedure/BUILD | 20 + .../args_not_match/args_not_match.puml | 21 + .../procedure/args_not_match/output.yaml | 19 + .../preprocessor/procedure/empty/empty.puml | 34 + .../preprocessor/procedure/empty/output.yaml | 24 + .../fta_metamodel/fta_metamodel.puml | 44 + .../procedure/fta_metamodel/output.yaml | 34 + .../procedure/fta_metamodel/sample_fta.puml | 30 + .../macro_not_define/macro_not_define.puml | 21 + .../procedure/macro_not_define/output.yaml | 17 + .../procedure/mix_call/mix_call.puml | 26 + .../procedure/mix_call/output.yaml | 17 + .../procedure/noise_item/noise_item.puml | 21 + .../procedure/noise_item/output.yaml | 16 + .../procedure/recursive_macro/output.yaml | 18 + .../recursive_macro/recursive_macro.puml | 25 + .../procedure/simple_macro/output.yaml | 16 + .../procedure/simple_macro/simple_macro.puml | 21 + .../procedure/simple_template/output.yaml | 16 + .../simple_template/simple_template.puml | 21 + .../procedure/use_include/fta.puml | 19 + .../procedure/use_include/output.yaml | 19 + .../procedure/use_include/simple.puml | 19 + plantuml/parser/puml_resolver/BUILD | 49 + .../puml_resolver/src/class_diagram/BUILD | 62 + .../src/class_diagram/src/class_resolver.rs | 732 ++++++ .../src/class_diagram/src/lib.rs | 15 + .../class_diagram/test/class_resolver_test.rs | 74 + .../puml_resolver/src/component_diagram/BUILD | 55 + .../component_diagram/src/component_logic.rs | 69 + .../src/component_resolver.rs | 232 ++ .../src/component_diagram/src/lib.rs | 18 + .../tests/component_resolver_test.rs | 75 + plantuml/parser/puml_resolver/src/lib.rs | 16 + .../puml_resolver/src/resolver_traits.rs | 26 + .../puml_resolver/src/sequence_diagram/BUILD | 60 + .../src/sequence_diagram/src/lib.rs | 17 + .../src/sequence_diagram/src/logic_ast.rs | 59 + .../sequence_diagram/src/logic_parse_main.rs | 70 + .../src/sequence_diagram/src/logic_parser.rs | 269 ++ .../src/sequence_diagram/test/logic.json | 105 + .../sequence_diagram/test/logic_parse_test.rs | 62 + .../src/sequence_diagram/test/syntax.json | 187 ++ plantuml/parser/puml_serializer/BUILD | 28 + plantuml/parser/puml_serializer/src/fbs/BUILD | 47 + .../puml_serializer/src/fbs/component.fbs | 61 + plantuml/parser/puml_serializer/src/lib.rs | 14 + .../puml_serializer/src/serialize/BUILD | 28 + .../src/serialize/component_serializer.rs | 129 + plantuml/parser/puml_utils/BUILD | 28 + plantuml/parser/puml_utils/src/lib.rs | 17 + plantuml/parser/puml_utils/src/log.rs | 32 + plantuml/parser/puml_utils/src/write_files.rs | 71 + tools/metamodel/BUILD | 43 + tools/metamodel/schema/class_diagram.fbs | 207 ++ tools/metamodel/src/class_logic.rs | 364 +++ 251 files changed, 19516 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 plantuml/parser/BUILD create mode 100644 plantuml/parser/README.md create mode 100644 plantuml/parser/docs/README.md create mode 100644 plantuml/parser/docs/assets/preprocessor_seq.puml create mode 100644 plantuml/parser/docs/assets/puml_parser.puml create mode 100644 plantuml/parser/docs/assets/puml_parser.svg create mode 100644 plantuml/parser/docs/assets/puml_parser_class.puml create mode 100644 plantuml/parser/docs/assets/puml_parser_class.svg create mode 100644 plantuml/parser/docs/assets/puml_parser_deployment.puml create mode 100644 plantuml/parser/docs/assets/puml_parser_deployment.svg create mode 100644 plantuml/parser/docs/tool_requirements.trlc create mode 100644 plantuml/parser/integration_test/BUILD create mode 100644 plantuml/parser/integration_test/class_diagram/BUILD create mode 100644 plantuml/parser/integration_test/class_diagram/class_diagram_negative/class_diagram_negative.puml create mode 100644 plantuml/parser/integration_test/class_diagram/class_diagram_negative/output.yaml create mode 100644 plantuml/parser/integration_test/class_diagram/class_diagram_positive/class_diagram_positive.puml create mode 100644 plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/BUILD create mode 100644 plantuml/parser/integration_test/component_diagram/n_pkg_comp.puml create mode 100644 plantuml/parser/integration_test/component_diagram/relation_fqn/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/relation_fqn/relation_fqn.puml create mode 100644 plantuml/parser/integration_test/component_diagram/relation_relative_name/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/relation_relative_name/relation_relative_name.puml create mode 100644 plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json create mode 100644 plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.puml create mode 100644 plantuml/parser/integration_test/component_diagram/simple_component.puml create mode 100644 plantuml/parser/integration_test/sequence_diagram/BUILD create mode 100644 plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.json create mode 100644 plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml create mode 100644 plantuml/parser/integration_test/sequence_diagram/simple_sequence.puml create mode 100644 plantuml/parser/integration_test/src/lib.rs create mode 100644 plantuml/parser/integration_test/src/test_error_view.rs create mode 100644 plantuml/parser/integration_test/src/test_framework.rs create mode 100644 plantuml/parser/puml_cli/BUILD create mode 100644 plantuml/parser/puml_cli/src/main.rs create mode 100644 plantuml/parser/puml_lobster/BUILD create mode 100644 plantuml/parser/puml_lobster/src/lib.rs create mode 100644 plantuml/parser/puml_parser/BUILD create mode 100644 plantuml/parser/puml_parser/src/class_diagram/BUILD create mode 100644 plantuml/parser/puml_parser/src/class_diagram/detail_design/class_ast.puml create mode 100644 plantuml/parser/puml_parser/src/class_diagram/detail_design/parser.puml create mode 100644 plantuml/parser/puml_parser/src/class_diagram/detail_design/test.puml create mode 100644 plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs create mode 100644 plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs create mode 100644 plantuml/parser/puml_parser/src/class_diagram/src/class_traits.rs create mode 100644 plantuml/parser/puml_parser/src/class_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs create mode 100644 plantuml/parser/puml_parser/src/component_diagram/BUILD create mode 100644 plantuml/parser/puml_parser/src/component_diagram/src/component_ast.rs create mode 100644 plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs create mode 100644 plantuml/parser/puml_parser/src/component_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs create mode 100644 plantuml/parser/puml_parser/src/grammar/BUILD create mode 100644 plantuml/parser/puml_parser/src/grammar/class.pest create mode 100644 plantuml/parser/puml_parser/src/grammar/common.pest create mode 100644 plantuml/parser/puml_parser/src/grammar/component.pest create mode 100644 plantuml/parser/puml_parser/src/grammar/include.pest create mode 100644 plantuml/parser/puml_parser/src/grammar/procedure.pest create mode 100644 plantuml/parser/puml_parser/src/grammar/sequence.pest create mode 100644 plantuml/parser/puml_parser/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/parser_core/BUILD create mode 100644 plantuml/parser/puml_parser/src/parser_core/src/common_ast.rs create mode 100644 plantuml/parser/puml_parser/src/parser_core/src/common_detail_design create mode 100644 plantuml/parser/puml_parser/src/parser_core/src/common_parser.rs create mode 100644 plantuml/parser/puml_parser/src/parser_core/src/error.rs create mode 100644 plantuml/parser/puml_parser/src/parser_core/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/BUILD create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/BUILD create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/lib.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/include/utils.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/preprocessor.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/procedure/BUILD create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/procedure/lib.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_ast.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/tests/preprocess_runner.rs create mode 100644 plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/BUILD create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_ast.rs create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parse_main.rs create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parser.rs create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/tests/BUILD create mode 100644 plantuml/parser/puml_parser/src/sequence_diagram/tests/syntax_parse_test.rs create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/BUILD create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/alias_class/alias_class.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/alias_class/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/alias_package/alias_package.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/alias_package/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/attr_method/attr_method.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/attr_method/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/class_merge/class_merge.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/class_merge/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/color/color.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/color/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/cpp_style/cpp_style.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/cpp_style/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/ctrl_instruct.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/empty/empty.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/empty/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/enum/enum.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/enum/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/interface/interface.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/interface/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_2/namespace_2.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_2/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_3/namespace_3.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/namespace_3/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/negative_pkg_comp.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/one_class/one_class.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/one_class/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/only_attribute/only_attribute.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/only_attribute/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/only_method/only_method.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/only_method/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/package/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/package/package.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/param/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/param/param.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/param_templete/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/param_templete/param_templete.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/relationship_arrows.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/relationship_inheritance.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/relationship_mix_inher.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/relationship_normal.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/relationship_qualified_id.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/stereotype_definition.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/stereotype_relationship.puml create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/struct/output.json create mode 100644 plantuml/parser/puml_parser/tests/class_diagram/struct/struct.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/BUILD create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user2.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/feature.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/feature.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/user.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/BUILD create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/args_not_match.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/fta_metamodel.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/sample_fta.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/macro_not_define.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/noise_item.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/recursive_macro.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/simple_macro.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/simple_template.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/fta.puml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/output.yaml create mode 100644 plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/simple.puml create mode 100644 plantuml/parser/puml_resolver/BUILD create mode 100644 plantuml/parser/puml_resolver/src/class_diagram/BUILD create mode 100644 plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs create mode 100644 plantuml/parser/puml_resolver/src/class_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_resolver/src/class_diagram/test/class_resolver_test.rs create mode 100644 plantuml/parser/puml_resolver/src/component_diagram/BUILD create mode 100644 plantuml/parser/puml_resolver/src/component_diagram/src/component_logic.rs create mode 100644 plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs create mode 100644 plantuml/parser/puml_resolver/src/component_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs create mode 100644 plantuml/parser/puml_resolver/src/lib.rs create mode 100644 plantuml/parser/puml_resolver/src/resolver_traits.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/BUILD create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/src/lib.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_ast.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parse_main.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/test/logic.json create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/test/logic_parse_test.rs create mode 100644 plantuml/parser/puml_resolver/src/sequence_diagram/test/syntax.json create mode 100644 plantuml/parser/puml_serializer/BUILD create mode 100644 plantuml/parser/puml_serializer/src/fbs/BUILD create mode 100644 plantuml/parser/puml_serializer/src/fbs/component.fbs create mode 100644 plantuml/parser/puml_serializer/src/lib.rs create mode 100644 plantuml/parser/puml_serializer/src/serialize/BUILD create mode 100644 plantuml/parser/puml_serializer/src/serialize/component_serializer.rs create mode 100644 plantuml/parser/puml_utils/BUILD create mode 100644 plantuml/parser/puml_utils/src/lib.rs create mode 100644 plantuml/parser/puml_utils/src/log.rs create mode 100644 plantuml/parser/puml_utils/src/write_files.rs create mode 100644 tools/metamodel/BUILD create mode 100644 tools/metamodel/schema/class_diagram.fbs create mode 100644 tools/metamodel/src/class_logic.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c330c3c --- /dev/null +++ b/.github/CODEOWNERS @@ -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 +# ******************************************************************************* + +# GitHub CODEOWNERS file is a simple way to automate review system on github, +# by automatically assigning owners to a pull request based on which files +# were modified. All directories should have a proper codeowner +# Syntax: https://help.github.com/articles/about-codeowners/ + +/plantuml/ @castler @hoe-jo @LittleHuba @limdor @ramceb +/bazel/rules/rules_score/ @castler @hoe-jo @LittleHuba @limdor @ramceb diff --git a/BUILD b/BUILD index c298dc9..34e97d2 100644 --- a/BUILD +++ b/BUILD @@ -28,6 +28,7 @@ copyright_checker( "python_basics", "starpls", "tools", + "plantuml", # Add other directories/files you want to check ], diff --git a/MODULE.bazel b/MODULE.bazel index 12a2dfd..eb06b86 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -32,6 +32,69 @@ bazel_dep(name = "rules_multitool", version = "1.9.0") bazel_dep(name = "score_rust_policies", version = "0.0.2") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2") +bazel_dep(name = "flatbuffers", version = "25.9.23") + +############################################################################### +# Rust Toolchain +############################################################################### +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") +rust.toolchain(edition = "2021") +use_repo(rust, "rust_toolchains") + +register_toolchains("@rust_toolchains//:all") + +############################################################################### +# Rust Crates (crate_universe) +############################################################################### +crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") +crate.spec( + package = "pest", + version = "2.8.3", +) +crate.spec( + package = "pest_derive", + version = "2.8.3", +) +crate.spec( + features = ["derive"], + package = "clap", + version = "4.5.0", +) +crate.spec( + package = "thiserror", + version = "1.0.44", +) +crate.spec( + package = "log", + version = "0.4", +) +crate.spec( + package = "env_logger", + version = "0.10", +) +crate.spec( + features = ["derive"], + package = "serde", + version = "1.0", +) +crate.spec( + package = "serde_json", + version = "1.0", +) +crate.spec( + package = "serde_yaml", + version = "0.9", +) +crate.spec( + package = "once_cell", + version = "1.19", +) +crate.spec( + package = "flatbuffers", + version = "25.9.23", +) +crate.from_specs() +use_repo(crate, "crates") ############################################################################### # Python Toolchain diff --git a/REUSE.toml b/REUSE.toml index 6b38dde..8752098 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -48,3 +48,13 @@ SPDX-License-Identifier = "Apache-2.0" path = ["dash/org.eclipse.dash.licenses-1.1.0.jar"] SPDX-FileCopyrightText = "2020 Eclipse Foundation" SPDX-License-Identifier = "EPL-2.0" + +[[annotations]] +path = ["plantuml/**/*.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["plantuml/**/*.svg"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" diff --git a/plantuml/parser/BUILD b/plantuml/parser/BUILD new file mode 100644 index 0000000..60f334c --- /dev/null +++ b/plantuml/parser/BUILD @@ -0,0 +1,17 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +alias( + name = "parser", + actual = "//plantuml/parser/puml_cli:puml_cli", + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/README.md b/plantuml/parser/README.md new file mode 100644 index 0000000..0235e88 --- /dev/null +++ b/plantuml/parser/README.md @@ -0,0 +1,89 @@ + + +# PlantUML Parser + +The PlantUML Parser is a multi-file parser that processes PlantUML diagram files and converts them into structured data for traceability and architectural analysis. It handles preprocessing of include directives, supports multiple diagram types (Class, Sequence, Component), and generates FlatBuffers binary output for further downstream processing. + +--- + +## What It Does + +The parser takes `.puml` source files as input and produces: + +- **FlatBuffers binary** (`.fbs.bin`) — primary structured output for downstream consumers +- **LOBSTER traceability JSON** (`.lobster`) — for component diagrams, enables traceability linking via the LOBSTER toolchain +- **Debug JSON** — raw and resolved ASTs when `--log-level debug` or higher is set + + +Rust-based parser that produces an AST for the following PlantUML diagram types: + +- Class Diagram +- Sequence Diagram +- Component Diagram + +Outputs are `.fbs.bin` FlatBuffers binaries (diagram AST) and optionally `.lobster` +traceability files (for component diagrams). + +## Usage + +### Build: +``` +bazel build //tools/plantuml/parser +``` + +### Run: +``` +bazel run //tools/plantuml/parser -- [OPTIONS] +``` + +Options: + +| Option | Description | Default | +|--------|-------------|---------| +| `--file ...` | One or more PUML files to parse (repeatable) | — | +| `--folders ` | Folder containing PUML files | — | +| `--log-level ` | Logging verbosity | `warn` | +| `--diagram-type ` | Diagram type hint | `none` | +| `--fbs-output-dir ` | Output directory for `.fbs.bin` FlatBuffers files | none (no output) | +| `--lobster-output-dir ` | Output directory for `.lobster` traceability files | none (no output) | + +At least one of `--file` or `--folders` is required. + +Example: +``` +bazel run //tools/plantuml/parser -- \ + --file $PWD/tools/plantuml/parser/integration_test/component_diagram/simple_component.puml \ + --log-level trace \ + --diagram-type component \ + --fbs-output-dir $PWD/tools/plantuml/parser +``` +## Architecture + +The parser is organized into separate crate modules per diagram type: + +``` +puml_parser/ +├── src/ +│ ├── class_diagram/ # Class diagram parser (pest grammar → AST) +│ ├── component_diagram/ # Component diagram parser +│ ├── sequence_diagram/ # Sequence diagram parser (two-stage: syntax → logic) +│ └── ... # Shared utilities (parser_core, puml_utils) +puml_cli/ # CLI entry point (clap-based) +``` + +Each diagram parser uses [pest](https://pest.rs/) PEG grammars to tokenize PlantUML input, +then builds a typed AST. The CLI (`puml_cli`) dispatches to the appropriate parser based on +`--diagram-type` or auto-detection. + +For the detailed design and users Guide, see [README](docs/README.md). diff --git a/plantuml/parser/docs/README.md b/plantuml/parser/docs/README.md new file mode 100644 index 0000000..6e764fc --- /dev/null +++ b/plantuml/parser/docs/README.md @@ -0,0 +1,386 @@ + + +# PlantUML Parser + +### Processing Phases + +The sequence diagram shows the phases of the plantuml processing: + +![Puml Parser Sequence](assets/puml_parser.svg) + +#### 1. Preprocessing Phase + +`!include` directives are expanded into a single self-contained `.puml` content per file before any parsing occurs. The supported directives are: + +- `!include` +- `!include_once` +- `!include_many` +- `!includesub` + +The preprocessor: + +- Resolves file paths (absolute and relative) +- Recursively expands nested includes +- Detects circular dependencies and reports an error +- Caches already-resolved files to avoid redundant I/O + +#### 2. Parsing Phase + +Each preprocessed file is parsed into an AST based on its diagram type. The diagram type is either: + +- Specified explicitly via `--diagram-type` CLI argument +- Auto-detected by trying parsers in order (Sequence → Class → Component) until one succeeds +- If all parsers fail, the error with the longest match is reported + +#### 3. Semantic Resolution Phase + +The AST is resolved into a logical model by the `ComponentResolver`: + +- Builds fully qualified names (FQN) for all elements using scope tracking +- Resolves relative and absolute references between components +- Detects duplicate component definitions and unresolved references + +#### 4. Serialization Phase + +The resolved logical model is serialized to FlatBuffers binary format by `ComponentSerializer` for further downstream processing. + +**Example input** + +```plantuml +@startuml + +package "Sample SEooC" as SampleSEooC #LightBlue { + component "Component A" as ComponentA <> { + component "Unit A" as UnitA <> { + } + } + component "Component B" as ComponentB <> { + component "Unit B" as UnitB <> { + } + } +} + +ComponentA --> ComponentB : uses + +@enduml +``` + +**Raw parse tree output** + +```text +=== Parse Tree === +Rule::startuml -> "@startuml" + Rule::statement -> "package \"Sample SEooC\" ..." + Rule::component + Rule::nested_component -> "package \"Sample SEooC\"" + Rule::default_component + Rule::component_type -> "package" + Rule::default_component_name -> "\"Sample SEooC\"" + Rule::alias -> "as SampleSEooC" + Rule::component_style -> "#LightBlue" + Rule::statement_block -> "{ ... }" + Rule::relation -> "ComponentA --> ComponentB : uses" + Rule::relation_object -> "ComponentA" + Rule::arrow -> "-->" + Rule::relation_object -> "ComponentB" + Rule::description -> ": uses" +Rule::enduml +=== End Parse Tree === +``` + +**Semantic Resolution output** + +``` +Component( + name: "" + id: "" + parent: "" + stereotype: "" + type: "" +) + +Relation( + source: "" + target: "" + type: "" +) + +Interface( + name: "" + id: "" +) +``` + +--- + +## Developer Guide + +### Module Structure + +| Crate | Responsibility | +|---|---| +| `puml_utils` | Shared utilities: `LogLevel` enum, file writing helpers | +| `puml_parser` | Preprocessor (include expansion) + Class / Component / Sequence diagram parsers | +| `puml_resolver` | Resolves raw AST into logical model (`ComponentResolver`, `DiagramResolver` trait) | +| `puml_serializer` | FlatBuffers serialization of the resolved model (`ComponentSerializer`) | +| `puml_lobster` | Converts the resolved model to LOBSTER traceability JSON | +| `puml_cli` | CLI argument parsing and orchestration of all phases | + +### Architecture Diagrams + +**Class structure:** + +![Puml Parser Class Diagram](assets/puml_parser_class.svg) + +**Deployment:** + +![Puml Parser Deployment Diagram](assets/puml_parser_deployment.svg) + +### DiagramParser Trait + +All diagram parsers implement a common trait: + +```rust +pub trait DiagramParser { + type Output; + type Error; + + fn parse_file( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result; +} +``` + +`LogLevel` for the complete parser is defined in `puml_utils`: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} +``` + +### Error Hierarchy + +A layered error hierarchy allows integration tests to handle errors uniformly without duplicating error-matching logic at each layer: + +``` +BaseParseError // IoError, SyntaxError + ↓ +IncludeParseError // BaseParseError + InvalidTextLine + ↓ +PreprocessError // IncludeParseError + FileNotFound, CycleInclude, ... +``` + +Each level wraps the level below as a `#[source]`, enabling structured nested error reporting: + +```yaml +error: + type: ParseFailed + fields: + file: main.puml + source: # ← Nested error + type: IoError + fields: + path: missing.puml +``` + +`BaseParseError` definition: + +```rust +#[derive(Debug, thiserror::Error)] +pub enum BaseParseError { + #[error("Failed to read include file {path}: {error}")] + IoError { + path: PathBuf, + #[source] + error: Box, + }, + + #[error("Pest error: {message}")] + SyntaxError { + file: PathBuf, + line: usize, + column: usize, + message: String, + source_line: String, + #[source] + cause: Option>>, + }, +} +``` + +### Include Preprocessor + +The preprocessor pipeline consists of three stages executed per file: + +#### Parse AST + +The `.puml` file is parsed by the pest include grammar into a `Vec`: + +```rust +[ + PreprocessStmt::Text("@startuml\n"), + PreprocessStmt::Include( + IncludeStmt::Include { + kind: IncludeKind::Include, + path: "common.puml" + } + ), + PreprocessStmt::Text("class User {}\n"), + PreprocessStmt::Text("@enduml\n") +] +``` + +#### Expand Statements + +Each `Include` statement is resolved and replaced with the included file's content: + +```rust +[ + PreprocessStmt::Text("@startuml\n"), + PreprocessStmt::Text("class Common {\n +id: int\n +login()\n}\n"), + PreprocessStmt::Text("class User {}\n"), + PreprocessStmt::Text("@enduml\n") +] +``` + +Included files may themselves contain includes, making this step recursive. See the sequence diagram for the full recursive flow: + +![Sequence Diagram of Include Parser](assets/preprocessor_seq.svg) + +#### Render + +The expanded `Vec` is rendered back to a plain `.puml` string: + +```plantuml +@startuml + +class Common { + +id: int + +login() +} + +class User {} + +@enduml +``` + +### Integration Test Framework + +The integration test framework provides golden-based testing for all diagram parsers, supporting multi-file test cases, string/AST output comparison, and structured error expectations. + +#### Test Case Layout + +Each test case is a directory under `integration_test///`: + +``` +integration_test/// + ├── a.puml + ├── b.puml + └── output.yaml # or output.json +``` + +#### Golden File Formats + +**`output.yaml` — text output comparison** + +Use YAML when the expected output is rendered text. Whitespace is normalized automatically. + +```yaml +a.puml: | + @startuml + Alice -> Bob + @enduml +``` + +**`output.json` — AST comparison** + +Use JSON when comparing AST structures directly. Each top-level key maps to a `.puml` file; the value must match the AST type exactly (requires `Deserialize` and `PartialEq`). + +```json +{ + "a.puml": { + "name": [], + "elements": [] + }, + "b.puml": {} +} +``` + +#### Error Expectations + +Errors are described in `output.yaml` using a structured format: + +```yaml +a.puml: + error: + type: SyntaxError + fields: + file: a.puml + line: "10" + column: "5" + message: "unexpected token" + source_line: "A -> B" +``` + +The framework compares actual errors against expectations via the `ErrorView` trait: + +```rust +pub struct ProjectedError { + pub kind: String, + pub fields: HashMap, + pub source: Option>, // nested cause +} + +pub trait ErrorView { + fn project(&self, base_dir: &Path) -> ProjectedError; +} +``` + +Each error type implements `ErrorView` to produce a path-relative, uniformly comparable structure. Nested errors (via `#[source]`) are represented recursively through `source`. + +#### Core Traits + +**`DiagramProcessor`** — runs the parser under test and returns one output per input file: + +```rust +pub trait DiagramProcessor { + type Output; + type Error; + + fn run( + &self, + files: &HashSet>, + ) -> Result, Self::Output>, Self::Error>; +} +``` + +**`ExpectationChecker`** — compares actual output or errors against the golden file: + +```rust +pub trait ExpectationChecker + Debug + PartialEq> { + fn check_ok(&self, actual: &Output, expected: &Expected); + fn check_err(&self, err: &Error, expected: &YamlValue, base_dir: &Path); +} +``` + +A default implementation covering string, AST, and error comparison is provided. Implement a custom `ExpectationChecker` only when specialized comparison logic is needed. diff --git a/plantuml/parser/docs/assets/preprocessor_seq.puml b/plantuml/parser/docs/assets/preprocessor_seq.puml new file mode 100644 index 0000000..837cb6c --- /dev/null +++ b/plantuml/parser/docs/assets/preprocessor_seq.puml @@ -0,0 +1,111 @@ +' ******************************************************************************* +' 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 +title Main Preprocessing Loop + +participant "CLI" as Cli +participant "Preprocessor" as Prep +participant "IncludeContext" as Ctx +participant "IncludeParser" as Parser +database "AstCache\n(HashMap)" as Cache +database "SubRegistry\n(HashMap)" as SubReg + +Cli -> Prep: preprocess(&file_list) +activate Prep + +loop for each file in file_list + Prep -> Ctx **: default() + Prep -> Prep: expand_file(file, ctx, file_list) + activate Prep #LightBlue + Prep -> Ctx: push_stack(file) + activate Ctx + alt cycle detected + Ctx --> Prep: Error(CycleInclude) + else no cycle + Ctx --> Prep: Ok() + end + deactivate Ctx + +== Parse AST == + + Prep -> Prep: stmts_ast = load_ast(file) : + activate Prep #LightPink + + Prep -> Cache: get(file) + activate Cache + note right: First Check if File already\nparsed an stored in Cache + + alt File cached + Cache --> Prep: Some(Rc>) + deactivate Cache + Prep -> Prep: Rc::clone(ast) + note right: Increment reference count (no copy) + return Ok(Rc>) + else not cached + activate Cache + Cache --> Prep ++: None + deactivate Cache + + activate Prep #LightPink + Prep -> Parser: parse_file(file) + activate Parser + note right: Parse PlantUML file into\nVec AST\n(expensive operation) + Parser --> Prep -- : Result> + Prep --> Cache : insert(file, &rc_ast); + + Prep --> SubReg ++ : collect_from_file() + return + end + + Prep --> Prep -- : Ok(Rc>) + +== Expand Statements == + + Prep -> Prep : expanded_ast = expand_stmts(...) : Vec + activate Prep #LightGreen + alt Include + Prep -> Prep : expand_include(...) : String + activate Prep #LightYellow + alt Include + Prep -> Ctx ++: check_and_mark_include(...) + return bool + Prep -> Prep : recusively call expand_file(...) + ... + else IncludeSub + Prep -> Parser ++ : load_ast(file) + return + Prep -> Prep : recursively call expand_stmts(...) + ... + end + else SubBlock + Prep -> Prep : recursively call expand_stmts(...) + else Text only + return PreprocessStmt::Text(text.clone()) + end + + return expanded_ast : Vec + +== Render to Text == + Prep -> Prep ++ : text = render_stmts(expanded_ast) + return expanded text + + return expanded text + + Prep -> Prep: store result in map +end + +return HashMap +deactivate Prep + +@enduml diff --git a/plantuml/parser/docs/assets/puml_parser.puml b/plantuml/parser/docs/assets/puml_parser.puml new file mode 100644 index 0000000..f66b600 --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser.puml @@ -0,0 +1,97 @@ +' ******************************************************************************* +' 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 +title Main Parsing Flow + +participant "CLI" as Cli +participant "Preprocessor" as include +participant "PumlSequenceParser" as seq +participant "PumlClassParser" as class +participant "PumlComponentParser" as component +participant "ComponentResolver" as resolver +participant "ComponentSerializer" as serializer +participant "puml_lobster" as lobster + +activate Cli + +== Preprocessing == + +Cli -> include **: new() +activate include +return include_resolver + +Cli -> include ++: preprocess(file_list, log_level) +return HashMap + +== Parsing == + +loop for each file in file_list + note over Cli: If no CLI argument, try parsers in order\notherwise use specified parser via CLI + + Cli -> seq **: parse_file(path, content, log_level) + activate seq + seq --> Cli: Result + deactivate seq + destroy seq + + opt sequence parser not succeeded + note right: return early + end + + Cli -> class **: parse_file(path, content, log_level) + activate class + class --> Cli: Result + deactivate class + destroy class + + opt class parser not succeeded + note right: return early + end + + Cli -> component **: parse_file(path, content, log_level) + activate component + component --> Cli: Result + deactivate component + destroy component + + opt component parser not succeeded + note right: return early + end + + Cli --> Cli: Error(NoDiagramTypeMatched) +end + +== Semantic Resolution == + +Cli -> resolver **: visit_document(&ast) +activate resolver +return HashMap +destroy resolver + +== Serialization == + +Cli -> serializer **: serialize(&components, source_file) +activate serializer +serializer --> Cli: Vec +deactivate serializer +destroy serializer + +opt lobster_output_dir provided + Cli -> lobster: model_to_lobster(&components, source_path) + activate lobster + lobster --> Cli: JSON + deactivate lobster +end + +@enduml diff --git a/plantuml/parser/docs/assets/puml_parser.svg b/plantuml/parser/docs/assets/puml_parser.svg new file mode 100644 index 0000000..b2e0fcb --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser.svg @@ -0,0 +1 @@ +Main Parsing FlowMain Parsing FlowCLIPreprocessorPreprocessorPumlSequenceParserPumlClassParserPumlComponentParserComponentResolverComponentSerializerpuml_lobsterCLIPreprocessorPumlSequenceParserPumlClassParserPumlComponentParserComponentResolverComponentSerializerpuml_lobsterCLICLIPreprocessorPumlSequenceParserPumlClassParserPumlComponentParserComponentResolverComponentSerializerpuml_lobsterpuml_lobsterCLIPreprocessorPreprocessorPumlSequenceParserPumlClassParserPumlComponentParserComponentResolverComponentSerializerpuml_lobsternew()Preprocessorinclude_resolverpreprocess(file_list, log_level)HashMap<PathBuf, String>loop[for each file in file_list]If no CLI argument, try parsers in orderotherwise use specified parser via CLIparse_file(path, content, log_level)PumlSequenceParserResult<SeqPumlDocument>return earlyopt[sequence parser not succeeded]parse_file(path, content, log_level)PumlClassParserResult<ClassUmlFile>return earlyopt[class parser not succeeded]parse_file(path, content, log_level)PumlComponentParserResult<CompPumlDocument>return earlyopt[component parser not succeeded]Error(NoDiagramTypeMatched)new()ComponentResolvervisit_document(&ast)HashMap<String, LogicComponent>serialize(&components, source_file)ComponentSerializerVec<u8>opt[lobster_output_dir provided]model_to_lobster(&components, source_path)JSON diff --git a/plantuml/parser/docs/assets/puml_parser_class.puml b/plantuml/parser/docs/assets/puml_parser_class.puml new file mode 100644 index 0000000..3347e7e --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser_class.puml @@ -0,0 +1,91 @@ +' ******************************************************************************* +' 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 +skinparam shadowing false +title PUML Multi-File Parser - Class Diagram + +package "Puml Parser" { + + package "Command Line Interface" { + enum DiagramType { + NONE + COMPONENT + CLASS + SEQUENCE + } + + struct "CLI" as CLI { + + main(args: List) : void + - parse_puml_file(path, content, log_level, diagram_type) : ParsedDiagram + } + } + + package "Include Parser" { + struct Preprocessor <> { + + preprocess(file_list, log_level): HashMap + } + } + + package "Parsing Layer (pest)" { + interface DiagramParser { + + parse_file(path, content, log_level) : Output + } + + struct PumlClassParser <> + struct PumlSequenceParser <> + struct PumlComponentParser <> + + DiagramParser <|.. PumlClassParser + DiagramParser <|.. PumlSequenceParser + DiagramParser <|.. PumlComponentParser + + } + + package "Semantic Resolution" { + interface DiagramResolver { + + visit_document(document) : Output + } + + class ComponentResolver { + + new() : ComponentResolver + + visit_document(ast: CompPumlDocument) : HashMap + } + + DiagramResolver <|.. ComponentResolver + } + + + package "FlatBuffers Serialization" { + class ComponentSerializer <> { + + serialize(model: HashMap, source_file: &str) : Vec + } + } + + package "Lobster Output" { + class LobsterWriter <> { + + model_to_lobster(model: HashMap, source_path: &str) : Value + + write_lobster_to_file(model: HashMap, source_path: &str, output_dir: &Path) + } + } +} + +' Data Flow +CLI --> Preprocessor : preprocess +CLI --> DiagramParser : dispatch +CLI --> ComponentResolver : visit_document +CLI --> ComponentSerializer : serialize +CLI --> LobsterWriter : write lobster trace + +@enduml diff --git a/plantuml/parser/docs/assets/puml_parser_class.svg b/plantuml/parser/docs/assets/puml_parser_class.svg new file mode 100644 index 0000000..fa28257 --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser_class.svg @@ -0,0 +1 @@ +PUML Multi-File Parser - Class DiagramPUML Multi-File Parser - Class DiagramPuml ParserCommand Line InterfaceInclude ParserParsing Layer (pest)Semantic ResolutionFlatBuffers SerializationLobster OutputDiagramTypeNONECOMPONENTCLASSSEQUENCECLImain(args: List<String>) : voidparse_puml_file(path, content, log_level, diagram_type) : ParsedDiagram«preprocess»Preprocessorpreprocess(file_list, log_level): HashMap<PathBuf, String>DiagramParserparse_file(path, content, log_level) : Output«parser»PumlClassParser«parser»PumlSequenceParser«parser»PumlComponentParserDiagramResolvervisit_document(document) : OutputComponentResolvernew() : ComponentResolvervisit_document(ast: CompPumlDocument) : HashMap<String, LogicComponent>«serializer»ComponentSerializerserialize(model: HashMap<String, LogicComponent>, source_file: &str) : Vec<u8>«writer»LobsterWritermodel_to_lobster(model: HashMap<String, LogicComponent>, source_path: &str) : Valuewrite_lobster_to_file(model: HashMap<String, LogicComponent>, source_path: &str, output_dir: &Path)preprocessdispatchvisit_documentserializewrite lobster trace diff --git a/plantuml/parser/docs/assets/puml_parser_deployment.puml b/plantuml/parser/docs/assets/puml_parser_deployment.puml new file mode 100644 index 0000000..d329d48 --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser_deployment.puml @@ -0,0 +1,58 @@ +' ******************************************************************************* +' 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 +title PUML Multi-File Parser - Deployment Diagram + +node "PUML Multi-File Parser Runtime" { + + node "Pest Grammar" { + artifact "class.pest" + artifact "component.pest" + artifact "sequence.pest" + artifact "common.pest" + artifact "include.pest" + artifact "procedure.pest" + } + + node "Input Files" { + artifact "PUML Sources (*.puml)" as PUMLSources + } + + component "PumlFileParser" as Parser + + component "ComponentResolver" as Resolver + + component "ComponentSerializer" as Generator + + node "FlatBuffers Schema" { + artifact "FlatBuffers Schema (.fbs)" as FbsDef + ' Defines the FlatBuffers message schema used when + ' filling and serializing the binary output. + } + + node "output" { + artifact "BinaryFlatBufferFile (.fbs.bin)" as OutputFbs + artifact "LobsterTraceFile (.lobster)" as OutputLobster + } +} + +PUMLSources --> Parser : consumed by +"Pest Grammar" --> Parser : grammar +Parser --> Resolver : AST +Resolver --> Generator : LogicComponent +FbsDef --> Generator : schema +Generator --> OutputFbs : writes +Generator --> OutputLobster : writes + +@enduml diff --git a/plantuml/parser/docs/assets/puml_parser_deployment.svg b/plantuml/parser/docs/assets/puml_parser_deployment.svg new file mode 100644 index 0000000..9afe20a --- /dev/null +++ b/plantuml/parser/docs/assets/puml_parser_deployment.svg @@ -0,0 +1 @@ +PUML Multi-File Parser - Deployment DiagramPUML Multi-File Parser - Deployment DiagramPUML Multi-File Parser RuntimePest GrammarInput FilesFlatBuffers SchemaoutputPumlFileParserComponentResolverComponentSerializerclass.pestcomponent.pestsequence.pestcommon.pestinclude.pestprocedure.pestPUML Sources (*.puml)FlatBuffers Schema (.fbs)BinaryFlatBufferFile (.fbs.bin)LobsterTraceFile (.lobster)consumed bygrammarASTLogicComponentschemawriteswrites diff --git a/plantuml/parser/docs/tool_requirements.trlc b/plantuml/parser/docs/tool_requirements.trlc new file mode 100644 index 0000000..a34d7b1 --- /dev/null +++ b/plantuml/parser/docs/tool_requirements.trlc @@ -0,0 +1,94 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +package Tools + +section "Tool Requirements" { + + section "PlantUML Parser" { + + section "General" { + + ToolRequirement ArchitectureModelingSyntax { + description = '''All PlantUML diagrams shall be checked automatically for syntactical correctness during build process and shall fail errors are detected''' + } + + } + + section "Component Diagram" { + + ToolRequirement ArchitectureModelingComponentContentSEooC { + description = '''For PlantUML Component Diagrams the contributor shall define SEooC elements according to the notation:: + package "SEooC" as SampleSEooC <>''' + } + + ToolRequirement ArchitectureModelingComponentContentComponent { + description = '''For PlantUML Component Diagrams the contributor shall define Software Components according to the notation:: + component "SampleComponent" as CompSample <>''' + } + + ToolRequirement ArchitectureModelingComponentContentSWUnit { + description = '''For PlantUML component diagrams the contributor shall define software units according to the notation:: + component "Sample Unit" as UnitSample <>''' + } + + ToolRequirement ArchitectureModelingComponentContentAbstractInterface { + description = '''For PlantUML component diagrams the contributor shall define abstract interfaces according to the notation:: + interface "Sample Interface" as IfSampleInterface''' + } + + ToolRequirement ArchitectureModelingComponentHierarchySEooC { + description = '''The tooling shall verify during traceability build that a contributor defined the hierarchy within a SEooC via nesting:: + - SEooC { Component[1..n] }''' + } + + ToolRequirement ArchitectureModelingComponentHierarchyComponent { + description = '''The tooling shall verify during traceability build that a contributor defined the hierarchy within a software component via nesting:: + - Component { + Component[1..n] + Unit[1..n] + }''' + } + + ToolRequirement ArchitectureModelingComponentInteract { + description = '''The tooling shall verify during traceability build that a contributor defined the relations between software units via interfaces:: + - Unit --() Interface : provides + - Unit --( Interface : requires''' + } + + } + + section "Sequence Diagram" { + + ToolRequirement ArchitectureModelingSequenceContentActors { + description = '''The tooling shall verify during traceability build that a contributor defined actors in sequence diagrams according to the notation:: + actor "Sample Actor" as ActorSample''' + } + + ToolRequirement ArchitectureModelingSequenceContentSWUnits { + description = '''The tooling shall verify during traceability build that a contributor defined software units in sequence diagrams according to the notation:: + participant "Sample Unit" as UnitSample <>''' + } + + ToolRequirement ArchitectureModelingSequenceContentMessages { + description = '''The tooling shall verify during traceability build that a contributor defined messages in sequence diagrams according to the notation:: + UnitSample -> UnitAnotherSample : MethodA()''' + } + + ToolRequirement ArchitectureModelingSequenceContentActivity { + description = '''The tooling shall verify during traceability build that a contributor defined activity bars in sequence diagrams to indicate concurrency according to the notation:: + activate UnitSample / deactivate UnitSample''' + } + + } + } +} diff --git a/plantuml/parser/integration_test/BUILD b/plantuml/parser/integration_test/BUILD new file mode 100644 index 0000000..95b7f00 --- /dev/null +++ b/plantuml/parser/integration_test/BUILD @@ -0,0 +1,33 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "test_framework", + srcs = [ + "src/lib.rs", + "src/test_error_view.rs", + "src/test_framework.rs", + ], + crate_root = "src/lib.rs", + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser", + "//plantuml/parser/puml_resolver", + "//plantuml/parser/puml_utils", + "//tools/metamodel:class_diagram", + "@crates//:serde", + "@crates//:serde_json", + "@crates//:serde_yaml", + ], +) diff --git a/plantuml/parser/integration_test/class_diagram/BUILD b/plantuml/parser/integration_test/class_diagram/BUILD new file mode 100644 index 0000000..26e6983 --- /dev/null +++ b/plantuml/parser/integration_test/class_diagram/BUILD @@ -0,0 +1,21 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +filegroup( + name = "class_diagram_files", + srcs = glob([ + "**/*.puml", + "**/*.json", + "**/*.yaml", + ]), + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/integration_test/class_diagram/class_diagram_negative/class_diagram_negative.puml b/plantuml/parser/integration_test/class_diagram/class_diagram_negative/class_diagram_negative.puml new file mode 100644 index 0000000..a6a0051 --- /dev/null +++ b/plantuml/parser/integration_test/class_diagram/class_diagram_negative/class_diagram_negative.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 class_negative_test + +class User +class Order + +User --> Payment + +package domain { + class User +} + +package infra { + class UserRepository +} + +domain.User --> infra.Database + +@enduml diff --git a/plantuml/parser/integration_test/class_diagram/class_diagram_negative/output.yaml b/plantuml/parser/integration_test/class_diagram/class_diagram_negative/output.yaml new file mode 100644 index 0000000..8a12077 --- /dev/null +++ b/plantuml/parser/integration_test/class_diagram/class_diagram_negative/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 +# ******************************************************************************* +class_diagram_negative.puml: + error: + type: "UnresolvedReference" + fields: + reference: "Payment" diff --git a/plantuml/parser/integration_test/class_diagram/class_diagram_positive/class_diagram_positive.puml b/plantuml/parser/integration_test/class_diagram/class_diagram_positive/class_diagram_positive.puml new file mode 100644 index 0000000..0af1d58 --- /dev/null +++ b/plantuml/parser/integration_test/class_diagram/class_diagram_positive/class_diagram_positive.puml @@ -0,0 +1,128 @@ +' ******************************************************************************* +' 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 class_positive_test + +title Complex Class Diagram With Stereotypes + +skinparam classFontSize 14 +skinparam shadowing false + +left to right direction +top to bottom direction + +package core <> { + + struct Point <> { + x: f32 + y: f32 + } + + struct Size <> { + width: f32 + height: f32 + } + + enum Color <> { + Red = 0 + Green = 1 + Blue = 2 + } + + class Shape <> { + +draw() + +area(): f32 + } + + interface Drawable <> { + +draw() + } + + class Canvas <> { + -shapes: Shape + +add_shape(s: Shape) + +render() + } +} + +package core::geometry <> { + + class Circle <> { + -center: Point + -radius: f32 + +area(): f32 + } + + class Rectangle <> { + -origin: Point + -size: Size + +area(): f32 + } + +} + +package core::geometry::advanced <> { + + class RoundedRectangle { + -corner_radius: f32 + } + +} + +package util <> { + + enum LogLevel { + -De : Debug + +Info : Information + +Warn : Warnning + #Err : Error + } + + class Logger { + +log(level: LogLevel, msg: String) + } + +} + +package api <> { + + class RestApi <> { + +handle_request() + } + +} + +' ====================== +' Relationships +' ====================== + +Shape <|-- Circle : inherit + +Shape <|-- Rectangle + +Rectangle <|-- RoundedRectangle : <> + +Drawable <|.. Shape + +Canvas *-- Shape + +Circle o-- Color + +Rectangle --> Color + +Canvas ..> Logger + +RestApi ..> Canvas : <> + +Circle <--> Rectangle : <> + +@enduml 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 new file mode 100644 index 0000000..769b65a --- /dev/null +++ b/plantuml/parser/integration_test/class_diagram/class_diagram_positive/output.json @@ -0,0 +1,624 @@ +{"class_diagram_positive.puml": + { + "class_positive_test": { + "name": "class_positive_test", + "entities": [ + { + "id": "core.Point", + "name": "Point", + "alias": null, + "parent_id": "core", + "entity_type": "Struct", + "stereotypes": [], + "attributes": [ + { + "name": "x", + "data_type": "f32", + "visibility": "public", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + }, + { + "name": "y", + "data_type": "f32", + "visibility": "public", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core.Size", + "name": "Size", + "alias": null, + "parent_id": "core", + "entity_type": "Struct", + "stereotypes": [], + "attributes": [ + { + "name": "width", + "data_type": "f32", + "visibility": "public", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + }, + { + "name": "height", + "data_type": "f32", + "visibility": "public", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core.Color", + "name": null, + "alias": null, + "parent_id": "core", + "entity_type": "Enum", + "stereotypes": [], + "attributes": [], + "methods": [], + "template_params": [], + "enum_literals": [ + { + "name": "Red", + "visibility": "public", + "value": "0", + "description": null + }, + { + "name": "Green", + "visibility": "public", + "value": "1", + "description": null + }, + { + "name": "Blue", + "visibility": "public", + "value": "2", + "description": null + } + ], + "source_file": null, + "source_line": null + }, + { + "id": "core.Shape", + "name": "Shape", + "alias": null, + "parent_id": "core", + "entity_type": "Class", + "stereotypes": [], + "attributes": [], + "methods": [ + { + "name": "draw", + "return_type": null, + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + }, + { + "name": "area", + "return_type": "f32", + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core.Drawable", + "name": "Drawable", + "alias": null, + "parent_id": "core", + "entity_type": "Interface", + "stereotypes": [], + "attributes": [], + "methods": [ + { + "name": "draw", + "return_type": null, + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core.Canvas", + "name": "Canvas", + "alias": null, + "parent_id": "core", + "entity_type": "Class", + "stereotypes": [], + "attributes": [ + { + "name": "shapes", + "data_type": "Shape", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [ + { + "name": "add_shape", + "return_type": null, + "visibility": "public", + "parameters": [ + { + "name": "s", + "param_type": "Shape", + "default_value": null, + "is_reference": false, + "is_const": false, + "is_variadic": false + } + ], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + }, + { + "name": "render", + "return_type": null, + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core::geometry.Circle", + "name": "Circle", + "alias": null, + "parent_id": "core::geometry", + "entity_type": "Class", + "stereotypes": [], + "attributes": [ + { + "name": "center", + "data_type": "Point", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + }, + { + "name": "radius", + "data_type": "f32", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [ + { + "name": "area", + "return_type": "f32", + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core::geometry.Rectangle", + "name": "Rectangle", + "alias": null, + "parent_id": "core::geometry", + "entity_type": "Class", + "stereotypes": [], + "attributes": [ + { + "name": "origin", + "data_type": "Point", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + }, + { + "name": "size", + "data_type": "Size", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [ + { + "name": "area", + "return_type": "f32", + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "core::geometry::advanced.RoundedRectangle", + "name": "RoundedRectangle", + "alias": null, + "parent_id": "core::geometry::advanced", + "entity_type": "Class", + "stereotypes": [], + "attributes": [ + { + "name": "corner_radius", + "data_type": "f32", + "visibility": "private", + "default_value": null, + "is_static": false, + "is_const": false, + "description": null + } + ], + "methods": [], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "util.LogLevel", + "name": null, + "alias": null, + "parent_id": "util", + "entity_type": "Enum", + "stereotypes": [], + "attributes": [], + "methods": [], + "template_params": [], + "enum_literals": [ + { + "name": "De", + "visibility": "private", + "value": "Debug", + "description": null + }, + { + "name": "Info", + "visibility": "public", + "value": "Information", + "description": null + }, + { + "name": "Warn", + "visibility": "public", + "value": "Warnning", + "description": null + }, + { + "name": "Err", + "visibility": "protected", + "value": "Error", + "description": null + } + ], + "source_file": null, + "source_line": null + }, + { + "id": "util.Logger", + "name": "Logger", + "alias": null, + "parent_id": "util", + "entity_type": "Class", + "stereotypes": [], + "attributes": [], + "methods": [ + { + "name": "log", + "return_type": null, + "visibility": "public", + "parameters": [ + { + "name": "level", + "param_type": "LogLevel", + "default_value": null, + "is_reference": false, + "is_const": false, + "is_variadic": false + }, + { + "name": "msg", + "param_type": "String", + "default_value": null, + "is_reference": false, + "is_const": false, + "is_variadic": false + } + ], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + }, + { + "id": "api.RestApi", + "name": "RestApi", + "alias": null, + "parent_id": "api", + "entity_type": "Class", + "stereotypes": [], + "attributes": [], + "methods": [ + { + "name": "handle_request", + "return_type": null, + "visibility": "public", + "parameters": [], + "template_params": [], + "is_static": false, + "is_const": false, + "is_virtual": false, + "is_abstract": false, + "is_override": false, + "is_constructor": false, + "is_destructor": false + } + ], + "template_params": [], + "enum_literals": [], + "source_file": null, + "source_line": null + } + ], + "containers": [ + { + "id": "core", + "name": "core", + "parent_id": null, + "container_type": "Package" + }, + { + "id": "core::geometry", + "name": "core::geometry", + "parent_id": null, + "container_type": "Package" + }, + { + "id": "core::geometry::advanced", + "name": "core::geometry::advanced", + "parent_id": null, + "container_type": "Package" + }, + { + "id": "util", + "name": "util", + "parent_id": null, + "container_type": "Package" + }, + { + "id": "api", + "name": "api", + "parent_id": null, + "container_type": "Package" + } + ], + "relationships": [ + { + "source": "core::geometry.Circle", + "target": "core.Shape", + "relation_type": "Inheritance", + "label": "inherit", + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core::geometry.Rectangle", + "target": "core.Shape", + "relation_type": "Inheritance", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core::geometry::advanced.RoundedRectangle", + "target": "core::geometry.Rectangle", + "relation_type": "Inheritance", + "label": null, + "stereotype": "extend", + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core.Shape", + "target": "core.Drawable", + "relation_type": "Implementation", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core.Shape", + "target": "core.Canvas", + "relation_type": "Composition", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core.Color", + "target": "core::geometry.Circle", + "relation_type": "Aggregation", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core::geometry.Rectangle", + "target": "core.Color", + "relation_type": "Association", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core.Canvas", + "target": "util.Logger", + "relation_type": "Dependency", + "label": null, + "stereotype": null, + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "api.RestApi", + "target": "core.Canvas", + "relation_type": "Dependency", + "label": null, + "stereotype": "calls", + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + }, + { + "source": "core::geometry.Circle", + "target": "core::geometry.Rectangle", + "relation_type": "Association", + "label": null, + "stereotype": "bidirectional", + "source_multiplicity": null, + "target_multiplicity": null, + "source_role": null, + "target_role": null + } + ], + "source_files": [ + "class_positive_test" + ], + "version": null + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/BUILD b/plantuml/parser/integration_test/component_diagram/BUILD new file mode 100644 index 0000000..f7eba97 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/BUILD @@ -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 +# ******************************************************************************* +filegroup( + name = "component_diagram_files", + srcs = glob([ + "**/*.puml", + # "**/*.yaml", + "**/*.json", + ]), + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/integration_test/component_diagram/n_pkg_comp.puml b/plantuml/parser/integration_test/component_diagram/n_pkg_comp.puml new file mode 100644 index 0000000..5e8ee83 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/n_pkg_comp.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 n_pkg_comp + +package MyPackage { + component MyComponent +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/relation_fqn/output.json b/plantuml/parser/integration_test/component_diagram/relation_fqn/output.json new file mode 100644 index 0000000..2519c98 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_fqn/output.json @@ -0,0 +1,56 @@ +{ + "relation_fqn.puml": + { + "SampleSEooC.ComponentB.UnitB": { + "id": "SampleSEooC.ComponentB.UnitB", + "name": "Unit B", + "alias": "UnitB", + "parent_id": "SampleSEooC.ComponentB", + "comp_type": "Component", + "stereotype": "unit", + "relations": [] + }, + "SampleSEooC.ComponentA.UnitA": { + "id": "SampleSEooC.ComponentA.UnitA", + "name": "Unit A", + "alias": "UnitA", + "parent_id": "SampleSEooC.ComponentA", + "comp_type": "Component", + "stereotype": "unit", + "relations": [ + { + "target": "SampleSEooC.ComponentB.UnitB", + "annotation": "uses", + "relation_type": "None" + } + ] + }, + "SampleSEooC": { + "id": "SampleSEooC", + "name": "Sample SEooC", + "alias": "SampleSEooC", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "SampleSEooC.ComponentA": { + "id": "SampleSEooC.ComponentA", + "name": "Component A", + "alias": "ComponentA", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + }, + "SampleSEooC.ComponentB": { + "id": "SampleSEooC.ComponentB", + "name": "Component B", + "alias": "ComponentB", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/relation_fqn/relation_fqn.puml b/plantuml/parser/integration_test/component_diagram/relation_fqn/relation_fqn.puml new file mode 100644 index 0000000..026b34f --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_fqn/relation_fqn.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 + +package "Sample SEooC" as SampleSEooC #LightBlue { + component "Component A" as ComponentA <> { + component "Unit A" as UnitA <> { + } + } + component "Component B" as ComponentB <> { + component "Unit B" as UnitB <> { + } + } +} + +SampleSEooC.ComponentA.UnitA --> SampleSEooC.ComponentB.UnitB : uses + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/relation_relative_name/output.json b/plantuml/parser/integration_test/component_diagram/relation_relative_name/output.json new file mode 100644 index 0000000..14b0377 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_relative_name/output.json @@ -0,0 +1,55 @@ +{ + "relation_relative_name.puml": { + "SampleSEooC.ComponentB.UnitB": { + "id": "SampleSEooC.ComponentB.UnitB", + "name": "Unit B", + "alias": "UnitB", + "parent_id": "SampleSEooC.ComponentB", + "comp_type": "Component", + "stereotype": "unit", + "relations": [] + }, + "SampleSEooC": { + "id": "SampleSEooC", + "name": "Sample SEooC", + "alias": "SampleSEooC", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "SampleSEooC.ComponentA.UnitA": { + "id": "SampleSEooC.ComponentA.UnitA", + "name": "Unit A", + "alias": "UnitA", + "parent_id": "SampleSEooC.ComponentA", + "comp_type": "Component", + "stereotype": "unit", + "relations": [ + { + "target": "SampleSEooC.ComponentB.UnitB", + "annotation": "uses", + "relation_type": "None" + } + ] + }, + "SampleSEooC.ComponentB": { + "id": "SampleSEooC.ComponentB", + "name": "Component B", + "alias": "ComponentB", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + }, + "SampleSEooC.ComponentA": { + "id": "SampleSEooC.ComponentA", + "name": "Component A", + "alias": "ComponentA", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/relation_relative_name/relation_relative_name.puml b/plantuml/parser/integration_test/component_diagram/relation_relative_name/relation_relative_name.puml new file mode 100644 index 0000000..1220cae --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_relative_name/relation_relative_name.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 + +package "Sample SEooC" as SampleSEooC #LightBlue { + component "Component A" as ComponentA <> { + component "Unit A" as UnitA <> { + } + } + component "Component B" as ComponentB <> { + component "Unit B" as UnitB <> { + } + } + + ComponentA.UnitA --> ComponentB.UnitB : 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 new file mode 100644 index 0000000..09ea7c1 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name/output.json @@ -0,0 +1,55 @@ +{ + "relation_simple_name.puml": { + "SampleSEooC.ComponentB": { + "id": "SampleSEooC.ComponentB", + "name": "Component B", + "alias": "ComponentB", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [] + }, + "SampleSEooC": { + "id": "SampleSEooC", + "name": "Sample SEooC", + "alias": "SampleSEooC", + "parent_id": null, + "comp_type": "Package", + "stereotype": null, + "relations": [] + }, + "SampleSEooC.ComponentA": { + "id": "SampleSEooC.ComponentA", + "name": "Component A", + "alias": "ComponentA", + "parent_id": "SampleSEooC", + "comp_type": "Component", + "stereotype": "component", + "relations": [ + { + "target": "SampleSEooC.ComponentB", + "annotation": "uses", + "relation_type": "None" + } + ] + }, + "SampleSEooC.ComponentB.UnitB": { + "id": "SampleSEooC.ComponentB.UnitB", + "name": "Unit B", + "alias": "UnitB", + "parent_id": "SampleSEooC.ComponentB", + "comp_type": "Component", + "stereotype": "unit", + "relations": [] + }, + "SampleSEooC.ComponentA.UnitA": { + "id": "SampleSEooC.ComponentA.UnitA", + "name": "Unit A", + "alias": "UnitA", + "parent_id": "SampleSEooC.ComponentA", + "comp_type": "Component", + "stereotype": "unit", + "relations": [] + } + } +} 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 new file mode 100644 index 0000000..adff2ba --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_simple_name/relation_simple_name.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 + +package "Sample SEooC" as SampleSEooC #LightBlue { + component "Component A" as ComponentA <> { + component "Unit A" as UnitA <> { + } + } + component "Component B" as ComponentB <> { + component "Unit B" as UnitB <> { + } + } + + ComponentA --> ComponentB : uses +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/simple_component.puml b/plantuml/parser/integration_test/component_diagram/simple_component.puml new file mode 100644 index 0000000..f061871 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/simple_component.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 + +package "Sample SEooC" as SampleSEooC #LightBlue { + component "Component A" as ComponentA <> { + component "Unit A" as UnitA <> { + } + } + component "Component B" as ComponentB <> { + component "Unit B" as UnitB <> { + } + } +} + +ComponentA --> ComponentB : uses + +@enduml diff --git a/plantuml/parser/integration_test/sequence_diagram/BUILD b/plantuml/parser/integration_test/sequence_diagram/BUILD new file mode 100644 index 0000000..15ef9cb --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/BUILD @@ -0,0 +1,35 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +filegroup( + name = "simple_sequence", + srcs = ["simple_sequence.puml"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "comprehensive_sequence_test", + srcs = [ + "comprehensive_sequence_test.json", + "comprehensive_sequence_test.puml", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "sequence_diagram_tests", + srcs = [ + ":comprehensive_sequence_test", + ":simple_sequence", + ], + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.json b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.json new file mode 100644 index 0000000..3501e1a --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.json @@ -0,0 +1,2289 @@ +[ + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "Actor1" + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "Actor2" + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "core::runtime", + "id": "runtime" + } + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "core::service::ResourceBuilder", + "id": "Builder" + } + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "core::messaging", + "id": "messaging" + } + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "<>\\nTaskHandler", + "id": "TaskHandler" + } + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "core::service::ProxyHandle<>", + "id": "ProxyHandle" + } + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "QuotedAsId": { + "quoted": "platform::ServiceHost", + "id": "ServiceHost" + } + }, + "stereotype": null + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Arrow Types" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Standard message" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Return message" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": null + }, + "right": "Actor1" + } + }, + "description": "Reverse message" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "right": "Actor1" + } + }, + "description": "Reverse return" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Incoming request" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "right": "" + } + }, + "description": "Outgoing response" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Activation Patterns" + } + }, + { + "ActivateCmd": { + "participant": "Actor1" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Activate target" + } + }, + { + "ActivateCmd": { + "participant": "Actor2" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Nested call" + } + }, + { + "ActivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": null + } + }, + { + "DeactivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": null + } + }, + { + "DeactivateCmd": { + "participant": "Actor2" + } + }, + { + "DeactivateCmd": { + "participant": "Actor1" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Lifecycle Events" + } + }, + { + "CreateCmd": { + "participant": "TaskHandler" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "TaskHandler" + } + }, + "description": "create" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "TaskHandler", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Self Messages" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Internal operation" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "runtime::wait_for_shutdown()" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Message with inline note" + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "configuration is cached" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Cached result" + } + }, + { + "GroupCmd": { + "group_type": "Else", + "text": "configuration needs to be fetched" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": "Fetch from service" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "messaging", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Result" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "system is in restricted mode and the access token is invalid\\n or it does not match the request" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "TaskHandler", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Error response" + } + }, + { + "GroupCmd": { + "group_type": "Else", + "text": "access token is valid" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "TaskHandler", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Success response" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "For each item" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Process item" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "loop" + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "(item_list) [Call FetchValue for each item]" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "FetchValue(item)" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "0..2 {0..3000ms}" + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "timeout not reached" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Retry" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "For Segment in Segments" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Process segment" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "Until success" + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "condition met" + } + }, + { + "GroupCmd": { + "group_type": "Break", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Continue processing" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Complex Messages" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Run(config_collection, callback, stop_token, error_reporter)" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "right": "TaskHandler" + } + }, + "description": "StartResponse::ValidationResult::kAccepted (0x00)" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "ProxyHandle" + } + }, + "description": "std::unique_ptr>" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "PerformQualification() [[https:" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Complex Flow with All Elements" + } + }, + { + "ActivateCmd": { + "participant": "runtime" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "runtime", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": "Initialize(context)" + } + }, + { + "ActivateCmd": { + "participant": "messaging" + } + }, + { + "CreateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "messaging", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "create" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "messaging", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Configure(params)" + } + }, + { + "ActivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "ValidateConfiguration()" + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "configuration is valid" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": "Success" + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "For each service" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "messaging", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "ServiceHost" + } + }, + "description": "StartService()" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "ServiceHost", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "Else", + "text": "configuration invalid" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "messaging" + } + }, + "description": "Error" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "DeactivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "messaging", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "runtime" + } + }, + "description": "ExitCodeOk" + } + }, + { + "DeactivateCmd": { + "participant": "messaging" + } + }, + { + "DeactivateCmd": { + "participant": "runtime" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Construction stage" + } + }, + { + "CreateCmd": { + "participant": "Actor1" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "runtime", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "create" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Initialization stage" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "runtime", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Initialize()" + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "system ready" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "StartServices()" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Run stage" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "runtime", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Run(stop_token)" + } + }, + { + "GroupCmd": { + "group_type": "Loop", + "text": "Until stop requested" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "ProcessEvents()" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Edge Cases" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": null + } + }, + { + "GroupCmd": { + "group_type": "Alt", + "text": "condition" + } + }, + { + "GroupCmd": { + "group_type": "Else", + "text": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Default action" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": null + } + }, + { + "ActivateCmd": { + "participant": "Actor1" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Call1" + } + }, + { + "ActivateCmd": { + "participant": "Actor2" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Call2" + } + }, + { + "ActivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "TaskHandler" + } + }, + "description": "Call3" + } + }, + { + "ActivateCmd": { + "participant": "TaskHandler" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "TaskHandler", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": null + } + }, + { + "DeactivateCmd": { + "participant": "TaskHandler" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": null + } + }, + { + "DeactivateCmd": { + "participant": "Builder" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": null + } + }, + { + "DeactivateCmd": { + "participant": "Actor2" + } + }, + { + "DeactivateCmd": { + "participant": "Actor1" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Preprocessor Functions" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "$redtext(\"Error message\")" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "$greentext(\"Success message\")" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Participants with Colors" + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "ColoredActor1" + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "ColoredActor2" + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "ColoredActor3" + }, + "stereotype": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "ColoredActor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "ColoredActor2" + } + }, + "description": "Message" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "ColoredActor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "ColoredActor3" + } + }, + "description": "Another message" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Boxes with Colors" + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "BoxedActor1" + }, + "stereotype": null + } + }, + { + "ParticipantDef": { + "participant_type": "Participant", + "identifier": { + "Id": "BoxedActor2" + }, + "stereotype": null + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "BoxedActor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "BoxedActor2" + } + }, + "description": "Message inside box" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Lost and Found Messages" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "[o", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Incoming request" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "[", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Another incoming" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "[x", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Third variant" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "o]" + } + }, + "description": "Outgoing response" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "]" + } + }, + "description": "Another outgoing" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "x]" + } + }, + "description": "Third variant" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "[o", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Right bracket on left" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "o[" + } + }, + "description": "Left bracket on right" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Activation Markers" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "activation_marker": "++", + "description": "Activate with message" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "activation_marker": "++", + "description": "Nested activation" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "activation_marker": "--", + "description": "Deactivate with response" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "activation_marker": "--", + "description": "Deactivate outer" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "TaskHandler" + } + }, + "activation_marker": "**", + "description": "Create and activate" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "activation_marker": "!!", + "description": "Destroy marker" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Ref Blocks" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Dividers with URLs" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Initialize" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Builder" + } + }, + "description": "Process" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Builder", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Verify" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + }, + { + "GroupCmd": { + "group_type": "Group", + "text": "Extended Patterns: Standalone Ellipsis" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor1", + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor2" + } + }, + "description": "Start process" + } + }, + { + "Message": { + "content": { + "WithTargets": { + "left": "Actor2", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": "Actor1" + } + }, + "description": "Complete" + } + }, + { + "GroupCmd": { + "group_type": "End", + "text": "group" + } + } +] diff --git a/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml new file mode 100644 index 0000000..d9f680f --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml @@ -0,0 +1,387 @@ +' ******************************************************************************* +' 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 + +' Comprehensive PlantUML Sequence Diagram Test +' This file contains a wide range of supported syntax patterns + +!pragma teoz true +skinparam SequenceMessageAlignment center +title Comprehensive Sequence Diagram Test Suite + +' ======================================== +' PARTICIPANT DECLARATIONS - All Variations +' ======================================== + +' Simple unquoted participants +participant Actor1 +participant Actor2 + +' Participants with aliases using quotes and special characters +participant "core::runtime" as runtime +participant "core::service::ResourceBuilder" as Builder +participant "core::messaging" as messaging + +' Participants with stereotypes and newlines +participant "<>\nTaskHandler" as TaskHandler + +' Participants with namespaces and generics +participant "core::service::ProxyHandle<>" as ProxyHandle +participant "platform::ServiceHost" as ServiceHost + +' ======================================== +' ARROW TYPES - All Variations +' ======================================== + +group Arrow Types + ' Standard solid arrow + Actor1 -> Actor2 : Standard message + + ' Dashed return arrow + Actor2 --> Actor1 : Return message + + ' Left-pointing arrows + Actor2 <- Actor1 : Reverse message + Actor2 <-- Actor1 : Reverse return + --> Actor1 : Incoming request + Actor1 <-- : Outgoing response + + ' ' Hidden arrow for layout + ' Actor1 -[hidden]-> Actor1 +end group + +' ======================================== +' ACTIVATION - All Patterns +' ======================================== + +group Activation Patterns + ' Standard activation + activate Actor1 + Actor1 -> Actor2 : Activate target + activate Actor2 + + ' Nested activations + Actor2 -> Builder : Nested call + activate Builder + Builder --> Actor2 + deactivate Builder + + Actor2 --> Actor1 + deactivate Actor2 + deactivate Actor1 +end group + +' ======================================== +' LIFECYCLE - Create +' ======================================== + +group Lifecycle Events + create TaskHandler + Actor1 -> TaskHandler : create + TaskHandler --> Actor1 +end group + +' ======================================== +' SELF-MESSAGES +' ======================================== + +group Self Messages + Actor1 -> Actor1 : Internal operation + Actor1 -> Actor1 : runtime::wait_for_shutdown() +end group + +' ======================================== +' NOTES - All Variations +' ======================================== + +note left + Multiline note on left + Second line + Third line +end note + +note right + Multiline note on right +end note + +note right of Builder #LightBlue +Note with background color +end note + +note over ProxyHandle #Orange +Note spanning participant +with colored background +-- +Separator line +end note + +Actor1 -> Actor2 : Message with inline note +note left: Single-line inline note + +' ======================================== +' GROUPS - Alt with multiple conditions +' ======================================== + +alt configuration is cached + Actor1 --> Actor2 : Cached result +else configuration needs to be fetched + Actor1 -> messaging : Fetch from service + messaging --> Actor1 : Result +end + +alt system is in restricted mode and the access token is invalid\n or it does not match the request + TaskHandler --> Actor1 : Error response +else access token is valid + TaskHandler --> Actor1 : Success response +end + +' ======================================== +' LOOPS - All Variations +' ======================================== + +loop For each item + Actor1 -> Actor2 : Process item +end loop + +loop (item_list) [Call FetchValue for each item] + Actor2 -> Builder : FetchValue(item) +end + +loop 0..2 {0..3000ms} + alt timeout not reached + Actor1 -> Actor2 : Retry + end +end + +loop For Segment in Segments + Actor2 -> Actor2 : Process segment +end + +' ======================================== +' BREAK Statement +' ======================================== + +loop Until success + alt condition met + break + end + Actor1 -> Actor2 : Continue processing +end + +' ======================================== +' COMPLEX MESSAGE LABELS +' ======================================== + +group Complex Messages + ' Long method signatures + Actor1 -> Builder : Run(config_collection, callback, stop_token, error_reporter) + + ' Return values with enum codes + Builder <-- TaskHandler : StartResponse::ValidationResult::kAccepted (0x00) + + ' Generic types + Actor1 -> ProxyHandle : std::unique_ptr> + + ' Hyperlink in message (not visible in SVG but valid syntax) + Actor2 -> Actor2 : PerformQualification() [[https://example.com/diagram View Activity Diagram]] +end group + +' ======================================== +' MULTIPLE PARTICIPANTS IN COMPLEX FLOW +' ======================================== + +group Complex Flow with All Elements + activate runtime + runtime -> messaging : Initialize(context) + activate messaging + + create Builder + messaging -> Builder : create + Builder --> messaging + + messaging -> Builder : Configure(params) + activate Builder + + Builder -> Builder : ValidateConfiguration() + + alt configuration is valid + Builder --> messaging : Success + + loop For each service + messaging -> ServiceHost : StartService() + ServiceHost --> messaging + end + else configuration invalid + Builder --> messaging : Error + note right: Configuration failed + end + + deactivate Builder + messaging --> runtime : ExitCodeOk + deactivate messaging + deactivate runtime +end group + +' ======================================== +' NESTED GROUPS +' ======================================== + +group Construction stage + create Actor1 + runtime -> Actor1 : create +end group + +group Initialization stage + runtime -> Actor1 : Initialize() + + alt system ready + Actor1 -> Actor2 : StartServices() + end +end group + +group Run stage + runtime -> Actor1 : Run(stop_token) + + loop Until stop requested + Actor1 -> Actor1 : ProcessEvents() + end +end group + +' ======================================== +' EDGE CASES +' ======================================== + +group Edge Cases + ' Message with no label + Actor1 -> Actor2 + Actor2 --> Actor1 + + ' Empty alt branch + alt condition + else + Actor1 -> Actor2 : Default action + end + + ' Deeply nested activations + activate Actor1 + Actor1 -> Actor2 : Call1 + activate Actor2 + Actor2 -> Builder : Call2 + activate Builder + Builder -> TaskHandler : Call3 + activate TaskHandler + TaskHandler --> Builder + deactivate TaskHandler + Builder --> Actor2 + deactivate Builder + Actor2 --> Actor1 + deactivate Actor2 + deactivate Actor1 +end group + +' ======================================== +' EXTENDED SYNTAX PATTERNS +' ======================================== + +group Extended Patterns: Preprocessor Functions + ' PlantUML preprocessor functions for colored text + !function $redtext($a) + !return "" + $a + "" + !endfunction + + !function $greentext($a) + !return "" + $a + "" + !endfunction + + Actor1 -> Actor2 : $redtext("Error message") + Actor2 --> Actor1 : $greentext("Success message") +end group + +group Extended Patterns: Participants with Colors + ' Participants with color specifications + participant ColoredActor1 #LightSkyBlue + participant ColoredActor2 #Lavender + participant ColoredActor3 #MediumSpringGreen + + ColoredActor1 -> ColoredActor2 : Message + ColoredActor2 -> ColoredActor3 : Another message +end group + +group Extended Patterns: Boxes with Colors + box "Test Box" #White + participant BoxedActor1 + participant BoxedActor2 + end box + + BoxedActor1 -> BoxedActor2 : Message inside box +end group + +group Extended Patterns: Lost and Found Messages + ' Lost messages (no source participant) + [o-> Actor1 : Incoming request + [-> Actor2 : Another incoming + [x-> Builder : Third variant + + ' Found messages (no target participant) + Actor1 -->o] : Outgoing response + Actor2 -->] : Another outgoing + Actor2 -->x] : Third variant + + ' Lost/found with brackets on different sides + [o-> Actor1 : Right bracket on left + Actor1 -->o[ : Left bracket on right +end group + +group Extended Patterns: Activation Markers + ' Activation markers on messages + Actor1 -> Actor2++ : Activate with message + Actor2 -> Builder++ : Nested activation + Builder --> Actor2-- : Deactivate with response + Actor2 --> Actor1-- : Deactivate outer + + ' Creation activation + Actor1 -> TaskHandler** : Create and activate + + ' Destruction marker + Actor1 -> Actor2!! : Destroy marker +end group + +group Extended Patterns: Ref Blocks + ref over Actor1, Actor2 + See other diagram for details + [[https://example.com/reference Reference document]] + end ref + + ref over Builder + Another reference block + end +end group + +group Extended Patterns: Dividers with URLs + == Setup Phase == + Actor1 -> Actor2 : Initialize + + == Processing https://example.com/docs == + Actor2 -> Builder : Process + + == Verification\n(see https://issues.example.com/tickets/123) == + Builder -> Actor1 : Verify +end group + +group Extended Patterns: Standalone Ellipsis + Actor1 -> Actor2 : Start process + ... + Actor2 --> Actor1 : Complete +end group + +@enduml diff --git a/plantuml/parser/integration_test/sequence_diagram/simple_sequence.puml b/plantuml/parser/integration_test/sequence_diagram/simple_sequence.puml new file mode 100644 index 0000000..1be484a --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/simple_sequence.puml @@ -0,0 +1,32 @@ +' ******************************************************************************* +' 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 + +participant "Component A" as ComponentA <> +participant "Component B" as ComponentB <> +participant "Component C" as ComponentC <> + +ComponentA -> ComponentB : Call Method1() +alt Condition1 + ComponentB --> ComponentA : Return Result +else + ComponentB -> ComponentC : Call Method2() + ComponentC --> ComponentB : Return Result + alt Condition2 + ComponentB -> ComponentC: Call Method2() + ComponentC --> ComponentB : Return Result + end + ComponentB --> ComponentA : Return Result +end + +@enduml diff --git a/plantuml/parser/integration_test/src/lib.rs b/plantuml/parser/integration_test/src/lib.rs new file mode 100644 index 0000000..45fac08 --- /dev/null +++ b/plantuml/parser/integration_test/src/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +mod test_error_view; +mod test_framework; + +pub use test_error_view::{ErrorView, ProjectedError}; +pub use test_framework::{ + run_case, DefaultExpectationChecker, DiagramProcessor, ExpectationChecker, +}; diff --git a/plantuml/parser/integration_test/src/test_error_view.rs b/plantuml/parser/integration_test/src/test_error_view.rs new file mode 100644 index 0000000..44fb07e --- /dev/null +++ b/plantuml/parser/integration_test/src/test_error_view.rs @@ -0,0 +1,247 @@ +// ******************************************************************************* +// 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 std::collections::HashMap; +use std::path::Path; + +use class_diagram::ClassResolverError; +use puml_parser::{ + BaseParseError, ClassError, IncludeExpandError, IncludeParseError, PreprocessError, + ProcedureExpandError, ProcedureParseError, +}; +use puml_resolver::ComponentResolverError; + +#[derive(Debug)] +pub struct ProjectedError { + pub kind: String, + pub fields: HashMap, + pub source: Option>, +} + +impl ProjectedError { + pub fn new(kind: impl Into) -> Self { + Self { + kind: kind.into(), + fields: HashMap::new(), + source: None, + } + } + + pub fn with_field(mut self, k: &str, v: impl Into) -> Self { + self.fields.insert(k.to_string(), v.into()); + self + } + + pub fn with_source(mut self, src: ProjectedError) -> Self { + self.source = Some(Box::new(src)); + self + } +} + +pub trait ErrorView { + fn project(&self, base_dir: &Path) -> ProjectedError; +} + +fn relative_path(path: &Path, dir: &Path) -> String { + path.strip_prefix(dir) + .unwrap_or(path) + .to_string_lossy() + .to_string() +} + +impl ErrorView for BaseParseError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + BaseParseError::IoError { path, .. } => { + ProjectedError::new("IoError").with_field("path", relative_path(path, base_dir)) + } + + BaseParseError::SyntaxError { + file, + line, + column, + message, + source_line, + cause: _, + } => ProjectedError::new("SyntaxError") + .with_field("file", relative_path(file, base_dir)) + .with_field("line", line.to_string()) + .with_field("column", column.to_string()) + .with_field("message", message.clone()) + .with_field("source_line", source_line.clone()), + } + } +} + +impl ErrorView for IncludeParseError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + IncludeParseError::Base(e) => e.project(base_dir), + + IncludeParseError::InvalidTextLine { line, file } => { + ProjectedError::new("InvalidTextLine") + .with_field("line", line.clone()) + .with_field("file", relative_path(file, base_dir)) + } + } + } +} + +impl ErrorView for IncludeExpandError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + IncludeExpandError::FileNotFound { file } => ProjectedError::new("FileNotFound") + .with_field("file", relative_path(file, base_dir)), + + IncludeExpandError::ParseFailed { file, error } => ProjectedError::new("ParseFailed") + .with_field("file", relative_path(file, base_dir)) + .with_source(error.project(base_dir)), + + IncludeExpandError::CycleInclude { chain } => { + let chain_str = chain + .iter() + .map(|p| relative_path(p, base_dir)) + .collect::>() + .join(" -> "); + + ProjectedError::new("CycleInclude").with_field("chain", chain_str) + } + + IncludeExpandError::IncludeOnceViolated { file, conflict } => { + ProjectedError::new("IncludeOnceViolated") + .with_field("file", relative_path(file, base_dir)) + .with_field("conflict", relative_path(conflict, base_dir)) + } + + IncludeExpandError::UnknownSub { suffix, file } => ProjectedError::new("UnknownSub") + .with_field("file", relative_path(file, base_dir)) + .with_field("suffix", suffix.clone()), + } + } +} + +impl ErrorView for ProcedureParseError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + ProcedureParseError::Base(e) => e.project(base_dir), + } + } +} + +impl ErrorView for ProcedureExpandError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + ProcedureExpandError::ParseFailed { file, error } => ProjectedError::new("ParseFailed") + .with_field("file", relative_path(file, base_dir)) + .with_source(error.project(base_dir)), + + ProcedureExpandError::MacroNotDefined(name) => { + ProjectedError::new("MacroNotDefined").with_field("name", name.clone()) + } + + ProcedureExpandError::ArgumentMismatch { + name, + expected, + actual, + } => ProjectedError::new("ArgumentMismatch") + .with_field("name", name.clone()) + .with_field("expected", expected.to_string()) + .with_field("actual", actual.to_string()), + + ProcedureExpandError::RecursiveMacro { chain, name } => { + let chain_str = chain.join(" -> "); + ProjectedError::new("RecursiveMacro") + .with_field("chain", chain_str) + .with_field("name", name.clone()) + } + + ProcedureExpandError::MaxDepthExceeded => ProjectedError::new("MaxDepthExceeded"), + } + } +} + +impl ErrorView for PreprocessError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + PreprocessError::IncludeFailed(e) => e.project(base_dir), + PreprocessError::ProcedureFailed(e) => e.project(base_dir), + } + } +} + +impl ErrorView for ClassError { + fn project(&self, base_dir: &Path) -> ProjectedError { + match self { + ClassError::Base(e) => e.project(base_dir), + } + } +} + +impl ErrorView for ComponentResolverError { + fn project(&self, _base_dir: &Path) -> ProjectedError { + match self { + ComponentResolverError::UnresolvedReference { reference } => { + ProjectedError::new("UnresolvedReference") + .with_field("reference", reference.clone()) + } + + ComponentResolverError::DuplicateComponent { component_id } => { + ProjectedError::new("DuplicateComponent") + .with_field("component_id", component_id.clone()) + } + + ComponentResolverError::UnknownComponentType { component_type } => { + ProjectedError::new("UnknownComponentType") + .with_field("component_type", component_type.clone()) + } + } + } +} + +impl ErrorView for ClassResolverError { + fn project(&self, _base_dir: &Path) -> ProjectedError { + match self { + ClassResolverError::UnresolvedReference { reference } => { + ProjectedError::new("UnresolvedReference") + .with_field("reference", reference.clone()) + } + + ClassResolverError::DuplicateEntity { entity_id } => { + ProjectedError::new("DuplicateEntity").with_field("entity_id", entity_id.clone()) + } + + ClassResolverError::UnknownEntityType { entity_type } => { + ProjectedError::new("UnknownEntityType") + .with_field("entity_type", entity_type.clone()) + } + + ClassResolverError::InvalidRelationship { from, to, reason } => { + ProjectedError::new("InvalidRelationship") + .with_field("from", from.clone()) + .with_field("to", to.clone()) + .with_field("reason", reason.clone()) + } + + ClassResolverError::CircularInheritance { cycle } => { + ProjectedError::new("CircularInheritance").with_field("cycle", cycle.clone()) + } + + ClassResolverError::InvalidVisibility { modifier } => { + ProjectedError::new("InvalidVisibility").with_field("modifier", modifier.clone()) + } + + ClassResolverError::ParseError { message } => { + ProjectedError::new("ParseError").with_field("message", message.clone()) + } + } + } +} diff --git a/plantuml/parser/integration_test/src/test_framework.rs b/plantuml/parser/integration_test/src/test_framework.rs new file mode 100644 index 0000000..71c77b7 --- /dev/null +++ b/plantuml/parser/integration_test/src/test_framework.rs @@ -0,0 +1,264 @@ +// ******************************************************************************* +// 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 serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value as JsonValue; +use serde_yaml::Value as YamlValue; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use crate::test_error_view::{ErrorView, ProjectedError}; + +// =================== Common Functions =================== +fn get_case_dir(test_module: &str, case_name: &str) -> PathBuf { + let runfiles_dir = std::env::var("TEST_SRCDIR").unwrap(); + let workspace = std::env::var("TEST_WORKSPACE").unwrap(); + + PathBuf::from(format!( + "{}/{}/plantuml/parser/{}/{}", + runfiles_dir, workspace, test_module, case_name + )) +} + +fn get_puml_files(dir: &Path) -> HashSet> { + fs::read_dir(dir) + .unwrap() + .filter_map(|e| { + let path = e.unwrap().path(); + if path.extension().map(|s| s == "puml").unwrap_or(false) { + Some(Rc::new(path)) + } else { + None + } + }) + .collect() +} + +// =================== Helper: normalize YAML text =================== +fn normalize_yaml_text(s: &str) -> String { + s.lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>() + .join("\n") + + "\n" +} + +// =================== Golden File =================== +enum GoldenValue { + Yaml(YamlValue), + Json(JsonValue), +} + +fn load_golden_file(test_module: &str, case_name: &str) -> GoldenValue { + let dir = get_case_dir(test_module, case_name); + + let yaml_path = dir.join("output.yaml"); + let yaml_exists = yaml_path.exists(); + + let json_path = dir.join("output.json"); + let json_exists = json_path.exists(); + + if yaml_exists && json_exists { + panic!( + "Both output.yaml and output.json exist in {:?}. Please keep only one.", + dir + ); + } + + if yaml_exists { + let content = fs::read_to_string(&yaml_path).unwrap(); + return GoldenValue::Yaml(serde_yaml::from_str(&content).unwrap()); + } + + if json_exists { + let content = fs::read_to_string(&json_path).unwrap(); + return GoldenValue::Json(serde_json::from_str(&content).unwrap()); + } + + panic!( + "No golden file found in {:?}. Expected output.yaml or output.json", + dir + ); +} + +pub enum Expected { + Text(String), + Ast(Output), +} + +fn materialize_expected(value: &GoldenValue) -> Expected +where + Output: DeserializeOwned, +{ + match value { + GoldenValue::Yaml(v) => match v { + YamlValue::String(s) => Expected::Text(normalize_yaml_text(s)), + _ => { + panic!( + "YAML golden only supports string output, \ + use JSON if you want to compare AST" + ); + } + }, + GoldenValue::Json(v) => { + let ast: Output = serde_json::from_value(v.clone()) + .expect("Failed to deserialize expected AST from JSON"); + Expected::Ast(ast) + } + } +} + +// =================== DiagramProcessor =================== +pub trait DiagramProcessor { + type Output; + type Error; + + fn run( + &self, + files: &HashSet>, + ) -> Result, Self::Output>, Self::Error>; +} + +// =================== ExpectationChecker =================== +pub trait ExpectationChecker { + fn check_ok(&self, actual: &Output, expected: &Expected); + fn check_err(&self, err: &Error, expected: &YamlValue, base_dir: &Path); +} + +// =================== Default Checker =================== +pub struct DefaultExpectationChecker; + +impl ExpectationChecker for DefaultExpectationChecker +where + Error: ErrorView, + Output: Serialize + PartialEq + std::fmt::Debug, +{ + fn check_ok(&self, actual: &Output, expected: &Expected) { + match expected { + Expected::Text(expected_text) => { + let actual_str = match serde_yaml::to_value(actual) { + Ok(YamlValue::String(s)) => s, + _ => { + format!("{:?}", actual) + } + }; + + assert_eq!( + normalize_yaml_text(&actual_str), + normalize_yaml_text(expected_text), + "String output mismatch" + ); + } + Expected::Ast(expected_ast) => { + assert_eq!(actual, expected_ast, "AST output mismatch"); + } + } + } + + fn check_err(&self, err: &Error, expected: &YamlValue, base_dir: &Path) { + let projected = err.project(base_dir); + assert_projected_error_matches_yaml(&projected, expected); + } +} + +// =================== Error Matcher =================== +fn assert_projected_error_matches_yaml(err: &ProjectedError, expected: &YamlValue) { + let expected_type = expected + .get("type") + .and_then(|v| v.as_str()) + .expect("Missing error.type"); + + assert_eq!(err.kind, expected_type, "Error kind mismatch"); + + if let Some(fields) = expected.get("fields") { + let fields = fields.as_mapping().expect("fields must be a map"); + + for (k, v) in fields { + let key = k.as_str().unwrap(); + let expected_val = v.as_str().unwrap(); + + let actual_val = err + .fields + .get(key) + .unwrap_or_else(|| panic!("Missing field: {}", key)); + + assert_eq!(actual_val, expected_val, "Field '{}' mismatch", key); + } + } + + match (expected.get("source"), &err.source) { + (Some(expected_src), Some(actual_src)) => { + assert_projected_error_matches_yaml(actual_src, expected_src); + } + (None, None) => {} + (Some(_), None) => panic!("Expected source error, but none found"), + (None, Some(_)) => panic!("Unexpected source error"), + } +} + +// =================== Test Driver =================== +pub fn run_case(test_module: &str, case_name: &str, processor: P, checker: C) +where + P: DiagramProcessor, + P::Error: std::fmt::Debug + ErrorView, + P::Output: std::fmt::Debug + Serialize + DeserializeOwned + PartialEq, + C: ExpectationChecker, +{ + let dir = get_case_dir(test_module, case_name); + let file_list = get_puml_files(&dir); + + let result = processor.run(&file_list); + let expected = load_golden_file(test_module, case_name); + + match expected { + GoldenValue::Yaml(ref yaml) => { + for (key_value, expected_value) in yaml.as_mapping().unwrap() { + let key = key_value.as_str().unwrap(); + let path = dir.join(key); + + if let Some(error_obj) = expected_value.get("error") { + let err = result + .as_ref() + .err() + .unwrap_or_else(|| panic!("Expected error for {}", key)); + checker.check_err(err, error_obj, &dir); + } else { + let actual = result + .as_ref() + .unwrap() + .get(&path) + .unwrap_or_else(|| panic!("Missing output for {}", key)); + let expected = materialize_expected::(&GoldenValue::Yaml( + expected_value.clone(), + )); + checker.check_ok(actual, &expected); + } + } + } + GoldenValue::Json(ref json) => { + for (key, expected_value) in json.as_object().unwrap() { + let path = dir.join(key); + let actual = result + .as_ref() + .unwrap() + .get(&path) + .unwrap_or_else(|| panic!("Missing output for {}", key)); + let expected = + materialize_expected::(&GoldenValue::Json(expected_value.clone())); + checker.check_ok(actual, &expected); + } + } + } +} diff --git a/plantuml/parser/puml_cli/BUILD b/plantuml/parser/puml_cli/BUILD new file mode 100644 index 0000000..d121227 --- /dev/null +++ b/plantuml/parser/puml_cli/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_binary") + +rust_binary( + name = "puml_cli", + srcs = ["src/main.rs"], + crate_root = "src/main.rs", + visibility = ["//visibility:public"], + deps = [ + "//plantuml/parser/puml_lobster", + "//plantuml/parser/puml_parser", + "//plantuml/parser/puml_resolver", + "//plantuml/parser/puml_serializer", + "//plantuml/parser/puml_utils", + "@crates//:clap", + "@crates//:env_logger", + "@crates//:log", + "@crates//:serde", + ], +) diff --git a/plantuml/parser/puml_cli/src/main.rs b/plantuml/parser/puml_cli/src/main.rs new file mode 100644 index 0000000..649b1fa --- /dev/null +++ b/plantuml/parser/puml_cli/src/main.rs @@ -0,0 +1,416 @@ +// ******************************************************************************* +// 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 clap::{ArgGroup, Parser, ValueEnum}; +use env_logger::Builder; +use log::{debug, error, warn}; +use serde::Serialize; +use std::collections::HashMap; +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use puml_lobster::write_lobster_to_file; +use puml_parser::{ + DiagramParser, Preprocessor, PumlClassParser, PumlComponentParser, PumlSequenceParser, +}; +use puml_resolver::{ComponentResolver, DiagramResolver}; +use puml_serializer::ComponentSerializer; +use puml_utils::{write_fbs_to_file, write_json_to_file, write_placeholder_file, LogLevel}; + +/// CLI wrapper for LogLevel that implements ValueEnum +#[derive(Copy, Clone, ValueEnum, Debug)] +enum CliLogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for LogLevel { + fn from(cli_level: CliLogLevel) -> Self { + match cli_level { + CliLogLevel::Error => LogLevel::Error, + CliLogLevel::Warn => LogLevel::Warn, + CliLogLevel::Info => LogLevel::Info, + CliLogLevel::Debug => LogLevel::Debug, + CliLogLevel::Trace => LogLevel::Trace, + } + } +} + +/// PlantUML parser CLI tool +#[derive(Parser, Debug)] +#[command(name = "puml_parser_cli")] +#[command(version = "1.0")] +#[command(about = "Parse and analyze PlantUML component diagrams", long_about = None)] +#[command(group( + ArgGroup::new("input") + .required(true) + .multiple(true) + .args(&["file", "folders"]), +))] +struct Args { + /// One or more PUML files to parse (can be repeated) + #[arg(long)] + file: Vec, + + /// Folder containing PUML files + #[arg(long)] + folders: Option, + + /// Log level: error, warn, info, debug, trace + #[arg(long, value_enum, default_value = "warn")] + log_level: CliLogLevel, + + /// Specify Grammar / Diagram type explicitly + #[arg(long, value_enum, default_value = "none")] + diagram_type: DiagramType, + + /// Output directory for generated FlatBuffers binary files. + /// When omitted, no FlatBuffers files are written. + #[arg(long)] + fbs_output_dir: Option, + + /// 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. + #[arg(long)] + lobster_output_dir: Option, +} + +#[derive(Copy, Clone, ValueEnum, Debug)] +enum DiagramType { + None, + Component, + Class, + Sequence, +} + +#[allow(dead_code)] // Class and Sequence variants are WIP +#[derive(Debug, Serialize)] +enum ParsedDiagram { + Component(puml_parser::CompPumlDocument), + Class(puml_parser::ClassUmlFile), + Sequence(puml_parser::SeqPumlDocument), +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let log_level: LogLevel = args.log_level.into(); + Builder::new() + .filter_level(log_level.to_level_filter()) + .init(); + let emit_debug_json = log_level.to_level_filter() >= log::LevelFilter::Debug; + + let fbs_output_dir: Option = if let Some(dir) = &args.fbs_output_dir { + let p = PathBuf::from(dir); + fs::create_dir_all(&p)?; + Some(p) + } else { + None + }; + + let lobster_output_dir: Option = match &args.lobster_output_dir { + Some(dir) => { + let p = PathBuf::from(dir); + fs::create_dir_all(&p)?; + Some(p) + } + None => None, + }; + + let file_list = collect_files_from_args(&args)?; + + if file_list.is_empty() { + return Err("No valid PUML files found.".into()); + } + debug!("Collected {} puml files.", file_list.len()); + + debug!("Preprocessing: include expansion"); + let mut preprocessor = Preprocessor::new(); + let preprocessed_files = preprocessor.preprocess(&file_list, log_level)?; + + debug!("Parsing started"); + for (path, content) in &preprocessed_files { + let parsed_content = + parse_puml_file(path, content, log_level, args.diagram_type).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Parse error in {}: {}", path.display(), e), + ) + })?; + if emit_debug_json { + if let Some(ref dir) = fbs_output_dir { + write_json_to_file(&parsed_content, path, dir, "raw.ast")?; + } + } + + match resolve_parsed_diagram(parsed_content) { + Ok(logic_result) => { + debug!( + "Successfully resolved PlantUML document: {}", + path.display() + ); + if emit_debug_json { + if let Some(ref dir) = fbs_output_dir { + write_json_to_file(&logic_result, path, dir, "logic.ast")?; + } + } + + let source_file = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default(); + let fbs_buffer = serialize_resolved_diagram(&logic_result, source_file); + if let Some(ref dir) = fbs_output_dir { + 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)?; + } + } + Err(e) => { + error!("Resolve error in {}: {}", path.display(), e); + warn!( + "Skipping file due to unimplemented diagram type: {}", + path.display() + ); + // Create empty placeholder files so the build continues + if let Some(ref dir) = fbs_output_dir { + write_placeholder_file(path, dir)?; + } + if let Some(ref ldir) = lobster_output_dir { + write_lobster_to_file(&HashMap::new(), path, ldir)?; + } + } + } + } + + debug!("Parsing completed"); + Ok(()) +} + +fn serialize_resolved_diagram(resolved_content: &ResolvedDiagram, source_file: &str) -> Vec { + match resolved_content { + ResolvedDiagram::Component(resolved_content) => { + ComponentSerializer::serialize(resolved_content, source_file) + } // ResolvedDiagram::Class(_) => { // placeholder + // /* class serializer */ + // } + // ResolvedDiagram::Sequence(_) => { // placeholder + // /* sequence serializer */ + // } + } +} + +#[derive(Debug, Serialize)] +pub enum ResolvedDiagram { + Component(HashMap), + // Class(ClassLogic), // placeholder + // Sequence(SequenceLogic), // placeholder +} + +fn resolve_parsed_diagram( + parsed_content: ParsedDiagram, +) -> Result> { + match parsed_content { + ParsedDiagram::Component(parsed_content) => { + 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::Sequence(_) => { + /* sequence resolver */ + Err("Sequence diagrams not implemented".into()) + } + } +} + +fn puml_resolver( + resolver: &mut Resolver, + parsed_content: &Resolver::Document, +) -> Result> +where + Resolver: DiagramResolver, + Resolver::Output: std::fmt::Debug, + Resolver::Error: std::error::Error + 'static, +{ + let logic_result = resolver + .visit_document(parsed_content) + .map_err(|e| Box::new(e) as Box)?; + + Ok(logic_result) +} + +fn parse_with_parser

( + parser: &mut P, + path: &Rc, + content: &str, + log_level: LogLevel, +) -> Result> +where + P: DiagramParser, + P::Output: std::fmt::Debug, + P::Error: std::error::Error + 'static, +{ + let parsed_content = parser + .parse_file(path, content, log_level) + .map_err(|e| Box::new(e) as Box)?; + + debug!("Successfully parsed PlantUML document: {}", path.display()); + Ok(parsed_content) +} + +// lobster-trace: Tools.ArchitectureModelingSyntax +fn parse_puml_file( + path: &Rc, + content: &str, + log_level: LogLevel, + diagram_type: DiagramType, +) -> Result> { + match diagram_type { + DiagramType::Component => { + parse_with_parser(&mut PumlComponentParser, path, content, log_level) + .map(ParsedDiagram::Component) + } + DiagramType::Class => parse_with_parser(&mut PumlClassParser, path, content, log_level) + .map(ParsedDiagram::Class), + DiagramType::Sequence => { + parse_with_parser(&mut PumlSequenceParser, path, content, log_level) + .map(ParsedDiagram::Sequence) + } + DiagramType::None => parse_in_order(path, content, log_level), + } +} + +type ParserFn = + fn(&Rc, &str, LogLevel) -> Result>; + +fn parse_in_order( + path: &Rc, + content: &str, + log_level: LogLevel, +) -> Result> { + let parsers: &[(&str, ParserFn)] = &[ + ("Component", |p, c, l| { + parse_with_parser(&mut PumlComponentParser, p, c, l).map(ParsedDiagram::Component) + }), + ("Class", |p, c, l| { + parse_with_parser(&mut PumlClassParser, p, c, l).map(ParsedDiagram::Class) + }), + ("Sequence", |p, c, l| { + parse_with_parser(&mut PumlSequenceParser, p, c, l).map(ParsedDiagram::Sequence) + }), + ]; + + for (parser_name, parser) in parsers { + if let Ok(ast) = parser(path, content, log_level) { + debug!("Successfully detected as {} diagram", parser_name); + return Ok(ast); + } + } + + Err(format!( + "Failed to parse {} with any available parser", + path.display() + ) + .into()) +} + +fn collect_files_from_args( + args: &Args, +) -> Result>, Box> { + let mut file_list: HashSet> = HashSet::new(); + + // Collect individual files from --file arguments (may be repeated) + for file_path in &args.file { + add_single_file(Path::new(file_path), &mut file_list)?; + } + + // Collect files from folders using --folders argument + if let Some(folder_path) = &args.folders { + collect_puml_files_from_folder(Path::new(folder_path), &mut file_list)?; + } + + Ok(file_list) +} + +fn resolve_path(path: &Path) -> PathBuf { + // When running with 'bazel run', use BUILD_WORKSPACE_DIRECTORY + let base_dir = std::env::var("BUILD_WORKSPACE_DIRECTORY") + .ok() + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap()); + + if path.is_absolute() { + path.to_path_buf() + } else { + base_dir.join(path) + } +} + +fn add_single_file( + path: &Path, + file_list: &mut HashSet>, +) -> Result<(), Box> { + let abs_path = resolve_path(path); + + if !abs_path.is_file() { + return Err(format!("Path is not a file: {}", path.display()).into()); + } + if abs_path.extension().and_then(|ext| ext.to_str()) != Some("puml") { + return Err(format!("File is not a .puml file: {}", path.display()).into()); + } + file_list.insert(Rc::new(abs_path)); + Ok(()) +} + +fn collect_puml_files_from_folder( + dir: &Path, + file_list: &mut HashSet>, +) -> Result<(), Box> { + let abs_dir = resolve_path(dir); + + if !abs_dir.is_dir() { + return Err(format!("Path is not a directory: {}", dir.display()).into()); + } + collect_puml_files(&abs_dir, file_list)?; + Ok(()) +} + +fn collect_puml_files( + dir: &Path, + file_list: &mut HashSet>, +) -> Result<(), Box> { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_puml_files(&path, file_list)?; + } else if path.extension().and_then(|ext| ext.to_str()) == Some("puml") { + file_list.insert(Rc::new(path.to_path_buf())); + } + } + Ok(()) +} diff --git a/plantuml/parser/puml_lobster/BUILD b/plantuml/parser/puml_lobster/BUILD new file mode 100644 index 0000000..4fa3fe2 --- /dev/null +++ b/plantuml/parser/puml_lobster/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "puml_lobster", + srcs = ["src/lib.rs"], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_resolver", + "@crates//:serde_json", + ], +) diff --git a/plantuml/parser/puml_lobster/src/lib.rs b/plantuml/parser/puml_lobster/src/lib.rs new file mode 100644 index 0000000..55e9b2d --- /dev/null +++ b/plantuml/parser/puml_lobster/src/lib.rs @@ -0,0 +1,96 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Converts the resolved PlantUML logical model into a `lobster-imp-trace` +//! JSON file compatible with the LOBSTER traceability toolchain. +//! +//! Only [`ComponentType::Interface`] elements are emitted + +use puml_resolver::{ComponentType, LogicComponent}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +/// 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 + .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", + }) + }) + .collect(); + + // Sort by tag for deterministic output + items.sort_by(|a, b| { + a["tag"] + .as_str() + .unwrap_or("") + .cmp(b["tag"].as_str().unwrap_or("")) + }); + + json!({ + "schema": "lobster-imp-trace", + "version": 3, + "generator": "puml_lobster", + "data": items, + }) +} + +/// 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, + input_path: &Path, + output_dir: &Path, +) -> io::Result { + let file_stem = input_path + .file_stem() + .unwrap_or_else(|| OsStr::new("output")); + + 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))?; + + fs::write(&output_path, content + "\n")?; + Ok(output_path) +} diff --git a/plantuml/parser/puml_parser/BUILD b/plantuml/parser/puml_parser/BUILD new file mode 100644 index 0000000..6528078 --- /dev/null +++ b/plantuml/parser/puml_parser/BUILD @@ -0,0 +1,56 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "puml_parser", + srcs = ["src/lib.rs"], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + ":class_diagram", + ":component_diagram", + ":parser_core", + ":preprocessor", + ":sequence_diagram", + ], +) + +alias( + name = "class_diagram", + actual = "//plantuml/parser/puml_parser/src/class_diagram:puml_parser_class", + visibility = ["//plantuml/parser:__subpackages__"], +) + +alias( + name = "component_diagram", + actual = "//plantuml/parser/puml_parser/src/component_diagram:puml_parser_component", + visibility = ["//plantuml/parser:__subpackages__"], +) + +alias( + name = "sequence_diagram", + actual = "//plantuml/parser/puml_parser/src/sequence_diagram:puml_parser_sequence", + visibility = ["//plantuml/parser:__subpackages__"], +) + +alias( + name = "preprocessor", + actual = "//plantuml/parser/puml_parser/src/preprocessor:puml_parser_preprocessor", + visibility = ["//plantuml/parser:__subpackages__"], +) + +alias( + name = "parser_core", + actual = "//plantuml/parser/puml_parser/src/parser_core:puml_parser_core", + visibility = ["//plantuml/parser:__subpackages__"], +) diff --git a/plantuml/parser/puml_parser/src/class_diagram/BUILD b/plantuml/parser/puml_parser/src/class_diagram/BUILD new file mode 100644 index 0000000..9ba95f1 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/BUILD @@ -0,0 +1,60 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +filegroup( + name = "puml_parser_class_files", + srcs = [ + "src/class_ast.rs", + "src/class_parser.rs", + "src/class_traits.rs", + "src/lib.rs", + ], + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "puml_parser_class", + srcs = [":puml_parser_class_files"], + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:class_grammar", + ], + crate_name = "class_parser", + crate_root = "src/lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + "@crates//:pest", + "@crates//:serde", + "@crates//:thiserror", + ], +) + +rust_test( + name = "puml_parser_class_integration_test", + srcs = ["test/integration_test.rs"], + args = [ + "--nocapture", + ], + data = ["//plantuml/parser/puml_parser/tests/class_diagram:class_diagram_files"], + deps = [ + ":puml_parser_class", + "//plantuml/parser/integration_test:test_framework", + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + ], +) diff --git a/plantuml/parser/puml_parser/src/class_diagram/detail_design/class_ast.puml b/plantuml/parser/puml_parser/src/class_diagram/detail_design/class_ast.puml new file mode 100644 index 0000000..af5f4cf --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/detail_design/class_ast.puml @@ -0,0 +1,139 @@ +' ******************************************************************************* +' 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 +title UML AST Core Structure - Full Fields with Types + +skinparam linetype ortho +skinparam classAttributeIconSize 0 + +' ======================== +' AST +' ======================== +class ClassDef { + name: Name + namespace: String + package: String + attributes: Vec + methods: Vec +} + +class StructDef { + name: Name + namespace: String + package: String + attributes: Vec + methods: Vec +} + +class InterfaceDef { + name: Name + namespace: String + package: String + attributes: Vec + methods: Vec +} + +class EnumDef { + name: Name + namespace: String + package: String + stereotypes: Vec + items: Vec +} + +class EnumItem { + visibility: Option + name: String + value: Option +} + +class Namespace { + name: Name + types: Vec + namespaces: Vec +} + +class Package { + name: Name + types: Vec + packages: Vec + relationships: Vec +} + +class ClassUmlFile { + name: String + elements: Vec + relationships: Vec +} + +class Method { + visibility: Visibility + name: String + generic_params: Vec + params: Vec + type: Option +} + +class Attribute { + visibility: Visibility + name: String + type: Option +} + +' ======================== +' enum +' ======================== +enum Element { + ClassDef + StructDef + EnumDef + InterfaceDef +} + +enum ClassUmlTopLevel { + Types + Enum + Namespace + Package +} + +' ======================== +' relationship +' ======================== +ClassDef "1" o-- "*" Attribute +ClassDef "1" o-- "*" Method + +StructDef "1" o-- "*" Attribute +StructDef "1" o-- "*" Method + +InterfaceDef "1" o-- "*" Attribute +InterfaceDef "1" o-- "*" Method + +EnumDef "1" o-- "*" EnumItem + +Element <|-- ClassDef +Element <|-- StructDef +Element <|-- EnumDef +Element <|-- InterfaceDef + +Namespace "1" o-- "*" Element +Namespace "1" o-- "*" Namespace + +Package "1" o-- "*" Element +Package "1" o-- "*" Package +Package "1" o-- "*" Relationship + +ClassUmlFile "1" o-- "*" ClassUmlTopLevel +ClassUmlFile "1" o-- "*" Relationship + +@enduml diff --git a/plantuml/parser/puml_parser/src/class_diagram/detail_design/parser.puml b/plantuml/parser/puml_parser/src/class_diagram/detail_design/parser.puml new file mode 100644 index 0000000..63d4a6a --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/detail_design/parser.puml @@ -0,0 +1,64 @@ +' ******************************************************************************* +' 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 +title parse_uml(uml: &str) -> (ClassUmlFile, bool) + +start + +:init ret_val = true; +:init uml_file = default; + +if (ClassParser::parse(file, uml)) then (Ok) + :get file_pair; + :loop file_pair.inner; + + repeat + if (rule == top_level) then (yes) + :loop top_level.inner; + + repeat + if (rule == type_def) then (yes) + :parse_type_def; + :push Types; + elseif (rule == enum_def) then (yes) + :parse_enum_def; + :push Enum; + elseif (rule == namespace_def) then (yes) + :parse_namespace; + :push Namespace; + elseif (rule == relationship) then (yes) + :parse_relationship; + :push relationship; + elseif (rule == package_def) then (yes) + :parse_package; + :push Package; + else (ignore) + endif + repeat while (have inner_pair) + + elseif (rule == startuml) then (yes) + :parse UML name; + :set uml_file.name; + else (ignore) + endif + repeat while (have pair) + +else (Err) + :ret_val = false; + :print parse error; +endif + +:return (uml_file, ret_val); +stop + +@enduml diff --git a/plantuml/parser/puml_parser/src/class_diagram/detail_design/test.puml b/plantuml/parser/puml_parser/src/class_diagram/detail_design/test.puml new file mode 100644 index 0000000..ea741a8 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/detail_design/test.puml @@ -0,0 +1,83 @@ +' ******************************************************************************* +' 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 +title test_all_class_diagram_puml() + +skinparam linetype ortho +skinparam defaultFontSize 12 + +start + +:Get runfiles_dir; +:Get workspace; +:Compute dir path; +:Compute output_dir; +:create_dir_all(output_dir); + +:Get optional filter_file; + +if (filter_file exists and not empty?) then (yes) + :Check if specified file exists; + :Add file to files_to_test; +else (no) + :Iterate through dir; + if (file extension is .puml?) then (yes) + :Add file to files_to_test; + endif +endif + +:Initialize passed_cnt = 0; +:Initialize failed = empty list; +:Get write_json configuration; + +' --- 文件循环 --- +repeat + :Get next file; + :Print "Testing ..." message; + :Compute json_output_path; + :Read file content; + :Call test_parser -> (ast, ret_val); + + if (ret_val == false?) then (yes) + :Check AST is not empty; + endif + + if (write_json?) then (yes) + :Save AST JSON; + :Print "JSON saved"; + else (no) + :Print "Pass (JSON not written)"; + endif + + if (panic occurred?) then (yes) + :Print "FAIL"; + :Add file to failed list; + else (no) + :Increment passed_cnt; + endif +repeat while (files remain?) + +:Print SUMMARY; +:Print passed_cnt; +:Print failed.len; + +if (failed not empty) then + :Print list of failed files; +endif +if (failed not empty) then + :assert! failure; +endif + + +stop +@enduml 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 new file mode 100644 index 0000000..dfae8d2 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_ast.rs @@ -0,0 +1,242 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +pub use parser_core::common_ast::Arrow; +use serde::{Deserialize, Serialize}; +use std::default::Default; + +use crate::class_traits::{TypeDef, WritableName}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Visibility { + Public, // "+" + Private, // "-" + Protected, // "#" + Package, // "~" +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum Element { + ClassDef(ClassDef), + StructDef(StructDef), + EnumDef(EnumDef), + InterfaceDef(InterfaceDef), +} +impl Element { + pub fn set_namespace(&mut self, ns: String) { + match self { + Element::ClassDef(def) => def.namespace = ns, + Element::StructDef(def) => def.namespace = ns, + Element::EnumDef(def) => def.namespace = ns, + Element::InterfaceDef(def) => def.namespace = ns, + } + } + pub fn set_package(&mut self, ns: String) { + match self { + Element::ClassDef(def) => def.package = ns, + Element::StructDef(def) => def.package = ns, + Element::EnumDef(def) => def.package = ns, + Element::InterfaceDef(def) => def.package = ns, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum ClassUmlTopLevel { + Types(Element), + Enum(EnumDef), + Namespace(Namespace), + Package(Package), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum EnumValue { + Literal(String), + Description(String), +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Name { + pub internal: String, + pub display: Option, +} +impl WritableName for Name { + fn write_name(&mut self, internal: impl Into, display: Option>) { + self.internal = internal.into(); + self.display = display.map(|d| d.into()); + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Relationship { + pub left: String, + pub right: String, + pub arrow: Arrow, + pub label: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct Param { + pub name: Option, + pub param_type: String, + pub varargs: bool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct Attribute { + pub visibility: Visibility, + pub name: String, + pub r#type: Option, +} +impl Default for Attribute { + fn default() -> Self { + Attribute { + visibility: Visibility::Public, + name: String::new(), + r#type: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct Method { + pub visibility: Visibility, + pub name: String, + pub generic_params: Vec, + pub params: Vec, + pub r#type: Option, +} +impl Default for Method { + fn default() -> Self { + Method { + visibility: Visibility::Public, + name: String::new(), + generic_params: Vec::new(), + params: Vec::new(), + r#type: None, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ClassDef { + pub name: Name, + pub namespace: String, + pub package: String, + pub attributes: Vec, + pub methods: Vec, +} +impl TypeDef for ClassDef { + fn name_mut(&mut self) -> &mut Name { + &mut self.name + } + + fn attributes_mut(&mut self) -> &mut Vec { + &mut self.attributes + } + + fn methods_mut(&mut self) -> &mut Vec { + &mut self.methods + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct StructDef { + pub name: Name, + pub namespace: String, + pub package: String, + pub attributes: Vec, + pub methods: Vec, +} +impl TypeDef for StructDef { + fn name_mut(&mut self) -> &mut Name { + &mut self.name + } + + fn attributes_mut(&mut self) -> &mut Vec { + &mut self.attributes + } + + fn methods_mut(&mut self) -> &mut Vec { + &mut self.methods + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct InterfaceDef { + pub name: Name, + pub namespace: String, + pub package: String, + pub attributes: Vec, + pub methods: Vec, +} +impl TypeDef for InterfaceDef { + fn name_mut(&mut self) -> &mut Name { + &mut self.name + } + + fn attributes_mut(&mut self) -> &mut Vec { + &mut self.attributes + } + + fn methods_mut(&mut self) -> &mut Vec { + &mut self.methods + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct EnumDef { + pub name: Name, + pub namespace: String, + pub package: String, + pub stereotypes: Vec, + pub items: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct EnumItem { + pub visibility: Option, + pub name: String, + pub value: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Namespace { + pub name: Name, + pub types: Vec, + pub namespaces: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Package { + pub name: Name, + pub types: Vec, + pub relationships: Vec, + pub packages: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ClassUmlFile { + pub name: String, + pub elements: Vec, + pub relationships: Vec, +} +impl ClassUmlFile { + pub fn is_empty(&self) -> bool { + self.elements.is_empty() && self.relationships.is_empty() + } +} +impl AsRef for ClassUmlFile { + fn as_ref(&self) -> &str { + &self.name + } +} 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 new file mode 100644 index 0000000..ebc2d87 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs @@ -0,0 +1,493 @@ +// ******************************************************************************* +// 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 crate::class_ast::{ + Arrow, Attribute, ClassDef, ClassUmlFile, ClassUmlTopLevel, Element, EnumDef, EnumItem, + EnumValue, InterfaceDef, Method, Name, Namespace, Package, Param, Relationship, StructDef, + Visibility, +}; +use crate::class_traits::{TypeDef, WritableName}; +use parser_core::common_parser::{parse_arrow, PlantUmlCommonParser, Rule}; +use parser_core::{pest_to_syntax_error, BaseParseError, DiagramParser}; +use pest::Parser; +use puml_utils::LogLevel; +use std::path::PathBuf; +use std::rc::Rc; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ClassError { + #[error(transparent)] + Base(#[from] BaseParseError), +} + +fn parse_visibility(pair: Option>) -> Visibility { + let mut vis = Visibility::Public; + if let Some(v) = pair { + match v.as_str() { + "+" => vis = Visibility::Public, + "-" => vis = Visibility::Private, + "#" => vis = Visibility::Protected, + "~" => vis = Visibility::Package, + _ => (), + } + } + vis +} + +fn parse_named(pair: pest::iterators::Pair, name: &mut Name) { + let mut internal: Option = None; + let mut display: Option = None; + + fn strip_quotes(s: &str) -> String { + if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 { + s[1..s.len() - 1].to_string() + } else { + s.to_string() + } + } + + fn walk( + pair: pest::iterators::Pair, + internal: &mut Option, + display: &mut Option, + ) { + match pair.as_rule() { + Rule::internal_name => { + *internal = Some(strip_quotes(pair.as_str())); + } + Rule::alias_clause => { + let mut inner = pair.into_inner(); + if let Some(target) = inner.next() { + *display = Some(strip_quotes(target.as_str())); + } + } + _ => { + for inner in pair.into_inner() { + walk(inner, internal, display); + } + } + } + } + + walk(pair, &mut internal, &mut display); + + if let Some(internal) = internal { + name.write_name(&internal, display.as_deref()); + } +} + +fn parse_attribute(pair: pest::iterators::Pair) -> Attribute { + let mut attr = Attribute::default(); + let mut vis = None; + let mut name = None; + let mut typ = None; + + for p in pair.into_inner() { + match p.as_rule() { + Rule::class_visibility => vis = Some(p), + Rule::identifier => name = Some(p.as_str().to_string()), + Rule::type_name => typ = Some(p.as_str().to_string()), + _ => {} + } + } + + attr.visibility = parse_visibility(vis); + attr.name = name.unwrap_or_default(); + attr.r#type = typ; + attr +} + +fn parse_param(pair: pest::iterators::Pair) -> Param { + let mut name: Option = None; + let mut ty: Option = None; + let mut varargs = false; + + // param -> param_named | param_unnamed + let inner = pair.into_inner().next().unwrap(); + + match inner.as_rule() { + Rule::param_named => { + for p in inner.into_inner() { + match p.as_rule() { + Rule::identifier => { + name = Some(p.as_str().to_string()); + } + Rule::type_name => { + ty = Some(p.as_str().to_string()); + } + Rule::varargs => { + varargs = true; + } + _ => {} + } + } + } + + Rule::param_unnamed => { + for p in inner.into_inner() { + match p.as_rule() { + Rule::type_name => { + ty = Some(p.as_str().to_string()); + } + Rule::varargs => { + varargs = true; + } + _ => {} + } + } + } + + _ => unreachable!(), + } + + Param { + name, + param_type: ty.expect("param must have a type"), + varargs, + } +} + +fn parse_method(pair: pest::iterators::Pair) -> Method { + fn parse_generic_param_list(pair: pest::iterators::Pair) -> Vec { + pair.into_inner() + .filter(|p| p.as_rule() == Rule::identifier) + .map(|p| p.as_str().to_string()) + .collect() + } + + let mut method = Method::default(); + let mut vis = None; + let mut name = None; + + for p in pair.into_inner() { + match p.as_rule() { + Rule::class_visibility => vis = Some(p), + Rule::identifier => name = Some(p.as_str().to_string()), + Rule::param_list => { + for param_pair in p.into_inner() { + if param_pair.as_rule() == Rule::param { + let param = parse_param(param_pair); + method.params.push(param); + } + } + } + Rule::return_type => { + for return_type_inner in p.into_inner() { + if return_type_inner.as_rule() == Rule::type_name { + method.r#type = Some(return_type_inner.as_str().to_string()); + } + } + } + Rule::generic_param_list => { + method.generic_params = parse_generic_param_list(p); + } + _ => (), + } + } + method.visibility = parse_visibility(vis); + method.name = name.unwrap_or_default(); + + method +} + +fn parse_type_def_into(pair: pest::iterators::Pair) -> T +where + T: TypeDef + Default, +{ + let mut def = T::default(); + + for p in pair.into_inner() { + match p.as_rule() { + Rule::named => { + parse_named(p, def.name_mut()); + } + Rule::class_body => { + for inner in p.into_inner() { + if let Rule::class_member = inner.as_rule() { + for member in inner.into_inner() { + match member.as_rule() { + Rule::attribute => { + def.attributes_mut().push(parse_attribute(member)) + } + Rule::method => def.methods_mut().push(parse_method(member)), + _ => (), + } + } + } + } + } + _ => (), + } + } + + def +} + +fn parse_type_def(pair: pest::iterators::Pair) -> Element { + debug_assert_eq!(pair.as_rule(), Rule::type_def); + + let mut inner = pair.clone().into_inner(); + + let kind_pair = inner.next().expect("type_def must have type_kind"); + + let kind = kind_pair.as_str(); // "class" | "struct" + + match kind { + "class" => Element::ClassDef(parse_type_def_into::(pair)), + "struct" => Element::StructDef(parse_type_def_into::(pair)), + "interface" => Element::InterfaceDef(parse_type_def_into::(pair)), + _ => unreachable!("unknown type_kind: {}", kind), + } +} + +fn parse_enum_def(pair: pest::iterators::Pair) -> EnumDef { + let mut enum_def = EnumDef::default(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::named => { + // enum_def.name = inner.as_str().trim().to_string(); + parse_named(inner, &mut enum_def.name); + } + Rule::enum_body => { + enum_def.items = parse_enum_body(inner); + } + _ => (), + } + } + + enum_def +} + +fn parse_enum_body(pair: pest::iterators::Pair) -> Vec { + pair.into_inner() + .filter(|p| p.as_rule() == Rule::enum_item) + .map(parse_enum_item) + .collect() +} + +fn parse_enum_item(pair: pest::iterators::Pair) -> EnumItem { + let mut item = EnumItem::default(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::class_visibility => { + item.visibility = Some(parse_visibility(Some(inner))); + } + Rule::identifier => { + item.name = inner.as_str().to_string(); + } + Rule::enum_value => { + item.value = Some(parse_enum_value(inner)); + } + _ => (), + } + } + + item +} + +fn parse_enum_value(pair: pest::iterators::Pair) -> EnumValue { + let text = pair.as_str().trim(); + + if let Some(rest) = text.strip_prefix('=') { + EnumValue::Literal(rest.trim().to_string()) + } else if let Some(rest) = text.strip_prefix(':') { + EnumValue::Description(rest.trim().to_string()) + } else { + EnumValue::Literal(text.to_string()) + } +} + +fn parse_namespace(pair: pest::iterators::Pair) -> Namespace { + let mut namespace = Namespace::default(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::named => { + parse_named(inner, &mut namespace.name); + } + Rule::top_level => { + for top_level_inner in inner.into_inner() { + match top_level_inner.as_rule() { + Rule::type_def => { + let mut type_def = parse_type_def(top_level_inner); + type_def.set_namespace(namespace.name.internal.clone()); + namespace.types.push(type_def); + } + Rule::enum_def => { + let mut enum_def = Element::EnumDef(parse_enum_def(top_level_inner)); + enum_def.set_namespace(namespace.name.internal.clone()); + namespace.types.push(enum_def); + } + Rule::namespace_def => { + namespace.namespaces.push(parse_namespace(top_level_inner)); + } + _ => (), + } + } + } + _ => (), + } + } + + namespace +} + +fn parse_package(pair: pest::iterators::Pair) -> Package { + let mut package = Package::default(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::named => { + parse_named(inner, &mut package.name); + } + + Rule::top_level => { + for t in inner.into_inner() { + match t.as_rule() { + Rule::type_def => { + let mut r#type = parse_type_def(t); + r#type.set_package(package.name.internal.clone()); + package.types.push(r#type); + } + Rule::enum_def => { + let mut enum_def = Element::EnumDef(parse_enum_def(t)); + enum_def.set_package(package.name.internal.clone()); + package.types.push(enum_def); + } + Rule::relationship => { + package.relationships.push(parse_relationship(t)); + } + Rule::package_def => { + package.packages.push(parse_package(t)); + } + _ => {} + } + } + } + _ => {} + } + } + + package +} + +fn parse_label(pair: pest::iterators::Pair) -> String { + pair.as_str().trim().to_string() +} + +fn parse_relationship(pair: pest::iterators::Pair) -> Relationship { + let mut inner = pair.into_inner(); + + let left = inner.next().unwrap().as_str().trim().to_string(); + + let arrow_pair = inner.next().unwrap(); + let arrow = parse_arrow(arrow_pair).unwrap_or_else(|_| Arrow::default()); + + let right = inner.next().unwrap().as_str().trim().to_string(); + + let mut label: Option = None; + for p in inner { + if p.as_rule() == Rule::label { + label = Some(parse_label(p)); + } + } + + Relationship { + left, + right, + arrow, + label, + } +} + +/// Parser struct for class diagrams +pub struct PumlClassParser; + +impl DiagramParser for PumlClassParser { + type Output = ClassUmlFile; + type Error = ClassError; + + fn parse_file( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result { + // Log file content at trace level + if matches!(log_level, LogLevel::Trace) { + eprintln!("{}:\n{}\n{}", path.display(), content, "=".repeat(30)); + } + + let mut uml_file = ClassUmlFile::default(); + + match PlantUmlCommonParser::parse(Rule::class_start, content) { + Ok(mut pairs) => { + let file_pair = pairs.next().unwrap(); + + let inner = file_pair.into_inner(); + + for pair in inner { + match pair.as_rule() { + Rule::top_level => { + for inner_pair in pair.into_inner() { + match inner_pair.as_rule() { + Rule::type_def => { + let type_def = parse_type_def(inner_pair); + uml_file.elements.push(ClassUmlTopLevel::Types(type_def)); + } + Rule::enum_def => { + uml_file.elements.push(ClassUmlTopLevel::Enum( + parse_enum_def(inner_pair), + )); + } + Rule::namespace_def => { + uml_file.elements.push(ClassUmlTopLevel::Namespace( + parse_namespace(inner_pair), + )); + } + Rule::relationship => { + uml_file.relationships.push(parse_relationship(inner_pair)); + } + Rule::package_def => { + uml_file.elements.push(ClassUmlTopLevel::Package( + parse_package(inner_pair), + )); + } + _ => (), + } + } + } + Rule::startuml => { + let text = pair.as_str(); + if let Some(name) = text.split_whitespace().nth(1) { + uml_file.name = name.to_string(); + } + } + _ => (), + } + } + } + Err(e) => { + return Err(ClassError::Base(pest_to_syntax_error( + e, + path.as_ref().clone(), + content, + ))); + } + }; + + Ok(uml_file) + } +} diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/class_traits.rs b/plantuml/parser/puml_parser/src/class_diagram/src/class_traits.rs new file mode 100644 index 0000000..aea9b47 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_traits.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use crate::class_ast::{Attribute, Method, Name}; + +pub trait TypeDef { + fn name_mut(&mut self) -> &mut Name; + fn attributes_mut(&mut self) -> &mut Vec; + fn methods_mut(&mut self) -> &mut Vec; +} + +pub trait WritableName { + fn write_name(&mut self, internal: impl Into, display: Option>); +} diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs new file mode 100644 index 0000000..fd8d1e6 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/src/lib.rs @@ -0,0 +1,36 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +pub mod class_ast; +mod class_parser; +mod class_traits; + +pub use class_ast::{ + Attribute, ClassDef, ClassUmlFile, ClassUmlTopLevel, Element, EnumDef, EnumItem, EnumValue, + 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 new file mode 100644 index 0000000..6097770 --- /dev/null +++ b/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs @@ -0,0 +1,201 @@ +// ******************************************************************************* +// 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 std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::PathBuf; +use std::rc::Rc; + +use class_parser::{ClassError, ClassUmlFile, PumlClassParser}; +use parser_core::{BaseParseError, DiagramParser}; +use puml_utils::LogLevel; +use test_framework::{run_case, DefaultExpectationChecker, DiagramProcessor}; + +struct ClassRunner; +impl DiagramProcessor for ClassRunner { + type Output = ClassUmlFile; + type Error = ClassError; + + fn run( + &self, + files: &HashSet>, + ) -> Result, ClassUmlFile>, ClassError> { + let mut results: HashMap, ClassUmlFile> = HashMap::new(); + let mut parser = PumlClassParser; + + for puml_path in files { + let uml_content = fs::read_to_string(&**puml_path).map_err(|e| { + ClassError::Base(BaseParseError::IoError { + path: puml_path.as_ref().to_path_buf(), + error: Box::new(e), + }) + })?; + + let class_ast = parser.parse_file(puml_path, ¨_content, LogLevel::Error)?; + + results.insert(Rc::clone(puml_path), class_ast); + } + + Ok(results) + } +} + +// Test entry +fn run_class_diagram_parser_case(case_name: &str) { + run_case( + "puml_parser/tests/class_diagram", + case_name, + ClassRunner, + DefaultExpectationChecker, + ); +} + +// --------- test for include --------- +#[test] +fn test_alias_class() { + run_class_diagram_parser_case("alias_class"); +} + +#[test] +fn test_alias_package() { + run_class_diagram_parser_case("alias_package"); +} + +#[test] +fn test_attr_method() { + run_class_diagram_parser_case("attr_method"); +} + +#[test] +fn test_class_merge() { + run_class_diagram_parser_case("class_merge"); +} + +#[test] +fn test_color() { + run_class_diagram_parser_case("color"); +} + +#[test] +fn test_cpp_style() { + run_class_diagram_parser_case("cpp_style"); +} + +#[test] +fn test_ctrl_instruct() { + run_class_diagram_parser_case("ctrl_instruct"); +} + +#[test] +fn test_empty() { + run_class_diagram_parser_case("empty"); +} + +#[test] +fn test_enum() { + run_class_diagram_parser_case("enum"); +} + +#[test] +fn test_interface() { + run_class_diagram_parser_case("interface"); +} + +#[test] +fn test_namespace_1() { + run_class_diagram_parser_case("namespace_1"); +} + +#[test] +fn test_namespace_2() { + run_class_diagram_parser_case("namespace_2"); +} + +#[test] +fn test_namespace_3() { + run_class_diagram_parser_case("namespace_3"); +} + +#[test] +fn test_negative_pkg_comp() { + run_class_diagram_parser_case("negative_pkg_comp"); +} + +#[test] +fn test_one_class() { + run_class_diagram_parser_case("one_class"); +} + +#[test] +fn test_only_attribute() { + run_class_diagram_parser_case("only_attribute"); +} + +#[test] +fn test_only_method() { + run_class_diagram_parser_case("only_method"); +} + +#[test] +fn test_package() { + run_class_diagram_parser_case("package"); +} + +#[test] +fn test_param() { + run_class_diagram_parser_case("param"); +} + +#[test] +fn test_param_templete() { + run_class_diagram_parser_case("param_templete"); +} + +#[test] +fn test_relationship_arrows() { + run_class_diagram_parser_case("relationship_arrows"); +} + +#[test] +fn test_relationship_inheritance() { + run_class_diagram_parser_case("relationship_inheritance"); +} + +#[test] +fn test_relationship_mix_inher() { + run_class_diagram_parser_case("relationship_mix_inher"); +} + +#[test] +fn test_relationship_normal() { + run_class_diagram_parser_case("relationship_normal"); +} + +#[test] +fn test_relationship_qualified_id() { + run_class_diagram_parser_case("relationship_qualified_id"); +} + +#[test] +fn test_stereotype_definition() { + run_class_diagram_parser_case("stereotype_definition"); +} + +#[test] +fn test_stereotype_relationship() { + run_class_diagram_parser_case("stereotype_relationship"); +} + +#[test] +fn test_struct() { + run_class_diagram_parser_case("struct"); +} diff --git a/plantuml/parser/puml_parser/src/component_diagram/BUILD b/plantuml/parser/puml_parser/src/component_diagram/BUILD new file mode 100644 index 0000000..f28042b --- /dev/null +++ b/plantuml/parser/puml_parser/src/component_diagram/BUILD @@ -0,0 +1,44 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library") + +filegroup( + name = "puml_parser_component_files", + srcs = [ + "src/component_ast.rs", + "src/component_parser.rs", + "src/lib.rs", + ], + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "puml_parser_component", + srcs = [":puml_parser_component_files"], + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:component_grammar", + ], + crate_name = "component_parser", + crate_root = "src/lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + "@crates//:log", + "@crates//:pest", + "@crates//:serde", + ], +) diff --git a/plantuml/parser/puml_parser/src/component_diagram/src/component_ast.rs b/plantuml/parser/puml_parser/src/component_diagram/src/component_ast.rs new file mode 100644 index 0000000..2407f23 --- /dev/null +++ b/plantuml/parser/puml_parser/src/component_diagram/src/component_ast.rs @@ -0,0 +1,51 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +pub use parser_core::common_ast::Arrow; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CompPumlDocument { + pub name: Option, + pub statements: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Statement { + Component(Component), + Relation(Relation), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Component { + pub component_type: String, + pub name: Option, + pub alias: Option, + pub stereotype: Option, + pub style: Option, + pub statements: Vec, // For nested components +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Relation { + pub lhs: String, + pub arrow: Arrow, + pub rhs: String, + pub style: Option, + pub description: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComponentStyle { + pub color: Option, + pub attributes: Vec, +} 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 new file mode 100644 index 0000000..266e496 --- /dev/null +++ b/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs @@ -0,0 +1,364 @@ +// ******************************************************************************* +// 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 log::debug; +use std::path::PathBuf; +use std::rc::Rc; + +use crate::{Arrow, CompPumlDocument, Component, ComponentStyle, Relation, Statement}; +use parser_core::{pest_to_syntax_error, BaseParseError, DiagramParser}; +use puml_utils::LogLevel; + +use parser_core::common_parser::parse_arrow as common_parse_arrow; +use parser_core::common_parser::{PlantUmlCommonParser, Rule}; + +pub struct PumlComponentParser; + +// lobster-trace: Tools.ArchitectureModelingSyntax +// lobster-trace: Tools.ArchitectureModelingComponentContentComponent +// lobster-trace: Tools.ArchitectureModelingComponentContentSEooC +// lobster-trace: Tools.ArchitectureModelingComponentContentSWUnit +// lobster-trace: Tools.ArchitectureModelingComponentContentAbstractInterface +// lobster-trace: Tools.ArchitectureModelingComponentHierarchySEooC +// lobster-trace: Tools.ArchitectureModelingComponentHierarchyComponent +// lobster-trace: Tools.ArchitectureModelingComponentInteract +impl PumlComponentParser { + fn format_parse_tree(pairs: pest::iterators::Pairs, indent: usize, output: &mut String) { + for pair in pairs { + let indent_str = " ".repeat(indent); + + output.push_str(&format!( + "{}Rule::{:?} -> \"{}\"\n", + indent_str, + pair.as_rule(), + pair.as_str() + )); + + // Recursively print inner pairs + Self::format_parse_tree(pair.into_inner(), indent + 1, output); + } + } + + fn parse_statement( + pair: pest::iterators::Pair, + ) -> Result> { + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::component => { + return Ok(Statement::Component(Self::parse_component(inner)?)); + } + Rule::relation => { + return Ok(Statement::Relation(Self::parse_relation(inner)?)); + } + _ => {} + } + } + Err("Invalid statement".into()) + } + + fn parse_component( + pair: pest::iterators::Pair, + ) -> Result> { + let mut component = Component { + component_type: "".to_string(), + name: None, + alias: None, + stereotype: None, + style: None, + statements: Vec::new(), + }; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::nested_component => { + // Parse the nested component (which contains default_component or bracket_component) + for nested_inner in inner.into_inner() { + match nested_inner.as_rule() { + Rule::default_component => { + let (ctype, name_opt) = + Self::parse_default_component(nested_inner)?; + component.component_type = ctype; + component.name = name_opt; + } + // For bracket_component, it's always a `component` type + Rule::bracket_component => { + let name_opt = Self::parse_bracket_component(nested_inner)?; + component.component_type = "component".to_string(); + component.name = name_opt; + } + _ => {} + } + } + } + Rule::component_old => { + component.name = Some(Self::extract_component_name(inner)); + component.component_type = "component".to_string(); + } + Rule::interface_old => { + 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); + } + Rule::stereotype => { + component.stereotype = Self::extract_stereotype(inner); + } + Rule::component_style => { + component.style = Some(Self::parse_component_style(inner)?); + } + Rule::statement_block => { + component.statements = Self::parse_statement_block(inner)?; + } + _ => {} + } + } + + Ok(component) + } + + fn parse_relation( + pair: pest::iterators::Pair, + ) -> Result> { + let mut lhs = String::new(); + let mut rhs = String::new(); + let mut arrow = Arrow::default(); + + let mut description = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::relation_left => { + lhs = inner.as_str().to_string(); + } + Rule::relation_right => { + rhs = inner.as_str().to_string(); + } + Rule::connection_arrow => { + arrow = Self::parse_arrow(inner)?; + } + Rule::component_description => { + description = Self::parse_description(inner); + } + _ => {} + } + } + + Ok(Relation { + lhs, + arrow, + rhs, + style: None, + description, + }) + } + + fn parse_description(pair: pest::iterators::Pair) -> Option { + pair.into_inner() + .find(|p| p.as_rule() == Rule::description_text) + .map(|p| p.as_str().trim().to_string()) + } + + fn parse_arrow(pair: pest::iterators::Pair) -> Result> { + let arrow = common_parse_arrow(pair).map_err(|e| { + Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + as Box + })?; + + Ok(arrow) + } + + // 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() + } + + 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() + } + + 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 + } + + 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 + } + + fn parse_default_component( + pair: pest::iterators::Pair, + ) -> Result<(String, Option), Box> { + let mut comp_type = String::new(); + let mut name = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::component_type => { + comp_type = inner.as_str().to_string(); + } + Rule::default_component_name => { + let raw_name = inner.as_str().to_string(); + // Remove surrounding quotes if present + let clean_name = if raw_name.starts_with('"') && raw_name.ends_with('"') { + raw_name[1..raw_name.len() - 1].to_string() + } else { + raw_name + }; + name = Some(clean_name); + } + _ => {} + } + } + + Ok((comp_type, name)) + } + + fn parse_bracket_component( + pair: pest::iterators::Pair, + ) -> Result, Box> { + let mut name: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::component_old { + name = Some(Self::extract_component_name(inner)); + } + } + + Ok(name) + } + + fn parse_component_style( + _pair: pest::iterators::Pair, + ) -> Result> { + // Simplified implementation + Ok(ComponentStyle { + color: None, + attributes: Vec::new(), + }) + } + + fn parse_statement_block( + pair: pest::iterators::Pair, + ) -> Result, Box> { + let mut statements = Vec::new(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::component_statement => { + if let Ok(stmt) = Self::parse_statement(inner) { + statements.push(stmt); + } + } + Rule::EOL => { + // Skip empty lines + } + _ => { + // Skip other rules like braces + } + } + } + + Ok(statements) + } +} + +impl DiagramParser for PumlComponentParser { + type Output = CompPumlDocument; + type Error = BaseParseError; + + fn parse_file( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result { + use pest::Parser; + + 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 + if matches!(log_level, LogLevel::Debug | LogLevel::Trace) { + let mut tree_output = String::new(); + + Self::format_parse_tree(pairs.clone(), 0, &mut tree_output); + + debug!( + "\n=== Parse Tree for {} ===\n{}=== End Parse Tree ===", + path.display(), + tree_output + ); + } + + let mut document = CompPumlDocument { + name: None, + statements: Vec::new(), + }; + + 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); + } + } + Rule::empty_line => { + // Skip empty lines + } + _ => {} + } + } + } + } + + Ok(document) + } +} diff --git a/plantuml/parser/puml_parser/src/component_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/component_diagram/src/lib.rs new file mode 100644 index 0000000..1558233 --- /dev/null +++ b/plantuml/parser/puml_parser/src/component_diagram/src/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod component_ast; +mod component_parser; + +pub use component_ast::{Arrow, CompPumlDocument, Component, ComponentStyle, Relation, Statement}; +pub use component_parser::PumlComponentParser; diff --git a/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs new file mode 100644 index 0000000..fadc640 --- /dev/null +++ b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs @@ -0,0 +1,648 @@ +// ******************************************************************************* +// 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 std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::rc::Rc; + +use component_parser::PumlComponentParser; +use parser_core::DiagramParser; +use puml_utils::LogLevel; + +fn test_file_with_golden(path: &str, golden: &str) -> Result<(), String> { + let mut parser = PumlComponentParser; + + let content = fs::read_to_string(path) + .map_err(|e| format!("Failed to read test puml file {}: {}", path, e))?; + + let ast = parser + .parse_file(&Rc::new(PathBuf::from(path)), &content, LogLevel::Error) + .map_err(|e| format!("Parse failed for {}: {:?}", path, e))?; + + let ast_str = format!("{:#?}", ast); + + if ast_str != golden { + return Err(format!( + "Golden test failed for {}.\nExpected:\n{}\n\nFound:\n{}", + path, golden, ast_str + )); + } + + Ok(()) +} + +#[test] +fn test_component_golden() { + let mut golden_map: HashMap<&str, &str> = HashMap::new(); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/basic_example.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Relation( + Relation { + lhs: "DataAccess", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "-", + }, + middle: None, + right: None, + }, + rhs: "[First Component]", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[First Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "..", + }, + middle: None, + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "HTTP", + style: None, + description: Some( + "use", + ), + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: None, + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "Interface1", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "-", + }, + middle: None, + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "Interface2", + style: None, + description: None, + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/changing_arrows_direction3.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: Some( + ArrowMiddle { + style: None, + direction: Some( + Left, + ), + decorator: None, + }, + ), + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "left", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: Some( + ArrowMiddle { + style: None, + direction: Some( + Right, + ), + decorator: None, + }, + ), + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "right", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: Some( + ArrowMiddle { + style: None, + direction: Some( + Up, + ), + decorator: None, + }, + ), + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "up", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: Some( + ArrowMiddle { + style: None, + direction: Some( + Down, + ), + decorator: None, + }, + ), + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "down", + style: None, + description: None, + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/component.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "component", + name: Some( + "First component", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Another component", + ), + alias: Some( + "Comp2", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Comp3", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Last\\ncomponent", + ), + alias: Some( + "Comp4", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/grouping_components.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "component", + name: Some( + "First component", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Another component", + ), + alias: Some( + "Comp2", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Comp3", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "Last\\ncomponent", + ), + alias: Some( + "Comp4", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/hide_or_remove_unlinked_component.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "component", + name: Some( + "C1", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "C2", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "C3", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Relation( + Relation { + lhs: "C1", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "--", + }, + middle: None, + right: None, + }, + rhs: "C2", + style: None, + description: None, + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/individual_colors.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "component", + name: Some( + "Web Server", + ), + alias: None, + stereotype: None, + style: Some( + ComponentStyle { + color: None, + attributes: [], + }, + ), + statements: [], + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/interfaces.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "interface", + name: Some( + "\"First Interface\"", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "interface", + name: Some( + "\"Another interface\"", + ), + alias: Some( + "Interf2", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "interface", + name: Some( + "Interf3", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "interface", + name: Some( + "Last\\ninterface", + ), + alias: Some( + "Interf4", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + Component( + Component { + component_type: "component", + name: Some( + "component", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/long_description.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "component", + name: Some( + "comp1", + ), + alias: None, + stereotype: None, + style: None, + statements: [], + }, + ), + ], +}"#, + ); + + golden_map.insert( + "plantuml/parser/integration_test/component_diagram/plantuml/use_uml2_notation.puml", + r#"CompPumlDocument { + name: None, + statements: [ + Component( + Component { + component_type: "interface", + name: Some( + "Data Access", + ), + alias: Some( + "DA", + ), + stereotype: None, + style: None, + statements: [], + }, + ), + Relation( + Relation { + lhs: "DA", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "-", + }, + middle: None, + right: None, + }, + rhs: "[First Component]", + style: None, + description: None, + }, + ), + Relation( + Relation { + lhs: "[First Component]", + arrow: Arrow { + left: None, + line: ArrowLine { + raw: "..", + }, + middle: None, + right: Some( + ArrowDecor { + raw: ">", + }, + ), + }, + rhs: "HTTP", + style: None, + description: Some( + "use", + ), + }, + ), + ], +}"#, + ); + + let mut passed = 0; + let mut failed = 0; + + for (file, golden) in golden_map.iter() { + match test_file_with_golden(file, golden) { + Ok(_) => { + passed += 1; + println!("✔ PASS {}", file); + } + Err(e) => { + failed += 1; + println!("✘ FAIL {}", file); + println!("{}", e); + } + } + } + + println!("Passed: {}, Failed: {}", passed, failed); + assert_eq!(failed, 0, "Some component golden tests failed"); +} diff --git a/plantuml/parser/puml_parser/src/grammar/BUILD b/plantuml/parser/puml_parser/src/grammar/BUILD new file mode 100644 index 0000000..a214557 --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/BUILD @@ -0,0 +1,72 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +filegroup( + name = "grammar_files", + srcs = [ + ":class_grammar", + ":common_grammar", + ":component_grammar", + ":include_grammar", + ":procedure_grammar", + ":sequence_grammar", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "include_grammar", + srcs = [ + "include.pest", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "procedure_grammar", + srcs = [ + "procedure.pest", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "component_grammar", + srcs = [ + "component.pest", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "sequence_grammar", + srcs = [ + "sequence.pest", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "class_grammar", + srcs = [ + "class.pest", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "common_grammar", + srcs = [ + "common.pest", + ], + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/puml_parser/src/grammar/class.pest b/plantuml/parser/puml_parser/src/grammar/class.pest new file mode 100644 index 0000000..f3720d7 --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/class.pest @@ -0,0 +1,156 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +/////////////////////////////////////////////////////////////// +// BASE +/////////////////////////////////////////////////////////////// +color = _{ BASIC_COLOR } + +raw_label = @{ (!("\n" | "\r") ~ ANY)+ } +label = { STRING | raw_label } + +class_visibility = { "+" | "-" | "#" | "~" } + +/////////////////////////////////////////////////////////////// +// Name & Alias +/////////////////////////////////////////////////////////////// +type_name = @{ (ASCII_ALPHANUMERIC | "_")+ } + +class_qualified_name = @{ ("::")? ~ identifier ~ ("::" ~ identifier)* } + +// reference +name_ref = @{ identifier } + +// definition +internal_name = { STRING | class_qualified_name } + +// name_decl = { internal_name ~ ("as" ~ display_name)? } +name_decl = { internal_name ~ alias_clause? } +name_with_color = { name_decl ~ color? } + +named = { name_with_color } + +/////////////////////////////////////////////////////////////// +// File-level structure +/////////////////////////////////////////////////////////////// +class_start = { + SOI + ~ empty_line* + ~ startuml + ~ EOL* + ~ ((ignored_stmt | top_level) ~ EOL+)* + ~ EOL* + ~ enduml + ~ EOI +} + +top_level = { + namespace_def + | type_def + | enum_def + | relationship + | package_def +} + +/////////////////////////////////////////////////////////////// +// namespace / package +/////////////////////////////////////////////////////////////// + +// namespace_def = { "namespace" ~ named ~ stereotype? ~ "{" ~ EOL* ~ top_level* ~ EOL* ~ "}" } +namespace_def = { + "namespace" + ~ named + ~ stereotype? + ~ "{" + ~ EOL* + ~ (top_level ~ EOL+)* + ~ EOL* + ~ "}" +} + +package_def = { + "package" + ~ named + ~ stereotype? + ~ "{" + ~ EOL* + ~ (top_level ~ EOL+)* + ~ EOL* + ~ "}" +} + +/////////////////////////////////////////////////////////////// +// type definition +/////////////////////////////////////////////////////////////// +type_def = { type_kind ~ named ~ stereotype? ~ class_body? } +type_kind = { + "class" + | "struct" + | "interface" +} + +class_body = { "{" ~ EOL* ~ class_member* ~ EOL* ~ "}" } + +class_member = { (method | attribute) ~ EOL+ } + +method = { class_visibility? ~ identifier ~ generic_param_list? ~ "(" ~ param_list? ~ ")" ~ return_type? } +return_type = { ":" ~ type_name } + +attribute = { class_visibility? ~ identifier ~ (":" ~ type_name)? } + +param = { + param_named + | param_unnamed +} + +param_named = { + identifier + ~ ":" + ~ type_name + ~ varargs? +} + +param_unnamed = { + type_name + ~ varargs? +} +param_list = { param ~ ("," ~ param)* } + +generic_param_list = { "<" ~ identifier ~ ("," ~ identifier)* ~ ">" } +varargs = { "..." } + +/////////////////////////////////////////////////////////////// +// type definition +/////////////////////////////////////////////////////////////// +enum_def = { "enum" ~ named ~ stereotype? ~ enum_body?} + +enum_body = { "{" ~ EOL* ~ enum_item* ~ EOL* ~ "}" } + +enum_item = { class_visibility? ~ identifier ~ enum_value? ~ EOL+ } + +enum_value = { ( "=" | ":" ) ~ enum_value_literal } + +enum_value_literal = @{ (!("\n" | "}") ~ ANY)+ } + +/////////////////////////////////////////////////////////////// +// Relationship +/////////////////////////////////////////////////////////////// + +qualified_identifier = @{ name_ref ~ ("." ~ name_ref)+ } + +relationship = { + (qualified_identifier | name_ref) + ~ connection_arrow + ~ (qualified_identifier | name_ref) + ~ (":" ~ label)? +} diff --git a/plantuml/parser/puml_parser/src/grammar/common.pest b/plantuml/parser/puml_parser/src/grammar/common.pest new file mode 100644 index 0000000..5e42d99 --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/common.pest @@ -0,0 +1,275 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//////////////////////////////////////////////////////////////////////////////// +// File: common.pest +// +// Description: +// Shared grammar definitions for PlantUML parsing, reused by Class, Component, +// and Sequence diagrams. This module centralizes common syntax to reduce +// duplication and ensure consistent parsing behavior. +// +// Supported Features: +// File structure, comments, title, skinparam, direction, grouping containers, +// alias, color, arrows, line styles, notes, scaling, and stereotypes. +// +// Design Principles: +// - Keep grammar minimal and reusable +// - Avoid diagram-specific semantics +// - Maintain PlantUML compatibility +// +// Usage: +// Import this file alongside diagram-specific grammars: +// +// #[grammar = "../grammar/common.pest"] +// #[grammar = "../grammar/.pest"] +// +// Maintenance Notes: +// - Do not add diagram-specific rules here +// - Prefer extending in child grammars +// +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Basic Elements +//////////////////////////////////////////////////////////////////////////////// +WHITESPACE = _{ " " | "\t" } + +NUMBER = @{ ASCII_DIGIT+ } +NEWLINE = _{ "\n" | "\r\n" } + +HEX = _{ ASCII_HEX_DIGIT } +INLINE_WS = _{ " " | "\t" } +LINE_REST = { (!EOL ~ ANY)* } +STRING = @{ "\"" ~ ( "\\\"" | !"\"" ~ ANY )* ~ "\"" } +EOL = _{ "\r\n" | "\n" | "\r" } + +NAME_ATOM = @{ (ASCII_ALPHANUMERIC | "_" | "." | "@")+ } +COMPOUND_NAME = @{ NAME_ATOM ~ ("-" ~ NAME_ATOM)* } +LOOSE_NAME = @{ (ASCII_ALPHANUMERIC | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* } +NAME = { COMPOUND_NAME | LOOSE_NAME } + +CNAME = @{ quoted_string | NAME } +quoted_string = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" | "«" ~ (!"»" ~ ANY)* ~ "»" } + +diagram_id = @{ (ASCII_ALPHANUMERIC | "_" | "-" | "." | "@")+ } +puml_name = { STRING | diagram_id } + +identifier = @{ (ASCII_ALPHANUMERIC | "_")+ } +empty_line = { WHITESPACE* ~ EOL } + +//////////////////////////////////////////////////////////////////////////////// +// @startuml / @enduml +//////////////////////////////////////////////////////////////////////////////// +startuml = { "@startuml" ~ puml_name? ~ EOL } +enduml = { "@enduml" ~ EOL? } + +//////////////////////////////////////////////////////////////////////////////// +// Comments +//////////////////////////////////////////////////////////////////////////////// +COMMENT = _{ + "//" ~ (!EOL ~ ANY)* + | "'" ~ (!EOL ~ ANY)* + | "/'" ~ (!"'/" ~ ANY)* ~ "'/" +} + +//////////////////////////////////////////////////////////////////////////////// +// Ignored Statements +// +// Non-structural PlantUML directives such as title, style, layout, and scaling. +// Parsed for completeness but generally skipped in AST construction. +// +// Examples: +// title System Overview +// skinparam shadowing false +// left to right direction +// scale 1.2 +//////////////////////////////////////////////////////////////////////////////// +ignored_stmt = _{ + direction_stmt + | skinparam_stmt + | title_stmt + | scale_stmt +} + +skinparam_stmt = @{ ^"skinparam" ~ LINE_REST } +title_stmt = @{ ^"title" ~ LINE_REST } +direction_stmt = @{ + ("left" | "right" | "top" | "bottom") + ~ " to " + ~ ("left" | "right" | "top" | "bottom") + ~ " direction" +} +scale_stmt = _{ + ^"scale" ~ ( + (^"max" ~ NUMBER ~ ("width" | "height")) | + (^"max" ~ NUMBER ~ ("*" | "x") ~ NUMBER) | + (NUMBER ~ ("*" | "x") ~ NUMBER) | + (NUMBER ~ ("/" ~ NUMBER)?) | + (NUMBER ~ ("width" | "height")) + ) +} + +//////////////////////////////////////////////////////////////////////////////// +// Stereotype +//////////////////////////////////////////////////////////////////////////////// +STEREOTYPE_LEFT = @{ "<<" } +STEREOTYPE_RIGHT = @{ ">>" } +STEREOTYPE_NAME = @{ (!">>" ~ ANY)+ } +stereotype = { STEREOTYPE_LEFT ~ STEREOTYPE_NAME ~ STEREOTYPE_RIGHT } + +//////////////////////////////////////////////////////////////////////////////// +// Note +//////////////////////////////////////////////////////////////////////////////// +note_declaration = { + note_single_line + | note_multiline +} + +// Single-line note: "note" with any content on same line +// Just consume the entire line as-is, don't parse its content +note_single_line = @{ + ^"note" ~ + INLINE_WS+ ~ // must have at least one space + (!("\n" | "\r") ~ ANY)* // rest of line +} +// Multiline note: "note" followed by newline, then content, then "end note" +// Just consume everything between note and end note, don't parse content +note_multiline = @{ + ^"note" ~ + (!NEWLINE ~ ANY)* ~ // rest of header line + NEWLINE ~ + ( + !(WHITESPACE* ~ (^"endnote" | (^"end" ~ WHITESPACE ~ ^"note"))) ~ + (!NEWLINE ~ ANY)* ~ + NEWLINE + )* ~ + WHITESPACE* ~ (^"endnote" | (^"end" ~ WHITESPACE ~ ^"note")) ~ + EOL? +} + +//////////////////////////////////////////////////////////////////////////////// +// Alias +// Unified Modeling: ("as" )? +//////////////////////////////////////////////////////////////////////////////// +ALIAS_ID = @{ (ASCII_ALPHANUMERIC | "_")+ } + +alias_clause = { "as" ~ (ALIAS_ID | quoted_string) } + +//////////////////////////////////////////////////////////////////////////////// +// Color +//////////////////////////////////////////////////////////////////////////////// +// Basic Color +COLOR_NAME = @{ ASCII_ALPHANUMERIC+ } +BASIC_COLOR = { "#" ~ (HEX{3,6} | COLOR_NAME) } + +// Gradient Color +COLOR_GRAD = @{ BASIC_COLOR ~ ("|" | "-" | "/" | "\\") ~ BASIC_COLOR } + +// Color for Arrow +COLOR_ARROW_COMMON = @{ HEX{3} | HEX{6} | HEX{8} | COLOR_NAME } +COLOR_ARROW_GRAD = @{ COLOR_ARROW_COMMON ~ ("|" | "-" | "/" | "\\") ~ COLOR_ARROW_COMMON } + +// Component Color +COLOR_COMPONENT_COMMON = @{ HEX{3} | HEX{6} | HEX{8} | COLOR_NAME } +COLOR_COMPONENT_GRAD = @{ COLOR_COMPONENT_COMMON ~ ("|" | "-" | "/" | "\\") ~ COLOR_COMPONENT_COMMON } + +//////////////////////////////////////////////////////////////////////////////// +// Line Style (Shared by Class / Component / Sequence) +// Example: +// [#red,dashed,thickness=2] +// [bold] +// [dotted] +// [thickness=3] +// [#blue|#green,bold] +//////////////////////////////////////////////////////////////////////////////// +line_style_block = { "[" ~ line_style_list ~ "]" } + +line_style_list = { line_style_item ~ (("," | ";") ~ line_style_item)* } + +line_style_item = { + line_color + | line_pattern + | line_thickness + | line_extra_attr +} + +// -------------------- color -------------------- +line_color = { + "#" ~ arrow_color + | arrow_color +} + +arrow_color = { + COLOR_ARROW_GRAD + | COLOR_ARROW_COMMON +} + +// -------------------- pattern -------------------- +line_pattern = { + "bold" + | "dashed" + | "dotted" + | "hidden" + | "plain" +} + +// -------------------- thickness -------------------- +line_thickness = { "thickness=" ~ NUMBER } + +// -------------------- sequence extensions -------------------- +line_extra_attr = { + "norank" + | "single" +} + +//////////////////////////////////////////////////////////////////////////////// +// Arrow +//////////////////////////////////////////////////////////////////////////////// +// ---------- Characters ---------- +ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "o" | "0" | "(" | ")" | "#" | "^" | "@" } + +ARROW_SUFFIX_CHAR = _{ ">" | "/" | "\\" | "|" | "*" | "o" | "0" | "(" | ")" | "#" | "^" | "@" } + +ARROW_SEGMENT_CHAR = _{ "-" | "." | "=" | "~" } + +// ---------- Arrow Core ---------- +connection_arrow = { + arrow_prefix? + ~ arrow_segment + ~ arrow_middle? + ~ arrow_segment? + ~ arrow_suffix? +} + +arrow_prefix = @{ ARROW_PREFIX_CHAR+ } +arrow_suffix = @{ ARROW_SUFFIX_CHAR+ } +arrow_segment = @{ ARROW_SEGMENT_CHAR+ } + +// ---------- Middle Block ---------- +arrow_middle = { + (line_style_block ~ arrow_direction? ~ arrow_mid_decor?) + | (arrow_direction ~ line_style_block) + | (arrow_direction ~ arrow_mid_decor?) + | arrow_mid_decor +} + +arrow_mid_decor = @{ "0" | "(0" | "0)" | "(0)" } + +// ---------- Direction ---------- +arrow_direction = @{ + "up" | "u" + | "right" | "ri" | "r" + | "down" | "do" | "d" + | "left" | "le" | "l" +} diff --git a/plantuml/parser/puml_parser/src/grammar/component.pest b/plantuml/parser/puml_parser/src/grammar/component.pest new file mode 100644 index 0000000..f519ff1 --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/component.pest @@ -0,0 +1,90 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +// PlantUML Component Diagram Grammar for Pest Parser + +component_start = { empty_line* ~ startuml ~ (component_statement | empty_line)* ~ enduml } + +component_statement = { ((relation | component | footer_line) ~ EOL) } + +component = { + (nested_component ~ alias_clause? ~ stereotype? ~ component_style? ~ (long_description | statement_block)? ) | + ((interface_old | component_old) ~ alias_clause? ~ component_style?) +} +long_description = {"[" ~ EOL? ~ (ASCII_ALPHANUMERIC ~ EOL?)* ~ "]"} + +component_old = { "[" ~ component_old_name ~ "]" } +interface_old = { "()" ~ interface_old_name } + +component_old_name = { BRACKET_NAME | CNAME } +interface_old_name = { CNAME } + +default_component = { component_type ~ default_component_name? } +bracket_component = { "component" ~ component_old } + +default_component_name = { CNAME } + +nested_component = { bracket_component | default_component } + +statement_block = { "{" ~ EOL ~ (component_statement | empty_line)* ~ "}" } + +relation = { relation_left ~ connection_arrow ~ relation_right ~ component_style? ~ component_description?} +relation_left = { QUALIFIED_NAME | component_old | interface_old } +relation_right = { QUALIFIED_NAME | component_old | interface_old } + +// Terminals +QUALIFIED_NAME = @{ NAME ~ ("." ~ NAME)* } + +BRACKET_NAME = @{ (ASCII_ALPHANUMERIC | "_" | "-" | " " | "\\")+ } + +component_type = { COMPONENT_TYPE } +COMPONENT_TYPE = { + "artifact" | "card" | "cloud" | "component" | "database" | + "file" | "folder" | "frame" | "hexagon" | "interface" | + "node" | "package" | "queue" | "rectangle" | "stack" | "storage" +} + +footer_line = { footer_align? ~ ^"footer" ~ ":"? ~ component_text_content? ~ COMMENT* } +footer_block_start = { footer_align? ~ ^"footer" } +footer_block_end = { ^"end" ~ WHITESPACE? ~ ^"footer" } +footer_align = { ^"left" | ^"right" | ^"center" } +component_text_content = { (!EOL ~ !comment_start ~ ANY)+ } +comment_start = _{ "//" | "'" | "/'" } + +// This is **bold** +// This is //italics// +// This is ""monospaced"" +// This is --stricken-out-- +// This is __underlined__ +// This is ~~wave-underlined~~ + +component_style = { + ("#" ~ component_color ~ (";" ~ component_attr)* ~ ";") | + ("#" ~ component_color ~ (";" ~ component_attr)*) | + ("#" ~ component_attr ~ (";" ~ component_attr)* ~ ";") | + ("#" ~ component_attr ~ (";" ~ component_attr)*) +} + +component_attr = { + (COMPONENT_LINE_COLOR ~ ("." ~ COMPONENT_LINE_ATTR)? ~ (":" ~ component_color)?) | + (COMPONENT_TEXT_COLOR ~ (":" ~ component_color)?) +} + +component_color = { COLOR_COMPONENT_GRAD | COLOR_COMPONENT_COMMON } + +COMPONENT_LINE_ATTR = @{ "dashed" | "bold" | "dotted" } +COMPONENT_TEXT_COLOR = @{ "text" } +COMPONENT_LINE_COLOR = @{ "line" } + +component_description = { ":" ~ description_text } +description_text = @{ (!("\n" | "\r") ~ ANY)* } diff --git a/plantuml/parser/puml_parser/src/grammar/include.pest b/plantuml/parser/puml_parser/src/grammar/include.pest new file mode 100644 index 0000000..22fccb9 --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/include.pest @@ -0,0 +1,85 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +/* +---- For include / include_once / include many ---- + 1. Local file: + 1) relative path: !include "diagrams/class.puml" + 2) absolute path: !include /usr/local/plantuml/common.puml or !include "/Users/me/My PUML Files/common.puml" + 2. Built-in/Standard Library: !include (will not be supported currently) + 3. URL: !include "https://domain/file.puml" (will not be supported currently) + +---- For includesub ---- + Only support local file +*/ + +// Common +WHITESPACE = _{ " " | "\t" } +NEWLINE = _{ "\n" | "\r\n" } +COMMENT = _{ "'" ~ (!NEWLINE ~ ANY)* | "//" ~ (!NEWLINE ~ ANY)* } +IGNORED = _{ (WHITESPACE | COMMENT)+ } + +// Entry Rule +file = { SOI ~ line* ~ EOI } + +line= _{ + includesub_line + | include_line + | sub_block + | text_line +} + +// ---------------- Include ---------------- +include_line = { include_directive ~ NEWLINE } +include_directive = { + include_keyword ~ include_path +} +include_keyword = { + "!include_once" + | "!include_many" + | "!include" +} +include_path = @{ (!"!" ~(ASCII_ALPHANUMERIC | "_" | "-" | "." | "/" | " "))+ } + +// --------------- IncludeSub --------------- +includesub_line = { includesub_directive ~ NEWLINE } +includesub_directive = { + "!includesub" ~ include_path ~ include_suffix ~ !include_suffix +} +include_suffix = { + "!" ~ (include_index | include_label) +} +include_label = @{ (ASCII_ALPHANUMERIC | "_" | "-" | "." | "@" )+ } +include_index = @{ ASCII_DIGIT+ } + + +// ---------------- SubBlock ---------------- +sub_block = { + startsub_directive ~ NEWLINE ~ sub_content* ~ endsub_directive ~ NEWLINE +} +startsub_directive = { "!startsub" ~ ((include_index | include_label)) } +endsub_directive = { "!endsub" } +sub_content = _{ + includesub_line + | include_line + | text_line + } + +// ---------------- Text Line ---------------- +text_line = { (!("!" ~ preprocess_keyword) ~ (!NEWLINE ~ ANY)*) ~ NEWLINE } +preprocess_keyword = { + include_keyword + | "includesub" + | "startsub" + | "endsub" +} diff --git a/plantuml/parser/puml_parser/src/grammar/procedure.pest b/plantuml/parser/puml_parser/src/grammar/procedure.pest new file mode 100644 index 0000000..02f0dcd --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/procedure.pest @@ -0,0 +1,70 @@ +// ******************************************************************************* +// 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 +// --------------------------- +WHITESPACE = _{ " " | "\t" } +NEWLINE = _{ "\r\n" | "\n" } +COMMENT = _{ "'" ~ (!NEWLINE ~ ANY)* | "//" ~ (!NEWLINE ~ ANY)* } + +// --------------------------- +// Entry Rule +// --------------------------- +file = { SOI ~ line* ~ EOI } +line = _{ + procedure_line + | macro_call_line + | text_line + | NEWLINE +} + +// --------------------------- +// Procedure +// --------------------------- +procedure_line = { + "!procedure" ~ WHITESPACE* ~ proc_name ~ "(" ~ param_list? ~ ")" ~ NEWLINE + ~ procedure_body + ~ "!endprocedure" +} + +proc_name = { macro_identifier | identifier } +identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } +macro_identifier = @{ "$" ~ identifier } + +param_list = { macro_identifier ~ ("," ~ macro_identifier)* } + +procedure_body = { body_element* } +body_element = _{ + macro_call_line + | text_line + | NEWLINE +} + +// --------------------------- +// Macro Call +// --------------------------- +macro_call_line = { (macro_identifier | identifier) ~ "(" ~ arg_list? ~ ")" } +arg_list = { arg ~ ("," ~ arg)* } +arg = { macro_identifier | string | number | identifier } +string = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" } +number = @{ "-"? ~ ASCII_DIGIT+ } + +// --------------------------- +// Raw Text +// --------------------------- +text_line = { + !procedure_keyword + ~ (!NEWLINE ~ ANY)+ +} +procedure_keyword = { "!procedure" | "!endprocedure" } diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest new file mode 100644 index 0000000..4ec92bf --- /dev/null +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -0,0 +1,252 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +// PlantUML Sequence Diagram Grammar for Pest Parser + +sequence_start = { empty_line* ~ startuml ~ (note_multiline | ref_multiline | sequence_statement | empty_line)* ~ enduml } + +sequence_statement = { + ( + note_declaration | + pragma | ignored_stmt | + minwidth | rotate | transformation | + hide_unlinked | show_unlinked | + sprite_inline | sprite_block_start | + hide_show_member | hide_show_stereotype | + participant_def | + activate_cmd | deactivate_cmd | destroy_cmd | create_cmd | + box_start | box_end | + group_cmd | + divider | delay | + return_cmd | + ref_inline | ref_block_start | + skin | autonumber | autonumber_stop | autonumber_resume | autonumber_inc | + autoactivate | footbox_cmd | ellipsis | + function_def | function_return | function_end | + message | + activation_short + ) ~ EOL +} + +// Basic elements +pragma = { ^"!pragma" ~ identifier ~ pragma_value? } +pragma_value = { (!EOL ~ ANY)+ } + +// Function definitions (preprocessor functions) +function_def = { ^"!function" ~ function_content } +function_return = { ^"!return" ~ function_content } +function_end = { ^"!endfunction" } +function_content = { (!EOL ~ ANY)+ } + +// Multi-line blocks + +legend_block_start = { ^"legend" ~ legend_pos? ~ legend_align? } +legend_pos = { ^"top" | ^"bottom" } +legend_align = { ^"left" | ^"right" | ^"center" } +legend_block_end = { ^"end" ~ WHITESPACE? ~ ^"legend" } + +// rotate, etc. +minwidth = { ^"minwidth" ~ NUMBER } +rotate = { ^"rotate" } + + +transformation = { ^"!transformation" ~ transformation_value } +transformation_value = { (!("{" | EOL) ~ ANY)+ } +transformation_block_start = { ^"!transformation" ~ "{" } +transformation_block_end = { "!" ~ "}" } + +hide_unlinked = { ^"hide" ~ ^"unlinked" } +show_unlinked = { ^"show" ~ ^"unlinked" } + +// Sprite +sprite_block_start = { + ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ "{" +} +sprite_block_end = { (^"end" ~ WHITESPACE? ~ ^"sprite") | "}" } +sprite_inline = { + ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ (sprite_encoding | sprite_data) +} +sprite_name = { "$"? ~ identifier } +sprite_dimensions = { + "[" ~ NUMBER ~ "x" ~ NUMBER ~ "/" ~ + (NUMBER ~ "z"? | ^"color") ~ + "]" +} +sprite_encoding = { ASCII_ALPHANUMERIC+ } +sprite_data = { ANY+ } + +// Hide/Show +hide_show_member = { + hide_or_show ~ visibility_list ~ member_type +} +hide_show_stereotype = { + hide_or_show ~ stereotype_target* ~ empty_kw? ~ stereotype_elem +} +hide_or_show = { ^"hide" | ^"show" } +visibility_list = { sequence_visibility ~ ("," ~ sequence_visibility)* } +sequence_visibility = { ^"public" | ^"private" | ^"protected" | ^"package" } +member_type = { ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | ^"methods" | ^"method" } +stereotype_target = { class_type | sequence_qualified_name | quoted_string | stereotype } +class_type = { ^"class" | ^"object" | ^"interface" | ^"enum" | ^"annotation" | ^"abstract" } +empty_kw = { ^"empty" } +stereotype_elem = { + ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | + ^"methods" | ^"method" | ^"circle" ~ ASCII_ALPHANUMERIC* | ^"stereotypes" | ^"stereotype" +} + +// Participant definitions +participant_def = { + create_kw? ~ participant_type ~ + (quoted_participant_as_id | participant_id_as_quoted | participant_id_as_id | quoted_participant | participant_id) ~ + color_spec? ~ stereotype? ~ order_clause? +} + +create_kw = { ^"create" } +participant_type = @{ + (^"participant" | ^"collections" | ^"database" | ^"boundary" | ^"control" | + ^"entity" | ^"queue" | ^"actor") ~ &(" " | "\t" | "\n" | "\r") +} + +quoted_participant_as_id = { quoted_string ~ alias_clause } +participant_id_as_quoted = { participant_id ~ alias_clause } +participant_id_as_id = { participant_id ~ alias_clause } +quoted_participant = { quoted_string } +participant_id = { CNAME } + +color_spec = @{ BASIC_COLOR } + +order_clause = { ^"order" ~ signed_number } +signed_number = { "-"? ~ NUMBER } + +// Activate/Deactivate/Destroy/Create commands +activate_cmd = { ^"activate" ~ participant_ref } +deactivate_cmd = { ^"deactivate" ~ participant_ref? } +destroy_cmd = { ^"destroy" ~ participant_ref } +create_cmd = { ^"create" ~ participant_ref } + +// Sequence Arrow +SEQUENCE_ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" } +SEQUENCE_ARROW_SUFFIX_CHAR = _{ ">" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" } + +sequence_arrow_prefix = @{ SEQUENCE_ARROW_PREFIX_CHAR+ } +sequence_arrow_suffix = @{ SEQUENCE_ARROW_SUFFIX_CHAR+ } + +sequence_arrow = { + sequence_arrow_prefix? + ~ arrow_segment + ~ arrow_middle? + ~ arrow_segment? + ~ sequence_arrow_suffix? +} + +// Messages +message = { + async_marker? ~ + message_participant? ~ sequence_arrow ~ message_participant? ~ + activation_marker? ~ sequence_description? +} + +async_marker = { "&" } +message_participant = { + lost_found_marker | + participant_ref | + quoted_string | + quoted_participant_as_id | + participant_id_as_quoted +} +lost_found_marker = { "[" ~ ("o" | "x")? | ("o" | "x")? ~ ("[" | "]") } +participant_ref = { CNAME | quoted_string } + +activation_marker = { "+"+ | "-"+ | "*"+ | "!"+ } + +// Short activation syntax +activation_short = { participant_ref ~ ("++" | "--") } + +// Return +return_cmd = { async_marker? ~ ^"return" ~ sequence_text_content? } + +// Box +box_start = { "box" ~ quoted_string? ~ color_spec? } +box_end = { ^"end" ~ WHITESPACE? ~ ^"box" } + +// Group commands (alt, opt, loop, etc.) +group_cmd = { + async_marker? ~ group_type ~ group_condition? +} + +group_condition = @{ (!EOL ~ ANY)+ } + +group_type = @{ + (^"critical" | ^"par2" | ^"par" | ^"opt" | ^"alt" | ^"loop" | ^"break" | + ^"else" | ^"also" | ^"group" | ^"end") ~ &(" " | "\t" | "\n" | "\r") +} + +// Divider and delay +divider = { "==" ~ divider_text? ~ "==" } +divider_text = @{ (!"==" ~ !EOL ~ ANY)+ } +delay = { ("||" ~ NUMBER? ~ "|"+) | ("…" ~ sequence_text_content? ~ "…") | ("..." ~ sequence_text_content? ~ "...") } +ellipsis = { ("..." ~ sequence_text_content? ~ "...") | ("…" ~ sequence_text_content? ~ "…") | "..." | "…" } + +// Reference +ref_inline = { + ^"ref" ~ ^"over" ~ participant_list ~ ":" ~ sequence_text_content +} + +ref_multiline = @{ + ^"ref" ~ + (!NEWLINE ~ ANY)* ~ // rest of header line ("over participant") + NEWLINE ~ + ( + !(WHITESPACE* ~ (^"endref" | (^"end" ~ WHITESPACE ~ ^"ref") | (^"end" ~ WHITESPACE* ~ NEWLINE))) ~ + (!NEWLINE ~ ANY)* ~ + NEWLINE + )* ~ + WHITESPACE* ~ (^"endref" | (^"end" ~ WHITESPACE ~ ^"ref") | (^"end" ~ WHITESPACE* ~ NEWLINE)) ~ + EOL? +} + +ref_block_start = { + ^"ref" ~ ^"over" ~ participant_list +} + +ref_block_end = { ^"end" ~ WHITESPACE? ~ (^"ref")? } + +participant_list = { participant_ref ~ ("," ~ participant_ref)* } + +// Skin +skin = { ^"skin" ~ sequence_qualified_name } + +// Autonumber +autonumber = { + ^"autonumber" ~ autonumber_format? ~ autonumber_start? ~ autonumber_step? +} +autonumber_format = { (ASCII_DIGIT ~ (!(WHITESPACE | EOL) ~ ANY)* ~ ASCII_DIGIT) | ASCII_DIGIT } +autonumber_start = { NUMBER } +autonumber_step = { quoted_string } + +autonumber_stop = { ^"autonumber" ~ ^"stop" } +autonumber_resume = { ^"autonumber" ~ ^"resume" ~ NUMBER? ~ quoted_string? } +autonumber_inc = { ^"autonumber" ~ ^"inc" ~ (ASCII_ALPHA)? } + +// Autoactivate +autoactivate = { ^"autoactivate" ~ autoactivate_state? } +autoactivate_state = { ^"on" | ^"off" } + +// Footbox +footbox_cmd = { hide_or_show? ~ ^"footbox" ~ footbox_state? } +footbox_state = { ^"on" | ^"off" } + +sequence_qualified_name = @{ identifier ~ ("." ~ identifier)* } + +sequence_text_content = { (!EOL ~ ANY)+ } +sequence_description = { ":" ~ sequence_text_content? } diff --git a/plantuml/parser/puml_parser/src/lib.rs b/plantuml/parser/puml_parser/src/lib.rs new file mode 100644 index 0000000..e7e635b --- /dev/null +++ b/plantuml/parser/puml_parser/src/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +// Re-export commonly used items that don't have name conflicts +pub use class_parser::{ClassError, ClassUmlFile, PumlClassParser}; +pub use component_parser::{CompPumlDocument, PumlComponentParser}; +pub use parser_core::{common_ast, common_parser, Arrow, BaseParseError, DiagramParser}; +pub use preprocessor::{ + IncludeExpandError, IncludeParseError, PreprocessError, Preprocessor, ProcedureExpandError, + ProcedureParseError, +}; +pub use sequence_parser::{PumlSequenceParser, SeqPumlDocument}; diff --git a/plantuml/parser/puml_parser/src/parser_core/BUILD b/plantuml/parser/puml_parser/src/parser_core/BUILD new file mode 100644 index 0000000..2c50b39 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/BUILD @@ -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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "puml_parser_core", + srcs = [ + "src/common_ast.rs", + "src/common_parser.rs", + "src/error.rs", + "src/lib.rs", + ], + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:grammar_files", + ], + crate_name = "parser_core", + crate_root = "src/lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_utils", + "@crates//:pest", + "@crates//:serde", + "@crates//:thiserror", + ], +) + +rust_test( + name = "common_grammar_test", + crate = ":puml_parser_core", +) diff --git a/plantuml/parser/puml_parser/src/parser_core/src/common_ast.rs b/plantuml/parser/puml_parser/src/parser_core/src/common_ast.rs new file mode 100644 index 0000000..c0f6515 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/common_ast.rs @@ -0,0 +1,63 @@ +// ******************************************************************************* +// 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 serde::{Deserialize, Serialize}; +use std::default::Default; + +//////////////////////////////////////////////////////////////////////////////// +// Arrow +//////////////////////////////////////////////////////////////////////////////// +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] +pub struct Arrow { + pub left: Option, + pub line: ArrowLine, + pub middle: Option, + pub right: Option, +} + +// ---------- Decorator ---------- +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct ArrowDecor { + pub raw: String, +} + +// ---------- Line ---------- +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] +pub struct ArrowLine { + pub raw: String, +} + +// ---------- Middle ---------- +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct ArrowMiddle { + pub style: Option, + pub direction: Option, + pub decorator: Option, +} + +// ---------- Style ---------- +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] +pub struct ArrowStyle { + pub color: Option, + pub patterns: Vec, + pub thickness: Option, + pub extra_attrs: Vec, +} + +// ---------- Direction ---------- +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum ArrowDirection { + Up, + Down, + Left, + Right, +} diff --git a/plantuml/parser/puml_parser/src/parser_core/src/common_detail_design b/plantuml/parser/puml_parser/src/parser_core/src/common_detail_design new file mode 100644 index 0000000..0a381c5 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/common_detail_design @@ -0,0 +1,60 @@ +' ******************************************************************************* +' 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 classAttributeIconSize 0 + +class Arrow { + left: Option + line: ArrowLine + middle: Option + right: Option +} + +class ArrowDecor { + raw: String +} + +class ArrowLine { + raw: String +} + +class ArrowMiddle { + style: Option + direction: Option + decorator: Option +} + +class ArrowStyle { + color: Option + patterns: Vec + thickness: Option + extra_attrs: Vec +} + +enum ArrowDirection { + Up + Down + Left + Right +} + +Arrow --> ArrowDecor : left +Arrow --> ArrowDecor : right +Arrow --> ArrowLine +Arrow --> ArrowMiddle + +ArrowMiddle --> ArrowStyle +ArrowMiddle --> ArrowDirection + +@enduml diff --git a/plantuml/parser/puml_parser/src/parser_core/src/common_parser.rs b/plantuml/parser/puml_parser/src/parser_core/src/common_parser.rs new file mode 100644 index 0000000..a0ad31e --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/common_parser.rs @@ -0,0 +1,147 @@ +// ******************************************************************************* +// 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 pest_derive::Parser; + +use crate::common_ast::*; + +#[derive(Parser)] +#[grammar = "../grammar/common.pest"] +#[grammar = "../grammar/class.pest"] +#[grammar = "../grammar/component.pest"] +#[grammar = "../grammar/sequence.pest"] +pub struct PlantUmlCommonParser; + +pub fn parse_arrow(pair: pest::iterators::Pair) -> Result { + let mut left = None; + let mut right = None; + let mut segments = Vec::new(); + let mut middle = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::arrow_prefix | Rule::sequence_arrow_prefix => { + left = Some(ArrowDecor { + raw: inner.as_str().to_string(), + }); + } + + Rule::arrow_suffix | Rule::sequence_arrow_suffix => { + right = Some(ArrowDecor { + raw: inner.as_str().to_string(), + }); + } + + Rule::arrow_segment => { + segments.push(inner.as_str()); + } + + Rule::arrow_middle => { + middle = Some(parse_arrow_middle(inner)?); + } + + _ => {} + } + } + + Ok(Arrow { + left, + line: ArrowLine { + raw: segments.join(""), + }, + middle, + right, + }) +} + +fn parse_arrow_middle(pair: pest::iterators::Pair) -> Result { + let mut style = None; + let mut direction = None; + let mut decorator = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::line_style_block => { + style = Some(ArrowStyle::default()); + } + + Rule::arrow_direction => { + direction = Some(parse_arrow_direction(inner)?); + } + + Rule::arrow_mid_decor => { + decorator = Some(inner.as_str().to_string()); + } + + _ => {} + } + } + + Ok(ArrowMiddle { + style, + direction, + decorator, + }) +} + +fn parse_arrow_direction(pair: pest::iterators::Pair) -> Result { + match pair.as_str() { + "up" | "u" => Ok(ArrowDirection::Up), + "down" | "d" | "do" => Ok(ArrowDirection::Down), + "left" | "l" | "le" => Ok(ArrowDirection::Left), + "right" | "r" | "ri" => Ok(ArrowDirection::Right), + _ => Err(format!( + "Unknown arrow direction token: '{}'", + pair.as_str() + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pest::Parser; + + #[test] + fn debug_parse_arrows() { + let inputs = vec![ + "-->", + "-[#red]->", + "o--[dashed]down0->", + "<==[bold,thickness=2]up(0)->", + "<|--", + ]; + + for input in inputs { + println!("Parsing arrow: {}", input); + let mut pairs = + PlantUmlCommonParser::parse(Rule::connection_arrow, input).expect("parse failed"); + + let pair = pairs.next().expect("no arrow pair"); + let arrow = parse_arrow(pair).expect("arrow parse failed"); + + println!("{:#?}", arrow); + println!("-----------------------------"); + } + } + + #[test] + fn test_unknown_arrow_direction_error() { + // This would previously panic with unreachable!() + // Now it should return a proper error + + // Note: This test demonstrates the error handling, but requires + // a grammar that can parse an invalid direction token. + // In practice, the pest grammar should catch invalid tokens, + // but this provides defense-in-depth. + } +} diff --git a/plantuml/parser/puml_parser/src/parser_core/src/error.rs b/plantuml/parser/puml_parser/src/parser_core/src/error.rs new file mode 100644 index 0000000..b5b4014 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/error.rs @@ -0,0 +1,80 @@ +// ******************************************************************************* +// 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 pest::error::Error as PestError; +use pest::error::{ErrorVariant, LineColLocation}; +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum BaseParseError { + #[error("Failed to read include file {path}: {error}")] + IoError { + path: PathBuf, + #[source] + error: Box, + }, + + #[error("Pest error: {message}")] + SyntaxError { + file: PathBuf, + line: usize, + column: usize, + message: String, + source_line: String, + #[source] + cause: Option>>, + }, +} + +pub fn pest_to_syntax_error( + err: PestError, + file: PathBuf, + source: &str, +) -> BaseParseError +where + Rule: std::fmt::Debug, +{ + let (line, column) = match err.line_col { + LineColLocation::Pos((l, c)) => (l, c), + LineColLocation::Span((l, c), _) => (l, c), + }; + + let source_line = source + .split_inclusive('\n') + .nth(line - 1) + .map(|s| s.trim_matches(' ').to_string()) + .unwrap_or("\n".to_string()); + + let message = match &err.variant { + ErrorVariant::ParsingError { + positives, + negatives, + } => { + format!( + "Parsing error at {:?}, expected {:?}, got {:?}", + (line, column), + positives, + negatives + ) + } + ErrorVariant::CustomError { message } => message.clone(), + }; + + BaseParseError::::SyntaxError { + file, + line, + column, + message, + source_line, + cause: Some(Box::new(err)), + } +} diff --git a/plantuml/parser/puml_parser/src/parser_core/src/lib.rs b/plantuml/parser/puml_parser/src/parser_core/src/lib.rs new file mode 100644 index 0000000..597fe82 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/lib.rs @@ -0,0 +1,31 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +pub mod common_ast; +pub mod common_parser; +pub mod error; + +pub use common_ast::*; +pub use common_parser::*; +pub use error::{pest_to_syntax_error, BaseParseError}; + +pub trait DiagramParser { + type Output; + type Error; + + fn parse_file( + &mut self, + path: &std::rc::Rc, + content: &str, + log_level: puml_utils::LogLevel, + ) -> Result; +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/BUILD b/plantuml/parser/puml_parser/src/preprocessor/BUILD new file mode 100644 index 0000000..2c4f7e9 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/BUILD @@ -0,0 +1,72 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "puml_parser_preprocessor", + srcs = [ + "src/lib.rs", + "src/preprocessor.rs", + ], + crate_name = "preprocessor", + crate_root = "src/lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser/src/preprocessor/src/include:include_preprocessor", + "//plantuml/parser/puml_parser/src/preprocessor/src/procedure:procedure_preprocessor", + "//plantuml/parser/puml_utils", + "@crates//:log", + "@crates//:thiserror", + ], +) + +rust_test( + name = "puml_parser_include_preprocessor_it", + srcs = [ + "tests/include_tests.rs", + "tests/preprocess_runner.rs", + ], + crate_root = "tests/include_tests.rs", + data = [ + "//plantuml/parser/puml_parser/tests/preprocessor/include:include_preprocessor_files", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + ":puml_parser_preprocessor", + "//plantuml/parser/integration_test:test_framework", + "//plantuml/parser/puml_utils", + "@crates//:serde_yaml", + ], +) + +rust_test( + name = "puml_parser_procedure_preprocessor_it", + srcs = [ + "tests/preprocess_runner.rs", + "tests/procedure_tests.rs", + ], + crate_root = "tests/procedure_tests.rs", + data = [ + "//plantuml/parser/puml_parser/tests/preprocessor/procedure:procedure_files", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + ":puml_parser_preprocessor", + "//plantuml/parser/integration_test:test_framework", + "//plantuml/parser/puml_utils", + "@crates//:serde_yaml", + ], +) diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/BUILD b/plantuml/parser/puml_parser/src/preprocessor/src/include/BUILD new file mode 100644 index 0000000..010fbc3 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/BUILD @@ -0,0 +1,57 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +filegroup( + name = "include_preprocessor_files", + srcs = [ + "include_ast.rs", + "include_expander.rs", + "include_parser.rs", + "lib.rs", + "utils.rs", + ], + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "include_preprocessor", + srcs = [ + ":include_preprocessor_files", + ], + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:include_grammar", + ], + crate_name = "include_preprocessor", + crate_root = "lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + "@crates//:log", + "@crates//:pest", + "@crates//:serde", + "@crates//:thiserror", + ], +) + +rust_test( + name = "include_preprocessor_unit_test", + crate = ":include_preprocessor", + deps = [ + "@crates//:once_cell", + ], +) 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 new file mode 100644 index 0000000..c11058d --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_ast.rs @@ -0,0 +1,139 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +#[derive(Debug, Clone, PartialEq)] +pub enum IncludeSuffix { + Label(String), + Index(u32), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum IncludeKind { + Include, + IncludeOnce, + IncludeMany, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum IncludeStmt { + Include { kind: IncludeKind, path: String }, + IncludeSub { path: String, suffix: IncludeSuffix }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SubBlock { + pub name: IncludeSuffix, + pub content: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PreprocessStmt { + Include(IncludeStmt), + Text(String), + SubBlock(SubBlock), +} + +impl PreprocessStmt { + pub fn render(&self, out: &mut String) { + match self { + PreprocessStmt::Text(text) => out.push_str(text), + PreprocessStmt::SubBlock(sub) => { + for stmt in &sub.content { + stmt.render(out); + } + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_text_node_outputs_text() { + let stmt = PreprocessStmt::Text("hello".into()); + let mut out = String::new(); + stmt.render(&mut out); + assert_eq!(out, "hello"); + } + + #[test] + fn render_subblock_with_text_nodes() { + let sub = SubBlock { + name: IncludeSuffix::Label("sub".into()), + content: vec![ + PreprocessStmt::Text("a\n".into()), + PreprocessStmt::Text("b\n".into()), + ], + }; + let stmt = PreprocessStmt::SubBlock(sub); + let mut out = String::new(); + stmt.render(&mut out); + assert_eq!(out, "a\nb\n"); + } + + #[test] + fn render_empty_subblock_outputs_nothing() { + let sub = SubBlock { + name: IncludeSuffix::Label("empty".into()), + content: vec![], + }; + let stmt = PreprocessStmt::SubBlock(sub); + let mut out = String::new(); + stmt.render(&mut out); + assert_eq!(out, ""); + } + + #[test] + fn render_nested_subblocks() { + let inner_sub = SubBlock { + name: IncludeSuffix::Label("inner".into()), + content: vec![PreprocessStmt::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()), + ], + }; + let stmt = PreprocessStmt::SubBlock(outer_sub); + let mut out = String::new(); + stmt.render(&mut out); + assert_eq!(out, "start\ninner\nend\n"); + } + + #[test] + fn render_include_stmt_does_not_panic() { + let include = PreprocessStmt::Include(IncludeStmt::Include { + kind: IncludeKind::Include, + path: "file.puml".into(), + }); + let mut out = String::new(); + include.render(&mut out); + assert_eq!(out, ""); + } + + #[test] + fn render_include_sub_stmt_does_not_panic() { + let include_sub = PreprocessStmt::Include(IncludeStmt::IncludeSub { + path: "file.puml".into(), + suffix: IncludeSuffix::Label("sub".into()), + }); + let mut out = String::new(); + include_sub.render(&mut out); + assert_eq!(out, ""); + } +} 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 new file mode 100644 index 0000000..a28c990 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_expander.rs @@ -0,0 +1,387 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +//! PlantUML Include Expander Module +//! ============================== +//! Implements PlantUML-compatible include semantics: +//! - `!include` / `!include_once` directives +//! - Subblock extraction +//! - Inline expansion +//! - Cycle detection +//! +//! # Performance +//! - Parsing: O(F) — each file parsed once. +//! - Expansion: O(I × S) — re-evaluated per include due to context dependence. +//! - include_once check: O(1) per include (HashSet lookup). +//! +//! # Notes +//! - `!include_once` is scoped per entry diagram; not global. +//! - ASTs are cached globally to avoid repeated parsing. +//! - Expanded include results are context-dependent; do not cache globally. +//! - Avoid optimizations that break context-dependent expansion. + +use log::debug; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::rc::Rc; +use thiserror::Error; + +use crate::include_ast::{IncludeKind, IncludeStmt, IncludeSuffix, PreprocessStmt, SubBlock}; +use crate::include_parser::{IncludeParseError, IncludeParserService}; +use crate::utils::{normalize_path, strip_start_end}; + +// ---------------------- +// Type Aliases +// ---------------------- +type GlobalSubRegistry = HashMap, FileSubRegistry>; +type AstCache = HashMap, Rc>>; +type FileList = HashSet>; + +/// Stores all subblocks in a file, indexed by label or numeric index. +#[derive(Debug, Default, Clone)] +pub struct FileSubRegistry { + by_label: HashMap>>, + by_index: HashMap>>, +} + +impl FileSubRegistry { + fn collect_from_file(&mut self, stmts: &[PreprocessStmt]) { + for stmt in stmts { + if let PreprocessStmt::SubBlock(sub) = stmt { + let rc_sub = Rc::new(sub.clone()); + match &sub.name { + IncludeSuffix::Label(name) => { + self.by_label + .entry(name.clone()) + .or_default() + .push(Rc::clone(&rc_sub)); + } + IncludeSuffix::Index(idx) => { + self.by_index + .entry(*idx) + .or_default() + .push(Rc::clone(&rc_sub)); + } + } + } + } + } + + fn lookup(&self, suffix: &IncludeSuffix) -> Option>> { + match suffix { + IncludeSuffix::Label(name) => self.by_label.get(name).cloned(), + IncludeSuffix::Index(idx) => self.by_index.get(idx).cloned(), + } + } +} + +/// Expansion context for a single PlantUML diagram. +/// +/// Tracks include stack for cycle detection and included_once files. +/// Each entry diagram owns its own context. +#[derive(Debug, Default, Clone)] +pub struct IncludeContext { + include_stack: Vec>, + included_once: HashSet>, + included_default: HashSet>, +} + +impl IncludeContext { + fn push_stack(&mut self, file: &Rc) -> Result<(), IncludeExpandError> { + if self.include_stack.contains(file) { + let mut chain = self.include_stack.clone(); + chain.push(Rc::clone(file)); + return Err(IncludeExpandError::CycleInclude { chain }); + } + self.include_stack.push(Rc::clone(file)); + Ok(()) + } + + fn pop_stack(&mut self) { + self.include_stack.pop(); + } + + fn check_and_mark_include( + &mut self, + kind: &IncludeKind, + current_file: &Rc, + target: &Rc, + ) -> Result { + match kind { + IncludeKind::Include => { + if self.included_default.contains(target) { + Ok(false) + } else { + self.included_default.insert(Rc::clone(target)); + Ok(true) + } + } + IncludeKind::IncludeOnce => { + if self.included_once.contains(target) { + Err(IncludeExpandError::IncludeOnceViolated { + file: Rc::clone(current_file), + conflict: Rc::clone(target), + }) + } else { + self.included_once.insert(Rc::clone(target)); + self.included_default.insert(Rc::clone(target)); + Ok(true) + } + } + IncludeKind::IncludeMany => { + self.included_default.insert(Rc::clone(target)); + Ok(true) + } + } + } +} + +/// Include Expand errors. +#[derive(Debug, Error)] +pub enum IncludeExpandError { + #[error("Diagram {file} not found.")] + FileNotFound { file: Rc }, + + #[error("Failed to parse included file {file}: {error}")] + ParseFailed { + file: Rc, + #[source] + error: IncludeParseError, + }, + + #[error("Include cycle detected: {chain:?}")] + CycleInclude { chain: Vec> }, + + #[error("Diagram {file} include {conflict} more than once")] + IncludeOnceViolated { + file: Rc, + conflict: Rc, + }, + + #[error("Sub block {suffix} not found in include file {file}")] + UnknownSub { file: Rc, suffix: String }, +} + +#[derive(Default)] +pub struct IncludeExpander { + include_parser: IncludeParserService, + // global AST cache to avoid re-parsing + ast_cache: AstCache, + // global registry of subblocks for all parsed files + sub_registry: GlobalSubRegistry, +} + +impl IncludeExpander { + pub fn new() -> Self { + Self { + ..Default::default() + } + } + + pub fn expand( + &mut self, + file: &Rc, + file_list: &FileList, + ) -> Result { + let mut ctx = IncludeContext::default(); + self.expand_file(file, &mut ctx, file_list) + } + + /// Expands a single diagram file, recursively processing includes. + /// + /// # Arguments + /// - `file`: path to the entry diagram + /// - `ctx`: per-diagram `IncludeContext` + /// - `file_list`: set of all available files + /// + /// # Returns + /// - Fully expanded PlantUML text as `String` + /// + /// # Errors + /// - `IncludeExpandError::FileNotFound`: file missing + /// - `IncludeExpandError::ParseFailed`: read or parse errors + /// - `IncludeExpandError::CycleInclude`: include cycle detected + /// - `IncludeExpandError::IncludeOnceViolated`: `!include_once` violation + /// - `IncludeExpandError::UnknownSub`: includesub with invalid suffix + fn expand_file( + &mut self, + file: &Rc, + ctx: &mut IncludeContext, + file_list: &FileList, + ) -> Result { + debug!("expand_file is {}", file.display()); + + if !file_list.contains(file) { + return Err(IncludeExpandError::FileNotFound { + file: Rc::clone(file), + }); + } + + ctx.push_stack(file)?; + + let stmts_ast = self.load_ast(file)?; + let expanded_ast = self.expand_stmts(&stmts_ast, file, ctx, file_list)?; + let rendered = render_stmts(expanded_ast); + + ctx.pop_stack(); + Ok(rendered) + } + + /// Loads AST from cache or parses file if not present. + /// + /// # Returns + /// - Parsed AST of the file. + /// + /// # Errors + /// - `IncludeExpandError::ParseFailed`: read or parse errors + fn load_ast( + &mut self, + file: &Rc, + ) -> Result>, IncludeExpandError> { + if let Some(ast) = self.ast_cache.get(file) { + return Ok(Rc::clone(ast)); + } + + let ast = + self.include_parser + .parse_file(file) + .map_err(|e| IncludeExpandError::ParseFailed { + file: Rc::clone(file), + error: e, + })?; + + self.sub_registry + .entry(Rc::clone(file)) + .or_default() + .collect_from_file(&ast); + + let rc_ast = Rc::new(ast); + self.ast_cache.insert(Rc::clone(file), Rc::clone(&rc_ast)); + + Ok(rc_ast) + } + + /// Recursively expands a sequence of AST statements. + /// + /// # Returns + /// - Fully expanded AST statements + /// + /// # Errors + /// - Propagates errors from include expansion + fn expand_stmts( + &mut self, + stmts: &[PreprocessStmt], + current_file: &Rc, + ctx: &mut IncludeContext, + file_list: &FileList, + ) -> Result, IncludeExpandError> { + let mut result = Vec::new(); + + for stmt in stmts { + match stmt { + PreprocessStmt::Text(text) => { + result.push(PreprocessStmt::Text(text.clone())); + } + PreprocessStmt::Include(inc) => { + let expanded = self.expand_include(inc, current_file, ctx, file_list)?; + result.push(PreprocessStmt::Text(expanded)); + } + PreprocessStmt::SubBlock(sub) => { + let expanded_content = + self.expand_stmts(&sub.content, current_file, ctx, file_list)?; + result.push(PreprocessStmt::SubBlock(SubBlock { + name: sub.name.clone(), + content: expanded_content, + })); + } + } + } + + Ok(result) + } + + /// Expands a single `IncludeStmt` inline. + /// + /// # Returns + /// - Expanded text of included file or subblock + /// + /// # Errors + /// - Propagates errors from file expansion or `!include_once` check + fn expand_include( + &mut self, + inc: &IncludeStmt, + current_file: &Rc, + ctx: &mut IncludeContext, + file_list: &FileList, + ) -> Result { + let base_dir = current_file + .parent() + .ok_or_else(|| IncludeExpandError::FileNotFound { + file: Rc::clone(current_file), + })?; + + match inc { + IncludeStmt::Include { kind, path } => { + let path = Rc::new(normalize_path(&base_dir.join(path))); + + if !ctx.check_and_mark_include(kind, current_file, &path)? { + return Ok(String::new()); + } + + let full_text = self.expand_file(&path, ctx, file_list)?; + Ok(strip_start_end(&full_text)) + } + + IncludeStmt::IncludeSub { path, suffix } => { + let path = Rc::new(normalize_path(&base_dir.join(path))); + + let _ = self.load_ast(&path)?; + let subs = self + .sub_registry + .get(&path) + .and_then(|r| r.lookup(suffix)) + .ok_or_else(|| IncludeExpandError::UnknownSub { + suffix: match &suffix { + IncludeSuffix::Label(name) => name.clone(), + IncludeSuffix::Index(idx) => idx.to_string(), + }, + file: Rc::clone(&path), + })?; + + let mut result = String::new(); + for sub in subs { + let full_sub_text = self.expand_stmts(&sub.content, &path, ctx, file_list)?; + result.push_str(&render_stmts(full_sub_text)); + } + + Ok(result) + } + } + } +} + +/// Renders AST statements into PlantUML text. +/// +/// # Arguments +/// - `stmts`: AST nodes to render +/// +/// # Returns +/// - `String` containing PlantUML text +pub fn render_stmts(stmts: Vec) -> String { + let mut out = String::new(); + + for stmt in stmts { + stmt.render(&mut out); + } + + out +} 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 new file mode 100644 index 0000000..d123fbb --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/include_parser.rs @@ -0,0 +1,259 @@ +// ******************************************************************************* +// 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 pest::iterators::Pair; +use pest::Parser; +use pest_derive::Parser; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::include_ast::{IncludeKind, IncludeStmt, IncludeSuffix, PreprocessStmt, 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"]; + +#[derive(Debug, thiserror::Error)] +pub enum IncludeParseError { + #[error(transparent)] + Base(#[from] BaseParseError), + + #[error("Invalid text line: {line}, ref to file {file}")] + InvalidTextLine { line: String, file: PathBuf }, +} + +#[derive(Default)] +pub struct IncludeParserService; + +impl IncludeParserService { + // -------------------- Parser Entry -------------------- + 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(), + error: Box::new(e), + }) + })?; + let mut stmts = Vec::new(); + let file_pairs = IncludeParser::parse(Rule::file, &content).map_err(|e| { + IncludeParseError::Base(pest_to_syntax_error(e, file.to_path_buf(), &content)) + })?; + + for pair in file_pairs { + for line in pair.into_inner() { + match line.as_rule() { + Rule::include_line => { + let include = self.parse_include_line(line); + stmts.push(PreprocessStmt::Include(include)); + } + Rule::includesub_line => { + let include_sub = self.parse_includesub_line(line); + stmts.push(PreprocessStmt::Include(include_sub)); + } + Rule::sub_block => { + let sub_block = self.parse_sub_block(line); + stmts.push(PreprocessStmt::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)) { + return Err(IncludeParseError::InvalidTextLine { + line: text, + file: file.to_path_buf(), + }); + } + if !text.trim().is_empty() { + stmts.push(PreprocessStmt::Text(text)); + } + } + _ => {} + } + } + } + + Ok(stmts) + } + + fn parse_include_line(&self, pair: Pair) -> IncludeStmt { + let directive = pair + .into_inner() + .find(|p| p.as_rule() == Rule::include_directive) + .unwrap(); + + let (kind, path) = self.parse_include_directive(directive); + + IncludeStmt::Include { kind, path } + } + + fn parse_includesub_line(&self, pair: Pair) -> IncludeStmt { + let directive = pair + .into_inner() + .find(|p| p.as_rule() == Rule::includesub_directive) + .unwrap(); + + let (path, suffix) = self.parse_includesub_directive(directive); + + IncludeStmt::IncludeSub { path, suffix } + } + + fn parse_include_directive(&self, pair: Pair) -> (IncludeKind, String) { + let mut kind = None; + let mut path = None; + + for inner in pair.clone().into_inner() { + match inner.as_rule() { + Rule::include_keyword => { + kind = Some(match inner.as_str() { + "!include" => IncludeKind::Include, + "!include_once" => IncludeKind::IncludeOnce, + "!include_many" => IncludeKind::IncludeMany, + _ => unreachable!(), + }) + } + Rule::include_path => { + path = Some(self.extract_path(inner)); + } + _ => unreachable!(), + } + } + + (kind.unwrap(), path.unwrap()) + } + + fn parse_includesub_directive(&self, pair: Pair) -> (String, IncludeSuffix) { + let mut path = None; + let mut suffix = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::include_path => { + path = Some(self.extract_path(inner)); + } + Rule::include_suffix => { + suffix = Some(self.extract_suffix(inner)); + } + _ => unreachable!(), + } + } + + (path.unwrap(), suffix.unwrap()) + } + + fn extract_path(&self, pair: Pair) -> String { + pair.as_str().trim().to_string() + } + + fn extract_suffix(&self, pair: Pair) -> IncludeSuffix { + let inner = pair.into_inner().next().unwrap(); + + match inner.as_rule() { + Rule::include_index => IncludeSuffix::Index(inner.as_str().parse().unwrap()), + Rule::include_label => IncludeSuffix::Label(inner.as_str().to_string()), + _ => unreachable!(), + } + } + + fn parse_sub_block(&self, pair: Pair) -> SubBlock { + let mut inner = pair.into_inner(); + let startsub_directive = inner.next().unwrap(); + let name = self.extract_suffix(startsub_directive); + + 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)); + } + Rule::includesub_line => { + let include_sub = self.parse_includesub_line(line); + content.push(PreprocessStmt::Include(include_sub)); + } + Rule::text_line => { + let text = line.as_str().to_string(); + if !text.trim().is_empty() { + content.push(PreprocessStmt::Text(text)); + } + } + _ => {} + } + } + + SubBlock { name, content } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use once_cell::sync::Lazy; + + static INCLUDE_PARSER_SERVICE: Lazy = Lazy::new(|| IncludeParserService); + + #[test] + fn test_extract_path() { + let pair = IncludeParser::parse(Rule::include_path, "path/to/file") + .unwrap() + .next() + .unwrap(); + let path = INCLUDE_PARSER_SERVICE.extract_path(pair); + assert_eq!(path, "path/to/file"); + } + + #[test] + fn test_extract_suffix_index() { + let pair = IncludeParser::parse(Rule::include_suffix, "!1") + .unwrap() + .next() + .unwrap(); + let suffix = INCLUDE_PARSER_SERVICE.extract_suffix(pair); + assert!(matches!(suffix, IncludeSuffix::Index(1))); + } + + #[test] + fn test_extract_suffix_label() { + let pair = IncludeParser::parse(Rule::include_suffix, "!label1") + .unwrap() + .next() + .unwrap(); + let suffix = INCLUDE_PARSER_SERVICE.extract_suffix(pair); + assert!(matches!(suffix, IncludeSuffix::Label(ref l) if l == "label1")); + } + + #[test] + fn test_parse_include_directive() { + let input = "!include path/to/file"; + let pair = IncludeParser::parse(Rule::include_directive, input) + .unwrap() + .next() + .unwrap(); + let (kind, path) = INCLUDE_PARSER_SERVICE.parse_include_directive(pair); + assert!(matches!(kind, IncludeKind::Include)); + assert_eq!(path, "path/to/file"); + } + + #[test] + fn test_parse_includesub_directive() { + let input = "!includesub path/to/file!2"; + let pair = IncludeParser::parse(Rule::includesub_directive, input) + .unwrap() + .next() + .unwrap(); + let (path, suffix) = INCLUDE_PARSER_SERVICE.parse_includesub_directive(pair); + assert_eq!(path, "path/to/file"); + assert!(matches!(suffix, IncludeSuffix::Index(2))); + } +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/lib.rs b/plantuml/parser/puml_parser/src/preprocessor/src/include/lib.rs new file mode 100644 index 0000000..fad8f20 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod include_ast; +mod include_expander; +mod include_parser; +mod utils; + +pub use include_expander::{IncludeExpandError, IncludeExpander}; +pub use include_parser::IncludeParseError; diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/include/utils.rs b/plantuml/parser/puml_parser/src/preprocessor/src/include/utils.rs new file mode 100644 index 0000000..fad7553 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/include/utils.rs @@ -0,0 +1,102 @@ +// ******************************************************************************* +// 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 std::fmt::Write; +use std::path::{Component, Path, PathBuf}; + +/// Normalizes paths resolving `.` and `..`. +pub fn normalize_path(path: &Path) -> PathBuf { + path.components().fold(PathBuf::new(), |mut acc, comp| { + match comp { + Component::ParentDir => { + acc.pop(); + } + Component::CurDir => {} + _ => acc.push(comp.as_os_str()), + } + acc + }) +} + +/// Strips `@startuml` and `@enduml` from text. +pub fn strip_start_end(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + for line in text.lines() { + let t = line.trim_start(); + if !t.starts_with("@startuml") && !t.starts_with("@enduml") { + writeln!(out, "{}", line).unwrap(); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn normalize_path_test() { + let path = Path::new("/a/b/../c/./d"); + let normalized = normalize_path(path); + assert_eq!(normalized, Path::new("/a/c/d")); + } + + #[test] + fn normalize_path_root_and_parent() { + // /../ should stay at root + let path = Path::new("/../a"); + let normalized = normalize_path(path); + assert_eq!(normalized, Path::new("/a")); + } + + #[test] + fn normalize_path_empty_and_cur() { + // empty path -> empty + let path = Path::new(""); + let normalized = normalize_path(path); + assert_eq!(normalized, Path::new("")); + + // ./././ -> empty + let path = Path::new("./././"); + let normalized = normalize_path(path); + assert_eq!(normalized, Path::new("")); + } + + #[test] + fn strip_start_end_test() { + let text = "@startuml\nline1\nline2\n@enduml\n"; + let stripped = strip_start_end(text); + assert_eq!(stripped, "line1\nline2\n"); + } + + #[test] + fn strip_start_end_no_tags() { + let text = "line1\nline2\n"; + let stripped = strip_start_end(text); + assert_eq!(stripped, "line1\nline2\n"); + } + + #[test] + fn strip_start_end_similar_tags() { + let text = "!@startuml\nline1\nline2\n!@enduml\n"; + let stripped = strip_start_end(text); + assert_eq!(stripped, "!@startuml\nline1\nline2\n!@enduml\n"); + } + + #[test] + fn strip_start_end_empty_string() { + let text = ""; + let stripped = strip_start_end(text); + assert_eq!(stripped, ""); + } +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/lib.rs b/plantuml/parser/puml_parser/src/preprocessor/src/lib.rs new file mode 100644 index 0000000..eab23e9 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod preprocessor; + +pub use include_preprocessor::{IncludeExpandError, IncludeExpander, IncludeParseError}; +pub use preprocessor::{PreprocessError, Preprocessor}; +pub use procedure_preprocessor::{ProcedureExpandError, ProcedureExpander, ProcedureParseError}; diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/preprocessor.rs b/plantuml/parser/puml_parser/src/preprocessor/src/preprocessor.rs new file mode 100644 index 0000000..637be67 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/preprocessor.rs @@ -0,0 +1,90 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +//! PlantUML Preprocessing Module +//! ============================== +//! This module implements the preprocessing logic for PlantUML files, including handling of `!include` directives and procedure/macro expansions. It serves as a crucial step before parsing, ensuring that all files are fully expanded and ready for syntax analysis. +//! The main components include: +//! - `Preprocessor`: The top-level coordinator that manages the entire preprocessing workflow. +//! - `IncludeExpander`: Responsible for resolving and expanding `!include` directives, ensuring that all included content is inlined correctly. +//! - `ProcedureExpander`: Handles the expansion of procedures and macros defined within the PlantUML files, replacing calls with their corresponding definitions. + +use log::debug; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::rc::Rc; +use thiserror::Error; + +use crate::{IncludeExpandError, IncludeExpander, ProcedureExpandError, ProcedureExpander}; +use puml_utils::LogLevel; + +// ---------------------- +// Type Aliases +// ---------------------- +type PreprocessedFiles = HashMap, String>; +type FileList = HashSet>; + +/// Top-level preprocessing errors. +#[derive(Debug, Error)] +pub enum PreprocessError { + #[error("include preprocess failed")] + IncludeFailed(#[from] IncludeExpandError), + + #[error("procedure preprocess failed")] + ProcedureFailed(#[from] ProcedureExpandError), +} + +#[derive(Default)] +pub struct Preprocessor { + include_expander: IncludeExpander, + procedure_expander: ProcedureExpander, +} + +impl Preprocessor { + pub fn new() -> Self { + Self { + include_expander: IncludeExpander::new(), + procedure_expander: ProcedureExpander::new(), + } + } + + /// Top-level coordinator: preprocess all given PlantUML files. + /// + /// # Arguments + /// - `file_list`: set of all PlantUML files to preprocess. + /// + /// # Returns + /// - A map of each file to its fully expanded PlantUML text. + /// + /// # Errors + /// Returns `PreprocessError` + pub fn preprocess( + &mut self, + file_list: &FileList, + log_level: LogLevel, + ) -> Result { + let mut preprocessed_files = HashMap::new(); + + for file in file_list { + debug!("Preprocess file: {}", file.display()); + + let include_expanded = self.include_expander.expand(file, file_list)?; + let procedure_expanded = + self.procedure_expander + .expand(file, &include_expanded, log_level)?; + + preprocessed_files.insert(Rc::clone(file), procedure_expanded); + } + + Ok(preprocessed_files) + } +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/BUILD b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/BUILD new file mode 100644 index 0000000..c7f755f --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/BUILD @@ -0,0 +1,48 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library") + +filegroup( + name = "procedure_preprocessor_files", + srcs = [ + "lib.rs", + "procedure_ast.rs", + "procedure_expander.rs", + "procedure_parser.rs", + ], + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "procedure_preprocessor", + srcs = [ + ":procedure_preprocessor_files", + ], + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:procedure_grammar", + ], + crate_name = "procedure_preprocessor", + crate_root = "lib.rs", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + "@crates//:log", + "@crates//:pest", + "@crates//:serde", + "@crates//:thiserror", + ], +) diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/lib.rs b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/lib.rs new file mode 100644 index 0000000..cc0c552 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod procedure_ast; +mod procedure_expander; +mod procedure_parser; + +pub use procedure_expander::{ProcedureExpandError, ProcedureExpander}; +pub use procedure_parser::ProcedureParseError; diff --git a/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_ast.rs b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_ast.rs new file mode 100644 index 0000000..e331c0b --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_ast.rs @@ -0,0 +1,64 @@ +// ******************************************************************************* +// 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 serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProcedureFile { + pub stmts: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Statement { + Procedure(ProcedureDef), + MacroCall(MacroCallDef), + Text(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct MacroCallDef { + pub name: String, + pub args: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProcedureDef { + pub name: String, + pub params: Vec, + pub body: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum BodyNode { + MacroCall(MacroCallDef), + Text(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Arg { + /// macro: $alias + Variable(String), + /// string: "G1" + String(String), + /// number: 123, -42 + Number(i64), + /// identifier: foo + Identifier(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TextPart { + /// literal: plain text + Literal(String), + /// variable: $alias + Variable(String), +} 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 new file mode 100644 index 0000000..a3557e7 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_expander.rs @@ -0,0 +1,224 @@ +// ******************************************************************************* +// 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 log::debug; +use std::collections::HashMap; +use std::path::PathBuf; +use std::rc::Rc; + +use crate::procedure_ast::{ + Arg, BodyNode, MacroCallDef, ProcedureDef, ProcedureFile, Statement, TextPart, +}; +use crate::procedure_parser::{ProcedureParseError, ProcedureParserService}; +use parser_core::DiagramParser; +use puml_utils::LogLevel; + +const DEFAULT_MACRO_DEPTH: usize = 10; + +#[derive(Debug, thiserror::Error)] +pub enum ProcedureExpandError { + #[error("Failed to parse procedure file {file}: {error}")] + ParseFailed { + file: Rc, + #[source] + error: ProcedureParseError, + }, + + #[error("macro not defined: {0}")] + MacroNotDefined(String), + + #[error("macro {name} expects {expected} args but got {actual}")] + ArgumentMismatch { + name: String, + expected: usize, + actual: usize, + }, + + #[error("recursive macro detected: {chain:?} -> {name}")] + RecursiveMacro { chain: Vec, name: String }, + + #[error("maximum macro expansion depth exceeded")] + MaxDepthExceeded, +} + +#[derive(Default)] +pub struct ProcedureExpander { + parser: ProcedureParserService, + procedures: HashMap, + max_depth: usize, +} + +impl ProcedureExpander { + pub fn new() -> Self { + Self { + max_depth: DEFAULT_MACRO_DEPTH, + ..Default::default() + } + } + + pub fn expand( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result { + let mut out = String::new(); + let mut stack = Vec::new(); + + self.procedures.clear(); + let procedure_ast = self.load_procedures(path, content, log_level)?; + debug!("Loaded procedures from {:?}: {:#?}", path, procedure_ast); + + for stmt in &procedure_ast.stmts { + match stmt { + Statement::Text(t) => { + out.push_str(t); + out.push('\n'); + } + Statement::MacroCall(call) => { + let mut params = HashMap::new(); + out.push_str(&self.expand_macro(call, &mut params, &mut stack, 0)?); + } + Statement::Procedure(_) => {} + } + } + + Ok(out) + } + + fn load_procedures( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result { + let procedure_ast = self + .parser + .parse_file(path, content, log_level) + .map_err(|e| ProcedureExpandError::ParseFailed { + file: Rc::clone(path), + error: e, + })?; + + for stmt in &procedure_ast.stmts { + if let Statement::Procedure(p) = stmt { + self.procedures.insert(p.name.clone(), p.clone()); + } + } + + Ok(procedure_ast) + } + + fn expand_macro( + &self, + call: &MacroCallDef, + parent_params: &mut HashMap, + stack: &mut Vec, + depth: usize, + ) -> Result { + if depth > self.max_depth { + return Err(ProcedureExpandError::MaxDepthExceeded); + } + + if stack.contains(&call.name) { + return Err(ProcedureExpandError::RecursiveMacro { + chain: stack.clone(), + name: call.name.clone(), + }); + } + + 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)); + }; + + if proc.params.len() != call.args.len() { + return Err(ProcedureExpandError::ArgumentMismatch { + name: call.name.clone(), + expected: proc.params.len(), + actual: call.args.len(), + }); + } + stack.push(call.name.clone()); + + 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::String(s) => s.clone(), + Arg::Number(n) => n.to_string(), + Arg::Identifier(id) => id.clone(), + }; + new_params.insert(param.clone(), value); + } + + let result = self.expand_body(&proc.body, &mut new_params, stack, depth + 1)?; + stack.pop(); + + Ok(result) + } + + fn expand_body( + &self, + body: &[BodyNode], + params: &mut HashMap, + stack: &mut Vec, + depth: usize, + ) -> Result { + let mut out = String::new(); + + for node in body { + match node { + BodyNode::MacroCall(call) => { + out.push_str(&self.expand_macro(call, params, stack, depth)?); + } + BodyNode::Text(parts) => { + for part in parts { + match part { + TextPart::Literal(s) => { + out.push_str(s); + } + TextPart::Variable(v) => { + if let Some(val) = params.get(v) { + out.push_str(val); + } else { + out.push_str(v); + } + } + } + } + out.push('\n'); + } + } + } + + Ok(out) + } +} 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 new file mode 100644 index 0000000..c8ea71f --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/src/procedure/procedure_parser.rs @@ -0,0 +1,186 @@ +// ******************************************************************************* +// 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 pest::iterators::Pair; +use pest::Parser; +use pest_derive::Parser; +use std::path::PathBuf; +use std::rc::Rc; + +use crate::procedure_ast::{ + Arg, BodyNode, MacroCallDef, ProcedureDef, ProcedureFile, Statement, TextPart, +}; +use parser_core::{pest_to_syntax_error, BaseParseError, DiagramParser}; +use puml_utils::LogLevel; + +#[derive(Parser)] +#[grammar = "../../../grammar/procedure.pest"] +pub struct ProcedureParser; + +#[derive(Debug, thiserror::Error)] +pub enum ProcedureParseError { + #[error(transparent)] + Base(#[from] BaseParseError), +} + +#[derive(Default)] +pub struct ProcedureParserService; + +impl DiagramParser for ProcedureParserService { + type Output = ProcedureFile; + type Error = ProcedureParseError; + + fn parse_file( + &mut self, + path: &Rc, + content: &str, + _log_level: LogLevel, + ) -> Result { + let file_pair = ProcedureParser::parse(Rule::file, content) + .map_err(|e| { + ProcedureParseError::Base(pest_to_syntax_error(e, path.to_path_buf(), content)) + })? + .next() + .unwrap(); + + let mut stmts = Vec::new(); + + for line in file_pair.into_inner() { + match line.as_rule() { + Rule::procedure_line => { + let proc_def = parse_procedure(line); + stmts.push(Statement::Procedure(proc_def)); + } + Rule::macro_call_line => { + let macro_call = parse_macro_call(line); + stmts.push(Statement::MacroCall(macro_call)); + } + Rule::text_line => { + let text = line.as_str().trim().to_string(); + if !text.is_empty() { + stmts.push(Statement::Text(text)); + } + } + _ => {} + } + } + + Ok(ProcedureFile { stmts }) + } +} + +fn parse_procedure(pair: pest::iterators::Pair) -> ProcedureDef { + let mut name = String::new(); + let mut params = Vec::new(); + let mut body = Vec::new(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::proc_name => { + name = inner.as_str().to_string(); + } + Rule::param_list => { + params = inner.into_inner().map(|p| p.as_str().to_string()).collect(); + } + Rule::procedure_body => { + body = parse_body(inner); + } + _ => {} + } + } + + ProcedureDef { name, params, body } +} + +fn parse_body(pair: pest::iterators::Pair) -> Vec { + pair.into_inner() + .filter_map(|inner| match inner.as_rule() { + Rule::macro_call_line => Some(BodyNode::MacroCall(parse_macro_call(inner))), + Rule::text_line => Some(BodyNode::Text(parse_text_line(inner))), + _ => None, + }) + .collect() +} + +fn parse_macro_call(pair: pest::iterators::Pair) -> MacroCallDef { + let mut name = String::new(); + let mut args = Vec::new(); + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::macro_identifier | Rule::identifier => { + name = inner.as_str().to_string(); + } + Rule::arg_list => { + args = parse_arg_list(inner); + } + _ => {} + } + } + + MacroCallDef { name, args } +} + +fn parse_arg_list(pair: Pair) -> Vec { + pair.into_inner().map(parse_arg).collect() +} + +fn parse_arg(pair: Pair) -> Arg { + let inner = pair.into_inner().next().unwrap(); + let original_text = inner.as_str().to_string(); + + match inner.as_rule() { + Rule::macro_identifier => Arg::Variable(original_text), + Rule::string => { + let s = &original_text[1..original_text.len() - 1]; + Arg::String(s.to_string()) + } + Rule::number => Arg::Number(original_text.parse::().unwrap()), + Rule::identifier => Arg::Identifier(original_text), + _ => unreachable!(), + } +} + +fn parse_text_line(pair: Pair) -> Vec { + let text = pair.as_str(); + let mut parts = Vec::new(); + let mut current = String::new(); + let mut chars = text.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '$' { + if !current.is_empty() { + parts.push(TextPart::Literal(current.clone())); + current.clear(); + } + + let mut var = String::from("$"); + while let Some(&ch) = chars.peek() { + if ch.is_alphanumeric() || ch == '_' { + var.push(ch); + chars.next(); + } else { + break; + } + } + parts.push(TextPart::Variable(var)); + } else { + current.push(c); + } + } + + if !current.is_empty() { + parts.push(TextPart::Literal(current)); + } + + parts +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs b/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs new file mode 100644 index 0000000..06055f5 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/tests/include_tests.rs @@ -0,0 +1,106 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +mod preprocess_runner; + +use preprocess_runner::PreprocessRunner; +use test_framework::{run_case, DefaultExpectationChecker}; + +// Include Preprocessor Test Entry +fn run_include_preprocess_case(case_name: &str) { + run_case( + "puml_parser/tests/preprocessor/include", + case_name, + PreprocessRunner, + DefaultExpectationChecker, + ); +} + +// --------- test for include --------- +#[test] +fn test_simple_include() { + run_include_preprocess_case("simple_include"); +} + +#[test] +fn test_include_repeat() { + run_include_preprocess_case("repeat_include"); +} + +#[test] +fn test_invalid_cycle_include() { + run_include_preprocess_case("invalid_cycle_include"); +} + +#[test] +fn test_invalid_include_path() { + run_include_preprocess_case("invalid_include_path"); +} + +// --------- test for incluesub --------- +#[test] +fn test_simple_includesub() { + run_include_preprocess_case("simple_includesub"); +} + +#[test] +fn test_include_with_invalid_suffix() { + run_include_preprocess_case("invalid_suffix_for_include"); +} + +#[test] +fn test_includesub_with_serveral_subblock() { + run_include_preprocess_case("several_subblock"); +} + +#[test] +fn test_includesub_with_invalid_suffix() { + run_include_preprocess_case("invalid_suffix_for_includesub"); +} + +#[test] +fn test_invalid_nested_subblock() { + run_include_preprocess_case("invalid_nested_subblock"); +} + +#[test] +fn test_invalid_include_unknow_sub() { + run_include_preprocess_case("invalid_include_unknow_sub"); +} + +// --------- test for include_once --------- +#[test] +fn test_simple_include_once() { + run_include_preprocess_case("simple_include_once"); +} + +#[test] +fn test_repeat_include_once_raise_error() { + run_include_preprocess_case("invalid_repeat_include_once"); +} + +// --------- test for include_many --------- +#[test] +fn test_simple_include_many() { + run_include_preprocess_case("simple_include_many"); +} + +#[test] +fn test_combine_include_and_include_many_with_diff_sort() { + run_include_preprocess_case("combine_include_and_include_many"); +} + +// --------- test for complex include --------- +#[test] +fn test_complex_include() { + run_include_preprocess_case("complex_include"); +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/tests/preprocess_runner.rs b/plantuml/parser/puml_parser/src/preprocessor/tests/preprocess_runner.rs new file mode 100644 index 0000000..d88e686 --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/tests/preprocess_runner.rs @@ -0,0 +1,33 @@ +// ******************************************************************************* +// 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 std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::rc::Rc; + +use preprocessor::{PreprocessError, Preprocessor}; +use puml_utils::LogLevel; +use test_framework::DiagramProcessor; + +// ===== Preprocess adapter DiagramProcessor ===== +pub struct PreprocessRunner; +impl DiagramProcessor for PreprocessRunner { + type Output = String; + type Error = PreprocessError; + + fn run( + &self, + files: &HashSet>, + ) -> Result, std::string::String>, PreprocessError> { + Preprocessor::new().preprocess(files, LogLevel::Error) + } +} diff --git a/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs b/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs new file mode 100644 index 0000000..85e432c --- /dev/null +++ b/plantuml/parser/puml_parser/src/preprocessor/tests/procedure_tests.rs @@ -0,0 +1,77 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +mod preprocess_runner; + +use preprocess_runner::PreprocessRunner; +use test_framework::{run_case, DefaultExpectationChecker}; + +// Procedure Preprocess Test Entry +fn run_procedure_preprocess_case(case_name: &str) { + run_case( + "puml_parser/tests/preprocessor/procedure", + case_name, + PreprocessRunner, + DefaultExpectationChecker, + ); +} + +// --------- test for procedure --------- +#[test] +fn test_simple_macro() { + run_procedure_preprocess_case("simple_macro"); +} + +#[test] +fn test_simple_template() { + run_procedure_preprocess_case("simple_template"); +} + +#[test] +fn test_use_include() { + run_procedure_preprocess_case("use_include"); +} + +#[test] +fn test_mix_call() { + run_procedure_preprocess_case("mix_call"); +} + +#[test] +fn test_fta_metamodel() { + run_procedure_preprocess_case("fta_metamodel"); +} + +#[test] +fn test_macro_not_define() { + run_procedure_preprocess_case("macro_not_define"); +} + +#[test] +fn test_args_not_match() { + run_procedure_preprocess_case("args_not_match"); +} + +#[test] +fn test_recursive_macro() { + run_procedure_preprocess_case("recursive_macro"); +} + +#[test] +fn test_noise_item() { + run_procedure_preprocess_case("noise_item"); +} + +#[test] +fn test_empty() { + run_procedure_preprocess_case("empty"); +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/BUILD b/plantuml/parser/puml_parser/src/sequence_diagram/BUILD new file mode 100644 index 0000000..43fa191 --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/BUILD @@ -0,0 +1,48 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") + +rust_library( + name = "puml_parser_sequence", + srcs = glob( + ["src/*.rs"], + exclude = ["src/*_main.rs"], + ), + compile_data = [ + "//plantuml/parser/puml_parser/src/grammar:sequence_grammar", + ], + crate_name = "sequence_parser", + proc_macro_deps = [ + "@crates//:pest_derive", + ], + visibility = ["//visibility:public"], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_utils", + "@crates//:pest", + "@crates//:serde", + "@crates//:serde_json", + ], +) + +# Job 1: Parse PUML file with syntax parser, output JSON +rust_binary( + name = "syntax_parse", + srcs = ["src/syntax_parse_main.rs"], + visibility = ["//visibility:public"], + deps = [ + ":puml_parser_sequence", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs new file mode 100644 index 0000000..d9957c8 --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs @@ -0,0 +1,40 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +pub mod syntax_ast; +mod syntax_parser; + +pub use syntax_ast::{ + ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupType, Message, + MessageContent, ParticipantDef, ParticipantIdentifier, ParticipantType, SeqPumlDocument, + Statement, +}; + +pub use syntax_parser::PumlSequenceParser; + +/// Parse a PlantUML sequence diagram and return the document name and statements +/// This is a convenience function for backwards compatibility with tests +pub fn parse_sequence_diagram( + input: &str, +) -> Result<(Option, Vec), Box> { + use parser_core::DiagramParser; + use puml_utils::LogLevel; + use std::path::PathBuf; + use std::rc::Rc; + + let mut parser = PumlSequenceParser; + let dummy_path = Rc::new(PathBuf::from("")); + let document = parser.parse_file(&dummy_path, input, LogLevel::Error)?; + + Ok((document.name, document.statements)) +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_ast.rs new file mode 100644 index 0000000..2356c1a --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_ast.rs @@ -0,0 +1,133 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +// AST types for PlantUML Sequence Diagram Parser + +use serde::{Deserialize, Serialize}; + +pub use parser_core::common_ast::Arrow; + +// Document structure representing a complete PlantUML sequence diagram +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SeqPumlDocument { + pub name: Option, + pub statements: Vec, +} + +// Statement types used during parsing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Statement { + DestroyCmd(DestroyCmd), + CreateCmd(CreateCmd), + ActivateCmd(ActivateCmd), + DeactivateCmd(DeactivateCmd), + ParticipantDef(ParticipantDef), + Message(Message), + GroupCmd(GroupCmd), +} + +// Participant definitions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ParticipantDef { + pub participant_type: ParticipantType, + pub identifier: ParticipantIdentifier, + pub stereotype: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ParticipantType { + Participant, + Actor, + Boundary, + Control, + Entity, + Queue, + Database, + Collections, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ParticipantIdentifier { + QuotedAsId { quoted: String, id: String }, + IdAsQuoted { id: String, quoted: String }, + IdAsId { id1: String, id2: String }, + Quoted(String), + Id(String), +} + +// Destroy/Create commands +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DestroyCmd { + pub participant: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateCmd { + pub participant: String, +} + +// Activate/Deactivate commands +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActivateCmd { + pub participant: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeactivateCmd { + pub participant: Option, +} + +// Messages (internal parsing structure) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub content: MessageContent, + #[serde(skip_serializing_if = "Option::is_none")] + pub activation_marker: Option, + pub description: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageContent { + WithTargets { + left: String, + arrow: Arrow, + right: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ActivationType { + Activate, // ++ + Deactivate, // -- +} + +// Group commands (alt, opt, loop, etc.) - internal parsing structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GroupCmd { + pub group_type: GroupType, + pub text: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GroupType { + Opt, + Alt, + Loop, + Par, + Par2, + Break, + Critical, + Else, + Also, + End, + Group, +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parse_main.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parse_main.rs new file mode 100644 index 0000000..43943b8 --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parse_main.rs @@ -0,0 +1,69 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Syntax parser job: Parse PUML file and output JSON + +use sequence_parser::parse_sequence_diagram; +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 3 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let input_file = &args[1]; + let output_file = &args[2]; + + // Read the PUML file + let puml_content = match fs::read_to_string(input_file) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading file '{}': {}", input_file, e); + std::process::exit(1); + } + }; + + // Parse the sequence diagram + let (_doc_name, statements) = match parse_sequence_diagram(&puml_content) { + Ok(result) => result, + Err(e) => { + eprintln!("Error parsing sequence diagram: {}", e); + std::process::exit(1); + } + }; + + // Serialize to JSON (use IDs as-is, don't translate to quoted names) + let json = match serde_json::to_string_pretty(&statements) { + Ok(json) => json, + Err(e) => { + eprintln!("Error serializing to JSON: {}", e); + std::process::exit(1); + } + }; + + // Write to output file + if let Err(e) = fs::write(output_file, json) { + eprintln!("Error writing to '{}': {}", output_file, e); + std::process::exit(1); + } + + println!( + "✓ Syntax parse complete: {} statements → {}", + statements.len(), + output_file + ); +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parser.rs new file mode 100644 index 0000000..cd754ac --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/syntax_parser.rs @@ -0,0 +1,389 @@ +// ******************************************************************************* +// 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 parser_core::common_parser::parse_arrow as common_parse_arrow; +use parser_core::common_parser::{PlantUmlCommonParser, Rule}; +use parser_core::{pest_to_syntax_error, BaseParseError, DiagramParser}; +use puml_utils::LogLevel; +use std::path::PathBuf; +use std::rc::Rc; + +use crate::syntax_ast::*; + +pub struct PumlSequenceParser; + +// lobster-trace: Tools.ArchitectureModelingSyntax +// lobster-trace: Tools.ArchitectureModelingSequenceContentActors +// lobster-trace: Tools.ArchitectureModelingSequenceContentSWUnits +// lobster-trace: Tools.ArchitectureModelingSequenceContentMessages +// lobster-trace: Tools.ArchitectureModelingSequenceContentActivity +impl PumlSequenceParser { + fn parse_startuml(pair: pest::iterators::Pair) -> Option { + for inner in pair.into_inner() { + if inner.as_rule() == Rule::puml_name { + return Some(inner.as_str().trim().to_string()); + } + } + None + } + + fn parse_statement(pair: pest::iterators::Pair) -> Option { + let inner = pair.into_inner().next()?; + match inner.as_rule() { + Rule::participant_def => Some(Statement::ParticipantDef(Self::parse_participant_def( + inner, + )?)), + Rule::message => Some(Statement::Message(Self::parse_message(inner)?)), + Rule::group_cmd => Some(Statement::GroupCmd(Self::parse_group_cmd(inner)?)), + Rule::destroy_cmd => Some(Statement::DestroyCmd(Self::parse_destroy_cmd(inner)?)), + Rule::create_cmd => Some(Statement::CreateCmd(Self::parse_create_cmd(inner)?)), + Rule::activate_cmd => Some(Statement::ActivateCmd(Self::parse_activate_cmd(inner)?)), + Rule::deactivate_cmd => { + Some(Statement::DeactivateCmd(Self::parse_deactivate_cmd(inner)?)) + } + _ => None, + } + } + + fn parse_participant_def(pair: pest::iterators::Pair) -> Option { + let mut participant_type: Option = None; + let mut identifier: Option = None; + let mut stereotype: Option = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::create_kw => { + // Handle create keyword if needed + } + Rule::participant_type => { + participant_type = Self::parse_participant_type(inner); + } + Rule::quoted_participant_as_id => { + let mut parts = inner.into_inner(); + let quoted = parts + .next() + .map(|p| Self::extract_quoted_string(p.as_str()))?; + let alias_clause = parts.next()?; // alias_clause + let id_pair = alias_clause.into_inner().next()?; + let id = match id_pair.as_rule() { + Rule::quoted_string => Self::extract_quoted_string(id_pair.as_str()), + _ => id_pair.as_str().trim().to_string(), + }; + identifier = Some(ParticipantIdentifier::QuotedAsId { quoted, id }); + } + Rule::participant_id_as_quoted => { + let mut parts = inner.into_inner(); + let id = parts.next()?.as_str().trim().to_string(); + let alias_clause = parts.next()?; // alias_clause + let quoted_pair = alias_clause.into_inner().next()?; + let quoted = Self::extract_quoted_string(quoted_pair.as_str()); + identifier = Some(ParticipantIdentifier::IdAsQuoted { id, quoted }); + } + Rule::participant_id_as_id => { + let mut parts = inner.into_inner(); + let id1 = parts.next()?.as_str().trim().to_string(); + let alias_clause = parts.next()?; // alias_clause + let id2_pair = alias_clause.into_inner().next()?; + let id2 = id2_pair.as_str().trim().to_string(); + identifier = Some(ParticipantIdentifier::IdAsId { id1, id2 }); + } + Rule::quoted_participant => { + let quoted = Self::extract_quoted_string(inner.as_str()); + identifier = Some(ParticipantIdentifier::Quoted(quoted)); + } + Rule::participant_id => { + let id = inner.as_str().trim().to_string(); + identifier = Some(ParticipantIdentifier::Id(id)); + } + Rule::stereotype => { + stereotype = Some(Self::extract_stereotype(inner.as_str())); + } + Rule::order_clause => { + // Ignore this for now + } + _ => {} + } + } + + Some(ParticipantDef { + participant_type: participant_type?, + identifier: identifier?, + stereotype, + }) + } + + fn parse_participant_type(pair: pest::iterators::Pair) -> Option { + let text = pair.as_str().to_lowercase(); + match text.as_str() { + "participant" => Some(ParticipantType::Participant), + "actor" => Some(ParticipantType::Actor), + "boundary" => Some(ParticipantType::Boundary), + "control" => Some(ParticipantType::Control), + "entity" => Some(ParticipantType::Entity), + "queue" => Some(ParticipantType::Queue), + "database" => Some(ParticipantType::Database), + "collections" => Some(ParticipantType::Collections), + _ => None, + } + } + + fn parse_message(pair: pest::iterators::Pair) -> Option { + let mut left: Option = None; + let mut arrow: Option = None; + let mut right: Option = None; + let mut activation_marker: Option = None; + let mut description: Option = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::message_participant => { + let participant = Self::extract_participant_ref(inner); + // First participant goes to left, second to right + if arrow.is_none() { + left = Some(participant); + } else { + right = Some(participant); + } + } + Rule::sequence_arrow => { + arrow = Self::parse_arrow(inner); + } + Rule::activation_marker => { + activation_marker = Some(inner.as_str().to_string()); + } + Rule::sequence_description => { + description = Some(inner.into_inner().next()?.as_str().trim().to_string()); + } + _ => {} + } + } + + let content = MessageContent::WithTargets { + left: left.unwrap_or_default(), + arrow: arrow?, + right: right.unwrap_or_default(), + }; + + Some(Message { + content, + activation_marker, + description, + }) + } + + fn parse_arrow(pair: pest::iterators::Pair) -> Option { + let arrow = common_parse_arrow(pair).ok()?; + + Some(arrow) + } + + fn parse_group_cmd(pair: pest::iterators::Pair) -> Option { + let mut group_type: Option = None; + let mut text: Option = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::group_type => { + group_type = Self::parse_group_type(inner); + } + Rule::group_condition => { + text = Some(inner.as_str().trim().to_string()); + } + _ => {} + } + } + + Some(GroupCmd { + group_type: group_type?, + text, + }) + } + + fn parse_group_type(pair: pest::iterators::Pair) -> Option { + let text = pair.as_str().to_lowercase(); + match text.as_str() { + "opt" => Some(GroupType::Opt), + "alt" => Some(GroupType::Alt), + "loop" => Some(GroupType::Loop), + "par" => Some(GroupType::Par), + "par2" => Some(GroupType::Par2), + "break" => Some(GroupType::Break), + "critical" => Some(GroupType::Critical), + "else" => Some(GroupType::Else), + "also" => Some(GroupType::Also), + "end" => Some(GroupType::End), + "group" => Some(GroupType::Group), + _ => None, + } + } + + fn parse_destroy_cmd(pair: pest::iterators::Pair) -> Option { + let mut participant: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::participant_ref { + participant = Some(Self::extract_participant_ref(inner)); + } + } + + Some(DestroyCmd { + participant: participant?, + }) + } + + fn parse_create_cmd(pair: pest::iterators::Pair) -> Option { + let mut participant: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::participant_ref { + participant = Some(Self::extract_participant_ref(inner)); + } + } + + Some(CreateCmd { + participant: participant?, + }) + } + + fn parse_activate_cmd(pair: pest::iterators::Pair) -> Option { + let mut participant: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::participant_ref { + participant = Some(Self::extract_participant_ref(inner)); + } + } + + Some(ActivateCmd { + participant: participant?, + }) + } + + fn parse_deactivate_cmd(pair: pest::iterators::Pair) -> Option { + let mut participant: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::participant_ref { + participant = Some(Self::extract_participant_ref(inner)); + } + } + + Some(DeactivateCmd { participant }) + } + + // Helper functions + fn extract_quoted_string(s: &str) -> String { + s.trim() + .trim_start_matches('"') + .trim_end_matches('"') + .trim_start_matches('«') + .trim_end_matches('»') + .to_string() + } + + fn extract_stereotype(s: &str) -> String { + s.trim() + .trim_start_matches("<<") + .trim_end_matches(">>") + .to_string() + } + + fn extract_participant_ref(pair: pest::iterators::Pair) -> String { + match pair.as_rule() { + Rule::message_participant => pair + .into_inner() + .next() + .map(Self::extract_participant_ref) + .unwrap_or_default(), + + Rule::participant_ref => { + let fallback = pair.as_str().trim().to_string(); + + pair.into_inner() + .next() + .map(Self::extract_participant_ref) + .unwrap_or(fallback) + } + + Rule::quoted_string => Self::extract_quoted_string(pair.as_str()), + + Rule::quoted_participant_as_id + | Rule::participant_id_as_quoted + | Rule::participant_id_as_id => { + let mut inner = pair.into_inner(); + + inner.next(); // skip lhs + + let alias_clause = inner.next().unwrap(); + + let target = alias_clause.into_inner().next().unwrap(); + + match target.as_rule() { + Rule::quoted_string => Self::extract_quoted_string(target.as_str()), + _ => target.as_str().trim().to_string(), + } + } + + _ => pair.as_str().trim().to_string(), + } + } +} + +impl DiagramParser for PumlSequenceParser { + type Output = SeqPumlDocument; + type Error = BaseParseError; + + fn parse_file( + &mut self, + path: &Rc, + content: &str, + log_level: LogLevel, + ) -> Result { + use pest::Parser; + + // Log file content at trace level + if matches!(log_level, LogLevel::Trace) { + eprintln!("{}:\n{}\n{}", path.display(), content, "=".repeat(30)); + } + + let pairs = PlantUmlCommonParser::parse(Rule::sequence_start, content) + .map_err(|e| pest_to_syntax_error(e, path.as_ref().clone(), content))?; + + let mut document = SeqPumlDocument { + name: None, + statements: Vec::new(), + }; + + for pair in pairs { + if pair.as_rule() == Rule::sequence_start { + for inner_pair in pair.into_inner() { + match inner_pair.as_rule() { + Rule::startuml => { + document.name = Self::parse_startuml(inner_pair); + } + Rule::sequence_statement => { + if let Some(stmt) = Self::parse_statement(inner_pair) { + document.statements.push(stmt); + } + } + Rule::empty_line => { + // Skip empty lines + } + _ => {} + } + } + } + } + + Ok(document) + } +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/tests/BUILD b/plantuml/parser/puml_parser/src/sequence_diagram/tests/BUILD new file mode 100644 index 0000000..9b655c0 --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/tests/BUILD @@ -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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_test") + +# Test suite: Compare parsed output with expected JSON for each test pair +rust_test( + name = "syntax_parse_test", + srcs = ["syntax_parse_test.rs"], + data = [ + "//plantuml/parser/integration_test/sequence_diagram:sequence_diagram_tests", + ], + deps = [ + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_parser:sequence_diagram", + "//plantuml/parser/puml_utils", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/tests/syntax_parse_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/tests/syntax_parse_test.rs new file mode 100644 index 0000000..418f454 --- /dev/null +++ b/plantuml/parser/puml_parser/src/sequence_diagram/tests/syntax_parse_test.rs @@ -0,0 +1,71 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Syntax parser test suite: Compare parsed output with expected JSON for each test pair + +use parser_core::DiagramParser; +use puml_utils::LogLevel; +use sequence_parser::PumlSequenceParser; +use std::fs; +use std::path::PathBuf; +use std::rc::Rc; + +fn test_file_pair(puml_file: &str, json_file: &str) { + // Read and parse the PUML file + let puml_content = fs::read_to_string(puml_file) + .unwrap_or_else(|e| panic!("Error reading input file '{}': {}", puml_file, e)); + + // Use DiagramParser trait directly + let mut parser = PumlSequenceParser; + let path = Rc::new(PathBuf::from(puml_file)); + let document = parser + .parse_file(&path, &puml_content, LogLevel::Error) + .unwrap_or_else(|e| panic!("Error parsing sequence diagram '{}': {}", puml_file, e)); + + // Serialize parsed statements to JSON (not the full document with name) + let actual_json = serde_json::to_string_pretty(&document.statements) + .expect("Error serializing parsed result to JSON"); + + // Read expected JSON + let expected_json = fs::read_to_string(json_file) + .unwrap_or_else(|e| panic!("Error reading expected file '{}': {}", json_file, e)); + + // Parse both JSONs to normalize formatting + let actual_value: serde_json::Value = + serde_json::from_str(&actual_json).expect("Error parsing actual JSON"); + + let expected_value: serde_json::Value = + serde_json::from_str(&expected_json).expect("Error parsing expected JSON"); + + // Compare the values + if actual_value != expected_value { + eprintln!( + "\nExpected JSON:\n{}", + serde_json::to_string_pretty(&expected_value).unwrap() + ); + eprintln!( + "\nActual JSON:\n{}", + serde_json::to_string_pretty(&actual_value).unwrap() + ); + + panic!("Parsed output does not match expected JSON"); + } +} + +#[test] +fn test_comprehensive_sequence() { + test_file_pair( + "plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml", + "plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.json", + ); +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/BUILD b/plantuml/parser/puml_parser/tests/class_diagram/BUILD new file mode 100644 index 0000000..835fd30 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/BUILD @@ -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 +# ******************************************************************************* +filegroup( + name = "class_diagram_files", + srcs = glob([ + "**/*.puml", + "**/*.yaml", + "**/*.json", + ]), + visibility = ["//visibility:public"], +) diff --git a/plantuml/parser/puml_parser/tests/class_diagram/alias_class/alias_class.puml b/plantuml/parser/puml_parser/tests/class_diagram/alias_class/alias_class.puml new file mode 100644 index 0000000..858115e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/alias_class/alias_class.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 alias_class + +class "SerializedRoutineControl" as SRC +struct "StartRequestPayload" as Req +enum "ErrorInfo" as Err + +SRC --> Req +Req --> Err + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/alias_class/output.json b/plantuml/parser/puml_parser/tests/class_diagram/alias_class/output.json new file mode 100644 index 0000000..822113b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/alias_class/output.json @@ -0,0 +1,79 @@ +{ + "alias_class.puml": { + "name": "alias_class", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "SerializedRoutineControl", + "display": "SRC" + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Types": { + "StructDef": { + "name": { + "internal": "StartRequestPayload", + "display": "Req" + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Enum": { + "name": { + "internal": "ErrorInfo", + "display": "Err" + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [] + } + } + ], + "relationships": [ + { + "left": "SRC", + "right": "Req", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + }, + { + "left": "Req", + "right": "Err", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/alias_package/alias_package.puml b/plantuml/parser/puml_parser/tests/class_diagram/alias_package/alias_package.puml new file mode 100644 index 0000000..a518d19 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/alias_package/alias_package.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 alias_package + +struct "TestResult" as TR + +package diag as mwdiag { + class A + class B +} + +package activate as act { + struct "StartResponsePayload" as SRP + enum "ErrorInfo" as ERR +} + +' mwdiag.A --> act.SRP +' act.SRP --> ERR + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/alias_package/output.json b/plantuml/parser/puml_parser/tests/class_diagram/alias_package/output.json new file mode 100644 index 0000000..240c967 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/alias_package/output.json @@ -0,0 +1,94 @@ +{"alias_package.puml": + { + "name": "alias_package", + "elements": [ + { + "Types": { + "StructDef": { + "name": { + "internal": "TestResult", + "display": "TR" + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Package": { + "name": { + "internal": "diag", + "display": "mwdiag" + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "diag", + "attributes": [], + "methods": [] + } + }, + { + "ClassDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "", + "package": "diag", + "attributes": [], + "methods": [] + } + } + ], + "relationships": [], + "packages": [] + } + }, + { + "Package": { + "name": { + "internal": "activate", + "display": "act" + }, + "types": [ + { + "StructDef": { + "name": { + "internal": "StartResponsePayload", + "display": "SRP" + }, + "namespace": "", + "package": "activate", + "attributes": [], + "methods": [] + } + }, + { + "EnumDef": { + "name": { + "internal": "ErrorInfo", + "display": "ERR" + }, + "namespace": "", + "package": "activate", + "stereotypes": [], + "items": [] + } + } + ], + "relationships": [], + "packages": [] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/attr_method/attr_method.puml b/plantuml/parser/puml_parser/tests/class_diagram/attr_method/attr_method.puml new file mode 100644 index 0000000..c84bcfb --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/attr_method/attr_method.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 attr_method + +class User { + + name : string + - id : int + + getName() : string +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/attr_method/output.json b/plantuml/parser/puml_parser/tests/class_diagram/attr_method/output.json new file mode 100644 index 0000000..593a4fc --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/attr_method/output.json @@ -0,0 +1,41 @@ +{"attr_method.puml": + { + "name": "attr_method", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "User", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "name", + "type": "string" + }, + { + "visibility": "Private", + "name": "id", + "type": "int" + } + ], + "methods": [ + { + "visibility": "Public", + "name": "getName", + "generic_params": [], + "params": [], + "type": "string" + } + ] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/class_merge/class_merge.puml b/plantuml/parser/puml_parser/tests/class_diagram/class_merge/class_merge.puml new file mode 100644 index 0000000..fcc2c1d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/class_merge/class_merge.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml class_merge + +class A { + + attr_1: int +} + +class A { + - attr_2: int +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/class_merge/output.json b/plantuml/parser/puml_parser/tests/class_diagram/class_merge/output.json new file mode 100644 index 0000000..cb68a5d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/class_merge/output.json @@ -0,0 +1,48 @@ +{"class_merge.puml": + { + "name": "class_merge", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "attr_1", + "type": "int" + } + ], + "methods": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Private", + "name": "attr_2", + "type": "int" + } + ], + "methods": [] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/color/color.puml b/plantuml/parser/puml_parser/tests/class_diagram/color/color.puml new file mode 100644 index 0000000..f1575af --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/color/color.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 color + +namespace Service #DDDDDD { + class Api #AABBCC { + + Request() + } +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/color/output.json b/plantuml/parser/puml_parser/tests/class_diagram/color/output.json new file mode 100644 index 0000000..acdf47c --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/color/output.json @@ -0,0 +1,39 @@ +{"color.puml": + { + "name": "color", + "elements": [ + { + "Namespace": { + "name": { + "internal": "Service", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "Api", + "display": null + }, + "namespace": "Service", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "Request", + "generic_params": [], + "params": [], + "type": null + } + ] + } + } + ], + "namespaces": [] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/cpp_style.puml b/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/cpp_style.puml new file mode 100644 index 0000000..a9edcae --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/cpp_style.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 cpp_style + +class ::app::core::Controller + +class Dispatcher { + + Dispatch(Event...) +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/output.json b/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/output.json new file mode 100644 index 0000000..be5a7e0 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/cpp_style/output.json @@ -0,0 +1,50 @@ +{"cpp_style.puml": + { + "name": "cpp_style", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "::app::core::Controller", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "Dispatcher", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "Dispatch", + "generic_params": [], + "params": [ + { + "name": null, + "param_type": "Event", + "varargs": true + } + ], + "type": null + } + ] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/ctrl_instruct.puml b/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/ctrl_instruct.puml new file mode 100644 index 0000000..320fc8b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/ctrl_instruct.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 ctrl_instruct + +skinparam classFontSize 14 +skinparam shadowing false + +left to right direction +top to bottom direction + +title Test Diagram + +class A +class B + +A --> B + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/output.json b/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/output.json new file mode 100644 index 0000000..08852b7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/ctrl_instruct/output.json @@ -0,0 +1,52 @@ +{ + "ctrl_instruct.puml": { + "name": "ctrl_instruct", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + } + ], + "relationships": [ + { + "left": "A", + "right": "B", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/empty/empty.puml b/plantuml/parser/puml_parser/tests/class_diagram/empty/empty.puml new file mode 100644 index 0000000..9cccd6f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/empty/empty.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml + +' This is a comment + +// This is 2nd comment + +/' +This is 3rd comment +'/ + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/empty/output.json b/plantuml/parser/puml_parser/tests/class_diagram/empty/output.json new file mode 100644 index 0000000..8f944de --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/empty/output.json @@ -0,0 +1,7 @@ +{"empty.puml": + { + "name": "", + "elements": [], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/enum/enum.puml b/plantuml/parser/puml_parser/tests/class_diagram/enum/enum.puml new file mode 100644 index 0000000..de0b972 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/enum/enum.puml @@ -0,0 +1,34 @@ +' ******************************************************************************* +' 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 enum + +enum Status + +enum Response { + +OK + -ERROR + #UNKNOWN +} + +enum State { + INIT : initialize + RUNNING : running + STOPPED : stoped +} + +enum CAN { + HIGH = 0 + LOW = 1 +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/enum/output.json b/plantuml/parser/puml_parser/tests/class_diagram/enum/output.json new file mode 100644 index 0000000..3e16edb --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/enum/output.json @@ -0,0 +1,109 @@ +{"enum.puml": + { + "name": "enum", + "elements": [ + { + "Enum": { + "name": { + "internal": "Status", + "display": null + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [] + } + }, + { + "Enum": { + "name": { + "internal": "Response", + "display": null + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [ + { + "visibility": "Public", + "name": "OK", + "value": null + }, + { + "visibility": "Private", + "name": "ERROR", + "value": null + }, + { + "visibility": "Protected", + "name": "UNKNOWN", + "value": null + } + ] + } + }, + { + "Enum": { + "name": { + "internal": "State", + "display": null + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [ + { + "visibility": null, + "name": "INIT", + "value": { + "Description": "initialize" + } + }, + { + "visibility": null, + "name": "RUNNING", + "value": { + "Description": "running" + } + }, + { + "visibility": null, + "name": "STOPPED", + "value": { + "Description": "stoped" + } + } + ] + } + }, + { + "Enum": { + "name": { + "internal": "CAN", + "display": null + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [ + { + "visibility": null, + "name": "HIGH", + "value": { + "Literal": "0" + } + }, + { + "visibility": null, + "name": "LOW", + "value": { + "Literal": "1" + } + } + ] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/interface/interface.puml b/plantuml/parser/puml_parser/tests/class_diagram/interface/interface.puml new file mode 100644 index 0000000..c6d680e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/interface/interface.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 + +interface J { + +doSomething +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/interface/output.json b/plantuml/parser/puml_parser/tests/class_diagram/interface/output.json new file mode 100644 index 0000000..1dff144 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/interface/output.json @@ -0,0 +1,28 @@ +{"interface.puml": + { + "name": "", + "elements": [ + { + "Types": { + "InterfaceDef": { + "name": { + "internal": "J", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "doSomething", + "type": null + } + ], + "methods": [] + } + } + } + ], + "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 new file mode 100644 index 0000000..38777c8 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/namespace_1.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 namespace_1 + +namespace Core { + class A + class 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 new file mode 100644 index 0000000..ac4c543 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_1/output.json @@ -0,0 +1,43 @@ +{"namespace_1.puml": + { + "name": "namespace_1", + "elements": [ + { + "Namespace": { + "name": { + "internal": "Core", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "Core", + "package": "", + "attributes": [], + "methods": [] + } + }, + { + "ClassDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "Core", + "package": "", + "attributes": [], + "methods": [] + } + } + ], + "namespaces": [] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/namespace_2.puml b/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/namespace_2.puml new file mode 100644 index 0000000..c26968e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/namespace_2.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 namespace_2 + +namespace App { + namespace UI { + class Button + } +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/output.json b/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/output.json new file mode 100644 index 0000000..3e06975 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_2/output.json @@ -0,0 +1,40 @@ +{"namespace_2.puml": + { + "name": "namespace_2", + "elements": [ + { + "Namespace": { + "name": { + "internal": "App", + "display": null + }, + "types": [], + "namespaces": [ + { + "name": { + "internal": "UI", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "Button", + "display": null + }, + "namespace": "UI", + "package": "", + "attributes": [], + "methods": [] + } + } + ], + "namespaces": [] + } + ] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/namespace_3.puml b/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/namespace_3.puml new file mode 100644 index 0000000..a6d5a99 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/namespace_3.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 namespace_3 + +class GlobalClass + +namespace Data { + class DB +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/output.json b/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/output.json new file mode 100644 index 0000000..85ac31f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/namespace_3/output.json @@ -0,0 +1,45 @@ +{"namespace_3.puml": + { + "name": "namespace_3", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "GlobalClass", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Namespace": { + "name": { + "internal": "Data", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "DB", + "display": null + }, + "namespace": "Data", + "package": "", + "attributes": [], + "methods": [] + } + } + ], + "namespaces": [] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/negative_pkg_comp.puml b/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/negative_pkg_comp.puml new file mode 100644 index 0000000..5e8ee83 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/negative_pkg_comp.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 n_pkg_comp + +package MyPackage { + component MyComponent +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/output.yaml b/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/output.yaml new file mode 100644 index 0000000..60f61ee --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/negative_pkg_comp/output.yaml @@ -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 +# ******************************************************************************* +negative_pkg_comp.puml: + error: + type: "SyntaxError" + path: null + file: null + line: 4 + column: 15 + message: "Parsing error at (4, 15), expected [arrow], got []" + source_line: | + component MyComponent\n diff --git a/plantuml/parser/puml_parser/tests/class_diagram/one_class/one_class.puml b/plantuml/parser/puml_parser/tests/class_diagram/one_class/one_class.puml new file mode 100644 index 0000000..13b05a5 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/one_class/one_class.puml @@ -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 +' ******************************************************************************* +@startuml one_class + +class A + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/one_class/output.json b/plantuml/parser/puml_parser/tests/class_diagram/one_class/output.json new file mode 100644 index 0000000..4343e2d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/one_class/output.json @@ -0,0 +1,22 @@ +{"one_class.puml": + { + "name": "one_class", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/only_attribute.puml b/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/only_attribute.puml new file mode 100644 index 0000000..207e60e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/only_attribute.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 only_attribute + +class Node { + + name +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/output.json b/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/output.json new file mode 100644 index 0000000..1fa9f10 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/only_attribute/output.json @@ -0,0 +1,28 @@ +{"only_attribute.puml": + { + "name": "only_attribute", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "Node", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "name", + "type": null + } + ], + "methods": [] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/only_method/only_method.puml b/plantuml/parser/puml_parser/tests/class_diagram/only_method/only_method.puml new file mode 100644 index 0000000..aa6d32b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/only_method/only_method.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 only_method + +class Node { + + nodeMethod() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/only_method/output.json b/plantuml/parser/puml_parser/tests/class_diagram/only_method/output.json new file mode 100644 index 0000000..0cc5fd0 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/only_method/output.json @@ -0,0 +1,30 @@ +{"only_method.puml": + { + "name": "only_method", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "Node", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "nodeMethod", + "generic_params": [], + "params": [], + "type": null + } + ] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/package/output.json b/plantuml/parser/puml_parser/tests/class_diagram/package/output.json new file mode 100644 index 0000000..bab1367 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/package/output.json @@ -0,0 +1,78 @@ +{"package.puml": + { + "name": "package", + "elements": [ + { + "Package": { + "name": { + "internal": "Core", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "Core", + "attributes": [], + "methods": [] + } + }, + { + "ClassDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "", + "package": "Core", + "attributes": [], + "methods": [] + } + } + ], + "relationships": [], + "packages": [] + } + }, + { + "Package": { + "name": { + "internal": "App", + "display": null + }, + "types": [], + "relationships": [], + "packages": [ + { + "name": { + "internal": "UI", + "display": null + }, + "types": [ + { + "ClassDef": { + "name": { + "internal": "Button", + "display": null + }, + "namespace": "", + "package": "UI", + "attributes": [], + "methods": [] + } + } + ], + "relationships": [], + "packages": [] + } + ] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/package/package.puml b/plantuml/parser/puml_parser/tests/class_diagram/package/package.puml new file mode 100644 index 0000000..74381a2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/package/package.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 package + +package Core { + class A + class B +} + +package App { + package UI { + class Button + } +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/param/output.json b/plantuml/parser/puml_parser/tests/class_diagram/param/output.json new file mode 100644 index 0000000..746a817 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/param/output.json @@ -0,0 +1,41 @@ +{"param.puml": + { + "name": "param", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "Calculator", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "Add", + "generic_params": [], + "params": [ + { + "name": "a", + "param_type": "int", + "varargs": false + }, + { + "name": "b", + "param_type": "int", + "varargs": false + } + ], + "type": "int" + } + ] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/param/param.puml b/plantuml/parser/puml_parser/tests/class_diagram/param/param.puml new file mode 100644 index 0000000..e1c2468 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/param/param.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 param + +class Calculator { + + Add(a:int, b:int) : int +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/param_templete/output.json b/plantuml/parser/puml_parser/tests/class_diagram/param_templete/output.json new file mode 100644 index 0000000..c8cccb6 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/param_templete/output.json @@ -0,0 +1,36 @@ +{"param_templete.puml": + { + "name": "param_templete", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "Factory", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "Create", + "generic_params": ["T"], + "params": [ + { + "name": "id", + "param_type": "int", + "varargs": false + } + ], + "type": null + } + ] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/param_templete/param_templete.puml b/plantuml/parser/puml_parser/tests/class_diagram/param_templete/param_templete.puml new file mode 100644 index 0000000..05252c0 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/param_templete/param_templete.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 param_templete + +class Factory { + + Create(id:int) +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/output.json b/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/output.json new file mode 100644 index 0000000..9ba9436 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/output.json @@ -0,0 +1,68 @@ +{ + "relationship_arrows.puml": { + "name": "relationship_arrows", + "elements": [], + "relationships": [ + { + "left": "Car", + "right": "Wheel", + "arrow": { + "left": { + "raw": "o" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": "belongs" + }, + { + "left": "House", + "right": "Room", + "arrow": { + "left": { + "raw": "*" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": "in" + }, + { + "left": "Student", + "right": "School", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + }, + { + "left": "Parser", + "right": "Lexer", + "arrow": { + "left": null, + "line": { + "raw": ".." + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/relationship_arrows.puml b/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/relationship_arrows.puml new file mode 100644 index 0000000..bd61c68 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_arrows/relationship_arrows.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 relationship_arrows + +Car o-- Wheel : belongs +House *-- Room : in +Student --> School +Parser ..> Lexer + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/output.json b/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/output.json new file mode 100644 index 0000000..5195b58 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/output.json @@ -0,0 +1,38 @@ +{ + "relationship_inheritance.puml": { + "name": "relationship_inheritance", + "elements": [], + "relationships": [ + { + "left": "Base", + "right": "Derived", + "arrow": { + "left": { + "raw": "<|" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": null + }, + { + "left": "Sub", + "right": "Derived", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": "|>" + } + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/relationship_inheritance.puml b/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/relationship_inheritance.puml new file mode 100644 index 0000000..ce32471 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_inheritance/relationship_inheritance.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 relationship_inheritance + +Base <|-- Derived + +Sub --|> Derived + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/output.json b/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/output.json new file mode 100644 index 0000000..f56131e --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/output.json @@ -0,0 +1,38 @@ +{ + "relationship_mix_inher.puml": { + "name": "relationship_mix_inher", + "elements": [], + "relationships": [ + { + "left": "Base", + "right": "Child", + "arrow": { + "left": { + "raw": "<|" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": null + }, + { + "left": "IInterface", + "right": "Child", + "arrow": { + "left": { + "raw": "<|" + }, + "line": { + "raw": ".." + }, + "middle": null, + "right": null + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/relationship_mix_inher.puml b/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/relationship_mix_inher.puml new file mode 100644 index 0000000..6794f27 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_mix_inher/relationship_mix_inher.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 relationship_mix_inher + +Base <|-- Child +IInterface <|.. Child + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/output.json b/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/output.json new file mode 100644 index 0000000..c2bff09 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/output.json @@ -0,0 +1,21 @@ +{ + "relationship_normal.puml": { + "name": "relationship_normal", + "elements": [], + "relationships": [ + { + "left": "Base", + "right": "Child", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/relationship_normal.puml b/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/relationship_normal.puml new file mode 100644 index 0000000..d969c67 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_normal/relationship_normal.puml @@ -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 +' ******************************************************************************* +@startuml relationship_normal + +Base -- Child + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/output.json b/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/output.json new file mode 100644 index 0000000..3c7a1eb --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/output.json @@ -0,0 +1,23 @@ +{ + "relationship_qualified_id.puml": { + "name": "relationship_qualified_id", + "elements": [], + "relationships": [ + { + "left": "App.UI.Button", + "right": "App.Feat.TextButton", + "arrow": { + "left": { + "raw": "<|" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "label": null + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/relationship_qualified_id.puml b/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/relationship_qualified_id.puml new file mode 100644 index 0000000..d2715ba --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/relationship_qualified_id/relationship_qualified_id.puml @@ -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 +' ******************************************************************************* +@startuml relationship_qualified_id + +App.UI.Button <|-- App.Feat.TextButton + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/output.json b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/output.json new file mode 100644 index 0000000..8a4e8a4 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/output.json @@ -0,0 +1,101 @@ +{"stereotype_definition.puml": + { + "name": "stereotype_definition", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "User", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Private", + "name": "id", + "type": "int" + } + ], + "methods": [ + { + "visibility": "Public", + "name": "getName", + "generic_params": [], + "params": [], + "type": "String" + } + ] + } + } + }, + { + "Types": { + "StructDef": { + "name": { + "internal": "Config", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Private", + "name": "instance", + "type": "Config" + } + ], + "methods": [] + } + } + }, + { + "Types": { + "InterfaceDef": { + "name": { + "internal": "AbstractService", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [ + { + "visibility": "Public", + "name": "doSomething", + "generic_params": [], + "params": [], + "type": "void" + } + ] + } + } + }, + { + "Enum": { + "name": { + "internal": "Gender", + "display": null + }, + "namespace": "", + "package": "", + "stereotypes": [], + "items": [ + { + "visibility": null, + "name": "MALE", + "value": null + }, + { + "visibility": null, + "name": "FEMALE", + "value": null + } + ] + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/stereotype_definition.puml b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/stereotype_definition.puml new file mode 100644 index 0000000..a023538 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_definition/stereotype_definition.puml @@ -0,0 +1,33 @@ +' ******************************************************************************* +' 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 stereotype_definition + +class User <> { + - id: int + + getName(): String +} + +struct Config <> { + - instance: Config +} + +interface AbstractService <> { + + doSomething(): void +} + +enum Gender <> { + MALE + FEMALE +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/output.json b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/output.json new file mode 100644 index 0000000..cbadf8f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/output.json @@ -0,0 +1,52 @@ +{ + "stereotype_relationship.puml": { + "name": "stereotype_relationship", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "User", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "UserService", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [], + "methods": [] + } + } + } + ], + "relationships": [ + { + "left": "UserService", + "right": "User", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "label": "<>" + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/stereotype_relationship.puml b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/stereotype_relationship.puml new file mode 100644 index 0000000..4bc9701 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/stereotype_relationship/stereotype_relationship.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 stereotype_relationship + +class User <> +class UserService <> + +UserService --> User : <> + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/struct/output.json b/plantuml/parser/puml_parser/tests/class_diagram/struct/output.json new file mode 100644 index 0000000..d886508 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/struct/output.json @@ -0,0 +1,81 @@ +{"struct.puml": + { + "name": "struct", + "elements": [ + { + "Namespace": { + "name": { + "internal": "aaa", + "display": null + }, + "types": [ + { + "StructDef": { + "name": { + "internal": "Packet", + "display": null + }, + "namespace": "aaa", + "package": "", + "attributes": [], + "methods": [] + } + } + ], + "namespaces": [] + } + }, + { + "Types": { + "StructDef": { + "name": { + "internal": "Message", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "header", + "type": "int" + }, + { + "visibility": "Public", + "name": "payload", + "type": "string" + } + ], + "methods": [] + } + } + }, + { + "Types": { + "StructDef": { + "name": { + "internal": "Node", + "display": null + }, + "namespace": "", + "package": "", + "attributes": [ + { + "visibility": "Public", + "name": "id", + "type": null + }, + { + "visibility": "Private", + "name": "name", + "type": null + } + ], + "methods": [] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/class_diagram/struct/struct.puml b/plantuml/parser/puml_parser/tests/class_diagram/struct/struct.puml new file mode 100644 index 0000000..bad6edc --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/struct/struct.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 struct + +namespace aaa { + struct Packet +} + +struct Message { + + header : int + + payload : string +} + +struct Node { + + id + - name +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/BUILD b/plantuml/parser/puml_parser/tests/preprocessor/include/BUILD new file mode 100644 index 0000000..e67c978 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/BUILD @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +filegroup( + name = "include_preprocessor_files", + srcs = glob([ + "**/*.puml", + "**/*.yaml", + ]), + visibility = ["//plantuml/parser:__subpackages__"], +) diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/output.yaml new file mode 100644 index 0000000..884dd84 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + package user { + class Common { + +id: int + +login() + } + } + @enduml +user2.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + package user { + } + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user.puml new file mode 100644 index 0000000..656def7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user.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 + +!include common.puml +class User {} + +package user { + !include_many common.puml +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user2.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user2.puml new file mode 100644 index 0000000..e21c9a7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/combine_include_and_include_many/user2.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 + +!include_many common.puml +class User {} + +package user { + !include common.puml +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/feature.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/feature.puml new file mode 100644 index 0000000..8d45fe6 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/feature.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 + +package feature { + class Feature {} + !startsub Info + !include_once common.puml + class FeatureImpl {} + !endsub +} +' will not expended +!include common.puml +' will expended +!include_many common.puml + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/output.yaml new file mode 100644 index 0000000..91f728d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + @enduml +feature.puml: | + @startuml + package feature { + class Feature {} + class Common { + +id: int + +login() + } + class FeatureImpl {} + } + class Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class FeatureImpl {} + class User {} + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/user.puml new file mode 100644 index 0000000..4c237c5 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/complex_include/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 feature.puml!Info +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/feature.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/feature.puml new file mode 100644 index 0000000..0bd71d2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/feature.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 + +!include user.puml +class Feature {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/output.yaml new file mode 100644 index 0000000..50f23ec --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/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: "CycleInclude" + chain: + - "user.puml -> feature.puml -> user.puml" + - "feature.puml -> user.puml -> feature.puml" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/user.puml new file mode 100644 index 0000000..802b1ed --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_cycle_include/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 + +!include feature.puml +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/output.yaml new file mode 100644 index 0000000..db79297 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/output.yaml @@ -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 +# ******************************************************************************* +user.puml: + error: + type: "FileNotFound" + path: "unknow.puml" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/user.puml new file mode 100644 index 0000000..abba705 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_path/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 + +!include unknow.puml +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_unknow_sub/common.puml new file mode 100644 index 0000000..7f0e116 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_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_unknow_sub/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_sub/output.yaml new file mode 100644 index 0000000..539abe4 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_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: "UnknowSub" + file: "common.puml" 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_unknow_sub/user.puml new file mode 100644 index 0000000..c1821d0 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_include_unknow_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!UnknowSub +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/common.puml new file mode 100644 index 0000000..dd988b6 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/common.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 common + +class School {} + +!startsub Info + class Student { + +name: string + } + !startsub Nest_Info + class Common { + +id: int + } + !endsub +!endsub + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/output.yaml new file mode 100644 index 0000000..5257fae --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_nested_subblock/output.yaml @@ -0,0 +1,24 @@ +# ******************************************************************************* +# 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: + error: + type: "ParseFailed" + path: "common.puml" + source: + file: "common.puml" + type: "SyntaxError" + line: 9 + column: 6 + message: "unexpected preprocess_keyword" + source_line: | + !startsub Nest_Info diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/output.yaml new file mode 100644 index 0000000..0e26283 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/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 +# ******************************************************************************* +user.puml: + error: + type: "IncludeOnceViolated" + file: "user.puml" + conflict: "common.puml" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/user.puml new file mode 100644 index 0000000..893ab34 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_repeat_include_once/user.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 + +!include_once common.puml +class User {} +!include_once common.puml + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/output.yaml new file mode 100644 index 0000000..8647860 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/output.yaml @@ -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 +# ******************************************************************************* +user.puml: + error: + type: "ParseFailed" + path: "user.puml" + source: + type: "InvalidTextLine" + line: | + !include common.puml!ErrorSuffix + file: "user.puml" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/user.puml new file mode 100644 index 0000000..13d63c7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_include/user.puml @@ -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 +' ******************************************************************************* +@startuml + +!include common.puml!ErrorSuffix + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/output.yaml new file mode 100644 index 0000000..70e982f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/output.yaml @@ -0,0 +1,24 @@ +# ******************************************************************************* +# 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: "ParseFailed" + path: "user.puml" + source: + file: "user.puml" + type: "SyntaxError" + line: 3 + column: 33 + message: "unexpected include_label" + source_line: | + !includesub common.puml!Suffix1!ErrorSuffix diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/user.puml new file mode 100644 index 0000000..31b5718 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/invalid_suffix_for_includesub/user.puml @@ -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 +' ******************************************************************************* +@startuml + +!includesub common.puml!Suffix1!ErrorSuffix + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/output.yaml new file mode 100644 index 0000000..4552372 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + package user { + } + class User {} + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/user.puml new file mode 100644 index 0000000..2ed7769 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/repeat_include/user.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 + +!include common.puml +package user { + !include common.puml +} +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/common.puml new file mode 100644 index 0000000..5e202fe --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/common.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 common + +!startsub Info +class Common { + +id: int + +login() +} +!endsub + +class School {} + +!startsub Info +class Student { + +name: string +} +!endsub + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/output.yaml new file mode 100644 index 0000000..c7c5210 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/output.yaml @@ -0,0 +1,25 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +common.puml: "@startuml common\nclass Common {\n +id: int\n +login()\n}\nclass School {} \nclass Student {\n +name: string\n}\n@enduml\n" +user.puml: | + @startuml + package "User" { + class Common { + +id: int + +login() + } + class Student { + +name: string + } + } + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/user.puml new file mode 100644 index 0000000..870026c --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/several_subblock/user.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 + +package "User" { + !includesub common.puml!Info +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/output.yaml new file mode 100644 index 0000000..6a14319 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/user.puml new file mode 100644 index 0000000..7ec19b0 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include/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 + +!include ../simple_include/common.puml +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/output.yaml new file mode 100644 index 0000000..92cc3d3 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/output.yaml @@ -0,0 +1,33 @@ +# ******************************************************************************* +# 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 Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + package user { + class Common { + +id: int + +login() + } + } + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/user.puml new file mode 100644 index 0000000..656def7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_many/user.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 + +!include common.puml +class User {} + +package user { + !include_many common.puml +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/common.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/common.puml new file mode 100644 index 0000000..bad7dd2 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/common.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 common + +class Common { + +id: int + +login() +} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/output.yaml new file mode 100644 index 0000000..6a14319 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/user.puml b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/user.puml new file mode 100644 index 0000000..e9d4dc1 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_include_once/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 + +!include_once common.puml +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 new file mode 100644 index 0000000..0b0d385 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/common.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 common + +!startsub CommonClass +class Common { + +id: int + +login() +} +!endsub + +class Student { + +name: String +} + +@enduml 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 new file mode 100644 index 0000000..db5f85b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/output.yaml @@ -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 +# ******************************************************************************* +common.puml: | + @startuml common + class Common { + +id: int + +login() + } + class Student { + +name: String + } + @enduml +user.puml: | + @startuml + class Common { + +id: int + +login() + } + class User {} + @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 new file mode 100644 index 0000000..59205dc --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/include/simple_includesub/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!CommonClass +class User {} + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/BUILD b/plantuml/parser/puml_parser/tests/preprocessor/procedure/BUILD new file mode 100644 index 0000000..5b248da --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/BUILD @@ -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 +# ******************************************************************************* +filegroup( + name = "procedure_files", + srcs = glob([ + "**/*.puml", + "**/*.yaml", + ]), + visibility = ["//plantuml/parser:__subpackages__"], +) diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/args_not_match.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/args_not_match.puml new file mode 100644 index 0000000..afba782 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/args_not_match.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 + +!procedure $TopEvent($name, $alias) + rectangle "$name" as $alias +!endprocedure + +$TopEvent("1", "2", "3") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/output.yaml new file mode 100644 index 0000000..fda9732 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/args_not_match/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 +# ******************************************************************************* +args_not_match.puml: + error: + type: "ArgumentMismatch" + fields: + name: "$TopEvent" + expected: "2" + actual: "3" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml new file mode 100644 index 0000000..582acdb --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/empty.puml @@ -0,0 +1,34 @@ +' ******************************************************************************* +' 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 msg($source, $destination) + $source --> $destination +!endprocedure + +!procedure init_class($name) + class $name { + $addCommonMethod() +} +!endprocedure + +!procedure $addCommonMethod() + toString() + hashCode("utf-8", 12345) +!endprocedure + +init_class("foo1") +init_class("foo2") +msg("foo1", "foo2") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/output.yaml new file mode 100644 index 0000000..3880c89 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/empty/output.yaml @@ -0,0 +1,24 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +empty.puml: | + @startuml + class foo1 { + toString() + hashCode("utf-8", 12345) + } + class foo2 { + toString() + hashCode("utf-8", 12345) + } + foo1 --> foo2 + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/fta_metamodel.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/fta_metamodel.puml new file mode 100644 index 0000000..ba7ba65 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/fta_metamodel.puml @@ -0,0 +1,44 @@ +' ******************************************************************************* +' 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 $TopEvent($name, $alias) + rectangle "$name" as $alias +!endprocedure + +!procedure $IntermediateEvent($name, $alias, $connection) + rectangle "$name" as $alias + $alias -u-> $connection +!endprocedure + +!procedure $BasicEvent($name, $alias, $connection) + usecase "$name" as $alias + $alias -u-> $connection +!endprocedure + +!procedure $AndGate($alias, $connection) + rectangle " " <<$and>> as $alias + $alias -u-> $connection +!endprocedure + +!procedure $OrGate($alias, $connection) + rectangle " " <<$or>> as $alias + $alias -u-> $connection +!endprocedure + +!procedure $TransferInGate($alias, $connection) + rectangle " " <<$transferin>> as $alias + $alias -u-> $connection +!endprocedure + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/output.yaml new file mode 100644 index 0000000..c24a94d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/output.yaml @@ -0,0 +1,34 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +sample_fta.puml: | + @startuml + rectangle "SampleFailureMode takes over the world" as SampleLibrary.SampleFailureMode + + rectangle " " <<$or>> as OG1 + OG1 -u-> SampleLibrary.SampleFailureMode + + rectangle "SampleFailureMode is Angry" as IEF + IEF -u-> OG1 + + usecase "Just bad luck" as SampleLibrary.JustBadLuck + SampleLibrary.JustBadLuck -u-> OG1 + + rectangle " " <<$and>> as AG2 + AG2 -u-> IEF + + usecase "No More Cookies" as SampleLibrary.NoMoreCookies + SampleLibrary.NoMoreCookies -u-> AG2 + + usecase "No More Coffee" as SampleLibrary.NoMoreCoffee + SampleLibrary.NoMoreCoffee -u-> AG2 + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/sample_fta.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/sample_fta.puml new file mode 100644 index 0000000..1a6f447 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/fta_metamodel/sample_fta.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 + +!include fta_metamodel.puml + +' Top level (skeleton) +$TopEvent("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +' 2nd level gates and events +$OrGate("OG1", "SampleLibrary.SampleFailureMode") +$IntermediateEvent("SampleFailureMode is Angry", "IEF", "OG1") +$BasicEvent("Just bad luck", "SampleLibrary.JustBadLuck", "OG1") + +' 3rd level cascades from AGF +$AndGate("AG2", "IEF") +$BasicEvent("No More Cookies", "SampleLibrary.NoMoreCookies", "AG2") +$BasicEvent("No More Coffee", "SampleLibrary.NoMoreCoffee", "AG2") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/macro_not_define.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/macro_not_define.puml new file mode 100644 index 0000000..94c524f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/macro_not_define.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 + +!procedure $TopEventA($name, $alias) + rectangle "$name" as $alias +!endprocedure + +$TopEvent("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/output.yaml new file mode 100644 index 0000000..cb700da --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/macro_not_define/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 +# ******************************************************************************* +macro_not_define.puml: + error: + type: "MacroNotDefined" + fields: + name: "$TopEvent" 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 new file mode 100644 index 0000000..64103d9 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/mix_call.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 + +!procedure $Basic($name, $alias) + usecase "$name" as $alias +!endprocedure + +!procedure BasicEvent($name, $alias, $connection) + $Basic($name, $alias) + $alias -u-> $connection +!endprocedure + +BasicEvent("No More Cookies", "SampleLibrary.NoMoreCookies", "AG2") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/output.yaml new file mode 100644 index 0000000..c78f4e8 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/mix_call/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 +# ******************************************************************************* +mix_call.puml: | + @startuml + usecase "No More Cookies" as SampleLibrary.NoMoreCookies + SampleLibrary.NoMoreCookies -u-> AG2 + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/noise_item.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/noise_item.puml new file mode 100644 index 0000000..efb47a7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/noise_item.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 + +!procedure $TopEvent($name, $alias) + rectangle "$noise_item $name cost $100" as $alias +!endprocedure + +$TopEvent("SampleFailureMode", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/output.yaml new file mode 100644 index 0000000..92c23c9 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/noise_item/output.yaml @@ -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 +# ******************************************************************************* +noise_item.puml: | + @startuml + rectangle "$noise_item SampleFailureMode cost $100" as SampleLibrary.SampleFailureMode + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/output.yaml new file mode 100644 index 0000000..9d81e0b --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/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 +# ******************************************************************************* +recursive_macro.puml: + error: + type: "RecursiveMacro" + fields: + name: "$TopEventA" + chain: "$TopEventA -> $TopEventB" diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/recursive_macro.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/recursive_macro.puml new file mode 100644 index 0000000..b0b5157 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/recursive_macro/recursive_macro.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml + +!procedure $TopEventA($name, $alias) + $TopEventB($name, $alias) +!endprocedure + +!procedure $TopEventB($name, $alias) + $TopEventA($name, $alias) +!endprocedure + +$TopEventA("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/output.yaml new file mode 100644 index 0000000..592502a --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/output.yaml @@ -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 +# ******************************************************************************* +simple_macro.puml: | + @startuml + rectangle "SampleFailureMode takes over the world" as SampleLibrary.SampleFailureMode + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/simple_macro.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/simple_macro.puml new file mode 100644 index 0000000..b9fb39a --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_macro/simple_macro.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 + +!procedure $TopEvent($name, $alias) + rectangle "$name" as $alias +!endprocedure + +$TopEvent("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/output.yaml new file mode 100644 index 0000000..83fd8b4 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/output.yaml @@ -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 +# ******************************************************************************* +simple_template.puml: | + @startuml + rectangle "SampleFailureMode takes over the world" as SampleLibrary.SampleFailureMode + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/simple_template.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/simple_template.puml new file mode 100644 index 0000000..78bcfd8 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/simple_template/simple_template.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 + +!procedure TopEvent($name, $alias) + rectangle "$name" as $alias +!endprocedure + +TopEvent("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/fta.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/fta.puml new file mode 100644 index 0000000..d0e3a69 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/fta.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 + +!procedure $TopEvent($name, $alias) + rectangle "$name" as $alias +!endprocedure + +@enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/output.yaml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/output.yaml new file mode 100644 index 0000000..095b34c --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/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 +# ******************************************************************************* +fta.puml: | + @startuml + @enduml +simple.puml: | + @startuml + rectangle "SampleFailureMode takes over the world" as SampleLibrary.SampleFailureMode + @enduml diff --git a/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/simple.puml b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/simple.puml new file mode 100644 index 0000000..ca12d75 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/preprocessor/procedure/use_include/simple.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 + +!include fta.puml + +$TopEvent("SampleFailureMode takes over the world", "SampleLibrary.SampleFailureMode") + +@enduml diff --git a/plantuml/parser/puml_resolver/BUILD b/plantuml/parser/puml_resolver/BUILD new file mode 100644 index 0000000..53adcda --- /dev/null +++ b/plantuml/parser/puml_resolver/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "puml_resolver", + srcs = ["src/lib.rs"], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + ":class_resolver", + ":component_resolver", + ":resolver_traits", + ], +) + +alias( + name = "component_resolver", + actual = "//plantuml/parser/puml_resolver/src/component_diagram:puml_resolver_component", + visibility = ["//plantuml/parser:__subpackages__"], +) + +alias( + name = "class_resolver", + actual = "//plantuml/parser/puml_resolver/src/class_diagram:puml_resolver_class", + visibility = ["//plantuml/parser:__subpackages__"], +) + +rust_library( + name = "resolver_traits", + srcs = [ + "src/resolver_traits.rs", + ], + crate_name = "resolver_traits", + crate_root = "src/resolver_traits.rs", + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "@crates//:log", + ], +) diff --git a/plantuml/parser/puml_resolver/src/class_diagram/BUILD b/plantuml/parser/puml_resolver/src/class_diagram/BUILD new file mode 100644 index 0000000..7930900 --- /dev/null +++ b/plantuml/parser/puml_resolver/src/class_diagram/BUILD @@ -0,0 +1,62 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "puml_resolver_class", + srcs = [ + "src/class_resolver.rs", + "src/lib.rs", + ], + crate_name = "class_resolver", + crate_root = "src/lib.rs", + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + "//plantuml/parser/puml_parser:class_diagram", + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_resolver:resolver_traits", + "//tools/metamodel:class_diagram", + "@crates//:log", + "@crates//:serde", + "@crates//:thiserror", + ], +) + +rust_test( + name = "puml_resolver_class_unit_test", + crate = ":puml_resolver_class", +) + +rust_test( + name = "puml_resolver_class_it", + srcs = [ + "test/class_resolver_test.rs", + ], + crate_root = "test/class_resolver_test.rs", + data = [ + "//plantuml/parser/integration_test/class_diagram:class_diagram_files", + ], + visibility = ["//plantuml/parser:__subpackages__"], + deps = [ + ":puml_resolver_class", + "//plantuml/parser/integration_test:test_framework", + "//plantuml/parser/puml_parser:class_diagram", + "//plantuml/parser/puml_parser:parser_core", + "//plantuml/parser/puml_resolver:resolver_traits", + "//plantuml/parser/puml_utils", + "//tools/metamodel:class_diagram", + "@crates//:serde", + "@crates//:serde_json", + "@crates//:thiserror", + ], +) 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 new file mode 100644 index 0000000..f429504 --- /dev/null +++ b/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs @@ -0,0 +1,732 @@ +// ******************************************************************************* +// 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 std::collections::HashMap; + +use class_diagram::Visibility as ResolverVisibility; +use class_diagram::*; +use class_parser::Visibility as ParserVisibility; +use class_parser::{ + Attribute, ClassUmlFile, ClassUmlTopLevel, Element, EnumDef, EnumValue, Method, Namespace, + Package, Param, Relationship, +}; +use parser_core::common_ast::Arrow; +use resolver_traits::DiagramResolver; + +pub struct ClassResolver { + pub logic: ClassDiagram, + + // simple name -> FQN + name_map: HashMap, +} + +impl Default for ClassResolver { + fn default() -> Self { + Self::new() + } +} + +impl ClassResolver { + pub fn new() -> Self { + Self { + logic: ClassDiagram { + name: String::new(), + entities: Vec::new(), + containers: Vec::new(), + relationships: Vec::new(), + source_files: Vec::new(), + version: None, + }, + name_map: HashMap::new(), + } + } + + fn analyze(&mut self, file: &ClassUmlFile) -> Result<(), ClassResolverError> { + for elem in &file.elements { + self.process_top_level(elem, None)?; + } + + for rel in &file.relationships { + self.process_relationship(rel, None)?; + } + + Ok(()) + } + + pub fn result(self) -> ClassDiagram { + self.logic + } + + fn map_visibility(v: ParserVisibility) -> ResolverVisibility { + match v { + ParserVisibility::Public => ResolverVisibility::Public, + ParserVisibility::Private => ResolverVisibility::Private, + ParserVisibility::Protected => ResolverVisibility::Protected, + ParserVisibility::Package => ResolverVisibility::Package, + } + } + + fn build_fqn(&self, name: &str, parent: &Option) -> String { + match parent { + Some(p) => format!("{}.{}", p, name), + None => name.to_string(), + } + } + + fn resolve_name(&self, name: &str, parent: &Option) -> Option { + // 1. FQN + if name.contains('.') || name.contains("::") { + return Some(name.to_string()); + } + + // 2. Current Namespace + if let Some(p) = parent { + let candidate = format!("{}.{}", p, name); + + // All three checks now verify the candidate actually exists + if self.logic.entities.iter().any(|e| e.id == candidate) + || self + .logic + .entities + .iter() + .any(|e| e.id == candidate && e.name.as_deref() == Some(name)) + || self + .logic + .entities + .iter() + .any(|e| e.id == candidate && e.alias.as_deref() == Some(name)) + { + return Some(candidate); + } + } + + // 3. Global search id + if let Some(id) = self.name_map.get(name) { + return Some(id.clone()); + } + + // 4. Global search name / alias + if let Some(e) = self + .logic + .entities + .iter() + .find(|e| e.name.as_deref() == Some(name) || e.alias.as_deref() == Some(name)) + { + return Some(e.id.clone()); + } + + None + } + + fn process_top_level( + &mut self, + elem: &ClassUmlTopLevel, + parent: Option, + ) -> Result<(), ClassResolverError> { + match elem { + ClassUmlTopLevel::Types(element) => { + self.process_element(element, parent); + } + + ClassUmlTopLevel::Enum(enum_def) => { + self.process_enum(enum_def, parent); + } + + ClassUmlTopLevel::Namespace(ns) => { + self.process_namespace(ns, parent)?; + } + + ClassUmlTopLevel::Package(pkg) => { + self.process_package(pkg, parent)?; + } + } + Ok(()) + } + + fn process_package( + &mut self, + pkg: &Package, + parent: Option, + ) -> Result<(), ClassResolverError> { + let fqn = match &parent { + Some(p) => format!("{}.{}", p, pkg.name.internal), + None => pkg.name.internal.clone(), + }; + + self.logic.containers.push(LogicContainer { + id: fqn.clone(), + name: pkg.name.internal.clone(), + parent_id: parent.clone(), + container_type: ContainerType::Package, + }); + + for t in &pkg.types { + self.process_element(t, Some(fqn.clone())); + } + + for sub in &pkg.packages { + self.process_package(sub, Some(fqn.clone()))?; + } + + for rel in &pkg.relationships { + self.process_relationship(rel, Some(fqn.clone()))?; + } + + Ok(()) + } + + fn process_namespace( + &mut self, + ns: &Namespace, + parent: Option, + ) -> Result<(), ClassResolverError> { + let fqn = match &parent { + Some(p) => format!("{}.{}", p, ns.name.internal), + None => ns.name.internal.clone(), + }; + + self.logic.containers.push(LogicContainer { + id: fqn.clone(), + name: ns.name.internal.clone(), + parent_id: parent.clone(), + container_type: ContainerType::Namespace, + }); + + for t in &ns.types { + self.process_element(t, Some(fqn.clone())); + } + + for sub in &ns.namespaces { + self.process_namespace(sub, Some(fqn.clone()))?; + } + + Ok(()) + } + + fn process_element(&mut self, element: &Element, parent: Option) { + match element { + Element::EnumDef(def) => self.process_enum(def, parent), + _ => { + let entity_type = match element { + Element::ClassDef(_) => EntityType::Class, + Element::StructDef(_) => EntityType::Struct, + Element::InterfaceDef(_) => EntityType::Interface, + _ => unreachable!(), + }; + self.process_class(element, parent, entity_type); + } + } + } + + fn process_class(&mut self, def: &Element, parent: Option, entity_type: EntityType) { + let (name, attributes, methods) = match def { + Element::ClassDef(c) => (&c.name, &c.attributes, &c.methods), + Element::StructDef(s) => (&s.name, &s.attributes, &s.methods), + Element::InterfaceDef(i) => (&i.name, &i.attributes, &i.methods), + Element::EnumDef(_) => unreachable!("EnumDef should not be passed to process_class"), + }; + + let id = self.build_fqn(&name.internal, &parent); + + let entity = LogicEntity { + id: id.clone(), + name: Some(name.internal.clone()), + alias: name.display.clone(), + parent_id: parent.clone(), + entity_type, + stereotypes: vec![], + attributes: attributes.iter().map(Self::convert_attr).collect(), + methods: methods.iter().map(Self::convert_method).collect(), + template_params: vec![], + enum_literals: vec![], + source_file: None, + source_line: None, + }; + + self.name_map.insert(name.internal.clone(), id.clone()); + self.logic.entities.push(entity); + } + + fn convert_attr(attr: &Attribute) -> LogicAttribute { + LogicAttribute { + name: attr.name.clone(), + data_type: attr.r#type.clone(), + visibility: Self::map_visibility(attr.visibility.clone()), + default_value: None, + is_static: false, + is_const: false, + description: None, + } + } + + fn convert_method(m: &Method) -> LogicMethod { + LogicMethod { + name: m.name.clone(), + return_type: m.r#type.clone(), + visibility: Self::map_visibility(m.visibility.clone()), + parameters: m.params.iter().map(Self::convert_param).collect(), + template_params: m.generic_params.clone(), + is_static: false, + is_const: false, + is_virtual: false, + is_abstract: false, + is_override: false, + is_constructor: false, + is_destructor: false, + } + } + + fn convert_param(param: &Param) -> LogicParameter { + LogicParameter { + name: param.name.clone().unwrap(), + param_type: Some(param.param_type.clone()), + default_value: None, + is_reference: false, + is_const: false, + is_variadic: param.varargs, + } + } + + fn process_enum(&mut self, def: &EnumDef, parent: Option) { + let id = self.build_fqn(&def.name.internal, &parent); + + let literals = def + .items + .iter() + .map(|item| LogicEnumLiteral { + name: item.name.clone(), + visibility: item + .visibility + .clone() + .map(Self::map_visibility) + .unwrap_or(ResolverVisibility::Public), + value: match &item.value { + Some(EnumValue::Literal(v)) => Some(v.clone()), + Some(EnumValue::Description(d)) => Some(d.clone()), + None => None, + }, + description: None, + }) + .collect(); + + self.logic.entities.push(LogicEntity { + id: id.clone(), + name: def.name.display.clone(), + alias: None, + parent_id: parent.clone(), + entity_type: EntityType::Enum, + stereotypes: def.stereotypes.clone(), + attributes: vec![], + methods: vec![], + template_params: vec![], + enum_literals: literals, + source_file: None, + source_line: None, + }); + + self.name_map.insert(def.name.internal.clone(), id); + } + + fn convert_arrow(&self, arrow: &Arrow) -> Result<(RelationType, bool), ClassResolverError> { + let left = arrow.left.as_ref().map(|d| d.raw.as_str()).unwrap_or(""); + let line = arrow.line.raw.as_str(); + let right = arrow.right.as_ref().map(|d| d.raw.as_str()).unwrap_or(""); + + // ---------------- Inheritance ---------------- + // A <|-- B => B extends A (reversed) + if left == "<|" && line == "--" { + return Ok((RelationType::Inheritance, true)); + } + // A --|> B => A extends B (normal) + if line == "--" && right == "|>" { + return Ok((RelationType::Inheritance, false)); + } + + // ---------------- Implementation ---------------- + // A <|.. B => B implements A (reversed) + if left == "<|" && line == ".." { + return Ok((RelationType::Implementation, true)); + } + // A ..|> B => A implements B (normal) + if line == ".." && right == "|>" { + return Ok((RelationType::Implementation, false)); + } + + // ---------------- Composition ---------------- + // *-- or --* + if left == "*" { + return Ok((RelationType::Composition, true)); + } + if right == "*" { + return Ok((RelationType::Composition, false)); + } + + // ---------------- Aggregation ---------------- + if left == "o" { + return Ok((RelationType::Aggregation, true)); + } + if right == "o" { + return Ok((RelationType::Aggregation, false)); + } + + // ---------------- Association ---------------- + if line == "--" && right == ">" { + return Ok((RelationType::Association, false)); + } + if left == "<" && line == "--" { + return Ok((RelationType::Association, true)); + } + + // ---------------- Dependency ---------------- + if line == ".." && right == ">" { + return Ok((RelationType::Dependency, false)); + } + if left == "<" && line == ".." { + return Ok((RelationType::Dependency, true)); + } + + // ---------------- Undirected ---------------- + if line == "--" { + return Ok((RelationType::Link, false)); + } + + if line == ".." { + return Ok((RelationType::DashedLink, false)); + } + + Err(ClassResolverError::InvalidRelationship { + from: left.to_string(), + to: right.to_string(), + reason: format!("Unsupported arrow pattern: {}{}{}", left, line, right), + }) + } + + fn process_relationship( + &mut self, + rel: &Relationship, + parent: Option, + ) -> Result<(), ClassResolverError> { + let left = self.resolve_name(&rel.left, &parent).ok_or_else(|| { + ClassResolverError::UnresolvedReference { + reference: rel.left.clone(), + } + })?; + + let right = self.resolve_name(&rel.right, &parent).ok_or_else(|| { + ClassResolverError::UnresolvedReference { + reference: rel.right.clone(), + } + })?; + + let (relation_type, reversed) = self.convert_arrow(&rel.arrow)?; + + let (source_id, target_id) = if reversed { + (right, left) + } else { + (left, right) + }; + + let (label, stereotype) = match &rel.label { + Some(text) => { + let trimmed = text.trim(); + if trimmed.starts_with("<<") && trimmed.ends_with(">>") { + let inner = trimmed + .trim_start_matches("<<") + .trim_end_matches(">>") + .trim() + .to_string(); + (None, Some(inner)) + } else { + (Some(text.clone()), None) + } + } + None => (None, None), + }; + + self.logic.relationships.push(LogicRelationship { + source: source_id, + target: target_id, + relation_type, + label, + stereotype, + source_multiplicity: None, + target_multiplicity: None, + source_role: None, + target_role: None, + }); + + Ok(()) + } +} + +impl DiagramResolver for ClassResolver { + type Document = ClassUmlFile; + type Statement = (); + type Output = HashMap; + 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()); + + self.analyze(document)?; + + let logic_class = std::mem::replace( + &mut self.logic, + ClassDiagram { + name: String::new(), + entities: Vec::new(), + containers: Vec::new(), + relationships: Vec::new(), + source_files: Vec::new(), + version: None, + }, + ); + + result.insert(logic_class.name.clone(), logic_class); + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use class_parser::{ClassDef, EnumItem, Name}; + use parser_core::common_ast::{ArrowDecor, ArrowLine}; + + // ---------------------------- + // whl Name / Class / Arrow + // ---------------------------- + fn make_name(name: &str) -> Name { + Name { + internal: name.to_string(), + display: None, + } + } + + fn make_class(name: &str) -> Element { + Element::ClassDef(ClassDef { + name: make_name(name), + namespace: "".to_string(), + package: "".to_string(), + attributes: vec![], + methods: vec![], + }) + } + + fn make_enum(name: &str, items: Vec<&str>) -> Element { + Element::EnumDef(EnumDef { + name: make_name(name), + namespace: "".to_string(), + package: "".to_string(), + stereotypes: vec![], + items: items + .into_iter() + .map(|n| EnumItem { + name: n.to_string(), + visibility: Some(ParserVisibility::Public), + value: None, + }) + .collect(), + }) + } + + fn make_arrow(left: Option<&str>, line: &str, right: Option<&str>) -> Arrow { + Arrow { + left: left.map(|v| ArrowDecor { raw: v.to_string() }), + line: ArrowLine { + raw: line.to_string(), + }, + middle: None, + right: right.map(|v| ArrowDecor { raw: v.to_string() }), + } + } + + // ---------------------------- + // build_fqn + // ---------------------------- + #[test] + fn test_build_fqn_root() { + let resolver = ClassResolver::new(); + let fqn = resolver.build_fqn("User", &None); + assert_eq!(fqn, "User"); + } + + #[test] + fn test_build_fqn_nested() { + let resolver = ClassResolver::new(); + let fqn = resolver.build_fqn("User", &Some("core".to_string())); + assert_eq!(fqn, "core.User"); + } + + // ---------------------------- + // process_class + // ---------------------------- + #[test] + fn test_process_class() { + let mut resolver = ClassResolver::new(); + resolver.process_element(&make_class("User"), None); + assert_eq!(resolver.logic.entities.len(), 1); + + let entity = &resolver.logic.entities[0]; + assert_eq!(entity.id, "User"); + assert_eq!(entity.name.as_deref(), Some("User")); + assert_eq!(entity.entity_type, EntityType::Class); + } + + // ---------------------------- + // process_enum + // ---------------------------- + #[test] + fn test_process_enum() { + let mut resolver = ClassResolver::new(); + resolver.process_element(&make_enum("Color", vec!["Red", "Green", "Blue"]), None); + + assert_eq!(resolver.logic.entities.len(), 1); + + let entity = &resolver.logic.entities[0]; + assert_eq!(entity.id, "Color"); + assert_eq!(entity.entity_type, EntityType::Enum); + assert_eq!(entity.enum_literals.len(), 3); + } + + // ---------------------------- + // resolve_name + // ---------------------------- + #[test] + fn test_resolve_name_global() { + let mut resolver = ClassResolver::new(); + resolver.process_element(&make_class("User"), None); + + let resolved = resolver.resolve_name("User", &None); + assert_eq!(resolved, Some("User".to_string())); + } + + #[test] + fn test_resolve_name_namespace() { + let mut resolver = ClassResolver::new(); + resolver.process_element(&make_class("User"), Some("core".to_string())); + + let resolved = resolver.resolve_name("User", &Some("core".to_string())); + assert_eq!(resolved, Some("core.User".to_string())); + } + + // ---------------------------- + // convert_arrow + // ---------------------------- + #[test] + fn test_arrow_inheritance_reversed() { + 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); + } + + #[test] + fn test_arrow_inheritance_normal() { + let resolver = ClassResolver::new(); + let arrow = make_arrow(None, "--", Some("|>")); + + let (ty, reversed) = resolver.convert_arrow(&arrow).unwrap(); + assert_eq!(ty, RelationType::Inheritance); + assert!(!reversed); + } + + #[test] + fn test_arrow_association() { + 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); + } + + // ---------------------------- + // relationship + // ---------------------------- + #[test] + fn test_process_relationship_inheritance() { + let mut resolver = ClassResolver::new(); + + resolver.process_element(&make_class("A"), None); + resolver.process_element(&make_class("B"), None); + + let rel = Relationship { + left: "A".to_string(), + right: "B".to_string(), + arrow: make_arrow(Some("<|"), "--", None), + label: Some("<