Skip to content

Commit

Permalink
protoparse: add support for special syntax for Any messages in messag…
Browse files Browse the repository at this point in the history
…e literals in option values (#486)
jhump authored Feb 4, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 260eab9 commit 1bb2aa9
Showing 6 changed files with 929 additions and 715 deletions.
81 changes: 71 additions & 10 deletions desc/protoparse/ast/options.go
Original file line number Diff line number Diff line change
@@ -163,21 +163,41 @@ func NewOptionNameNode(parts []*FieldReferenceNode, dots []*RuneNode) *OptionNam
}

// FieldReferenceNode is a reference to a field name. It can indicate a regular
// field (simple unqualified name) or an extension field (possibly-qualified
// name that is enclosed either in brackets or parentheses).
// field (simple unqualified name), an extension field (possibly-qualified name
// that is enclosed either in brackets or parentheses), or an "any" type
// reference (a type URL in the form "server.host/fully.qualified.Name" that is
// enclosed in brackets).
//
// This is used in options to indicate the names of custom options (which are
// Extension names are used in options to refer to custom options (which are
// actually extensions), in which case the name is enclosed in parentheses "("
// and ")". It is also used in message literals to set extension fields, in
// which case the name is enclosed in square brackets "[" and "]".
// and ")". They can also be used to refer to extension fields of options.
//
// Example:
// Extension names are also used in message literals to set extension fields,
// in which case the name is enclosed in square brackets "[" and "]".
//
// "Any" type references can only be used in message literals, and are not
// allowed in option names. They are always enclosed in square brackets. An
// "any" type reference is distinguished from an extension name by the presence
// of a slash, which must be present in an "any" type reference and must be
// absent in an extension name.
//
// Examples:
// foobar
// (foo.bar)
// [foo.bar]
// [type.googleapis.com/foo.bar]
//
type FieldReferenceNode struct {
compositeNode
Open *RuneNode // only present for extension names
Name IdentValueNode
Close *RuneNode // only present for extension names
Open *RuneNode // only present for extension names and "any" type references

// only present for "any" type references
UrlPrefix IdentValueNode
Slash *RuneNode

Name IdentValueNode

Close *RuneNode // only present for extension names and "any" type references
}

// NewFieldReferenceNode creates a new *FieldReferenceNode for a regular field.
@@ -219,14 +239,55 @@ func NewExtensionFieldReferenceNode(openSym *RuneNode, name IdentValueNode, clos
}
}

// NewAnyTypeReferenceNode creates a new *FieldReferenceNode for an "any"
// type reference. All args must be non-nil. The openSym and closeSym runes
// should be "[" and "]". The slashSym run should be "/".
func NewAnyTypeReferenceNode(openSym *RuneNode, urlPrefix IdentValueNode, slashSym *RuneNode, name IdentValueNode, closeSym *RuneNode) *FieldReferenceNode {
if name == nil {
panic("name is nil")
}
if openSym == nil {
panic("openSym is nil")
}
if closeSym == nil {
panic("closeSym is nil")
}
if urlPrefix == nil {
panic("urlPrefix is nil")
}
if slashSym == nil {
panic("slashSym is nil")
}
children := []Node{openSym, urlPrefix, slashSym, name, closeSym}
return &FieldReferenceNode{
compositeNode: compositeNode{
children: children,
},
Open: openSym,
UrlPrefix: urlPrefix,
Slash: slashSym,
Name: name,
Close: closeSym,
}
}

// IsExtension reports if this is an extension name or not (e.g. enclosed in
// punctuation, such as parentheses or brackets).
func (a *FieldReferenceNode) IsExtension() bool {
return a.Open != nil
return a.Open != nil && a.Slash == nil
}

// IsExtension reports if this is an extension name or not (e.g. enclosed in
// punctuation, such as parentheses or brackets).
func (a *FieldReferenceNode) IsAnyTypeReference() bool {
return a.Slash != nil
}

