diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index f3c0d2a77..7eb081d5d 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -2,4 +2,6 @@ #### Improvements 🧹 +- Composition: links pointing to own board are purged [#2203](https://github.com/terrastruct/d2/pull/2203) + #### Bugfixes ⛑️ diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 517b226eb..7d4eb7ba1 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -7,6 +7,7 @@ import ( "io" "io/fs" "net/url" + "slices" "strconv" "strings" @@ -1224,6 +1225,11 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) { obj.Link = nil continue } + + if slices.Equal(linkKey.IDA(), obj.Graph.IDA()) { + obj.Link = nil + continue + } } for _, b := range g.Layers { c.validateBoardLinks(b) diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 631a409f6..379c6bfb1 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -2317,6 +2317,27 @@ scenarios: { tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Link.Value) }, }, + { + name: "no-self-link", + text: ` +x.link: scenarios.a + +layers: { + g: { + s.link: _.layers.g + } +} + +scenarios: { + a: { + b + } +}`, + assertions: func(t *testing.T, g *d2graph.Graph) { + tassert.Equal(t, (*d2graph.Scalar)(nil), g.Scenarios[0].Objects[0].Link) + tassert.Equal(t, (*d2graph.Scalar)(nil), g.Layers[0].Objects[0].Link) + }, + }, { name: "link-board-not-found-1", text: `x.link: layers.x diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index 7a72512ef..40c37e55e 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -84,6 +84,66 @@ func (g *Graph) RootBoard() *Graph { return g } +func (g *Graph) IDA() []string { + if g == nil { + return nil + } + + var parts []string + + current := g + for current != nil { + if current.Name != "" { + parts = append(parts, current.Name) + } + current = current.Parent + } + + for i := 0; i < len(parts)/2; i++ { + j := len(parts) - 1 - i + parts[i], parts[j] = parts[j], parts[i] + } + + if len(parts) == 0 { + return []string{"root"} + } + + parts = append([]string{"root"}, parts...) + + if g.Parent != nil { + var containerName string + if len(g.Parent.Layers) > 0 { + for _, l := range g.Parent.Layers { + if l == g { + containerName = "layers" + break + } + } + } + if len(g.Parent.Scenarios) > 0 { + for _, s := range g.Parent.Scenarios { + if s == g { + containerName = "scenarios" + break + } + } + } + if len(g.Parent.Steps) > 0 { + for _, s := range g.Parent.Steps { + if s == g { + containerName = "steps" + break + } + } + } + if containerName != "" { + parts = append(parts[:1], append([]string{containerName}, parts[1:]...)...) + } + } + + return parts +} + type LayoutGraph func(context.Context, *Graph) error type RouteEdges func(context.Context, *Graph, []*Edge) error diff --git a/testdata/d2compiler/TestCompile/no-self-link.exp.json b/testdata/d2compiler/TestCompile/no-self-link.exp.json new file mode 100644 index 000000000..2bd2fe671 --- /dev/null +++ b/testdata/d2compiler/TestCompile/no-self-link.exp.json @@ -0,0 +1,695 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,0:0:0-13:1:100", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:19:20", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:6:7", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:1:2", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:2:3-1:6:7", + "value": [ + { + "string": "link", + "raw_string": "link" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:8:9-1:19:20", + "value": [ + { + "string": "scenarios.a", + "raw_string": "scenarios.a" + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,3:0:22-7:1:67", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,3:0:22-3:6:28", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,3:0:22-3:6:28", + "value": [ + { + "string": "layers", + "raw_string": "layers" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,3:8:30-7:1:67", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,4:2:34-6:3:65", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,4:2:34-4:3:35", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,4:2:34-4:3:35", + "value": [ + { + "string": "g", + "raw_string": "g" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,4:5:37-6:3:65", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:4:43-5:22:61", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:4:43-5:10:49", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:4:43-5:5:44", + "value": [ + { + "string": "s", + "raw_string": "s" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:6:45-5:10:49", + "value": [ + { + "string": "link", + "raw_string": "link" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:12:51-5:22:61", + "value": [ + { + "string": "_.layers.g", + "raw_string": "_.layers.g" + } + ] + } + } + } + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,9:0:69-13:1:100", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,9:0:69-9:9:78", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,9:0:69-9:9:78", + "value": [ + { + "string": "scenarios", + "raw_string": "scenarios" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,9:11:80-13:1:100", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,10:2:84-12:3:98", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,10:2:84-10:3:85", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,10:2:84-10:3:85", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,10:5:87-12:3:98", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,11:4:93-11:5:94", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,11:4:93-11:5:94", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,11:4:93-11:5:94", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + } + ] + } + } + } + } + ] + } + } + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "x", + "id_val": "x", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:6:7", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:1:2", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:2:3-1:6:7", + "value": [ + { + "string": "link", + "raw_string": "link" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "x" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "link": { + "value": "root.scenarios.a" + }, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ], + "layers": [ + { + "name": "g", + "isFolderOnly": false, + "ast": { + "range": ",0:0:0-1:0:0", + "nodes": [ + { + "map_key": { + "range": ",0:0:0-0:0:0", + "key": { + "range": ",0:0:0-0:0:0", + "path": [ + { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "s" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": ",0:0:0-1:0:0", + "nodes": [ + { + "map_key": { + "range": ",0:0:0-0:0:0", + "key": { + "range": ",0:0:0-0:0:0", + "path": [ + { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "link" + } + ] + } + } + ] + }, + "primary": { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "root.layers.g" + } + ] + } + }, + "value": {} + } + } + ] + } + } + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "s", + "id_val": "s", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:4:43-5:10:49", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:4:43-5:5:44", + "value": [ + { + "string": "s", + "raw_string": "s" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,5:6:45-5:10:49", + "value": [ + { + "string": "link", + "raw_string": "link" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "s" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + } + ], + "scenarios": [ + { + "name": "a", + "isFolderOnly": false, + "ast": { + "range": ",0:0:0-1:0:0", + "nodes": [ + { + "map_key": { + "range": ",0:0:0-0:0:0", + "key": { + "range": ",0:0:0-0:0:0", + "path": [ + { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": ",0:0:0-1:0:0", + "nodes": [ + { + "map_key": { + "range": ",0:0:0-0:0:0", + "key": { + "range": ",0:0:0-0:0:0", + "path": [ + { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "link" + } + ] + } + } + ] + }, + "primary": { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "root.scenarios.a" + } + ] + } + }, + "value": {} + } + } + ] + } + } + } + }, + { + "map_key": { + "range": ",0:0:0-0:0:0", + "key": { + "range": ",0:0:0-0:0:0", + "path": [ + { + "unquoted_string": { + "range": ",0:0:0-0:0:0", + "value": [ + { + "string": "b" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "x", + "id_val": "x", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:6:7", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:0:1-1:1:2", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,1:2:3-1:6:7", + "value": [ + { + "string": "link", + "raw_string": "link" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "x" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "b", + "id_val": "b", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,11:4:93-11:5:94", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/no-self-link.d2,11:4:93-11:5:94", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "b" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + } + ] + }, + "err": null +}