Skip to content

Commit e606fac

Browse files
authored
verify that reserved names are valid identifiers (#65)
1 parent 94b7e85 commit e606fac

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

parser/validate.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ func validateMessage(res *result, isProto3 bool, name protoreflect.FullName, md
203203
// or reserved ranges or reserved names
204204
rsvdNames := map[string]struct{}{}
205205
for _, n := range md.ReservedName {
206+
// validate reserved name while we're here
207+
if !isIdentifier(n) {
208+
node := findMessageReservedNameNode(res.MessageNode(md), n)
209+
nodeInfo := res.file.NodeInfo(node)
210+
if err := handler.HandleErrorf(nodeInfo.Start(), "%s: reserved name %q is not a valid identifier", scope, n); err != nil {
211+
return err
212+
}
213+
}
206214
rsvdNames[n] = struct{}{}
207215
}
208216
fieldTags := map[int32]string{}
@@ -242,6 +250,60 @@ func validateMessage(res *result, isProto3 bool, name protoreflect.FullName, md
242250
return nil
243251
}
244252

253+
func isIdentifier(s string) bool {
254+
if len(s) == 0 {
255+
return false
256+
}
257+
for i, r := range s {
258+
if i == 0 && r >= '0' && r <= '9' {
259+
// can't start with number
260+
return false
261+
}
262+
// alphanumeric and underscore ok; everything else bad
263+
switch {
264+
case r >= '0' && r <= '9':
265+
case r >= 'a' && r <= 'z':
266+
case r >= 'A' && r <= 'Z':
267+
case r == '_':
268+
default:
269+
return false
270+
}
271+
}
272+
return true
273+
}
274+
275+
func findMessageReservedNameNode(msgNode ast.MessageDeclNode, name string) ast.Node {
276+
var decls []ast.MessageElement
277+
switch msgNode := msgNode.(type) {
278+
case *ast.MessageNode:
279+
decls = msgNode.Decls
280+
case *ast.GroupNode:
281+
decls = msgNode.Decls
282+
default:
283+
// leave decls empty
284+
}
285+
return findReservedNameNode(msgNode, decls, name)
286+
}
287+
288+
func findReservedNameNode[T ast.Node](parent ast.Node, decls []T, name string) ast.Node {
289+
for _, decl := range decls {
290+
// NB: We have to convert to empty interface first, before we can do a type
291+
// assertion because type assertions on type parameters aren't allowed. (The
292+
// compiler cannot yet know whether T is an interface type or not.)
293+
rsvd, ok := any(decl).(*ast.ReservedNode)
294+
if !ok {
295+
continue
296+
}
297+
for _, rsvdName := range rsvd.Names {
298+
if rsvdName.AsString() == name {
299+
return rsvdName
300+
}
301+
}
302+
}
303+
// couldn't find it? Instead of puking, report position of the parent.
304+
return parent
305+
}
306+
245307
func validateEnum(res *result, isProto3 bool, name protoreflect.FullName, ed *descriptorpb.EnumDescriptorProto, handler *reporter.Handler) error {
246308
scope := fmt.Sprintf("enum %s", name)
247309

@@ -331,6 +393,14 @@ func validateEnum(res *result, isProto3 bool, name protoreflect.FullName, ed *de
331393
// or reserved ranges or reserved names
332394
rsvdNames := map[string]struct{}{}
333395
for _, n := range ed.ReservedName {
396+
// validate reserved name while we're here
397+
if !isIdentifier(n) {
398+
node := findEnumReservedNameNode(res.EnumNode(ed), n)
399+
nodeInfo := res.file.NodeInfo(node)
400+
if err := handler.HandleErrorf(nodeInfo.Start(), "%s: reserved name %q is not a valid identifier", scope, n); err != nil {
401+
return err
402+
}
403+
}
334404
rsvdNames[n] = struct{}{}
335405
}
336406
for _, ev := range ed.Value {
@@ -354,6 +424,15 @@ func validateEnum(res *result, isProto3 bool, name protoreflect.FullName, ed *de
354424
return nil
355425
}
356426

427+
func findEnumReservedNameNode(enumNode ast.Node, name string) ast.Node {
428+
var decls []ast.EnumElement
429+
if enumNode, ok := enumNode.(*ast.EnumNode); ok {
430+
decls = enumNode.Decls
431+
// if not the right type, we leave decls empty
432+
}
433+
return findReservedNameNode(enumNode, decls, name)
434+
}
435+
357436
func validateField(res *result, isProto3 bool, name protoreflect.FullName, fld *descriptorpb.FieldDescriptorProto, handler *reporter.Handler) error {
358437
scope := fmt.Sprintf("field %s", name)
359438

parser/validate_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,79 @@ func TestBasicValidation(t *testing.T) {
512512
} } } }`,
513513
expectedErr: `test.proto:10:55: message nesting depth must be less than 32`,
514514
},
515+
"failure_message_invalid_reserved_name": {
516+
contents: `syntax = "proto3";
517+
message Foo {
518+
reserved "foo", "b_a_r9", " blah ";
519+
}`,
520+
expectedErr: `test.proto:3:72: message Foo: reserved name " blah " is not a valid identifier`,
521+
},
522+
"failure_message_invalid_reserved_name2": {
523+
contents: `syntax = "proto3";
524+
message Foo {
525+
reserved "foo", "_bar123", "123";
526+
}`,
527+
expectedErr: `test.proto:3:73: message Foo: reserved name "123" is not a valid identifier`,
528+
},
529+
"failure_message_invalid_reserved_name3": {
530+
contents: `syntax = "proto3";
531+
message Foo {
532+
reserved "foo" "_bar123" "@y!!";
533+
}`,
534+
expectedErr: `test.proto:3:55: message Foo: reserved name "foo_bar123@y!!" is not a valid identifier`,
535+
},
536+
"failure_message_invalid_reserved_name4": {
537+
contents: `syntax = "proto3";
538+
message Foo {
539+
reserved "";
540+
}`,
541+
expectedErr: `test.proto:3:55: message Foo: reserved name "" is not a valid identifier`,
542+
},
543+
"success_message_reserved_name": {
544+
contents: `syntax = "proto3";
545+
message Foo {
546+
reserved "foo", "_bar123", "A_B_C_1_2_3";
547+
}`,
548+
},
549+
"failure_enum_invalid_reserved_name": {
550+
contents: `syntax = "proto3";
551+
enum Foo {
552+
BAR = 0;
553+
reserved "foo", "b_a_r9", " blah ";
554+
}`,
555+
expectedErr: `test.proto:4:72: enum Foo: reserved name " blah " is not a valid identifier`,
556+
},
557+
"failure_enum_invalid_reserved_name2": {
558+
contents: `syntax = "proto3";
559+
enum Foo {
560+
BAR = 0;
561+
reserved "foo", "_bar123", "123";
562+
}`,
563+
expectedErr: `test.proto:4:73: enum Foo: reserved name "123" is not a valid identifier`,
564+
},
565+
"failure_enum_invalid_reserved_name3": {
566+
contents: `syntax = "proto3";
567+
enum Foo {
568+
BAR = 0;
569+
reserved "foo" "_bar123" "@y!!";
570+
}`,
571+
expectedErr: `test.proto:4:55: enum Foo: reserved name "foo_bar123@y!!" is not a valid identifier`,
572+
},
573+
"failure_enum_invalid_reserved_name4": {
574+
contents: `syntax = "proto3";
575+
enum Foo {
576+
BAR = 0;
577+
reserved "";
578+
}`,
579+
expectedErr: `test.proto:4:55: enum Foo: reserved name "" is not a valid identifier`,
580+
},
581+
"success_enum_reserved_name": {
582+
contents: `syntax = "proto3";
583+
enum Foo {
584+
BAR = 0;
585+
reserved "foo", "_bar123", "A_B_C_1_2_3";
586+
}`,
587+
},
515588
}
516589

517590
for name, tc := range testCases {

0 commit comments

Comments
 (0)