Skip to content

Commit d186061

Browse files
authored
field: fix missing optional support (#95)
1 parent 8ed22a1 commit d186061

22 files changed

+577
-131
lines changed

Diff for: .travis.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ go_import_path: github.com/lyft/protoc-gen-star
44

55
env:
66
matrix:
7-
- PROTOC_VER="3.5.1"
8-
- PROTOC_VER="3.6.1"
7+
- PROTOC_VER="3.5.0"
8+
- PROTOC_VER="3.6.0"
9+
- PROTOC_VER="3.17.0"
910

1011
before_install:
1112
- mkdir -p $GOPATH/bin

Diff for: Makefile

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# the name of this package
22
PKG := $(shell go list .)
3+
PROTOC_VER := $(shell protoc --version | cut -d' ' -f2)
34

45
.PHONY: bootstrap
56
bootstrap: testdata # set up the project for development
@@ -16,15 +17,27 @@ lint: # lints the package for common code smells
1617

1718
.PHONY: quick
1819
quick: testdata # runs all tests without the race detector or coverage
20+
ifeq ($(PROTOC_VER), 3.17.0)
21+
go test $(PKGS) --tags=proto3_presence
22+
else
1923
go test $(PKGS)
24+
endif
2025

2126
.PHONY: tests
2227
tests: testdata # runs all tests against the package with race detection and coverage percentage
28+
ifeq ($(PROTOC_VER), 3.17.0)
29+
go test -race -cover ./... --tags=proto3_presence
30+
else
2331
go test -race -cover ./...
32+
endif
2433

2534
.PHONY: cover
2635
cover: testdata # runs all tests against the package, generating a coverage report and opening it in the browser
36+
ifeq ($(PROTOC_VER), 3.17.0)
37+
go test -race -covermode=atomic -coverprofile=cover.out ./... --tags=proto3_presence || true
38+
else
2739
go test -race -covermode=atomic -coverprofile=cover.out ./... || true
40+
endif
2841
go tool cover -html cover.out -o cover.html
2942
open cover.html
3043

@@ -76,6 +89,10 @@ testdata-go: protoc-gen-go bin/protoc-gen-debug # generate go-specific testdata
7689
testdata-names \
7790
testdata-packages \
7891
testdata-outputs
92+
ifeq ($(PROTOC_VER), 3.17.0)
93+
cd lang/go && $(MAKE) \
94+
testdata-presence
95+
endif
7996

8097
vendor: # install project dependencies
8198
which glide || (curl https://glide.sh/get | sh)

Diff for: field.go

+44-2
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,29 @@ type Field interface {
1717
Message() Message
1818

1919
// InOneOf returns true if the field is in a OneOf of the parent Message.
20+
// This will return true for synthetic oneofs (proto3 field presence) as well.
2021
InOneOf() bool
2122

22-
// OneOf returns the OneOf that this field is apart of. Nil is returned if
23+
// InRealOneOf returns true if the field is in a OneOf of the parent Message.
24+
// This will return false for synthetic oneofs, and will only include 'real' oneofs.
25+
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/field_presence.md
26+
InRealOneOf() bool
27+
28+
// OneOf returns the OneOf that this field is a part of. Nil is returned if
2329
// the field is not within a OneOf.
2430
OneOf() OneOf
2531

2632
// Type returns the FieldType of this Field.
2733
Type() FieldType
2834

29-
// Required returns whether or not the field is labeled as required. This
35+
// HasPresence returns true for all fields that have explicit presence as defined by:
36+
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/field_presence.md
37+
HasPresence() bool
38+
39+
// HasOptionalKeyword returns whether the field is labeled as optional.
40+
HasOptionalKeyword() bool
41+
42+
// Required returns whether the field is labeled as required. This
3043
// will only be true if the syntax is proto2.
3144
Required() bool
3245

@@ -61,6 +74,35 @@ func (f *field) Type() FieldType { return f.typ }
6174
func (f *field) setMessage(m Message) { f.msg = m }
6275
func (f *field) setOneOf(o OneOf) { f.oneof = o }
6376

77+
func (f *field) InRealOneOf() bool {
78+
return f.InOneOf() && !f.desc.GetProto3Optional()
79+
}
80+
81+
func (f *field) HasPresence() bool {
82+
if f.InOneOf() {
83+
return true
84+
}
85+
86+
if f.Type().IsEmbed() {
87+
return true
88+
}
89+
90+
if !f.Type().IsRepeated() && !f.Type().IsMap() {
91+
if f.Syntax() == Proto2 {
92+
return true
93+
}
94+
return f.HasOptionalKeyword()
95+
}
96+
return false
97+
}
98+
99+
func (f *field) HasOptionalKeyword() bool {
100+
if f.Syntax() == Proto3 {
101+
return f.desc.GetProto3Optional()
102+
}
103+
return f.desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_OPTIONAL
104+
}
105+
64106
func (f *field) Required() bool {
65107
return f.Syntax().SupportsRequiredPrefix() &&
66108
f.desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REQUIRED

Diff for: field_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,58 @@ func TestField_OneOf(t *testing.T) {
9999
assert.True(t, f.InOneOf())
100100
}
101101

102+
func TestField_InRealOneOf(t *testing.T) {
103+
t.Parallel()
104+
105+
f := dummyField()
106+
assert.False(t, f.InRealOneOf())
107+
108+
f = dummyOneOfField(false)
109+
assert.True(t, f.InRealOneOf())
110+
111+
f = dummyOneOfField(true)
112+
assert.False(t, f.InRealOneOf())
113+
}
114+
115+
func TestField_HasPresence(t *testing.T) {
116+
t.Parallel()
117+
118+
f := dummyField()
119+
f.addType(&repT{scalarT: &scalarT{}})
120+
assert.False(t, f.HasPresence())
121+
122+
f.addType(&mapT{repT: &repT{scalarT: &scalarT{}}})
123+
assert.False(t, f.HasPresence())
124+
125+
f.addType(&scalarT{})
126+
assert.False(t, f.HasPresence())
127+
128+
opt := true
129+
f.desc = &descriptor.FieldDescriptorProto{Proto3Optional: &opt}
130+
assert.True(t, f.HasPresence())
131+
}
132+
133+
func TestField_HasOptionalKeyword(t *testing.T) {
134+
t.Parallel()
135+
136+
optLabel := descriptor.FieldDescriptorProto_LABEL_OPTIONAL
137+
138+
f := &field{msg: &msg{parent: dummyFile()}}
139+
assert.False(t, f.HasOptionalKeyword())
140+
141+
f.desc = &descriptor.FieldDescriptorProto{Label: &optLabel}
142+
assert.False(t, f.HasOptionalKeyword())
143+
144+
f = dummyField()
145+
assert.False(t, f.HasOptionalKeyword())
146+
147+
f = dummyOneOfField(false)
148+
assert.False(t, f.HasOptionalKeyword())
149+
150+
f = dummyOneOfField(true)
151+
assert.True(t, f.HasOptionalKeyword())
152+
}
153+
102154
func TestField_Type(t *testing.T) {
103155
t.Parallel()
104156

@@ -194,3 +246,23 @@ func dummyField() *field {
194246
f.addType(t)
195247
return f
196248
}
249+
250+
func dummyOneOfField(synthetic bool) *field {
251+
m := dummyMsg()
252+
o := dummyOneof()
253+
str := descriptor.FieldDescriptorProto_TYPE_STRING
254+
var oIndex int32
255+
oIndex = 1
256+
f := &field{desc: &descriptor.FieldDescriptorProto{
257+
Name: proto.String("field"),
258+
Type: &str,
259+
OneofIndex: &oIndex,
260+
Proto3Optional: &synthetic,
261+
}}
262+
o.addField(f)
263+
m.addField(f)
264+
m.addOneOf(o)
265+
t := &scalarT{}
266+
f.addType(t)
267+
return f
268+
}

Diff for: field_type.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ type FieldType interface {
2222
// repeated fields containing embeds will still return false.
2323
IsEmbed() bool
2424

25-
// IsOptional returns true if the message's syntax is not Proto2 or
26-
// the field is prefixed as optional.
25+
// IsOptional returns true if the field is prefixed as optional.
2726
IsOptional() bool
2827

2928
// IsRequired returns true if and only if the field is prefixed as required.

Diff for: field_type_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ func TestScalarT_Key(t *testing.T) {
8888
func TestScalarT_IsOptional(t *testing.T) {
8989
t.Parallel()
9090

91+
s := &scalarT{}
92+
f := dummyOneOfField(true)
93+
f.addType(s)
94+
95+
assert.True(t, s.IsOptional())
96+
97+
fl := dummyFile()
98+
fl.desc.Syntax = nil
99+
f.Message().setParent(fl)
100+
101+
assert.True(t, s.IsOptional())
102+
103+
req := descriptor.FieldDescriptorProto_LABEL_REQUIRED
104+
f.desc.Label = &req
105+
106+
assert.False(t, s.IsOptional())
107+
}
108+
109+
func TestScalarT_IsNotOptional(t *testing.T) {
110+
t.Parallel()
111+
91112
s := &scalarT{}
92113
f := dummyField()
93114
f.addType(s)

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ require (
66
github.com/golang/protobuf v1.5.2
77
github.com/spf13/afero v1.3.3
88
github.com/stretchr/testify v1.6.1
9+
google.golang.org/protobuf v1.26.0 // indirect
910
)

Diff for: init_option.go

+8
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ func FileSystem(fs afero.Fs) InitOption { return func(g *Generator) { g.persiste
4747
func BiDirectional() InitOption {
4848
return func(g *Generator) { g.workflow = &onceWorkflow{workflow: &standardWorkflow{BiDi: true}} }
4949
}
50+
51+
// SupportedFeatures allows defining protoc features to enable / disable.
52+
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/implementing_proto3_presence.md#signaling-that-your-code-generator-supports-proto3-optional
53+
func SupportedFeatures(feat *uint64) InitOption {
54+
return func(g *Generator) {
55+
g.persister.SetSupportedFeatures(feat)
56+
}
57+
}

Diff for: lang/go/Makefile

+13
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,18 @@ testdata-outputs: ../../bin/protoc-gen-debug
3838
cd -; \
3939
done
4040

41+
testdata-presence: ../../bin/protoc-gen-debug
42+
cd testdata/presence && \
43+
set -e; for subdir in `find . -mindepth 1 -maxdepth 1 -type d`; do \
44+
cd $$subdir; \
45+
params=`cat params`; \
46+
protoc -I . -I .. \
47+
--plugin=protoc-gen-debug=../../../../../bin/protoc-gen-debug \
48+
--debug_out=".:." \
49+
--go_out="$$params:." \
50+
`find . -name "*.proto"`; \
51+
cd -; \
52+
done
53+
4154
../../bin/protoc-gen-debug:
4255
cd ../.. && $(MAKE) bin/protoc-gen-debug

Diff for: lang/go/testdata/presence/types/params

Whitespace-only changes.

Diff for: lang/go/testdata/presence/types/proto3.proto

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
syntax="proto3";
2+
package names.types;
3+
option go_package = "example.com/foo/bar";
4+
5+
import "google/protobuf/duration.proto";
6+
import "google/protobuf/type.proto";
7+
8+
message Proto3 {
9+
double double = 1;
10+
float float = 2;
11+
int64 int64 = 3;
12+
sfixed64 sfixed64 = 4;
13+
sint64 sint64 = 5;
14+
uint64 uint64 = 6;
15+
fixed64 fixed64 = 7;
16+
int32 int32 = 8;
17+
sfixed32 sfixed32 = 9;
18+
sint32 sint32 = 10;
19+
uint32 uint32 = 11;
20+
fixed32 fixed32 = 12;
21+
bool bool = 13;
22+
string string = 14;
23+
bytes bytes = 15;
24+
25+
Enum enum = 16;
26+
google.protobuf.Syntax ext_enum = 17;
27+
Message msg = 18;
28+
google.protobuf.Duration ext_msg = 19;
29+
30+
repeated double repeated_scalar = 20;
31+
repeated Enum repeated_enum = 21;
32+
repeated google.protobuf.Syntax repeated_ext_enum = 22;
33+
repeated Message repeated_msg = 23;
34+
repeated google.protobuf.Duration repeated_ext_msg = 24;
35+
36+
map<string, float> map_scalar = 25;
37+
map<int32, Enum> map_enum = 26;
38+
map<uint64, google.protobuf.Syntax> map_ext_enum = 27;
39+
map<fixed32, Message> map_msg = 28;
40+
map<sfixed64, google.protobuf.Duration> map_ext_msg = 29;
41+
42+
enum Enum {VALUE = 0;}
43+
44+
message Message {}
45+
46+
message Optional {
47+
optional double double = 1;
48+
optional float float = 2;
49+
optional int64 int64 = 3;
50+
optional sfixed64 sfixed64 = 4;
51+
optional sint64 sint64 = 5;
52+
optional uint64 uint64 = 6;
53+
optional fixed64 fixed64 = 7;
54+
optional int32 int32 = 8;
55+
optional sfixed32 sfixed32 = 9;
56+
optional sint32 sint32 = 10;
57+
optional uint32 uint32 = 11;
58+
optional fixed32 fixed32 = 12;
59+
optional bool bool = 13;
60+
optional string string = 14;
61+
optional bytes bytes = 15;
62+
optional Enum enum = 16;
63+
optional google.protobuf.Syntax ext_enum = 17;
64+
optional Optional msg = 18;
65+
optional google.protobuf.Duration ext_msg = 19;
66+
}
67+
}

Diff for: lang/go/type_name.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (c context) Type(f pgs.Field) TypeName {
2525
t = scalarType(ft.ProtoType())
2626
}
2727

28-
if f.Syntax() == pgs.Proto2 {
28+
if f.HasPresence() {
2929
return t.Pointer()
3030
}
3131

0 commit comments

Comments
 (0)