Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: Reserved keywords only unquoted #800

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 56 additions & 52 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
if f.Name == "shape" {
continue
}
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
if _, ok := d2graph.BoardKeywords[f.Name]; ok && f.IsUnquoted {
continue
}
c.compileField(obj, f)
Expand All @@ -139,25 +139,27 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
}

func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
if isStyleReserved {
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
return
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(obj.Attributes, f)
return
} else if f.Name == "style" {
if f.Map() == nil {
if f.IsUnquoted {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
if isStyleReserved {
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
return
}
c.compileStyle(obj.Attributes, f.Map())
if obj.Attributes.Style.Animated != nil {
c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(obj.Attributes, f)
return
} else if f.Name == "style" {
if f.Map() == nil {
return
}
c.compileStyle(obj.Attributes, f.Map())
if obj.Attributes.Style.Animated != nil {
c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
}
return
}
return
}

obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
Expand Down Expand Up @@ -548,57 +550,59 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {

func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {
for _, f := range m.Fields {
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
if _, ok := d2graph.BoardKeywords[f.Name]; ok && f.IsUnquoted {
continue
}
c.validateKey(obj, f)
}
}

func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isReserved := d2graph.ReservedKeywords[keyword]
if isReserved {
switch obj.Attributes.Shape.Value {
case d2target.ShapeSQLTable, d2target.ShapeClass:
default:
if len(obj.Children) > 0 && (f.Name == "width" || f.Name == "height") {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf("%s cannot be used on container: %s", f.Name, obj.AbsID()))
if f.IsUnquoted {
keyword := strings.ToLower(f.Name)
_, isReserved := d2graph.ReservedKeywords[keyword]
if isReserved {
switch obj.Attributes.Shape.Value {
case d2target.ShapeSQLTable, d2target.ShapeClass:
default:
if len(obj.Children) > 0 && (f.Name == "width" || f.Name == "height") {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf("%s cannot be used on container: %s", f.Name, obj.AbsID()))
}
}
}

switch obj.Attributes.Shape.Value {
case d2target.ShapeCircle, d2target.ShapeSquare:
checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil)
if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value {
c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Attributes.Shape.Value)
switch obj.Attributes.Shape.Value {
case d2target.ShapeCircle, d2target.ShapeSquare:
checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil)
if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value {
c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Attributes.Shape.Value)
}
}
}

switch f.Name {
case "style":
if obj.Attributes.Style.ThreeDee != nil {
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`)
switch f.Name {
case "style":
if obj.Attributes.Style.ThreeDee != nil {
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`)
}
}
}
if obj.Attributes.Style.DoubleBorder != nil {
if obj.Attributes.Shape.Value != "" && obj.Attributes.Shape.Value != d2target.ShapeSquare && obj.Attributes.Shape.Value != d2target.ShapeRectangle && obj.Attributes.Shape.Value != d2target.ShapeCircle && obj.Attributes.Shape.Value != d2target.ShapeOval {
c.errorf(obj.Attributes.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
if obj.Attributes.Style.DoubleBorder != nil {
if obj.Attributes.Shape.Value != "" && obj.Attributes.Shape.Value != d2target.ShapeSquare && obj.Attributes.Shape.Value != d2target.ShapeRectangle && obj.Attributes.Shape.Value != d2target.ShapeCircle && obj.Attributes.Shape.Value != d2target.ShapeOval {
c.errorf(obj.Attributes.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
}
}
case "shape":
if obj.Attributes.Shape.Value == d2target.ShapeImage && obj.Attributes.Icon == nil {
c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
}
}
case "shape":
if obj.Attributes.Shape.Value == d2target.ShapeImage && obj.Attributes.Icon == nil {
c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
}

in := d2target.IsShape(obj.Attributes.Shape.Value)
_, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value]
if !in && arrowheadIn {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value))
in := d2target.IsShape(obj.Attributes.Shape.Value)
_, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value]
if !in && arrowheadIn {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value))
}
}
return
}
return
}

if obj.Attributes.Shape.Value == d2target.ShapeImage {
Expand Down
14 changes: 14 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1899,6 +1899,20 @@ Chinchillas_Collectibles.chinchilla -> Chinchillas.id`,
tassert.Equal(t, 2, *g.Edges[0].SrcTableColumnIndex)
},
},
{
name: "quoted-reserved",
text: `"3d": {
"width": hello
'multiple'
'layers': yes
}
"3d"."width" -> me
a."style"
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 7, len(g.Objects))
},
},
}

