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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions bazel/rules/rules_score/docs/_assets/scope_check.puml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ package "dependable_element" as de {
}

note bottom of outofscope
Rejected: "Not in certified scope
//third_party/foo:__subpackages__,
stopping at other"
Scope violation:
Unit //pkg:MyUnit depends on //other/unrelated:thing,
but the dependency is not in any certified scope
(package '//other/unrelated' is not covered).
end note

@enduml
31 changes: 22 additions & 9 deletions bazel/rules/rules_score/docs/tool_reference/scope_check.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ patterns:
``scope`` is for dependencies that are *not* explicitly named as
``implementation`` targets themselves (e.g. third-party libraries pulled in
transitively) but are still known and accepted as part of the unit. Every
``unit`` and ``component`` also implicitly certifies its own explicitly named
targets (``implementation``, nested ``components``).
``unit`` also implicitly certifies its own ``implementation`` targets.
Components aggregate the scopes of their units and nested components.

Scopes are collected transitively: a ``component``'s certified scope is the
union of its own units' and nested components' scopes, and a
Expand All @@ -57,14 +57,26 @@ What is checked
For each ``unit``, an aspect (``cc_dependencies_aspect``) walks the
``deps`` / ``implementation_deps`` / ``exported_deps`` attributes of its
``implementation`` targets and collects every transitively reached label.
``dependable_element`` then checks each collected label against the tree of
certified scopes built from all ``scope`` declarations in the element: a
label is in scope if it (or an enclosing package / subpackage wildcard) was
declared somewhere. Any dependency that is not covered fails the check with:
``dependable_element`` checks each collected dependency label against the
combined certified scopes, including the units' own ``implementation``
targets. A dependency is covered if any of the following applies:

* Its exact target label is included.
* Its package is covered by a ``__pkg__`` declaration.
* Its package or an ancestor package is covered by a
``__subpackages__`` declaration.

All matches require the scope declaration and dependency to belong to
the same repository.

Uncovered dependencies are reported as warnings when the enclosing
``dependable_element`` has ``maturity = "development"``; otherwise they
fail the build. Each message identifies the originating unit and dependency:

.. code-block:: text

Not in certified scope <label>, stopping at <path segment>
Unit <unit label> depends on <dependency label>, but the dependency
is not in any certified scope (<reason>).

A certified scope that is declared more than once (e.g. by two units) is
also rejected:
Expand All @@ -80,9 +92,10 @@ Implementation

bazel/rules/rules_score/private/
├── cc_dependency_aspect.bzl # CcDependencyInfo: transitive labels reached from a cc target
├── unit.bzl # CertifiedScope(transitive_scopes = scope attr)
├── unit.bzl # CertifiedScope: scope attr + implementation targets
├── component.bzl # aggregates CertifiedScope + dependent_labels from children
└── dependable_element.bzl # builds the scope tree and validates dependent_labels against it
├── certified_scope.bzl # builds the repository/package index and collects scope violations
└── dependable_element.bzl # aggregates scopes and applies its maturity policy

