1package protoparse
2
3import (
4	"fmt"
5	"io"
6	"io/ioutil"
7	"strings"
8	"testing"
9
10	"github.com/golang/protobuf/jsonpb"
11	"github.com/golang/protobuf/proto"
12	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
13
14	"github.com/jhump/protoreflect/desc"
15	_ "github.com/jhump/protoreflect/internal/testprotos"
16	"github.com/jhump/protoreflect/internal/testutil"
17)
18
19func TestSimpleLink(t *testing.T) {
20	fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles("desc_test_complex.proto")
21	testutil.Ok(t, err)
22
23	b, err := ioutil.ReadFile("../../internal/testprotos/desc_test_complex.protoset")
24	testutil.Ok(t, err)
25	var files dpb.FileDescriptorSet
26	err = proto.Unmarshal(b, &files)
27	testutil.Ok(t, err)
28	testutil.Require(t, proto.Equal(files.File[0], fds[0].AsProto()), "linked descriptor did not match output from protoc:\nwanted: %s\ngot: %s", toString(files.File[0]), toString(fds[0].AsProto()))
29}
30
31func TestMultiFileLink(t *testing.T) {
32	for _, name := range []string{"desc_test2.proto", "desc_test_defaults.proto", "desc_test_field_types.proto", "desc_test_options.proto", "desc_test_proto3.proto", "desc_test_wellknowntypes.proto"} {
33		fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles(name)
34		testutil.Ok(t, err)
35
36		exp, err := desc.LoadFileDescriptor(name)
37		testutil.Ok(t, err)
38
39		checkFiles(t, fds[0], exp, map[string]struct{}{})
40	}
41}
42
43func TestProto3Optional(t *testing.T) {
44	fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles("proto3_optional/desc_test_proto3_optional.proto")
45	testutil.Ok(t, err)
46
47	data, err := ioutil.ReadFile("../../internal/testprotos/proto3_optional/desc_test_proto3_optional.protoset")
48	testutil.Ok(t, err)
49	var fdset dpb.FileDescriptorSet
50	err = proto.Unmarshal(data, &fdset)
51	testutil.Ok(t, err)
52
53	exp, err := desc.CreateFileDescriptorFromSet(&fdset)
54	testutil.Ok(t, err)
55	// not comparing source code info
56	exp.AsFileDescriptorProto().SourceCodeInfo = nil
57	for _, dep := range exp.GetDependencies() {
58		dep.AsFileDescriptorProto().SourceCodeInfo = nil
59	}
60
61	checkFiles(t, fds[0], exp, map[string]struct{}{})
62}
63
64func checkFiles(t *testing.T, act, exp *desc.FileDescriptor, checked map[string]struct{}) {
65	if _, ok := checked[act.GetName()]; ok {
66		// already checked
67		return
68	}
69	checked[act.GetName()] = struct{}{}
70
71	testutil.Require(t, proto.Equal(exp.AsFileDescriptorProto(), act.AsProto()), "linked descriptor did not match output from protoc:\nwanted: %s\ngot: %s", toString(exp.AsProto()), toString(act.AsProto()))
72
73	for i, dep := range act.GetDependencies() {
74		checkFiles(t, dep, exp.GetDependencies()[i], checked)
75	}
76}
77
78func toString(m proto.Message) string {
79	msh := jsonpb.Marshaler{Indent: "  "}
80	s, err := msh.MarshalToString(m)
81	if err != nil {
82		panic(err)
83	}
84	return s
85}
86
87func TestLinkerValidation(t *testing.T) {
88	testCases := []struct {
89		input  map[string]string
90		errMsg string
91	}{
92		{
93			map[string]string{
94				"foo.proto":  `syntax = "proto3"; package namespace.a; import "foo2.proto"; import "foo3.proto"; import "foo4.proto"; message Foo{ b.Bar a = 1; b.Baz b = 2; b.Buzz c = 3; }`,
95				"foo2.proto": `syntax = "proto3"; package namespace.b; message Bar{}`,
96				"foo3.proto": `syntax = "proto3"; package namespace.b; message Baz{}`,
97				"foo4.proto": `syntax = "proto3"; package namespace.b; message Buzz{}`,
98			},
99			"", // should succeed
100		},
101		{
102			map[string]string{
103				"foo.proto": "import \"foo2.proto\"; message fubar{}",
104			},
105			`foo.proto:1:8: file not found: foo2.proto`,
106		},
107		{
108			map[string]string{
109				"foo.proto":  "import \"foo2.proto\"; message fubar{}",
110				"foo2.proto": "import \"foo.proto\"; message baz{}",
111			},
112			`foo.proto:1:8: cycle found in imports: "foo.proto" -> "foo2.proto" -> "foo.proto"`,
113		},
114		{
115			map[string]string{
116				"foo.proto": "enum foo { bar = 1; baz = 2; } enum fu { bar = 1; baz = 2; }",
117			},
118			`foo.proto:1:42: duplicate symbol bar: already defined as enum value; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`,
119		},
120		{
121			map[string]string{
122				"foo.proto": "message foo {} enum foo { V = 0; }",
123			},
124			"foo.proto:1:16: duplicate symbol foo: already defined as message",
125		},
126		{
127			map[string]string{
128				"foo.proto": "message foo { optional string a = 1; optional string a = 2; }",
129			},
130			"foo.proto:1:38: duplicate symbol foo.a: already defined as field",
131		},
132		{
133			map[string]string{
134				"foo.proto":  "message foo {}",
135				"foo2.proto": "enum foo { V = 0; }",
136			},
137			"foo2.proto:1:1: duplicate symbol foo: already defined as message in \"foo.proto\"",
138		},
139		{
140			map[string]string{
141				"foo.proto": "message foo { optional blah a = 1; }",
142			},
143			"foo.proto:1:24: field foo.a: unknown type blah",
144		},
145		{
146			map[string]string{
147				"foo.proto": "message foo { optional bar.baz a = 1; } service bar { rpc baz (foo) returns (foo); }",
148			},
149			"foo.proto:1:24: field foo.a: invalid type: bar.baz is a method, not a message or enum",
150		},
151		{
152			map[string]string{
153				"foo.proto": "message foo { extensions 1 to 2; } extend foo { optional string a = 1; } extend foo { optional int32 b = 1; }",
154			},
155			"foo.proto:1:106: field b: duplicate extension: a and b are both using tag 1",
156		},
157		{
158			map[string]string{
159				"foo.proto": "package fu.baz; extend foobar { optional string a = 1; }",
160			},
161			"foo.proto:1:24: unknown extendee type foobar",
162		},
163		{
164			map[string]string{
165				"foo.proto": "package fu.baz; service foobar{} extend foobar { optional string a = 1; }",
166			},
167			"foo.proto:1:41: extendee is invalid: fu.baz.foobar is a service, not a message",
168		},
169		{
170			map[string]string{
171				"foo.proto": "message foo{} message bar{} service foobar{ rpc foo(foo) returns (bar); }",
172			},
173			"foo.proto:1:53: method foobar.foo: invalid request type: foobar.foo is a method, not a message",
174		},
175		{
176			map[string]string{
177				"foo.proto": "message foo{} message bar{} service foobar{ rpc foo(bar) returns (foo); }",
178			},
179			"foo.proto:1:67: method foobar.foo: invalid response type: foobar.foo is a method, not a message",
180		},
181		{
182			map[string]string{
183				"foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }",
184			},
185			"foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar",
186		},
187		{
188			map[string]string{
189				"foo.proto":  "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }",
190				"foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }",
191				"foo3.proto": "package fu.baz; message baz{ }",
192			},
193			"foo.proto:1:70: field fu.baz.foobar.a: unknown type baz; resolved to fu.baz which is not defined; consider using a leading dot",
194		},
195		{
196			map[string]string{
197				"foo.proto": `
198					syntax = "proto2";
199					package foo;
200					import "google/protobuf/descriptor.proto";
201					extend google.protobuf.FileOptions           { optional string fil_foo = 12000; }
202					extend google.protobuf.MessageOptions        { optional string msg_foo = 12000; }
203					extend google.protobuf.FieldOptions          { optional string fld_foo = 12000 [(fld_foo) = "extension"]; }
204					extend google.protobuf.OneofOptions          { optional string oof_foo = 12000; }
205					extend google.protobuf.EnumOptions           { optional string enm_foo = 12000; }
206					extend google.protobuf.EnumValueOptions      { optional string env_foo = 12000; }
207					extend google.protobuf.ExtensionRangeOptions { optional string ext_foo = 12000; }
208					extend google.protobuf.ServiceOptions        { optional string svc_foo = 12000; }
209					extend google.protobuf.MethodOptions         { optional string mtd_foo = 12000; }
210					option (fil_foo) = "file";
211					message Foo {
212						option (msg_foo) = "message";
213						oneof foo {
214							option (oof_foo) = "oneof";
215							string bar = 1 [(fld_foo) = "field"];
216						}
217						extensions 100 to 200 [(ext_foo) = "extensionrange"];
218					}
219					enum Baz {
220						option (enm_foo) = "enum";
221						ZERO = 0 [(env_foo) = "enumvalue"];
222					}
223					service FooService {
224						option (svc_foo) = "service";
225						rpc Bar(Foo) returns (Foo) {
226							option (mtd_foo) = "method";
227						}
228					}
229					`,
230			},
231			"", // should success
232		},
233		{
234			map[string]string{
235				"foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }",
236			},
237			"foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated",
238		},
239		{
240			map[string]string{
241				"foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }",
242			},
243			"foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message",
244		},
245		{
246			map[string]string{
247				"foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }",
248			},
249			"foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message",
250		},
251		{
252			map[string]string{
253				"foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }",
254			},
255			"foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double",
256		},
257		{
258			map[string]string{
259				"foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }",
260			},
261			"foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK",
262		},
263		{
264			map[string]string{
265				"foo.proto": "option b = 123;",
266			},
267			"foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist",
268		},
269		{
270			map[string]string{
271				"foo.proto": "option (foo.bar) = 123;",
272			},
273			"foo.proto:1:8: unknown extension foo.bar",
274		},
275		{
276			map[string]string{
277				"foo.proto": "option uninterpreted_option = { };",
278			},
279			"foo.proto:1:8: invalid option 'uninterpreted_option'",
280		},
281		{
282			map[string]string{
283				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
284					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
285					"extend foo { optional int32 b = 10; }\n" +
286					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
287					"option (f).b = 123;",
288			},
289			"foo.proto:5:12: option (f).b: field b of foo does not exist",
290		},
291		{
292			map[string]string{
293				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
294					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
295					"extend foo { optional int32 b = 10; }\n" +
296					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
297					"option (f).a = 123;",
298			},
299			"foo.proto:5:16: option (f).a: expecting string, got integer",
300		},
301		{
302			map[string]string{
303				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
304					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
305					"extend foo { optional int32 b = 10; }\n" +
306					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
307					"option (b) = 123;",
308			},
309			"foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo",
310		},
311		{
312			map[string]string{
313				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
314					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
315					"extend foo { optional int32 b = 10; }\n" +
316					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
317					"option (foo) = 123;",
318			},
319			"foo.proto:5:8: invalid extension: foo is a message, not an extension",
320		},
321		{
322			map[string]string{
323				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
324					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
325					"extend foo { optional int32 b = 10; }\n" +
326					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
327					"option (foo.a) = 123;",
328			},
329			"foo.proto:5:8: invalid extension: foo.a is a field but not an extension",
330		},
331		{
332			map[string]string{
333				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
334					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
335					"extend foo { optional int32 b = 10; }\n" +
336					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
337					"option (f) = { a: [ 123 ] };",
338			},
339			"foo.proto:5:19: option (f): value is an array but field is not repeated",
340		},
341		{
342			map[string]string{
343				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
344					"message foo { repeated string a = 1; extensions 10 to 20; }\n" +
345					"extend foo { optional int32 b = 10; }\n" +
346					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
347					"option (f) = { a: [ \"a\", \"b\", 123 ] };",
348			},
349			"foo.proto:5:31: option (f): expecting string, got integer",
350		},
351		{
352			map[string]string{
353				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
354					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
355					"extend foo { optional int32 b = 10; }\n" +
356					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
357					"option (f) = { a: \"a\" };\n" +
358					"option (f) = { a: \"b\" };",
359			},
360			"foo.proto:6:8: option (f): non-repeated option field f already set",
361		},
362		{
363			map[string]string{
364				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
365					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
366					"extend foo { optional int32 b = 10; }\n" +
367					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
368					"option (f) = { a: \"a\" };\n" +
369					"option (f).a = \"b\";",
370			},
371			"foo.proto:6:12: option (f).a: non-repeated option field a already set",
372		},
373		{
374			map[string]string{
375				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
376					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
377					"extend foo { optional int32 b = 10; }\n" +
378					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
379					"option (f) = { a: \"a\" };\n" +
380					"option (f).(b) = \"b\";",
381			},
382			"foo.proto:6:18: option (f).(b): expecting int32, got string",
383		},
384		{
385			map[string]string{
386				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
387					"message foo { required string a = 1; required string b = 2; }\n" +
388					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
389					"option (f) = { a: \"a\" };\n",
390			},
391			"foo.proto:1:1: error in file options: some required fields missing: (f).b",
392		},
393		{
394			map[string]string{
395				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }",
396			},
397			"foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages",
398		},
399		{
400			map[string]string{
401				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }",
402			},
403			"", // should succeed
404		},
405		{
406			map[string]string{
407				"foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }",
408			},
409			"foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo",
410		},
411		{
412			map[string]string{
413				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }",
414			},
415			"", // should succeed
416		},
417		{
418			map[string]string{
419				"foo.proto": `syntax = "proto3"; package com.google; import "google/protobuf/wrappers.proto"; message Foo { google.protobuf.StringValue str = 1; }`,
420			},
421			"foo.proto:1:95: field com.google.Foo.str: unknown type google.protobuf.StringValue; resolved to com.google.protobuf.StringValue which is not defined; consider using a leading dot",
422		},
423		{
424			map[string]string{
425				"foo.proto": "syntax = \"proto2\";\n" +
426					"import \"google/protobuf/descriptor.proto\";\n" +
427					"message Foo {\n" +
428					"  optional group Bar = 1 { optional string name = 1; }\n" +
429					"}\n" +
430					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
431					"message Baz { option (foo).bar.name = \"abc\"; }\n",
432			},
433			"", // should succeed
434		},
435		{
436			map[string]string{
437				"foo.proto": "syntax = \"proto2\";\n" +
438					"import \"google/protobuf/descriptor.proto\";\n" +
439					"message Foo {\n" +
440					"  optional group Bar = 1 { optional string name = 1; }\n" +
441					"}\n" +
442					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
443					"message Baz { option (foo).Bar.name = \"abc\"; }\n",
444			},
445			"foo.proto:7:28: message Baz: option (foo).Bar.name: field Bar of Foo does not exist",
446		},
447		{
448			map[string]string{
449				"foo.proto": "syntax = \"proto2\";\n" +
450					"import \"google/protobuf/descriptor.proto\";\n" +
451					"extend google.protobuf.MessageOptions {\n" +
452					"  optional group Foo = 10001 { optional string name = 1; }\n" +
453					"}\n" +
454					"message Bar { option (foo).name = \"abc\"; }\n",
455			},
456			"", // should succeed
457		},
458		{
459			map[string]string{
460				"foo.proto": "syntax = \"proto2\";\n" +
461					"import \"google/protobuf/descriptor.proto\";\n" +
462					"extend google.protobuf.MessageOptions {\n" +
463					"  optional group Foo = 10001 { optional string name = 1; }\n" +
464					"}\n" +
465					"message Bar { option (Foo).name = \"abc\"; }\n",
466			},
467			"foo.proto:6:22: message Bar: invalid extension: Foo is a message, not an extension",
468		},
469		{
470			map[string]string{
471				"foo.proto": "syntax = \"proto2\";\n" +
472					"import \"google/protobuf/descriptor.proto\";\n" +
473					"message Foo {\n" +
474					"  optional group Bar = 1 { optional string name = 1; }\n" +
475					"}\n" +
476					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
477					"message Baz { option (foo) = { Bar< name: \"abc\" > }; }\n",
478			},
479			"", // should succeed
480		},
481		{
482			map[string]string{
483				"foo.proto": "syntax = \"proto2\";\n" +
484					"import \"google/protobuf/descriptor.proto\";\n" +
485					"message Foo {\n" +
486					"  optional group Bar = 1 { optional string name = 1; }\n" +
487					"}\n" +
488					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
489					"message Baz { option (foo) = { bar< name: \"abc\" > }; }\n",
490			},
491			"foo.proto:7:30: message Baz: option (foo): field bar not found (did you mean the group named Bar?)",
492		},
493		{
494			map[string]string{
495				"foo.proto": "syntax = \"proto2\";\n" +
496					"import \"google/protobuf/descriptor.proto\";\n" +
497					"message Foo { extensions 1 to 10; }\n" +
498					"extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" +
499					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
500					"message Baz { option (foo) = { [bar]< name: \"abc\" > }; }\n",
501			},
502			"", // should succeed
503		},
504		{
505			map[string]string{
506				"foo.proto": "syntax = \"proto2\";\n" +
507					"import \"google/protobuf/descriptor.proto\";\n" +
508					"message Foo { extensions 1 to 10; }\n" +
509					"extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" +
510					"extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" +
511					"message Baz { option (foo) = { [Bar]< name: \"abc\" > }; }\n",
512			},
513			"foo.proto:6:30: message Baz: option (foo): field Bar not found",
514		},
515	}
516	for i, tc := range testCases {
517		acc := func(filename string) (io.ReadCloser, error) {
518			f, ok := tc.input[filename]
519			if !ok {
520				return nil, fmt.Errorf("file not found: %s", filename)
521			}
522			return ioutil.NopCloser(strings.NewReader(f)), nil
523		}
524		names := make([]string, 0, len(tc.input))
525		for k := range tc.input {
526			names = append(names, k)
527		}
528		_, err := Parser{Accessor: acc}.ParseFiles(names...)
529		if tc.errMsg == "" {
530			if err != nil {
531				t.Errorf("case %d: expecting no error; instead got error %q", i, err)
532			}
533		} else if err == nil {
534			t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg)
535		} else if err.Error() != tc.errMsg {
536			t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err)
537		}
538	}
539}
540
541func TestProto3Enums(t *testing.T) {
542	file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }`
543	file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }`
544	getFileContents := func(file, syntax string) string {
545		contents := strings.Replace(file, "<SYNTAX>", syntax, 1)
546		label := ""
547		if syntax == "proto2" {
548			label = "optional"
549		}
550		return strings.Replace(contents, "<LABEL>", label, 1)
551	}
552
553	syntaxOptions := []string{"proto2", "proto3"}
554	for _, o1 := range syntaxOptions {
555		fc1 := getFileContents(file1, o1)
556
557		for _, o2 := range syntaxOptions {
558			fc2 := getFileContents(file2, o2)
559
560			// now parse the protos
561			acc := func(filename string) (io.ReadCloser, error) {
562				var data string
563				switch filename {
564				case "f1.proto":
565					data = fc1
566				case "f2.proto":
567					data = fc2
568				default:
569					return nil, fmt.Errorf("file not found: %s", filename)
570				}
571				return ioutil.NopCloser(strings.NewReader(data)), nil
572			}
573			_, err := Parser{Accessor: acc}.ParseFiles("f1.proto", "f2.proto")
574
575			if o1 != o2 && o2 == "proto3" {
576				expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message"
577				if err == nil {
578					t.Errorf("expecting validation error; instead got no error")
579				} else if err.Error() != expected {
580					t.Errorf("expecting validation error %q; instead got: %q", expected, err)
581				}
582			} else {
583				// other cases succeed (okay to for proto2 to use enum from proto3 file and
584				// obviously okay for proto2 importing proto2 and proto3 importing proto3)
585				testutil.Ok(t, err)
586			}
587		}
588	}
589}
590
591func TestCustomErrorReporterWithLinker(t *testing.T) {
592	input := map[string]string{
593		"a/b/b.proto": `package a.b;
594
595import "google/protobuf/descriptor.proto";
596
597extend google.protobuf.FieldOptions {
598  optional Foo foo = 50001;
599}
600
601message Foo {
602  optional string bar = 1;
603}`,
604		"a/c/c.proto": `import "a/b/b.proto";
605
606message ReferencesFooOption {
607  optional string baz = 1 [(a.b.foo).bat = "hello"];
608}`,
609	}
610	errMsg := "a/c/c.proto:4:38: field ReferencesFooOption.baz: option (a.b.foo).bat: field bat of a.b.Foo does not exist"
611
612	acc := func(filename string) (io.ReadCloser, error) {
613		f, ok := input[filename]
614		if !ok {
615			return nil, fmt.Errorf("file not found: %s", filename)
616		}
617		return ioutil.NopCloser(strings.NewReader(f)), nil
618	}
619	names := make([]string, 0, len(input))
620	for k := range input {
621		names = append(names, k)
622	}
623	var errs []error
624	_, err := Parser{
625		Accessor: acc,
626		ErrorReporter: func(errorWithPos ErrorWithPos) error {
627			errs = append(errs, errorWithPos)
628			// need to return nil to make sure this test case works
629			// this will result in us only getting an error from errorHandler.getError()
630			// we need to make sure this is called correctly in the linker so that all
631			// errors are properly propagated from the return value of linkFiles(), and
632			// therefor Parse returns ErrInvalidSource
633			return nil
634		},
635	}.ParseFiles(names...)
636	if err != ErrInvalidSource {
637		t.Errorf("expecting validation error %v; instead got: %v", ErrInvalidSource, err)
638	} else if len(errs) != 1 || errs[0].Error() != errMsg {
639		t.Errorf("expecting validation error %q; instead got: %q", errs[0].Error(), errMsg)
640	}
641}
642