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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ def _run_validation(ctx, arch_json, static_fbs_files):
validation_args.add("--architecture-json", arch_json)
validation_args.add_all("--component-fbs", static_fbs_files)
validation_args.add("--output", validation_log)
if ctx.attr.maturity == "development":
validation_args.add("--warn-on-errors")

# ctx.actions.run will fail the build if validation_cli returns non-zero exit code
ctx.actions.run(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"port_global_name_resolution.puml": {
"SampleSEooC": {
"id": "SampleSEooC",
"name": "SampleSEooC",
"alias": "SampleSEooC",
"parent_id": null,
"comp_type": "Package",
"stereotype": null,
"relations": []
},
"SampleSEooC.ClientComp": {
"id": "SampleSEooC.ClientComp",
"name": "ClientComp",
"alias": "ClientComp",
"parent_id": "SampleSEooC",
"comp_type": "Component",
"stereotype": "component",
"relations": [
{
"target": "SampleSEooC.ServerComp",
"annotation": "calls",
"relation_type": "None"
}
]
},
"SampleSEooC.ServerComp": {
"id": "SampleSEooC.ServerComp",
"name": "ServerComp",
"alias": "ServerComp",
"parent_id": "SampleSEooC",
"comp_type": "Component",
"stereotype": "component",
"relations": []
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
' *******************************************************************************
' 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

' Test: a top-level relation references a deeply nested port by its simple alias.
' The resolver must lift the endpoint to the port's parent component (ServerComp),
' not fail with UnresolvedReference.

package "SampleSEooC" as SampleSEooC {
component "ClientComp" as ClientComp <<component>> {
portout out1
}
component "ServerComp" as ServerComp <<component>> {
portin in1
}
}

ClientComp --> in1 : calls

@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"top_level_port.puml": {
"ExternalAPI": {
"id": "ExternalAPI",
"name": "ExternalAPI",
"alias": "ExternalAPI",
"parent_id": null,
"comp_type": "Interface",
"stereotype": null,
"relations": []
},
"ClientComp": {
"id": "ClientComp",
"name": "ClientComp",
"alias": "ClientComp",
"parent_id": null,
"comp_type": "Component",
"stereotype": "component",
"relations": [
{
"target": "ExternalAPI",
"annotation": "sends",
"relation_type": "None"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
' *******************************************************************************
' Copyright (c) 2026 Contributors to the Eclipse Foundation
'
' See the NOTICE file(s) distributed with this work for additional
' information regarding copyright ownership.
'
' This program and the accompanying materials are made available under the
' terms of the Apache License Version 2.0 which is available at
' https://www.apache.org/licenses/LICENSE-2.0
'
' SPDX-License-Identifier: Apache-2.0
' *******************************************************************************
@startuml

' Test: a top-level `interface` is a first-class entity (comp_type = Interface).
' A `portout` inside a component is a connector; the relation `out1 --> ExternalAPI`
' resolves `out1` to its parent ClientComp (port lifting), producing a component-to-
' interface relation. Top-level bare portout/portin/port declarations are ignored.

interface "ExternalAPI" as ExternalAPI

component "ClientComp" as ClientComp <<component>> {
portout out1
}

out1 --> ExternalAPI : sends

@enduml
17 changes: 3 additions & 14 deletions plantuml/parser/puml_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use clap::{ArgGroup, Parser, ValueEnum};
use env_logger::Builder;
use log::{debug, error, warn};
use log::debug;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;
Expand All @@ -29,7 +29,7 @@ use puml_resolver::{
ClassResolver, ComponentResolver, DiagramResolver, SequenceResolver, SequenceTree,
};
use puml_serializer::{ClassSerializer, ComponentSerializer};
use puml_utils::{write_fbs_to_file, write_json_to_file, write_placeholder_file, LogLevel};
use puml_utils::{write_fbs_to_file, write_json_to_file, LogLevel};

/// CLI wrapper for LogLevel that implements ValueEnum
#[derive(Copy, Clone, ValueEnum, Debug)]
Expand Down Expand Up @@ -193,18 +193,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
Err(e) => {
error!("Resolve error in {}: {}", path.display(), e);
warn!(
"Skipping file due to unimplemented diagram type: {}",
path.display()
);
// Create empty placeholder files so the build continues
if let Some(ref dir) = fbs_output_dir {
write_placeholder_file(path, dir)?;
}
if let Some(ref ldir) = lobster_output_dir {
write_lobster_to_file(LobsterModel::Empty, path, ldir)?;
}
return Err(format!("Resolve error in {}: {}", path.display(), e).into());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ impl ComponentResolver {
None
}

// Helper: recursively search for a port by local name within the given scope and its children,
// returning the port's parent component FQN when found.
// Helper: search for a port by local name within the given scope and any of its
// descendants, returning the port's parent component FQN when found.
fn find_port_in_scope_or_children(
scope: &[String],
port_local: &str,
Expand All @@ -96,13 +96,18 @@ impl ComponentResolver {
return Some(parent_fqn.clone());
}

// Search one level deeper for each component that has this scope as parent
// Search at any depth below the current scope: a port whose simple alias matches
// and whose parent component is a descendant of (or equal to) the current scope.
let scope_prefix = scope.join(".");
for (pfqn, parent_comp) in port_parents {
let parts: Vec<&str> = pfqn.split('.').collect();
if parts.last() == Some(&port_local)
&& parts.len() > 1
&& parts[..parts.len() - 2].join(".") == scope.join(".")
{
if parts.last() != Some(&port_local) {
continue;
}
let is_in_scope = scope.is_empty()
|| parent_comp == &scope_prefix
|| parent_comp.starts_with(&format!("{scope_prefix}."));
if is_in_scope {
return Some(parent_comp.clone());
}
}
Expand All @@ -125,8 +130,9 @@ impl ComponentResolver {
return Ok(comp.id.clone());
}
}
// Fallback: check if it's a port name and lift to parent component
// Search upward through scope levels AND through any nested component scope
// Fallback: check if it's a port name and lift to parent component.
// Search upward through scope levels — the innermost scope that contains a
// port with this alias wins (nearest-scope-first).
for i in (0..=self.scope.len()).rev() {
let outer_scope = &self.scope[..i];
if let Some(parent_fqn) =
Expand Down Expand Up @@ -209,15 +215,13 @@ impl ComponentResolver {
let local_id = port.alias.as_deref().unwrap_or(&port.name);
let fqn = self.make_fqn(local_id);

// Record port_fqn -> parent_fqn for relation lifting
if let Some(parent_fqn) = if self.scope.is_empty() {
None
if self.scope.is_empty() {
// Top-level ports are pure connectors/aliases, not entities — ignore them.
// Use `interface` to declare a top-level interface as a first-class entity.
} else {
Some(self.scope.join("."))
} {
self.port_parents.insert(fqn, parent_fqn);
// Nested port: record port_fqn -> parent_fqn for relation lifting.
self.port_parents.insert(fqn, self.scope.join("."));
}
// Ports at top-level (no parent) are simply ignored
}

/// After all statements are visited, replace any relation endpoint that is a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ fn test_together_with_relation() {
run_component_resolver_case("together_with_relation");
}

#[test]
fn test_port_global_name_resolution() {
run_component_resolver_case("port_global_name_resolution");
}

#[test]
fn test_top_level_port() {
run_component_resolver_case("top_level_port");
}

#[test]
fn test_port_deep_nesting() {
run_component_resolver_case("port_deep_nesting");
Expand Down
28 changes: 24 additions & 4 deletions validation/core/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ struct Args {

#[arg(long)]
output: Option<String>,

/// When set, validation errors are printed as warnings and the tool exits
/// with code 0. Intended for use during development (maturity=development).
#[arg(long, default_value_t = false)]
warn_on_errors: bool,
}

struct ValidationCliInputs {
Expand Down Expand Up @@ -95,7 +100,12 @@ fn run(args: Args) -> Result<(), String> {
let mut context = build_validation_context(inputs)?;
let validators = resolve_validators(&context)?;

run_selected_validators(args.output.as_deref(), &validators, &mut context)
run_selected_validators(
args.output.as_deref(),
args.warn_on_errors,
&validators,
&mut context,
)
}

fn resolve_validators(context: &ValidationContext) -> Result<Vec<SelectedValidator>, String> {
Expand All @@ -117,6 +127,7 @@ fn resolve_validators(context: &ValidationContext) -> Result<Vec<SelectedValidat

fn run_selected_validators(
output_path: Option<&str>,
warn_on_errors: bool,
validators: &[SelectedValidator],
context: &mut ValidationContext,
) -> Result<(), String> {
Expand All @@ -126,7 +137,7 @@ fn run_selected_validators(
merge_errors(&mut errors, run_validator(*validator, context));
}

finish_validation(output_path, &errors)
finish_validation(output_path, warn_on_errors, &errors)
}

fn run_validator(validator: SelectedValidator, context: &ValidationContext) -> Errors {
Expand Down Expand Up @@ -195,7 +206,11 @@ fn merge_errors(target: &mut Errors, incoming: Errors) {
}
}

fn finish_validation(output_path: Option<&str>, errors: &Errors) -> Result<(), String> {
fn finish_validation(
output_path: Option<&str>,
warn_on_errors: bool,
errors: &Errors,
) -> Result<(), String> {
if let Some(path) = output_path {
write_log(path, errors)?;
}
Expand All @@ -215,7 +230,12 @@ fn finish_validation(output_path: Option<&str>, errors: &Errors) -> Result<(), S
errors.messages.len(),
details
);
Err(output)
if warn_on_errors {
eprintln!("WARNING: {output}");
Ok(())
} else {
Err(output)
}
}
}

Expand Down
Loading