See the ``CertifiedScope``, ``UnitInfo.dependent_labels`` and
``ComponentInfo.dependent_labels`` providers in
Expand Down
2 changes: 1 addition & 1 deletion bazel/rules/rules_score/examples/integrator/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ architectural_design(

unit(
name = "integrator_unit",
scope = ["//:__pkg__"],
scope = ["@seooc//:sample_library"],
tests = [],
unit_design = [],
implementation = [":integrator_lib"],
Expand Down
2 changes: 1 addition & 1 deletion bazel/rules/rules_score/examples/minimal/.bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.5.1
8.7.0
10 changes: 8 additions & 2 deletions bazel/rules/rules_score/examples/minimal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,16 @@ architectural_design(
static = ["docs/static_design.puml"],
)

cc_library(
name = "my_unit_hdr",
srcs = [],
hdrs = ["src/my_unit.h"],
)

cc_library(
name = "my_unit_lib",
srcs = ["src/my_unit.cpp"],
hdrs = ["src/my_unit.h"],
deps = [":my_unit_hdr"],
)

cc_test(
Expand All @@ -78,7 +84,7 @@ unit_design(

unit(
name = "MyUnit",
scope = ["//:my_unit_lib"],
scope = [":my_unit_hdr"],
tests = [":my_unit_test"],
unit_design = [":MyUnit_design"],
implementation = [":my_unit_lib"],
Expand Down
110 changes: 110 additions & 0 deletions bazel/rules/rules_score/private/certified_scope.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

"""Pure helpers for certified-scope validation."""

_PACKAGE_SCOPE = "__pkg__"
_SUBPACKAGES_SCOPE = "__subpackages__"

def build_scope_index(certified_scopes):
"""Index certified scopes by repository and full package path.

Args:
certified_scopes: Iterable of label strings or Label objects.

Returns:
Scope index keyed by repository and full package path.
"""
scope_index = {}
for certified_scope in certified_scopes:
certified_scope = Label(certified_scope)
repository = scope_index.setdefault(certified_scope.workspace_name, default = {})
package_scope = repository.setdefault(certified_scope.package, default = {
"targets": {},
"covers_package": False,
"covers_subpackages": False,
})
if certified_scope.name == _PACKAGE_SCOPE:
package_scope["covers_package"] = True
elif certified_scope.name == _SUBPACKAGES_SCOPE:
package_scope["covers_subpackages"] = True
else:
package_scope["targets"][certified_scope.name] = True

return scope_index

def find_scope_violation(scope_index, dependency):
"""Return the reason a dependency is outside ``scope_index``, if any.

Args:
scope_index: An index returned by ``build_scope_index``.
dependency: Dependency Label to match against the certified scope.

Returns:
A violation reason string, or ``None`` when the dependency is covered.
"""
repository = scope_index.get(dependency.workspace_name)
if repository == None:
return "repository {} is not covered".format(dependency.workspace_name or "<main>")

package_scope = repository.get(dependency.package)
if package_scope != None and (
package_scope["covers_package"] or dependency.name in package_scope["targets"]
):
return None

package_segments = dependency.package.split("/") if dependency.package else []
for end_index in range(len(package_segments) + 1):
ancestor_scope = repository.get("/".join(package_segments[:end_index]))
if ancestor_scope != None and ancestor_scope["covers_subpackages"]:
return None

if package_scope == None:
return "package '//{}' is not covered".format(dependency.package)

declared_targets = package_scope["targets"]
return "dependency '{}' is not certified in package '{}'; declared certified targets: {}".format(
dependency.name,
dependency.package,
", ".join(declared_targets) if declared_targets else "none",
)

def collect_scope_violations(scope_index, unit_dependency_closures):
"""Return unit-attributed messages for dependencies outside ``scope_index``.

Args:
scope_index: An index returned by ``build_scope_index``.
unit_dependency_closures: Iterable of structs with ``unit_label`` and
``dependencies`` (a depset of dependency Labels) fields.

Returns:
List of violation messages.
"""
violation_cache = {}
violations = []
for unit_dependency_closure in unit_dependency_closures:
for dependency in unit_dependency_closure.dependencies.to_list():
if dependency not in violation_cache:
violation_cache[dependency] = find_scope_violation(scope_index, dependency)

violation = violation_cache[dependency]
if violation:
violations.append(
"Unit {} depends on {}, but the dependency is not in any certified scope ({}).".format(
unit_dependency_closure.unit_label,
dependency,
violation,
),
)

return violations
77 changes: 23 additions & 54 deletions bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ load(
"CurrentArchitectureProviderInfo",
"collect_current_architecture_aspect",
)
load(
"//bazel/rules/rules_score/private:certified_scope.bzl",
"build_scope_index",
"collect_scope_violations",
)
load(
"//bazel/rules/rules_score/private:lobster_config.bzl",
"MERGE_LOBSTER_ITEMS_ATTR",
Expand Down Expand Up @@ -1073,6 +1078,17 @@ def _cond_names(*conditioned_names):
"""
return [name for condition, name in conditioned_names if condition]

def _apply_certification_violations(maturity, violations):
"""Apply the caller-owned maturity policy to one validation phase."""
if not violations:
return

if maturity == "development":
for violation in violations:
print("WARNING: " + violation)
else:
fail("\n".join(violations))

def _dependable_element_index_impl(ctx):
"""Generate index.rst file with references to all dependable element artifacts.

Expand Down Expand Up @@ -1229,6 +1245,8 @@ def _dependable_element_index_impl(ctx):

if comp_info.dependent_labels:
collected_dependent_labels.append(comp_info.dependent_labels)
elif UnitInfo in comp_target:
collected_dependent_labels.append(comp_target[UnitInfo].dependent_labels)
if CertifiedScope in comp_target:
collected_certified_scopes.append(comp_target[CertifiedScope].transitive_scopes)

Expand Down Expand Up @@ -1354,62 +1372,13 @@ def _dependable_element_index_impl(ctx):

# Verify that all transitive implementation dependencies are within the
# certified scope declared for this dependable element.
# @todo: Make this check aware of the repository (@foo) for example.
certified_scopes = depset(transitive = collected_certified_scopes).to_list()
tree = {}
for certified_scope in certified_scopes:
certified_scope = Label(certified_scope)
node = tree
path = certified_scope.package.split("/")
last_element = path.pop()
for path in path:
node = node.setdefault(path, default = {})

if type(node) == type([]):
if certified_scope.name in node:
msg = "The same scope is covered twice: {}".format(certified_scope)
if ctx.attr.maturity == "development":
print("WARNING: " + msg)
continue
fail(msg)
node.append(certified_scope.name)
else:
inserted_element = node.setdefault(last_element, default = [])
if certified_scope.name in inserted_element:
msg = "The same scope is covered twice: {}".format(certified_scope)
if ctx.attr.maturity == "development":
print("WARNING: " + msg)
continue
fail(msg)
inserted_element.append(certified_scope.name)

dependencies = depset(transitive = collected_dependent_labels).to_list()
for dep in dependencies:
node = tree
for path in dep.package.split("/"):
if type(node) == type([]):
if "__subpackages__" in node:
break
elif dep.name in node:
break
else:
msg = "Not in certified scope {}, stopping at {}".format(dep, path)
if ctx.attr.maturity == "development":
print("WARNING: " + msg)
else:
fail(msg)
break

child = node.get(path)
if child == None:
msg = "Not in certified scope {}, stopping at {}".format(dep, path)
if ctx.attr.maturity == "development":
print("WARNING: " + msg)
else:
fail(msg)
break
else:
node = child
scope_index = build_scope_index(certified_scopes)
_apply_certification_violations(
ctx.attr.maturity,
collect_scope_violations(scope_index, dependencies),
)

# Integrity-level check: a dependable element must not depend on elements
# with a lower integrity level than its own (D > C > B > A).
Expand Down
11 changes: 8 additions & 3 deletions bazel/rules/rules_score/private/unit.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ def _unit_impl(ctx):
xml_files + implementation_class_fbs + [validation_log.file],
transitive = [design_depset],
)
certified_scopes = [impl.label for impl in ctx.attr.implementation]
certified_scopes.extend([Label(scope) for scope in ctx.attr.scope])

return [
DefaultInfo(files = all_files),
CertifiedScope(transitive_scopes = depset(ctx.attr.scope)),
CertifiedScope(transitive_scopes = depset(certified_scopes)),
UnitInfo(
name = ctx.label.name,
design_name = ctx.attr.design_name or ctx.label.name,
Expand All @@ -155,7 +157,10 @@ def _unit_impl(ctx):
implementation_sequence_fbs = depset(),
implementation = depset(ctx.attr.implementation),
tests = tests_depset,
dependent_labels = depset(transitive = collected_dependent_labels),
dependent_labels = depset([struct(
unit_label = ctx.label,
dependencies = depset(transitive = collected_dependent_labels),
)]),
validation_log = validation_log,
),
SphinxSourcesInfo(
Expand Down Expand Up @@ -281,7 +286,7 @@ def unit(
name = name,
unit_design = unit_design,
implementation = implementation,
scope = scope,
scope = [str(native.package_relative_label(scope_entry)) for scope_entry in scope],
tests = tests,
design_name = design_name or "",
testonly = testonly,
Expand Down
4 changes: 2 additions & 2 deletions bazel/rules/rules_score/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ UnitInfo = provider(
"implementation_sequence_fbs": "Depset of FlatBuffers binaries generated from implementation sequence diagrams.",
"implementation": "Depset of implementation targets (cc_library, rust_library, etc.).",
"tests": "Depset of test targets (cc_test, rust_test, etc.).",
"dependent_labels": "Depset of Labels that this unit's implementation depends on transitively (used for certification scope validation).",
"dependent_labels": "Depset containing one struct for this unit with its label and a depset of transitive implementation dependency labels.",
"validation_log": "Struct with file and name fields describing the UnitValidate (design-vs-implementation) log entry produced by this unit target.",
},
)
Expand Down Expand Up @@ -178,7 +178,7 @@ ComponentInfo = provider(
"components": "Depset of nested component and/or unit Targets that comprise this component.",
"tests": "Depset of test traceability files (.lobster) generated from unit test results, collected transitively from all nested components and units.",
"architecture": "Depset of architecture traceability files (.lobster) generated from unit architectural designs, collected transitively from all nested components and units.",
"dependent_labels": "Depset of Labels that this component's implementation depends on transitively (collected from all nested units and components, used for certification scope validation).",
"dependent_labels": "Depset of per-unit structs containing each unit label and its transitive implementation dependency depset, collected from nested units and components for certification scope validation.",
},
)

Expand Down
2 changes: 1 addition & 1 deletion bazel/rules/rules_score/test/.bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.5.1
8.7.0
Loading
Loading