diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 2a2e3becd..0b3ed9ed0 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -288,20 +288,28 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) { attrs.Height.Value = scalar.ScalarString() attrs.Height.MapKey = f.LastPrimaryKey() case "top": - _, err := strconv.Atoi(scalar.ScalarString()) + v, err := strconv.Atoi(scalar.ScalarString()) if err != nil { c.errorf(scalar, "non-integer top %#v: %s", scalar.ScalarString(), err) return } + if v < 0 { + c.errorf(scalar, "top must be a non-negative integer: %#v", scalar.ScalarString()) + return + } attrs.Top = &d2graph.Scalar{} attrs.Top.Value = scalar.ScalarString() attrs.Top.MapKey = f.LastPrimaryKey() case "left": - _, err := strconv.Atoi(scalar.ScalarString()) + v, err := strconv.Atoi(scalar.ScalarString()) if err != nil { c.errorf(scalar, "non-integer left %#v: %s", scalar.ScalarString(), err) return } + if v < 0 { + c.errorf(scalar, "left must be a non-negative integer: %#v", scalar.ScalarString()) + return + } attrs.Left = &d2graph.Scalar{} attrs.Left.Value = scalar.ScalarString() attrs.Left.MapKey = f.LastPrimaryKey() @@ -604,6 +612,13 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) { } } + if keyword == "top" && obj.Attributes.Left == nil { + c.errorf(f.LastPrimaryKey(), `keyword "top" currently cannot be set without also setting "left"`) + } + if keyword == "left" && obj.Attributes.Top == nil { + c.errorf(f.LastPrimaryKey(), `keyword "left" currently cannot be set without also setting "top"`) + } + switch f.Name { case "style": if obj.Attributes.Style.ThreeDee != nil { diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 85a27d860..9a09dbda7 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -124,6 +124,23 @@ x: { tassert.Equal(t, "200", g.Objects[0].Attributes.Top.Value) }, }, + { + name: "positions_disjoint", + text: `hey: { + top: 200 +} +`, + expErr: `d2/testdata/d2compiler/TestCompile/positions_disjoint.d2:2:2: keyword "top" currently cannot be set without also setting "left"`, + }, + { + name: "positions_negative", + text: `hey: { + top: 200 + left: -200 +} +`, + expErr: `d2/testdata/d2compiler/TestCompile/positions_negative.d2:3:8: left must be a non-negative integer: "-200"`, + }, { name: "equal_dimensions_on_circle", diff --git a/testdata/d2compiler/TestCompile/positions_disjoint.exp.json b/testdata/d2compiler/TestCompile/positions_disjoint.exp.json new file mode 100644 index 000000000..cced8f7d8 --- /dev/null +++ b/testdata/d2compiler/TestCompile/positions_disjoint.exp.json @@ -0,0 +1,12 @@ +{ + "graph": null, + "err": { + "ioerr": null, + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile/positions_disjoint.d2,1:1:8-1:9:16", + "errmsg": "d2/testdata/d2compiler/TestCompile/positions_disjoint.d2:2:2: keyword \"top\" currently cannot be set without also setting \"left\"" + } + ] + } +} diff --git a/testdata/d2compiler/TestCompile/positions_negative.exp.json b/testdata/d2compiler/TestCompile/positions_negative.exp.json new file mode 100644 index 000000000..308bba5fe --- /dev/null +++ b/testdata/d2compiler/TestCompile/positions_negative.exp.json @@ -0,0 +1,12 @@ +{ + "graph": null, + "err": { + "ioerr": null, + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile/positions_negative.d2,2:7:24-2:11:28", + "errmsg": "d2/testdata/d2compiler/TestCompile/positions_negative.d2:3:8: left must be a non-negative integer: \"-200\"" + } + ] + } +}