Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions validation/core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
50 changes: 48 additions & 2 deletions validation/core/docs/specifications/class_design_sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -74,6 +103,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 {
Expand Down Expand Up @@ -104,8 +148,10 @@ 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 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 |

## Debug Output

Expand Down
53 changes: 53 additions & 0 deletions validation/core/integration_test/class_design_sequence/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of 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_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",
],
)

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",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// *******************************************************************************
// Copyright (c) 2026 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Apache License Version 2.0 which is available at
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// 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_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");
}

#[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_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");
}

#[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");
}

#[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");
}
Original file line number Diff line number Diff line change
@@ -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__"],
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading