diff --git a/d2oracle/edit.go b/d2oracle/edit.go index 4120240c9..99b8d99f5 100644 --- a/d2oracle/edit.go +++ b/d2oracle/edit.go @@ -22,6 +22,12 @@ import ( "oss.terrastruct.com/d2/d2target" ) +type OutsideScopeError struct{} + +func (e OutsideScopeError) Error() string { + return "operation would modify AST outside of given scope" +} + func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, newKey string, err error) { defer xdefer.Errorf(&err, "failed to create %#v", key) @@ -763,7 +769,7 @@ func appendMapKey(m *d2ast.Map, mk *d2ast.Key) { } } -func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) { +func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) { defer xdefer.Errorf(&err, "failed to delete %#v", key) mk, err := d2parser.ParseMapKey(key) @@ -779,6 +785,19 @@ func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) { edgeTrimCommon(mk) } + boardG := g + baseAST := g.AST + + if len(boardPath) > 0 { + // When compiling a nested board, we can read from boardG but only write to baseBoardG + boardG = GetBoardGraph(g, boardPath) + if boardG == nil { + return nil, fmt.Errorf("board %v not found", boardPath) + } + // TODO beter name + baseAST = boardG.BaseAST + } + g2, err := deleteReserved(g, mk) if err != nil { return nil, err @@ -831,28 +850,44 @@ func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) { return recompile(g.AST) } - prevG, _ := recompile(g.AST) + prevG, _ := recompile(boardG.AST) - g, err = renameConflictsToParent(g, mk.Key) + boardG, err = renameConflictsToParent(boardG, mk.Key) if err != nil { return nil, err } - obj, ok := g.Root.HasChild(d2graph.Key(mk.Key)) + obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key)) if !ok { return g, nil } - g, err = deleteObject(g, mk.Key, obj) + if len(boardPath) > 0 { + // TODO null + writeableRefs := getWriteableRefs(obj, baseAST) + if len(writeableRefs) != len(obj.References) { + return nil, OutsideScopeError{} + } + } + + boardG, err = deleteObject(boardG, baseAST, mk.Key, obj) if err != nil { return nil, err } - if err := updateNear(prevG, g, &key, nil, false); err != nil { + if err := updateNear(prevG, boardG, &key, nil, false); err != nil { return nil, err } - return recompile(g.AST) + if len(boardPath) > 0 { + replaced := ReplaceBoardNode(g.AST, baseAST, boardPath) + if !replaced { + return nil, fmt.Errorf("board %v AST not found", boardPath) + } + return recompile(g.AST) + } + + return recompile(boardG.AST) } func bumpChildrenUnderscores(m *d2ast.Map) { @@ -1210,7 +1245,7 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error { return nil } -func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d2graph.Graph, error) { +func deleteObject(g *d2graph.Graph, baseAST *d2ast.Map, key *d2ast.KeyPath, obj *d2graph.Object) (*d2graph.Graph, error) { var refEdges []*d2ast.Edge for _, ref := range obj.References { if ref.InEdge() { diff --git a/d2oracle/edit_test.go b/d2oracle/edit_test.go index 7fead42c1..68c061e50 100644 --- a/d2oracle/edit_test.go +++ b/d2oracle/edit_test.go @@ -4872,7 +4872,8 @@ func TestDelete(t *testing.T) { t.Parallel() testCases := []struct { - name string + name string + boardPath []string text string key string @@ -6520,6 +6521,66 @@ cdpdxz cm `, }, + { + name: "layers-basic", + + text: `a +layers: { + x: { + b + c + } +} +`, + key: `c`, + boardPath: []string{"root", "layers", "x"}, + + exp: `a +layers: { + x: { + b + } +} +`, + }, + { + name: "scenarios-basic", + + text: `a +scenarios: { + x: { + b + c + } +} +`, + key: `c`, + boardPath: []string{"root", "scenarios", "x"}, + + exp: `a +scenarios: { + x: { + b + } +} +`, + }, + { + name: "scenarios-inherited", + + text: `a +scenarios: { + x: { + b + c + } +} +`, + key: `a`, + boardPath: []string{"root", "scenarios", "x"}, + + expErr: `failed to delete "a": operation would modify AST outside of given scope`, + }, } for _, tc := range testCases { @@ -6530,7 +6591,7 @@ cm et := editTest{ text: tc.text, testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) { - return d2oracle.Delete(g, tc.key) + return d2oracle.Delete(g, tc.boardPath, tc.key) }, exp: tc.exp, diff --git a/testdata/d2oracle/TestDelete/layers-basic.exp.json b/testdata/d2oracle/TestDelete/layers-basic.exp.json new file mode 100644 index 000000000..a2fa2900f --- /dev/null +++ b/testdata/d2oracle/TestDelete/layers-basic.exp.json @@ -0,0 +1,319 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-6:0:31", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-0:1:1", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-0:1:1", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-0:1:1", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + }, + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,1:0:2-5:1:30", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,1:0:2-1:6:8", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,1:0:2-1:6:8", + "value": [ + { + "string": "layers", + "raw_string": "layers" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,1:8:10-5:1:30", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,2:2:14-4:3:28", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,2:2:14-2:3:15", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,2:2:14-2:3:15", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,2:5:17-4:3:28", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "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": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-0:1:1", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,0:0:0-0:1:1", + "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 + } + ], + "layers": [ + { + "name": "x", + "isFolderOnly": false, + "ast": { + "range": ",1:0:0-2: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": "b" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + } + ] + }, + "baseAST": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,2:5:17-4:3:28", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "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": "b", + "id_val": "b", + "references": [ + { + "key": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/layers-basic.d2,3:4:23-3:5:24", + "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": "" +} diff --git a/testdata/d2oracle/TestDelete/scenarios-basic.exp.json b/testdata/d2oracle/TestDelete/scenarios-basic.exp.json new file mode 100644 index 000000000..762c4efc2 --- /dev/null +++ b/testdata/d2oracle/TestDelete/scenarios-basic.exp.json @@ -0,0 +1,386 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-6:0:34", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + }, + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,1:0:2-5:1:33", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,1:0:2-1:9:11", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,1:0:2-1:9:11", + "value": [ + { + "string": "scenarios", + "raw_string": "scenarios" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,1:11:13-5:1:33", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,2:2:17-4:3:31", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,2:2:17-2:3:18", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,2:2:17-2:3:18", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,2:5:20-4:3:31", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "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": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "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 + } + ], + "scenarios": [ + { + "name": "x", + "isFolderOnly": false, + "ast": { + "range": ",1:0:0-2: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": "a" + } + ] + } + } + ] + }, + "primary": {}, + "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": {} + } + } + ] + }, + "baseAST": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,2:5:20-4:3:31", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "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": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,0:0:0-0:1:1", + "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": "b", + "id_val": "b", + "references": [ + { + "key": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2oracle/TestDelete/scenarios-basic.d2,3:4:26-3:5:27", + "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": "" +} diff --git a/testdata/d2oracle/TestDelete/scenarios-inherited.exp.json b/testdata/d2oracle/TestDelete/scenarios-inherited.exp.json new file mode 100644 index 000000000..85a38a782 --- /dev/null +++ b/testdata/d2oracle/TestDelete/scenarios-inherited.exp.json @@ -0,0 +1,4 @@ +{ + "graph": null, + "err": "failed to delete \"a\": operation would modify AST outside of given scope" +}