Skip to content

Commit aa322a9

Browse files
hoe-jocastler
authored andcommitted
[plantuml parser]: fix port resolution
1 parent 86b0a6b commit aa322a9

6 files changed

Lines changed: 153 additions & 16 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"port_global_name_resolution.puml": {
3+
"SampleSEooC": {
4+
"id": "SampleSEooC",
5+
"name": "SampleSEooC",
6+
"alias": "SampleSEooC",
7+
"parent_id": null,
8+
"comp_type": "Package",
9+
"stereotype": null,
10+
"relations": []
11+
},
12+
"SampleSEooC.ClientComp": {
13+
"id": "SampleSEooC.ClientComp",
14+
"name": "ClientComp",
15+
"alias": "ClientComp",
16+
"parent_id": "SampleSEooC",
17+
"comp_type": "Component",
18+
"stereotype": "component",
19+
"relations": [
20+
{
21+
"target": "SampleSEooC.ServerComp",
22+
"annotation": "calls",
23+
"relation_type": "None"
24+
}
25+
]
26+
},
27+
"SampleSEooC.ServerComp": {
28+
"id": "SampleSEooC.ServerComp",
29+
"name": "ServerComp",
30+
"alias": "ServerComp",
31+
"parent_id": "SampleSEooC",
32+
"comp_type": "Component",
33+
"stereotype": "component",
34+
"relations": []
35+
}
36+
}
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
' *******************************************************************************
2+
' Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
'
4+
' See the NOTICE file(s) distributed with this work for additional
5+
' information regarding copyright ownership.
6+
'
7+
' This program and the accompanying materials are made available under the
8+
' terms of the Apache License Version 2.0 which is available at
9+
' https://www.apache.org/licenses/LICENSE-2.0
10+
'
11+
' SPDX-License-Identifier: Apache-2.0
12+
' *******************************************************************************
13+
@startuml
14+
15+
' Test: a top-level relation references a deeply nested port by its simple alias.
16+
' The resolver must lift the endpoint to the port's parent component (ServerComp),
17+
' not fail with UnresolvedReference.
18+
19+
package "SampleSEooC" as SampleSEooC {
20+
component "ClientComp" as ClientComp <<component>> {
21+
portout out1
22+
}
23+
component "ServerComp" as ServerComp <<component>> {
24+
portin in1
25+
}
26+
}
27+
28+
ClientComp --> in1 : calls
29+
30+
@enduml
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"top_level_port.puml": {
3+
"ExternalAPI": {
4+
"id": "ExternalAPI",
5+
"name": "ExternalAPI",
6+
"alias": "ExternalAPI",
7+
"parent_id": null,
8+
"comp_type": "Interface",
9+
"stereotype": null,
10+
"relations": []
11+
},
12+
"ClientComp": {
13+
"id": "ClientComp",
14+
"name": "ClientComp",
15+
"alias": "ClientComp",
16+
"parent_id": null,
17+
"comp_type": "Component",
18+
"stereotype": "component",
19+
"relations": [
20+
{
21+
"target": "ExternalAPI",
22+
"annotation": "sends",
23+
"relation_type": "None"
24+
}
25+
]
26+
}
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
' *******************************************************************************
2+
' Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
'
4+
' See the NOTICE file(s) distributed with this work for additional
5+
' information regarding copyright ownership.
6+
'
7+
' This program and the accompanying materials are made available under the
8+
' terms of the Apache License Version 2.0 which is available at
9+
' https://www.apache.org/licenses/LICENSE-2.0
10+
'
11+
' SPDX-License-Identifier: Apache-2.0
12+
' *******************************************************************************
13+
@startuml
14+
15+
' Test: a top-level `interface` is a first-class entity (comp_type = Interface).
16+
' A `portout` inside a component is a connector; the relation `out1 --> ExternalAPI`
17+
' resolves `out1` to its parent ClientComp (port lifting), producing a component-to-
18+
' interface relation. Top-level bare portout/portin/port declarations are ignored.
19+
20+
interface "ExternalAPI" as ExternalAPI
21+
22+
component "ClientComp" as ClientComp <<component>> {
23+
portout out1
24+
}
25+
26+
out1 --> ExternalAPI : sends
27+
28+
@enduml

plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ impl ComponentResolver {
8181
None
8282
}
8383

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

99-
// Search one level deeper for each component that has this scope as parent
99+
// Search at any depth below the current scope: a port whose simple alias matches
100+
// and whose parent component is a descendant of (or equal to) the current scope.
101+
let scope_prefix = scope.join(".");
100102
for (pfqn, parent_comp) in port_parents {
101103
let parts: Vec<&str> = pfqn.split('.').collect();
102-
if parts.last() == Some(&port_local)
103-
&& parts.len() > 1
104-
&& parts[..parts.len() - 2].join(".") == scope.join(".")
105-
{
104+
if parts.last() != Some(&port_local) {
105+
continue;
106+
}
107+
let is_in_scope = scope.is_empty()
108+
|| parent_comp == &scope_prefix
109+
|| parent_comp.starts_with(&format!("{scope_prefix}."));
110+
if is_in_scope {
106111
return Some(parent_comp.clone());
107112
}
108113
}
@@ -125,8 +130,9 @@ impl ComponentResolver {
125130
return Ok(comp.id.clone());
126131
}
127132
}
128-
// Fallback: check if it's a port name and lift to parent component
129-
// Search upward through scope levels AND through any nested component scope
133+
// Fallback: check if it's a port name and lift to parent component.
134+
// Search upward through scope levels — the innermost scope that contains a
135+
// port with this alias wins (nearest-scope-first).
130136
for i in (0..=self.scope.len()).rev() {
131137
let outer_scope = &self.scope[..i];
132138
if let Some(parent_fqn) =
@@ -209,15 +215,13 @@ impl ComponentResolver {
209215
let local_id = port.alias.as_deref().unwrap_or(&port.name);
210216
let fqn = self.make_fqn(local_id);
211217

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

223227
/// After all statements are visited, replace any relation endpoint that is a

plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ fn test_together_with_relation() {
129129
run_component_resolver_case("together_with_relation");
130130
}
131131

132+
#[test]
133+
fn test_port_global_name_resolution() {
134+
run_component_resolver_case("port_global_name_resolution");
135+
}
136+
137+
#[test]
138+
fn test_top_level_port() {
139+
run_component_resolver_case("top_level_port");
140+
}
141+
132142
#[test]
133143
fn test_port_deep_nesting() {
134144
run_component_resolver_case("port_deep_nesting");

0 commit comments

Comments
 (0)