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