Skip to content

Commit 0142a07

Browse files
authored
Fix panic that can occur when interpreting options in lenient mode (#331)
The panic fix is tiny. But this commit is bigger because other changes/improvements were called for: most of the code changes are to improve error handling when in lenient mode. After I fixed the panic, the test case was failing in a different way, due to an issue with how errors (even in lenient mode) were still being passed to an error reporter and causing the stage to fail. This issue was introduced in #279. That PR moved things around, pushing the responsibility of calling `interp.reporter.HandleError` down, so that interpreting a single option could potentially report multiple errors (instead of failing fast and only reporting the first). But when in lenient mode, we don't actually want to send those errors to the reporter: sending the error to the reporter meant the error would get memoized and then returned in subsequent handle calls, which could cause the process to fail when it should be lenient and also can cause it to report the wrong error in lenient mode. So now we demarcate the parts of the process where errors are tolerated (i.e. where lenience mode is actually activated) with a new field on the interpreter that is examined when errors are reported.
1 parent 0de629a commit 0142a07

File tree

7 files changed

+168
-96
lines changed

7 files changed

+168
-96
lines changed

internal/options.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,24 @@ import (
1818
"google.golang.org/protobuf/types/descriptorpb"
1919

2020
"github.com/bufbuild/protocompile/ast"
21-
"github.com/bufbuild/protocompile/reporter"
2221
)
2322

2423
type hasOptionNode interface {
2524
OptionNode(part *descriptorpb.UninterpretedOption) ast.OptionDeclNode
2625
FileNode() ast.FileDeclNode // needed in order to query for NodeInfo
2726
}
2827

29-
func FindFirstOption(res hasOptionNode, handler *reporter.Handler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
28+
type errorHandler func(span ast.SourceSpan, format string, args ...interface{}) error
29+
30+
func FindFirstOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
3031
return findOption(res, handler, scope, opts, name, false, true)
3132
}
3233

33-
func FindOption(res hasOptionNode, handler *reporter.Handler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
34+
func FindOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
3435
return findOption(res, handler, scope, opts, name, true, false)
3536
}
3637

37-
func findOption(res hasOptionNode, handler *reporter.Handler, scope string, opts []*descriptorpb.UninterpretedOption, name string, exact, first bool) (int, error) {
38+
func findOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string, exact, first bool) (int, error) {
3839
found := -1
3940
for i, opt := range opts {
4041
if exact && len(opt.Name) != 1 {
@@ -51,7 +52,7 @@ func findOption(res hasOptionNode, handler *reporter.Handler, scope string, opts
5152
fn := res.FileNode()
5253
node := optNode.GetName()
5354
nodeInfo := fn.NodeInfo(node)
54-
return -1, handler.HandleErrorf(nodeInfo, "%s: option %s cannot be defined more than once", scope, name)
55+
return -1, handler(nodeInfo, "%s: option %s cannot be defined more than once", scope, name)
5556
}
5657
found = i
5758
}

options/options.go

Lines changed: 114 additions & 64 deletions
Large diffs are not rendered by default.

options/options_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ func TestOptionsInUnlinkedFiles(t *testing.T) {
129129
assert.Equal(t, "foo.bar", fd.GetOptions().GetGoPackage())
130130
},
131131
},
132+
{
133+
name: "file options, not custom",
134+
contents: `option go_package = "foo.bar"; option must_link = "FOO";`,
135+
uninterpreted: map[string]interface{}{
136+
"test.proto:must_link": "FOO",
137+
},
138+
checkInterpreted: func(t *testing.T, fd *descriptorpb.FileDescriptorProto) {
139+
assert.Equal(t, "foo.bar", fd.GetOptions().GetGoPackage())
140+
},
141+
},
132142
{
133143
name: "message options",
134144
contents: `message Test { option (must.link) = 1.234; option deprecated = true; }`,
@@ -244,6 +254,25 @@ func TestOptionsInUnlinkedFiles(t *testing.T) {
244254
}
245255
}
246256

257+
func TestOptionsInUnlinkedFileInvalid(t *testing.T) {
258+
t.Parallel()
259+
h := reporter.NewHandler(nil)
260+
ast, err := parser.Parse(
261+
"test.proto",
262+
strings.NewReader(
263+
`syntax = "proto2";
264+
package foo;
265+
option malformed_non_existent = true;
266+
option features.utf8_validation = NONE;`,
267+
), h)
268+
require.NoError(t, err, "failed to parse")
269+
res, err := parser.ResultFromAST(ast, false, h)
270+
require.NoError(t, err, "failed to produce descriptor proto")
271+
_, err = options.InterpretUnlinkedOptions(res)
272+
require.ErrorContains(t, err,
273+
`test.proto:4:29: field "google.protobuf.FeatureSet.utf8_validation" was not introduced until edition 2023`)
274+
}
275+
247276
func buildUninterpretedMapForFile(fd *descriptorpb.FileDescriptorProto, opts map[string]interface{}) {
248277
buildUninterpretedMap(fd.GetName(), fd.GetOptions().GetUninterpretedOption(), opts)
249278
for _, md := range fd.GetMessageType() {

parser/result.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ func (r *result) addMessageBody(msgd *descriptorpb.DescriptorProto, body *ast.Me
686686

687687
func (r *result) isMessageSetWireFormat(scope string, md *descriptorpb.DescriptorProto, handler *reporter.Handler) (*descriptorpb.UninterpretedOption, error) {
688688
uo := md.GetOptions().GetUninterpretedOption()
689-
index, err := internal.FindOption(r, handler, scope, uo, "message_set_wire_format")
689+
index, err := internal.FindOption(r, handler.HandleErrorf, scope, uo, "message_set_wire_format")
690690
if err != nil {
691691
return nil, err
692692
}

parser/validate.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func validateNoFeatures(res *result, syntax protoreflect.Syntax, scope string, o
112112
// Editions is allowed to use features
113113
return nil
114114
}
115-
if index, err := internal.FindFirstOption(res, handler, scope, opts, "features"); err != nil {
115+
if index, err := internal.FindFirstOption(res, handler.HandleErrorf, scope, opts, "features"); err != nil {
116116
return err
117117
} else if index >= 0 {
118118
optNode := res.OptionNode(opts[index])
@@ -135,7 +135,7 @@ func validateMessage(res *result, syntax protoreflect.Syntax, name protoreflect.
135135
}
136136
}
137137

138-
if index, err := internal.FindOption(res, handler, scope, md.Options.GetUninterpretedOption(), "map_entry"); err != nil {
138+
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, md.Options.GetUninterpretedOption(), "map_entry"); err != nil {
139139
return err
140140
} else if index >= 0 {
141141
optNode := res.OptionNode(md.Options.GetUninterpretedOption()[index])
@@ -331,7 +331,7 @@ func validateEnum(res *result, syntax protoreflect.Syntax, name protoreflect.Ful
331331

332332
allowAlias := false
333333
var allowAliasOpt *descriptorpb.UninterpretedOption
334-
if index, err := internal.FindOption(res, handler, scope, ed.Options.GetUninterpretedOption(), "allow_alias"); err != nil {
334+
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, ed.Options.GetUninterpretedOption(), "allow_alias"); err != nil {
335335
return err
336336
} else if index >= 0 {
337337
allowAliasOpt = ed.Options.UninterpretedOption[index]
@@ -481,7 +481,7 @@ func validateField(res *result, syntax protoreflect.Syntax, name protoreflect.Fu
481481
return err
482482
}
483483
}
484-
if index, err := internal.FindOption(res, handler, scope, fld.Options.GetUninterpretedOption(), "packed"); err != nil {
484+
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, fld.Options.GetUninterpretedOption(), "packed"); err != nil {
485485
return err
486486
} else if index >= 0 {
487487
optNode := res.OptionNode(fld.Options.GetUninterpretedOption()[index])
@@ -491,7 +491,7 @@ func validateField(res *result, syntax protoreflect.Syntax, name protoreflect.Fu
491491
}
492492
}
493493
} else if syntax == protoreflect.Proto3 {
494-
if index, err := internal.FindOption(res, handler, scope, fld.Options.GetUninterpretedOption(), "default"); err != nil {
494+
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, fld.Options.GetUninterpretedOption(), "default"); err != nil {
495495
return err
496496
} else if index >= 0 {
497497
optNode := res.OptionNode(fld.Options.GetUninterpretedOption()[index])

reporter/errors.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,36 @@ type ErrorWithPos interface {
3939

4040
// Error creates a new ErrorWithPos from the given error and source position.
4141
func Error(span ast.SourceSpan, err error) ErrorWithPos {
42-
return errorWithSpan{SourceSpan: span, underlying: err}
42+
var ewp ErrorWithPos
43+
if errors.As(err, &ewp) {
44+
// replace existing position with given one
45+
return &errorWithSpan{SourceSpan: span, underlying: ewp.Unwrap()}
46+
}
47+
return &errorWithSpan{SourceSpan: span, underlying: err}
4348
}
4449

4550
// Errorf creates a new ErrorWithPos whose underlying error is created using the
4651
// given message format and arguments (via fmt.Errorf).
4752
func Errorf(span ast.SourceSpan, format string, args ...interface{}) ErrorWithPos {
48-
return errorWithSpan{SourceSpan: span, underlying: fmt.Errorf(format, args...)}
53+
return Error(span, fmt.Errorf(format, args...))
4954
}
5055

5156
type errorWithSpan struct {
5257
ast.SourceSpan
5358
underlying error
5459
}
5560

56-
func (e errorWithSpan) Error() string {
61+
func (e *errorWithSpan) Error() string {
5762
sourcePos := e.GetPosition()
5863
return fmt.Sprintf("%s: %v", sourcePos, e.underlying)
5964
}
6065

61-
func (e errorWithSpan) GetPosition() ast.SourcePos {
66+
func (e *errorWithSpan) GetPosition() ast.SourcePos {
6267
return e.Start()
6368
}
6469

65-
func (e errorWithSpan) Unwrap() error {
70+
func (e *errorWithSpan) Unwrap() error {
6671
return e.underlying
6772
}
6873

69-
var _ ErrorWithPos = errorWithSpan{}
74+
var _ ErrorWithPos = (*errorWithSpan)(nil)

reporter/reporter.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,7 @@ func (h *Handler) HandleError(err error) error {
153153
// call to HandleError or HandleErrorf), that same error is returned and the
154154
// given error is not reported.
155155
func (h *Handler) HandleErrorWithPos(span ast.SourceSpan, err error) error {
156-
if ewp, ok := err.(ErrorWithPos); ok {
157-
// replace existing position with given one
158-
err = errorWithSpan{SourceSpan: span, underlying: ewp.Unwrap()}
159-
} else {
160-
err = errorWithSpan{SourceSpan: span, underlying: err}
161-
}
162-
return h.HandleError(err)
156+
return h.HandleError(Error(span, err))
163157
}
164158

165159
// HandleErrorf handles an error with the given source position, creating the
@@ -191,14 +185,7 @@ func (h *Handler) HandleWarning(err ErrorWithPos) {
191185
// HandleWarningWithPos handles a warning with the given source position. This will
192186
// delegate to the handler's configured reporter.
193187
func (h *Handler) HandleWarningWithPos(span ast.SourceSpan, err error) {
194-
ewp, ok := err.(ErrorWithPos)
195-
if ok {
196-
// replace existing position with given one
197-
ewp = errorWithSpan{SourceSpan: span, underlying: ewp.Unwrap()}
198-
} else {
199-
ewp = errorWithSpan{SourceSpan: span, underlying: err}
200-
}
201-
h.HandleWarning(ewp)
188+
h.HandleWarning(Error(span, err))
202189
}
203190

204191
// HandleWarningf handles a warning with the given source position, creating the

0 commit comments

Comments
 (0)