Skip to content

Commit 6048c2a

Browse files
authored
Merge pull request #94 from yoheimuta/support-extension-declarations
Support Extension Declarations for proto2 and editions
2 parents 17dcac0 + 048a0d9 commit 6048c2a

File tree

8 files changed

+457
-25
lines changed

8 files changed

+457
-25
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
syntax = "proto2";
2+
3+
message Foo {
4+
extensions 4 to 1000 [
5+
declaration = {
6+
number: 4,
7+
full_name: ".my.package.event_annotations",
8+
type: ".logs.proto.ValidationAnnotations",
9+
repeated: true },
10+
declaration = {
11+
number: 999,
12+
full_name: ".foo.package.bar",
13+
type: "int32"}];
14+
}
15+
16+
message Bar {
17+
extensions 1000 to 2000 [
18+
declaration = {
19+
number: 1000,
20+
full_name: ".foo.package",
21+
type: ".foo.type"
22+
}
23+
];
24+
}

lexer/scanner/token.go

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ const (
5959
TMAP
6060
TRESERVED
6161
TEXTENSIONS
62+
TDECLARATION
63+
TNUMBER
64+
TFULLNAME
65+
TTYPE
6266
TENUM
6367
TSTREAM
6468
TGROUP
@@ -92,28 +96,32 @@ func asMiscToken(ch rune) Token {
9296

9397
func asKeywordToken(st string) Token {
9498
m := map[string]Token{
95-
"syntax": TSYNTAX,
96-
"edition": TEDITION,
97-
"service": TSERVICE,
98-
"rpc": TRPC,
99-
"returns": TRETURNS,
100-
"message": TMESSAGE,
101-
"extend": TEXTEND,
102-
"import": TIMPORT,
103-
"package": TPACKAGE,
104-
"option": TOPTION,
105-
"repeated": TREPEATED,
106-
"required": TREQUIRED,
107-
"optional": TOPTIONAL,
108-
"weak": TWEAK,
109-
"public": TPUBLIC,
110-
"oneof": TONEOF,
111-
"map": TMAP,
112-
"reserved": TRESERVED,
113-
"extensions": TEXTENSIONS,
114-
"enum": TENUM,
115-
"stream": TSTREAM,
116-
"group": TGROUP,
99+
"syntax": TSYNTAX,
100+
"edition": TEDITION,
101+
"service": TSERVICE,
102+
"rpc": TRPC,
103+
"returns": TRETURNS,
104+
"message": TMESSAGE,
105+
"extend": TEXTEND,
106+
"import": TIMPORT,
107+
"package": TPACKAGE,
108+
"option": TOPTION,
109+
"repeated": TREPEATED,
110+
"required": TREQUIRED,
111+
"optional": TOPTIONAL,
112+
"weak": TWEAK,
113+
"public": TPUBLIC,
114+
"oneof": TONEOF,
115+
"map": TMAP,
116+
"reserved": TRESERVED,
117+
"extensions": TEXTENSIONS,
118+
"number": TNUMBER,
119+
"full_name": TFULLNAME,
120+
"type": TTYPE,
121+
"declaration": TDECLARATION,
122+
"enum": TENUM,
123+
"stream": TSTREAM,
124+
"group": TGROUP,
117125
}
118126

119127
if t, ok := m[st]; ok {

parser/declaration.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package parser
2+
3+
import (
4+
"github.com/yoheimuta/go-protoparser/v4/lexer/scanner"
5+
"github.com/yoheimuta/go-protoparser/v4/parser/meta"
6+
)
7+
8+
// Declaration is an option of extension ranges.
9+
type Declaration struct {
10+
Number string
11+
FullName string
12+
Type string
13+
Reserved bool
14+
Repeated bool
15+
16+
// Comments are the optional ones placed at the beginning.
17+
Comments []*Comment
18+
// InlineComment is the optional one placed at the ending.
19+
InlineComment *Comment
20+
// InlineCommentBehindLeftCurly is the optional one placed behind a left curly.
21+
InlineCommentBehindLeftCurly *Comment
22+
// Meta is the meta information.
23+
Meta meta.Meta
24+
}
25+
26+
// SetInlineComment implements the HasInlineCommentSetter interface.
27+
func (d *Declaration) SetInlineComment(comment *Comment) {
28+
d.InlineComment = comment
29+
}
30+
31+
// Accept dispatches the call to the visitor.
32+
func (d *Declaration) Accept(v Visitor) {
33+
if !v.VisitDeclaration(d) {
34+
return
35+
}
36+
37+
for _, comment := range d.Comments {
38+
comment.Accept(v)
39+
}
40+
if d.InlineComment != nil {
41+
d.InlineComment.Accept(v)
42+
}
43+
if d.InlineCommentBehindLeftCurly != nil {
44+
d.InlineCommentBehindLeftCurly.Accept(v)
45+
}
46+
}
47+
48+
// ParseDeclaration parses a declaration.
49+
//
50+
// declaration = "declaration" "=" "{"
51+
// "number" ":" number ","
52+
// "full_name" ":" string ","
53+
// "type" ":" string ","
54+
// "repeated" ":" bool ","
55+
// "reserved" ":" bool
56+
// "}"
57+
//
58+
// See https://protobuf.dev/programming-guides/extension_declarations/
59+
func (p *Parser) ParseDeclaration() (*Declaration, error) {
60+
p.lex.NextKeyword()
61+
if p.lex.Token != scanner.TDECLARATION {
62+
return nil, p.unexpected("declaration")
63+
}
64+
startPos := p.lex.Pos
65+
66+
p.lex.Next()
67+
if p.lex.Token != scanner.TEQUALS {
68+
return nil, p.unexpected("=")
69+
}
70+
71+
p.lex.Next()
72+
if p.lex.Token != scanner.TLEFTCURLY {
73+
return nil, p.unexpected("{")
74+
}
75+
76+
inlineLeftCurly := p.parseInlineComment()
77+
78+
var number string
79+
var fullName string
80+
var typeStr string
81+
var repeated bool
82+
var reserved bool
83+
84+
for {
85+
p.lex.Next()
86+
if p.lex.Token == scanner.TRIGHTCURLY {
87+
break
88+
}
89+
if p.lex.Token != scanner.TCOMMA {
90+
p.lex.UnNext()
91+
}
92+
93+
p.lex.NextKeyword()
94+
if p.lex.Token == scanner.TNUMBER {
95+
p.lex.Next()
96+
if p.lex.Token != scanner.TCOLON {
97+
return nil, p.unexpected(":")
98+
}
99+
p.lex.NextNumberLit()
100+
if p.lex.Token != scanner.TINTLIT {
101+
return nil, p.unexpected("number")
102+
}
103+
number = p.lex.Text
104+
} else if p.lex.Token == scanner.TFULLNAME {
105+
p.lex.Next()
106+
if p.lex.Token != scanner.TCOLON {
107+
return nil, p.unexpected(":")
108+
}
109+
p.lex.NextStrLit()
110+
if p.lex.Token != scanner.TSTRLIT {
111+
return nil, p.unexpected("full_name string")
112+
}
113+
fullName = p.lex.Text
114+
} else if p.lex.Token == scanner.TTYPE {
115+
p.lex.Next()
116+
if p.lex.Token != scanner.TCOLON {
117+
return nil, p.unexpected(":")
118+
}
119+
p.lex.NextStrLit()
120+
if p.lex.Token != scanner.TSTRLIT {
121+
return nil, p.unexpected("type string")
122+
}
123+
typeStr = p.lex.Text
124+
} else if p.lex.Token == scanner.TREPEATED {
125+
p.lex.Next()
126+
if p.lex.Token != scanner.TCOLON {
127+
return nil, p.unexpected(":")
128+
}
129+
p.lex.Next()
130+
if p.lex.Token != scanner.TIDENT {
131+
return nil, p.unexpected("repeated bool")
132+
}
133+
repeated = p.lex.Text == "true"
134+
} else if p.lex.Token == scanner.TRESERVED {
135+
p.lex.Next()
136+
if p.lex.Token != scanner.TCOLON {
137+
return nil, p.unexpected(":")
138+
}
139+
p.lex.Next()
140+
if p.lex.Token != scanner.TIDENT {
141+
return nil, p.unexpected("reserved bool")
142+
}
143+
reserved = p.lex.Text == "true"
144+
} else {
145+
return nil, p.unexpected("number, full_name, type, repeated, reserved, or }")
146+
}
147+
}
148+
149+
return &Declaration{
150+
Number: number,
151+
FullName: fullName,
152+
Type: typeStr,
153+
Reserved: reserved,
154+
Repeated: repeated,
155+
InlineCommentBehindLeftCurly: inlineLeftCurly,
156+
Meta: meta.Meta{Pos: startPos.Position, LastPos: p.lex.Pos.Position},
157+
}, nil
158+
}

parser/declaration_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package parser_test
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"testing"
7+
8+
"github.com/yoheimuta/go-protoparser/v4/internal/util_test"
9+
"github.com/yoheimuta/go-protoparser/v4/lexer"
10+
"github.com/yoheimuta/go-protoparser/v4/parser"
11+
"github.com/yoheimuta/go-protoparser/v4/parser/meta"
12+
)
13+
14+
func TestParser_ParseDeclaration(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
input string
18+
wantDeclaration *parser.Declaration
19+
wantErr bool
20+
}{
21+
{
22+
name: "parsing an empty",
23+
wantErr: true,
24+
},
25+
{
26+
name: "parsing an excerpt from the official reference",
27+
input: `declaration = {
28+
number: 4,
29+
full_name: ".my.package.event_annotations",
30+
type: ".logs.proto.ValidationAnnotations",
31+
repeated: true }`,
32+
wantDeclaration: &parser.Declaration{
33+
Number: "4",
34+
FullName: `".my.package.event_annotations"`,
35+
Type: `".logs.proto.ValidationAnnotations"`,
36+
Repeated: true,
37+
Meta: meta.Meta{
38+
Pos: meta.Position{
39+
Offset: 0,
40+
Line: 1,
41+
Column: 1,
42+
},
43+
LastPos: meta.Position{
44+
Offset: 153,
45+
Line: 5,
46+
Column: 22,
47+
},
48+
},
49+
},
50+
},
51+
{
52+
name: "parsing another excerpt from the official reference",
53+
input: `declaration = {
54+
number: 500,
55+
full_name: ".my.package.event_annotations",
56+
type: ".logs.proto.ValidationAnnotations",
57+
reserved: true }`,
58+
wantDeclaration: &parser.Declaration{
59+
Number: "500",
60+
FullName: `".my.package.event_annotations"`,
61+
Type: `".logs.proto.ValidationAnnotations"`,
62+
Reserved: true,
63+
Meta: meta.Meta{
64+
Pos: meta.Position{
65+
Offset: 0,
66+
Line: 1,
67+
Column: 1,
68+
},
69+
LastPos: meta.Position{
70+
Offset: 155,
71+
Line: 5,
72+
Column: 22,
73+
},
74+
},
75+
},
76+
},
77+
}
78+
79+
for _, test := range tests {
80+
test := test
81+
t.Run(test.name, func(t *testing.T) {
82+
p := parser.NewParser(lexer.NewLexer(strings.NewReader(test.input)))
83+
got, err := p.ParseDeclaration()
84+
switch {
85+
case test.wantErr:
86+
if err == nil {
87+
t.Errorf("got err nil, but want err")
88+
}
89+
return
90+
case !test.wantErr && err != nil:
91+
t.Errorf("got err %v, but want nil", err)
92+
return
93+
}
94+
95+
if !reflect.DeepEqual(got, test.wantDeclaration) {
96+
t.Errorf("got %v, but want %v", util_test.PrettyFormat(got), util_test.PrettyFormat(test.wantDeclaration))
97+
}
98+
99+
if !p.IsEOF() {
100+
t.Errorf("got not eof, but want eof")
101+
}
102+
})
103+
}
104+
105+
}

0 commit comments

Comments
 (0)