From ce1c5f64e3b4ea35dd31ae4cb974306e9e0bfaf8 Mon Sep 17 00:00:00 2001 From: sheng Date: Fri, 14 Aug 2026 17:51:58 +0800 Subject: [PATCH 1/3] Add class design sequence validation --- validation/core/BUILD | 2 + .../class_design_sequence/BUILD | 42 ++++ .../class_design_sequence_suite.rs | 95 +++++++ .../BUILD | 37 +++ .../class_diagram.puml | 28 +++ .../expected.yaml | 20 ++ .../sequence_diagram.puml | 20 ++ .../negative_participant_method_missing/BUILD | 37 +++ .../class_diagram.puml | 20 ++ .../expected.yaml | 20 ++ .../sequence_diagram.puml | 20 ++ .../negative_participant_missing/BUILD | 37 +++ .../class_diagram.puml | 20 ++ .../expected.yaml | 19 ++ .../sequence_diagram.puml | 20 ++ .../BUILD | 37 +++ .../class_diagram.puml | 22 ++ .../expected.yaml | 13 + .../sequence_diagram.puml | 20 ++ .../BUILD | 37 +++ .../class_diagram.puml | 22 ++ .../expected.yaml | 13 + .../sequence_diagram.puml | 20 ++ .../BUILD | 37 +++ .../class_diagram.puml | 27 ++ .../expected.yaml | 13 + .../sequence_diagram.puml | 21 ++ .../BUILD | 37 +++ .../class_diagram.puml | 22 ++ .../expected.yaml | 13 + .../sequence_diagram.puml | 20 ++ validation/core/src/models/mod.rs | 1 + .../src/models/sequence_diagram_models.rs | 77 +++++- validation/core/src/profiles/unit.rs | 57 ++++- .../class_design_sequence_validator.rs | 236 ++++++++++++++++++ .../component_sequence_validator.rs | 12 +- validation/core/src/validators/mod.rs | 2 + .../class_design_sequence_validator_test.rs | 155 ++++++++++++ .../core/src/validators/test/fixtures.rs | 2 +- 39 files changed, 1333 insertions(+), 20 deletions(-) create mode 100644 validation/core/integration_test/class_design_sequence/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/sequence_diagram.puml create mode 100644 validation/core/src/validators/class_design_sequence_validator.rs create mode 100644 validation/core/src/validators/test/class_design_sequence_validator_test.rs diff --git a/validation/core/BUILD b/validation/core/BUILD index 2edf1253..ef392594 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -58,6 +58,7 @@ rust_library( "src/results/mod.rs", "src/validators/bazel_component_validator.rs", "src/validators/class_design_implementation_validator.rs", + "src/validators/class_design_sequence_validator.rs", "src/validators/component_internal_api_validator.rs", "src/validators/component_public_api_validator.rs", "src/validators/component_sequence_validator.rs", @@ -66,6 +67,7 @@ rust_library( "src/validators/shared/diagram_analysis.rs", "src/validators/shared/helpers.rs", "src/validators/shared/mod.rs", + "src/validators/test/class_design_sequence_validator_test.rs", "src/validators/test/component_internal_api_validator_test.rs", "src/validators/test/component_public_api_validator_test.rs", "src/validators/test/component_sequence_validator_test.rs", diff --git a/validation/core/integration_test/class_design_sequence/BUILD b/validation/core/integration_test/class_design_sequence/BUILD new file mode 100644 index 00000000..26a1bff8 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/BUILD @@ -0,0 +1,42 @@ +# ******************************************************************************* +# 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") + +filegroup( + name = "class_design_sequence_test_data", + srcs = [ + "//validation/core/integration_test/class_design_sequence/negative_participant_method_missing:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_missing:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match:case_data", + ], +) + +rust_test( + name = "class_design_sequence_integration_test", + srcs = ["class_design_sequence_suite.rs"], + crate_root = "class_design_sequence_suite.rs", + data = [ + ":class_design_sequence_test_data", + ], + deps = [ + "//validation/core:validation_cli", + "//validation/core/integration_test:test_framework", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs new file mode 100644 index 00000000..27cdf97c --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs @@ -0,0 +1,95 @@ +// *******************************************************************************[test] +fn positive_participant_short_name_unique_match_suite_case() { + assert_case("positive_participant_short_name_unique_match"); +} +// 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 test_framework::{ + assert_cli_result, collect_case_fbs_files, load_expected_yaml_fixture, normalize_yaml_result, + run_validation_profile, CliRunResult, +}; + +const SUITE_DIR: &str = "class_design_sequence"; + +fn run_case_from_cli( + case_dir: &str, + design_class_fbs_paths: &[String], + sequence_fbs_paths: &[String], +) -> CliRunResult { + run_validation_profile( + &format!("class_design_sequence_{case_dir}"), + "unit", + serde_json::json!({ + "design_classes": design_class_fbs_paths, + "sequence_diagrams": sequence_fbs_paths, + }), + ) +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); + let design_class_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "unit_design_class"); + let sequence_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "unit_design_sequence"); + + let result = if !design_class_fbs_paths.is_empty() && !sequence_fbs_paths.is_empty() { + run_case_from_cli(case_dir, &design_class_fbs_paths, &sequence_fbs_paths) + } else { + panic!( + "missing generated FBS fixtures for {case_dir}: expected at least one unit_design_class/*.fbs.bin and unit_design_sequence/*.fbs.bin", + ); + }; + + let result = normalize_yaml_result(result); + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn positive_participant_alias_display_name_class_name_match_suite_case() { + assert_case("positive_participant_alias_display_name_class_name_match"); +} + +#[test] +fn positive_participant_alias_display_name_namespace_match_suite_case() { + assert_case("positive_participant_alias_display_name_namespace_match"); +} + +#[test] +fn positive_participant_namespace_callee_method_match_suite_case() { + assert_case("positive_participant_namespace_callee_method_match"); +} + +#[test] +fn positive_participant_short_name_namespace_match_suite_case() { + assert_case("positive_participant_short_name_namespace_match"); +} + +#[test] +fn positive_participant_short_name_unique_match_suite_case() { + assert_case("positive_participant_short_name_unique_match"); +} + +#[test] +fn negative_participant_missing_suite_case() { + assert_case("negative_participant_missing"); +} + +#[test] +fn negative_participant_ambiguous_short_name_suite_case() { + assert_case("negative_participant_ambiguous_short_name"); +} + +#[test] +fn negative_participant_method_missing_suite_case() { + assert_case("negative_participant_method_missing"); +} diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/class_diagram.puml new file mode 100644 index 00000000..d4c5d3d7 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/class_diagram.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 class_diagram + +namespace unit_1 { + class Controller { + + Execute() : void + } +} + +namespace unit_2 { + class Controller { + + Execute() : void + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/expected.yaml new file mode 100644 index 00000000..ae97a157 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Class] Sequence participant "Controller" matches multiple classes in the class diagram. + Participant : "Controller" + Matching classes : "unit_1.Controller", "unit_2.Controller" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/sequence_diagram.puml" + Sequence source line : 16 + Fix : Rename participant "Controller" in the sequence diagram to a unique class id, or rename one of the matching classes in the class diagram. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/sequence_diagram.puml new file mode 100644 index 00000000..327c0a62 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name/sequence_diagram.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 sequence_diagram + +participant Controller + +Controller -> Controller : Execute() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/class_diagram.puml new file mode 100644 index 00000000..e659419d --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/class_diagram.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 class_diagram + +class Repository { + + Store() : void +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml new file mode 100644 index 00000000..ffbbb1f9 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Method] Sequence function "FindById" from sequence call "Repository" -> "Repository" : "FindById" not found on target class "Repository" in the class diagram. + Sequence call : "Repository" -> "Repository" : "FindById" + Target class : "Repository" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.puml" + Sequence source line : 18 + Fix : Add method "FindById" to class "Repository" in the class diagram, or change or remove that sequence call. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.puml new file mode 100644 index 00000000..96a72e6a --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.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 sequence_diagram + +participant Repository + +Repository -> Repository : FindById() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_missing/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_missing/class_diagram.puml new file mode 100644 index 00000000..7dca3c5f --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing/class_diagram.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 class_diagram + +class Controller { + + Execute() : void +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_missing/expected.yaml new file mode 100644 index 00000000..1a111261 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing/expected.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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Class] Sequence participant "Repository" has no matching class in the class diagram. + Participant : "Repository" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_missing/sequence_diagram.puml" + Sequence source line : 16 + Fix : Add class "Repository" to the class diagram, or remove the participant from the sequence diagram. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_missing/sequence_diagram.puml new file mode 100644 index 00000000..fae3055c --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing/sequence_diagram.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 sequence_diagram + +participant Repository + +Repository -> Repository : Execute() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/class_diagram.puml new file mode 100644 index 00000000..1668e090 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/class_diagram.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 class_diagram + +namespace A { + class Repository { + + FindById() : void + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/sequence_diagram.puml new file mode 100644 index 00000000..0c5bc953 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match/sequence_diagram.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 sequence_diagram + +participant "Repository" as Repo + +Repo -> Repo : FindById() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/class_diagram.puml new file mode 100644 index 00000000..9725a38f --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/class_diagram.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 class_diagram + +namespace unit_1 { + class Controller { + + Execute() : void + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/sequence_diagram.puml new file mode 100644 index 00000000..e2dba124 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match/sequence_diagram.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 sequence_diagram + +participant "unit_1::Controller" as controller + +controller -> controller : Execute() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml new file mode 100644 index 00000000..15556558 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.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 class_diagram + +class Controller { + + HandleRequest() : void +} + +namespace unit_1 { + class StateStore { + + LoadState() : void + + SaveState() : void + } +} + +@enduml \ No newline at end of file diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml new file mode 100644 index 00000000..02618daa --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.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 sequence_diagram + +participant Controller +participant "unit_1::StateStore" as StateStore + +Controller -> StateStore : LoadState() + +@enduml \ No newline at end of file diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/class_diagram.puml new file mode 100644 index 00000000..1668e090 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/class_diagram.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 class_diagram + +namespace A { + class Repository { + + FindById() : void + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/sequence_diagram.puml new file mode 100644 index 00000000..96a72e6a --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match/sequence_diagram.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 sequence_diagram + +participant Repository + +Repository -> Repository : FindById() + +@enduml diff --git a/validation/core/src/models/mod.rs b/validation/core/src/models/mod.rs index e0d31986..221543e5 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -33,4 +33,5 @@ pub use component_diagram_models::{ }; pub use sequence_diagram_models::{ is_external_endpoint, ObservedSequenceCall, SequenceDiagramIndex, SequenceDiagramInputs, + SequenceParticipantInfo, }; diff --git a/validation/core/src/models/sequence_diagram_models.rs b/validation/core/src/models/sequence_diagram_models.rs index 73cae587..aab9a207 100644 --- a/validation/core/src/models/sequence_diagram_models.rs +++ b/validation/core/src/models/sequence_diagram_models.rs @@ -38,6 +38,66 @@ pub struct ObservedSequenceCall { pub source_location: SourceLocation, } +/// Validation-only participant metadata keyed by the participant reference name +/// used in sequence interactions. +pub struct SequenceParticipantInfo { + pub display_name: String, + pub source_location: SourceLocation, +} + +impl SequenceParticipantInfo { + // TODO: Remove this normalization once class diagram identifiers also use + // `::` namespaces directly instead of `.`. + pub fn normalize_qualified_name(reference: &str) -> String { + reference.replace("::", ".") + } +} + +fn strip_supported_html_style_tags(text: &str) -> String { + let mut normalized = String::new(); + let mut index = 0; + + while index < text.len() { + let remaining = &text[index..]; + + if let Some(tag_len) = supported_html_style_tag_length(remaining) { + index += tag_len; + continue; + } + + let ch = remaining.chars().next().expect("remaining is non-empty"); + normalized.push(ch); + index += ch.len_utf8(); + } + + normalized +} + +fn supported_html_style_tag_length(text: &str) -> Option { + if !text.starts_with('<') { + return None; + } + + let end = text.find('>')?; + let tag = text[1..end].trim().to_ascii_lowercase(); + + let known_tags = [ + "b", "/b", "i", "/i", "u", "/u", "s", "/s", "w", "/w", "img", "/img", "font", "/font", + ]; + let styled_tags = ["color", "back", "size"]; + + let is_known_tag = known_tags.contains(&tag.as_str()); + let is_styled_tag = styled_tags.iter().any(|styled_tag| { + tag == format!("/{styled_tag}") || tag.starts_with(&format!("{styled_tag}:")) + }); + + if is_known_tag || is_styled_tag { + Some(end + 1) + } else { + None + } +} + impl SequenceDiagramInputs { /// Build a [`SequenceDiagramIndex`] from sequence diagram inputs. pub fn to_sequence_diagram_index(&self, result: &mut ValidationResult) -> SequenceDiagramIndex { @@ -47,7 +107,7 @@ impl SequenceDiagramInputs { /// Indexed sequence-diagram data prepared for validators. pub struct SequenceDiagramIndex { - participants: BTreeMap, + participants: BTreeMap, observed_calls: Vec, } @@ -68,7 +128,10 @@ impl SequenceDiagramIndex { // declared in more than one input diagram. participants .entry(reference_name) - .or_insert_with(|| participant.source_location.clone()); + .or_insert_with(|| SequenceParticipantInfo { + display_name: strip_supported_html_style_tags(&participant.display_name), + source_location: participant.source_location.clone(), + }); } collect_block_data(&diagram.root, &mut observed_calls, result); @@ -80,10 +143,18 @@ impl SequenceDiagramIndex { } } - pub fn participants(&self) -> &BTreeMap { + pub fn participants(&self) -> &BTreeMap { &self.participants } + pub fn declared_participants(&self) -> impl Iterator { + self.participants.keys().map(String::as_str) + } + + pub fn participant_info(&self, participant: &str) -> Option<&SequenceParticipantInfo> { + self.participants.get(participant) + } + pub fn observed_calls(&self) -> &[ObservedSequenceCall] { &self.observed_calls } diff --git a/validation/core/src/profiles/unit.rs b/validation/core/src/profiles/unit.rs index aa095f65..49e53ad1 100644 --- a/validation/core/src/profiles/unit.rs +++ b/validation/core/src/profiles/unit.rs @@ -11,21 +11,51 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use crate::models::{ClassDiagramInputs, ClassEntityIndex}; -use crate::readers::ClassDiagramReader; -use crate::validators::validate_class_design_implementation; +use crate::models::{ + ClassDiagramInputs, ClassEntityIndex, SequenceDiagramIndex, SequenceDiagramInputs, +}; +use crate::readers::{ClassDiagramReader, SequenceDiagramReader}; +use crate::validators::{validate_class_design_implementation, validate_class_design_sequence}; use crate::ValidationResult; use serde::Deserialize; use super::profile::{merge_results, read_and_convert, ProfileRun}; +type ProfileValidator<'a> = Box Option + 'a>; + #[derive(Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct UnitInputs { design_classes: Vec, + sequence_diagrams: Vec, implementation_classes: Vec, } +fn registered_validators<'a>( + design_classes: &'a Option, + sequence_diagrams: &'a Option, + implementation_classes: &'a Option, +) -> Vec> { + vec![ + Box::new(move || { + let (design_classes, implementation_classes) = + (design_classes.as_ref()?, implementation_classes.as_ref()?); + Some(validate_class_design_implementation( + design_classes, + implementation_classes, + )) + }), + Box::new(move || { + let (design_classes, sequence_diagrams) = + (design_classes.as_ref()?, sequence_diagrams.as_ref()?); + Some(validate_class_design_sequence( + design_classes, + sequence_diagrams, + )) + }), + ] +} + pub fn run(inputs: &UnitInputs) -> Result { let mut result = ValidationResult::default(); let design_classes = read_and_convert::( @@ -38,16 +68,21 @@ pub fn run(inputs: &UnitInputs) -> Result { &mut result, |raw: ClassDiagramInputs, errs| ClassEntityIndex::build_index(&raw, errs), )?; + let sequence_diagrams = read_and_convert::( + inputs.sequence_diagrams.as_slice(), + &mut result, + |raw: SequenceDiagramInputs, errs| raw.to_sequence_diagram_index(errs), + )?; + + let validators = + registered_validators(&design_classes, &sequence_diagrams, &implementation_classes); let mut ran_validator = false; - if let (Some(design_classes), Some(implementation_classes)) = - (design_classes.as_ref(), implementation_classes.as_ref()) - { - merge_results( - &mut result, - validate_class_design_implementation(design_classes, implementation_classes), - ); - ran_validator = true; + for validator in validators { + if let Some(validator_result) = validator() { + merge_results(&mut result, validator_result); + ran_validator = true; + } } Ok(ProfileRun { diff --git a/validation/core/src/validators/class_design_sequence_validator.rs b/validation/core/src/validators/class_design_sequence_validator.rs new file mode 100644 index 00000000..f36fcf54 --- /dev/null +++ b/validation/core/src/validators/class_design_sequence_validator.rs @@ -0,0 +1,236 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Validation: compare unit class-design entities with sequence-diagram usage. + +use std::collections::BTreeSet; + +use super::shared::{extract_method_name, format_sequence_call}; +use crate::models::{ClassEntityIndex, SequenceDiagramIndex, SequenceParticipantInfo}; +use crate::{Diagnostics, ErrorBuilder, ErrorCategory, ValidationResult}; + +/// Run class-design-vs-sequence validation. +pub fn validate_class_design_sequence( + design_classes: &ClassEntityIndex, + sequence_diagram: &SequenceDiagramIndex, +) -> ValidationResult { + ClassDesignSequenceValidator::new(design_classes, sequence_diagram).run() +} + +struct ClassDesignSequenceValidator<'a> { + design_classes: &'a ClassEntityIndex, + sequence_diagram: &'a SequenceDiagramIndex, + result: ValidationResult, +} + +impl<'a> ClassDesignSequenceValidator<'a> { + fn new( + design_classes: &'a ClassEntityIndex, + sequence_diagram: &'a SequenceDiagramIndex, + ) -> Self { + Self { + design_classes, + sequence_diagram, + result: ValidationResult::default(), + } + } + + fn run(mut self) -> ValidationResult { + append_debug_log( + &mut self.result.diagnostics, + self.design_classes, + self.sequence_diagram, + ); + self.check_participant_class_consistency(); + self.check_message_operation_consistency(); + self.result + } + + fn check_participant_class_consistency(&mut self) { + for (participant, participant_info) in self.sequence_diagram.participants() { + let (source_file, source_line) = participant_info.source_location.display(); + + match self.resolve_participant_class(participant) { + ParticipantResolution::Matched(_) => {} + ParticipantResolution::Missing => { + self.result.add_failure( + ErrorBuilder::new(ErrorCategory::Class) + .title(format!( + "sequence participant \"{participant}\" has no matching class in the class diagram" + )) + .field("participant", format!("\"{participant}\"")) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "add class \"{participant}\" to the class diagram, or remove the participant from the sequence diagram" + )) + .build(), + ); + } + ParticipantResolution::Ambiguous(matches) => { + let matching_classes = format_name_set(&matches); + self.result.add_failure( + ErrorBuilder::new(ErrorCategory::Class) + .title(format!( + "sequence participant \"{participant}\" matches multiple classes in the class diagram" + )) + .field("participant", format!("\"{participant}\"")) + .field("matching classes", matching_classes.clone()) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "rename participant \"{participant}\" in the sequence diagram to a unique class id, or rename one of the matching classes in the class diagram" + )) + .build(), + ); + } + } + } + } + + fn check_message_operation_consistency(&mut self) { + for observed_call in self.sequence_diagram.observed_calls() { + let ParticipantResolution::Matched(callee_class) = + self.resolve_participant_class(&observed_call.callee) + else { + continue; + }; + + let method_name = extract_method_name(&observed_call.method); + if method_name.is_empty() { + continue; + } + + if callee_class.methods.iter().any(|method| method.name == method_name) { + continue; + } + + let sequence_call = + format_sequence_call(&observed_call.caller, &observed_call.callee, method_name); + let (source_file, source_line) = observed_call.source_location.display(); + + self.result.add_failure( + ErrorBuilder::new(ErrorCategory::Method) + .title(format!( + "sequence function \"{method_name}\" from sequence call {sequence_call} not found on target class \"{}\" in the class diagram", + callee_class.id, + )) + .field("sequence call", sequence_call) + .field("target class", format!("\"{}\"", callee_class.id)) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "add method \"{method_name}\" to class \"{}\" in the class diagram, or change or remove that sequence call", + callee_class.id, + )) + .build(), + ); + } + } + + fn resolve_participant_class(&self, participant: &str) -> ParticipantResolution<'a> { + if let Some(entity) = self.resolve_by_class_id(participant) { + return ParticipantResolution::Matched(entity); + } + + if let Some(participant_info) = self.sequence_diagram.participant_info(participant) { + let display_name = participant_info.display_name.as_str(); + + if display_name != participant { + if let Some(entity) = self.resolve_by_class_id(display_name) { + return ParticipantResolution::Matched(entity); + } + + match self.resolve_by_class_name(display_name) { + ParticipantResolution::Missing => {} + matched_or_ambiguous => return matched_or_ambiguous, + } + } + } + + self.resolve_by_class_name(participant) + } + + fn resolve_by_class_id(&self, reference: &str) -> Option<&'a class_diagram::SimpleEntity> { + if let Some(entity) = self.design_classes.find_by_id(reference) { + return Some(entity); + } + + let normalized_reference = SequenceParticipantInfo::normalize_qualified_name(reference); + if normalized_reference == reference { + return None; + } + + self.design_classes.find_by_id(&normalized_reference) + } + + fn resolve_by_class_name(&self, class_name: &str) -> ParticipantResolution<'a> { + let short_name_matches: Vec<_> = self + .design_classes + .entities() + .filter(|entity| entity.name == class_name) + .collect(); + + match short_name_matches.as_slice() { + [] => ParticipantResolution::Missing, + [entity] => ParticipantResolution::Matched(entity), + entities => ParticipantResolution::Ambiguous( + entities.iter().map(|entity| entity.id.clone()).collect(), + ), + } + } +} + +enum ParticipantResolution<'a> { + Matched(&'a class_diagram::SimpleEntity), + Missing, + Ambiguous(BTreeSet), +} + +fn format_name_set(names: &BTreeSet) -> String { + names + .iter() + .map(|name| format!("\"{name}\"")) + .collect::>() + .join(", ") +} + +fn append_debug_log( + diagnostics: &mut Diagnostics, + design_classes: &ClassEntityIndex, + sequence_diagram: &SequenceDiagramIndex, +) { + diagnostics.debug(|| "Design classes available for sequence validation:".to_string()); + for entity in design_classes.entities() { + diagnostics.debug(|| format!(" {}", entity.id)); + } + + diagnostics.debug(|| "Observed participants from sequence diagrams:".to_string()); + for participant in sequence_diagram.declared_participants() { + diagnostics.debug(|| format!(" {participant}")); + } + + diagnostics.debug(|| "Observed sequence calls from sequence diagrams:".to_string()); + for observed_call in sequence_diagram.observed_calls() { + diagnostics.debug(|| { + format!( + " {} -> {} : {}", + observed_call.caller, observed_call.callee, observed_call.method + ) + }); + } +} + +#[cfg(test)] +#[path = "test/class_design_sequence_validator_test.rs"] +mod tests; diff --git a/validation/core/src/validators/component_sequence_validator.rs b/validation/core/src/validators/component_sequence_validator.rs index a5cc8a37..1c2c66af 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -16,13 +16,14 @@ use std::collections::{BTreeMap, BTreeSet}; -use sequence_logic::SourceLocation; - use super::shared::{ best_string_suggestion, build_observed_call_contexts, build_unit_bindings, format_name_list, intersect_interfaces, SequenceCallContext, UnitBindings, }; -use crate::models::{is_external_endpoint, ComponentDiagramArchitecture, SequenceDiagramIndex}; +use crate::models::{ + is_external_endpoint, ComponentDiagramArchitecture, SequenceDiagramIndex, + SequenceParticipantInfo, +}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -37,7 +38,7 @@ pub fn validate_component_sequence( type ConnectedUnitPairs = BTreeMap<(String, String), BTreeSet>; struct ComponentSequenceValidator<'a> { - participants: &'a BTreeMap, + participants: &'a BTreeMap, observed_call_contexts: Vec>, connected_unit_pairs: ConnectedUnitPairs, unit_bindings: UnitBindings, @@ -158,7 +159,8 @@ impl<'a> ComponentSequenceValidator<'a> { for participant in self.participants.keys().filter(|participant| { !is_external_endpoint(participant) && !self.unit_bindings.contains_key(*participant) }) { - let (source_file, source_line) = self.participants[participant].display(); + let (source_file, source_line) = + self.participants[participant].source_location.display(); let error = ErrorBuilder::new(ErrorCategory::Naming) .title(format!( diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 190482fc..ba23d7ad 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -15,6 +15,7 @@ mod bazel_component_validator; mod class_design_implementation_validator; +mod class_design_sequence_validator; mod component_internal_api_validator; mod component_public_api_validator; mod component_sequence_validator; @@ -27,6 +28,7 @@ pub(crate) mod fixtures; pub use bazel_component_validator::validate_bazel_component; pub use class_design_implementation_validator::validate_class_design_implementation; +pub use class_design_sequence_validator::validate_class_design_sequence; pub use component_internal_api_validator::validate_component_internal_api; pub use component_public_api_validator::validate_component_public_api; pub use component_sequence_validator::validate_component_sequence; diff --git a/validation/core/src/validators/test/class_design_sequence_validator_test.rs b/validation/core/src/validators/test/class_design_sequence_validator_test.rs new file mode 100644 index 00000000..dbe8ece8 --- /dev/null +++ b/validation/core/src/validators/test/class_design_sequence_validator_test.rs @@ -0,0 +1,155 @@ +// ******************************************************************************* +// 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 super::super::fixtures::*; +use super::*; +use crate::models::{ClassDiagramInputs, ClassEntityIndex, SequenceDiagramInputs}; +use crate::ValidationResult; +use class_diagram::ClassDiagram; + +fn validate( + design_classes: ClassDiagramInputs, + sequence_diagrams: SequenceDiagramInputs, +) -> ValidationResult { + let mut setup_result = ValidationResult::default(); + let design_index = ClassEntityIndex::build_index(&design_classes, &mut setup_result); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); + assert!( + setup_result.is_empty(), + "test fixture setup failed: {:?}", + setup_result.failures + ); + + validate_class_design_sequence(&design_index, &sequence_index) +} + +fn class_diagrams(entities: Vec) -> ClassDiagramInputs { + vec![ClassDiagram { + name: "class_design".to_string(), + entities, + }] +} + +fn class_entity(id: &str, namespace: Option<&str>) -> class_diagram::SimpleEntity { + let mut entity = class_interface(id, namespace); + entity.entity_type = class_diagram::EntityType::Class; + entity +} + +#[test] +fn passes_when_all_sequence_participants_match_design_classes() { + let design_classes = class_diagrams(vec![ + class_entity("Controller", None), + class_entity("Repository", None), + ]); + let sequence_diagrams = sequence_diagrams(&["Controller", "Repository"]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_sequence_participant_missing_from_design_classes() { + let design_classes = class_diagrams(vec![class_entity("Controller", None)]); + let sequence_diagrams = sequence_calls(&[("Controller", "Repository", "FindById()")]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains( + "[Class] Sequence participant \"Repository\" has no matching class in the class diagram." + )); + assert!(validation_result.failures[0].contains("\"Repository\"")); +} + +#[test] +fn matches_sequence_participant_against_fully_qualified_class_id() { + let design_classes = class_diagrams(vec![class_entity("Controller", Some("unit_1"))]); + let sequence_diagrams = sequence_diagrams(&["unit_1::Controller"]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn matches_sequence_participant_against_unique_short_name() { + let design_classes = class_diagrams(vec![class_entity("Controller", Some("unit_1"))]); + let sequence_diagrams = sequence_diagrams(&["Controller"]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_sequence_participant_with_ambiguous_short_name() { + let design_classes = class_diagrams(vec![ + class_entity("Controller", Some("unit_1")), + class_entity("Controller", Some("unit_2")), + ]); + let sequence_diagrams = sequence_diagrams(&["Controller"]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains( + "[Class] Sequence participant \"Controller\" matches multiple classes in the class diagram." + )); + assert!(validation_result.failures[0].contains("\"unit_1.Controller\"")); + assert!(validation_result.failures[0].contains("\"unit_2.Controller\"")); +} + +#[test] +fn passes_when_sequence_call_targets_existing_method_on_callee_class() { + let mut repository = class_entity("Repository", None); + repository.methods = vec![method("FindById")]; + + let design_classes = class_diagrams(vec![class_entity("Controller", None), repository]); + let sequence_diagrams = sequence_calls(&[("Controller", "Repository", "FindById()")]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_sequence_call_method_missing_from_callee_class() { + let mut repository = class_entity("Repository", None); + repository.methods = vec![method("Store")]; + + let design_classes = class_diagrams(vec![class_entity("Controller", None), repository]); + let sequence_diagrams = sequence_calls(&[("Controller", "Repository", "FindById()")]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains( + "[Method] Sequence function \"FindById\" from sequence call \"Controller\" -> \"Repository\" : \"FindById\" not found on target class \"Repository\" in the class diagram." + )); + assert!(validation_result.failures[0].contains("\"Repository\"")); +} + +#[test] +fn passes_when_sequence_self_call_targets_existing_method() { + let mut controller = class_entity("Controller", None); + controller.methods = vec![method("Validate")]; + + let design_classes = class_diagrams(vec![controller]); + let sequence_diagrams = sequence_calls(&[("Controller", "Controller", "Validate()")]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert!(validation_result.failures.is_empty()); +} diff --git a/validation/core/src/validators/test/fixtures.rs b/validation/core/src/validators/test/fixtures.rs index d7ef5145..15f35966 100644 --- a/validation/core/src/validators/test/fixtures.rs +++ b/validation/core/src/validators/test/fixtures.rs @@ -213,7 +213,7 @@ fn simple_entity(name: &str, entity_type: EntityType, namespace: Option<&str>) - } } -fn method(name: &str) -> Method { +pub(super) fn method(name: &str) -> Method { Method { name: name.to_string(), return_type: None, From 5a8ee0f3f9999100e0c7748f7b1fb1066ed13e58 Mon Sep 17 00:00:00 2001 From: sheng Date: Fri, 14 Aug 2026 17:51:58 +0800 Subject: [PATCH 2/3] Support inherited class-design-sequence method lookup --- .../specifications/class_design_sequence.md | 20 +- .../class_design_sequence/BUILD | 5 +- .../class_design_sequence_suite.rs | 25 ++- .../expected.yaml | 4 +- .../BUILD | 37 ++++ .../class_diagram.puml | 26 +++ .../expected.yaml | 20 ++ .../sequence_diagram.puml | 20 ++ .../BUILD | 37 ++++ .../class_diagram.puml | 26 +++ .../expected.yaml | 13 ++ .../sequence_diagram.puml | 20 ++ .../BUILD | 37 ++++ .../class_diagram.puml | 31 +++ .../expected.yaml | 13 ++ .../sequence_diagram.puml | 20 ++ .../class_design_sequence_validator.rs | 200 +++++++++++++++--- .../class_design_sequence_validator_test.rs | 2 +- 18 files changed, 508 insertions(+), 48 deletions(-) create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/sequence_diagram.puml diff --git a/validation/core/docs/specifications/class_design_sequence.md b/validation/core/docs/specifications/class_design_sequence.md index 8d501b76..5c73d947 100644 --- a/validation/core/docs/specifications/class_design_sequence.md +++ b/validation/core/docs/specifications/class_design_sequence.md @@ -74,6 +74,21 @@ resolve either on the target class itself or on inherited operations available through its base classes or interfaces. The sequence may only invoke behavior that the class design actually declares or inherits. +Operation lookup shall follow these rules: + +1. Check the target class itself for a method with the requested name. +2. If not found locally, traverse outgoing `Inheritance` and `Implementation` + relations recursively. +3. Track visited class ids while traversing to avoid infinite recursion caused + by cycles in the resolved relationship graph. +4. Treat inherited `private` methods as not accessible to the target class. +5. Treat inherited non-`private` methods as valid matches. + +As a result, a sequence call is valid when the target class declares the method +itself or inherits an accessible method from a base class or implemented +interface. A method that exists only as a private inherited member shall not be +accepted as a valid target operation. + ```text ' class diagram class Repository { @@ -104,8 +119,9 @@ Controller -> Controller : Validate() |---|---| | Sequence participant has no matching design class | Participant-Class Consistency | | Sequence participant matches multiple design classes ambiguously | Participant-Class Consistency | -| Sequence message targets a class that does not declare the called operation | Message-Operation Consistency | -| Sequence self-call targets a class that does not declare the called operation | Message-Operation Consistency | +| Sequence message targets a class that does not declare or accessibly inherit the called operation | Message-Operation Consistency | +| Sequence self-call targets a class that does not declare or accessibly inherit the called operation | Message-Operation Consistency | +| Sequence message targets a method that exists only as a private inherited operation | Message-Operation Consistency | ## Debug Output diff --git a/validation/core/integration_test/class_design_sequence/BUILD b/validation/core/integration_test/class_design_sequence/BUILD index 26a1bff8..eff5efb5 100644 --- a/validation/core/integration_test/class_design_sequence/BUILD +++ b/validation/core/integration_test/class_design_sequence/BUILD @@ -16,11 +16,14 @@ load("@rules_rust//rust:defs.bzl", "rust_test") filegroup( name = "class_design_sequence_test_data", srcs = [ - "//validation/core/integration_test/class_design_sequence/negative_participant_method_missing:case_data", "//validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_method_missing:case_data", "//validation/core/integration_test/class_design_sequence/negative_participant_missing:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match:case_data", ], diff --git a/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs index 27cdf97c..e94d5688 100644 --- a/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs +++ b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs @@ -1,7 +1,4 @@ -// *******************************************************************************[test] -fn positive_participant_short_name_unique_match_suite_case() { - assert_case("positive_participant_short_name_unique_match"); -} +// ******************************************************************************* // Copyright (c) 2026 Contributors to the Eclipse Foundation // // See the NOTICE file(s) distributed with this work for additional @@ -54,6 +51,16 @@ fn assert_case(case_dir: &str) { assert_cli_result(case_dir, &expected, &result); } +#[test] +fn positive_participant_abstract_base_method_match_suite_case() { + assert_case("positive_participant_abstract_base_method_match"); +} + +#[test] +fn positive_participant_multilevel_inherited_method_match_suite_case() { + assert_case("positive_participant_multilevel_inherited_method_match"); +} + #[test] fn positive_participant_alias_display_name_class_name_match_suite_case() { assert_case("positive_participant_alias_display_name_class_name_match"); @@ -74,11 +81,6 @@ fn positive_participant_short_name_namespace_match_suite_case() { assert_case("positive_participant_short_name_namespace_match"); } -#[test] -fn positive_participant_short_name_unique_match_suite_case() { - assert_case("positive_participant_short_name_unique_match"); -} - #[test] fn negative_participant_missing_suite_case() { assert_case("negative_participant_missing"); @@ -93,3 +95,8 @@ fn negative_participant_ambiguous_short_name_suite_case() { fn negative_participant_method_missing_suite_case() { assert_case("negative_participant_method_missing"); } + +#[test] +fn negative_participant_private_inherited_method_suite_case() { + assert_case("negative_participant_private_inherited_method"); +} diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml index ffbbb1f9..850bc08d 100644 --- a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing/expected.yaml @@ -12,9 +12,9 @@ # ******************************************************************************* should_pass: false error_contains: | - [Method] Sequence function "FindById" from sequence call "Repository" -> "Repository" : "FindById" not found on target class "Repository" in the class diagram. + [Method] Sequence function "FindById" from sequence call "Repository" -> "Repository" : "FindById" not found on target class "Repository" or its accessible inherited types in the class diagram. Sequence call : "Repository" -> "Repository" : "FindById" Target class : "Repository" Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_method_missing/sequence_diagram.puml" Sequence source line : 18 - Fix : Add method "FindById" to class "Repository" in the class diagram, or change or remove that sequence call. + Fix : Add method "FindById" to class "Repository" or one of its accessible inherited types in the class diagram, or change or remove that sequence call. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/class_diagram.puml new file mode 100644 index 00000000..0211710b --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/class_diagram.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 class_diagram + +class BaseStore { + - LoadState() : void +} + +class StateStore { + + SaveState() : void +} + +BaseStore <|-- StateStore + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/expected.yaml new file mode 100644 index 00000000..f7861b4a --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Method] Sequence function "LoadState" from sequence call "StateStore" -> "StateStore" : "LoadState" exists only as a private inherited method on target class "StateStore" in the class diagram. + Sequence call : "StateStore" -> "StateStore" : "LoadState" + Target class : "StateStore" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/sequence_diagram.puml" + Sequence source line : 18 + Fix : Consider changing method "LoadState" to public or protected on an inherited type of class "StateStore", add an accessible wrapper on that class, or change or remove that sequence call. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/sequence_diagram.puml new file mode 100644 index 00000000..d8ec8abc --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method/sequence_diagram.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 sequence_diagram + +participant StateStore + +StateStore -> StateStore : LoadState() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/class_diagram.puml new file mode 100644 index 00000000..32c0f10f --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/class_diagram.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 class_diagram + +abstract class ReadCapabilityBase { + + LoadState() : void +} + +class StateStore { + + SaveState() : void +} + +ReadCapabilityBase <|-- StateStore + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/sequence_diagram.puml new file mode 100644 index 00000000..d8ec8abc --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match/sequence_diagram.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 sequence_diagram + +participant StateStore + +StateStore -> StateStore : LoadState() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/class_diagram.puml new file mode 100644 index 00000000..3d2fb535 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/class_diagram.puml @@ -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 +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml class_diagram + +class RootStore { + + LoadState() : void +} + +class IntermediateStore { + + RefreshCache() : void +} + +class StateStore { + + SaveState() : void +} + +RootStore <|-- IntermediateStore +IntermediateStore <|-- StateStore + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/sequence_diagram.puml new file mode 100644 index 00000000..d8ec8abc --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match/sequence_diagram.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 sequence_diagram + +participant StateStore + +StateStore -> StateStore : LoadState() + +@enduml diff --git a/validation/core/src/validators/class_design_sequence_validator.rs b/validation/core/src/validators/class_design_sequence_validator.rs index f36fcf54..33b80dfd 100644 --- a/validation/core/src/validators/class_design_sequence_validator.rs +++ b/validation/core/src/validators/class_design_sequence_validator.rs @@ -16,8 +16,11 @@ use std::collections::BTreeSet; use super::shared::{extract_method_name, format_sequence_call}; -use crate::models::{ClassEntityIndex, SequenceDiagramIndex, SequenceParticipantInfo}; +use crate::models::{ + ClassEntityIndex, ObservedSequenceCall, SequenceDiagramIndex, SequenceParticipantInfo, +}; use crate::{Diagnostics, ErrorBuilder, ErrorCategory, ValidationResult}; +use class_diagram::{RelationType, Visibility}; /// Run class-design-vs-sequence validation. pub fn validate_class_design_sequence( @@ -33,6 +36,13 @@ struct ClassDesignSequenceValidator<'a> { result: ValidationResult, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MethodLookupResult { + FoundAccessible, + FoundPrivateInherited, + NotFound, +} + impl<'a> ClassDesignSequenceValidator<'a> { fn new( design_classes: &'a ClassEntityIndex, @@ -100,41 +110,83 @@ impl<'a> ClassDesignSequenceValidator<'a> { fn check_message_operation_consistency(&mut self) { for observed_call in self.sequence_diagram.observed_calls() { - let ParticipantResolution::Matched(callee_class) = - self.resolve_participant_class(&observed_call.callee) - else { - continue; - }; + self.validate_observed_call(observed_call); + } + } - let method_name = extract_method_name(&observed_call.method); - if method_name.is_empty() { - continue; - } + fn validate_observed_call(&mut self, observed_call: &ObservedSequenceCall) { + let ParticipantResolution::Matched(callee_class) = + self.resolve_participant_class(&observed_call.callee) + else { + return; + }; - if callee_class.methods.iter().any(|method| method.name == method_name) { - continue; - } + let method_name = extract_method_name(&observed_call.method); + if method_name.is_empty() { + return; + } - let sequence_call = - format_sequence_call(&observed_call.caller, &observed_call.callee, method_name); - let (source_file, source_line) = observed_call.source_location.display(); - - self.result.add_failure( - ErrorBuilder::new(ErrorCategory::Method) - .title(format!( - "sequence function \"{method_name}\" from sequence call {sequence_call} not found on target class \"{}\" in the class diagram", - callee_class.id, - )) - .field("sequence call", sequence_call) - .field("target class", format!("\"{}\"", callee_class.id)) - .field("sequence source file", format!("\"{source_file}\"")) - .field("sequence source line", source_line.to_string()) - .fix(format!( - "add method \"{method_name}\" to class \"{}\" in the class diagram, or change or remove that sequence call", - callee_class.id, - )) - .build(), - ); + let method_lookup = self.class_or_ancestors_define_method( + callee_class, + method_name, + false, + &mut BTreeSet::new(), + ); + if method_lookup == MethodLookupResult::FoundAccessible { + return; + } + + self.result.add_failure(self.method_lookup_failure( + observed_call, + callee_class, + method_name, + method_lookup, + )); + } + + fn method_lookup_failure( + &self, + observed_call: &ObservedSequenceCall, + callee_class: &'a class_diagram::SimpleEntity, + method_name: &str, + method_lookup: MethodLookupResult, + ) -> String { + let sequence_call = + format_sequence_call(&observed_call.caller, &observed_call.callee, method_name); + let (source_file, source_line) = observed_call.source_location.display(); + + match method_lookup { + MethodLookupResult::FoundAccessible => { + unreachable!("accessible methods should return early") + } + MethodLookupResult::FoundPrivateInherited => ErrorBuilder::new(ErrorCategory::Method) + .title(format!( + "sequence function \"{method_name}\" from sequence call {sequence_call} exists only as a private inherited method on target class \"{}\" in the class diagram", + callee_class.id, + )) + .field("sequence call", sequence_call) + .field("target class", format!("\"{}\"", callee_class.id)) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "consider changing method \"{method_name}\" to public or protected on an inherited type of class \"{}\", add an accessible wrapper on that class, or change or remove that sequence call", + callee_class.id, + )) + .build(), + MethodLookupResult::NotFound => ErrorBuilder::new(ErrorCategory::Method) + .title(format!( + "sequence function \"{method_name}\" from sequence call {sequence_call} not found on target class \"{}\" or its accessible inherited types in the class diagram", + callee_class.id, + )) + .field("sequence call", sequence_call) + .field("target class", format!("\"{}\"", callee_class.id)) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "add method \"{method_name}\" to class \"{}\" or one of its accessible inherited types in the class diagram, or change or remove that sequence call", + callee_class.id, + )) + .build(), } } @@ -189,6 +241,88 @@ impl<'a> ClassDesignSequenceValidator<'a> { ), } } + + fn class_or_ancestors_define_method( + &self, + entity: &'a class_diagram::SimpleEntity, + method_name: &str, + inherited: bool, + visited_ids: &mut BTreeSet, + ) -> MethodLookupResult { + let local_result = Self::method_lookup_on_entity(entity, method_name, inherited); + if local_result != MethodLookupResult::NotFound { + return local_result; + } + + if !visited_ids.insert(entity.id.clone()) { + return MethodLookupResult::NotFound; + } + + self.related_parent_or_interface_defines_method(entity, method_name, visited_ids) + } + + fn method_lookup_on_entity( + entity: &'a class_diagram::SimpleEntity, + method_name: &str, + inherited: bool, + ) -> MethodLookupResult { + let mut found_private_inherited = false; + + for method in &entity.methods { + if method.name != method_name { + continue; + } + + if inherited && matches!(method.visibility, Visibility::Private) { + found_private_inherited = true; + continue; + } + + return MethodLookupResult::FoundAccessible; + } + + if found_private_inherited { + MethodLookupResult::FoundPrivateInherited + } else { + MethodLookupResult::NotFound + } + } + + fn related_parent_or_interface_defines_method( + &self, + entity: &'a class_diagram::SimpleEntity, + method_name: &str, + visited_ids: &mut BTreeSet, + ) -> MethodLookupResult { + let mut found_private_inherited = false; + + for relationship in &entity.relationships { + if relationship.source != entity.id + || !matches!( + relationship.relation_type, + RelationType::Inheritance | RelationType::Implementation + ) + { + continue; + } + + let Some(parent) = self.design_classes.find_by_id(&relationship.target) else { + continue; + }; + + match self.class_or_ancestors_define_method(parent, method_name, true, visited_ids) { + MethodLookupResult::FoundAccessible => return MethodLookupResult::FoundAccessible, + MethodLookupResult::FoundPrivateInherited => found_private_inherited = true, + MethodLookupResult::NotFound => {} + } + } + + if found_private_inherited { + MethodLookupResult::FoundPrivateInherited + } else { + MethodLookupResult::NotFound + } + } } enum ParticipantResolution<'a> { diff --git a/validation/core/src/validators/test/class_design_sequence_validator_test.rs b/validation/core/src/validators/test/class_design_sequence_validator_test.rs index dbe8ece8..b15b5971 100644 --- a/validation/core/src/validators/test/class_design_sequence_validator_test.rs +++ b/validation/core/src/validators/test/class_design_sequence_validator_test.rs @@ -136,7 +136,7 @@ fn reports_sequence_call_method_missing_from_callee_class() { assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains( - "[Method] Sequence function \"FindById\" from sequence call \"Controller\" -> \"Repository\" : \"FindById\" not found on target class \"Repository\" in the class diagram." + "[Method] Sequence function \"FindById\" from sequence call \"Controller\" -> \"Repository\" : \"FindById\" not found on target class \"Repository\" or its accessible inherited types in the class diagram." )); assert!(validation_result.failures[0].contains("\"Repository\"")); } From 8dd189f743e364683b010bdda5845c109a9b2d1b Mon Sep 17 00:00:00 2001 From: sheng Date: Wed, 19 Aug 2026 18:01:55 +0800 Subject: [PATCH 3/3] Support special sequence participant display matching --- .../specifications/class_design_sequence.md | 30 + .../class_design_sequence/BUILD | 8 + .../class_design_sequence_suite.rs | 40 ++ .../BUILD | 37 ++ .../class_diagram.puml | 26 + .../expected.yaml | 21 + .../sequence_diagram.puml | 20 + .../BUILD | 37 ++ .../class_diagram.puml | 20 + .../expected.yaml | 20 + .../sequence_diagram.puml | 20 + .../BUILD | 37 ++ .../class_diagram.puml | 18 + .../expected.yaml | 21 + .../sequence_diagram.puml | 18 + .../BUILD | 37 ++ .../class_diagram.puml | 26 + .../expected.yaml | 21 + .../sequence_diagram.puml | 18 + .../class_diagram.puml | 2 +- .../sequence_diagram.puml | 2 +- .../BUILD | 37 ++ .../class_diagram.puml | 20 + .../expected.yaml | 13 + .../sequence_diagram.puml | 20 + .../BUILD | 37 ++ .../class_diagram.puml | 18 + .../expected.yaml | 13 + .../sequence_diagram.puml | 18 + .../BUILD | 37 ++ .../class_diagram.puml | 26 + .../expected.yaml | 13 + .../sequence_diagram.puml | 18 + .../BUILD | 37 ++ .../class_diagram.puml | 18 + .../expected.yaml | 13 + .../sequence_diagram.puml | 18 + .../class_design_sequence_validator.rs | 536 ++++++++++++++++-- .../class_design_sequence_validator_test.rs | 74 ++- 39 files changed, 1390 insertions(+), 55 deletions(-) create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/sequence_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/BUILD create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/class_diagram.puml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/expected.yaml create mode 100644 validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/sequence_diagram.puml diff --git a/validation/core/docs/specifications/class_design_sequence.md b/validation/core/docs/specifications/class_design_sequence.md index 5c73d947..b109744c 100644 --- a/validation/core/docs/specifications/class_design_sequence.md +++ b/validation/core/docs/specifications/class_design_sequence.md @@ -50,6 +50,35 @@ to exactly one class in the design class model. This check validates that the sequence does not reference unknown or ambiguous classes. +Participant resolution shall follow this order: + +1. Match the participant reference itself against a class id. +2. If the participant has a different display name, match that display name + against a class id. +3. If the display name still does not resolve, match the display name against a + unique class short name. +4. If the display name uses one supported special form, derive additional class + candidates from that form. +5. If none of the above resolves uniquely, fall back to matching the + participant reference against a unique class short name. + +The supported special display forms are: + +- `:Name`, which contributes `Name` as a short-name candidate. +- `prefix:qualified::Type`, which contributes `qualified::Type` as an id + candidate and both `qualified::Type` and `Type` as short-name candidates. + +Only the first non-empty display line participates in class matching. If the +display name contains additional non-empty lines or escaped line fragments +after the primary line, they shall be ignored for matching and may be reported +through debug or warning output. + +The following participant display forms are invalid and shall be rejected as +participant-class failures: + +- a primary display line containing more than one standalone `:` separator +- a primary display line containing `:` without a non-empty right-hand side + ```text ' class diagram class Controller @@ -119,6 +148,7 @@ Controller -> Controller : Validate() |---|---| | Sequence participant has no matching design class | Participant-Class Consistency | | Sequence participant matches multiple design classes ambiguously | Participant-Class Consistency | +| Sequence participant uses a disallowed special display form | Participant-Class Consistency | | Sequence message targets a class that does not declare or accessibly inherit the called operation | Message-Operation Consistency | | Sequence self-call targets a class that does not declare or accessibly inherit the called operation | Message-Operation Consistency | | Sequence message targets a method that exists only as a private inherited operation | Message-Operation Consistency | diff --git a/validation/core/integration_test/class_design_sequence/BUILD b/validation/core/integration_test/class_design_sequence/BUILD index eff5efb5..9ed70d42 100644 --- a/validation/core/integration_test/class_design_sequence/BUILD +++ b/validation/core/integration_test/class_design_sequence/BUILD @@ -18,14 +18,22 @@ filegroup( srcs = [ "//validation/core/integration_test/class_design_sequence/negative_participant_ambiguous_short_name:case_data", "//validation/core/integration_test/class_design_sequence/negative_participant_method_missing:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion:case_data", "//validation/core/integration_test/class_design_sequence/negative_participant_missing:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion:case_data", "//validation/core/integration_test/class_design_sequence/negative_participant_private_inherited_method:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix:case_data", + "//validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_abstract_base_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_class_name_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_alias_display_name_namespace_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_multilevel_inherited_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match:case_data", "//validation/core/integration_test/class_design_sequence/positive_participant_short_name_namespace_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match:case_data", + "//validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match:case_data", ], ) diff --git a/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs index e94d5688..774cdcef 100644 --- a/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs +++ b/validation/core/integration_test/class_design_sequence/class_design_sequence_suite.rs @@ -81,6 +81,26 @@ fn positive_participant_short_name_namespace_match_suite_case() { assert_case("positive_participant_short_name_namespace_match"); } +#[test] +fn positive_participant_special_display_leading_colon_short_name_match_suite_case() { + assert_case("positive_participant_special_display_leading_colon_short_name_match"); +} + +#[test] +fn positive_participant_special_display_qualified_type_match_suite_case() { + assert_case("positive_participant_special_display_qualified_type_match"); +} + +#[test] +fn positive_participant_special_display_short_type_match_suite_case() { + assert_case("positive_participant_special_display_short_type_match"); +} + +#[test] +fn positive_participant_special_display_encoded_newline_match_suite_case() { + assert_case("positive_participant_special_display_encoded_newline_match"); +} + #[test] fn negative_participant_missing_suite_case() { assert_case("negative_participant_missing"); @@ -96,7 +116,27 @@ fn negative_participant_method_missing_suite_case() { assert_case("negative_participant_method_missing"); } +#[test] +fn negative_participant_method_missing_with_suggestion_suite_case() { + assert_case("negative_participant_method_missing_with_suggestion"); +} + #[test] fn negative_participant_private_inherited_method_suite_case() { assert_case("negative_participant_private_inherited_method"); } + +#[test] +fn negative_participant_missing_with_suggestion_suite_case() { + assert_case("negative_participant_missing_with_suggestion"); +} + +#[test] +fn negative_participant_special_display_multiple_colons_suite_case() { + assert_case("negative_participant_special_display_multiple_colons"); +} + +#[test] +fn negative_participant_special_display_empty_suffix_suite_case() { + assert_case("negative_participant_special_display_empty_suffix"); +} diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/class_diagram.puml new file mode 100644 index 00000000..279b84be --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/class_diagram.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 class_diagram + +abstract class RepositoryBase { + + FindById() : void +} + +class Repository { + + Store() : void +} + +RepositoryBase <|-- Repository + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/expected.yaml new file mode 100644 index 00000000..15344dd7 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/expected.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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Method] Sequence function "FindByIds" from sequence call "Repository" -> "Repository" : "FindByIds" not found on target class "Repository" or its accessible inherited types in the class diagram. + Sequence call : "Repository" -> "Repository" : "FindByIds" + Target class : "Repository" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/sequence_diagram.puml" + Sequence source line : 18 + Suggestion for "FindByIds" : Did you mean method "FindById"? + Fix : Add method "FindByIds" to class "Repository" or one of its accessible inherited types in the class diagram, or change or remove that sequence call. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/sequence_diagram.puml new file mode 100644 index 00000000..3041c552 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_method_missing_with_suggestion/sequence_diagram.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 sequence_diagram + +participant Repository + +Repository -> Repository : FindByIds() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/class_diagram.puml new file mode 100644 index 00000000..26889ac6 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/class_diagram.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 class_diagram + +class Repository { + + Execute() : void +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/expected.yaml new file mode 100644 index 00000000..ec9992d4 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Class] Sequence participant "Repositry" has no matching class in the class diagram. + Participant : "Repositry" + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/sequence_diagram.puml" + Sequence source line : 16 + Suggestion for "Repositry" : Did you mean class "Repository"? + Fix : Add class "Repositry" to the class diagram, or remove the participant from the sequence diagram. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/sequence_diagram.puml new file mode 100644 index 00000000..a96996fc --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_missing_with_suggestion/sequence_diagram.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 sequence_diagram + +participant Repositry + +Repositry -> Repositry : Execute() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/class_diagram.puml new file mode 100644 index 00000000..be9fcad7 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/class_diagram.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 class_diagram + +class Runtime + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/expected.yaml new file mode 100644 index 00000000..9185e237 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/expected.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 +# ******************************************************************************* +should_pass: false +error_contains: |- + [Class] Sequence participant "singletonInstance:" uses an invalid kind of display name. + Participant : "singletonInstance:" + Display name : "singletonInstance:" + Invalid form : "singletonInstance:" uses ':' without a non-empty right-hand side + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/sequence_diagram.puml" + Sequence source line : 16 + Fix : Use one supported form such as :Name, prefix:qualified::Type, or provide an unambiguous alias. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/sequence_diagram.puml new file mode 100644 index 00000000..6b00f453 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_empty_suffix/sequence_diagram.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 sequence_diagram + +participant "singletonInstance:" + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/BUILD b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/class_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/class_diagram.puml new file mode 100644 index 00000000..92441dc6 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/class_diagram.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 class_diagram + +namespace score { + namespace mw { + namespace com { + namespace impl { + class Runtime + } + } + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/expected.yaml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/expected.yaml new file mode 100644 index 00000000..a481fcfb --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/expected.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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Class] Sequence participant "instance:score::mw::com::impl::Runtime:Impl" uses an invalid kind of display name. + Participant : "instance:score::mw::com::impl::Runtime:Impl" + Display name : "instance:score::mw::com::impl::Runtime:Impl" + Invalid form : "instance:score::mw::com::impl::Runtime:Impl" contains multiple standalone ':' separators + Sequence source file : "validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/sequence_diagram.puml" + Sequence source line : 16 + Fix : Use one supported form such as :Name, prefix:qualified::Type, or provide an unambiguous alias. diff --git a/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/sequence_diagram.puml new file mode 100644 index 00000000..f2324391 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/negative_participant_special_display_multiple_colons/sequence_diagram.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 sequence_diagram + +participant "instance:score::mw::com::impl::Runtime:Impl" + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml index 15556558..a88b9122 100644 --- a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/class_diagram.puml @@ -24,4 +24,4 @@ namespace unit_1 { } } -@enduml \ No newline at end of file +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml index 02618daa..548269a3 100644 --- a/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml +++ b/validation/core/integration_test/class_design_sequence/positive_participant_namespace_callee_method_match/sequence_diagram.puml @@ -18,4 +18,4 @@ participant "unit_1::StateStore" as StateStore Controller -> StateStore : LoadState() -@enduml \ No newline at end of file +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/class_diagram.puml new file mode 100644 index 00000000..c7846db4 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/class_diagram.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 class_diagram + +class Process { + + Execute() : void +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/sequence_diagram.puml new file mode 100644 index 00000000..d0d83fe1 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_encoded_newline_match/sequence_diagram.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 sequence_diagram + +participant ":Process/nara::com user" as help + +help -> help : Execute() + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/class_diagram.puml new file mode 100644 index 00000000..aada9971 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/class_diagram.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 class_diagram + +class OS + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/sequence_diagram.puml new file mode 100644 index 00000000..4d9c33d9 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_leading_colon_short_name_match/sequence_diagram.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 sequence_diagram + +participant ":OS" + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/class_diagram.puml new file mode 100644 index 00000000..8a607ddf --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/class_diagram.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 class_diagram + +namespace score { + namespace mw { + namespace com { + namespace impl { + class Runtime + } + } + } +} + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/sequence_diagram.puml new file mode 100644 index 00000000..418da8fb --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_qualified_type_match/sequence_diagram.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 sequence_diagram + +participant "singletonInstance:score::mw::com::impl::Runtime" + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/BUILD b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/BUILD new file mode 100644 index 00000000..30f3d121 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +unit_design( + name = "unit_design", + dynamic = ["sequence_diagram.puml"], + static = ["class_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":unit_design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/class_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/class_diagram.puml new file mode 100644 index 00000000..be9fcad7 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/class_diagram.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 class_diagram + +class Runtime + +@enduml diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/expected.yaml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/sequence_diagram.puml b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/sequence_diagram.puml new file mode 100644 index 00000000..1994b331 --- /dev/null +++ b/validation/core/integration_test/class_design_sequence/positive_participant_special_display_short_type_match/sequence_diagram.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 sequence_diagram + +participant "singletonInstance:Runtime" + +@enduml diff --git a/validation/core/src/validators/class_design_sequence_validator.rs b/validation/core/src/validators/class_design_sequence_validator.rs index 33b80dfd..3d63f7dc 100644 --- a/validation/core/src/validators/class_design_sequence_validator.rs +++ b/validation/core/src/validators/class_design_sequence_validator.rs @@ -15,7 +15,7 @@ use std::collections::BTreeSet; -use super::shared::{extract_method_name, format_sequence_call}; +use super::shared::{best_string_suggestion, extract_method_name, format_sequence_call}; use crate::models::{ ClassEntityIndex, ObservedSequenceCall, SequenceDiagramIndex, SequenceParticipantInfo, }; @@ -68,44 +68,122 @@ impl<'a> ClassDesignSequenceValidator<'a> { fn check_participant_class_consistency(&mut self) { for (participant, participant_info) in self.sequence_diagram.participants() { - let (source_file, source_line) = participant_info.source_location.display(); - - match self.resolve_participant_class(participant) { - ParticipantResolution::Matched(_) => {} - ParticipantResolution::Missing => { - self.result.add_failure( - ErrorBuilder::new(ErrorCategory::Class) - .title(format!( - "sequence participant \"{participant}\" has no matching class in the class diagram" - )) - .field("participant", format!("\"{participant}\"")) - .field("sequence source file", format!("\"{source_file}\"")) - .field("sequence source line", source_line.to_string()) - .fix(format!( - "add class \"{participant}\" to the class diagram, or remove the participant from the sequence diagram" - )) - .build(), - ); - } - ParticipantResolution::Ambiguous(matches) => { - let matching_classes = format_name_set(&matches); - self.result.add_failure( - ErrorBuilder::new(ErrorCategory::Class) - .title(format!( - "sequence participant \"{participant}\" matches multiple classes in the class diagram" - )) - .field("participant", format!("\"{participant}\"")) - .field("matching classes", matching_classes.clone()) - .field("sequence source file", format!("\"{source_file}\"")) - .field("sequence source line", source_line.to_string()) - .fix(format!( - "rename participant \"{participant}\" in the sequence diagram to a unique class id, or rename one of the matching classes in the class diagram" - )) - .build(), - ); - } + self.validate_participant(participant, participant_info); + } + } + + fn validate_participant( + &mut self, + participant: &str, + participant_info: &SequenceParticipantInfo, + ) { + let (source_file, source_line) = participant_info.source_location.display(); + + if let Some(display_issue) = unsupported_participant_display_form(participant_info) { + self.report_unsupported_participant_display_name( + participant, + participant_info, + &source_file, + source_line, + display_issue, + ); + return; + } + + self.log_ignored_special_display_suffix( + participant, + participant_info, + &source_file, + source_line, + ); + + match self.resolve_participant_class(participant) { + ParticipantResolution::Matched(_) => {} + ParticipantResolution::Missing => { + self.result.add_failure(self.missing_participant_failure( + participant, + participant_info, + &source_file, + source_line, + )) } + ParticipantResolution::Ambiguous(matches) => { + self.result.add_failure(self.ambiguous_participant_failure( + participant, + &source_file, + source_line, + &matches, + )) + } + } + } + + fn log_ignored_special_display_suffix( + &self, + participant: &str, + participant_info: &SequenceParticipantInfo, + source_file: &str, + source_line: u32, + ) { + if let Some(ignored_suffix) = ignored_special_display_suffix(participant_info) { + log::warn!( + "sequence participant \"{}\" ignores trailing special display text \"{}\" at {}:{}; it is not treated as namespace or class matching data", + participant, + ignored_suffix, + source_file, + source_line, + ); + } + } + + fn missing_participant_failure( + &self, + participant: &str, + participant_info: &SequenceParticipantInfo, + source_file: &str, + source_line: u32, + ) -> String { + let error = ErrorBuilder::new(ErrorCategory::Class) + .title(format!( + "sequence participant \"{participant}\" has no matching class in the class diagram" + )) + .field("participant", format!("\"{participant}\"")) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "add class \"{participant}\" to the class diagram, or remove the participant from the sequence diagram" + )); + + if let Some(suggested_class) = self + .best_participant_class_suggestion(participant, participant_info) + .as_deref() + { + error.suggest(participant, Some("class"), suggested_class) + } else { + error } + .build() + } + + fn ambiguous_participant_failure( + &self, + participant: &str, + source_file: &str, + source_line: u32, + matches: &BTreeSet, + ) -> String { + ErrorBuilder::new(ErrorCategory::Class) + .title(format!( + "sequence participant \"{participant}\" matches multiple classes in the class diagram" + )) + .field("participant", format!("\"{participant}\"")) + .field("matching classes", format_name_set(matches)) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "rename participant \"{participant}\" in the sequence diagram to a unique class id, or rename one of the matching classes in the class diagram" + )) + .build() } fn check_message_operation_consistency(&mut self) { @@ -173,7 +251,8 @@ impl<'a> ClassDesignSequenceValidator<'a> { callee_class.id, )) .build(), - MethodLookupResult::NotFound => ErrorBuilder::new(ErrorCategory::Method) + MethodLookupResult::NotFound => { + let error = ErrorBuilder::new(ErrorCategory::Method) .title(format!( "sequence function \"{method_name}\" from sequence call {sequence_call} not found on target class \"{}\" or its accessible inherited types in the class diagram", callee_class.id, @@ -185,34 +264,152 @@ impl<'a> ClassDesignSequenceValidator<'a> { .fix(format!( "add method \"{method_name}\" to class \"{}\" or one of its accessible inherited types in the class diagram, or change or remove that sequence call", callee_class.id, - )) - .build(), + )); + + if let Some(suggested_method) = self + .best_method_suggestion(callee_class, method_name) + .as_deref() + { + error.suggest(method_name, Some("method"), suggested_method) + } else { + error + } + .build() + } } } + fn best_participant_class_suggestion( + &self, + participant: &str, + participant_info: &SequenceParticipantInfo, + ) -> Option { + let class_candidates: BTreeSet = self + .design_classes + .entities() + .flat_map(|entity| [entity.id.clone(), entity.name.clone()]) + .filter(|candidate| !candidate.is_empty()) + .collect(); + + participant_suggestion_queries(participant, participant_info) + .into_iter() + .find_map(|query| { + best_string_suggestion(&query, class_candidates.iter().map(String::as_str)) + }) + } + + fn best_method_suggestion( + &self, + callee_class: &'a class_diagram::SimpleEntity, + method_name: &str, + ) -> Option { + let mut visited_ids = BTreeSet::new(); + let mut method_candidates = BTreeSet::new(); + self.collect_related_method_names(callee_class, &mut visited_ids, &mut method_candidates); + + best_string_suggestion(method_name, method_candidates.iter().map(String::as_str)) + } + fn resolve_participant_class(&self, participant: &str) -> ParticipantResolution<'a> { - if let Some(entity) = self.resolve_by_class_id(participant) { - return ParticipantResolution::Matched(entity); + if let Some(resolution) = self.resolve_class_from_participant(participant) { + return resolution; } if let Some(participant_info) = self.sequence_diagram.participant_info(participant) { - let display_name = participant_info.display_name.as_str(); - - if display_name != participant { - if let Some(entity) = self.resolve_by_class_id(display_name) { - return ParticipantResolution::Matched(entity); - } + if let Some(resolution) = self.resolve_class_from_display_name( + participant, + participant_info.display_name.as_str(), + ) { + return resolution; + } - match self.resolve_by_class_name(display_name) { - ParticipantResolution::Missing => {} - matched_or_ambiguous => return matched_or_ambiguous, - } + if let Some(resolution) = self.resolve_class_from_special_display_form(participant_info) + { + return resolution; } } self.resolve_by_class_name(participant) } + fn resolve_class_from_participant( + &self, + participant: &str, + ) -> Option> { + self.resolve_by_class_id(participant) + .map(ParticipantResolution::Matched) + } + + fn resolve_class_from_display_name( + &self, + participant: &str, + display_name: &str, + ) -> Option> { + if display_name == participant { + return None; + } + + if let Some(entity) = self.resolve_by_class_id(display_name) { + return Some(ParticipantResolution::Matched(entity)); + } + + match self.resolve_by_class_name(display_name) { + ParticipantResolution::Missing => None, + matched_or_ambiguous => Some(matched_or_ambiguous), + } + } + + fn resolve_class_from_special_display_form( + &self, + participant_info: &SequenceParticipantInfo, + ) -> Option> { + let display_candidates = class_match_candidates_from_display(participant_info); + + for id_candidate in display_candidates.id_candidates { + if let Some(entity) = self.resolve_by_class_id(&id_candidate) { + return Some(ParticipantResolution::Matched(entity)); + } + } + + for name_candidate in display_candidates.name_candidates { + match self.resolve_by_class_name(&name_candidate) { + ParticipantResolution::Missing => {} + matched_or_ambiguous => return Some(matched_or_ambiguous), + } + } + + None + } + + fn report_unsupported_participant_display_name( + &mut self, + participant: &str, + participant_info: &SequenceParticipantInfo, + source_file: &str, + source_line: u32, + display_issue: UnsupportedParticipantDisplayForm, + ) { + self.result.add_failure( + ErrorBuilder::new(ErrorCategory::Class) + .title(format!( + "sequence participant \"{participant}\" uses an invalid kind of display name" + )) + .field("participant", format!("\"{participant}\"")) + .field("display name", format!("\"{}\"", participant_info.display_name)) + .field( + "invalid form", + display_issue.invalid_form(&participant_info.display_name), + ) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix( + "use one supported form such as :Name, prefix:qualified::Type, or provide an unambiguous alias" + .to_string(), + ) + .build(), + ); + } + fn resolve_by_class_id(&self, reference: &str) -> Option<&'a class_diagram::SimpleEntity> { if let Some(entity) = self.design_classes.find_by_id(reference) { return Some(entity); @@ -323,6 +520,43 @@ impl<'a> ClassDesignSequenceValidator<'a> { MethodLookupResult::NotFound } } + + fn collect_related_method_names( + &self, + entity: &'a class_diagram::SimpleEntity, + visited_ids: &mut BTreeSet, + method_candidates: &mut BTreeSet, + ) { + if !visited_ids.insert(entity.id.clone()) { + return; + } + + method_candidates.extend( + entity + .methods + .iter() + .map(|method| method.name.as_str()) + .filter(|name| !name.is_empty()) + .map(str::to_string), + ); + + for relationship in &entity.relationships { + if relationship.source != entity.id + || !matches!( + relationship.relation_type, + RelationType::Inheritance | RelationType::Implementation + ) + { + continue; + } + + let Some(parent) = self.design_classes.find_by_id(&relationship.target) else { + continue; + }; + + self.collect_related_method_names(parent, visited_ids, method_candidates); + } + } } enum ParticipantResolution<'a> { @@ -331,6 +565,204 @@ enum ParticipantResolution<'a> { Ambiguous(BTreeSet), } +#[derive(Default)] +struct ClassMatchCandidates { + id_candidates: Vec, + name_candidates: Vec, +} + +#[derive(Clone, Copy)] +enum UnsupportedParticipantDisplayForm { + MultipleStandaloneColons, + EmptyColonSuffix, +} + +impl UnsupportedParticipantDisplayForm { + fn invalid_form(self, display_name: &str) -> String { + match self { + Self::MultipleStandaloneColons => format!( + "\"{}\" contains multiple standalone ':' separators", + display_name + ), + Self::EmptyColonSuffix => { + format!( + "\"{}\" uses ':' without a non-empty right-hand side", + display_name + ) + } + } + } +} + +fn unsupported_participant_display_form( + participant_info: &SequenceParticipantInfo, +) -> Option { + let primary_line = first_nonempty_display_line(&participant_info.display_name)?; + let separator_colons = separator_colon_positions(primary_line); + + if separator_colons.len() > 1 { + return Some(UnsupportedParticipantDisplayForm::MultipleStandaloneColons); + } + + if let Some(colon_index) = separator_colons.first() { + if primary_line[colon_index + 1..].trim().is_empty() { + return Some(UnsupportedParticipantDisplayForm::EmptyColonSuffix); + } + } + + None +} + +fn class_match_candidates_from_display( + participant_info: &SequenceParticipantInfo, +) -> ClassMatchCandidates { + let Some(primary_line) = first_nonempty_display_line(&participant_info.display_name) else { + return ClassMatchCandidates::default(); + }; + + let separator_colons = separator_colon_positions(primary_line); + if separator_colons.len() != 1 { + return ClassMatchCandidates::default(); + } + + let colon_index = separator_colons[0]; + if colon_index == 0 { + let short_name = primary_line[1..].trim(); + if short_name.is_empty() { + return ClassMatchCandidates::default(); + } + + return ClassMatchCandidates { + id_candidates: Vec::new(), + name_candidates: vec![short_name.to_string()], + }; + } + + let Some(type_text) = text_after_separator_colon(primary_line, colon_index) else { + return ClassMatchCandidates::default(); + }; + + class_match_candidates_from_type_text(type_text) +} + +fn participant_suggestion_queries( + participant: &str, + participant_info: &SequenceParticipantInfo, +) -> Vec { + let mut queries = BTreeSet::new(); + + insert_participant_suggestion_query(&mut queries, participant); + + if participant_info.display_name != participant { + insert_participant_suggestion_query(&mut queries, &participant_info.display_name); + } + + let display_candidates = class_match_candidates_from_display(participant_info); + for candidate in display_candidates + .id_candidates + .into_iter() + .chain(display_candidates.name_candidates) + { + insert_participant_suggestion_query(&mut queries, &candidate); + } + + queries.into_iter().collect() +} + +fn insert_participant_suggestion_query(queries: &mut BTreeSet, query: &str) { + if query.is_empty() { + return; + } + + queries.insert(query.to_string()); + + let normalized_query = SequenceParticipantInfo::normalize_qualified_name(query); + if normalized_query != query { + queries.insert(normalized_query); + } +} + +fn text_after_separator_colon(primary_line: &str, colon_index: usize) -> Option<&str> { + let type_text = primary_line[colon_index + 1..].trim(); + (!type_text.is_empty()).then_some(type_text) +} + +fn class_match_candidates_from_type_text(type_text: &str) -> ClassMatchCandidates { + let mut candidates = ClassMatchCandidates { + id_candidates: Vec::new(), + name_candidates: Vec::new(), + }; + + if type_text.contains("::") { + candidates.id_candidates.push(type_text.to_string()); + } + + candidates.name_candidates.push(type_text.to_string()); + + if let Some(short_name) = type_text.rsplit("::").next().map(str::trim) { + if !short_name.is_empty() && short_name != type_text { + candidates.name_candidates.push(short_name.to_string()); + } + } + + candidates +} + +fn ignored_special_display_suffix(participant_info: &SequenceParticipantInfo) -> Option { + let normalized_segments = normalized_display_segments(&participant_info.display_name); + let (primary_line, ignored_segments) = normalized_segments.split_first()?; + let separator_colons = separator_colon_positions(primary_line); + + if separator_colons.len() != 1 || ignored_segments.is_empty() { + return None; + } + + Some(ignored_segments.join(" | ")) +} + +fn first_nonempty_display_line(display_name: &str) -> Option<&str> { + normalized_display_segments(display_name).into_iter().next() +} + +fn normalized_display_segments(display_name: &str) -> Vec<&str> { + let mut lines = Vec::new(); + + for physical_line in display_name.lines() { + for escaped_line in physical_line + .split("\\n") + .flat_map(|segment| segment.split("/n")) + { + let trimmed = escaped_line.trim(); + if !trimmed.is_empty() { + lines.push(trimmed); + } + } + } + + lines +} + +fn separator_colon_positions(text: &str) -> Vec { + const COLON: u8 = b':'; + + let bytes = text.as_bytes(); + + bytes + .iter() + .enumerate() + .filter_map(|(index, byte)| { + if *byte != COLON { + return None; + } + + let previous_is_colon = index > 0 && bytes[index - 1] == COLON; + let next_is_colon = index + 1 < bytes.len() && bytes[index + 1] == COLON; + + (!previous_is_colon && !next_is_colon).then_some(index) + }) + .collect() +} + fn format_name_set(names: &BTreeSet) -> String { names .iter() diff --git a/validation/core/src/validators/test/class_design_sequence_validator_test.rs b/validation/core/src/validators/test/class_design_sequence_validator_test.rs index b15b5971..a85cbfca 100644 --- a/validation/core/src/validators/test/class_design_sequence_validator_test.rs +++ b/validation/core/src/validators/test/class_design_sequence_validator_test.rs @@ -15,7 +15,7 @@ use super::super::fixtures::*; use super::*; use crate::models::{ClassDiagramInputs, ClassEntityIndex, SequenceDiagramInputs}; use crate::ValidationResult; -use class_diagram::ClassDiagram; +use class_diagram::{ClassDiagram, RelationType, Relationship}; fn validate( design_classes: ClassDiagramInputs, @@ -73,6 +73,18 @@ fn reports_sequence_participant_missing_from_design_classes() { assert!(validation_result.failures[0].contains("\"Repository\"")); } +#[test] +fn reports_sequence_participant_missing_with_fuzzy_class_suggestion() { + let design_classes = class_diagrams(vec![class_entity("Repository", None)]); + let sequence_diagrams = sequence_diagrams(&["Repositry"]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("Suggestion for \"Repositry\" : Did you mean class \"Repository\"?")); +} + #[test] fn matches_sequence_participant_against_fully_qualified_class_id() { let design_classes = class_diagrams(vec![class_entity("Controller", Some("unit_1"))]); @@ -141,6 +153,32 @@ fn reports_sequence_call_method_missing_from_callee_class() { assert!(validation_result.failures[0].contains("\"Repository\"")); } +#[test] +fn reports_sequence_call_method_missing_with_inherited_fuzzy_suggestion() { + let mut repository_base = class_entity("RepositoryBase", None); + repository_base.methods = vec![method("FindById")]; + + let mut repository = class_entity("Repository", None); + repository.relationships = vec![relationship( + "Repository", + "RepositoryBase", + RelationType::Inheritance, + )]; + + let design_classes = class_diagrams(vec![ + class_entity("Controller", None), + repository, + repository_base, + ]); + let sequence_diagrams = sequence_calls(&[("Controller", "Repository", "FindByIds()")]); + + let validation_result = validate(design_classes, sequence_diagrams); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("Suggestion for \"FindByIds\" : Did you mean method \"FindById\"?")); +} + #[test] fn passes_when_sequence_self_call_targets_existing_method() { let mut controller = class_entity("Controller", None); @@ -153,3 +191,37 @@ fn passes_when_sequence_self_call_targets_existing_method() { assert!(validation_result.failures.is_empty()); } + +#[test] +fn extracts_ignored_special_display_suffix() { + let mut sequence_diagrams = sequence_diagrams(&["help"]); + sequence_diagrams.diagrams[0].participants[0].display_name = + ":Process/nara::com user".to_string(); + sequence_diagrams.diagrams[0].participants[0].alias = Some("help".to_string()); + + let mut setup_result = ValidationResult::default(); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); + assert!( + setup_result.is_empty(), + "test fixture setup failed: {:?}", + setup_result.failures + ); + + let participant_info = sequence_index.participant_info("help").unwrap(); + + assert_eq!( + ignored_special_display_suffix(participant_info), + Some("ara::com user".to_string()) + ); +} + +fn relationship(source: &str, target: &str, relation_type: RelationType) -> Relationship { + Relationship { + source: source.to_string(), + target: target.to_string(), + relation_type, + source_multiplicity: None, + target_multiplicity: None, + source_location: dummy_source_location(), + } +}