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

field: fix missing optional support #95

Merged
merged 10 commits into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ go_import_path: github.com/lyft/protoc-gen-star

env:
matrix:
- PROTOC_VER="3.5.1"
Copy link
Contributor

Choose a reason for hiding this comment

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

could we please keep the other ones and just add on 3.16.0

Copy link
Contributor

Choose a reason for hiding this comment

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

You may need to use https://mickey.dev/posts/go-build-tags-testing/ (for example) to mark which tests apply to the older protoc versions versus newer. But I think I really don't want to lose test coverage for the old versions, esp since it should be relatively easy to keep them. WDYT.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, this is turning out to be tricker than I anticipated.

I need to first edit the makefiles to ensure they don't compile the proto3 optional bits, then ensure I add the build tags for the golang tests themselves..

I am testing by isolating the testing harness into testdata/proto3_presence for now, but let me know if you prefer a different name.

- PROTOC_VER="3.6.1"
- PROTOC_VER="3.5.0"
- PROTOC_VER="3.6.0"
- PROTOC_VER="3.17.0"

before_install:
- mkdir -p $GOPATH/bin
Expand Down
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# the name of this package
PKG := $(shell go list .)
PROTOC_VER := $(shell protoc --version | cut -d' ' -f2)

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

.PHONY: quick
quick: testdata # runs all tests without the race detector or coverage
ifeq ($(PROTOC_VER), 3.17.0)
go test $(PKGS) --tags=proto3_presence
else
go test $(PKGS)
endif

.PHONY: tests
tests: testdata # runs all tests against the package with race detection and coverage percentage
ifeq ($(PROTOC_VER), 3.17.0)
go test -race -cover ./... --tags=proto3_presence
else
go test -race -cover ./...
endif

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

Expand Down Expand Up @@ -76,6 +89,10 @@ testdata-go: protoc-gen-go bin/protoc-gen-debug # generate go-specific testdata
testdata-names \
testdata-packages \
testdata-outputs
ifeq ($(PROTOC_VER), 3.17.0)
cd lang/go && $(MAKE) \
testdata-presence
endif

