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