for _, tc := range testCases {
Expand Down
9 changes: 1 addition & 8 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ func (obj *Object) EnsureChild(ida []string) *Object {
return obj
}

// TODO IDA has to change from []string to []StringBox or something that retains data on quoting mechanism
_, is := ReservedKeywordHolders[ida[0]]
if len(ida) == 1 && !is {
_, ok := ReservedKeywords[ida[0]]
Expand Down Expand Up @@ -1021,14 +1022,6 @@ func (e *Edge) AbsID() string {
}

func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) {
for _, id := range [][]string{srcID, dstID} {
for _, p := range id {
if _, ok := ReservedKeywords[p]; ok {
return nil, errors.New("cannot connect to reserved keyword")
}
}
}

src := obj.ensureChildEdge(srcID)
dst := obj.ensureChildEdge(dstID)

Expand Down
2 changes: 1 addition & 1 deletion d2ir/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ layers: {
name: "2/bad_edge",
run: func(t testing.TB) {
_, err := compile(t, `layers -> scenarios`)
assert.ErrorString(t, err, `TestCompile/layers/errs/2/bad_edge.d2:1:1: edge with board keyword alone doesn't make sense`)
assert.ErrorString(t, err, `TestCompile/layers/errs/2/bad_edge.d2:1:1: cannot create edges between boards`)
},
},
{
Expand Down
72 changes: 43 additions & 29 deletions d2ir/d2ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ type Field struct {
// *Map.
parent Node

Name string `json:"name"`
Name string `json:"name"`
IsUnquoted bool `json:"-"`

// Primary_ to avoid clashing with Primary(). We need to keep it exported for
// encoding/json to marshal it so cannot prefix _ instead.
Expand Down Expand Up @@ -648,18 +649,24 @@ func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext) (*Field, error)
}

func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext) (*Field, error) {
head := kp.Path[i].Unbox().ScalarString()
head := kp.Path[i].Unbox()
headString := head.ScalarString()
unquotedString, headUnquoted := head.(*d2ast.UnquotedString)
if headUnquoted {
// Do not consider it unquoted if it has any escapes
headUnquoted = len(headString) == len(unquotedString.ScalarString())
}

if head == "_" {
if headString == "_" {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), `parent "_" can only be used in the beginning of paths, e.g. "_.x"`)
}

if findBoardKeyword(head) != -1 && NodeBoardKind(m) == "" {
if len(findBoardKeywords(headString)) > 0 && NodeBoardKind(m) == "" && headUnquoted {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head)
}

for _, f := range m.Fields {
if !strings.EqualFold(f.Name, head) {
if !strings.EqualFold(f.Name, headString) {
continue
}

Expand Down Expand Up @@ -687,8 +694,9 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext) (*Field,
}

f := &Field{
parent: m,
Name: head,
parent: m,
Name: headString,
IsUnquoted: headUnquoted,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to check that there are no escapes. Use len(headString) == len(head.(*d2ast.UnquotedString).RawString). i.e there are no escapes if the length of the string value is equal to it's length in the AST.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsUnquoted could be confusing then, but i can't think of a word that encapsulates "is unquoted or is escaped". Raw already means something else. will leave as IsUnquoted for now unless u have a word

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea just add a comment on IsUnquoted. I have no better word in mind.

}
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
if refctx != nil {
Expand Down Expand Up @@ -786,26 +794,33 @@ func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext) (*Edge, error) {
return f.Map().CreateEdge(eid, refctx)
}

ij := findProhibitedEdgeKeyword(eid.SrcPath...)
if ij != -1 {
return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "reserved keywords are prohibited in edges")
ijs := findProhibitedEdgeKeywords(eid.SrcPath...)
for _, ij := range ijs {
if refctx.Edge.Src.Path[ij].UnquotedString != nil {
return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "reserved keywords are prohibited in edges")
}
}
ij = findBoardKeyword(eid.SrcPath...)
if ij == len(eid.SrcPath)-1 {
return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense")
ijs = findBoardKeywords(eid.SrcPath...)
for _, ij := range ijs {
if refctx.Edge.Src.Path[ij].UnquotedString != nil {
return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "cannot create edges between boards")
}
}
src := m.GetField(eid.SrcPath...)
if NodeBoardKind(src) != "" {
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards")
}

ij = findProhibitedEdgeKeyword(eid.DstPath...)
if ij != -1 {
return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "reserved keywords are prohibited in edges")
ijs = findProhibitedEdgeKeywords(eid.DstPath...)
for _, ij := range ijs {
if refctx.Edge.Dst.Path[ij].UnquotedString != nil {
return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "reserved keywords are prohibited in edges")
}
}
ij = findBoardKeyword(eid.DstPath...)
if ij == len(eid.DstPath)-1 {
return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense")
ijs = findBoardKeywords(eid.DstPath...)
for _, ij := range ijs {
if refctx.Edge.Dst.Path[ij].UnquotedString != nil {
return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "cannot create edges between boards")
}
}
dst := m.GetField(eid.DstPath...)
if NodeBoardKind(dst) != "" {
Expand Down Expand Up @@ -988,25 +1003,24 @@ func countUnderscores(p []string) int {
return 0
}

func findBoardKeyword(ida ...string) int {
func findBoardKeywords(ida ...string) (out []int) {
for i := range ida {
if _, ok := d2graph.BoardKeywords[ida[i]]; ok {
return i
out = append(out, i)
}
}
return -1
return
}

func findProhibitedEdgeKeyword(ida ...string) int {
func findProhibitedEdgeKeywords(ida ...string) (out []int) {
for i := range ida {
if _, ok := d2graph.SimpleReservedKeywords[ida[i]]; ok {
return i
}
if _, ok := d2graph.ReservedKeywordHolders[ida[i]]; ok {
return i
out = append(out, i)
} else if _, ok := d2graph.ReservedKeywordHolders[ida[i]]; ok {
out = append(out, i)
}
}
return -1
return
}

func parentRef(n Node) Reference {
Expand Down