vendor: # install project dependencies
which glide || (curl https://glide.sh/get | sh)
Expand Down
46 changes: 44 additions & 2 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,29 @@ type Field interface {
Message() Message

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

// OneOf returns the OneOf that this field is apart of. Nil is returned if
// InRealOneOf returns true if the field is in a OneOf of the parent Message.
// This will return false for synthetic oneofs, and will only include 'real' oneofs.
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/field_presence.md
InRealOneOf() bool

// OneOf returns the OneOf that this field is a part of. Nil is returned if
// the field is not within a OneOf.
OneOf() OneOf

// Type returns the FieldType of this Field.
Type() FieldType

// Required returns whether or not the field is labeled as required. This
// HasPresence returns true for all fields that have explicit presence as defined by:
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/field_presence.md
HasPresence() bool

// HasOptionalKeyword returns whether the field is labeled as optional.
HasOptionalKeyword() bool

// Required returns whether the field is labeled as required. This
// will only be true if the syntax is proto2.
Required() bool

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

func (f *field) InRealOneOf() bool {
return f.InOneOf() && !f.desc.GetProto3Optional()
}

func (f *field) HasPresence() bool {
dkunitsk marked this conversation as resolved.
Show resolved Hide resolved
if f.InOneOf() {
return true
}

if f.Type().IsEmbed() {
return true
}

if !f.Type().IsRepeated() && !f.Type().IsMap() {
if f.Syntax() == Proto2 {
return true
}
return f.HasOptionalKeyword()
}
return false
}

func (f *field) HasOptionalKeyword() bool {
if f.Syntax() == Proto3 {
dkunitsk marked this conversation as resolved.
Show resolved Hide resolved
return f.desc.GetProto3Optional()
}
return f.desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_OPTIONAL
}

func (f *field) Required() bool {
return f.Syntax().SupportsRequiredPrefix() &&
f.desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REQUIRED
Expand Down
72 changes: 72 additions & 0 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,58 @@ func TestField_OneOf(t *testing.T) {
assert.True(t, f.InOneOf())
}

func TestField_InRealOneOf(t *testing.T) {
t.Parallel()

f := dummyField()
assert.False(t, f.InRealOneOf())

f = dummyOneOfField(false)
assert.True(t, f.InRealOneOf())

f = dummyOneOfField(true)
assert.False(t, f.InRealOneOf())
}

func TestField_HasPresence(t *testing.T) {
t.Parallel()

f := dummyField()
f.addType(&repT{scalarT: &scalarT{}})
assert.False(t, f.HasPresence())

f.addType(&mapT{repT: &repT{scalarT: &scalarT{}}})
assert.False(t, f.HasPresence())

f.addType(&scalarT{})
assert.False(t, f.HasPresence())

opt := true
f.desc = &descriptor.FieldDescriptorProto{Proto3Optional: &opt}
assert.True(t, f.HasPresence())
}

func TestField_HasOptionalKeyword(t *testing.T) {
t.Parallel()

optLabel := descriptor.FieldDescriptorProto_LABEL_OPTIONAL

f := &field{msg: &msg{parent: dummyFile()}}
dkunitsk marked this conversation as resolved.
Show resolved Hide resolved
assert.False(t, f.HasOptionalKeyword())

f.desc = &descriptor.FieldDescriptorProto{Label: &optLabel}
assert.False(t, f.HasOptionalKeyword())

f = dummyField()
assert.False(t, f.HasOptionalKeyword())

f = dummyOneOfField(false)
assert.False(t, f.HasOptionalKeyword())

f = dummyOneOfField(true)
assert.True(t, f.HasOptionalKeyword())
}

func TestField_Type(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -194,3 +246,23 @@ func dummyField() *field {
f.addType(t)
return f
}

func dummyOneOfField(synthetic bool) *field {
dkunitsk marked this conversation as resolved.
Show resolved Hide resolved
m := dummyMsg()
o := dummyOneof()
str := descriptor.FieldDescriptorProto_TYPE_STRING
var oIndex int32
oIndex = 1
f := &field{desc: &descriptor.FieldDescriptorProto{
Name: proto.String("field"),
Type: &str,
OneofIndex: &oIndex,
Proto3Optional: &synthetic,
}}
o.addField(f)
m.addField(f)
m.addOneOf(o)
t := &scalarT{}
f.addType(t)
return f
}
3 changes: 1 addition & 2 deletions field_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ type FieldType interface {
// repeated fields containing embeds will still return false.
IsEmbed() bool

// IsOptional returns true if the message's syntax is not Proto2 or
// the field is prefixed as optional.
// IsOptional returns true if the field is prefixed as optional.
IsOptional() bool

// IsRequired returns true if and only if the field is prefixed as required.
Expand Down
21 changes: 21 additions & 0 deletions field_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ func TestScalarT_Key(t *testing.T) {
func TestScalarT_IsOptional(t *testing.T) {
t.Parallel()

s := &scalarT{}
f := dummyOneOfField(true)
f.addType(s)

assert.True(t, s.IsOptional())

fl := dummyFile()
fl.desc.Syntax = nil
f.Message().setParent(fl)

assert.True(t, s.IsOptional())

req := descriptor.FieldDescriptorProto_LABEL_REQUIRED
f.desc.Label = &req

assert.False(t, s.IsOptional())
}

func TestScalarT_IsNotOptional(t *testing.T) {
t.Parallel()

s := &scalarT{}
f := dummyField()
f.addType(s)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ require (
github.com/golang/protobuf v1.5.2
github.com/spf13/afero v1.3.3
github.com/stretchr/testify v1.6.1
google.golang.org/protobuf v1.26.0 // indirect
)
8 changes: 8 additions & 0 deletions init_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ func FileSystem(fs afero.Fs) InitOption { return func(g *Generator) { g.persiste
func BiDirectional() InitOption {
return func(g *Generator) { g.workflow = &onceWorkflow{workflow: &standardWorkflow{BiDi: true}} }
}

// SupportedFeatures allows defining protoc features to enable / disable.
// See: https://github.com/protocolbuffers/protobuf/blob/v3.17.0/docs/implementing_proto3_presence.md#signaling-that-your-code-generator-supports-proto3-optional
func SupportedFeatures(feat *uint64) InitOption {
return func(g *Generator) {
g.persister.SetSupportedFeatures(feat)
}
}
13 changes: 13 additions & 0 deletions lang/go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,18 @@ testdata-outputs: ../../bin/protoc-gen-debug
cd -; \
done

testdata-presence: ../../bin/protoc-gen-debug
cd testdata/presence && \
set -e; for subdir in `find . -mindepth 1 -maxdepth 1 -type d`; do \
cd $$subdir; \
params=`cat params`; \
protoc -I . -I .. \
--plugin=protoc-gen-debug=../../../../../bin/protoc-gen-debug \
--debug_out=".:." \
--go_out="$$params:." \
`find . -name "*.proto"`; \
cd -; \
done

../../bin/protoc-gen-debug:
cd ../.. && $(MAKE) bin/protoc-gen-debug
Empty file.
67 changes: 67 additions & 0 deletions lang/go/testdata/presence/types/proto3.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
syntax="proto3";
package names.types;
option go_package = "example.com/foo/bar";

import "google/protobuf/duration.proto";
import "google/protobuf/type.proto";

message Proto3 {
double double = 1;
float float = 2;
int64 int64 = 3;
sfixed64 sfixed64 = 4;
sint64 sint64 = 5;
uint64 uint64 = 6;
fixed64 fixed64 = 7;
int32 int32 = 8;
sfixed32 sfixed32 = 9;
sint32 sint32 = 10;
uint32 uint32 = 11;
fixed32 fixed32 = 12;
bool bool = 13;
string string = 14;
bytes bytes = 15;

Enum enum = 16;
google.protobuf.Syntax ext_enum = 17;
Message msg = 18;
google.protobuf.Duration ext_msg = 19;

repeated double repeated_scalar = 20;
repeated Enum repeated_enum = 21;
repeated google.protobuf.Syntax repeated_ext_enum = 22;
repeated Message repeated_msg = 23;
repeated google.protobuf.Duration repeated_ext_msg = 24;

map<string, float> map_scalar = 25;
map<int32, Enum> map_enum = 26;
map<uint64, google.protobuf.Syntax> map_ext_enum = 27;
map<fixed32, Message> map_msg = 28;
map<sfixed64, google.protobuf.Duration> map_ext_msg = 29;

enum Enum {VALUE = 0;}

message Message {}

message Optional {
optional double double = 1;
optional float float = 2;
optional int64 int64 = 3;
optional sfixed64 sfixed64 = 4;
optional sint64 sint64 = 5;
optional uint64 uint64 = 6;
optional fixed64 fixed64 = 7;
optional int32 int32 = 8;
optional sfixed32 sfixed32 = 9;
optional sint32 sint32 = 10;
optional uint32 uint32 = 11;
optional fixed32 fixed32 = 12;
optional bool bool = 13;
optional string string = 14;
optional bytes bytes = 15;
optional Enum enum = 16;
optional google.protobuf.Syntax ext_enum = 17;
optional Optional msg = 18;
optional google.protobuf.Duration ext_msg = 19;
}
}
2 changes: 1 addition & 1 deletion lang/go/type_name.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (c context) Type(f pgs.Field) TypeName {
t = scalarType(ft.ProtoType())
}

if f.Syntax() == pgs.Proto2 {
if f.HasPresence() {
return t.Pointer()
}

Expand Down
Loading