diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 4a04e7e04..843d8c331 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -38,6 +38,7 @@ - fixes panic when classes were mixed with layers incorrectly [#2448](https://github.com/terrastruct/d2/pull/2448) - fixes panic when gradient colors are used in sketch mode [#2481](https://github.com/terrastruct/d2/pull/2487) - fixes panic using glob ampersand filters with composite values [#2489](https://github.com/terrastruct/d2/pull/2489) + - fixes leaf ampersand filter when used with imports [#2494](https://github.com/terrastruct/d2/pull/2494) - Formatter: - fixes substitutions in quotes surrounded by text [#2462](https://github.com/terrastruct/d2/pull/2462) - CLI: fetch and render remote images of mimetype octet-stream correctly [#2370](https://github.com/terrastruct/d2/pull/2370) diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 702295099..61123712b 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -1726,6 +1726,33 @@ k expErr: `d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites`, }, + { + name: "imported-glob-leaf-filter", + + text: ` +***: { + &leaf: true + style: { + font-size: 30 + } +} +a: { + ...@x +} +`, + files: map[string]string{ + "x.d2": ` +b +`, + }, + assertions: func(t *testing.T, g *d2graph.Graph) { + assert.Equal(t, 2, len(g.Objects)) + assert.Equal(t, "b", g.Objects[0].Label.Value) + assert.Equal(t, "a", g.Objects[1].Label.Value) + assert.Equal(t, "30", g.Objects[0].Style.FontSize.Value) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Style.FontSize) + }, + }, { name: "import-nested-var", diff --git a/d2ir/compile.go b/d2ir/compile.go index 91c0739e8..6467fef1a 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -981,7 +981,7 @@ func (c *compiler) _ampersandPropertyFilter(propName string, value string, node c.errorf(key, `&leaf must be "true" or "false", got %q`, value) return false } - isLeaf := node.Map() == nil || !node.Map().IsContainer() + isLeaf := node.Map() == nil || !c.IsContainer(node.Map()) return isLeaf == boolVal case "connected": boolVal, err := strconv.ParseBool(value) diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go index 290e679de..d336d7894 100644 --- a/d2ir/d2ir.go +++ b/d2ir/d2ir.go @@ -705,7 +705,7 @@ func (m *Map) FieldCountRecursive() int { return acc } -func (m *Map) IsContainer() bool { +func (c *compiler) IsContainer(m *Map) bool { if m == nil { return false } @@ -714,6 +714,20 @@ func (m *Map) IsContainer() bool { for _, ref := range f.References { if ref.Primary() && ref.Context_.Key != nil && ref.Context_.Key.Value.Map != nil { for _, n := range ref.Context_.Key.Value.Map.Nodes { + if n.MapKey == nil { + if n.Import != nil { + impn, ok := c.peekImport(n.Import) + if ok { + for _, f := range impn.Fields { + _, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()] + if !(isReserved && f.Name.IsUnquoted()) { + return true + } + } + } + } + continue + } if len(n.MapKey.Edges) > 0 { return true } @@ -1333,7 +1347,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c * if refctx.Edge.Src.HasMultiGlob() { // If src has a double glob we only select leafs, those without children. - if src.Map().IsContainer() { + if c.IsContainer(src.Map()) { continue } if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) { @@ -1342,7 +1356,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c * } if refctx.Edge.Dst.HasMultiGlob() { // If dst has a double glob we only select leafs, those without children. - if dst.Map().IsContainer() { + if c.IsContainer(dst.Map()) { continue } if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) { diff --git a/d2ir/import.go b/d2ir/import.go index 2d6630203..dff3edbee 100644 --- a/d2ir/import.go +++ b/d2ir/import.go @@ -124,6 +124,53 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) { return ir, true } +func (c *compiler) peekImport(imp *d2ast.Import) (*Map, bool) { + impPath := imp.PathWithPre() + if impPath == "" && imp.Range != (d2ast.Range{}) { + return nil, false + } + + if len(c.importStack) > 0 { + if path.Ext(impPath) != ".d2" { + impPath += ".d2" + } + + if !filepath.IsAbs(impPath) { + impPath = path.Join(path.Dir(c.importStack[len(c.importStack)-1]), impPath) + } + } + + var f fs.File + var err error + if c.fs == nil { + f, err = os.Open(impPath) + } else { + f, err = c.fs.Open(impPath) + } + if err != nil { + return nil, false + } + defer f.Close() + + // Use a separate parse error to avoid polluting the main one + localErr := &d2parser.ParseError{} + ast, err := d2parser.Parse(impPath, f, &d2parser.ParseOptions{ + UTF16Pos: c.utf16Pos, + ParseError: localErr, + }) + if err != nil { + return nil, false + } + + ir := &Map{} + ir.initRoot() + ir.parent.(*Field).References[0].Context_.Scope = ast + + c.compileMap(ir, ast, ast) + + return ir, true +} + func nilScopeMap(n Node) { switch n := n.(type) { case *Map: diff --git a/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.exp.json b/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.exp.json new file mode 100644 index 000000000..410f77b04 --- /dev/null +++ b/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.exp.json @@ -0,0 +1,307 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,0:0:0-10:0:71", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,1:0:1-6:1:56", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,1:0:1-1:3:4", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,1:0:1-1:3:4", + "value": [ + { + "string": "***", + "raw_string": "***" + } + ], + "pattern": [ + "*", + "", + "*", + "", + "*" + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,1:5:6-6:1:56", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,2:2:10-2:13:21", + "ampersand": true, + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,2:3:11-2:7:15", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,2:3:11-2:7:15", + "value": [ + { + "string": "leaf", + "raw_string": "leaf" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "boolean": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,2:9:17-2:13:21", + "value": true + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,3:2:24-5:3:54", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,3:2:24-3:7:29", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,3:2:24-3:7:29", + "value": [ + { + "string": "style", + "raw_string": "style" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,3:9:31-5:3:54", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,4:4:37-4:17:50", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,4:4:37-4:13:46", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,4:4:37-4:13:46", + "value": [ + { + "string": "font-size", + "raw_string": "font-size" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "number": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,4:15:48-4:17:50", + "raw": "30", + "value": "30" + } + } + } + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:0:57-9:1:70", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:0:57-7:1:58", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:0:57-7:1:58", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:3:60-9:1:70", + "nodes": [ + { + "import": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,8:1:63-8:6:68", + "spread": true, + "pre": "", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,8:5:67-8:6:68", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + } + } + ] + } + } + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "iconStyle": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "b", + "id_val": "b", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/x.d2,1:0:1-1:1:2", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/x.d2,1:0:1-1:1:2", + "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": { + "fontSize": { + "value": "30" + } + }, + "iconStyle": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:0:57-7:1:58", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/imported-glob-leaf-filter.d2,7:0:57-7:1:58", + "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": {}, + "iconStyle": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +}