Repository: jhump/protoreflect (go)

Issue:
protoparse: index out of range in validateEnum
I've seen a couple of panics in my project (which uses `dynamic` to unmarshal unknown messages). And so have been working on some fuzzing harnesses to try and pin down a small reproducible example. I haven't found my issue yet but did find this index out of range panic in `protoparse`.

Would you be interested in me opening a PR with the basic fuzzing harnesses I've got?
A service like fuzzbuzz.io/fuzzit.dev might be interesting to add to this project to run fuzzing continuously (with a lot more processing power than my laptop...). They're offering free CPU for open source projects and I'd be happy to help integrate it if you want 🙂 

Fuzz harness:
```go
package fuzz

import (
	"bytes"
	"github.com/jhump/protoreflect/desc/protoparse"
	"io"
	"io/ioutil"
)

func FuzzParser(data []byte) int {
	files := bytes.Split(data, []byte("\n\n"))
	fileNum := 0

	parser := protoparse.Parser{
		Accessor: func(_ string) (closer io.ReadCloser, e error) {
			if fileNum >= len(files) {
				fileNum = 0
			}

			return ioutil.NopCloser(bytes.NewReader(files[fileNum])), nil
		},
	}

	_, err := parser.ParseFiles("foo")
	if err != nil {
		return 0
	}

	return 1
}
```

Crashing input:
```proto
syntax="proto3";enum o{C}
```

Stack trace:
```
panic: runtime error: index out of range

goroutine 1 [running]:
github.com/jhump/protoreflect/desc/protoparse.validateEnum.func1(0xc00010ec00, 0xc00014aa38)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:1362 +0x6a
github.com/jhump/protoreflect/desc/protoparse.validateEnum(0xc0000713e0, 0xc00014aa01, 0x0, 0x0, 0xc00010ec00, 0xc000071440, 0xc0000b1a68)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:1362 +0x177e
github.com/jhump/protoreflect/desc/protoparse.basicValidate(0xc0000713e0)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:1169 +0x1f2
github.com/jhump/protoreflect/desc/protoparse.parseProto(0x1316269, 0x3, 0x1694308, 0xc000117a60, 0xc00000aca0, 0x1357901, 0xc000071320)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:625 +0x134
github.com/jhump/protoreflect/desc/protoparse.parseProtoFile.func1(0x1358200, 0xc000117a60, 0x1316269, 0x3, 0xc00000aca0, 0x1, 0xc0000b1b88)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:413 +0xe8
github.com/jhump/protoreflect/desc/protoparse.parseProtoFile(0xc0000712c0, 0x1316269, 0x3, 0x0, 0xc00000aca0, 0x101, 0xc0000b1d30)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:414 +0xb06
github.com/jhump/protoreflect/desc/protoparse.parseProtoFiles(0xc0000712c0, 0xc0000b1e60, 0x1, 0x1, 0xc00000aca0, 0x2220101, 0xc0000b1d30)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:392 +0xdb
github.com/jhump/protoreflect/desc/protoparse.Parser.ParseFiles(0x0, 0x0, 0x0, 0x0, 0xc0000712c0, 0x0, 0x0, 0xc0000b1e60, 0x1, 0x1, ...)
	$GOPATH/src/github.com/jhump/protoreflect/desc/protoparse/parser.go:190 +0x202
github.com/jhump/protoreflect/internal/fuzz.FuzzParser(0x2220000, 0x19, 0x200000, 0x4)
	$GOPATH/src/github.com/jhump/protoreflect/internal/fuzz/parser.go:24 +0x203
go-fuzz-dep.Main(0xc0000b1f80, 0x1, 0x1)
	/var/folders/6m/r713l1xj6_dcykbx21svs09h0000gp/T/go-fuzz-build860565645/goroot/src/go-fuzz-dep/main.go:36 +0x1b6
main.main()
	/var/folders/6m/r713l1xj6_dcykbx21svs09h0000gp/T/go-fuzz-build860565645/gopath/src/github.com/jhump/protoreflect/internal/fuzz/go.fuzz.main/main.go:15 +0x52
exit status 2
```

Pull request:
fix panic in protoparse
Fixes #271 

