diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 498d06ed6..58fe48b56 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -6,3 +6,4 @@ - Fix executable plugins that implement standalone router [#1910](https://github.com/terrastruct/d2/pull/1910) - Fix compiler error with multiple nested spread substitutions [#1913](https://github.com/terrastruct/d2/pull/1913) +- Fix substitutions from imports into different scopes [#1914](https://github.com/terrastruct/d2/pull/1914) diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 7ce85198f..9a360bf0c 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -10,6 +10,7 @@ import ( "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" + "oss.terrastruct.com/util-go/mapfs" "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2format" @@ -23,6 +24,8 @@ func TestCompile(t *testing.T) { testCases := []struct { name string text string + // For tests that use imports, define `index.d2` as text and other files here + files map[string]string expErr string assertions func(t *testing.T, g *d2graph.Graph) @@ -2868,15 +2871,60 @@ d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowh expErr: `d2/testdata/d2compiler/TestCompile/fixed-pos-shape-hierarchy.d2:4:2: position keywords cannot be used with shape "hierarchy" d2/testdata/d2compiler/TestCompile/fixed-pos-shape-hierarchy.d2:5:2: position keywords cannot be used with shape "hierarchy"`, }, + { + name: "vars-in-imports", + text: `dev: { + vars: { + env: Dev + } + ...@template.d2 +} + +qa: { + vars: { + env: Qa + } + ...@template.d2 +} +`, + files: map[string]string{ + "template.d2": `env: { + label: ${env} Environment + vm: { + label: My Virtual machine! + } +}`, + }, + assertions: func(t *testing.T, g *d2graph.Graph) { + tassert.Equal(t, "dev.env", g.Objects[1].AbsID()) + tassert.Equal(t, "Dev Environment", g.Objects[1].Label.Value) + tassert.Equal(t, "qa.env", g.Objects[2].AbsID()) + tassert.Equal(t, "Qa Environment", g.Objects[2].Label.Value) + }, + }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - + opts := &d2compiler.CompileOptions{} + if tc.files != nil { + tc.files["index.d2"] = tc.text + renamed := make(map[string]string) + for file, content := range tc.files { + renamed[fmt.Sprintf("d2/testdata/d2compiler/TestCompile/%v", file)] = content + } + fs, err := mapfs.New(renamed) + assert.Success(t, err) + t.Cleanup(func() { + err = fs.Close() + assert.Success(t, err) + }) + opts.FS = fs + } d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) - g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), opts) if tc.expErr != "" { if err == nil { t.Fatalf("expected error with: %q", tc.expErr) diff --git a/d2ir/compile.go b/d2ir/compile.go index 995f48432..273637a9d 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -31,8 +31,7 @@ type compiler struct { imports []string // importStack is used to detect cyclic imports. importStack []string - // importCache enables reuse of files imported multiple times. - importCache map[string]*Map + seenImports map[string]struct{} utf16Pos bool // Stack of globs that must be recomputed at each new object in and below the current scope. @@ -62,7 +61,7 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) { err: &d2parser.ParseError{}, fs: opts.FS, - importCache: make(map[string]*Map), + seenImports: make(map[string]struct{}), utf16Pos: opts.UTF16Pos, } m := &Map{} diff --git a/d2ir/import.go b/d2ir/import.go index b933298dd..c415cf138 100644 --- a/d2ir/import.go +++ b/d2ir/import.go @@ -82,16 +82,11 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) { // Only get immediate imports. if len(c.importStack) == 2 { - if _, ok := c.importCache[impPath]; !ok { + if _, ok := c.seenImports[impPath]; !ok { c.imports = append(c.imports, imp.PathWithPre()) } } - ir, ok := c.importCache[impPath] - if ok { - return ir, true - } - var f fs.File var err error if c.fs == nil { @@ -113,13 +108,13 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) { return nil, false } - ir = &Map{} + ir := &Map{} ir.initRoot() ir.parent.(*Field).References[0].Context_.Scope = ast c.compileMap(ir, ast, ast) - c.importCache[impPath] = ir + c.seenImports[impPath] = struct{}{} return ir, true } diff --git a/testdata/d2compiler/TestCompile/vars-in-imports.exp.json b/testdata/d2compiler/TestCompile/vars-in-imports.exp.json new file mode 100644 index 000000000..05c63f5e3 --- /dev/null +++ b/testdata/d2compiler/TestCompile/vars-in-imports.exp.json @@ -0,0 +1,530 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-13:0:107", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-5:1:53", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-0:3:3", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-0:3:3", + "value": [ + { + "string": "dev", + "raw_string": "dev" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:5:5-5:1:53", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,1:2:9-3:3:33", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,1:2:9-1:6:13", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,1:2:9-1:6:13", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,1:8:15-3:3:33", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,2:4:21-2:12:29", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,2:4:21-2:7:24", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,2:4:21-2:7:24", + "value": [ + { + "string": "env", + "raw_string": "env" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,2:9:26-2:12:29", + "value": [ + { + "string": "Dev", + "raw_string": "Dev" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "import": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,4:2:36-4:17:51", + "spread": true, + "pre": "", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,4:6:40-4:14:48", + "value": [ + { + "string": "template", + "raw_string": "template" + } + ] + } + } + ] + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:0:55-12:1:106", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:0:55-7:2:57", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:0:55-7:2:57", + "value": [ + { + "string": "qa", + "raw_string": "qa" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:4:59-12:1:106", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,8:2:63-10:3:86", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,8:2:63-8:6:67", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,8:2:63-8:6:67", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,8:8:69-10:3:86", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,9:4:75-9:11:82", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,9:4:75-9:7:78", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,9:4:75-9:7:78", + "value": [ + { + "string": "env", + "raw_string": "env" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,9:9:80-9:11:82", + "value": [ + { + "string": "Qa", + "raw_string": "Qa" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "import": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,11:2:89-11:17:104", + "spread": true, + "pre": "", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,11:6:93-11:14:101", + "value": [ + { + "string": "template", + "raw_string": "template" + } + ] + } + } + ] + } + } + ] + } + } + } + } + ] + }, + "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": "dev", + "id_val": "dev", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-0:3:3", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,0:0:0-0:3:3", + "value": [ + { + "string": "dev", + "raw_string": "dev" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "dev" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "env", + "id_val": "env", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,0:0:0-0:3:3", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,0:0:0-0:3:3", + "value": [ + { + "string": "env", + "raw_string": "env" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "Dev Environment" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "env", + "id_val": "env", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,0:0:0-0:3:3", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,0:0:0-0:3:3", + "value": [ + { + "string": "env", + "raw_string": "env" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "Qa Environment" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "vm", + "id_val": "vm", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,2:2:37-2:4:39", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,2:2:37-2:4:39", + "value": [ + { + "string": "vm", + "raw_string": "vm" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "My Virtual machine!" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "vm", + "id_val": "vm", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,2:2:37-2:4:39", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/template.d2,2:2:37-2:4:39", + "value": [ + { + "string": "vm", + "raw_string": "vm" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "My Virtual machine!" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "qa", + "id_val": "qa", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:0:55-7:2:57", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/vars-in-imports.d2,7:0:55-7:2:57", + "value": [ + { + "string": "qa", + "raw_string": "qa" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "qa" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +}