diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index b401bb4f5..fdaf33951 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -3290,6 +3290,21 @@ a -> b: ${x} assert.Equal(t, "im a var", g.Edges[0].Label.Value) }, }, + { + name: "edge-map", + run: func(t *testing.T) { + g := assertCompile(t, ` +vars: { + x: im a var +} +a -> b: { + target-arrowhead.label: ${x} +} +`, "") + assert.Equal(t, 1, len(g.Edges)) + assert.Equal(t, "im a var", g.Edges[0].DstArrowhead.Label.Value) + }, + }, { name: "quoted-var", run: func(t *testing.T) { @@ -3325,6 +3340,23 @@ y: "hey ${x}" assert.Equal(t, `hey "hi"`, g.Objects[0].Label.Value) }, }, + { + name: "parent-scope", + run: func(t *testing.T) { + g := assertCompile(t, ` +vars: { + x: im root var +} +a: { + vars: { + b: im nested var + } + hi: ${x} +} +`, "") + assert.Equal(t, "im root var", g.Objects[1].Label.Value) + }, + }, } for _, tc := range tca { @@ -3538,6 +3570,19 @@ hi: ${x.z} `, `d2/testdata/d2compiler/TestCompile2/vars/errors/nested-missing.d2:7:1: could not resolve variable "x.z"`) }, }, + { + name: "out-of-scope", + run: func(t *testing.T) { + assertCompile(t, ` +a: { + vars: { + x: hey + } +} +hi: ${x} +`, `d2/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.d2:7:1: could not resolve variable "x"`) + }, + }, { name: "edge", run: func(t *testing.T) { diff --git a/d2ir/compile.go b/d2ir/compile.go index c8fae9c02..7495ea2c4 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -114,17 +114,31 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) { if e.Primary() != nil { c.resolveSubstitutions(varsStack, e.LastRef().AST(), e.Primary()) } + if e.Map() != nil { + c.compileSubstitutions(e.Map(), varsStack) + } } } func (c *compiler) resolveSubstitutions(varsStack []*Map, node d2ast.Node, scalar *Scalar) { - subbed := false + var subbed bool + var resolvedField *Field + switch s := scalar.Value.(type) { case *d2ast.UnquotedString: for i, box := range s.Value { if box.Substitution != nil { - resolvedField := c.resolveSubstitution(varsStack[0], node, box.Substitution) + for _, vars := range varsStack { + resolvedField = c.resolveSubstitution(vars, box.Substitution) + if resolvedField != nil { + break + } + } if resolvedField != nil { + if resolvedField.Composite != nil { + c.errorf(node, `cannot reference map variable "%s"`, strings.Join(box.Substitution.IDA(), ".")) + return + } // If lone and unquoted, replace with value of sub if len(s.Value) == 1 { scalar.Value = resolvedField.Primary().Value @@ -132,6 +146,9 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node d2ast.Node, scala s.Value[i].String = go2.Pointer(resolvedField.Primary().String()) subbed = true } + } else { + c.errorf(node, `could not resolve variable "%s"`, strings.Join(box.Substitution.IDA(), ".")) + return } } } @@ -141,10 +158,22 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node d2ast.Node, scala case *d2ast.DoubleQuotedString: for i, box := range s.Value { if box.Substitution != nil { - resolvedField := c.resolveSubstitution(varsStack[0], node, box.Substitution) + for _, vars := range varsStack { + resolvedField = c.resolveSubstitution(vars, box.Substitution) + if resolvedField != nil { + break + } + } if resolvedField != nil { + if resolvedField.Composite != nil { + c.errorf(node, `cannot reference map variable "%s"`, strings.Join(box.Substitution.IDA(), ".")) + return + } s.Value[i].String = go2.Pointer(resolvedField.Primary().String()) subbed = true + } else { + c.errorf(node, `could not resolve variable "%s"`, strings.Join(box.Substitution.IDA(), ".")) + return } } } @@ -154,7 +183,7 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node d2ast.Node, scala } } -func (c *compiler) resolveSubstitution(vars *Map, node d2ast.Node, substitution *d2ast.Substitution) *Field { +func (c *compiler) resolveSubstitution(vars *Map, substitution *d2ast.Substitution) *Field { var resolved *Field for _, p := range substitution.Path { if vars == nil { @@ -170,14 +199,7 @@ func (c *compiler) resolveSubstitution(vars *Map, node d2ast.Node, substitution resolved = r } - if resolved == nil { - c.errorf(node, `could not resolve variable "%s"`, strings.Join(substitution.IDA(), ".")) - } else if resolved.Composite != nil { - c.errorf(node, `cannot reference map variable "%s"`, strings.Join(substitution.IDA(), ".")) - } else { - return resolved - } - return nil + return resolved } func (c *compiler) overlayVars(base, overlay *Map) { diff --git a/testdata/d2compiler/TestCompile2/vars/basic/edge-map.exp.json b/testdata/d2compiler/TestCompile2/vars/basic/edge-map.exp.json new file mode 100644 index 000000000..cb64534ff --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/basic/edge-map.exp.json @@ -0,0 +1,350 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,0:0:0-7:0:68", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,1:0:1-3:1:24", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,1:0:1-1:4:5", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,1:0:1-1:4:5", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,1:6:7-3:1:24", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,2:2:11-2:13:22", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,2:2:11-2:3:12", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,2:2:11-2:3:12", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,2:5:14-2:13:22", + "value": [ + { + "string": "im a var", + "raw_string": "im a var" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-6:1:67", + "edges": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-4:6:31", + "src": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-4:1:26", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-4:1:26", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "src_arrow": "", + "dst": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:5:30-4:6:31", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:5:30-4:6:31", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + } + ] + }, + "dst_arrow": ">" + } + ], + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:8:33-6:1:67", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:2:37-5:30:65", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:2:37-5:24:59", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:2:37-5:18:53", + "value": [ + { + "string": "target-arrowhead", + "raw_string": "target-arrowhead" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:19:54-5:24:59", + "value": [ + { + "string": "label", + "raw_string": "label" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:26:61-5:27:62", + "value": [ + { + "substitution": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:26:61-5:30:65", + "spread": false, + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,5:28:63-5:29:64", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + } + } + ] + } + } + } + } + ] + } + } + } + } + ] + }, + "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": [ + { + "index": 0, + "isCurve": false, + "src_arrow": false, + "dst_arrow": true, + "dstArrowhead": { + "label": { + "value": "im a var" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "references": [ + { + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ], + "objects": [ + { + "id": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-4:1:26", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:0:25-4:1:26", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "a" + }, + "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/TestCompile2/vars/basic/edge-map.d2,4:5:30-4:6:31", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/edge-map.d2,4:5:30-4:6:31", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "b" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +} diff --git a/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.exp.json b/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.exp.json new file mode 100644 index 000000000..b9d8924d5 --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.exp.json @@ -0,0 +1,329 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,0:0:0-10:0:81", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,1:0:1-3:1:27", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,1:0:1-1:4:5", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,1:0:1-1:4:5", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,1:6:7-3:1:27", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,2:2:11-2:16:25", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,2:2:11-2:3:12", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,2:2:11-2:3:12", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,2:5:14-2:16:25", + "value": [ + { + "string": "im root var", + "raw_string": "im root var" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:0:28-9:1:80", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:0:28-4:1:29", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:0:28-4:1:29", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:3:31-9:1:80", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,5:2:35-7:3:67", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,5:2:35-5:6:39", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,5:2:35-5:6:39", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,5:8:41-7:3:67", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,6:4:47-6:20:63", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,6:4:47-6:5:48", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,6:4:47-6:5:48", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,6:7:50-6:20:63", + "value": [ + { + "string": "im nested var", + "raw_string": "im nested var" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:2:70-8:10:78", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:2:70-8:4:72", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:2:70-8:4:72", + "value": [ + { + "string": "hi", + "raw_string": "hi" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:6:74-8:7:75", + "value": [ + { + "substitution": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:6:74-8:10:78", + "spread": false, + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:8:76-8:9:77", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + } + } + ] + } + } + } + } + ] + } + } + } + } + ] + }, + "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": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:0:28-4:1:29", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,4:0:28-4:1:29", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "a" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "hi", + "id_val": "hi", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:2:70-8:4:72", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/parent-scope.d2,8:2:70-8:4:72", + "value": [ + { + "string": "hi", + "raw_string": "hi" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "im root var" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +} diff --git a/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.exp.json b/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.exp.json new file mode 100644 index 000000000..c9251b81e --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.exp.json @@ -0,0 +1,11 @@ +{ + "graph": null, + "err": { + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.d2,6:0:33-6:2:35", + "errmsg": "d2/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.d2:7:1: could not resolve variable \"x\"" + } + ] + } +}