Patch:
diff --git a/desc/protoparse/errors.go b/desc/protoparse/errors.go
index 07a6214..5dcf6ac 100644
--- a/desc/protoparse/errors.go
+++ b/desc/protoparse/errors.go
@@ -24,7 +24,7 @@ func defaultErrorReporter(err ErrorWithPos) error {
 
 type errorHandler struct {
 	reporter     ErrorReporter
-	errsReported bool
+	errsReported int
 	err          error
 }
 
@@ -42,7 +42,7 @@ func (h *errorHandler) handleError(err error) error {
 		return h.err
 	}
 	if ewp, ok := err.(ErrorWithPos); ok {
-		h.errsReported = true
+		h.errsReported++
 		err = h.reporter(ewp)
 	}
 	h.err = err
@@ -50,7 +50,7 @@ func (h *errorHandler) handleError(err error) error {
 }
 
 func (h *errorHandler) getError() error {
-	if h.errsReported && h.err == nil {
+	if h.errsReported > 0 && h.err == nil {
 		return ErrInvalidSource
 	}
 	return h.err
diff --git a/desc/protoparse/parser.go b/desc/protoparse/parser.go
index 6eb1acc..bdc55d4 100644
--- a/desc/protoparse/parser.go
+++ b/desc/protoparse/parser.go
@@ -436,7 +436,7 @@ func parseProtoFile(acc FileAccessor, filename string, importLoc *SourcePos, err
 
 	parsed.add(filename, result)
 
-	if errs.getError() != nil {
+	if errs.err != nil {
 		return // abort
 	}
 
@@ -543,6 +543,13 @@ func (r *parseResult) getMessageReservedRangeNode(rr *dpb.DescriptorProto_Reserv
 	return r.nodes[rr].(rangeDecl)
 }
 
+func (r *parseResult) getEnumNode(e *dpb.EnumDescriptorProto) node {
+	if r.nodes == nil {
+		return noSourceNode{pos: unknownPos(r.fd.GetName())}
+	}
+	return r.nodes[e]
+}
+
 func (r *parseResult) getEnumValueNode(e *dpb.EnumValueDescriptorProto) enumValueDecl {
 	if r.nodes == nil {
 		return noSourceNode{pos: unknownPos(r.fd.GetName())}
@@ -617,12 +624,13 @@ func (r *parseResult) putMethodNode(m *dpb.MethodDescriptorProto, n *methodNode)
 }
 
 func parseProto(filename string, r io.Reader, errs *errorHandler, validate bool) *parseResult {
+	beforeErrs := errs.errsReported
 	lx := newLexer(r, filename, errs)
 	protoParse(lx)
 
 	res := createParseResult(filename, lx.res, errs)
-	if validate {
-		basicValidate(res)
+	if validate && errs.err == nil {
+		basicValidate(res, errs.errsReported > beforeErrs)
 	}
 
 	return res
@@ -1155,18 +1163,18 @@ func writeEscapedBytes(buf *bytes.Buffer, b []byte) {
 	}
 }
 
-func basicValidate(res *parseResult) {
+func basicValidate(res *parseResult, containsErrors bool) {
 	fd := res.fd
 	isProto3 := fd.GetSyntax() == "proto3"
 
 	for _, md := range fd.MessageType {
-		if validateMessage(res, isProto3, "", md) != nil {
+		if validateMessage(res, isProto3, "", md, containsErrors) != nil {
 			return
 		}
 	}
 
 	for _, ed := range fd.EnumType {
-		if validateEnum(res, isProto3, "", ed) != nil {
+		if validateEnum(res, isProto3, "", ed, containsErrors) != nil {
 			return
 		}
 	}
@@ -1178,7 +1186,7 @@ func basicValidate(res *parseResult) {
 	}
 }
 
-func validateMessage(res *parseResult, isProto3 bool, prefix string, md *dpb.DescriptorProto) error {
+func validateMessage(res *parseResult, isProto3 bool, prefix string, md *dpb.DescriptorProto, containsErrors bool) error {
 	nextPrefix := md.GetName() + "."
 
 	for _, fld := range md.Field {
@@ -1192,12 +1200,12 @@ func validateMessage(res *parseResult, isProto3 bool, prefix string, md *dpb.Des
 		}
 	}
 	for _, ed := range md.EnumType {
-		if err := validateEnum(res, isProto3, nextPrefix, ed); err != nil {
+		if err := validateEnum(res, isProto3, nextPrefix, ed, containsErrors); err != nil {
 			return err
 		}
 	}
 	for _, nmd := range md.NestedType {
-		if err := validateMessage(res, isProto3, nextPrefix, nmd); err != nil {
+		if err := validateMessage(res, isProto3, nextPrefix, nmd, containsErrors); err != nil {
 			return err
 		}
 	}
@@ -1332,9 +1340,20 @@ func validateMessage(res *parseResult, isProto3 bool, prefix string, md *dpb.Des
 	return nil
 }
 
-func validateEnum(res *parseResult, isProto3 bool, prefix string, ed *dpb.EnumDescriptorProto) error {
+func validateEnum(res *parseResult, isProto3 bool, prefix string, ed *dpb.EnumDescriptorProto, containsErrors bool) error {
 	scope := fmt.Sprintf("enum %s%s", prefix, ed.GetName())
 
+	if !containsErrors && len(ed.Value) == 0 {
+		// we only check this if file parsing had no errors; otherwise, the file may have
+		// had an enum value, but the parser encountered an error processing it, in which
+		// case the value would be absent from the descriptor. In such a case, this error
+		// would be confusing and incorrect, so we just skip this check.
+		enNode := res.getEnumNode(ed)
+		if err := res.errs.handleError(ErrorWithSourcePos{Pos: enNode.start(), Underlying: fmt.Errorf("%s: enums must define at least one value", scope)}); err != nil {
+			return err
+		}
+	}
+
 	allowAlias := false
 	if index, err := findOption(res, scope, ed.Options.GetUninterpretedOption(), "allow_alias"); err != nil {
 		if err := res.errs.handleError(err); err != nil {
@@ -1359,7 +1378,7 @@ func validateEnum(res *parseResult, isProto3 bool, prefix string, ed *dpb.EnumDe
 		}
 	}
 
-	if isProto3 && ed.Value[0].GetNumber() != 0 {
+	if isProto3 && len(ed.Value) > 0 && ed.Value[0].GetNumber() != 0 {
 		evNode := res.getEnumValueNode(ed.Value[0])
 		if err := res.errs.handleError(ErrorWithSourcePos{Pos: evNode.getNumber().start(), Underlying: fmt.Errorf("%s: proto3 requires that first value in enum have numeric value of 0", scope)}); err != nil {
 			return err
diff --git a/desc/protoparse/proto.y b/desc/protoparse/proto.y
index a795fe7..4619998 100644
--- a/desc/protoparse/proto.y
+++ b/desc/protoparse/proto.y
@@ -755,15 +755,6 @@ fieldNames : fieldNames ',' stringLit {
 	}
 
 enum : _ENUM name '{' enumBody '}' {
-		c := 0
-		for _, el := range $4 {
-			if el.value != nil {
-				c++
-			}
-		}
-		if c == 0 {
-			lexError(protolex, $1.start(), "enums must define at least one value")
-		}
 		$$ = &enumNode{name: $2, decls: $4}
 		$$.setRange($1, $5)
 	}
diff --git a/desc/protoparse/proto.y.go b/desc/protoparse/proto.y.go
index 843df01..dbd72c0 100644
--- a/desc/protoparse/proto.y.go
+++ b/desc/protoparse/proto.y.go
@@ -198,7 +198,7 @@ const protoEofCode = 1
 const protoErrCode = 2
 const protoInitialStackSize = 16
 
-//line proto.y:1005
+//line proto.y:996
 
 //line yacctab:1
 var protoExca = [...]int{
@@ -2025,67 +2025,58 @@ protodefault:
 		protoDollar = protoS[protopt-5 : protopt+1]
 //line proto.y:757
 		{
-			c := 0
-			for _, el := range protoDollar[4].enDecls {
-				if el.value != nil {
-					c++
-				}
-			}
-			if c == 0 {
-				lexError(protolex, protoDollar[1].id.start(), "enums must define at least one value")
-			}
 			protoVAL.en = &enumNode{name: protoDollar[2].id, decls: protoDollar[4].enDecls}
 			protoVAL.en.setRange(protoDollar[1].id, protoDollar[5].b)
 		}
 	case 148:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:771
+//line proto.y:762
 		{
 			protoVAL.enDecls = append(protoDollar[1].enDecls, protoDollar[2].enDecls...)
 		}
 	case 150:
 		protoDollar = protoS[protopt-0 : protopt+1]
-//line proto.y:775
+//line proto.y:766
 		{
 			protoVAL.enDecls = nil
 		}
 	case 151:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:779
+//line proto.y:770
 		{
 			protoVAL.enDecls = []*enumElement{{option: protoDollar[1].opts[0]}}
 		}
 	case 152:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:782
+//line proto.y:773
 		{
 			protoVAL.enDecls = []*enumElement{{value: protoDollar[1].env}}
 		}
 	case 153:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:785
+//line proto.y:776
 		{
 			protoVAL.enDecls = []*enumElement{{reserved: protoDollar[1].resvd}}
 		}
 	case 154:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:788
+//line proto.y:779
 		{
 			protoVAL.enDecls = []*enumElement{{empty: protoDollar[1].b}}
 		}
 	case 155:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:791
+//line proto.y:782
 		{
 		}
 	case 156:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:793
+//line proto.y:784
 		{
 		}
 	case 157:
 		protoDollar = protoS[protopt-4 : protopt+1]
-//line proto.y:796
+//line proto.y:787
 		{
 			checkInt64InInt32Range(protolex, protoDollar[3].il.start(), protoDollar[3].il.val)
 			protoVAL.env = &enumValueNode{name: protoDollar[1].id, number: protoDollar[3].il}
@@ -2093,7 +2084,7 @@ protodefault:
 		}
 	case 158:
 		protoDollar = protoS[protopt-5 : protopt+1]
-//line proto.y:801
+//line proto.y:792
 		{
 			checkInt64InInt32Range(protolex, protoDollar[3].il.start(), protoDollar[3].il.val)
 			protoVAL.env = &enumValueNode{name: protoDollar[1].id, number: protoDollar[3].il, options: protoDollar[4].cmpctOpts}
@@ -2101,102 +2092,102 @@ protodefault:
 		}
 	case 159:
 		protoDollar = protoS[protopt-5 : protopt+1]
-//line proto.y:807
+//line proto.y:798
 		{
 			protoVAL.msg = &messageNode{name: protoDollar[2].id, decls: protoDollar[4].msgDecls}
 			protoVAL.msg.setRange(protoDollar[1].id, protoDollar[5].b)
 		}
 	case 160:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:812
+//line proto.y:803
 		{
 			protoVAL.msgDecls = append(protoDollar[1].msgDecls, protoDollar[2].msgDecls...)
 		}
 	case 162:
 		protoDollar = protoS[protopt-0 : protopt+1]
-//line proto.y:816
+//line proto.y:807
 		{
 			protoVAL.msgDecls = nil
 		}
 	case 163:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:820
+//line proto.y:811
 		{
 			protoVAL.msgDecls = []*messageElement{{field: protoDollar[1].fld}}
 		}
 	case 164:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:823
+//line proto.y:814
 		{
 			protoVAL.msgDecls = []*messageElement{{enum: protoDollar[1].en}}
 		}
 	case 165:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:826
+//line proto.y:817
 		{
 			protoVAL.msgDecls = []*messageElement{{nested: protoDollar[1].msg}}
 		}
 	case 166:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:829
+//line proto.y:820
 		{
 			protoVAL.msgDecls = []*messageElement{{extend: protoDollar[1].extend}}
 		}
 	case 167:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:832
+//line proto.y:823
 		{
 			protoVAL.msgDecls = []*messageElement{{extensionRange: protoDollar[1].ext}}
 		}
 	case 168:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:835
+//line proto.y:826
 		{
 			protoVAL.msgDecls = []*messageElement{{group: protoDollar[1].grp}}
 		}
 	case 169:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:838
+//line proto.y:829
 		{
 			protoVAL.msgDecls = []*messageElement{{option: protoDollar[1].opts[0]}}
 		}
 	case 170:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:841
+//line proto.y:832
 		{
 			protoVAL.msgDecls = []*messageElement{{oneOf: protoDollar[1].oo}}
 		}
 	case 171:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:844
+//line proto.y:835
 		{
 			protoVAL.msgDecls = []*messageElement{{mapField: protoDollar[1].mapFld}}
 		}
 	case 172:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:847
+//line proto.y:838
 		{
 			protoVAL.msgDecls = []*messageElement{{reserved: protoDollar[1].resvd}}
 		}
 	case 173:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:850
+//line proto.y:841
 		{
 			protoVAL.msgDecls = []*messageElement{{empty: protoDollar[1].b}}
 		}
 	case 174:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:853
+//line proto.y:844
 		{
 		}
 	case 175:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:855
+//line proto.y:846
 		{
 		}
 	case 176:
 		protoDollar = protoS[protopt-5 : protopt+1]
-//line proto.y:858
+//line proto.y:849
 		{
 			c := 0
 			for _, el := range protoDollar[4].extDecls {
@@ -2212,151 +2203,151 @@ protodefault:
 		}
 	case 177:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:872
+//line proto.y:863
 		{
 			protoVAL.extDecls = append(protoDollar[1].extDecls, protoDollar[2].extDecls...)
 		}
 	case 179:
 		protoDollar = protoS[protopt-0 : protopt+1]
-//line proto.y:876
+//line proto.y:867
 		{
 			protoVAL.extDecls = nil
 		}
 	case 180:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:880
+//line proto.y:871
 		{
 			protoVAL.extDecls = []*extendElement{{field: protoDollar[1].fld}}
 		}
 	case 181:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:883
+//line proto.y:874
 		{
 			protoVAL.extDecls = []*extendElement{{group: protoDollar[1].grp}}
 		}
 	case 182:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:886
+//line proto.y:877
 		{
 			protoVAL.extDecls = []*extendElement{{empty: protoDollar[1].b}}
 		}
 	case 183:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:889
+//line proto.y:880
 		{
 		}
 	case 184:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:891
+//line proto.y:882
 		{
 		}
 	case 185:
 		protoDollar = protoS[protopt-5 : protopt+1]
-//line proto.y:894
+//line proto.y:885
 		{
 			protoVAL.svc = &serviceNode{name: protoDollar[2].id, decls: protoDollar[4].svcDecls}
 			protoVAL.svc.setRange(protoDollar[1].id, protoDollar[5].b)
 		}
 	case 186:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:899
+//line proto.y:890
 		{
 			protoVAL.svcDecls = append(protoDollar[1].svcDecls, protoDollar[2].svcDecls...)
 		}
 	case 188:
 		protoDollar = protoS[protopt-0 : protopt+1]
-//line proto.y:903
+//line proto.y:894
 		{
 			protoVAL.svcDecls = nil
 		}
 	case 189:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:910
+//line proto.y:901
 		{
 			protoVAL.svcDecls = []*serviceElement{{option: protoDollar[1].opts[0]}}
 		}
 	case 190:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:913
+//line proto.y:904
 		{
 			protoVAL.svcDecls = []*serviceElement{{rpc: protoDollar[1].mtd}}
 		}
 	case 191:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:916
+//line proto.y:907
 		{
 			protoVAL.svcDecls = []*serviceElement{{empty: protoDollar[1].b}}
 		}
 	case 192:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:919
+//line proto.y:910
 		{
 		}
 	case 193:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:921
+//line proto.y:912
 		{
 		}
 	case 194:
 		protoDollar = protoS[protopt-10 : protopt+1]
-//line proto.y:924
+//line proto.y:915
 		{
 			protoVAL.mtd = &methodNode{name: protoDollar[2].id, input: protoDollar[4].rpcType, output: protoDollar[8].rpcType}
 			protoVAL.mtd.setRange(protoDollar[1].id, protoDollar[10].b)
 		}
 	case 195:
 		protoDollar = protoS[protopt-12 : protopt+1]
-//line proto.y:928
+//line proto.y:919
 		{
 			protoVAL.mtd = &methodNode{name: protoDollar[2].id, input: protoDollar[4].rpcType, output: protoDollar[8].rpcType, options: protoDollar[11].opts}
 			protoVAL.mtd.setRange(protoDollar[1].id, protoDollar[12].b)
 		}
 	case 196:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:933
+//line proto.y:924
 		{
 			protoVAL.rpcType = &rpcTypeNode{msgType: protoDollar[2].cid, streamKeyword: protoDollar[1].id}
 			protoVAL.rpcType.setRange(protoDollar[1].id, protoDollar[2].cid)
 		}
 	case 197:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:937
+//line proto.y:928
 		{
 			protoVAL.rpcType = &rpcTypeNode{msgType: protoDollar[1].cid}
 			protoVAL.rpcType.setRange(protoDollar[1].cid, protoDollar[1].cid)
 		}
 	case 198:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:942
+//line proto.y:933
 		{
 			protoVAL.opts = append(protoDollar[1].opts, protoDollar[2].opts...)
 		}
 	case 200:
 		protoDollar = protoS[protopt-0 : protopt+1]
-//line proto.y:946
+//line proto.y:937
 		{
 			protoVAL.opts = []*optionNode{}
 		}
 	case 201:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:950
+//line proto.y:941
 		{
 			protoVAL.opts = protoDollar[1].opts
 		}
 	case 202:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:953
+//line proto.y:944
 		{
 			protoVAL.opts = []*optionNode{}
 		}
 	case 203:
 		protoDollar = protoS[protopt-2 : protopt+1]
-//line proto.y:956
+//line proto.y:947
 		{
 		}
 	case 204:
 		protoDollar = protoS[protopt-1 : protopt+1]
-//line proto.y:958
+//line proto.y:949
 		{
 		}
 	}


Test patch:
diff --git a/desc/protoparse/error_reporting_test.go b/desc/protoparse/error_reporting_test.go
index c74d0a3..9c6e84a 100644
--- a/desc/protoparse/error_reporting_test.go
+++ b/desc/protoparse/error_reporting_test.go
@@ -47,6 +47,7 @@ func TestErrorReporting(t *testing.T) {
 					syntax = "proto";
 					package foo
 
+					enum State { A = 0; B = 1; C; D }
 					message Foo {
 						foo = 1;
 					}
@@ -54,8 +55,9 @@ func TestErrorReporting(t *testing.T) {
 			},
 			expectedErrs: []string{
 				"test.proto:2:50: syntax value must be 'proto2' or 'proto3'",
-				"test.proto:5:41: syntax error: unexpected \"message\", expecting ';' or '.'",
-				"test.proto:6:53: syntax error: unexpected '='",
+				"test.proto:5:41: syntax error: unexpected \"enum\", expecting ';' or '.'",
+				"test.proto:5:69: syntax error: unexpected ';', expecting '='",
+				"test.proto:7:53: syntax error: unexpected '='",
 			},
 		},
 		{
@@ -67,6 +69,7 @@ func TestErrorReporting(t *testing.T) {
 					message Foo {
 						string foo = 0;
 					}
+					enum State { C }
 					enum Bar {
 						BAZ = 1;
 						BUZZ = 1;
@@ -75,8 +78,9 @@ func TestErrorReporting(t *testing.T) {
 			},
 			expectedErrs: []string{
 				"test.proto:4:62: tag number 0 must be greater than zero",
-				"test.proto:7:55: enum Bar: proto3 requires that first value in enum have numeric value of 0",
-				"test.proto:8:56: enum Bar: values BAZ and BUZZ both have the same numeric value 1; use allow_alias option if intentional",
+				"test.proto:6:56: syntax error: unexpected '}', expecting '='",
+				"test.proto:8:55: enum Bar: proto3 requires that first value in enum have numeric value of 0",
+				"test.proto:9:56: enum Bar: values BAZ and BUZZ both have the same numeric value 1; use allow_alias option if intentional",
 			},
 		},
 		{
diff --git a/desc/protoparse/parser_test.go b/desc/protoparse/parser_test.go
index 1811e8e..f6b9ded 100644
--- a/desc/protoparse/parser_test.go
+++ b/desc/protoparse/parser_test.go
@@ -258,7 +258,7 @@ func TestBasicValidation(t *testing.T) {
 		},
 		{
 			contents: `enum Foo { }`,
-			errMsg:   `test.proto:1:1: enums must define at least one value`,
+			errMsg:   `test.proto:1:1: enum Foo: enums must define at least one value`,
 		},
 		{
 			contents: `message Foo { oneof Bar { } }`,