1// Copyright 2019 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package protodesc 6 7import ( 8 "fmt" 9 "strings" 10 "testing" 11 12 "google.golang.org/protobuf/encoding/prototext" 13 "google.golang.org/protobuf/internal/flags" 14 "google.golang.org/protobuf/proto" 15 "google.golang.org/protobuf/reflect/protoreflect" 16 "google.golang.org/protobuf/reflect/protoregistry" 17 18 "google.golang.org/protobuf/types/descriptorpb" 19) 20 21func mustParseFile(s string) *descriptorpb.FileDescriptorProto { 22 pb := new(descriptorpb.FileDescriptorProto) 23 if err := prototext.Unmarshal([]byte(s), pb); err != nil { 24 panic(err) 25 } 26 return pb 27} 28 29func cloneFile(in *descriptorpb.FileDescriptorProto) *descriptorpb.FileDescriptorProto { 30 return proto.Clone(in).(*descriptorpb.FileDescriptorProto) 31} 32 33var ( 34 proto2Enum = mustParseFile(` 35 syntax: "proto2" 36 name: "proto2_enum.proto" 37 package: "test.proto2" 38 enum_type: [{name:"Enum" value:[{name:"ONE" number:1}]}] 39 `) 40 proto3Message = mustParseFile(` 41 syntax: "proto3" 42 name: "proto3_message.proto" 43 package: "test.proto3" 44 message_type: [{ 45 name: "Message" 46 field: [ 47 {name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}, 48 {name:"bar" number:2 label:LABEL_OPTIONAL type:TYPE_STRING} 49 ] 50 }] 51 `) 52 extendableMessage = mustParseFile(` 53 syntax: "proto2" 54 name: "extendable_message.proto" 55 package: "test.proto2" 56 message_type: [{name:"Message" extension_range:[{start:1 end:1000}]}] 57 `) 58 importPublicFile1 = mustParseFile(` 59 syntax: "proto3" 60 name: "import_public1.proto" 61 dependency: ["proto2_enum.proto", "proto3_message.proto", "extendable_message.proto"] 62 message_type: [{name:"Public1"}] 63 `) 64 importPublicFile2 = mustParseFile(` 65 syntax: "proto3" 66 name: "import_public2.proto" 67 dependency: ["import_public1.proto"] 68 public_dependency: [0] 69 message_type: [{name:"Public2"}] 70 `) 71 importPublicFile3 = mustParseFile(` 72 syntax: "proto3" 73 name: "import_public3.proto" 74 dependency: ["import_public2.proto", "extendable_message.proto"] 75 public_dependency: [0] 76 message_type: [{name:"Public3"}] 77 `) 78 importPublicFile4 = mustParseFile(` 79 syntax: "proto3" 80 name: "import_public4.proto" 81 dependency: ["import_public2.proto", "import_public3.proto", "proto2_enum.proto"] 82 public_dependency: [0, 1] 83 message_type: [{name:"Public4"}] 84 `) 85) 86 87func TestNewFile(t *testing.T) { 88 tests := []struct { 89 label string 90 inDeps []*descriptorpb.FileDescriptorProto 91 inDesc *descriptorpb.FileDescriptorProto 92 inOpts FileOptions 93 wantDesc *descriptorpb.FileDescriptorProto 94 wantErr string 95 }{{ 96 label: "empty path", 97 inDesc: mustParseFile(``), 98 wantErr: `path must be populated`, 99 }, { 100 label: "empty package and syntax", 101 inDesc: mustParseFile(`name:"weird"`), 102 }, { 103 label: "invalid syntax", 104 inDesc: mustParseFile(`name:"weird" syntax:"proto9"`), 105 wantErr: `invalid syntax: "proto9"`, 106 }, { 107 label: "bad package", 108 inDesc: mustParseFile(`name:"weird" package:"$"`), 109 wantErr: `invalid package: "$"`, 110 }, { 111 label: "unresolvable import", 112 inDesc: mustParseFile(` 113 name: "test.proto" 114 dependency: "dep.proto" 115 `), 116 wantErr: `could not resolve import "dep.proto": not found`, 117 }, { 118 label: "unresolvable import but allowed", 119 inDesc: mustParseFile(` 120 name: "test.proto" 121 dependency: "dep.proto" 122 `), 123 inOpts: FileOptions{AllowUnresolvable: true}, 124 }, { 125 label: "duplicate import", 126 inDesc: mustParseFile(` 127 name: "test.proto" 128 dependency: ["dep.proto", "dep.proto"] 129 `), 130 inOpts: FileOptions{AllowUnresolvable: true}, 131 wantErr: `already imported "dep.proto"`, 132 }, { 133 label: "invalid weak import", 134 inDesc: mustParseFile(` 135 name: "test.proto" 136 dependency: "dep.proto" 137 weak_dependency: [-23] 138 `), 139 inOpts: FileOptions{AllowUnresolvable: true}, 140 wantErr: `invalid or duplicate weak import index: -23`, 141 }, { 142 label: "normal weak and public import", 143 inDesc: mustParseFile(` 144 name: "test.proto" 145 dependency: "dep.proto" 146 weak_dependency: [0] 147 public_dependency: [0] 148 `), 149 inOpts: FileOptions{AllowUnresolvable: true}, 150 }, { 151 label: "import public indirect dependency duplicate", 152 inDeps: []*descriptorpb.FileDescriptorProto{ 153 mustParseFile(`name:"leaf.proto"`), 154 mustParseFile(`name:"public.proto" dependency:"leaf.proto" public_dependency:0`), 155 }, 156 inDesc: mustParseFile(` 157 name: "test.proto" 158 dependency: ["public.proto", "leaf.proto"] 159 `), 160 }, { 161 label: "import public graph", 162 inDeps: []*descriptorpb.FileDescriptorProto{ 163 cloneFile(proto2Enum), 164 cloneFile(proto3Message), 165 cloneFile(extendableMessage), 166 cloneFile(importPublicFile1), 167 cloneFile(importPublicFile2), 168 cloneFile(importPublicFile3), 169 cloneFile(importPublicFile4), 170 }, 171 inDesc: mustParseFile(` 172 name: "test.proto" 173 package: "test.graph" 174 dependency: ["import_public4.proto"], 175 `), 176 // TODO: Test import public 177 }, { 178 label: "preserve source code locations", 179 inDesc: mustParseFile(` 180 name: "test.proto" 181 package: "fizz.buzz" 182 source_code_info: {location: [{ 183 span: [39,0,882,1] 184 }, { 185 path: [12] 186 span: [39,0,18] 187 leading_detached_comments: [" foo\n"," bar\n"] 188 }, { 189 path: [8,9] 190 span: [51,0,28] 191 leading_comments: " Comment\n" 192 }]} 193 `), 194 }, { 195 label: "invalid source code span", 196 inDesc: mustParseFile(` 197 name: "test.proto" 198 package: "fizz.buzz" 199 source_code_info: {location: [{ 200 span: [39] 201 }]} 202 `), 203 wantErr: `invalid span: [39]`, 204 }, { 205 label: "resolve relative reference", 206 inDesc: mustParseFile(` 207 name: "test.proto" 208 package: "fizz.buzz" 209 message_type: [{ 210 name: "A" 211 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:"B.C"}] 212 nested_type: [{name: "B"}] 213 }, { 214 name: "B" 215 nested_type: [{name: "C"}] 216 }] 217 `), 218 wantDesc: mustParseFile(` 219 name: "test.proto" 220 package: "fizz.buzz" 221 message_type: [{ 222 name: "A" 223 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:".fizz.buzz.B.C"}] 224 nested_type: [{name: "B"}] 225 }, { 226 name: "B" 227 nested_type: [{name: "C"}] 228 }] 229 `), 230 }, { 231 label: "resolve the wrong type", 232 inDesc: mustParseFile(` 233 name: "test.proto" 234 message_type: [{ 235 name: "M" 236 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:"E"}] 237 enum_type: [{name: "E" value: [{name:"V0" number:0}, {name:"V1" number:1}]}] 238 }] 239 `), 240 wantErr: `message field "M.F" cannot resolve type: resolved "M.E", but it is not an message`, 241 }, { 242 label: "auto-resolve unknown kind", 243 inDesc: mustParseFile(` 244 name: "test.proto" 245 message_type: [{ 246 name: "M" 247 field: [{name:"F" number:1 label:LABEL_OPTIONAL type_name:"E"}] 248 enum_type: [{name: "E" value: [{name:"V0" number:0}, {name:"V1" number:1}]}] 249 }] 250 `), 251 wantDesc: mustParseFile(` 252 name: "test.proto" 253 message_type: [{ 254 name: "M" 255 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_ENUM type_name:".M.E"}] 256 enum_type: [{name: "E" value: [{name:"V0" number:0}, {name:"V1" number:1}]}] 257 }] 258 `), 259 }, { 260 label: "unresolved import", 261 inDesc: mustParseFile(` 262 name: "test.proto" 263 package: "fizz.buzz" 264 dependency: "remote.proto" 265 `), 266 wantErr: `could not resolve import "remote.proto": not found`, 267 }, { 268 label: "unresolved message field", 269 inDesc: mustParseFile(` 270 name: "test.proto" 271 package: "fizz.buzz" 272 message_type: [{ 273 name: "M" 274 field: [{name:"F1" number:1 label:LABEL_OPTIONAL type:TYPE_ENUM type_name:"some.other.enum" default_value:"UNKNOWN"}] 275 }] 276 `), 277 wantErr: `message field "fizz.buzz.M.F1" cannot resolve type: "*.some.other.enum" not found`, 278 }, { 279 label: "unresolved default enum value", 280 inDesc: mustParseFile(` 281 name: "test.proto" 282 package: "fizz.buzz" 283 message_type: [{ 284 name: "M" 285 field: [{name:"F1" number:1 label:LABEL_OPTIONAL type:TYPE_ENUM type_name:"E" default_value:"UNKNOWN"}] 286 enum_type: [{name:"E" value:[{name:"V0" number:0}]}] 287 }] 288 `), 289 wantErr: `message field "fizz.buzz.M.F1" has invalid default: could not parse value for enum: "UNKNOWN"`, 290 }, { 291 label: "allowed unresolved default enum value", 292 inDesc: mustParseFile(` 293 name: "test.proto" 294 package: "fizz.buzz" 295 message_type: [{ 296 name: "M" 297 field: [{name:"F1" number:1 label:LABEL_OPTIONAL type:TYPE_ENUM type_name:".fizz.buzz.M.E" default_value:"UNKNOWN"}] 298 enum_type: [{name:"E" value:[{name:"V0" number:0}]}] 299 }] 300 `), 301 inOpts: FileOptions{AllowUnresolvable: true}, 302 }, { 303 label: "unresolved extendee", 304 inDesc: mustParseFile(` 305 name: "test.proto" 306 package: "fizz.buzz" 307 extension: [{name:"X" number:1 label:LABEL_OPTIONAL extendee:"some.extended.message" type:TYPE_MESSAGE type_name:"some.other.message"}] 308 `), 309 wantErr: `extension field "fizz.buzz.X" cannot resolve extendee: "*.some.extended.message" not found`, 310 }, { 311 label: "unresolved method input", 312 inDesc: mustParseFile(` 313 name: "test.proto" 314 package: "fizz.buzz" 315 service: [{ 316 name: "S" 317 method: [{name:"M" input_type:"foo.bar.input" output_type:".absolute.foo.bar.output"}] 318 }] 319 `), 320 wantErr: `service method "fizz.buzz.S.M" cannot resolve input: "*.foo.bar.input" not found`, 321 }, { 322 label: "allowed unresolved references", 323 inDesc: mustParseFile(` 324 name: "test.proto" 325 package: "fizz.buzz" 326 dependency: "remote.proto" 327 message_type: [{ 328 name: "M" 329 field: [{name:"F1" number:1 label:LABEL_OPTIONAL type_name:"some.other.enum" default_value:"UNKNOWN"}] 330 }] 331 extension: [{name:"X" number:1 label:LABEL_OPTIONAL extendee:"some.extended.message" type:TYPE_MESSAGE type_name:"some.other.message"}] 332 service: [{ 333 name: "S" 334 method: [{name:"M" input_type:"foo.bar.input" output_type:".absolute.foo.bar.output"}] 335 }] 336 `), 337 inOpts: FileOptions{AllowUnresolvable: true}, 338 }, { 339 label: "resolved but not imported", 340 inDeps: []*descriptorpb.FileDescriptorProto{mustParseFile(` 341 name: "dep.proto" 342 package: "fizz" 343 message_type: [{name:"M" nested_type:[{name:"M"}]}] 344 `)}, 345 inDesc: mustParseFile(` 346 name: "test.proto" 347 package: "fizz.buzz" 348 message_type: [{ 349 name: "M" 350 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:"M.M"}] 351 }] 352 `), 353 wantErr: `message field "fizz.buzz.M.F" cannot resolve type: resolved "fizz.M.M", but "dep.proto" is not imported`, 354 }, { 355 label: "resolved from remote import", 356 inDeps: []*descriptorpb.FileDescriptorProto{mustParseFile(` 357 name: "dep.proto" 358 package: "fizz" 359 message_type: [{name:"M" nested_type:[{name:"M"}]}] 360 `)}, 361 inDesc: mustParseFile(` 362 name: "test.proto" 363 package: "fizz.buzz" 364 dependency: "dep.proto" 365 message_type: [{ 366 name: "M" 367 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:"M.M"}] 368 }] 369 `), 370 wantDesc: mustParseFile(` 371 name: "test.proto" 372 package: "fizz.buzz" 373 dependency: "dep.proto" 374 message_type: [{ 375 name: "M" 376 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:".fizz.M.M"}] 377 }] 378 `), 379 }, { 380 label: "namespace conflict on enum value", 381 inDesc: mustParseFile(` 382 name: "test.proto" 383 enum_type: [{ 384 name: "foo" 385 value: [{name:"foo" number:0}] 386 }] 387 `), 388 wantErr: `descriptor "foo" already declared`, 389 }, { 390 label: "no namespace conflict on message field", 391 inDesc: mustParseFile(` 392 name: "test.proto" 393 message_type: [{ 394 name: "foo" 395 field: [{name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 396 }] 397 `), 398 }, { 399 label: "invalid name", 400 inDesc: mustParseFile(` 401 name: "test.proto" 402 message_type: [{name: "$"}] 403 `), 404 wantErr: `descriptor "" has an invalid nested name: "$"`, 405 }, { 406 label: "invalid empty enum", 407 inDesc: mustParseFile(` 408 name: "test.proto" 409 message_type: [{name:"M" enum_type:[{name:"E"}]}] 410 `), 411 wantErr: `enum "M.E" must contain at least one value declaration`, 412 }, { 413 label: "invalid enum value without number", 414 inDesc: mustParseFile(` 415 name: "test.proto" 416 message_type: [{name:"M" enum_type:[{name:"E" value:[{name:"one"}]}]}] 417 `), 418 wantErr: `enum value "M.one" must have a specified number`, 419 }, { 420 label: "valid enum", 421 inDesc: mustParseFile(` 422 name: "test.proto" 423 message_type: [{name:"M" enum_type:[{name:"E" value:[{name:"one" number:1}]}]}] 424 `), 425 }, { 426 label: "invalid enum reserved names", 427 inDesc: mustParseFile(` 428 name: "test.proto" 429 message_type: [{name:"M" enum_type:[{ 430 name: "E" 431 reserved_name: [""] 432 value: [{name:"V" number:0}] 433 }]}] 434 `), 435 // NOTE: In theory this should be an error. 436 // See https://github.com/protocolbuffers/protobuf/issues/6335. 437 /*wantErr: `enum "M.E" reserved names has invalid name: ""`,*/ 438 }, { 439 label: "duplicate enum reserved names", 440 inDesc: mustParseFile(` 441 name: "test.proto" 442 message_type: [{name:"M" enum_type:[{ 443 name: "E" 444 reserved_name: ["foo", "foo"] 445 }]}] 446 `), 447 wantErr: `enum "M.E" reserved names has duplicate name: "foo"`, 448 }, { 449 label: "valid enum reserved names", 450 inDesc: mustParseFile(` 451 name: "test.proto" 452 message_type: [{name:"M" enum_type:[{ 453 name: "E" 454 reserved_name: ["foo", "bar"] 455 value: [{name:"baz" number:1}] 456 }]}] 457 `), 458 }, { 459 label: "use of enum reserved names", 460 inDesc: mustParseFile(` 461 name: "test.proto" 462 message_type: [{name:"M" enum_type:[{ 463 name: "E" 464 reserved_name: ["foo", "bar"] 465 value: [{name:"foo" number:1}] 466 }]}] 467 `), 468 wantErr: `enum value "M.foo" must not use reserved name`, 469 }, { 470 label: "invalid enum reserved ranges", 471 inDesc: mustParseFile(` 472 name: "test.proto" 473 message_type: [{name:"M" enum_type:[{ 474 name: "E" 475 reserved_range: [{start:5 end:4}] 476 }]}] 477 `), 478 wantErr: `enum "M.E" reserved ranges has invalid range: 5 to 4`, 479 }, { 480 label: "overlapping enum reserved ranges", 481 inDesc: mustParseFile(` 482 name: "test.proto" 483 message_type: [{name:"M" enum_type:[{ 484 name: "E" 485 reserved_range: [{start:1 end:1000}, {start:10 end:100}] 486 }]}] 487 `), 488 wantErr: `enum "M.E" reserved ranges has overlapping ranges: 1 to 1000 with 10 to 100`, 489 }, { 490 label: "valid enum reserved names", 491 inDesc: mustParseFile(` 492 name: "test.proto" 493 message_type: [{name:"M" enum_type:[{ 494 name: "E" 495 reserved_range: [{start:1 end:10}, {start:100 end:1000}] 496 value: [{name:"baz" number:50}] 497 }]}] 498 `), 499 }, { 500 label: "use of enum reserved range", 501 inDesc: mustParseFile(` 502 name: "test.proto" 503 message_type: [{name:"M" enum_type:[{ 504 name: "E" 505 reserved_range: [{start:1 end:10}, {start:100 end:1000}] 506 value: [{name:"baz" number:500}] 507 }]}] 508 `), 509 wantErr: `enum value "M.baz" must not use reserved number 500`, 510 }, { 511 label: "unused enum alias feature", 512 inDesc: mustParseFile(` 513 name: "test.proto" 514 message_type: [{name:"M" enum_type:[{ 515 name: "E" 516 value: [{name:"baz" number:500}] 517 options: {allow_alias:true} 518 }]}] 519 `), 520 wantErr: `enum "M.E" allows aliases, but none were found`, 521 }, { 522 label: "enum number conflicts", 523 inDesc: mustParseFile(` 524 name: "test.proto" 525 message_type: [{name:"M" enum_type:[{ 526 name: "E" 527 value: [{name:"foo" number:0}, {name:"bar" number:1}, {name:"baz" number:1}] 528 }]}] 529 `), 530 wantErr: `enum "M.E" has conflicting non-aliased values on number 1: "baz" with "bar"`, 531 }, { 532 label: "aliased enum numbers", 533 inDesc: mustParseFile(` 534 name: "test.proto" 535 message_type: [{name:"M" enum_type:[{ 536 name: "E" 537 value: [{name:"foo" number:0}, {name:"bar" number:1}, {name:"baz" number:1}] 538 options: {allow_alias:true} 539 }]}] 540 `), 541 }, { 542 label: "invalid proto3 enum", 543 inDesc: mustParseFile(` 544 syntax: "proto3" 545 name: "test.proto" 546 message_type: [{name:"M" enum_type:[{ 547 name: "E" 548 value: [{name:"baz" number:500}] 549 }]}] 550 `), 551 wantErr: `enum "M.baz" using proto3 semantics must have zero number for the first value`, 552 }, { 553 label: "valid proto3 enum", 554 inDesc: mustParseFile(` 555 syntax: "proto3" 556 name: "test.proto" 557 message_type: [{name:"M" enum_type:[{ 558 name: "E" 559 value: [{name:"baz" number:0}] 560 }]}] 561 `), 562 }, { 563 label: "proto3 enum name prefix conflict", 564 inDesc: mustParseFile(` 565 syntax: "proto3" 566 name: "test.proto" 567 message_type: [{name:"M" enum_type:[{ 568 name: "E" 569 value: [{name:"e_Foo" number:0}, {name:"fOo" number:1}] 570 }]}] 571 `), 572 wantErr: `enum "M.E" using proto3 semantics has conflict: "fOo" with "e_Foo"`, 573 }, { 574 label: "proto2 enum has name prefix check", 575 inDesc: mustParseFile(` 576 name: "test.proto" 577 message_type: [{name:"M" enum_type:[{ 578 name: "E" 579 value: [{name:"e_Foo" number:0}, {name:"fOo" number:1}] 580 }]}] 581 `), 582 }, { 583 label: "proto3 enum same name prefix with number conflict", 584 inDesc: mustParseFile(` 585 syntax: "proto3" 586 name: "test.proto" 587 message_type: [{name:"M" enum_type:[{ 588 name: "E" 589 value: [{name:"e_Foo" number:0}, {name:"fOo" number:0}] 590 }]}] 591 `), 592 wantErr: `enum "M.E" has conflicting non-aliased values on number 0: "fOo" with "e_Foo"`, 593 }, { 594 label: "proto3 enum same name prefix with alias numbers", 595 inDesc: mustParseFile(` 596 syntax: "proto3" 597 name: "test.proto" 598 message_type: [{name:"M" enum_type:[{ 599 name: "E" 600 value: [{name:"e_Foo" number:0}, {name:"fOo" number:0}] 601 options: {allow_alias: true} 602 }]}] 603 `), 604 }, { 605 label: "invalid message reserved names", 606 inDesc: mustParseFile(` 607 name: "test.proto" 608 message_type: [{name:"M" nested_type:[{ 609 name: "M" 610 reserved_name: ["$"] 611 }]}] 612 `), 613 // NOTE: In theory this should be an error. 614 // See https://github.com/protocolbuffers/protobuf/issues/6335. 615 /*wantErr: `message "M.M" reserved names has invalid name: "$"`,*/ 616 }, { 617 label: "valid message reserved names", 618 inDesc: mustParseFile(` 619 name: "test.proto" 620 message_type: [{name:"M" nested_type:[{ 621 name: "M" 622 reserved_name: ["foo", "bar"] 623 field: [{name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 624 }]}] 625 `), 626 wantErr: `message field "M.M.foo" must not use reserved name`, 627 }, { 628 label: "valid message reserved names", 629 inDesc: mustParseFile(` 630 name: "test.proto" 631 message_type: [{name:"M" nested_type:[{ 632 name: "M" 633 reserved_name: ["foo", "bar"] 634 field: [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}] 635 oneof_decl: [{name:"foo"}] # not affected by reserved_name 636 }]}] 637 `), 638 }, { 639 label: "invalid reserved number", 640 inDesc: mustParseFile(` 641 name: "test.proto" 642 message_type: [{name:"M" nested_type:[{ 643 name: "M" 644 reserved_range: [{start:1 end:1}] 645 field: [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 646 }]}] 647 `), 648 wantErr: `message "M.M" reserved ranges has invalid field number: 0`, 649 }, { 650 label: "invalid reserved ranges", 651 inDesc: mustParseFile(` 652 name: "test.proto" 653 message_type: [{name:"M" nested_type:[{ 654 name: "M" 655 reserved_range: [{start:2 end:2}] 656 field: [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 657 }]}] 658 `), 659 wantErr: `message "M.M" reserved ranges has invalid range: 2 to 1`, 660 }, { 661 label: "overlapping reserved ranges", 662 inDesc: mustParseFile(` 663 name: "test.proto" 664 message_type: [{name:"M" nested_type:[{ 665 name: "M" 666 reserved_range: [{start:1 end:10}, {start:2 end:9}] 667 field: [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 668 }]}] 669 `), 670 wantErr: `message "M.M" reserved ranges has overlapping ranges: 1 to 9 with 2 to 8`, 671 }, { 672 label: "use of reserved message field number", 673 inDesc: mustParseFile(` 674 name: "test.proto" 675 message_type: [{name:"M" nested_type:[{ 676 name: "M" 677 reserved_range: [{start:10 end:20}, {start:20 end:30}, {start:30 end:31}] 678 field: [{name:"baz" number:30 label:LABEL_OPTIONAL type:TYPE_STRING}] 679 }]}] 680 `), 681 wantErr: `message field "M.M.baz" must not use reserved number 30`, 682 }, { 683 label: "invalid extension ranges", 684 inDesc: mustParseFile(` 685 name: "test.proto" 686 message_type: [{name:"M" nested_type:[{ 687 name: "M" 688 extension_range: [{start:-500 end:2}] 689 field: [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}] 690 }]}] 691 `), 692 wantErr: `message "M.M" extension ranges has invalid field number: -500`, 693 }, { 694 label: "overlapping reserved and extension ranges", 695 inDesc: mustParseFile(` 696 name: "test.proto" 697 message_type: [{name:"M" nested_type:[{ 698 name: "M" 699 reserved_range: [{start:15 end:20}, {start:1 end:3}, {start:7 end:10}] 700 extension_range: [{start:8 end:9}, {start:3 end:5}] 701 }]}] 702 `), 703 wantErr: `message "M.M" reserved and extension ranges has overlapping ranges: 7 to 9 with 8`, 704 }, { 705 label: "message field conflicting number", 706 inDesc: mustParseFile(` 707 name: "test.proto" 708 message_type: [{name:"M" nested_type:[{ 709 name: "M" 710 field: [ 711 {name:"one" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}, 712 {name:"One" number:1 label:LABEL_OPTIONAL type:TYPE_STRING} 713 ] 714 }]}] 715 `), 716 wantErr: `message "M.M" has conflicting fields: "One" with "one"`, 717 }, { 718 label: "invalid MessageSet", 719 inDesc: mustParseFile(` 720 syntax: "proto3" 721 name: "test.proto" 722 message_type: [{name:"M" nested_type:[{ 723 name: "M" 724 options: {message_set_wire_format:true} 725 }]}] 726 `), 727 wantErr: func() string { 728 if flags.ProtoLegacy { 729 return `message "M.M" is an invalid proto1 MessageSet` 730 } else { 731 return `message "M.M" is a MessageSet, which is a legacy proto1 feature that is no longer supported` 732 } 733 }(), 734 }, { 735 label: "valid MessageSet", 736 inDesc: mustParseFile(` 737 name: "test.proto" 738 message_type: [{name:"M" nested_type:[{ 739 name: "M" 740 extension_range: [{start:1 end:100000}] 741 options: {message_set_wire_format:true} 742 }]}] 743 `), 744 wantErr: func() string { 745 if flags.ProtoLegacy { 746 return "" 747 } else { 748 return `message "M.M" is a MessageSet, which is a legacy proto1 feature that is no longer supported` 749 } 750 }(), 751 }, { 752 label: "invalid extension ranges in proto3", 753 inDesc: mustParseFile(` 754 syntax: "proto3" 755 name: "test.proto" 756 message_type: [{name:"M" nested_type:[{ 757 name: "M" 758 extension_range: [{start:1 end:100000}] 759 }]}] 760 `), 761 wantErr: `message "M.M" using proto3 semantics cannot have extension ranges`, 762 }, { 763 label: "proto3 message fields conflict", 764 inDesc: mustParseFile(` 765 syntax: "proto3" 766 name: "test.proto" 767 message_type: [{name:"M" nested_type:[{ 768 name: "M" 769 field: [ 770 {name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}, 771 {name:"baz" number:2 label:LABEL_OPTIONAL type:TYPE_STRING} 772 ] 773 }]}] 774 `), 775 wantErr: `message "M.M" using proto3 semantics has conflict: "baz" with "_b_a_z_"`, 776 }, { 777 label: "proto3 message fields", 778 inDesc: mustParseFile(` 779 syntax: "proto3" 780 name: "test.proto" 781 message_type: [{name:"M" nested_type:[{ 782 name: "M" 783 field: [{name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}] 784 oneof_decl: [{name:"baz"}] # proto3 name conflict logic does not include oneof 785 }]}] 786 `), 787 }, { 788 label: "proto2 message fields with no conflict", 789 inDesc: mustParseFile(` 790 name: "test.proto" 791 message_type: [{name:"M" nested_type:[{ 792 name: "M" 793 field: [ 794 {name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}, 795 {name:"baz" number:2 label:LABEL_OPTIONAL type:TYPE_STRING} 796 ] 797 }]}] 798 `), 799 }, { 800 label: "proto3 message with unresolved enum", 801 inDesc: mustParseFile(` 802 name: "test.proto" 803 syntax: "proto3" 804 message_type: [{ 805 name: "M" 806 field: [ 807 {name:"enum" number:1 label:LABEL_OPTIONAL type:TYPE_ENUM type_name:".fizz.buzz.Enum"} 808 ] 809 }] 810 `), 811 inOpts: FileOptions{AllowUnresolvable: true}, 812 // TODO: Test field and oneof handling in validateMessageDeclarations 813 // TODO: Test unmarshalDefault 814 // TODO: Test validateExtensionDeclarations 815 // TODO: Test checkValidGroup 816 // TODO: Test checkValidMap 817 }, { 818 label: "empty service", 819 inDesc: mustParseFile(` 820 name: "test.proto" 821 service: [{name:"service"}] 822 `), 823 }, { 824 label: "service with method with unresolved", 825 inDesc: mustParseFile(` 826 name: "test.proto" 827 service: [{ 828 name: "service" 829 method: [{ 830 name:"method" 831 input_type:"foo" 832 output_type:".foo.bar.baz" 833 }] 834 }] 835 `), 836 inOpts: FileOptions{AllowUnresolvable: true}, 837 }, { 838 label: "service with wrong reference type", 839 inDeps: []*descriptorpb.FileDescriptorProto{ 840 cloneFile(proto3Message), 841 cloneFile(proto2Enum), 842 }, 843 inDesc: mustParseFile(` 844 name: "test.proto" 845 dependency: ["proto2_enum.proto", "proto3_message.proto"] 846 service: [{ 847 name: "service" 848 method: [{ 849 name: "method" 850 input_type: ".test.proto2.Enum", 851 output_type: ".test.proto3.Message" 852 }] 853 }] 854 `), 855 wantErr: `service method "service.method" cannot resolve input: resolved "test.proto2.Enum", but it is not an message`, 856 }} 857 858 for _, tt := range tests { 859 t.Run(tt.label, func(t *testing.T) { 860 r := new(protoregistry.Files) 861 for i, dep := range tt.inDeps { 862 f, err := tt.inOpts.New(dep, r) 863 if err != nil { 864 t.Fatalf("dependency %d: unexpected NewFile() error: %v", i, err) 865 } 866 if err := r.RegisterFile(f); err != nil { 867 t.Fatalf("dependency %d: unexpected Register() error: %v", i, err) 868 } 869 } 870 var gotDesc *descriptorpb.FileDescriptorProto 871 if tt.wantErr == "" && tt.wantDesc == nil { 872 tt.wantDesc = cloneFile(tt.inDesc) 873 } 874 gotFile, err := tt.inOpts.New(tt.inDesc, r) 875 if gotFile != nil { 876 gotDesc = ToFileDescriptorProto(gotFile) 877 } 878 if !proto.Equal(gotDesc, tt.wantDesc) { 879 t.Errorf("NewFile() mismatch:\ngot %v\nwant %v", gotDesc, tt.wantDesc) 880 } 881 if ((err == nil) != (tt.wantErr == "")) || !strings.Contains(fmt.Sprint(err), tt.wantErr) { 882 t.Errorf("NewFile() error:\ngot: %v\nwant: %v", err, tt.wantErr) 883 } 884 }) 885 } 886} 887 888func TestNewFiles(t *testing.T) { 889 fdset := &descriptorpb.FileDescriptorSet{ 890 File: []*descriptorpb.FileDescriptorProto{ 891 mustParseFile(` 892 name: "test.proto" 893 package: "fizz" 894 dependency: "dep.proto" 895 message_type: [{ 896 name: "M2" 897 field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:"M1"}] 898 }] 899 `), 900 // Inputs deliberately out of order. 901 mustParseFile(` 902 name: "dep.proto" 903 package: "fizz" 904 message_type: [{name:"M1"}] 905 `), 906 }, 907 } 908 f, err := NewFiles(fdset) 909 if err != nil { 910 t.Fatal(err) 911 } 912 m1, err := f.FindDescriptorByName("fizz.M1") 913 if err != nil { 914 t.Fatalf(`f.FindDescriptorByName("fizz.M1") = %v`, err) 915 } 916 m2, err := f.FindDescriptorByName("fizz.M2") 917 if err != nil { 918 t.Fatalf(`f.FindDescriptorByName("fizz.M2") = %v`, err) 919 } 920 if m2.(protoreflect.MessageDescriptor).Fields().ByName("F").Message() != m1 { 921 t.Fatalf(`m1.Fields().ByName("F").Message() != m2`) 922 } 923} 924 925func TestNewFilesImportCycle(t *testing.T) { 926 fdset := &descriptorpb.FileDescriptorSet{ 927 File: []*descriptorpb.FileDescriptorProto{ 928 mustParseFile(` 929 name: "test.proto" 930 package: "fizz" 931 dependency: "dep.proto" 932 `), 933 mustParseFile(` 934 name: "dep.proto" 935 package: "fizz" 936 dependency: "test.proto" 937 `), 938 }, 939 } 940 _, err := NewFiles(fdset) 941 if err == nil { 942 t.Fatal("NewFiles with import cycle: success, want error") 943 } 944} 945 946func TestSourceLocations(t *testing.T) { 947 fd := mustParseFile(` 948 name: "comments.proto" 949 message_type: [{ 950 name: "Message1" 951 field: [ 952 {name:"field1" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}, 953 {name:"field2" number:2 label:LABEL_OPTIONAL type:TYPE_STRING}, 954 {name:"field3" number:3 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}, 955 {name:"field4" number:4 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}, 956 {name:"field5" number:5 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:1}, 957 {name:"field6" number:6 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:1} 958 ] 959 extension: [ 960 {name:"extension1" number:100 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"}, 961 {name:"extension2" number:101 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"} 962 ] 963 nested_type: [{name:"Message1"}, {name:"Message2"}] 964 extension_range: {start:100 end:536870912} 965 oneof_decl: [{name:"oneof1"}, {name:"oneof2"}] 966 }, { 967 name: "Message2" 968 enum_type: { 969 name: "Enum1" 970 value: [ 971 {name: "FOO", number: 0}, 972 {name: "BAR", number: 1} 973 ] 974 } 975 }] 976 enum_type: { 977 name: "Enum1" 978 value: [ 979 {name: "FOO", number: 0}, 980 {name: "BAR", number: 1} 981 ] 982 } 983 service: { 984 name: "Service1" 985 method: [ 986 {name:"Method1" input_type:".Message1" output_type:".Message1"}, 987 {name:"Method2" input_type:".Message2" output_type:".Message2"} 988 ] 989 } 990 extension: [ 991 {name:"extension1" number:102 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"}, 992 {name:"extension2" number:103 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"} 993 ] 994 source_code_info: { 995 location: [ 996 {span:[0,0,69,1]}, 997 {path:[12] span:[0,0,18]}, 998 {path:[5,0] span:[3,0,8,1] leading_comments:" Enum1\r\n"}, 999 {path:[5,0,1] span:[3,5,10]}, 1000 {path:[5,0,2,0] span:[5,2,10] leading_comments:" FOO\r\n"}, 1001 {path:[5,0,2,0,1] span:[5,2,5]}, 1002 {path:[5,0,2,0,2] span:[5,8,9]}, 1003 {path:[5,0,2,1] span:[7,2,10] leading_comments:" BAR\r\n"}, 1004 {path:[5,0,2,1,1] span:[7,2,5]}, 1005 {path:[5,0,2,1,2] span:[7,8,9]}, 1006 {path:[4,0] span:[11,0,43,1] leading_comments:" Message1\r\n"}, 1007 {path:[4,0,1] span:[11,8,16]}, 1008 {path:[4,0,3,0] span:[13,2,21] leading_comments:" Message1.Message1\r\n"}, 1009 {path:[4,0,3,0,1] span:[13,10,18]}, 1010 {path:[4,0,3,1] span:[15,2,21] leading_comments:" Message1.Message2\r\n"}, 1011 {path:[4,0,3,1,1] span:[15,10,18]}, 1012 {path:[4,0,2,0] span:[18,2,29] leading_comments:" Message1.field1\r\n"}, 1013 {path:[4,0,2,0,4] span:[18,2,10]}, 1014 {path:[4,0,2,0,5] span:[18,11,17]}, 1015 {path:[4,0,2,0,1] span:[18,18,24]}, 1016 {path:[4,0,2,0,3] span:[18,27,28]}, 1017 {path:[4,0,2,1] span:[20,2,29] leading_comments:" Message1.field2\r\n"}, 1018 {path:[4,0,2,1,4] span:[20,2,10]}, 1019 {path:[4,0,2,1,5] span:[20,11,17]}, 1020 {path:[4,0,2,1,1] span:[20,18,24]}, 1021 {path:[4,0,2,1,3] span:[20,27,28]}, 1022 {path:[4,0,8,0] span:[22,2,27,3] leading_comments:" Message1.oneof1\r\n"}, 1023 {path:[4,0,8,0,1] span:[22,8,14]}, 1024 {path:[4,0,2,2] span:[24,4,22] leading_comments:" Message1.field3\r\n"}, 1025 {path:[4,0,2,2,5] span:[24,4,10]}, 1026 {path:[4,0,2,2,1] span:[24,11,17]}, 1027 {path:[4,0,2,2,3] span:[24,20,21]}, 1028 {path:[4,0,2,3] span:[26,4,22] leading_comments:" Message1.field4\r\n"}, 1029 {path:[4,0,2,3,5] span:[26,4,10]}, 1030 {path:[4,0,2,3,1] span:[26,11,17]}, 1031 {path:[4,0,2,3,3] span:[26,20,21]}, 1032 {path:[4,0,8,1] span:[29,2,34,3] leading_comments:" Message1.oneof2\r\n"}, 1033 {path:[4,0,8,1,1] span:[29,8,14]}, 1034 {path:[4,0,2,4] span:[31,4,22] leading_comments:" Message1.field5\r\n"}, 1035 {path:[4,0,2,4,5] span:[31,4,10]}, 1036 {path:[4,0,2,4,1] span:[31,11,17]}, 1037 {path:[4,0,2,4,3] span:[31,20,21]}, 1038 {path:[4,0,2,5] span:[33,4,22] leading_comments:" Message1.field6\r\n"}, 1039 {path:[4,0,2,5,5] span:[33,4,10]}, 1040 {path:[4,0,2,5,1] span:[33,11,17]}, 1041 {path:[4,0,2,5,3] span:[33,20,21]}, 1042 {path:[4,0,5] span:[36,2,24]}, 1043 {path:[4,0,5,0] span:[36,13,23]}, 1044 {path:[4,0,5,0,1] span:[36,13,16]}, 1045 {path:[4,0,5,0,2] span:[36,20,23]}, 1046 {path:[4,0,6] span:[37,2,42,3]}, 1047 {path:[4,0,6,0] span:[39,4,37] leading_comments:" Message1.extension1\r\n"}, 1048 {path:[4,0,6,0,2] span:[37,9,18]}, 1049 {path:[4,0,6,0,4] span:[39,4,12]}, 1050 {path:[4,0,6,0,5] span:[39,13,19]}, 1051 {path:[4,0,6,0,1] span:[39,20,30]}, 1052 {path:[4,0,6,0,3] span:[39,33,36]}, 1053 {path:[4,0,6,1] span:[41,4,37] leading_comments:" Message1.extension2\r\n"}, 1054 {path:[4,0,6,1,2] span:[37,9,18]}, 1055 {path:[4,0,6,1,4] span:[41,4,12]}, 1056 {path:[4,0,6,1,5] span:[41,13,19]}, 1057 {path:[4,0,6,1,1] span:[41,20,30]}, 1058 {path:[4,0,6,1,3] span:[41,33,36]}, 1059 {path:[7] span:[45,0,50,1]}, 1060 {path:[7,0] span:[47,2,35] leading_comments:" extension1\r\n"}, 1061 {path:[7,0,2] span:[45,7,15]}, 1062 {path:[7,0,4] span:[47,2,10]}, 1063 {path:[7,0,5] span:[47,11,17]}, 1064 {path:[7,0,1] span:[47,18,28]}, 1065 {path:[7,0,3] span:[47,31,34]}, 1066 {path:[7,1] span:[49,2,35] leading_comments:" extension2\r\n"}, 1067 {path:[7,1,2] span:[45,7,15]}, 1068 {path:[7,1,4] span:[49,2,10]}, 1069 {path:[7,1,5] span:[49,11,17]}, 1070 {path:[7,1,1] span:[49,18,28]}, 1071 {path:[7,1,3] span:[49,31,34]}, 1072 {path:[4,1] span:[53,0,61,1] leading_comments:" Message2\r\n"}, 1073 {path:[4,1,1] span:[53,8,16]}, 1074 {path:[4,1,4,0] span:[55,2,60,3] leading_comments:" Message2.Enum1\r\n"}, 1075 {path:[4,1,4,0,1] span:[55,7,12]}, 1076 {path:[4,1,4,0,2,0] span:[57,4,12] leading_comments:" Message2.FOO\r\n"}, 1077 {path:[4,1,4,0,2,0,1] span:[57,4,7]}, 1078 {path:[4,1,4,0,2,0,2] span:[57,10,11]}, 1079 {path:[4,1,4,0,2,1] span:[59,4,12] leading_comments:" Message2.BAR\r\n"}, 1080 {path:[4,1,4,0,2,1,1] span:[59,4,7]}, 1081 {path:[4,1,4,0,2,1,2] span:[59,10,11]}, 1082 {path:[6,0] span:[64,0,69,1] leading_comments:" Service1\r\n"}, 1083 {path:[6,0,1] span:[64,8,16]}, 1084 {path:[6,0,2,0] span:[66,2,43] leading_comments:" Service1.Method1\r\n"}, 1085 {path:[6,0,2,0,1] span:[66,6,13]}, 1086 {path:[6,0,2,0,2] span:[66,14,22]}, 1087 {path:[6,0,2,0,3] span:[66,33,41]}, 1088 {path:[6,0,2,1] span:[68,2,43] leading_comments:" Service1.Method2\r\n"}, 1089 {path:[6,0,2,1,1] span:[68,6,13]}, 1090 {path:[6,0,2,1,2] span:[68,14,22]}, 1091 {path:[6,0,2,1,3] span:[68,33,41]} 1092 ] 1093 } 1094 `) 1095 fileDesc, err := NewFile(fd, nil) 1096 if err != nil { 1097 t.Fatalf("NewFile error: %v", err) 1098 } 1099 1100 var walkDescs func(protoreflect.Descriptor, func(protoreflect.Descriptor)) 1101 walkDescs = func(d protoreflect.Descriptor, f func(protoreflect.Descriptor)) { 1102 f(d) 1103 if d, ok := d.(interface { 1104 Enums() protoreflect.EnumDescriptors 1105 }); ok { 1106 eds := d.Enums() 1107 for i := 0; i < eds.Len(); i++ { 1108 walkDescs(eds.Get(i), f) 1109 } 1110 } 1111 if d, ok := d.(interface { 1112 Values() protoreflect.EnumValueDescriptors 1113 }); ok { 1114 vds := d.Values() 1115 for i := 0; i < vds.Len(); i++ { 1116 walkDescs(vds.Get(i), f) 1117 } 1118 } 1119 if d, ok := d.(interface { 1120 Messages() protoreflect.MessageDescriptors 1121 }); ok { 1122 mds := d.Messages() 1123 for i := 0; i < mds.Len(); i++ { 1124 walkDescs(mds.Get(i), f) 1125 } 1126 } 1127 if d, ok := d.(interface { 1128 Fields() protoreflect.FieldDescriptors 1129 }); ok { 1130 fds := d.Fields() 1131 for i := 0; i < fds.Len(); i++ { 1132 walkDescs(fds.Get(i), f) 1133 } 1134 } 1135 if d, ok := d.(interface { 1136 Oneofs() protoreflect.OneofDescriptors 1137 }); ok { 1138 ods := d.Oneofs() 1139 for i := 0; i < ods.Len(); i++ { 1140 walkDescs(ods.Get(i), f) 1141 } 1142 } 1143 if d, ok := d.(interface { 1144 Extensions() protoreflect.ExtensionDescriptors 1145 }); ok { 1146 xds := d.Extensions() 1147 for i := 0; i < xds.Len(); i++ { 1148 walkDescs(xds.Get(i), f) 1149 } 1150 } 1151 if d, ok := d.(interface { 1152 Services() protoreflect.ServiceDescriptors 1153 }); ok { 1154 sds := d.Services() 1155 for i := 0; i < sds.Len(); i++ { 1156 walkDescs(sds.Get(i), f) 1157 } 1158 } 1159 if d, ok := d.(interface { 1160 Methods() protoreflect.MethodDescriptors 1161 }); ok { 1162 mds := d.Methods() 1163 for i := 0; i < mds.Len(); i++ { 1164 walkDescs(mds.Get(i), f) 1165 } 1166 } 1167 } 1168 1169 var numDescs int 1170 walkDescs(fileDesc, func(d protoreflect.Descriptor) { 1171 // The comment for every descriptor should be the full name itself. 1172 got := strings.TrimSpace(fileDesc.SourceLocations().ByDescriptor(d).LeadingComments) 1173 want := string(d.FullName()) 1174 if got != want { 1175 t.Errorf("comment mismatch: got %v, want %v", got, want) 1176 } 1177 numDescs++ 1178 }) 1179 if numDescs != 30 { 1180 t.Errorf("visited %d descriptor, expected 30", numDescs) 1181 } 1182} 1183