Skip to content

Commit b2d8780

Browse files
codewarnab1jehuang
authored andcommitted
Fix index-out-of-bounds panic in build_subgraph_layouts when subgraphs have no positioned nodes
SubgraphTree indexes into graph.subgraphs (0..N), but the local subgraphs vec skips any subgraph whose nodes have no layout positions yet, causing the two index spaces to diverge. Fix: build a graph_to_local mapping during the filter pass, size all_descendants by graph.subgraphs.len() so tree indices are always valid, and translate through graph_to_local in the bounds-expansion loop, skipping filtered-out subgraphs. Regression test added: tests/fixtures/flowchart/subgraph_empty.mmd reproduces a flowchart with a subgraph that has no positioned nodes. (cherry picked from commit 1042029)
1 parent 97600a0 commit b2d8780

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

src/layout/subgraphs.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,8 @@ pub(super) fn build_subgraph_layouts(
13211321
config: &LayoutConfig,
13221322
) -> Vec<SubgraphLayout> {
13231323
let mut subgraphs = Vec::new();
1324+
// Maps graph.subgraphs index -> local subgraphs index (None if skipped).
1325+
let mut graph_to_local: Vec<Option<usize>> = Vec::with_capacity(graph.subgraphs.len());
13241326
for sub in &graph.subgraphs {
13251327
let mut min_x = f32::MAX;
13261328
let mut min_y = f32::MAX;
@@ -1337,6 +1339,7 @@ pub(super) fn build_subgraph_layouts(
13371339
}
13381340

13391341
if min_x == f32::MAX {
1342+
graph_to_local.push(None);
13401343
continue;
13411344
}
13421345

@@ -1360,6 +1363,7 @@ pub(super) fn build_subgraph_layouts(
13601363
let width = base_width.max(min_label_width);
13611364
let extra_width = width - base_width;
13621365

1366+
graph_to_local.push(Some(subgraphs.len()));
13631367
subgraphs.push(SubgraphLayout {
13641368
label: sub.label.clone(),
13651369
label_block,
@@ -1375,8 +1379,9 @@ pub(super) fn build_subgraph_layouts(
13751379

13761380
if subgraphs.len() > 1 {
13771381
let tree = SubgraphTree::build(graph);
1378-
let mut all_descendants: Vec<Vec<usize>> = vec![Vec::new(); subgraphs.len()];
1379-
let mut order: Vec<usize> = Vec::with_capacity(subgraphs.len());
1382+
let n = graph.subgraphs.len();
1383+
let mut all_descendants: Vec<Vec<usize>> = vec![Vec::new(); n];
1384+
let mut order: Vec<usize> = Vec::with_capacity(n);
13801385
let mut stack: Vec<(usize, bool)> =
13811386
tree.top_level.iter().rev().map(|&i| (i, false)).collect();
13821387
while let Some((idx, visited)) = stack.pop() {
@@ -1400,20 +1405,26 @@ pub(super) fn build_subgraph_layouts(
14001405
}
14011406

14021407
for &i in &order {
1408+
let Some(local_i) = graph_to_local[i] else {
1409+
continue;
1410+
};
14031411
for &j in &all_descendants[i] {
14041412
if is_region_subgraph(&graph.subgraphs[j]) {
14051413
continue;
14061414
}
1415+
let Some(local_j) = graph_to_local[j] else {
1416+
continue;
1417+
};
14071418
let pad = if graph.kind == crate::ir::DiagramKind::State {
14081419
(theme.font_size * 1.8).max(24.0)
14091420
} else {
14101421
12.0
14111422
};
14121423
let (child_x, child_y, child_w, child_h) = {
1413-
let child = &subgraphs[j];
1424+
let child = &subgraphs[local_j];
14141425
(child.x, child.y, child.width, child.height)
14151426
};
1416-
let parent = &mut subgraphs[i];
1427+
let parent = &mut subgraphs[local_i];
14171428
let min_x = parent.x.min(child_x - pad);
14181429
let min_y = parent.y.min(child_y - pad);
14191430
let max_x = (parent.x + parent.width).max(child_x + child_w + pad);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
flowchart TD
2+
subgraph ROW1["Input"]
3+
direction LR
4+
A["Node A"]
5+
B["Node B"]
6+
end
7+
subgraph ROW2["Processing"]
8+
direction LR
9+
C["Node C"]
10+
D["Node D"]
11+
end
12+
subgraph ROW3["Annotations"]
13+
direction LR
14+
NOTE1("This subgraph has only\nannotation nodes that\nhave no positioned peers")
15+
NOTE2("Another annotation\nwithout a main node")
16+
NOTE1 -.- NOTE2
17+
end
18+
A --> C
19+
B --> D
20+
NOTE1 -.- A

tests/layout_suite.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ fn render_all_fixtures() {
316316
"flowchart/styles.mmd",
317317
"flowchart/subgraph.mmd",
318318
"flowchart/subgraph_direction.mmd",
319+
"flowchart/subgraph_empty.mmd",
319320
"flowchart/cycles.mmd",
320321
"gantt/basic.mmd",
321322
"gitgraph/basic.mmd",

0 commit comments

Comments
 (0)