From 7635c60d43fefa3beb969994fa2a2613ecd1fa43 Mon Sep 17 00:00:00 2001 From: sheng Date: Thu, 8 Oct 2026 13:35:56 +0800 Subject: [PATCH 1/2] add architectural unit --- .../rules/rules_score/examples/minimal/BUILD | 2 +- .../examples/minimal/docs/class_design.puml | 14 +- .../examples/minimal/src/my_unit.cpp | 15 +- .../examples/minimal/src/my_unit.h | 10 +- .../examples/minimal/src/my_unit_test.cpp | 12 +- .../unit_1/docs/unit_1_class_diagram.puml | 12 +- .../rules_score/examples/seooc/unit_1/foo.cpp | 5 + .../rules_score/examples/seooc/unit_1/foo.h | 4 + .../examples/seooc/unit_1/foo_test.cpp | 2 + .../rules_score/examples/seooc/unit_2/bar.cpp | 7 +- .../rules_score/examples/seooc/unit_2/bar.h | 8 +- .../examples/seooc/unit_2/bar_test.cpp | 3 + .../unit_2/docs/unit_2_class_diagram.puml | 23 +- .../seooc/unit_2/docs/unit_2_overview.puml | 14 +- .../unit_3/docs/unit_3_class_diagram.puml | 12 +- .../examples/seooc/unit_3/formatter.cpp | 9 +- .../examples/seooc/unit_3/formatter.h | 9 +- .../examples/seooc/unit_3/formatter_test.cpp | 3 + .../examples/some_other_library/BUILD | 8 +- .../some_other_library/abc_class_design.puml | 24 ++ .../private/dependable_element.bzl | 28 +- validation/core/BUILD | 2 + .../docs/requirements/tool_requirements.trlc | 20 ++ .../architectural_unit_class_design.md | 138 ++++++++ .../architectural_unit_class_design/BUILD | 36 +++ .../architectural_unit_class_design_suite.rs | 123 +++++++ .../negative_missing_class/BUILD | 35 ++ .../negative_missing_class/architecture.json | 26 ++ .../component_diagram.puml | 21 ++ .../negative_missing_class/expected.yaml | 26 ++ .../negative_path_prefix/BUILD | 49 +++ .../negative_path_prefix/architecture.json | 26 ++ .../component_diagram.puml | 21 ++ .../negative_path_prefix/expected.yaml | 21 ++ .../unit_1_class_diagram.puml | 22 ++ .../unit_2_class_diagram.puml | 26 ++ .../positive_refining_class/BUILD | 49 +++ .../positive_refining_class/architecture.json | 26 ++ .../component_diagram.puml | 21 ++ .../positive_refining_class/expected.yaml | 13 + .../unit_1_class_diagram.puml | 22 ++ .../unit_2_class_diagram.puml | 28 ++ .../BUILD | 61 ++++ .../architecture.json | 34 ++ .../component_diagram.puml | 23 ++ .../expected.yaml | 13 + .../unit_1_class_diagram.puml | 22 ++ .../unit_2_class_diagram.puml | 22 ++ .../unit_3_class_diagram.puml | 22 ++ .../unit_4_class_diagram.puml | 22 ++ .../core/src/profiles/dependable_element.rs | 104 +++++- ...chitectural_unit_class_design_validator.rs | 300 ++++++++++++++++++ .../class_design_implementation_validator.rs | 66 ++-- validation/core/src/validators/mod.rs | 3 + .../core/src/validators/shared/helpers.rs | 18 ++ validation/core/src/validators/shared/mod.rs | 3 +- 56 files changed, 1600 insertions(+), 88 deletions(-) create mode 100644 bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml create mode 100644 validation/core/docs/specifications/architectural_unit_class_design.md create mode 100644 validation/core/integration_test/architectural_unit_class_design/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml create mode 100644 validation/core/src/validators/architectural_unit_class_design_validator.rs diff --git a/bazel/rules/rules_score/examples/minimal/BUILD b/bazel/rules/rules_score/examples/minimal/BUILD index 72ca078a..42fd89ad 100644 --- a/bazel/rules/rules_score/examples/minimal/BUILD +++ b/bazel/rules/rules_score/examples/minimal/BUILD @@ -87,7 +87,7 @@ unit( component( name = "MyComponent", components = [":MyUnit"], - requirements = [], + requirements = [":feature_requirements"], tests = [], ) diff --git a/bazel/rules/rules_score/examples/minimal/docs/class_design.puml b/bazel/rules/rules_score/examples/minimal/docs/class_design.puml index 96ec0003..9bb9310c 100644 --- a/bazel/rules/rules_score/examples/minimal/docs/class_design.puml +++ b/bazel/rules/rules_score/examples/minimal/docs/class_design.puml @@ -15,10 +15,16 @@ ' Class diagram for MyUnit — internal design of the key-value store unit. -class MyUnit { - - store_ : std::unordered_map - + configure(key : const string&, value : const string&) : void - + get(key : const string&) : string +namespace my_element { + namespace MyComponent { + namespace MyUnit { + class KeyValueStore { + - store_ : std::unordered_map + + configure(key : const string&, value : const string&) : void + + get(key : const string&) : string + } + } + } } @enduml diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp b/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp index 7c11009e..e8f1022a 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp @@ -11,13 +11,22 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "src/my_unit.h" +#include "my_unit.h" -void MyUnit::configure(const std::string &key, const std::string &value) { +namespace my_element { +namespace MyComponent { +namespace MyUnit { + +void KeyValueStore::configure(const std::string &key, + const std::string &value) { store_[key] = value; } -std::string MyUnit::get(const std::string &key) const { +std::string KeyValueStore::get(const std::string &key) const { const auto it = store_.find(key); return it != store_.end() ? it->second : ""; } + +} // namespace MyUnit +} // namespace MyComponent +} // namespace my_element diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit.h b/bazel/rules/rules_score/examples/minimal/src/my_unit.h index 10160d0c..763c3b30 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit.h +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit.h @@ -16,7 +16,11 @@ #include #include -class MyUnit { +namespace my_element { +namespace MyComponent { +namespace MyUnit { + +class KeyValueStore { public: // trace: MinimalExample.FEAT_001 void configure(const std::string &key, const std::string &value); @@ -27,3 +31,7 @@ class MyUnit { private: std::unordered_map store_; }; + +} // namespace MyUnit +} // namespace MyComponent +} // namespace my_element diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp b/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp index 5c8a1204..51ee6b0d 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp @@ -13,15 +13,15 @@ #include -#include "src/my_unit.h" +#include "my_unit.h" TEST(MyUnitTest, ConfigureAndGet) { ::testing::Test::RecordProperty( - "lobster-tracing", "MinimalExample.FEAT_001 MinimalExample.FEAT_002"); + "lobster-tracing", "MinimalExample.FEAT_001,MinimalExample.FEAT_002"); ::testing::Test::RecordProperty("given", - "a default-constructed MyUnit instance"); - MyUnit unit{}; + "a default-constructed MyUnit::KeyValueStore instance"); + my_element::MyComponent::MyUnit::KeyValueStore unit{}; ::testing::Test::RecordProperty( "when", @@ -36,8 +36,8 @@ TEST(MyUnitTest, MissingKeyReturnsEmpty) { ::testing::Test::RecordProperty("lobster-tracing", "MinimalExample.FEAT_002"); ::testing::Test::RecordProperty("given", - "a default-constructed MyUnit instance"); - MyUnit unit{}; + "a default-constructed MyUnit::KeyValueStore instance"); + my_element::MyComponent::MyUnit::KeyValueStore unit{}; ::testing::Test::RecordProperty( "when", "When get is called with a key that hasn't been configured yet"); diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml index 6170a9c9..2e5f71a8 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml @@ -13,11 +13,15 @@ @startuml unit_1_class_diagram -namespace unit_1 { - class Foo { +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Foo { - + GetNumber() : uint8_t - + SetNumber(value : uint8_t) : void + + GetNumber() : uint8_t + + SetNumber(value : uint8_t) : void + } + } } } diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp b/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp index 65c52d8a..91338cb8 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp @@ -13,8 +13,13 @@ #include "unit_1/foo.h" +namespace safety_software_seooc_example { +namespace component_example { namespace unit_1 { // trace: SampleComponent.REQ_COMP_001 SampleLibraryAPI.GetNumber std::uint8_t Foo::GetNumber() const { return 42u; } + } // namespace unit_1 +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h index 30457e8c..e906873c 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h @@ -16,6 +16,8 @@ #include +namespace safety_software_seooc_example { +namespace component_example { namespace unit_1 { // trace: SampleComponent.REQ_COMP_002 @@ -28,5 +30,7 @@ class Foo final { }; } // namespace unit_1 +} // namespace component_example +} // namespace safety_software_seooc_example #endif // FOO_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp index a8323f28..883e38e5 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp @@ -17,6 +17,8 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; + TEST(Foo, GetNumber) { ::testing::Test::RecordProperty("lobster-tracing", "SampleComponent.REQ_COMP_001"); diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp b/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp index d4c3c584..f0bf40c1 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp @@ -19,10 +19,13 @@ namespace { std::uint8_t kExpectedNumber = 42u; } +namespace safety_software_seooc_example { +namespace component_example { namespace unit_2 { // trace: SampleComponent.REQ_COMP_004 SampleComponent.REQ_COMP_005 -Bar::Bar(std::unique_ptr foo) : foo_{std::move(foo)} {} +Bar::Bar(std::unique_ptr foo) + : foo_{std::move(foo)} {} // trace: SampleComponent.REQ_COMP_003 bool Bar::AssertNumber() const { @@ -30,3 +33,5 @@ bool Bar::AssertNumber() const { return foo_->GetNumber() == kExpectedNumber; } } // namespace unit_2 +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar.h b/bazel/rules/rules_score/examples/seooc/unit_2/bar.h index 6f24c0ee..c85bc8d8 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar.h +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar.h @@ -19,19 +19,23 @@ #include #include +namespace safety_software_seooc_example { +namespace component_example { namespace unit_2 { // trace: SampleComponent.REQ_COMP_003 SampleComponent.REQ_COMP_004 // SampleComponent.REQ_COMP_005 class Bar final { public: - explicit Bar(std::unique_ptr foo); + explicit Bar(std::unique_ptr foo); bool AssertNumber() const; private: - std::unique_ptr foo_; + std::unique_ptr foo_; }; } // namespace unit_2 +} // namespace component_example +} // namespace safety_software_seooc_example #endif // BAR_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp index eaa5fc8a..c5522966 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp @@ -15,6 +15,9 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; +namespace unit_2 = safety_software_seooc_example::component_example::unit_2; + TEST(Bar, AssertNumber) { ::testing::Test::RecordProperty( "lobster-tracing", "SampleComponent.REQ_COMP_003,SampleComponent.REQ_" diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml index fafab5f2..d7944d7c 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml @@ -13,15 +13,20 @@ @startuml unit_2_class_diagram -' Detail of `Bar`. No namespace: the declaration matches the bodyless `Bar` of -' unit_2_overview.puml (name, element type and stereotype) and is anchored onto -' `unit_2`. -class Bar <> { - -- - - foo_ : unique_ptr - -- - + Bar(foo : unique_ptr) - + AssertNumber() : bool +' Detail of Bar; Foo and the uses relationship are declared in +' unit_2_overview.puml. +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + class Bar <> { + -- + - foo_ : unique_ptr + -- + + Bar(foo : unique_ptr) + + AssertNumber() : bool + } + } + } } @enduml diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml index bdf08aa8..44bea2c9 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml @@ -17,12 +17,16 @@ ' All classes are references (no body); `Bar` is elaborated in ' unit_2_class_diagram.puml, `Foo` is owned and elaborated by unit_1. -namespace unit_1 { - class Foo <> -} +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Foo <> + } -namespace unit_2 { - class Bar <> + namespace unit_2 { + class Bar <> + } + } } Bar --> Foo : uses diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml index b8d504b4..4fc3a9b0 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml @@ -13,9 +13,15 @@ @startuml unit_3_class_diagram -namespace unit_3 { - class Formatter <> { - + Format(foo : const unit_1::Foo &) : std::string +namespace safety_software_seooc_example { + namespace component_example { + namespace sub_component_example { + namespace unit_3 { + class Formatter <> { + + Format(foo : const safety_software_seooc_example::component_example::unit_1::Foo &) : std::string + } + } + } } } diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp index 688bb7c9..271310ae 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp @@ -13,11 +13,18 @@ #include "unit_3/formatter.h" +namespace safety_software_seooc_example { +namespace component_example { +namespace sub_component_example { namespace unit_3 { // trace: SampleComponent.REQ_COMP_006 -std::string Formatter::Format(const unit_1::Foo &foo) const { +std::string Formatter::Format( + const safety_software_seooc_example::component_example::unit_1::Foo &foo) const { return std::to_string(foo.GetNumber()); } } // namespace unit_3 +} // namespace sub_component_example +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h index 1cd0e435..f851d99d 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h @@ -18,14 +18,21 @@ #include +namespace safety_software_seooc_example { +namespace component_example { +namespace sub_component_example { namespace unit_3 { class Formatter final { public: // trace: SampleComponent.REQ_COMP_006 - std::string Format(const unit_1::Foo &foo) const; + std::string Format( + const safety_software_seooc_example::component_example::unit_1::Foo &foo) const; }; } // namespace unit_3 +} // namespace sub_component_example +} // namespace component_example +} // namespace safety_software_seooc_example #endif // FORMATTER_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp index 6a636763..c1ee0a35 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp @@ -15,6 +15,9 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; +namespace unit_3 = safety_software_seooc_example::component_example::sub_component_example::unit_3; + TEST(Formatter, Format) { ::testing::Test::RecordProperty("lobster-tracing", "SampleComponent.REQ_COMP_006"); diff --git a/bazel/rules/rules_score/examples/some_other_library/BUILD b/bazel/rules/rules_score/examples/some_other_library/BUILD index 1b185bc4..91428b89 100644 --- a/bazel/rules/rules_score/examples/some_other_library/BUILD +++ b/bazel/rules/rules_score/examples/some_other_library/BUILD @@ -22,6 +22,7 @@ load( "fault_trees", "safety_analysis", "unit", + "unit_design", ) architectural_design( @@ -71,12 +72,17 @@ unit( name = "abc", scope = ["//:__pkg__"], tests = [], - unit_design = [], + unit_design = [":abc_design"], implementation = [ ":some_other_library", ], ) +unit_design( + name = "abc_design", + static = ["abc_class_design.puml"], +) + component( name = "component_example", components = [ diff --git a/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml b/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml new file mode 100644 index 00000000..d68ca28a --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml abc_class_design + +namespace other_seooc { + namespace component_example { + namespace abc { + class AbcImpl + } + } +} + +@enduml diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 8dd2e1d7..c1d4051c 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1009,17 +1009,27 @@ def _collect_architecture_components(ctx): return all_components -def _run_validation(ctx, arch_json, static_fbs_files): +def _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_label): """Run the dependable-element validation profile. Args: ctx: Rule context arch_json: The architecture JSON File object (already declared and written) static_fbs_files: Component-diagram FlatBuffer files from architectural_design targets. + unit_design_class_fbs_by_label: Dict from Bazel unit labels to their class-diagram FlatBuffer files. Returns: validation_log File object """ + unit_design_class_fbs = [ + depset(fbs_files) + for fbs_files in unit_design_class_fbs_by_label.values() + ] + action_inputs = depset( + direct = [arch_json] + static_fbs_files, + transitive = unit_design_class_fbs, + ).to_list() + return run_validation( ctx = ctx, validation_cli = ctx.executable._validation_cli, @@ -1027,8 +1037,12 @@ def _run_validation(ctx, arch_json, static_fbs_files): input_bundle = { "architecture": arch_json.path, "component_diagrams": [f.path for f in static_fbs_files], + "unit_design_class_diagrams": { + unit_label: [f.path for f in fbs_files] + for unit_label, fbs_files in unit_design_class_fbs_by_label.items() + }, }, - inputs = [arch_json] + static_fbs_files, + inputs = action_inputs, mnemonic = "DependableElementValidate", maturity = ctx.attr.maturity, log_level = get_log_level(ctx), @@ -1330,8 +1344,16 @@ def _dependable_element_index_impl(ctx): static_fbs_files.extend(ad[ArchitecturalDesignInfo].static.to_list()) validation_logs.extend(ad[ArchitecturalDesignInfo].validation_logs) + unit_design_class_fbs_by_label = {} + for unit_target in all_units.values(): + unit_design_class_fbs_by_label[str(unit_target.label)] = ( + unit_target[UnitInfo].unit_design_static_fbs.to_list() + ) + # Run validation; build fails automatically on non-zero exit - validation_logs.append(_run_validation(ctx, arch_json, static_fbs_files)) + validation_logs.append( + _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_label), + ) validation_output_files = [] for log in validation_logs: diff --git a/validation/core/BUILD b/validation/core/BUILD index 7381e77c..fe302e4e 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -22,6 +22,7 @@ exports_files( filegroup( name = "specifications", srcs = [ + "docs/specifications/architectural_unit_class_design.md", "docs/specifications/bazel_component.md", "docs/specifications/class_design_implementation.md", "docs/specifications/class_design_sequence.md", @@ -58,6 +59,7 @@ rust_library( "src/results/diagnostics.rs", "src/results/error_models.rs", "src/results/mod.rs", + "src/validators/architectural_unit_class_design_validator.rs", "src/validators/bazel_component_validator.rs", "src/validators/class_design_implementation_validator.rs", "src/validators/class_design_sequence_validator.rs", diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index 33c92771..dc7d5fe1 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -221,6 +221,26 @@ section "Tool Requirements" { } + section "Architectural Unit Class Design Validator" { + + ToolQualification.ToolRequirement ArchitecturalUnitClassDesignConsistency { + description = '''The validator shall report an error when a + unit declared in the architectural component diagram has no + corresponding class, struct, interface, or abstract-class + entity in the static class diagram of the unit design bound to + that unit. An enum alone shall not satisfy this requirement. + + A corresponding entity shall have a canonical identifier with + the architectural unit identifier as a complete path prefix, + followed by at least one additional entity-name segment. + References to entities outside that path shall not constitute + corresponding entities.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + } + section "Class Design Implementation Validator" { ToolQualification.ToolRequirement ClassDesignImplementationEntityConsistency { diff --git a/validation/core/docs/specifications/architectural_unit_class_design.md b/validation/core/docs/specifications/architectural_unit_class_design.md new file mode 100644 index 00000000..2d8313a9 --- /dev/null +++ b/validation/core/docs/specifications/architectural_unit_class_design.md @@ -0,0 +1,138 @@ + + +# Architectural Unit Class Design Specification + +## Purpose + +This validator enforces refinement consistency between a unit declared in an +architectural component diagram and the static class diagram of the +`unit_design` bound to that unit. + +It shall make sure that each architectural unit is refined by at least one +class, struct, interface, or abstract class owned by that unit in its detailed +design. Enums alone do not satisfy this refinement requirement. + +## What is Validated + +The validator compares two inputs associated through the Bazel `unit` target: + +| Input | Source | Meaning | +|---|---|---| +| Architectural unit | `architectural_design.static` component diagram | A `<>` entity and its canonical identifier | +| Detailed class design | `unit_design.static` class diagram bound to that Bazel `unit` | Class, struct, interface, and abstract-class entities that refine the unit | + +The existing Bazel-component validation establishes the mapping from an +architectural `<>` to its Bazel `unit` target. This validator then uses +that target's `unit_design` binding to select the detailed class diagram to +compare. It does not validate the Bazel-to-component mapping itself. + +### Unit-Class Refinement Consistency + +For every architectural `<>`, the static class diagrams bound to its +corresponding Bazel `unit` target must declare at least one class, struct, +interface, or abstract class in that unit's canonical identifier path. Matching +uses the full architectural unit ID as a complete path prefix, followed by at +least one entity-name segment. An enum does not satisfy this requirement. + +*(Requirement: {requirement:downstream-ref}`Tools.ArchitecturalUnitClassDesignConsistency`)* + +For example, `Filter` is an architectural unit with canonical identifier +`system.control.Filter`. Its Bazel target has design name `Filter`; the unit +design bound to that target refines it when it declares an entity such as +`system.control.Filter.FilterService`. + +```text +' architectural component diagram +package system { + package control { + component Filter <> + } +} +``` + +```text +@startuml +namespace system { + namespace control { + namespace Filter { + class FilterService + } + } +} +@enduml +``` + +The following does not refine `system.control.Filter`, because it has the same +leaf name under a different parent path: + +```text +@startuml +namespace system { + namespace sensor { + namespace Filter { + class SensorDriver + } + } +} +@enduml +``` + +Class diagrams are evaluated only within the class-diagram files bound to the +matching Bazel unit. A collaborator stub in another unit's namespace does not +refine the current unit. + +References to external entities are permitted, but do not satisfy this rule. +For example, a relationship from `FilterService` to +`system.sensor.Sensor.SensorDriver` is an external reference; only an eligible +entity declared in `Filter`'s unit design can satisfy the requirement. + +### Identifier Matching + +Identifiers are compared as canonical identifier paths. The eligible entity +identifier must begin with the full architectural unit ID followed by `.` and +at least one additional entity-name segment. A bare or lexical prefix match is not +sufficient: `system.control.Filtering.Service` does not refine +`system.control.Filter`. + +Aliases and display names do not take part in matching. + +### Not Validated Here + +This validator does not validate: + +- the correspondence between architectural units and Bazel `unit` targets; + that is covered by the Bazel Component validator. +- component-diagram interface declarations against public or internal API + diagrams; that is covered by the Component Public API and Component Internal + API validators. +- class members, methods, or relationships against implementation code; that + is covered by the Class Design Implementation validator. +- whether external class references are allowed architectural dependencies. + +## Failure Cases + +| Failure case | Validation rule | +|---|---| +| The bound static unit-design class diagrams declare no eligible entity below the full architectural unit ID | Unit-Class Refinement Consistency | +| The bound static unit-design class diagrams only declare eligible entities below another architectural unit ID | Unit-Class Refinement Consistency | +| The bound static unit-design class diagrams only reference external entities | Unit-Class Refinement Consistency | + +## Debug Output + +The validator shall emit debug output containing: + +- the architectural unit identifier; +- the Bazel `unit` target and its bound `unit_design` target; +- the eligible entity identifiers read from the bound static class diagrams; +- the eligible entity identifiers selected as refinements of each architectural unit. diff --git a/validation/core/integration_test/architectural_unit_class_design/BUILD b/validation/core/integration_test/architectural_unit_class_design/BUILD new file mode 100644 index 00000000..aaf0e90b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_test") + +filegroup( + name = "architectural_unit_class_design_test_data", + srcs = [ + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_class:case_data", + "//validation/core/integration_test/architectural_unit_class_design/negative_path_prefix:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_refining_class:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types:case_data", + ], +) + +rust_test( + name = "architectural_unit_class_design_integration_test", + srcs = ["architectural_unit_class_design_suite.rs"], + crate_root = "architectural_unit_class_design_suite.rs", + data = [":architectural_unit_class_design_test_data"], + deps = [ + "//validation/core:validation_cli", + "//validation/core/integration_test:test_framework", + "@crates_score_tooling//:serde_json", + ], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs new file mode 100644 index 00000000..0601f2fb --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs @@ -0,0 +1,123 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use test_framework::{ + assert_cli_result, case_file_path, collect_case_fbs_files, load_expected_yaml_fixture, + normalize_yaml_result, run_validation_profile, CliRunResult, +}; + +const SUITE_DIR: &str = "architectural_unit_class_design"; + +fn collect_unit_design_class_fbs( + architecture_path: &Path, + class_fbs_paths: &[String], +) -> BTreeMap> { + let architecture: Value = serde_json::from_slice( + &std::fs::read(architecture_path).expect("failed to read architecture fixture"), + ) + .expect("failed to parse architecture fixture"); + let components = architecture["components"] + .as_object() + .expect("architecture fixture must contain a components object"); + + let mut class_fbs_by_unit_label = BTreeMap::new(); + let mut assigned_paths = BTreeSet::new(); + for component in components.values() { + let Some(units) = component.get("units").and_then(Value::as_array) else { + continue; + }; + + for unit in units { + let label = unit["label"] + .as_str() + .expect("architecture unit must have a label"); + let design_name = unit["design_name"] + .as_str() + .expect("architecture unit must have a design_name"); + let file_prefix = format!("{design_name}_"); + let unit_class_fbs = class_fbs_paths + .iter() + .filter(|path| { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with(&file_prefix)) + }) + .cloned() + .collect::>(); + + for path in &unit_class_fbs { + assert!( + assigned_paths.insert(path.clone()), + "class-diagram FlatBuffer {path} matches multiple unit design names" + ); + } + class_fbs_by_unit_label.insert(label.to_string(), unit_class_fbs); + } + } + + assert_eq!( + assigned_paths.len(), + class_fbs_paths.len(), + "every unit-design class diagram must match a unit design_name" + ); + class_fbs_by_unit_label +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let class_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "unit_design_class"); + let architecture_path = case_file_path(&format!( + "validation/core/integration_test/{SUITE_DIR}/{case_dir}/architecture.json" + )); + let unit_design_class_diagrams = + collect_unit_design_class_fbs(&architecture_path, &class_fbs_paths); + + let result: CliRunResult = run_validation_profile( + &format!("architectural_unit_class_design_{case_dir}"), + "dependable-element", + serde_json::json!({ + "architecture": architecture_path.display().to_string(), + "component_diagrams": component_fbs_paths, + "unit_design_class_diagrams": unit_design_class_diagrams, + }), + ); + + let result = normalize_yaml_result(result); + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn positive_refining_class() { + assert_case("positive_refining_class"); +} + +#[test] +fn positive_refining_supported_entity_types() { + assert_case("positive_refining_supported_entity_types"); +} + +#[test] +fn negative_path_prefix() { + assert_case("negative_path_prefix"); +} + +#[test] +fn negative_missing_class() { + assert_case("negative_missing_class"); +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD new file mode 100644 index 00000000..259ff4b0 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD @@ -0,0 +1,35 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [":architecture"], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_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 +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml new file mode 100644 index 00000000..27f4c082 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: + - | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_1" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_1 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Unit design entities : + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_1". + - | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_2" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_2 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_2:unit_2 + Unit design entities : + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_2". diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD new file mode 100644 index 00000000..85f1e266 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_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 +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml new file mode 100644 index 00000000..3579992f --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/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: | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_1" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_1 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Entity : "safety_software_seooc_example.component_example.unit_1_extra.Service" + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml" + Design source line : 18 + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_1". diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml new file mode 100644 index 00000000..c4a0471f --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1_extra { + class Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml new file mode 100644 index 00000000..bbc5e0bf --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service <> + } + + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD new file mode 100644 index 00000000..85f1e266 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_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 +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/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/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml new file mode 100644 index 00000000..c3c9671b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml new file mode 100644 index 00000000..225637b4 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_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 +' Collaborator stub for a class owned by unit_1; it must not be indexed as a +' refinement of unit_2. +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service <> + } + + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD new file mode 100644 index 00000000..3a17cd47 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD @@ -0,0 +1,61 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of 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", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +unit_design( + name = "unit_3_design", + static = ["unit_3_class_diagram.puml"], +) + +unit_design( + name = "unit_4_design", + static = ["unit_4_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ":unit_3_design", + ":unit_4_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json new file mode 100644 index 00000000..428c3d63 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json @@ -0,0 +1,34 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_3:unit_3", + "design_name": "unit_3" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_4:unit_4", + "design_name": "unit_4" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml new file mode 100644 index 00000000..5106d645 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + component unit_3 <> + component unit_4 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/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/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml new file mode 100644 index 00000000..8666d650 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + struct Unit1State + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml new file mode 100644 index 00000000..f25a0a89 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + interface Unit2Port + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml new file mode 100644 index 00000000..df55eee9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_3 { + abstract class Unit3Base + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml new file mode 100644 index 00000000..20508d0b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_4 { + class Unit4Service + } + } +} +@enduml diff --git a/validation/core/src/profiles/dependable_element.rs b/validation/core/src/profiles/dependable_element.rs index 4ec0e9f3..f024c36b 100644 --- a/validation/core/src/profiles/dependable_element.rs +++ b/validation/core/src/profiles/dependable_element.rs @@ -12,21 +12,54 @@ // ******************************************************************************* use crate::models::{ - BazelArchitecture, BazelInput, ComponentDiagramArchitecture, ComponentDiagramInputs, + BazelArchitecture, BazelInput, ClassDiagramInputs, ClassEntityIndex, + ComponentDiagramArchitecture, ComponentDiagramInputs, +}; +use crate::readers::{BazelReader, ClassDiagramReader, ComponentDiagramReader}; +use crate::validators::{ + validate_architectural_unit_class_design, validate_bazel_component, UnitDesignEntity, }; -use crate::readers::{BazelReader, ComponentDiagramReader}; -use crate::validators::validate_bazel_component; use crate::ValidationResult; use serde::Deserialize; +use std::collections::BTreeMap; use super::profile::{merge_results, read_and_convert, ProfileRun}; +type ProfileValidator<'a> = Box Option + 'a>; + #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct DependableElementInputs { architecture: String, #[serde(default)] component_diagrams: Vec, + #[serde(default)] + unit_design_class_diagrams: Option>>, +} + +fn registered_validators<'a>( + bazel: &'a Option, + component: &'a Option, + unit_design_classes: &'a Option>>, +) -> Vec> { + vec![ + Box::new(move || { + let (bazel, component) = (bazel.as_ref()?, component.as_ref()?); + Some(validate_bazel_component(bazel, component)) + }), + Box::new(move || { + let (bazel, component, unit_design_classes) = ( + bazel.as_ref()?, + component.as_ref()?, + unit_design_classes.as_ref()?, + ); + Some(validate_architectural_unit_class_design( + bazel, + component, + unit_design_classes, + )) + }), + ] } pub fn run(inputs: &DependableElementInputs) -> Result { @@ -41,11 +74,40 @@ pub fn run(inputs: &DependableElementInputs) -> Result { &mut result, |raw: ComponentDiagramInputs, errs| raw.to_diagram_architecture(errs), )?; + let unit_design_classes = match &inputs.unit_design_class_diagrams { + Some(diagrams_by_label) => { + let mut entity_ids_by_label = BTreeMap::new(); + for (unit_label, paths) in diagrams_by_label { + let diagrams = read_and_convert::( + paths.as_slice(), + &mut result, + |raw: ClassDiagramInputs, _errs| raw, + )? + .unwrap_or_default(); + let index = ClassEntityIndex::build_diagram_index(&diagrams, &mut result); + let entities = index + .entities() + .filter(|entity| is_unit_refinement_entity(entity.entity_type)) + .map(|entity| UnitDesignEntity { + id: entity.id.clone(), + source_location: entity.source_location.clone(), + }) + .collect(); + entity_ids_by_label.insert(unit_label.clone(), entities); + } + Some(entity_ids_by_label) + } + None => None, + }; + + let validators = registered_validators(&bazel, &component, &unit_design_classes); let mut ran_validator = false; - if let (Some(bazel), Some(component)) = (bazel.as_ref(), component.as_ref()) { - merge_results(&mut result, validate_bazel_component(bazel, component)); - ran_validator = true; + for validator in validators { + if let Some(validator_result) = validator() { + merge_results(&mut result, validator_result); + ran_validator = true; + } } Ok(ProfileRun { @@ -53,3 +115,33 @@ pub fn run(inputs: &DependableElementInputs) -> Result { result, }) } + +fn is_unit_refinement_entity(entity_type: class_diagram::EntityType) -> bool { + matches!( + entity_type, + class_diagram::EntityType::Class + | class_diagram::EntityType::Struct + | class_diagram::EntityType::Interface + | class_diagram::EntityType::AbstractClass + ) +} + +#[cfg(test)] +mod tests { + use super::is_unit_refinement_entity; + use class_diagram::EntityType; + + #[test] + fn unit_refinement_accepts_class_like_entities_but_not_enums() { + for entity_type in [ + EntityType::Class, + EntityType::Struct, + EntityType::Interface, + EntityType::AbstractClass, + ] { + assert!(is_unit_refinement_entity(entity_type)); + } + + assert!(!is_unit_refinement_entity(EntityType::Enum)); + } +} diff --git a/validation/core/src/validators/architectural_unit_class_design_validator.rs b/validation/core/src/validators/architectural_unit_class_design_validator.rs new file mode 100644 index 00000000..85386867 --- /dev/null +++ b/validation/core/src/validators/architectural_unit_class_design_validator.rs @@ -0,0 +1,300 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use std::collections::BTreeMap; + +use source_location::SourceLocation; + +use crate::models::{BazelArchitecture, ComponentDiagramArchitecture}; +use crate::results::{Diagnostics, ErrorBuilder, ErrorCategory}; +use crate::validators::shared::{format_source_file, format_source_line}; +use crate::ValidationResult; + +#[derive(Clone, Debug)] +pub(crate) struct UnitDesignEntity { + pub id: String, + pub source_location: SourceLocation, +} + +/// Verify that every architectural unit is refined by an eligible entity whose +/// ID is nested below the full architectural unit ID in the bound unit design. +pub fn validate_architectural_unit_class_design( + bazel: &BazelArchitecture, + component: &ComponentDiagramArchitecture, + design_entities_by_unit_label: &BTreeMap>, +) -> ValidationResult { + let mut result = ValidationResult::default(); + + for (unit_key, architectural_unit) in &component.unit_set { + let Some(unit_label) = bazel.unit_set.get(unit_key) else { + // Bazel-component validation reports this independently. + continue; + }; + + let entities = design_entities_by_unit_label + .get(unit_label) + .map(Vec::as_slice) + .unwrap_or(&[]); + validate_unit(architectural_unit, unit_label, entities, &mut result); + } + + result +} + +fn validate_unit( + architectural_unit: &crate::models::LogicComponent, + unit_label: &str, + entities: &[UnitDesignEntity], + result: &mut ValidationResult, +) { + let refinements: Vec<&UnitDesignEntity> = entities + .iter() + .filter(|entity| refines_unit(&entity.id, &architectural_unit.id)) + .collect(); + + log_unit_diagnostics( + &mut result.diagnostics, + &architectural_unit.id, + unit_label, + entities, + &refinements, + ); + + if refinements.is_empty() { + result.add_failure(missing_refinement_error( + &architectural_unit.id, + unit_label, + entities, + )); + } +} + +fn refines_unit(entity_id: &str, unit_id: &str) -> bool { + entity_id + .to_lowercase() + .starts_with(&format!("{}.", unit_id.to_lowercase())) +} + +fn missing_refinement_error( + unit_id: &str, + unit_label: &str, + entities: &[UnitDesignEntity], +) -> String { + let mut error = ErrorBuilder::new(ErrorCategory::Design) + .title(format!( + "Architectural unit \"{unit_id}\" has no refining entity in its unit design" + )) + .field("architectural unit", unit_id) + .field("bazel unit", unit_label); + + if entities.is_empty() { + error = error.field("unit design entities", ""); + } else { + for entity in entities { + error = error + .field("entity", format!("\"{}\"", entity.id)) + .field( + "design source file", + format!("\"{}\"", format_source_file(&entity.source_location)), + ) + .field( + "design source line", + format_source_line(&entity.source_location), + ); + } + } + + error + .fix(format!( + "add a class, struct, interface, or abstract class whose ID is below the architectural unit ID \"{unit_id}\"" + )) + .build() +} + +fn log_unit_diagnostics( + diagnostics: &mut Diagnostics, + unit_id: &str, + unit_label: &str, + entities: &[UnitDesignEntity], + refinements: &[&UnitDesignEntity], +) { + diagnostics.debug(|| { + format!( + "Architectural unit: {}\nBazel unit: {}\nEligible entity identifiers: {}\nSelected refinements: {}", + unit_id, + unit_label, + display_entities(entities), + display_refinement_ids(refinements), + ) + }); +} + +fn display_entities(entities: &[UnitDesignEntity]) -> String { + display_list( + entities + .iter() + .map(|entity| { + let (file, line) = entity.source_location.display(); + format!("{} ({}:{})", entity.id, file, line) + }) + .collect(), + ) +} + +fn display_refinement_ids(entities: &[&UnitDesignEntity]) -> String { + display_list(entities.iter().map(|entity| entity.id.clone()).collect()) +} + +fn display_list(items: Vec) -> String { + if items.is_empty() { + "".to_string() + } else { + items.join(", ") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{ + BazelEntityRef, BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentType, + LogicComponent, + }; + use std::collections::BTreeMap; + + fn source_location() -> source_location::SourceLocation { + source_location::SourceLocation::new("test.puml", 1) + } + + fn component_entity(id: &str, parent_id: Option<&str>, stereotype: &str) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: id.rsplit('.').next().map(str::to_string), + alias: None, + parent_id: parent_id.map(str::to_string), + element_type: if stereotype == "SEooC" { + ComponentType::Package + } else { + ComponentType::Component + }, + stereotype: Some(stereotype.to_string()), + relations: Vec::new(), + source_location: source_location(), + } + } + + fn bazel_ref(label: &str, design_name: &str) -> BazelEntityRef { + BazelEntityRef { + label: label.to_string(), + design_name: design_name.to_string(), + } + } + + fn inputs( + class_ids: &[&str], + ) -> ( + BazelArchitecture, + ComponentDiagramArchitecture, + BTreeMap>, + ) { + let mut components = BTreeMap::new(); + components.insert( + "@//:system".to_string(), + BazelInputEntry { + units: Vec::new(), + components: vec![bazel_ref("@//:control", "control")], + }, + ); + components.insert( + "@//:control".to_string(), + BazelInputEntry { + units: vec![bazel_ref("@//:filter", "Filter")], + components: Vec::new(), + }, + ); + + let mut result = ValidationResult::default(); + let bazel = BazelInput { components }.to_bazel_architecture(&mut result); + let component = ComponentDiagramInputs { + entities: vec![ + component_entity("system", None, "SEooC"), + component_entity("system.control", Some("system"), "component"), + component_entity("system.control.Filter", Some("system.control"), "unit"), + ], + } + .to_diagram_architecture(&mut result); + assert!( + result.is_empty(), + "fixture setup failed: {:?}", + result.failures + ); + + let classes = BTreeMap::from([( + "@//:filter".to_string(), + class_ids + .iter() + .map(|id| UnitDesignEntity { + id: (*id).to_string(), + source_location: source_location(), + }) + .collect(), + )]); + + (bazel, component, classes) + } + + #[test] + fn accepts_a_class_in_the_bound_unit_design_namespace() { + let (bazel, component, classes) = inputs(&["system.control.Filter.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn rejects_a_class_from_another_namespace() { + let (bazel, component, classes) = inputs(&["system.control.OtherUnit.Service"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains( + "Architectural unit \"system.control.Filter\" has no refining entity in its unit design." + )); + assert!(result.failures[0].contains("Design source file : \"test.puml\"")); + assert!(result.failures[0].contains("Design source line : 1")); + } + + #[test] + fn rejects_a_class_with_the_same_unit_name_under_another_parent() { + let (bazel, component, classes) = inputs(&["system.other.Filter.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + + assert_eq!(result.failures.len(), 1); + } + + #[test] + fn rejects_a_lexical_but_not_segment_prefix() { + let (bazel, component, classes) = inputs(&["system.control.Filtering.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + + assert_eq!(result.failures.len(), 1); + } +} diff --git a/validation/core/src/validators/class_design_implementation_validator.rs b/validation/core/src/validators/class_design_implementation_validator.rs index 58b71a18..491e302c 100644 --- a/validation/core/src/validators/class_design_implementation_validator.rs +++ b/validation/core/src/validators/class_design_implementation_validator.rs @@ -14,7 +14,7 @@ //! Class implementation validation: compare unit design class diagrams with //! C++ implementation produced by the C++ parser. -use super::shared::best_string_suggestion; +use super::shared::{best_string_suggestion, format_source_file, format_source_line}; use crate::models::ClassEntityIndex; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::ValidationResult; @@ -22,7 +22,6 @@ use class_diagram::{ EnumLiteral, FreeFunctionDecl, MemberVariable, Method, Relationship, SimpleEntity, TemplateParameter, TypeAlias, }; -use source_location::SourceLocation; use std::collections::BTreeMap; /// Run design-class-vs-implementation-class validation using indexed inputs. @@ -574,9 +573,12 @@ impl ClassDesignImplementationValidator { .field("class", format!("\"{}\"", entity.id)) .field( "design source file", - format!("\"{}\"", source_file(&entity.source_location)), + format!("\"{}\"", format_source_file(&entity.source_location)), + ) + .field( + "design source line", + format_source_line(&entity.source_location), ) - .field("design source line", source_line(&entity.source_location)) .fix(format!( "add implementation class \"{}\" in the C++ class implementation, or remove it from the unit design", entity.id @@ -603,9 +605,12 @@ impl ClassDesignImplementationValidator { .field("free function", format!("\"{function_name}\"")) .field( "design source file", - format!("\"{}\"", source_file(&function.source_location)), + format!("\"{}\"", format_source_file(&function.source_location)), + ) + .field( + "design source line", + format_source_line(&function.source_location), ) - .field("design source line", source_line(&function.source_location)) .fix(format!( "add free function \"{function_name}\" to the C++ implementation, or remove it from the unit design" )); @@ -636,23 +641,23 @@ impl ClassDesignImplementationValidator { .field("design value", design_value) .field( "design source file", - format!("\"{}\"", source_file(&design_function.source_location)), + format!("\"{}\"", format_source_file(&design_function.source_location)), ) .field( "design source line", - source_line(&design_function.source_location), + format_source_line(&design_function.source_location), ) .field("implementation value", implementation_value) .field( "implementation source file", format!( "\"{}\"", - source_file(&implementation_function.source_location) + format_source_file(&implementation_function.source_location) ), ) .field( "implementation source line", - source_line(&implementation_function.source_location), + format_source_line(&implementation_function.source_location), ) .fix(format!( "make {field} on free function \"{function_name}\" consistent between the unit design and the C++ implementation" @@ -675,9 +680,12 @@ impl ClassDesignImplementationValidator { .field("member", format!("{member_type} \"{member_name}\"")) .field( "design source file", - format!("\"{}\"", source_file(&design_entity.source_location)), + format!("\"{}\"", format_source_file(&design_entity.source_location)), + ) + .field( + "design source line", + format_source_line(&design_entity.source_location), ) - .field("design source line", source_line(&design_entity.source_location)) .fix(format!( "add {member_type} \"{member_name}\" to entity \"{}\" in the C++ class implementation, or remove it from the unit design", design_entity.id @@ -712,17 +720,23 @@ impl ClassDesignImplementationValidator { .field("design value", design_value) .field( "design source file", - format!("\"{}\"", source_file(&design_entity.source_location)), + format!("\"{}\"", format_source_file(&design_entity.source_location)), + ) + .field( + "design source line", + format_source_line(&design_entity.source_location), ) - .field("design source line", source_line(&design_entity.source_location)) .field("implementation value", implementation_value) .field( "implementation source file", - format!("\"{}\"", source_file(&implementation_entity.source_location)), + format!( + "\"{}\"", + format_source_file(&implementation_entity.source_location) + ), ) .field( "implementation source line", - source_line(&implementation_entity.source_location), + format_source_line(&implementation_entity.source_location), ) .fix(format!( "make {field_reference} in entity \"{}\" consistent between the unit design and the C++ class implementation", @@ -739,26 +753,6 @@ fn format_field_reference(field: &str, style: FieldReferenceStyle) -> String { } } -fn source_file(source_location: &SourceLocation) -> String { - let (file, _) = source_location.display(); - - if file.is_empty() { - "".to_string() - } else { - file - } -} - -fn source_line(source_location: &SourceLocation) -> String { - let (_, line) = source_location.display(); - - if line == 0 { - "".to_string() - } else { - line.to_string() - } -} - fn format_class_comparison_debug( design_entity: &SimpleEntity, implementation_entity: &SimpleEntity, diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 011fbf5a..ee7b9fa4 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -13,6 +13,7 @@ //! Validator entrypoints for architecture checks. +mod architectural_unit_class_design_validator; mod bazel_component_validator; mod class_design_implementation_validator; mod class_design_sequence_validator; @@ -27,6 +28,8 @@ mod shared; #[path = "test/fixtures.rs"] pub(crate) mod fixtures; +pub use architectural_unit_class_design_validator::validate_architectural_unit_class_design; +pub(crate) use architectural_unit_class_design_validator::UnitDesignEntity; 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; diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs index 09c37675..0d9a037b 100644 --- a/validation/core/src/validators/shared/helpers.rs +++ b/validation/core/src/validators/shared/helpers.rs @@ -20,6 +20,24 @@ use strsim::jaro_winkler; pub(in crate::validators) const DEFAULT_SUGGESTION_THRESHOLD: f64 = 0.75; +pub(in crate::validators) fn format_source_file(source_location: &SourceLocation) -> String { + let (file, _) = source_location.display(); + if file.is_empty() { + "".to_string() + } else { + file + } +} + +pub(in crate::validators) fn format_source_line(source_location: &SourceLocation) -> String { + let (_, line) = source_location.display(); + if line == 0 { + "".to_string() + } else { + line.to_string() + } +} + pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> String { if names.is_empty() { return "".to_string(); diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs index 5a40198d..a5590727 100644 --- a/validation/core/src/validators/shared/mod.rs +++ b/validation/core/src/validators/shared/mod.rs @@ -22,5 +22,6 @@ pub(in crate::validators) use diagram_analysis::{ }; pub(in crate::validators) use helpers::{ best_id_suggestion, best_string_suggestion, earliest_source_by_id, extract_method_name, - format_name_list, format_sequence_call, intersect_interfaces, + format_name_list, format_sequence_call, format_source_file, format_source_line, + intersect_interfaces, }; From e2399f72694f49cd8830e8e0cea701b6cc1f22ef Mon Sep 17 00:00:00 2001 From: sheng Date: Mon, 12 Oct 2026 18:07:39 +0800 Subject: [PATCH 2/2] add architectural unit InternalInterface --- .../examples/seooc/design/static_design.puml | 2 +- .../unit_1/docs/unit_1_class_diagram.puml | 4 + .../rules_score/examples/seooc/unit_1/foo.h | 8 + .../private/dependable_element.bzl | 16 +- .../docs/requirements/tool_requirements.trlc | 14 + .../architectural_unit_class_design.md | 120 ++- .../architectural_unit_class_design/BUILD | 3 + .../architectural_unit_class_design_suite.rs | 17 + .../negative_missing_bound_interface/BUILD | 50 ++ .../architecture.json | 26 + .../component_diagram.puml | 24 + .../expected.yaml | 25 + .../internal_api.puml | 22 + .../unit_1_class_diagram.puml | 22 + .../unit_2_class_diagram.puml | 22 + .../BUILD | 50 ++ .../architecture.json | 26 + .../component_diagram.puml | 24 + .../expected.yaml | 26 + .../internal_api.puml | 22 + .../unit_1_class_diagram.puml | 25 + .../unit_2_class_diagram.puml | 25 + .../positive_internal_api_class_design/BUILD | 50 ++ .../architecture.json | 26 + .../component_diagram.puml | 24 + .../expected.yaml | 13 + .../internal_api.puml | 22 + .../unit_1_class_diagram.puml | 25 + .../unit_2_class_diagram.puml | 25 + .../core/src/profiles/dependable_element.rs | 20 +- ...chitectural_unit_class_design_validator.rs | 809 +++++++++++++++++- .../core/src/validators/shared/helpers.rs | 32 +- validation/core/src/validators/shared/mod.rs | 4 +- 33 files changed, 1570 insertions(+), 53 deletions(-) create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml create mode 100644 validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml diff --git a/bazel/rules/rules_score/examples/seooc/design/static_design.puml b/bazel/rules/rules_score/examples/seooc/design/static_design.puml index 32de5545..c6c71cc7 100644 --- a/bazel/rules/rules_score/examples/seooc/design/static_design.puml +++ b/bazel/rules/rules_score/examples/seooc/design/static_design.puml @@ -20,7 +20,7 @@ component component_example <> { interface InternalInterface unit_1 -l-( InternalInterface - unit_2 -r- InternalInterface + unit_2 )-r- InternalInterface } @enduml diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml index 2e5f71a8..eb0cad86 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml @@ -15,6 +15,10 @@ namespace safety_software_seooc_example { namespace component_example { + interface InternalInterface <> { + {abstract} GetData(BindingType binding) : safety_software_seooc_example::component_example::Data* + } + namespace unit_1 { class Foo { diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h index e906873c..6fb75f4e 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h @@ -18,6 +18,14 @@ namespace safety_software_seooc_example { namespace component_example { +struct BindingType; +struct Data; + +class InternalInterface { +public: + virtual Data *GetData(BindingType binding) = 0; +}; + namespace unit_1 { // trace: SampleComponent.REQ_COMP_002 diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index c1d4051c..26157e08 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1009,13 +1009,14 @@ def _collect_architecture_components(ctx): return all_components -def _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_label): +def _run_validation(ctx, arch_json, static_fbs_files, internal_api_fbs_files, unit_design_class_fbs_by_label): """Run the dependable-element validation profile. Args: ctx: Rule context arch_json: The architecture JSON File object (already declared and written) static_fbs_files: Component-diagram FlatBuffer files from architectural_design targets. + internal_api_fbs_files: Internal API FlatBuffer files from architectural_design targets. unit_design_class_fbs_by_label: Dict from Bazel unit labels to their class-diagram FlatBuffer files. Returns: validation_log File object @@ -1026,7 +1027,7 @@ def _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_l for fbs_files in unit_design_class_fbs_by_label.values() ] action_inputs = depset( - direct = [arch_json] + static_fbs_files, + direct = [arch_json] + static_fbs_files + internal_api_fbs_files, transitive = unit_design_class_fbs, ).to_list() @@ -1037,6 +1038,7 @@ def _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_l input_bundle = { "architecture": arch_json.path, "component_diagrams": [f.path for f in static_fbs_files], + "internal_api_diagrams": [f.path for f in internal_api_fbs_files], "unit_design_class_diagrams": { unit_label: [f.path for f in fbs_files] for unit_label, fbs_files in unit_design_class_fbs_by_label.items() @@ -1338,10 +1340,12 @@ def _dependable_element_index_impl(ctx): # Collect static FlatBuffers from architectural_design targets (the expected # static architecture) and verify them against the current architecture. static_fbs_files = [] + internal_api_fbs_files = [] validation_logs = [] for ad in ctx.attr.architectural_design: if ArchitecturalDesignInfo in ad: static_fbs_files.extend(ad[ArchitecturalDesignInfo].static.to_list()) + internal_api_fbs_files.extend(ad[ArchitecturalDesignInfo].internal_api.to_list()) validation_logs.extend(ad[ArchitecturalDesignInfo].validation_logs) unit_design_class_fbs_by_label = {} @@ -1352,7 +1356,13 @@ def _dependable_element_index_impl(ctx): # Run validation; build fails automatically on non-zero exit validation_logs.append( - _run_validation(ctx, arch_json, static_fbs_files, unit_design_class_fbs_by_label), + _run_validation( + ctx, + arch_json, + static_fbs_files, + internal_api_fbs_files, + unit_design_class_fbs_by_label, + ), ) validation_output_files = [] diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index dc7d5fe1..73f03ea0 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -239,6 +239,20 @@ section "Tool Requirements" { satisfied_by = Verifier } + ToolQualification.ToolRequirement ArchitecturalUnitInternalApiClassDesignConsistency { + description = '''When an interface declared in the component + diagram is also specified in the Internal API diagram and is + required by one or more units, the validator shall report an + error if any requiring unit's static class diagram does not + declare a corresponding interface containing every method + name specified by the Internal API interface. Units that + provide the interface are not checked by this requirement. + Interfaces with no required unit bindings are outside this + consistency check.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + } section "Class Design Implementation Validator" { diff --git a/validation/core/docs/specifications/architectural_unit_class_design.md b/validation/core/docs/specifications/architectural_unit_class_design.md index 2d8313a9..4cf63e32 100644 --- a/validation/core/docs/specifications/architectural_unit_class_design.md +++ b/validation/core/docs/specifications/architectural_unit_class_design.md @@ -23,14 +23,25 @@ It shall make sure that each architectural unit is refined by at least one class, struct, interface, or abstract class owned by that unit in its detailed design. Enums alone do not satisfy this refinement requirement. +It shall also make sure that an interface declared in the component diagram +and specified in the Internal API diagram is represented in the detailed class +design of every unit that requires that interface. The detail-design interface shall +contain every method name specified by the Internal API. Provider units are +not checked by this rule because they do not consume the interface contract. +This check validates the required-side design model; it does not claim that the +provider implements the interface in C++. + ## What is Validated -The validator compares two inputs associated through the Bazel `unit` target: +The validator uses four architectural inputs, with the detailed class design +associated through the Bazel `unit` target: | Input | Source | Meaning | |---|---|---| | Architectural unit | `architectural_design.static` component diagram | A `<>` entity and its canonical identifier | | Detailed class design | `unit_design.static` class diagram bound to that Bazel `unit` | Class, struct, interface, and abstract-class entities that refine the unit | +| Component interfaces and bindings | `architectural_design.static` component diagram | Interfaces and the units bound to each interface | +| Internal API | `architectural_design.internal_api` class diagram | Interface entities and their method names | The existing Bazel-component validation establishes the mapping from an architectural `<>` to its Bazel `unit` target. This validator then uses @@ -107,6 +118,101 @@ sufficient: `system.control.Filtering.Service` does not refine Aliases and display names do not take part in matching. +### Bound Interface and Method Consistency + +For each interface declared in the component diagram and also declared in the +Internal API diagram, the validator shall inspect every unit whose interface +binding role is `Required`. Each such unit's bound static class diagram shall +declare a corresponding interface entity and that detail-design interface +shall contain every method name declared by the Internal API interface. + +Units whose binding role is `Provided` are not checked by this rule. The +Internal API and component-interface consistency checks establish the existence +of the shared contract; this check ensures that a consumer's unit design +represents the contract it requires. It does not prove that a provider exists +or implements the interface in C++. + +The component interface and Internal API interface are matched by canonical +identifier, including their enclosing package/component context. Matching only +by the interface's final name segment across the diagrams is insufficient. +Within each Required unit's detail class diagram, the corresponding entity +must be an `Interface` declaration with the same canonical interface +identifier as the Internal API interface. The interface remains in the shared package/component +namespace; it is not moved beneath the unit namespace. Each Required unit's +diagram is checked independently, so repeating the same interface declaration +in different unit-design files does not make one unit's declaration satisfy +another Required unit's check. A collaborator stub or an interface declaration +that only appears in another unit's design does not satisfy the check. If +multiple detail interfaces in the same Required unit match the canonical +identifier, the validator shall report an ambiguity. + +Method names are compared case-sensitively. This rule does not compare method +parameters, return types, visibility, modifiers, or overload signatures. +Additional methods in the detail-design interface are permitted. + +If the component interface is not declared in the Internal API diagram, the +Component Internal API validator reports that issue; this rule does not report +it again. An Internal API interface with no Required unit bindings is outside +this check because there is no consumer unit detail class diagram to inspect. +The absence of a provider or requester role by itself is not an error under +this rule. + +For example, the component diagram below binds both `unit_1` and `unit_2` to +`InternalInterface`: + +```text +' component diagram +package example <> { + component service <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +``` + +The Internal API diagram defines the interface method: + +```text +' internal API diagram +namespace example { + namespace service { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +``` + +Only `unit_1` requires the interface, so only its detail class diagram must +declare the corresponding interface and `GetData()`. For example, this +declaration in `unit_1`'s design satisfies the interface consistency check. +The same diagram must also contain at least one refining entity below the +`unit_1` namespace to satisfy Unit-Class Refinement Consistency: + +```text +' unit_1 detail class diagram +namespace example { + namespace service { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_1 { + class Unit1Service + } + } +} +``` + +`unit_2` provides the interface, so this rule does not require its detail +class diagram to redeclare the shared contract. If `unit_1`'s design omits +`InternalInterface` or declares it without `GetData()`, validation fails for +`unit_1`. A provider implementation check is outside the scope of this rule. + +*(Requirement: {requirement:downstream-ref}`Tools.ArchitecturalUnitInternalApiClassDesignConsistency`)* + ### Not Validated Here This validator does not validate: @@ -116,9 +222,13 @@ This validator does not validate: - component-diagram interface declarations against public or internal API diagrams; that is covered by the Component Public API and Component Internal API validators. -- class members, methods, or relationships against implementation code; that - is covered by the Class Design Implementation validator. +- method signatures, modifiers, or class-design entities against C++ + implementation code; that is covered by the Class Design Implementation + validator. This specification checks only the presence of the bound + interface and Internal API method names in the unit's detail class design. - whether external class references are allowed architectural dependencies. +- whether the Provider exists or implements the interface; this rule validates + only the Required-side unit-design contract. ## Failure Cases @@ -127,6 +237,10 @@ This validator does not validate: | The bound static unit-design class diagrams declare no eligible entity below the full architectural unit ID | Unit-Class Refinement Consistency | | The bound static unit-design class diagrams only declare eligible entities below another architectural unit ID | Unit-Class Refinement Consistency | | The bound static unit-design class diagrams only reference external entities | Unit-Class Refinement Consistency | +| A unit with a Required binding to a component interface has no corresponding interface entity in its own detail class design | Bound Interface and Method Consistency | +| A Required unit's corresponding detail-design interface omits one or more Internal API method names | Bound Interface and Method Consistency | +| A Required unit's matching interface exists only in another unit's class design or as a collaborator stub | Bound Interface and Method Consistency | +| Multiple detail interfaces in one Required unit ambiguously match the component interface | Bound Interface and Method Consistency | ## Debug Output diff --git a/validation/core/integration_test/architectural_unit_class_design/BUILD b/validation/core/integration_test/architectural_unit_class_design/BUILD index aaf0e90b..7c20a0d6 100644 --- a/validation/core/integration_test/architectural_unit_class_design/BUILD +++ b/validation/core/integration_test/architectural_unit_class_design/BUILD @@ -16,8 +16,11 @@ load("@rules_rust//rust:defs.bzl", "rust_test") filegroup( name = "architectural_unit_class_design_test_data", srcs = [ + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface:case_data", "//validation/core/integration_test/architectural_unit_class_design/negative_missing_class:case_data", + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method:case_data", "//validation/core/integration_test/architectural_unit_class_design/negative_path_prefix:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design:case_data", "//validation/core/integration_test/architectural_unit_class_design/positive_refining_class:case_data", "//validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types:case_data", ], diff --git a/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs index 0601f2fb..1f9b693f 100644 --- a/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs +++ b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs @@ -80,6 +80,7 @@ fn collect_unit_design_class_fbs( fn assert_case(case_dir: &str) { let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let internal_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "internal_api"); let class_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "unit_design_class"); let architecture_path = case_file_path(&format!( "validation/core/integration_test/{SUITE_DIR}/{case_dir}/architecture.json" @@ -93,6 +94,7 @@ fn assert_case(case_dir: &str) { serde_json::json!({ "architecture": architecture_path.display().to_string(), "component_diagrams": component_fbs_paths, + "internal_api_diagrams": internal_api_fbs_paths, "unit_design_class_diagrams": unit_design_class_diagrams, }), ); @@ -117,7 +119,22 @@ fn negative_path_prefix() { assert_case("negative_path_prefix"); } +#[test] +fn negative_missing_bound_interface() { + assert_case("negative_missing_bound_interface"); +} + +#[test] +fn negative_missing_internal_api_method() { + assert_case("negative_missing_internal_api_method"); +} + #[test] fn negative_missing_class() { assert_case("negative_missing_class"); } + +#[test] +fn positive_internal_api_class_design() { + assert_case("positive_internal_api_class_design"); +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of 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", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml new file mode 100644 index 00000000..9fc694e1 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml @@ -0,0 +1,25 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: | + [Interface] Bound unit "safety_software_seooc_example.component_example.unit_1" has no corresponding interface "safety_software_seooc_example.component_example.InternalInterface" in its unit design. + Component interface : "safety_software_seooc_example.component_example.InternalInterface" + Bound unit : "safety_software_seooc_example.component_example.unit_1" + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Component source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml" + Component source line : 19 + Internal API source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml" + Internal API source line : 17 + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml" + Design source line : 18 + Fix : Declare interface "InternalInterface" in the unit design for "safety_software_seooc_example.component_example.unit_1" using the Internal API namespace. diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.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 +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml new file mode 100644 index 00000000..7b3ab28c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml new file mode 100644 index 00000000..a724306b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_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 +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of 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", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml new file mode 100644 index 00000000..f69fd42e --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Method] Unit design interface "safety_software_seooc_example.component_example.InternalInterface" is missing Internal API method(s) "GetData". + Component interface : "safety_software_seooc_example.component_example.InternalInterface" + Bound unit : "safety_software_seooc_example.component_example.unit_1" + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Detail interface : "safety_software_seooc_example.component_example.InternalInterface" + Component source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml" + Component source line : 19 + Internal API source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml" + Internal API source line : 17 + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml" + Design source line : 17 + Fix : Add "GetData" to the detail interface, matching the Internal API method names exactly. diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.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 +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml new file mode 100644 index 00000000..5b99e878 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} SetData(Data*): void + } + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml new file mode 100644 index 00000000..bcd3dbb1 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} SetData(Data*): void + } + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of 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", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/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/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.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 +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml new file mode 100644 index 00000000..a32454f7 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml new file mode 100644 index 00000000..a41cc8ab --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/src/profiles/dependable_element.rs b/validation/core/src/profiles/dependable_element.rs index f024c36b..ec562d0a 100644 --- a/validation/core/src/profiles/dependable_element.rs +++ b/validation/core/src/profiles/dependable_element.rs @@ -13,7 +13,7 @@ use crate::models::{ BazelArchitecture, BazelInput, ClassDiagramInputs, ClassEntityIndex, - ComponentDiagramArchitecture, ComponentDiagramInputs, + ComponentDiagramArchitecture, ComponentDiagramInputs, InternalApiIndex, }; use crate::readers::{BazelReader, ClassDiagramReader, ComponentDiagramReader}; use crate::validators::{ @@ -35,12 +35,15 @@ pub struct DependableElementInputs { component_diagrams: Vec, #[serde(default)] unit_design_class_diagrams: Option>>, + #[serde(default)] + internal_api_diagrams: Vec, } fn registered_validators<'a>( bazel: &'a Option, component: &'a Option, unit_design_classes: &'a Option>>, + internal_api: &'a Option, ) -> Vec> { vec![ Box::new(move || { @@ -57,6 +60,7 @@ fn registered_validators<'a>( bazel, component, unit_design_classes, + internal_api.as_ref(), )) }), ] @@ -74,6 +78,11 @@ pub fn run(inputs: &DependableElementInputs) -> Result { &mut result, |raw: ComponentDiagramInputs, errs| raw.to_diagram_architecture(errs), )?; + let internal_api = read_and_convert::( + inputs.internal_api_diagrams.as_slice(), + &mut result, + |raw: ClassDiagramInputs, _errs| InternalApiIndex::build_index(&raw), + )?; let unit_design_classes = match &inputs.unit_design_class_diagrams { Some(diagrams_by_label) => { let mut entity_ids_by_label = BTreeMap::new(); @@ -90,6 +99,13 @@ pub fn run(inputs: &DependableElementInputs) -> Result { .filter(|entity| is_unit_refinement_entity(entity.entity_type)) .map(|entity| UnitDesignEntity { id: entity.id.clone(), + entity_type: entity.entity_type, + method_names: entity + .methods + .iter() + .map(|method| method.name.clone()) + .filter(|name| !name.is_empty()) + .collect(), source_location: entity.source_location.clone(), }) .collect(); @@ -100,7 +116,7 @@ pub fn run(inputs: &DependableElementInputs) -> Result { None => None, }; - let validators = registered_validators(&bazel, &component, &unit_design_classes); + let validators = registered_validators(&bazel, &component, &unit_design_classes, &internal_api); let mut ran_validator = false; for validator in validators { diff --git a/validation/core/src/validators/architectural_unit_class_design_validator.rs b/validation/core/src/validators/architectural_unit_class_design_validator.rs index 85386867..7f3e789b 100644 --- a/validation/core/src/validators/architectural_unit_class_design_validator.rs +++ b/validation/core/src/validators/architectural_unit_class_design_validator.rs @@ -11,46 +11,282 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use source_location::SourceLocation; -use crate::models::{BazelArchitecture, ComponentDiagramArchitecture}; +use crate::models::{ + BazelArchitecture, ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, + InternalApiIndex, LogicComponentExt, +}; use crate::results::{Diagnostics, ErrorBuilder, ErrorCategory}; -use crate::validators::shared::{format_source_file, format_source_line}; +use crate::validators::shared::{format_quoted_list, format_source_file, format_source_line}; use crate::ValidationResult; #[derive(Clone, Debug)] pub(crate) struct UnitDesignEntity { pub id: String, + pub entity_type: class_diagram::EntityType, + pub method_names: Vec, pub source_location: SourceLocation, } +enum UnitDesignDiagnostic<'a> { + Refinement { + refinements: &'a [&'a UnitDesignEntity], + }, + BoundInterface { + api_interface: &'a crate::models::InternalApiInterface, + matching_interfaces: &'a [&'a UnitDesignEntity], + }, +} + /// Verify that every architectural unit is refined by an eligible entity whose /// ID is nested below the full architectural unit ID in the bound unit design. pub fn validate_architectural_unit_class_design( bazel: &BazelArchitecture, component: &ComponentDiagramArchitecture, design_entities_by_unit_label: &BTreeMap>, + internal_api: Option<&InternalApiIndex>, ) -> ValidationResult { let mut result = ValidationResult::default(); for (unit_key, architectural_unit) in &component.unit_set { - let Some(unit_label) = bazel.unit_set.get(unit_key) else { + let Some((unit_label, entities)) = + unit_design_context(unit_key, &bazel.unit_set, design_entities_by_unit_label) + else { // Bazel-component validation reports this independently. continue; }; - let entities = design_entities_by_unit_label - .get(unit_label) - .map(Vec::as_slice) - .unwrap_or(&[]); validate_unit(architectural_unit, unit_label, entities, &mut result); } + if let Some(internal_api) = internal_api { + validate_bound_interfaces_and_methods( + bazel, + component, + internal_api, + design_entities_by_unit_label, + &mut result, + ); + } + result } +fn unit_design_context<'a, K: Ord>( + unit_key: &K, + bazel_units: &'a BTreeMap, + design_entities_by_unit_label: &'a BTreeMap>, +) -> Option<(&'a str, &'a [UnitDesignEntity])> { + let unit_label = bazel_units.get(unit_key)?; + let entities = design_entities_by_unit_label + .get(unit_label) + .map(Vec::as_slice) + .unwrap_or(&[]); + Some((unit_label, entities)) +} + +fn validate_bound_interfaces_and_methods( + bazel: &BazelArchitecture, + component: &ComponentDiagramArchitecture, + internal_api: &InternalApiIndex, + design_entities_by_unit_label: &BTreeMap>, + result: &mut ValidationResult, +) { + let internal_interfaces: BTreeMap<&str, _> = internal_api + .interfaces() + .map(|interface| (interface.id.as_str(), interface)) + .collect(); + let component_interfaces: BTreeMap<&str, _> = component + .entities + .iter() + .filter(|entity| entity.is_interface() && entity.parent_id.is_some()) + .map(|interface| (interface.id.as_str(), interface)) + .collect(); + + for (unit_key, unit) in &component.unit_set { + let Some((unit_label, design_entities)) = + unit_design_context(unit_key, &bazel.unit_set, design_entities_by_unit_label) + else { + // The Bazel-component validator reports the missing Bazel mapping. + continue; + }; + + let bound_interface_targets: BTreeSet<&str> = unit + .relations + .iter() + .filter(|relation| { + relation.relation_type == ComponentRelationType::InterfaceBinding + && relation.source_role == EndpointRole::Required + }) + .map(|relation| relation.target.as_str()) + .collect(); + + for target in bound_interface_targets { + let Some(component_interface) = component_interfaces.get(target) else { + continue; + }; + let Some(api_interface) = internal_interfaces.get(target) else { + // The component-internal-API validator reports this mismatch. + continue; + }; + + validate_bound_unit_interface( + &unit.id, + unit_label, + component_interface, + api_interface, + design_entities, + result, + ); + } + } +} + +fn validate_bound_unit_interface( + unit_id: &str, + unit_label: &str, + component_interface: &crate::models::LogicComponent, + api_interface: &crate::models::InternalApiInterface, + design_entities: &[UnitDesignEntity], + result: &mut ValidationResult, +) { + let matching_interfaces: Vec<&UnitDesignEntity> = design_entities + .iter() + .filter(|entity| { + entity.entity_type == class_diagram::EntityType::Interface + && entity.id == api_interface.id + }) + .collect(); + + log_unit_design_diagnostics( + &mut result.diagnostics, + unit_id, + unit_label, + design_entities, + UnitDesignDiagnostic::BoundInterface { + api_interface, + matching_interfaces: &matching_interfaces, + }, + ); + + let Some(detail_interface) = matching_interfaces.first().copied() else { + result.add_failure(missing_interface_error( + unit_id, + unit_label, + &component_interface.id, + &component_interface.source_location, + api_interface, + design_entities, + )); + return; + }; + + // ClassEntityIndex rejects duplicate canonical IDs before entities reach + // this validator, so at most one exact interface can match here. + let detail_methods: BTreeSet<&str> = detail_interface + .method_names + .iter() + .map(String::as_str) + .collect(); + let missing_methods: BTreeSet<&str> = api_interface + .method_names + .iter() + .map(String::as_str) + .filter(|method| !detail_methods.contains(method)) + .collect(); + if !missing_methods.is_empty() { + result.add_failure(missing_methods_error( + unit_id, + unit_label, + &component_interface.id, + &component_interface.source_location, + api_interface, + detail_interface, + &missing_methods, + )); + } +} + +fn missing_interface_error( + unit_id: &str, + unit_label: &str, + interface_id: &str, + component_source: &SourceLocation, + api_interface: &crate::models::InternalApiInterface, + design_entities: &[UnitDesignEntity], +) -> String { + let mut error = ErrorBuilder::new(ErrorCategory::Interface) + .title(format!( + "Bound unit \"{unit_id}\" has no corresponding interface \"{interface_id}\" in its unit design" + )) + .field("component interface", format!("\"{interface_id}\"")) + .field("bound unit", format!("\"{unit_id}\"")) + .field("bazel unit", unit_label); + error = add_interface_source_fields(error, component_source, &api_interface.source_location); + let design_sources: BTreeSet<(String, u32)> = design_entities + .iter() + .map(|entity| entity.source_location.display()) + .collect(); + for (design_file, design_line) in design_sources { + error = error + .field("design source file", format!("\"{design_file}\"")) + .field("design source line", design_line.to_string()); + } + error + .fix(format!( + "declare interface \"{}\" in the unit design for \"{unit_id}\" using the Internal API namespace", + interface_id.rsplit('.').next().unwrap_or(interface_id) + )) + .build() +} + +fn missing_methods_error( + unit_id: &str, + unit_label: &str, + interface_id: &str, + component_source: &SourceLocation, + api_interface: &crate::models::InternalApiInterface, + detail_interface: &UnitDesignEntity, + missing_methods: &BTreeSet<&str>, +) -> String { + let methods = format_quoted_list(missing_methods.iter().copied()); + let (design_file, design_line) = detail_interface.source_location.display(); + let mut error = ErrorBuilder::new(ErrorCategory::Method) + .title(format!( + "Unit design interface \"{}\" is missing Internal API method(s) {methods}", + detail_interface.id + )) + .field("component interface", format!("\"{interface_id}\"")) + .field("bound unit", format!("\"{unit_id}\"")) + .field("bazel unit", unit_label) + .field("detail interface", format!("\"{}\"", detail_interface.id)); + error = add_interface_source_fields(error, component_source, &api_interface.source_location); + error + .field("design source file", format!("\"{design_file}\"")) + .field("design source line", design_line.to_string()) + .fix(format!( + "add {methods} to the detail interface, matching the Internal API method names exactly" + )) + .build() +} + +fn add_interface_source_fields( + error: ErrorBuilder, + component_source: &SourceLocation, + api_source: &SourceLocation, +) -> ErrorBuilder { + let (component_file, component_line) = component_source.display(); + let (api_file, api_line) = api_source.display(); + error + .field("component source file", format!("\"{component_file}\"")) + .field("component source line", component_line.to_string()) + .field("internal API source file", format!("\"{api_file}\"")) + .field("internal API source line", api_line.to_string()) +} + fn validate_unit( architectural_unit: &crate::models::LogicComponent, unit_label: &str, @@ -62,12 +298,14 @@ fn validate_unit( .filter(|entity| refines_unit(&entity.id, &architectural_unit.id)) .collect(); - log_unit_diagnostics( + log_unit_design_diagnostics( &mut result.diagnostics, &architectural_unit.id, unit_label, entities, - &refinements, + UnitDesignDiagnostic::Refinement { + refinements: &refinements, + }, ); if refinements.is_empty() { @@ -121,45 +359,89 @@ fn missing_refinement_error( .build() } -fn log_unit_diagnostics( +fn log_unit_design_diagnostics( diagnostics: &mut Diagnostics, unit_id: &str, unit_label: &str, entities: &[UnitDesignEntity], - refinements: &[&UnitDesignEntity], + context: UnitDesignDiagnostic<'_>, ) { diagnostics.debug(|| { - format!( - "Architectural unit: {}\nBazel unit: {}\nEligible entity identifiers: {}\nSelected refinements: {}", - unit_id, - unit_label, - display_entities(entities), - display_refinement_ids(refinements), - ) + match context { + UnitDesignDiagnostic::Refinement { refinements } => format!( + "Architectural unit: {}\nBazel unit: {}\nEligible unit-design entities: {}\nSelected refinements: {}", + unit_id, + unit_label, + display_entities(entities), + display_refinement_ids(refinements), + ), + UnitDesignDiagnostic::BoundInterface { + api_interface, + matching_interfaces, + } => { + let detail_interfaces = entities + .iter() + .filter(|entity| entity.entity_type == class_diagram::EntityType::Interface) + .map(|entity| { + format!( + "{} [{}]", + entity.id, + display_list(entity.method_names.iter().cloned()) + ) + }) + .collect::>(); + format!( + "Bound interface consistency:\nArchitectural unit: {}\nBazel unit: {}\nInternal API interface: {}\nRequired methods: {}\nDetail-design interfaces: {}\nExact matching interfaces: {}", + unit_id, + unit_label, + api_interface.id, + display_list(api_interface.method_names.iter().cloned()), + display_list(detail_interfaces), + display_entity_refs(matching_interfaces), + ) + } + } }); } fn display_entities(entities: &[UnitDesignEntity]) -> String { - display_list( - entities - .iter() - .map(|entity| { - let (file, line) = entity.source_location.display(); - format!("{} ({}:{})", entity.id, file, line) - }) - .collect(), + display_list(entities.iter().map(display_entity)) +} + +fn display_entity_refs(entities: &[&UnitDesignEntity]) -> String { + display_list(entities.iter().map(|entity| display_entity(entity))) +} + +fn display_entity(entity: &UnitDesignEntity) -> String { + let (file, line) = entity.source_location.display(); + format!( + "{} {:?} methods [{}] ({}:{})", + entity.id, + entity.entity_type, + display_list(entity.method_names.iter().cloned()), + file, + line, ) } fn display_refinement_ids(entities: &[&UnitDesignEntity]) -> String { - display_list(entities.iter().map(|entity| entity.id.clone()).collect()) + display_list(entities.iter().map(|entity| entity.id.clone())) } -fn display_list(items: Vec) -> String { - if items.is_empty() { +fn display_list(items: impl IntoIterator) -> String { + let mut output = String::new(); + let mut is_first = true; + for item in items { + if !is_first { + output.push_str(", "); + } + output.push_str(&item); + is_first = false; + } + if is_first { "".to_string() } else { - items.join(", ") + output } } @@ -167,9 +449,10 @@ fn display_list(items: Vec) -> String { mod tests { use super::*; use crate::models::{ - BazelEntityRef, BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentType, - LogicComponent, + BazelEntityRef, BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentRelationType, + ComponentType, EndpointRole, InternalApiIndex, LogicComponent, LogicRelation, }; + use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; use std::collections::BTreeMap; fn source_location() -> source_location::SourceLocation { @@ -245,6 +528,8 @@ mod tests { .iter() .map(|id| UnitDesignEntity { id: (*id).to_string(), + entity_type: class_diagram::EntityType::Class, + method_names: Vec::new(), source_location: source_location(), }) .collect(), @@ -257,7 +542,7 @@ mod tests { fn accepts_a_class_in_the_bound_unit_design_namespace() { let (bazel, component, classes) = inputs(&["system.control.Filter.FilterService"]); - let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); assert!( result.is_empty(), @@ -270,7 +555,7 @@ mod tests { fn rejects_a_class_from_another_namespace() { let (bazel, component, classes) = inputs(&["system.control.OtherUnit.Service"]); - let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); assert_eq!(result.failures.len(), 1); assert!(result.failures[0].contains( @@ -284,7 +569,7 @@ mod tests { fn rejects_a_class_with_the_same_unit_name_under_another_parent() { let (bazel, component, classes) = inputs(&["system.other.Filter.FilterService"]); - let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); assert_eq!(result.failures.len(), 1); } @@ -293,8 +578,458 @@ mod tests { fn rejects_a_lexical_but_not_segment_prefix() { let (bazel, component, classes) = inputs(&["system.control.Filtering.FilterService"]); - let result = validate_architectural_unit_class_design(&bazel, &component, &classes); + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); + + assert_eq!(result.failures.len(), 1); + } + + const INTERNAL_INTERFACE_ID: &str = + "safety_software_seooc_example.component_example.InternalInterface"; + + fn api_index() -> InternalApiIndex { + api_index_with_methods(&["GetData"]) + } + + fn api_index_with_methods(method_names: &[&str]) -> InternalApiIndex { + let source_location = source_location(); + InternalApiIndex::build_index(&[ClassDiagram { + name: "internal_api".to_string(), + entities: vec![SimpleEntity { + id: INTERNAL_INTERFACE_ID.to_string(), + name: "InternalInterface".to_string(), + enclosing_namespace_id: Some( + "safety_software_seooc_example.component_example".to_string(), + ), + stereotypes: Vec::new(), + entity_type: EntityType::Interface, + type_aliases: Vec::new(), + variables: Vec::new(), + methods: method_names + .iter() + .map(|name| Method { + name: (*name).to_string(), + return_type: None, + source_location: source_location.clone(), + visibility: Visibility::Public, + parameters: Vec::new(), + template_parameters: None, + modifiers: Vec::new(), + }) + .collect(), + template_parameters: None, + enum_literals: Vec::new(), + relationships: Vec::new(), + source_location, + }], + free_functions: Vec::new(), + }]) + } + + fn unit_label(name: &str) -> String { + format!("@//bazel/rules/rules_score/examples/seooc/{name}:{name}") + } + + fn bound_interface_inputs( + bindings: &[(&str, EndpointRole)], + ) -> ( + BazelArchitecture, + ComponentDiagramArchitecture, + BTreeMap>, + ) { + let mut bazel_units = BTreeMap::new(); + let mut component_units = BTreeMap::new(); + let mut designs = BTreeMap::new(); + + for (name, role) in bindings { + let unit_id = format!("safety_software_seooc_example.component_example.{name}"); + let key = (name.to_lowercase(), Some("component_example".to_string())); + let label = unit_label(name); + bazel_units.insert(key.clone(), label.clone()); + component_units.insert( + key, + LogicComponent { + id: unit_id, + name: Some((*name).to_string()), + alias: None, + parent_id: Some("safety_software_seooc_example.component_example".to_string()), + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations: vec![LogicRelation { + target: INTERNAL_INTERFACE_ID.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role: *role, + source_location: source_location(), + }], + source_location: source_location(), + }, + ); + designs.insert( + label, + vec![ + UnitDesignEntity { + id: INTERNAL_INTERFACE_ID.to_string(), + entity_type: EntityType::Interface, + method_names: vec!["GetData".to_string()], + source_location: source_location(), + }, + UnitDesignEntity { + id: format!( + "safety_software_seooc_example.component_example.{name}.UnitClass" + ), + entity_type: EntityType::Class, + method_names: Vec::new(), + source_location: source_location(), + }, + ], + ); + } + + let component = ComponentDiagramArchitecture { + seooc_set: BTreeMap::new(), + comp_set: BTreeMap::new(), + unit_set: component_units, + entities: vec![LogicComponent { + id: INTERNAL_INTERFACE_ID.to_string(), + name: Some("InternalInterface".to_string()), + alias: None, + parent_id: Some("safety_software_seooc_example.component_example".to_string()), + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + source_location: source_location(), + }], + }; + let bazel = BazelArchitecture { + seooc_set: BTreeMap::new(), + comp_set: BTreeMap::new(), + unit_set: bazel_units, + }; + (bazel, component, designs) + } + + #[test] + fn accepts_required_and_provided_units_when_the_required_design_is_complete() { + let (bazel, component, designs) = bound_interface_inputs(&[ + ("unit_1", EndpointRole::Required), + ("unit_2", EndpointRole::Provided), + ]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn does_not_require_provided_unit_design_to_declare_the_interface() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Provided)]); + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "provided-side interface declaration must not be checked: {:?}", + result.failures + ); + } + + #[test] + fn reports_when_any_bound_unit_lacks_the_interface() { + let (bazel, component, mut designs) = bound_interface_inputs(&[ + ("unit_1", EndpointRole::Provided), + ("unit_2", EndpointRole::Required), + ]); + designs + .get_mut(&unit_label("unit_2")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!( + result.failures[0].contains("safety_software_seooc_example.component_example.unit_2") + ); + assert!(result.failures[0].contains("no corresponding interface")); + } + + #[test] + fn reports_internal_api_method_missing_from_bound_unit_interface() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].method_names = + vec!["SetData".to_string()]; + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + assert!( + result.failures[0].contains("safety_software_seooc_example.component_example.unit_1") + ); + } + + #[test] + fn skips_internal_api_interface_without_unit_bindings() { + let (bazel, mut component, designs) = bound_interface_inputs(&[]); + component.entities.push(LogicComponent { + id: INTERNAL_INTERFACE_ID.to_string(), + name: Some("InternalInterface".to_string()), + alias: None, + parent_id: Some("example.service".to_string()), + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + source_location: source_location(), + }); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn requires_the_exact_canonical_interface_id() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].id = + "safety_software_seooc_example.component_example.other.InternalInterface".to_string(); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("no corresponding interface")); + } + + #[test] + fn accepts_additional_detail_interface_methods() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .push("LocalHelper".to_string()); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn compares_method_names_case_sensitively() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].method_names = + vec!["getdata".to_string()]; + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + } + + #[test] + fn reports_all_missing_api_methods_together() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .clear(); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index_with_methods(&["GetData", "SetData"])), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + assert!(result.failures[0].contains("SetData")); + } + + #[test] + fn ignores_units_without_interface_bindings() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + component + .unit_set + .values_mut() + .next() + .unwrap() + .relations + .clear(); + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn ignores_non_interface_binding_relations() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + component.unit_set.values_mut().next().unwrap().relations[0].relation_type = + ComponentRelationType::Dependency; + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn skips_component_interface_missing_from_internal_api() { + let (bazel, component, designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + let empty_internal_api = InternalApiIndex::build_index(&[]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&empty_internal_api), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn deduplicates_multiple_bindings_to_the_same_interface() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + // Add a second interface binding relation to the same interface + let unit = component.unit_set.values_mut().next().unwrap(); + unit.relations.push(LogicRelation { + target: INTERNAL_INTERFACE_ID.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role: EndpointRole::Provided, + source_location: source_location(), + }); + // Remove the interface from unit design to trigger failure + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + // Expect exactly 1 failure despite having 2 relations to the same interface + assert_eq!(result.failures.len(), 1); + } + + #[test] + fn deduplicates_overloaded_method_names_in_missing_methods_error() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .clear(); + + // Internal API with overloaded methods (same name) + let overloaded_api = api_index_with_methods(&["GetData", "GetData"]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&overloaded_api), + ); assert_eq!(result.failures.len(), 1); + let error = &result.failures[0]; + // Ensure "GetData" appears only once in the error title and fix message + assert_eq!(error.matches("\"GetData\"").count(), 2); // 1 in title, 1 in fix } } diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs index 0d9a037b..833f6ff2 100644 --- a/validation/core/src/validators/shared/helpers.rs +++ b/validation/core/src/validators/shared/helpers.rs @@ -43,9 +43,15 @@ pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> Strin return "".to_string(); } - names - .iter() - .map(|name| format!("\"{name}\"")) + format_quoted_list(names.iter().map(String::as_str)) +} + +pub(in crate::validators) fn format_quoted_list<'a>( + values: impl IntoIterator, +) -> String { + values + .into_iter() + .map(|value| format!("\"{value}\"")) .collect::>() .join(", ") } @@ -137,10 +143,11 @@ pub(in crate::validators) fn best_id_suggestion<'a>( #[cfg(test)] mod tests { use super::{ - best_id_suggestion, best_string_suggestion, earliest_source_by_id, - DEFAULT_SUGGESTION_THRESHOLD, + best_id_suggestion, best_string_suggestion, earliest_source_by_id, format_name_list, + format_quoted_list, DEFAULT_SUGGESTION_THRESHOLD, }; use source_location::SourceLocation; + use std::collections::BTreeSet; use strsim::jaro_winkler; #[test] @@ -148,6 +155,21 @@ mod tests { assert_eq!(best_string_suggestion("Service", std::iter::empty()), None); } + #[test] + fn format_quoted_list_preserves_order_and_duplicates() { + assert_eq!( + format_quoted_list(["second", "first", "second"]), + "\"second\", \"first\", \"second\"" + ); + } + + #[test] + fn format_name_list_keeps_sorted_order_and_empty_marker() { + let names = BTreeSet::from(["second".to_string(), "first".to_string()]); + assert_eq!(format_name_list(&names), "\"first\", \"second\""); + assert_eq!(format_name_list(&BTreeSet::new()), ""); + } + #[test] fn best_string_suggestion_accepts_exact_threshold_match() { let score = jaro_winkler("a", "baaa"); diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs index a5590727..66f33fa7 100644 --- a/validation/core/src/validators/shared/mod.rs +++ b/validation/core/src/validators/shared/mod.rs @@ -22,6 +22,6 @@ pub(in crate::validators) use diagram_analysis::{ }; pub(in crate::validators) use helpers::{ best_id_suggestion, best_string_suggestion, earliest_source_by_id, extract_method_name, - format_name_list, format_sequence_call, format_source_file, format_source_line, - intersect_interfaces, + format_name_list, format_quoted_list, format_sequence_call, format_source_file, + format_source_line, intersect_interfaces, };