func (a *FieldReferenceNode) Value() string {
if a.Open != nil {
if a.Slash != nil {
return string(a.Open.Rune) + string(a.UrlPrefix.AsIdentifier()) + string(a.Slash.Rune) + string(a.Name.AsIdentifier()) + string(a.Close.Rune)
}
return string(a.Open.Rune) + string(a.Name.AsIdentifier()) + string(a.Close.Rune)
} else {
return string(a.Name.AsIdentifier())
2 changes: 1 addition & 1 deletion desc/protoparse/lexer.go
Original file line number Diff line number Diff line change
@@ -370,7 +370,7 @@ func (l *protoLex) Lex(lval *protoSymType) int {
l.setError(lval, errors.New("invalid control character"))
return _ERROR
}
if !strings.ContainsRune(";,.:=-+(){}[]<>", c) {
if !strings.ContainsRune(";,.:=-+(){}[]<>/", c) {
l.setError(lval, errors.New("invalid character"))
return _ERROR
}
93 changes: 92 additions & 1 deletion desc/protoparse/linker_test.go
Original file line number Diff line number Diff line change
@@ -499,7 +499,7 @@ func TestLinkerValidation(t *testing.T) {
"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
"message Baz { option (foo) = { bar< name: \"abc\" > }; }\n",
},
"foo.proto:7:30: message Baz: option (foo): field bar not found (did you mean the group named Bar?)",
"foo.proto:7:32: message Baz: option (foo): field bar not found (did you mean the group named Bar?)",
},
{
map[string]string{
@@ -777,6 +777,97 @@ func TestLinkerValidation(t *testing.T) {
},
"foo.proto:8:6: syntax error: unexpected '.'",
},
{
map[string]string{
"foo.proto": "syntax = \"proto3\";\n" +
"package foo.bar;\n" +
"import \"google/protobuf/any.proto\";\n" +
"import \"google/protobuf/descriptor.proto\";\n" +
"message Foo { string a = 1; int32 b = 2; }\n" +
"extend google.protobuf.MessageOptions { optional google.protobuf.Any any = 10001; }\n" +
"message Baz {\n" +
" option (any) = {\n" +
" [type.googleapis.com/foo.bar.Foo] <\n" +
" a: \"abc\"\n" +
" b: 123\n" +
" >\n" +
" };\n" +
"}\n",
},
"", // should succeed
},
{
map[string]string{
"foo.proto": "syntax = \"proto3\";\n" +
"package foo.bar;\n" +
"import \"google/protobuf/descriptor.proto\";\n" +
"message Foo { string a = 1; int32 b = 2; }\n" +
"extend google.protobuf.MessageOptions { optional Foo f = 10001; }\n" +
"message Baz {\n" +
" option (f) = {\n" +
" [type.googleapis.com/foo.bar.Foo] <\n" +
" a: \"abc\"\n" +
" b: 123\n" +
" >\n" +
" };\n" +
"}\n",
},
"foo.proto:8:6: message foo.bar.Baz: option (foo.bar.f): type references are only allowed for google.protobuf.Any, but this type is foo.bar.Foo",
},
{
map[string]string{
"foo.proto": "syntax = \"proto3\";\n" +
"package foo.bar;\n" +
"import \"google/protobuf/any.proto\";\n" +
"import \"google/protobuf/descriptor.proto\";\n" +
"message Foo { string a = 1; int32 b = 2; }\n" +
"extend google.protobuf.MessageOptions { optional google.protobuf.Any any = 10001; }\n" +
"message Baz {\n" +
" option (any) = {\n" +
" [types.custom.io/foo.bar.Foo] <\n" +
" a: \"abc\"\n" +
" b: 123\n" +
" >\n" +
" };\n" +
"}\n",
},
"foo.proto:9:6: message foo.bar.Baz: option (foo.bar.any): could not resolve type reference types.custom.io/foo.bar.Foo",
},
{
map[string]string{
"foo.proto": "syntax = \"proto3\";\n" +
"package foo.bar;\n" +
"import \"google/protobuf/any.proto\";\n" +
"import \"google/protobuf/descriptor.proto\";\n" +
"message Foo { string a = 1; int32 b = 2; }\n" +
"extend google.protobuf.MessageOptions { optional google.protobuf.Any any = 10001; }\n" +
"message Baz {\n" +
" option (any) = {\n" +
" [type.googleapis.com/foo.bar.Foo]: 123\n" +
" };\n" +
"}\n",
},
"foo.proto:9:40: message foo.bar.Baz: option (foo.bar.any): type references for google.protobuf.Any must have message literal value",
},
{
map[string]string{
"foo.proto": "syntax = \"proto3\";\n" +
"package foo.bar;\n" +
"import \"google/protobuf/any.proto\";\n" +
"import \"google/protobuf/descriptor.proto\";\n" +
"message Foo { string a = 1; int32 b = 2; }\n" +
"extend google.protobuf.MessageOptions { optional google.protobuf.Any any = 10001; }\n" +
"message Baz {\n" +
" option (any) = {\n" +
" [type.googleapis.com/Foo] <\n" +
" a: \"abc\"\n" +
" b: 123\n" +
" >\n" +
" };\n" +
"}\n",
},
"foo.proto:9:6: message foo.bar.Baz: option (foo.bar.any): could not resolve type reference type.googleapis.com/Foo",
},
}
for i, tc := range testCases {
acc := func(filename string) (io.ReadCloser, error) {
Loading

0 comments on commit 1bb2aa9

Please sign in to comment.