diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 25dd853d..c0ceccba 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -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( diff --git a/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/output.json b/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/output.json new file mode 100644 index 00000000..3d132518 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/output.json @@ -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": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/port_global_name_resolution.puml b/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/port_global_name_resolution.puml new file mode 100644 index 00000000..748fc88a --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/port_global_name_resolution/port_global_name_resolution.puml @@ -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 <> { + portout out1 + } + component "ServerComp" as ServerComp <> { + portin in1 + } +} + +ClientComp --> in1 : calls + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/top_level_port/output.json b/plantuml/parser/integration_test/component_diagram/top_level_port/output.json new file mode 100644 index 00000000..b302cf8b --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/top_level_port/output.json @@ -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" + } + ] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/top_level_port/top_level_port.puml b/plantuml/parser/integration_test/component_diagram/top_level_port/top_level_port.puml new file mode 100644 index 00000000..431b5231 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/top_level_port/top_level_port.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 + +' 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 <> { + portout out1 +} + +out1 --> ExternalAPI : sends + +@enduml diff --git a/plantuml/parser/puml_cli/src/main.rs b/plantuml/parser/puml_cli/src/main.rs index 140b310a..e790c822 100644 --- a/plantuml/parser/puml_cli/src/main.rs +++ b/plantuml/parser/puml_cli/src/main.rs @@ -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; @@ -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)] @@ -193,18 +193,7 @@ fn main() -> Result<(), Box> { } } 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()); } } } diff --git a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs index 96c8d578..c112bbb4 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs @@ -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, @@ -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()); } } @@ -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) = @@ -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 diff --git a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs index 4c199e54..169a71ce 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs @@ -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"); diff --git a/validation/core/src/main.rs b/validation/core/src/main.rs index 2cb4bac6..31a5156e 100644 --- a/validation/core/src/main.rs +++ b/validation/core/src/main.rs @@ -44,6 +44,11 @@ struct Args { #[arg(long)] output: Option, + + /// 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 { @@ -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, String> { @@ -117,6 +127,7 @@ fn resolve_validators(context: &ValidationContext) -> Result, + warn_on_errors: bool, validators: &[SelectedValidator], context: &mut ValidationContext, ) -> Result<(), String> { @@ -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 { @@ -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)?; } @@ -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) + } } }