github.com/Big-big-orange/protoreflect@v0.0.0-20240408141420-285cedfdf6a4/desc/protoparse/reporting_test.go (about)

     1  package protoparse
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/Big-big-orange/protoreflect/internal/testutil"
    11  )
    12  
    13  func TestErrorReporting(t *testing.T) {
    14  	tooManyErrors := errors.New("too many errors")
    15  	limitedErrReporter := func(limit int, count *int) ErrorReporter {
    16  		return func(err ErrorWithPos) error {
    17  			*count++
    18  			if *count > limit {
    19  				return tooManyErrors
    20  			}
    21  			return nil
    22  		}
    23  	}
    24  	trackingReporter := func(errs *[]ErrorWithPos, count *int) ErrorReporter {
    25  		return func(err ErrorWithPos) error {
    26  			*count++
    27  			*errs = append(*errs, err)
    28  			return nil
    29  		}
    30  	}
    31  	fail := errors.New("failure!")
    32  	failFastReporter := func(count *int) ErrorReporter {
    33  		return func(err ErrorWithPos) error {
    34  			*count++
    35  			return fail
    36  		}
    37  	}
    38  
    39  	testCases := []struct {
    40  		fileNames       []string
    41  		files           map[string]string
    42  		expectedErrs    []string
    43  		expectedErrsAlt []string
    44  	}{
    45  		{
    46  			// multiple syntax errors
    47  			fileNames: []string{"test.proto"},
    48  			files: map[string]string{
    49  				"test.proto": `
    50  					syntax = "proto";
    51  					package foo
    52  
    53  					enum State { A = 0; B = 1; C; D }
    54  					message Foo {
    55  						foo = 1;
    56  					}
    57  					`,
    58  			},
    59  			expectedErrs: []string{
    60  				"test.proto:5:41: syntax error: expecting ';'",
    61  				"test.proto:5:69: syntax error: unexpected ';', expecting '='",
    62  				"test.proto:7:53: syntax error: unexpected '='",
    63  			},
    64  		},
    65  		{
    66  			// multiple validation errors
    67  			fileNames: []string{"test.proto"},
    68  			files: map[string]string{
    69  				"test.proto": `
    70  					syntax = "proto3";
    71  					message Foo {
    72  						string foo = 0;
    73  					}
    74  					enum State { C }
    75  					enum Bar {
    76  						BAZ = 1;
    77  						BUZZ = 1;
    78  					}
    79  					`,
    80  			},
    81  			expectedErrs: []string{
    82  				"test.proto:6:56: syntax error: unexpected '}', expecting '='",
    83  			},
    84  		},
    85  		{
    86  			// multiple link errors
    87  			fileNames: []string{"test.proto"},
    88  			files: map[string]string{
    89  				"test.proto": `
    90  					syntax = "proto3";
    91  					message Foo {
    92  						string foo = 1;
    93  					}
    94  					enum Bar {
    95  						BAZ = 0;
    96  						BAZ = 2;
    97  					}
    98  					service Bar {
    99  						rpc Foobar (Foo) returns (Foo);
   100  						rpc Foobar (Frob) returns (Nitz);
   101  					}
   102  					`,
   103  			},
   104  			expectedErrs: []string{
   105  				`test.proto:8:49: symbol "BAZ" already defined at test.proto:7:49; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`,
   106  				`test.proto:10:49: symbol "Bar" already defined at test.proto:6:46`,
   107  				`test.proto:12:53: symbol "Bar.Foobar" already defined at test.proto:11:53`,
   108  			},
   109  		},
   110  		{
   111  			// syntax errors across multiple files
   112  			fileNames: []string{"test1.proto", "test2.proto"},
   113  			files: map[string]string{
   114  				"test1.proto": `
   115  					syntax = "proto3";
   116  					import "test2.proto";
   117  					message Foo {
   118  						string foo = -1;
   119  					}
   120  					service Bar {
   121  						rpc Foo (Foo);
   122  					}
   123  					`,
   124  				"test2.proto": `
   125  					syntax = "proto3";
   126  					message Baz {
   127  						required string foo = 1;
   128  					}
   129  					service Service {
   130  						Foo; Bar; Baz;
   131  					}
   132  					`,
   133  			},
   134  			expectedErrs: []string{
   135  				"test2.proto:7:49: syntax error: unexpected identifier, expecting \"option\" or \"rpc\" or '}'",
   136  				"test1.proto:5:62: syntax error: unexpected '-', expecting int literal",
   137  				"test1.proto:8:62: syntax error: unexpected ';', expecting \"returns\"",
   138  			},
   139  			expectedErrsAlt: []string{
   140  				"test1.proto:5:62: syntax error: unexpected '-', expecting int literal",
   141  				"test1.proto:8:62: syntax error: unexpected ';', expecting \"returns\"",
   142  				"test2.proto:7:49: syntax error: unexpected identifier, expecting \"option\" or \"rpc\" or '}'",
   143  			},
   144  		},
   145  		{
   146  			// link errors across multiple files
   147  			fileNames: []string{"test1.proto", "test2.proto"},
   148  			files: map[string]string{
   149  				"test1.proto": `
   150  					syntax = "proto3";
   151  					import "test2.proto";
   152  					message Foo {
   153  						string foo = 1;
   154  					}
   155  					service Bar {
   156  						rpc Frob (Empty) returns (Nitz);
   157  					}
   158  					`,
   159  				"test2.proto": `
   160  					syntax = "proto3";
   161  					message Empty {}
   162  					enum Bar {
   163  						BAZ = 0;
   164  					}
   165  					service Foo {
   166  						rpc DoSomething (Empty) returns (Foo);
   167  					}
   168  					`,
   169  			},
   170  			expectedErrs: []string{
   171  				"test2.proto:8:82: method Foo.DoSomething: invalid response type: Foo is a service, not a message",
   172  			},
   173  		},
   174  	}
   175  
   176  	for i, tc := range testCases {
   177  		var p Parser
   178  		p.Accessor = FileContentsFromMap(tc.files)
   179  
   180  		var reported []ErrorWithPos
   181  		count := 0
   182  		p.ErrorReporter = trackingReporter(&reported, &count)
   183  		_, err := p.ParseFiles(tc.fileNames...)
   184  		reportedMsgs := make([]string, len(reported))
   185  		for j := range reported {
   186  			reportedMsgs[j] = reported[j].Error()
   187  		}
   188  		t.Logf("case #%d: got %d errors:\n\t%s", i+1, len(reported), strings.Join(reportedMsgs, "\n\t"))
   189  
   190  		// returns sentinel, but all actual errors in reported
   191  		testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1)
   192  		actual := make([]string, len(reported))
   193  		for j := range reported {
   194  			actual[j] = reported[j].Error()
   195  		}
   196  		expected := tc.expectedErrs
   197  		if len(tc.expectedErrsAlt) > 0 && !reflect.DeepEqual(tc.expectedErrs, actual) {
   198  			expected = tc.expectedErrsAlt
   199  		}
   200  		testutil.Eq(t, len(expected), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs))
   201  		testutil.Eq(t, expected, actual, "case #%d: wrong errors reported", i+1)
   202  		for j := range expected {
   203  			split := strings.SplitN(expected[j], ":", 4)
   204  			testutil.Eq(t, 4, len(split), "case #%d: expected %q [%d] to contain at least 4 elements split by :", i+1, tc.expectedErrs[j], j)
   205  			testutil.Eq(t, split[3], " "+reported[j].Unwrap().Error(), "case #%d: parse error underlying[%d] have %q; instead got %q", i+1, j, split[3], reported[j].Unwrap().Error())
   206  		}
   207  
   208  		count = 0
   209  		p.ErrorReporter = failFastReporter(&count)
   210  		_, err = p.ParseFiles(tc.fileNames...)
   211  		testutil.Eq(t, fail, err, "case #%d: parse should have failed fast", i+1)
   212  		testutil.Eq(t, 1, count, "case #%d: parse should have called reporter only once", i+1)
   213  
   214  		count = 0
   215  		p.ErrorReporter = limitedErrReporter(3, &count)
   216  		_, err = p.ParseFiles(tc.fileNames...)
   217  		if len(tc.expectedErrs) > 3 {
   218  			testutil.Eq(t, tooManyErrors, err, "case #%d: parse should have failed with too many errors", i+1)
   219  			testutil.Eq(t, 4, count, "case #%d: parse should have called reporter 4 times", i+1)
   220  		} else {
   221  			// less than threshold means reporter always returned nil,
   222  			// so parse returns ErrInvalidSource sentinel
   223  			testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1)
   224  			testutil.Eq(t, len(tc.expectedErrs), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs))
   225  		}
   226  	}
   227  }
   228  
   229  func TestWarningReporting(t *testing.T) {
   230  	type msg struct {
   231  		pos  SourcePos
   232  		text string
   233  	}
   234  	var msgs []msg
   235  	rep := func(warn ErrorWithPos) {
   236  		msgs = append(msgs, msg{
   237  			pos: warn.GetPosition(), text: warn.Unwrap().Error(),
   238  		})
   239  	}
   240  
   241  	testCases := []struct {
   242  		name            string
   243  		sources         map[string]string
   244  		expectedNotices []string
   245  	}{
   246  		{
   247  			name: "syntax proto2",
   248  			sources: map[string]string{
   249  				"test.proto": `syntax = "proto2"; message Foo {}`,
   250  			},
   251  		},
   252  		{
   253  			name: "syntax proto3",
   254  			sources: map[string]string{
   255  				"test.proto": `syntax = "proto3"; message Foo {}`,
   256  			},
   257  		},
   258  		{
   259  			name: "no syntax",
   260  			sources: map[string]string{
   261  				"test.proto": `message Foo {}`,
   262  			},
   263  			expectedNotices: []string{
   264  				"test.proto:1:1: no syntax specified; defaulting to proto2 syntax",
   265  			},
   266  		},
   267  		{
   268  			name: "used import",
   269  			sources: map[string]string{
   270  				"test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`,
   271  				"foo.proto":  `syntax = "proto3"; message Bar { string name = 1; }`,
   272  			},
   273  		},
   274  		{
   275  			name: "used public import",
   276  			sources: map[string]string{
   277  				"test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`,
   278  				// we're only asking to compile test.proto, so we won't report unused import for baz.proto
   279  				"foo.proto": `syntax = "proto3"; import public "bar.proto"; import "baz.proto";`,
   280  				"bar.proto": `syntax = "proto3"; message Bar { string name = 1; }`,
   281  				"baz.proto": `syntax = "proto3"; message Baz { }`,
   282  			},
   283  		},
   284  		{
   285  			name: "used nested public import",
   286  			sources: map[string]string{
   287  				"test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`,
   288  				"foo.proto":  `syntax = "proto3"; import public "baz.proto";`,
   289  				"baz.proto":  `syntax = "proto3"; import public "bar.proto";`,
   290  				"bar.proto":  `syntax = "proto3"; message Bar { string name = 1; }`,
   291  			},
   292  		},
   293  		{
   294  			name: "unused import",
   295  			sources: map[string]string{
   296  				"test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { string name = 1; }`,
   297  				"foo.proto":  `syntax = "proto3"; message Bar { string name = 1; }`,
   298  			},
   299  			expectedNotices: []string{
   300  				`test.proto:1:20: import "foo.proto" not used`,
   301  			},
   302  		},
   303  		{
   304  			name: "multiple unused imports",
   305  			sources: map[string]string{
   306  				"test.proto": `syntax = "proto3"; import "foo.proto"; import "bar.proto"; import "baz.proto"; message Test { Bar bar = 1; }`,
   307  				"foo.proto":  `syntax = "proto3"; message Foo {};`,
   308  				"bar.proto":  `syntax = "proto3"; message Bar {};`,
   309  				"baz.proto":  `syntax = "proto3"; message Baz {};`,
   310  			},
   311  			expectedNotices: []string{
   312  				`test.proto:1:20: import "foo.proto" not used`,
   313  				`test.proto:1:60: import "baz.proto" not used`,
   314  			},
   315  		},
   316  		{
   317  			name: "unused public import is not reported",
   318  			sources: map[string]string{
   319  				"test.proto": `syntax = "proto3"; import public "foo.proto"; message Foo { }`,
   320  				"foo.proto":  `syntax = "proto3"; message Bar { string name = 1; }`,
   321  			},
   322  		},
   323  		{
   324  			name: "unused descriptor.proto import",
   325  			sources: map[string]string{
   326  				"test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { }`,
   327  			},
   328  			expectedNotices: []string{
   329  				`test.proto:1:20: import "google/protobuf/descriptor.proto" not used`,
   330  			},
   331  		},
   332  		{
   333  			name: "explicitly used descriptor.proto import",
   334  			sources: map[string]string{
   335  				"test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; extend google.protobuf.MessageOptions { string foobar = 33333; }`,
   336  			},
   337  		},
   338  		{
   339  			// having options implicitly uses decriptor.proto
   340  			name: "implicitly used descriptor.proto import",
   341  			sources: map[string]string{
   342  				"test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { option deprecated = true; }`,
   343  			},
   344  		},
   345  		{
   346  			// makes sure we can use a given descriptor.proto to override non-custom options
   347  			name: "implicitly used descriptor.proto import with new option",
   348  			sources: map[string]string{
   349  				"test.proto":                       `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { option foobar = 123; }`,
   350  				"google/protobuf/descriptor.proto": `syntax = "proto2"; package google.protobuf; message MessageOptions { optional fixed32 foobar = 99; }`,
   351  			},
   352  		},
   353  	}
   354  	for _, testCase := range testCases {
   355  		t.Run(testCase.name, func(t *testing.T) {
   356  			accessor := FileContentsFromMap(testCase.sources)
   357  			p := Parser{
   358  				Accessor:        accessor,
   359  				WarningReporter: rep,
   360  			}
   361  			msgs = nil
   362  			_, err := p.ParseFiles("test.proto")
   363  			testutil.Ok(t, err)
   364  
   365  			actualNotices := make([]string, len(msgs))
   366  			for j, msg := range msgs {
   367  				actualNotices[j] = fmt.Sprintf("%s: %s", msg.pos, msg.text)
   368  			}
   369  			testutil.Eq(t, testCase.expectedNotices, actualNotices)
   370  		})
   371  	}
   372  }