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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions validation/core/docs/requirements/tool_requirements.trlc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ section "Tool Requirements" {
satisfied_by = Verifier
}

ToolQualification.ToolRequirement BazelComponentNonEmptyComposition {
description = '''The validator shall report an error when a
Bazel `dependable_element` or `component` target decomposes
into zero nested `component` or `unit` targets, even when the
corresponding PlantUML entity likewise shows no nested
elements.'''
derived_from = [UseCases.Validate_Architecture_Specification_Documents]
satisfied_by = Verifier
}

}

section "Component Sequence Validator" {
Expand Down
21 changes: 21 additions & 0 deletions validation/core/docs/specifications/bazel_component.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ the dependable element alias as parent. More deeply nested components use their
immediate enclosing component alias as parent.
*(Requirements: {requirement:downstream-ref}`Tools.BazelComponentNameCaseInsensitive`, {requirement:downstream-ref}`Tools.BazelComponentParentContext`)*

### Non-Empty Composition

Every Bazel `dependable_element` and `component` target must decompose into
at least one nested `component` or `unit`. A target with an empty
`components` attribute (and, for components, no nested `unit`) is reported
even if the PlantUML diagram agrees and shows the same element as empty --
the set-difference checks above only compare presence between Bazel and
PlantUML, so a childless entry that matches on both sides would otherwise go
unnoticed.
*(Requirement: {requirement:downstream-ref}`Tools.BazelComponentNonEmptyComposition`)*

```starlark
# Flagged: component_example has no nested unit() or component().
component(
name = "component_example",
components = [],
)
```

## Failure Cases

| Failure case | Validation rule |
Expand All @@ -107,6 +126,8 @@ immediate enclosing component alias as parent.
| Extra component in PlantUML | Component Consistency |
| Missing unit in PlantUML | Unit Consistency |
| Extra unit in PlantUML | Unit Consistency |
| Empty dependable element in Bazel build graph | Non-Empty Composition |
| Empty component in Bazel build graph | Non-Empty Composition |

## PlantUML Stereotype Reference

Expand Down
1 change: 1 addition & 0 deletions validation/core/integration_test/bazel_component/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ load("@rules_rust//rust:defs.bzl", "rust_test")
filegroup(
name = "bazel_component_test_data",
srcs = [
"//validation/core/integration_test/bazel_component/negative_empty_component:case_data",
"//validation/core/integration_test/bazel_component/negative_extra_component:case_data",
"//validation/core/integration_test/bazel_component/negative_extra_unit:case_data",
"//validation/core/integration_test/bazel_component/negative_missing_component:case_data",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@ fn negative_extra_component_suite_case() {
fn negative_wrong_stereotype_suite_case() {
assert_case("negative_wrong_stereotype");
}

#[test]
fn negative_empty_component_suite_case() {
assert_case("negative_empty_component");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design")
load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle")

architectural_design(
name = "design",
static = ["component_diagram.puml"],
visibility = ["//visibility:private"],
)

provider_fbs_fixture_bundle(
name = "fbs",
visibility = ["//visibility:public"],
deps = [":design"],
)

filegroup(
name = "case_data",
srcs = [
"architecture.json",
"expected.json",
":fbs",
],
visibility = ["//visibility:public"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"components": {
"safety_software_seooc_example": {
"units": [],
"components": [
"@//bazel/rules/rules_score/examples/seooc:component_example"
]
},
"@//bazel/rules/rules_score/examples/seooc:component_example": {
"units": [],
"components": []
}
}
}
Original file line number Diff line number Diff line change
@@ -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 component_diagram

package "Sample Seooc" as safety_software_seooc_example <<SEooC>> {
component "Component Example" as component_example <<component>> {
}
}

@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"should_pass": false,
"error_contains": [
"Empty component in Bazel build graph",
"Alias : \"component_example\""
]
}
113 changes: 112 additions & 1 deletion validation/core/src/validators/bazel_component_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
//! [`BazelComponentValidator`] performs a two-way set-difference between a
//! [`BazelArchitecture`] and a [`ComponentDiagramArchitecture`].

use std::collections::BTreeSet;

use crate::models::{BazelArchitecture, ComponentDiagramArchitecture};
use crate::{Diagnostics, ValidationResult};

Expand Down Expand Up @@ -55,6 +57,7 @@ impl BazelComponentValidator {
self.check_seooc(bazel, diagram);
self.check_components(bazel, diagram);
self.check_units(bazel, diagram);
self.check_non_empty_composition(bazel);
self.result
}

Expand Down Expand Up @@ -149,6 +152,52 @@ impl BazelComponentValidator {
}
}

/// Verify every Bazel `dependable_element`/`component` target decomposes
/// into at least one nested `component` or `unit`.
///
/// A childless entry (both `units` and `components` empty in the
/// architecture JSON) can silently agree with an equally childless
/// PlantUML entity, so the set-difference checks above never flag it.
/// This scans the Bazel side directly instead, since it reflects what is
/// actually built.
fn check_non_empty_composition(&mut self, bazel: &BazelArchitecture) {
let mut parents_with_children: BTreeSet<String> = BTreeSet::new();
for (_, parent) in bazel.comp_set.keys() {
if let Some(parent) = parent {
parents_with_children.insert(parent.clone());
}
}
for (_, parent) in bazel.unit_set.keys() {
if let Some(parent) = parent {
parents_with_children.insert(parent.clone());
}
}

for (key, label) in &bazel.seooc_set {
let (name, _) = key;
if !parents_with_children.contains(name) {
self.result
.add_failure(Self::format_empty("dependable element", name, label));
}
}
for (key, label) in &bazel.comp_set {
let (name, _) = key;
if !parents_with_children.contains(name) {
self.result
.add_failure(Self::format_empty("component", name, label));
}
}
}

fn format_empty(display_type: &str, name: &str, label: &str) -> String {
format!(
"Empty {display_type} in Bazel build graph:\n\
Alias : \"{name}\"\n\
Bazel label : {label}\n\
Required : A {display_type} must decompose into at least one nested unit or component; add one or remove the empty {display_type}",
)
}

fn format_missing(
display_type: &str,
stereotype: &str,
Expand Down Expand Up @@ -525,11 +574,73 @@ mod tests {
fn test_entity_without_alias_uses_id_as_key() {
let arch = make_arch(vec![
("my_de", vec![], vec!["@//pkg:comp_a"]),
("@//pkg:comp_a", vec![], vec![]),
("@//pkg:comp_a", vec!["@//pkg/u1:unit_1"], vec![]),
]);
let diagram = diagram(vec![
entity("my_de", None, None, Some("SEooC")),
entity("comp_a", None, Some("my_de"), Some("component")),
entity("unit_1", None, Some("comp_a"), Some("unit")),
]);
let errs = run_arch_validation(&arch, &diagram);
assert!(errs.is_empty(), "Expected pass, got: {:?}", errs.failures);
}

#[test]
fn test_empty_component_detected() {
let arch = make_arch(vec![
("my_de", vec![], vec!["@//pkg:comp_a"]),
("@//pkg:comp_a", vec![], vec![]),
]);
let diagram = diagram(vec![
entity("MyDE", Some("my_de"), None, Some("SEooC")),
entity("CompA", Some("comp_a"), Some("MyDE"), Some("component")),
]);
let errs = run_arch_validation(&arch, &diagram);
assert!(
errs.failures.iter().any(|m| m.contains("Empty component")),
"Expected empty component error, got: {:?}",
errs.failures
);
}

#[test]
fn test_empty_dependable_element_detected() {
let arch = make_arch(vec![("my_de", vec![], vec![])]);
let diagram = diagram(vec![entity("MyDE", Some("my_de"), None, Some("SEooC"))]);
let errs = run_arch_validation(&arch, &diagram);
assert!(
errs.failures
.iter()
.any(|m| m.contains("Empty dependable element")),
"Expected empty dependable element error, got: {:?}",
errs.failures
);
}

#[test]
fn test_component_with_nested_component_not_flagged_empty() {
// "bindings" has no direct units but decomposes into a nested
// component ("lola_component") - it must not be reported as empty.
let arch = make_arch(vec![
("my_de", vec![], vec!["@//pkg:bindings"]),
("@//pkg:bindings", vec![], vec!["@//pkg:lola_component"]),
("@//pkg:lola_component", vec!["@//pkg/u1:unit_1"], vec![]),
]);
let diagram = diagram(vec![
entity("MyDE", Some("my_de"), None, Some("SEooC")),
entity(
"Bindings",
Some("bindings"),
Some("MyDE"),
Some("component"),
),
entity(
"LolaComponent",
Some("lola_component"),
Some("Bindings"),
Some("component"),
),
entity("Unit1", Some("unit_1"), Some("LolaComponent"), Some("unit")),
]);
let errs = run_arch_validation(&arch, &diagram);
assert!(errs.is_empty(), "Expected pass, got: {:?}", errs.failures);
Expand Down
Loading