diff --git a/experimental/ast/decl_body.go b/experimental/ast/decl_body.go index 0422ee88..611c4715 100644 --- a/experimental/ast/decl_body.go +++ b/experimental/ast/decl_body.go @@ -35,6 +35,15 @@ import ( // the source file, rather than braces. type DeclBody struct{ declImpl[rawDeclBody] } +// HasBody is an AST node that contains a [Body]. +// +// [File], [DeclBody], and [DeclDef] all implement this interface. +type HasBody interface { + report.Spanner + + Body() DeclBody +} + type rawDeclBody struct { braces token.ID @@ -69,6 +78,11 @@ func (d DeclBody) Span() report.Span { } } +// Body implements [HasBody]. +func (d DeclBody) Body() DeclBody { + return d +} + // Decls returns a [seq.Inserter] over the declarations in this body. func (d DeclBody) Decls() seq.Inserter[DeclAny] { type slice = seq.SliceInserter2[DeclAny, DeclKind, arena.Untyped] diff --git a/experimental/ast/decl_def.go b/experimental/ast/decl_def.go index df168442..80b18a67 100644 --- a/experimental/ast/decl_def.go +++ b/experimental/ast/decl_def.go @@ -56,6 +56,8 @@ type rawDeclDef struct { options arena.Pointer[rawCompactOptions] body arena.Pointer[rawDeclBody] semi token.ID + + corrupt bool } // DeclDefArgs is arguments for creating a [DeclDef] with [Context.NewDeclDef]. @@ -106,6 +108,11 @@ func (d DeclDef) SetType(ty TypeAny) { // // See [DeclDef.Type] for details on where this keyword comes from. func (d DeclDef) Keyword() token.Token { + // There is also the special case of `optional group` and similar. + if g := d.Type().AsPrefixed().Type().AsPath().AsIdent(); g.Text() == "group" { + return g + } + path := d.Type().AsPath() if path.IsZero() { return token.Zero @@ -225,6 +232,17 @@ func (d DeclDef) Semicolon() token.Token { return d.raw.semi.In(d.Context()) } +// IsCorrupt reports whether or not some part of the parser decided that this +// definition is not interpretable as any specific kind of definition. +func (d DeclDef) IsCorrupt() bool { + return !d.IsZero() && d.raw.corrupt +} + +// the compiler to ignore it. See [DeclDef.IsCorrupt]. +func (d DeclDef) MarkCorrupt() { + d.raw.corrupt = true +} + // AsMessage extracts the fields from this definition relevant to interpreting // it as a message. // @@ -407,7 +425,7 @@ func (d DeclDef) AsOption() DefOption { // cases of the switch should then use the As* methods, such as // [DeclDef.AsMessage], to extract the relevant fields. func (d DeclDef) Classify() DefKind { - if d.IsZero() { + if d.IsZero() || d.IsCorrupt() { return DefKindInvalid } diff --git a/experimental/ast/path.go b/experimental/ast/path.go index fd100bed..08e1d1fe 100644 --- a/experimental/ast/path.go +++ b/experimental/ast/path.go @@ -22,7 +22,6 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/ext/iterx" - "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // Path represents a multi-part identifier. @@ -70,12 +69,11 @@ func (p Path) ToRelative() Path { // AsIdent returns the single identifier that comprises this path, or // the zero token. func (p Path) AsIdent() token.Token { - var buf [2]PathComponent - prefix := slicesx.AppendSeq(buf[:0], iterx.Limit(2, p.Components)) - if len(prefix) != 1 || !prefix[0].Separator().IsZero() { + first, ok := iterx.OnlyOne(p.Components) + if !ok || !first.Separator().IsZero() { return token.Zero } - return prefix[0].AsIdent() + return first.AsIdent() } // AsPredeclared returns the [predeclared.Name] that this path represents. @@ -276,7 +274,11 @@ func (p PathComponent) AsExtension() Path { // // May be zero, in the case of e.g. the second component of foo..bar. func (p PathComponent) AsIdent() token.Token { - return p.name.In(p.Context()) + tok := p.name.In(p.Context()) + if tok.Kind() == token.Ident { + return tok + } + return token.Zero } // rawPath is the raw contents of a Path without its Context. diff --git a/experimental/ast/predeclared/name.go b/experimental/ast/predeclared/name.go new file mode 100644 index 00000000..1609185d --- /dev/null +++ b/experimental/ast/predeclared/name.go @@ -0,0 +1,187 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by github.com/bufbuild/protocompile/internal/enum name.yaml. DO NOT EDIT. + +package predeclared + +import ( + "fmt" + + "github.com/bufbuild/protocompile/internal/iter" +) + +// Name is one of the built-in Protobuf names. These represent particular +// paths whose meaning the language overrides to mean something other than +// a relative path with that name. +type Name byte + +const ( + Unknown Name = iota + + // Varint types: 32/64-bit signed, unsigned, and Zig-Zag. + Int32 + Int64 + UInt32 + UInt64 + SInt32 + SInt64 + + // Fixed integer types: 32/64-bit unsigned and signed. + Fixed32 + Fixed64 + SFixed32 + SFixed64 + + // Floating-point types: 32/64-bit, using C-style names. + Float + Double + + // Booleans. + Bool + + // Textual strings (ostensibly UTF-8). + String + + // Arbitrary byte blobs. + Bytes + + // The special "type" map, the only generic type in Protobuf. + Map + + // The special "constant" max, used in range expressions. + Max + + // True and false constants for bool. + True + False + + // Special floating-point constants for infinity and NaN. + Inf + NAN + + // Aliases for the floating-point types with explicit bit-sizes. + Float32 = Float + Float64 = Double +) + +// String implements [fmt.Stringer]. +func (v Name) String() string { + if int(v) < 0 || int(v) > len(_table_Name_String) { + return fmt.Sprintf("Name(%v)", int(v)) + } + return _table_Name_String[int(v)] +} + +// GoString implements [fmt.GoStringer]. +func (v Name) GoString() string { + if int(v) < 0 || int(v) > len(_table_Name_GoString) { + return fmt.Sprintf("predeclaredName(%v)", int(v)) + } + return _table_Name_GoString[int(v)] +} + +// Lookup looks up a predefined identifier by name. +// +// If name does not name a predefined identifier, returns [Unknown]. +func Lookup(s string) Name { + return _table_Name_Lookup[s] +} + +// All returns an iterator over all distinct [Name] values. +func All() iter.Seq[Name] { + return func(yield func(Name) bool) { + for i := 0; i < 22; i++ { + if !yield(Name(i)) { + return + } + } + } +} + +var _table_Name_String = [...]string{ + Unknown: "unknown", + Int32: "int32", + Int64: "int64", + UInt32: "uint32", + UInt64: "uint64", + SInt32: "sint32", + SInt64: "sint64", + Fixed32: "fixed32", + Fixed64: "fixed64", + SFixed32: "sfixed32", + SFixed64: "sfixed64", + Float: "float", + Double: "double", + Bool: "bool", + String: "string", + Bytes: "bytes", + Map: "map", + Max: "max", + True: "true", + False: "false", + Inf: "inf", + NAN: "nan", +} + +var _table_Name_GoString = [...]string{ + Unknown: "Unknown", + Int32: "Int32", + Int64: "Int64", + UInt32: "UInt32", + UInt64: "UInt64", + SInt32: "SInt32", + SInt64: "SInt64", + Fixed32: "Fixed32", + Fixed64: "Fixed64", + SFixed32: "SFixed32", + SFixed64: "SFixed64", + Float: "Float", + Double: "Double", + Bool: "Bool", + String: "String", + Bytes: "Bytes", + Map: "Map", + Max: "Max", + True: "True", + False: "False", + Inf: "Inf", + NAN: "NAN", +} + +var _table_Name_Lookup = map[string]Name{ + "unknown": Unknown, + "int32": Int32, + "int64": Int64, + "uint32": UInt32, + "uint64": UInt64, + "sint32": SInt32, + "sint64": SInt64, + "fixed32": Fixed32, + "fixed64": Fixed64, + "sfixed32": SFixed32, + "sfixed64": SFixed64, + "float": Float, + "double": Double, + "bool": Bool, + "string": String, + "bytes": Bytes, + "map": Map, + "max": Max, + "true": True, + "false": False, + "inf": Inf, + "nan": NAN, +} +var _ iter.Seq[int] // Mark iter as used. diff --git a/experimental/ast/predeclared/predeclared.yaml b/experimental/ast/predeclared/name.yaml similarity index 100% rename from experimental/ast/predeclared/predeclared.yaml rename to experimental/ast/predeclared/name.yaml diff --git a/experimental/ast/predeclared/predeclared.go b/experimental/ast/predeclared/predeclared.go index b017d3ee..7cfb817f 100644 --- a/experimental/ast/predeclared/predeclared.go +++ b/experimental/ast/predeclared/predeclared.go @@ -12,176 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Code generated by github.com/bufbuild/protocompile/internal/enum predeclared.yaml. DO NOT EDIT. - -package predeclared - -import ( - "fmt" - - "github.com/bufbuild/protocompile/internal/iter" -) - -// Name is one of the built-in Protobuf names. These represent particular -// paths whose meaning the language overrides to mean something other than -// a relative path with that name. -type Name byte - -const ( - Unknown Name = iota - - // Varint types: 32/64-bit signed, unsigned, and Zig-Zag. - Int32 - Int64 - UInt32 - UInt64 - SInt32 - SInt64 - - // Fixed integer types: 32/64-bit unsigned and signed. - Fixed32 - Fixed64 - SFixed32 - SFixed64 - - // Floating-point types: 32/64-bit, using C-style names. - Float - Double - - // Booleans. - Bool - - // Textual strings (ostensibly UTF-8). - String - - // Arbitrary byte blobs. - Bytes - - // The special "type" map, the only generic type in Protobuf. - Map - - // The special "constant" max, used in range expressions. - Max - - // True and false constants for bool. - True - False - - // Special floating-point constants for infinity and NaN. - Inf - NAN - - // Aliases for the floating-point types with explicit bit-sizes. - Float32 = Float - Float64 = Double -) - -// String implements [fmt.Stringer]. -func (v Name) String() string { - if int(v) < 0 || int(v) > len(_table_Name_String) { - return fmt.Sprintf("Name(%v)", int(v)) - } - return _table_Name_String[int(v)] -} - -// GoString implements [fmt.GoStringer]. -func (v Name) GoString() string { - if int(v) < 0 || int(v) > len(_table_Name_GoString) { - return fmt.Sprintf("predeclaredName(%v)", int(v)) - } - return _table_Name_GoString[int(v)] -} - -// Lookup looks up a predefined identifier by name. +// package predeclared provides all of the identifiers with a special meaning +// in Protobuf. // -// If name does not name a predefined identifier, returns [Unknown]. -func Lookup(s string) Name { - return _table_Name_Lookup[s] -} - -// All returns an iterator over all distinct [Name] values. -func All() iter.Seq[Name] { - return func(yield func(Name) bool) { - for i := 0; i < 22; i++ { - if !yield(Name(i)) { - return - } - } - } -} +// These are not keywords, but are rather special names injected into scope in +// places where any user-defined path is allowed. For example, the identifier +// string overrides the meaning of a path with a single identifier called string, +// (such as a reference to a message named string in the current package) and as +// such counts as a predeclared identifier. +package predeclared -var _table_Name_String = [...]string{ - Unknown: "unknown", - Int32: "int32", - Int64: "int64", - UInt32: "uint32", - UInt64: "uint64", - SInt32: "sint32", - SInt64: "sint64", - Fixed32: "fixed32", - Fixed64: "fixed64", - SFixed32: "sfixed32", - SFixed64: "sfixed64", - Float: "float", - Double: "double", - Bool: "bool", - String: "string", - Bytes: "bytes", - Map: "map", - Max: "max", - True: "true", - False: "false", - Inf: "inf", - NAN: "nan", -} +//go:generate go run github.com/bufbuild/protocompile/internal/enum name.yaml -var _table_Name_GoString = [...]string{ - Unknown: "Unknown", - Int32: "Int32", - Int64: "Int64", - UInt32: "UInt32", - UInt64: "UInt64", - SInt32: "SInt32", - SInt64: "SInt64", - Fixed32: "Fixed32", - Fixed64: "Fixed64", - SFixed32: "SFixed32", - SFixed64: "SFixed64", - Float: "Float", - Double: "Double", - Bool: "Bool", - String: "String", - Bytes: "Bytes", - Map: "Map", - Max: "Max", - True: "True", - False: "False", - Inf: "Inf", - NAN: "NAN", +// IsScalar returns whether this predeclared name represents one of the scalar +// types. +func (v Name) IsScalar() bool { + return v >= Int32 && v <= Bytes } -var _table_Name_Lookup = map[string]Name{ - "unknown": Unknown, - "int32": Int32, - "int64": Int64, - "uint32": UInt32, - "uint64": UInt64, - "sint32": SInt32, - "sint64": SInt64, - "fixed32": Fixed32, - "fixed64": Fixed64, - "sfixed32": SFixed32, - "sfixed64": SFixed64, - "float": Float, - "double": Double, - "bool": Bool, - "string": String, - "bytes": Bytes, - "map": Map, - "max": Max, - "true": True, - "false": False, - "inf": Inf, - "nan": NAN, +// IsMapKey returns whether this predeclared name represents one of the map key +// types. +func (v Name) IsMapKey() bool { + return (v >= Int32 && v <= SFixed64) || v == Bool || v == String } -var _ iter.Seq[int] // Mark iter as used. diff --git a/experimental/ast/predeclared/predeclared_test.go b/experimental/ast/predeclared/predeclared_test.go new file mode 100644 index 00000000..66a5279b --- /dev/null +++ b/experimental/ast/predeclared/predeclared_test.go @@ -0,0 +1,65 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package predeclared_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bufbuild/protocompile/experimental/ast/predeclared" +) + +func TestPredicates(t *testing.T) { + t.Parallel() + + tests := []struct { + v predeclared.Name + scalar, key bool + }{ + {v: predeclared.Unknown}, + + {v: predeclared.Int32, scalar: true, key: true}, + {v: predeclared.Int64, scalar: true, key: true}, + {v: predeclared.UInt32, scalar: true, key: true}, + {v: predeclared.UInt64, scalar: true, key: true}, + {v: predeclared.SInt32, scalar: true, key: true}, + {v: predeclared.SInt64, scalar: true, key: true}, + + {v: predeclared.Fixed32, scalar: true, key: true}, + {v: predeclared.Fixed64, scalar: true, key: true}, + {v: predeclared.SFixed32, scalar: true, key: true}, + {v: predeclared.SFixed64, scalar: true, key: true}, + + {v: predeclared.Float, scalar: true}, + {v: predeclared.Double, scalar: true}, + + {v: predeclared.String, scalar: true, key: true}, + {v: predeclared.Bytes, scalar: true}, + {v: predeclared.Bool, scalar: true, key: true}, + + {v: predeclared.Map}, + {v: predeclared.Max}, + {v: predeclared.True}, + {v: predeclared.False}, + {v: predeclared.Inf}, + {v: predeclared.NAN}, + } + + for _, test := range tests { + assert.Equal(t, test.scalar, test.v.IsScalar()) + assert.Equal(t, test.key, test.v.IsMapKey()) + } +} diff --git a/experimental/ast/predeclared/doc.go b/experimental/ast/syntax/doc.go similarity index 52% rename from experimental/ast/predeclared/doc.go rename to experimental/ast/syntax/doc.go index b734c66d..e74d8f57 100644 --- a/experimental/ast/predeclared/doc.go +++ b/experimental/ast/syntax/doc.go @@ -12,14 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -// package predeclared provides all of the identifiers with a special meaning -// in Protobuf. -// -// These are not keywords, but are rather special names injected into scope in -// places where any user-defined path is allowed. For example, the identifier -// string overrides the meaning of a path with a single identifier called string, -// (such as a reference to a message named string in the current package) and as -// such counts as a predeclared identifier. -package predeclared +// package syntax specifies all of the syntax pragmas (including editions) +// that Protocompile understands. +package syntax + +import ( + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/iter" +) + +//go:generate go run github.com/bufbuild/protocompile/internal/enum syntax.yaml + +// Editions returns an iterator over all the editions in this package. +func Editions() iter.Seq[Syntax] { + return func(yield func(Syntax) bool) { + for i := 0; i < totalEditions; i++ { + if !yield(Syntax(i + int(Edition2023))) { + break + } + } + } +} -//go:generate go run github.com/bufbuild/protocompile/internal/enum predeclared.yaml +var totalEditions = iterx.Count(All(), func(s Syntax) bool { return s.IsEdition() }) diff --git a/experimental/ast/syntax/is_edition.go b/experimental/ast/syntax/is_edition.go new file mode 100644 index 00000000..a9a44e5a --- /dev/null +++ b/experimental/ast/syntax/is_edition.go @@ -0,0 +1,20 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntax + +// IsEdition returns whether this represents an edition. +func (s Syntax) IsEdition() bool { + return s != Proto2 && s != Proto3 +} diff --git a/experimental/ast/syntax/syntax.go b/experimental/ast/syntax/syntax.go new file mode 100644 index 00000000..7ca0bd60 --- /dev/null +++ b/experimental/ast/syntax/syntax.go @@ -0,0 +1,91 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by github.com/bufbuild/protocompile/internal/enum syntax.yaml. DO NOT EDIT. + +package syntax + +import ( + "fmt" + + "github.com/bufbuild/protocompile/internal/iter" +) + +// Syntax is a known syntax pragma. +// +// Not only does this include "proto2" and "proto3", but also all of the +// editions. +type Syntax int + +const ( + Unknown Syntax = iota + Proto2 + Proto3 + Edition2023 +) + +// String implements [fmt.Stringer]. +func (v Syntax) String() string { + if int(v) < 0 || int(v) > len(_table_Syntax_String) { + return fmt.Sprintf("Syntax(%v)", int(v)) + } + return _table_Syntax_String[int(v)] +} + +// GoString implements [fmt.GoStringer]. +func (v Syntax) GoString() string { + if int(v) < 0 || int(v) > len(_table_Syntax_GoString) { + return fmt.Sprintf("syntaxSyntax(%v)", int(v)) + } + return _table_Syntax_GoString[int(v)] +} + +// Lookup looks up a syntax pragma by name. +// +// If name does not name a known pragma, returns [Unknown]. +func Lookup(s string) Syntax { + return _table_Syntax_Lookup[s] +} + +// All returns an iterator over all known [Syntax] values. +func All() iter.Seq[Syntax] { + return func(yield func(Syntax) bool) { + for i := 1; i < 4; i++ { + if !yield(Syntax(i)) { + return + } + } + } +} + +var _table_Syntax_String = [...]string{ + Unknown: "", + Proto2: "proto2", + Proto3: "proto3", + Edition2023: "2023", +} + +var _table_Syntax_GoString = [...]string{ + Unknown: "Unknown", + Proto2: "Proto2", + Proto3: "Proto3", + Edition2023: "Edition2023", +} + +var _table_Syntax_Lookup = map[string]Syntax{ + "proto2": Proto2, + "proto3": Proto3, + "2023": Edition2023, +} +var _ iter.Seq[int] // Mark iter as used. diff --git a/experimental/ast/syntax/syntax.yaml b/experimental/ast/syntax/syntax.yaml new file mode 100644 index 00000000..55c5a51d --- /dev/null +++ b/experimental/ast/syntax/syntax.yaml @@ -0,0 +1,42 @@ +# Copyright 2020-2024 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Syntax + type: int + docs: | + Syntax is a known syntax pragma. + + Not only does this include "proto2" and "proto3", but also all of the + editions. + methods: + - kind: string + - kind: go-string + - kind: from-string + name: Lookup + docs: | + Lookup looks up a syntax pragma by name. + + If name does not name a known pragma, returns [Unknown]. + skip: [Unknown] + - kind: all + name: All + docs: | + All returns an iterator over all known [Syntax] values. + skip: [Unknown] + values: + - {name: Unknown, string: ""} + - {name: Proto2, string: proto2} + - {name: Proto3, string: proto3} + - {name: Edition2023, string: "2023"} + \ No newline at end of file diff --git a/experimental/ast/syntax/syntax_test.go b/experimental/ast/syntax/syntax_test.go new file mode 100644 index 00000000..9f0dd60d --- /dev/null +++ b/experimental/ast/syntax/syntax_test.go @@ -0,0 +1,37 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntax_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bufbuild/protocompile/experimental/ast/syntax" + "github.com/bufbuild/protocompile/internal/editions" + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/ext/mapsx" + "github.com/bufbuild/protocompile/internal/ext/slicesx" +) + +func TestEditions(t *testing.T) { + t.Parallel() + + assert.Equal(t, []syntax.Syntax{syntax.Edition2023}, slicesx.Collect(syntax.Editions())) + assert.Equal(t, + mapsx.KeySet(editions.SupportedEditions), + mapsx.CollectSet(iterx.Strings(syntax.Editions())), + ) +} diff --git a/experimental/ast/type_generic.go b/experimental/ast/type_generic.go index 961b6e3f..d02665f2 100644 --- a/experimental/ast/type_generic.go +++ b/experimental/ast/type_generic.go @@ -126,6 +126,12 @@ func (d TypeList) Brackets() token.Token { return d.raw.brackets.In(d.Context()) } +// SetBrackets sets the token tree for the brackets wrapping the argument list. +func (d TypeList) SetBrackets(brackets token.Token) { + d.Context().Nodes().panicIfNotOurs(brackets) + d.raw.brackets = brackets.ID() +} + // Len implements [seq.Indexer]. func (d TypeList) Len() int { if d.IsZero() { @@ -140,7 +146,7 @@ func (d TypeList) At(n int) TypeAny { return newTypeAny(d.Context(), d.raw.args[n].Value) } -// At implements [seq.Setter]. +// SetAt implements [seq.Setter]. func (d TypeList) SetAt(n int, ty TypeAny) { d.Context().Nodes().panicIfNotOurs(ty) d.raw.args[n].Value = ty.raw diff --git a/experimental/ast/zero_test.go b/experimental/ast/zero_test.go index a8e8f93c..682cbe23 100644 --- a/experimental/ast/zero_test.go +++ b/experimental/ast/zero_test.go @@ -88,7 +88,7 @@ func testZero[Node report.Spanner](t *testing.T) { ty := v.Type() for i := 0; i < ty.NumMethod(); i++ { m := ty.Method(i) - if m.Func.Type().NumIn() != 1 { + if m.Func.Type().NumIn() != 1 || m.Func.Type().NumOut() == 0 { continue // NumIn includes the receiver. } switch m.Name { diff --git a/experimental/internal/astx/encode.go b/experimental/internal/astx/encode.go index 2f7e77e4..90de3b75 100644 --- a/experimental/internal/astx/encode.go +++ b/experimental/internal/astx/encode.go @@ -326,7 +326,9 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { SemicolonSpan: c.span(decl.Semicolon()), } - if kind == compilerpb.Def_KIND_FIELD || kind == compilerpb.Def_KIND_UNSPECIFIED { + if kind == compilerpb.Def_KIND_FIELD || + kind == compilerpb.Def_KIND_GROUP || + kind == compilerpb.Def_KIND_UNSPECIFIED { proto.Type = c.type_(decl.Type()) } diff --git a/experimental/internal/taxa/classify.go b/experimental/internal/taxa/classify.go index f816461b..67f080d9 100644 --- a/experimental/internal/taxa/classify.go +++ b/experimental/internal/taxa/classify.go @@ -20,6 +20,7 @@ import ( "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" ) // IsFloat checks whether or not tok is intended to be a floating-point literal. @@ -41,12 +42,19 @@ func Classify(node report.Spanner) Noun { case ast.File: return TopLevel case ast.Path: - if id := node.AsIdent(); !id.IsZero() { - return classifyToken(id) + if first, ok := iterx.OnlyOne(node.Components); ok && first.Separator().IsZero() { + if id := first.AsIdent(); !id.IsZero() { + return classifyToken(id) + } + if !first.AsExtension().IsZero() { + return ExtensionName + } } + if node.Absolute() { return FullyQualifiedName } + return QualifiedName case ast.DeclAny: @@ -114,6 +122,8 @@ func Classify(node report.Spanner) Noun { return Classify(node.AsMethod()) case ast.DefKindOneof: return Classify(node.AsOneof()) + case ast.DefKindGroup: + return Classify(node.AsGroup()) default: return Def } @@ -137,12 +147,14 @@ func Classify(node report.Spanner) Noun { } else { return Option } - case ast.DefField, ast.DefGroup: + case ast.DefField: return Field + case ast.DefGroup: + return Group case ast.DefEnumValue: return EnumValue case ast.DefMethod: - return Service + return Method case ast.DefOneof: return Oneof @@ -197,6 +209,16 @@ func Classify(node report.Spanner) Noun { case ast.CompactOptions: return CompactOptions + + case ast.Signature: + switch { + case node.Inputs().IsZero() == node.Outputs().IsZero(): + return Signature + case !node.Inputs().IsZero(): + return MethodIns + default: + return MethodOuts + } } return Unknown @@ -278,6 +300,11 @@ func Keyword(text string) Noun { case "stream": return KeywordStream + case "map": + return PredeclaredMap + case "max": + return PredeclaredMax + default: return Ident } diff --git a/experimental/internal/taxa/noun.go b/experimental/internal/taxa/noun.go index 98dfbcc2..d33978e9 100644 --- a/experimental/internal/taxa/noun.go +++ b/experimental/internal/taxa/noun.go @@ -31,6 +31,8 @@ const ( Unrecognized TopLevel EOF + SyntaxMode + EditionMode Decl Empty Syntax @@ -48,6 +50,7 @@ const ( Service Extend Oneof + Group Option CustomOption Field @@ -56,11 +59,14 @@ const ( CompactOptions MethodIns MethodOuts + Signature FieldTag + FieldName OptionValue QualifiedName FullyQualifiedName ExtensionName + TypeURL Expr Range Array @@ -69,6 +75,9 @@ const ( Type TypePath TypeParams + TypePrefix + MapKey + MapValue Whitespace Comment Ident @@ -116,6 +125,8 @@ const ( KeywordRequired KeywordGroup KeywordStream + PredeclaredMap + PredeclaredMax total int = iota ) @@ -140,6 +151,8 @@ var _table_Noun_String = [...]string{ Unrecognized: "unrecognized token", TopLevel: "file scope", EOF: "end-of-file", + SyntaxMode: "syntax mode", + EditionMode: "editions mode", Decl: "declaration", Empty: "empty declaration", Syntax: "`syntax` declaration", @@ -157,6 +170,7 @@ var _table_Noun_String = [...]string{ Service: "service definition", Extend: "message extension block", Oneof: "oneof definition", + Group: "group definition", Option: "option setting", CustomOption: "custom option setting", Field: "message field", @@ -165,11 +179,14 @@ var _table_Noun_String = [...]string{ CompactOptions: "compact options", MethodIns: "method parameter list", MethodOuts: "method return type", + Signature: "method signature", FieldTag: "message field tag", + FieldName: "message field name", OptionValue: "option setting value", QualifiedName: "qualified name", FullyQualifiedName: "fully qualified name", ExtensionName: "extension name", + TypeURL: "`Any` type URL", Expr: "expression", Range: "range expression", Array: "array expression", @@ -178,6 +195,9 @@ var _table_Noun_String = [...]string{ Type: "type", TypePath: "type name", TypeParams: "type parameters", + TypePrefix: "type modifier", + MapKey: "map key type", + MapValue: "map value type", Whitespace: "whitespace", Comment: "comment", Ident: "identifier", @@ -225,6 +245,8 @@ var _table_Noun_String = [...]string{ KeywordRequired: "`required`", KeywordGroup: "`group`", KeywordStream: "`stream`", + PredeclaredMap: "`map`", + PredeclaredMax: "`max`", } var _table_Noun_GoString = [...]string{ @@ -232,6 +254,8 @@ var _table_Noun_GoString = [...]string{ Unrecognized: "Unrecognized", TopLevel: "TopLevel", EOF: "EOF", + SyntaxMode: "SyntaxMode", + EditionMode: "EditionMode", Decl: "Decl", Empty: "Empty", Syntax: "Syntax", @@ -249,6 +273,7 @@ var _table_Noun_GoString = [...]string{ Service: "Service", Extend: "Extend", Oneof: "Oneof", + Group: "Group", Option: "Option", CustomOption: "CustomOption", Field: "Field", @@ -257,11 +282,14 @@ var _table_Noun_GoString = [...]string{ CompactOptions: "CompactOptions", MethodIns: "MethodIns", MethodOuts: "MethodOuts", + Signature: "Signature", FieldTag: "FieldTag", + FieldName: "FieldName", OptionValue: "OptionValue", QualifiedName: "QualifiedName", FullyQualifiedName: "FullyQualifiedName", ExtensionName: "ExtensionName", + TypeURL: "TypeURL", Expr: "Expr", Range: "Range", Array: "Array", @@ -270,6 +298,9 @@ var _table_Noun_GoString = [...]string{ Type: "Type", TypePath: "TypePath", TypeParams: "TypeParams", + TypePrefix: "TypePrefix", + MapKey: "MapKey", + MapValue: "MapValue", Whitespace: "Whitespace", Comment: "Comment", Ident: "Ident", @@ -317,5 +348,7 @@ var _table_Noun_GoString = [...]string{ KeywordRequired: "KeywordRequired", KeywordGroup: "KeywordGroup", KeywordStream: "KeywordStream", + PredeclaredMap: "PredeclaredMap", + PredeclaredMax: "PredeclaredMax", } var _ iter.Seq[int] // Mark iter as used. diff --git a/experimental/internal/taxa/noun.yaml b/experimental/internal/taxa/noun.yaml index d9042290..cb189e7f 100644 --- a/experimental/internal/taxa/noun.yaml +++ b/experimental/internal/taxa/noun.yaml @@ -27,6 +27,9 @@ - {name: TopLevel, string: "file scope"} - {name: EOF, string: "end-of-file"} + - {name: SyntaxMode, string: "syntax mode"} + - {name: EditionMode, string: "editions mode"} + - {name: Decl, string: "declaration"} - {name: Empty, string: "empty declaration"} - {name: Syntax, string: "`syntax` declaration"} @@ -45,6 +48,7 @@ - {name: Service, string: "service definition"} - {name: Extend, string: "message extension block"} - {name: Oneof, string: "oneof definition"} + - {name: Group, string: "group definition"} - {name: Option, string: "option setting"} - {name: CustomOption, string: "custom option setting"} @@ -56,13 +60,16 @@ - {name: CompactOptions, string: "compact options"} - {name: MethodIns, string: "method parameter list"} - {name: MethodOuts, string: "method return type"} + - {name: Signature, string: "method signature"} - {name: FieldTag, string: "message field tag"} + - {name: FieldName, string: "message field name"} - {name: OptionValue, string: "option setting value"} - {name: QualifiedName, string: "qualified name"} - {name: FullyQualifiedName, string: "fully qualified name"} - {name: ExtensionName, string: "extension name"} + - {name: TypeURL, string: "`Any` type URL"} - {name: Expr, string: "expression"} - {name: Range, string: "range expression"} @@ -73,6 +80,10 @@ - {name: Type, string: "type"} - {name: TypePath, string: "type name"} - {name: TypeParams, string: "type parameters"} + - {name: TypePrefix, string: "type modifier"} + + - {name: MapKey, string: "map key type"} + - {name: MapValue, string: "map value type"} - {name: Whitespace, string: "whitespace"} - {name: Comment, string: "comment"} @@ -128,4 +139,7 @@ - {name: KeywordRepeated, string: "`repeated`"} - {name: KeywordRequired, string: "`required`"} - {name: KeywordGroup, string: "`group`"} - - {name: KeywordStream, string: "`stream`"} \ No newline at end of file + - {name: KeywordStream, string: "`stream`"} + + - {name: PredeclaredMap, string: "`map`"} + - {name: PredeclaredMax, string: "`max`"} \ No newline at end of file diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index 92e82ab3..edda1645 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -15,8 +15,10 @@ package parser import ( + "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/internal/ext/iterx" ) // errUnexpected is a low-level parser error for when we hit a token we don't @@ -85,3 +87,70 @@ func (e errMoreThanOne) Diagnose(d *report.Diagnostic) { report.Snippetf(e.first, "first one is here"), ) } + +// errHasOptions diagnoses the presence of compact options on a construct that +// does not permit them. +type errHasOptions struct { + what interface { + report.Spanner + Options() ast.CompactOptions + } +} + +func (e errHasOptions) Diagnose(d *report.Diagnostic) { + d.Apply( + report.Message("%s cannot specify %s", taxa.Classify(e.what), taxa.CompactOptions), + report.Snippetf(e.what.Options(), "help: remove this"), + ) +} + +// errHasSignature diagnoses the presence of a method signature on a non-method. +type errHasSignature struct { + what ast.DeclDef +} + +func (e errHasSignature) Diagnose(d *report.Diagnostic) { + d.Apply( + report.Message("%s appears to have %s", taxa.Classify(e.what), taxa.Signature), + report.Snippetf(e.what.Signature(), "help: remove this"), + ) +} + +// errBadNest diagnoses bad nesting: parent should not contain child. +type errBadNest struct { + parent classified + child report.Spanner + validParents taxa.Set +} + +func (e errBadNest) Diagnose(d *report.Diagnostic) { + what := taxa.Classify(e.child) + if e.parent.what == taxa.TopLevel { + d.Apply( + report.Message("unexpected %s at %s", what, e.parent.what), + report.Snippetf(e.child, "this %s cannot be declared here", what), + ) + } else { + d.Apply( + report.Message("unexpected %s within %s", what, e.parent.what), + report.Snippetf(e.child, "this %s...", what), + report.Snippetf(e.parent, "...cannot be declared within this %s", e.parent.what), + ) + } + + if e.validParents.Len() == 1 { + v, _ := iterx.First(e.validParents.All()) + if v == taxa.TopLevel { + // This case is just to avoid printing "within a top-level scope", + // which looks wrong. + d.Apply(report.Helpf("a %s can only appear at %s", what, v)) + } else { + d.Apply(report.Helpf("a %s can only appear within a %s", what, v)) + } + } else { + d.Apply(report.Helpf( + "a %s can only appear within one of %s", + what, e.validParents.Join("or"), + )) + } +} diff --git a/experimental/parser/diagnostics_string.go b/experimental/parser/diagnostics_string.go index 33db3614..cd040332 100644 --- a/experimental/parser/diagnostics_string.go +++ b/experimental/parser/diagnostics_string.go @@ -15,10 +15,12 @@ package parser import ( + "fmt" "strconv" "strings" "unicode/utf8" + "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" ) @@ -41,10 +43,15 @@ func (e errUnclosedString) Diagnose(d *report.Diagnostic) { if len(quoted) == 1 { d.Apply(report.Notef("this string consists of a single orphaned quote")) } else if strings.HasSuffix(quoted, quote) { - d.Apply(report.Notef("this string appears to end in an escaped quote; replace `\\%s` with `\\\\%[1]s%[1]s`", quote)) + d.Apply(report.SuggestEdits( + e.Token, + "this string appears to end in an escaped quote", + report.Edit{ + Start: e.Token.Span().Len() - 2, End: e.Token.Span().Len(), + Replace: fmt.Sprintf(`\\%v%v`, quote, quote), + }, + )) } - - // TODO: check to see if a " or ' escape exists in the string? } // errInvalidEscape diagnoses an invalid escape sequence within a string @@ -91,3 +98,33 @@ func (e errInvalidEscape) Diagnose(d *report.Diagnostic) { d.Apply(report.Snippet(e.Span)) } + +// errImpureString diagnoses a string literal that probably should not contain +// escapes or concatenation. +type errImpureString struct { + lit token.Token + where taxa.Place +} + +// Diagnose implements [report.Diagnose]. +func (e errImpureString) Diagnose(d *report.Diagnostic) { + text, _ := e.lit.AsString() + quote := e.lit.Text()[0] + d.Apply( + report.Message("non-canonical string literal %s", e.where.String()), + report.Snippet(e.lit), + report.SuggestEdits(e.lit, "replace it with a canonical string", report.Edit{ + Start: 0, End: e.lit.Span().Len(), + Replace: fmt.Sprintf("%c%v%c", quote, text, quote), + }), + ) + + if !e.lit.IsLeaf() { + d.Apply( + report.Notef( + "Protobuf implicitly concatenates adjacent %ss, like C or Python; this can lead to surprising behavior", + taxa.String, + ), + ) + } +} diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go new file mode 100644 index 00000000..8adad3e2 --- /dev/null +++ b/experimental/parser/legalize_decl.go @@ -0,0 +1,334 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "fmt" + "strings" + "unicode" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ast/predeclared" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/seq" + "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/ext/slicesx" + "github.com/bufbuild/protocompile/internal/ext/stringsx" +) + +// legalizeDecl legalizes a declaration. +// +// The parent definition is used for determining if a declaration nesting is +// permitted. +func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { + switch decl.Kind() { + case ast.DeclKindSyntax: + legalizeSyntax(p, parent, -1, nil, decl.AsSyntax()) + case ast.DeclKindPackage: + legalizePackage(p, parent, -1, nil, decl.AsPackage()) + case ast.DeclKindImport: + legalizeImport(p, parent, decl.AsImport(), nil) + + case ast.DeclKindRange: + legalizeRange(p, parent, decl.AsRange()) + + case ast.DeclKindBody: + body := decl.AsBody() + braces := body.Braces().Span() + p.Errorf("unexpected definition body in %v", parent.what).Apply( + report.Snippet(decl), + report.SuggestEdits( + braces, + "remove these braces", + report.Edit{Start: 0, End: 1}, + report.Edit{Start: braces.Len() - 1, End: braces.Len()}, + ), + ) + + seq.Values(body.Decls())(func(decl ast.DeclAny) bool { + // Treat bodies as being immediately inlined, hence we pass + // parent here and not body as the parent. + legalizeDecl(p, parent, decl) + return true + }) + + case ast.DeclKindDef: + def := decl.AsDef() + body := def.Body() + // legalizeDef also calls Classify(def). + // TODO: try to pass around a classified when possible. Generalize + // classified toe a generic type? + what := classified{def, taxa.Classify(def)} + + legalizeDef(p, parent, def) + seq.Values(body.Decls())(func(decl ast.DeclAny) bool { + legalizeDecl(p, what, decl) + return true + }) + } +} + +// legalizeDecl legalizes an extension or reserved range. +func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { + in := taxa.Extensions + validParents := taxa.Message.AsSet() + if decl.IsReserved() { + in = taxa.Reserved + validParents = validParents.With(taxa.Enum) + } + + if !validParents.Has(parent.what) { + p.Error(errBadNest{parent: parent, child: decl, validParents: validParents}) + return + } + + if options := decl.Options(); !options.IsZero() { + if in == taxa.Reserved { + p.Error(errHasOptions{decl}) + } else { + legalizeCompactOptions(p, options) + } + } + + want := taxa.NewSet(taxa.Int, taxa.Range) + if in == taxa.Reserved { + if p.Mode() == taxa.EditionMode { + want = want.With(taxa.Ident) + } else { + want = want.With(taxa.String) + } + } + + legalizeNumber := func(in taxa.Noun, expr ast.ExprAny, allowMax bool) { + switch expr.Kind() { + case ast.ExprKindPath: + if allowMax && expr.AsPath().AsPredeclared() == predeclared.Max { + return + } + + case ast.ExprKindLiteral: + lit := expr.AsLiteral() + if lit.Kind() == token.Number && !strings.Contains(lit.Text(), ".") { + return + } + case ast.ExprKindPrefixed: + expr := expr.AsPrefixed() + //nolint:gocritic // Intentional single-case switch. + switch expr.Prefix() { + case ast.ExprPrefixMinus: + lit := expr.Expr().AsLiteral() + if lit.Kind() != token.Number || strings.Contains(lit.Text(), ".") { + p.Error(errUnexpected{ + what: expr.Expr(), + where: taxa.Minus.After(), + want: taxa.Int.AsSet(), + }) + } + return + } + } + + want := taxa.Int.AsSet() + if allowMax { + want = want.With(taxa.PredeclaredMax) + } + + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: want, + }) + } + + var names, tags []ast.ExprAny + seq.Values(decl.Ranges())(func(expr ast.ExprAny) bool { + switch expr.Kind() { + case ast.ExprKindPath: + path := expr.AsPath() + if path.AsIdent().IsZero() || in == taxa.Extensions { + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: want, + }) + break + } + + names = append(names, expr) + + m := p.Mode() + if m == taxa.EditionMode { + break + } + p.Errorf("cannot use %vs in %v in %v", taxa.Ident, in, m).Apply( + report.Snippet(expr), + report.Snippetf(p.syntax, "%v is specified here", m), + report.SuggestEdits( + expr, + fmt.Sprintf("quote it to make it into a %v", taxa.String), + report.Edit{ + Start: 0, End: 0, Replace: `"`, + }, + report.Edit{ + Start: expr.Span().Len(), End: expr.Span().Len(), + Replace: `"`, + }, + ), + ) + + case ast.ExprKindLiteral: + lit := expr.AsLiteral() + if name, ok := lit.AsString(); ok { + if in == taxa.Extensions { + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: want, + }) + break + } + + names = append(names, expr) + if m := p.Mode(); m == taxa.EditionMode { + err := p.Errorf("cannot use %vs in %v in %v", taxa.String, in, m).Apply( + report.Snippet(expr), + report.Snippetf(p.syntax, "%v is specified here", m), + ) + + // Only suggest unquoting if it's already an identifier. + if isASCIIIdent(name) { + err.Apply(report.SuggestEdits( + lit, "replace this with an identifier", + report.Edit{ + Start: 0, End: lit.Span().Len(), + Replace: name, + }, + )) + } + + break + } + + if !isASCIIIdent(name) { + field := taxa.Field + if parent.what == taxa.Enum { + field = taxa.EnumValue + } + p.Errorf("reserved %v name is not a valid identifier", field).Apply( + report.Snippet(expr), + ) + break + } + + if !lit.IsPureString() { + p.Warn(errImpureString{lit.Token, in.In()}) + } + + break + } + + fallthrough + + case ast.ExprKindPrefixed: + legalizeNumber(in, expr, false) + tags = append(tags, expr) + + case ast.ExprKindRange: + lo, hi := expr.AsRange().Bounds() + legalizeNumber(in, lo, false) + legalizeNumber(in, hi, true) + tags = append(tags, expr) + + default: + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: want, + }) + } + + return true + }) + + if len(names) > 0 && len(tags) > 0 { + parentWhat := "field" + if parent.what == taxa.Enum { + parentWhat = "value" + } + + // We want to diagnose whichever element is least common in the range. + least := names + most := tags + leastWhat := "name" + mostWhat := "tag" + if len(names) > len(tags) || + // When tied, use whichever comes last lexicographically. + (len(names) == len(tags) && names[0].Span().Start < tags[0].Span().Start) { + least, most = most, least + leastWhat, mostWhat = mostWhat, leastWhat + } + + err := p.Errorf("cannot mix tags and names in %s", taxa.Reserved).Apply( + report.Snippetf(least[0], "this %s %s must go in its own %s", parentWhat, leastWhat, taxa.Reserved), + report.Snippetf(most[0], "but expected a %s %s because of this", parentWhat, mostWhat), + ) + + span := decl.Span() + var edits []report.Edit + for _, expr := range least { + // Delete leading whitespace and trailing whitespace (and a comma, too). + toDelete := expr.Span().GrowLeft(unicode.IsSpace).GrowRight(unicode.IsSpace) + if r, _ := stringsx.Rune(toDelete.After(), 0); r == ',' { + toDelete.End++ + } + + edits = append(edits, report.Edit{ + Start: toDelete.Start - span.Start, + End: toDelete.End - span.Start, + }) + } + + // If we're moving the last element out of the range, we need to obliterate + // the trailing comma. + comma := slicesx.LastPointer(most).Span() + if comma.End < slicesx.LastPointer(least).Span().End { + comma.Start = comma.End + comma = comma.GrowRight(unicode.IsSpace) + if r, _ := stringsx.Rune(comma.After(), 0); r == ',' { + comma.End++ + edits = append(edits, report.Edit{ + Start: comma.Start - span.Start, + End: comma.End - span.Start, + }) + } + } + + edits = append(edits, report.Edit{ + Start: span.Len(), End: span.Len(), + Replace: fmt.Sprintf("\n%sreserved %s;", span.Indentation(), iterx.Join( + iterx.Map(slicesx.Values(least), func(e ast.ExprAny) string { return e.Span().Text() }), + ", ", + )), + }) + + err.Apply(report.SuggestEdits( + span, + fmt.Sprintf("split the %s", taxa.Reserved), + edits..., + )) + } +} diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go new file mode 100644 index 00000000..b2768f12 --- /dev/null +++ b/experimental/parser/legalize_def.go @@ -0,0 +1,295 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" +) + +// Map of a def kind to the valid parents it can have. +// +// We use taxa.Set here because it already exists and is pretty cheap. +var validDefParents = [...]taxa.Set{ + ast.DefKindMessage: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group), + ast.DefKindEnum: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group), + ast.DefKindService: taxa.NewSet(taxa.TopLevel), + ast.DefKindExtend: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group), + ast.DefKindField: taxa.NewSet(taxa.Message, taxa.Group, taxa.Extend, taxa.Oneof), + ast.DefKindOneof: taxa.NewSet(taxa.Message, taxa.Group), + ast.DefKindGroup: taxa.NewSet(taxa.Message, taxa.Group, taxa.Extend), + ast.DefKindEnumValue: taxa.NewSet(taxa.Enum), + ast.DefKindMethod: taxa.NewSet(taxa.Service), + ast.DefKindOption: taxa.NewSet( + taxa.TopLevel, taxa.Message, taxa.Enum, taxa.Service, + taxa.Oneof, taxa.Group, taxa.Method, + ), +} + +// legalizeDef legalizes a definition. +// +// It will mark the definition as corrupt if it encounters any particularly +// egregious problems. +func legalizeDef(p *parser, parent classified, def ast.DeclDef) { + kind := def.Classify() + if !validDefParents[kind].Has(parent.what) { + p.Error(errBadNest{parent: parent, child: def, validParents: validDefParents[kind]}) + } + + switch kind { + case ast.DefKindMessage, ast.DefKindEnum, ast.DefKindService, ast.DefKindOneof, ast.DefKindExtend: + legalizeTypeDefLike(p, taxa.Classify(def), def) + case ast.DefKindField, ast.DefKindEnumValue, ast.DefKindGroup: + legalizeFieldLike(p, taxa.Classify(def), def) + case ast.DefKindOption: + legalizeOption(p, def) + case ast.DefKindMethod: + legalizeMethod(p, def) + } +} + +// legalizeTypeDefLike legalizes something that resembles a type definition: +// namely, messages, enums, oneofs, services, and extension blocks. +func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { + switch { + case def.Name().IsZero(): + def.MarkCorrupt() + kw := taxa.Keyword(def.Keyword().Text()) + p.Errorf("missing name %v", kw.After()).Apply( + report.Snippet(def), + ) + + case what == taxa.Extend: + legalizePath(p, what.In(), def.Name(), pathOptions{AllowAbsolute: true}) + + case def.Name().AsIdent().IsZero(): + def.MarkCorrupt() + kw := taxa.Keyword(def.Keyword().Text()) + + err := errUnexpected{ + what: def.Name(), + where: kw.After(), + want: taxa.Ident.AsSet(), + } + // Look for a separator, and use that instead. We can't "just" pick out + // the first separator, because def.Name might be a one-component + // extension path, e.g. (a.b.c). + def.Name().Components(func(pc ast.PathComponent) bool { + if pc.Separator().IsZero() { + return true + } + + err = errUnexpected{ + what: pc.Separator(), + where: taxa.Ident.In(), + want: taxa.Ident.AsSet(), + } + return false + }) + + p.Error(err).Apply( + report.Notef("the name of a %s must be a single identifier", what), + // TODO: Include a help that says to stick this into a file with + // the right package. + ) + } + + hasValue := !def.Equals().IsZero() || !def.Value().IsZero() + if hasValue { + p.Error(errUnexpected{ + what: report.Join(def.Equals(), def.Value()), + where: what.In(), + got: taxa.Classify(def.Value()), + }) + } + + if sig := def.Signature(); !sig.IsZero() { + p.Error(errHasSignature{def}) + } + + if def.Body().IsZero() { + // NOTE: There is currently no way to trip this diagnostic, because + // a message with no body is interpreted as a field. + p.Errorf("missing body for %v", what).Apply( + report.Snippet(def), + ) + } + + if options := def.Options(); !options.IsZero() { + p.Error(errHasOptions{def}) + } +} + +// legalizeFieldLike legalizes something that resembles a field definition: +// namely, fields, groups, and enum values. +func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef) { + if def.Name().IsZero() { + def.MarkCorrupt() + p.Errorf("missing name %v", what.In()).Apply( + report.Snippet(def), + ) + } else if def.Name().AsIdent().IsZero() { + def.MarkCorrupt() + p.Error(errUnexpected{ + what: def.Name(), + where: what.In(), + want: taxa.Ident.AsSet(), + }) + } + if def.Value().IsZero() { + what := taxa.FieldTag + if def.Classify() == ast.DefKindEnumValue { + what = taxa.EnumValue + } + p.Errorf("missing %v in declaration", what).Apply( + report.Snippet(def), + // TODO: We do not currently provide a suggested field number for + // cases where that is permitted, such as for non-extension-fields. + // + // However, that cannot happen until after IR lowering. Once that's + // implemented, we must come back here and set it up so that this + // diagnostic can be overridden by a later one, probably using + // diagnostic tags. + ) + } + + if sig := def.Signature(); !sig.IsZero() { + p.Error(errHasSignature{def}) + } + + switch what { + case taxa.Group: + if def.Body().IsZero() { + p.Errorf("missing body for %v", what).Apply( + report.Snippet(def), + ) + } + case taxa.Field, taxa.EnumValue: + if body := def.Body(); !body.IsZero() { + p.Error(errUnexpected{ + what: body, + where: what.In(), + }) + } + } + + if options := def.Options(); !options.IsZero() { + legalizeCompactOptions(p, options) + } + + if what == taxa.Field { + legalizeFieldType(p, def.Type()) + } +} + +// legalizeOption legalizes an option definition (see legalize_option.go). +func legalizeOption(p *parser, def ast.DeclDef) { + if sig := def.Signature(); !sig.IsZero() { + p.Error(errHasSignature{def}) + } + + if body := def.Body(); !body.IsZero() { + p.Error(errUnexpected{ + what: body, + where: taxa.Option.In(), + }) + } + + if options := def.Options(); !options.IsZero() { + p.Error(errHasOptions{def}) + } + + legalizeOptionEntry(p, def.AsOption().Option, def.Span()) +} + +// legalizeMethod legalizes a service method. +func legalizeMethod(p *parser, def ast.DeclDef) { + if def.Name().IsZero() { + def.MarkCorrupt() + p.Errorf("missing name %v", taxa.Method.In()).Apply( + report.Snippet(def), + ) + } else if def.Name().AsIdent().IsZero() { + def.MarkCorrupt() + p.Error(errUnexpected{ + what: def.Name(), + where: taxa.Method.In(), + want: taxa.Ident.AsSet(), + }) + } + + hasValue := !def.Equals().IsZero() || !def.Value().IsZero() + if hasValue { + p.Error(errUnexpected{ + what: report.Join(def.Equals(), def.Value()), + where: taxa.Method.In(), + got: taxa.Classify(def.Value()), + }) + } + + sig := def.Signature() + if sig.IsZero() { + def.MarkCorrupt() + p.Errorf("missing %v in %v", taxa.Signature, taxa.Method).Apply( + report.Snippet(def), + ) + } else { + // There are cases where part of the signature is present, but the + // span for one or the other half is zero because there were no brackets + // or type. + if sig.Inputs().Span().IsZero() { + def.MarkCorrupt() + p.Errorf("missing %v in %v", taxa.MethodIns, taxa.Method).Apply( + report.Snippetf(def.Name(), "expected type in %s after this", taxa.Parens), + ) + } else { + legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns) + } + + if sig.Outputs().Span().IsZero() { + def.MarkCorrupt() + var after report.Spanner + switch { + case !sig.Returns().IsZero(): + after = sig.Returns() + case !sig.Inputs().IsZero(): + after = sig.Inputs() + default: + after = def.Name() + } + + p.Errorf("missing %v in %v", taxa.MethodOuts, taxa.Method).Apply( + report.Snippetf(after, "expected type in %s after this", taxa.Parens), + ) + } else { + legalizeMethodParams(p, sig.Outputs(), taxa.MethodOuts) + } + } + + // Methods are unique in that they can end in either a ; or a {}. + // The parser already checks for defs to end with either one of these, + // so we don't need to do anything here. + + if options := def.Options(); !options.IsZero() { + p.Error(errHasOptions{def}).Apply( + report.Notef( + "service method options are applied using %v; declarations in the %v following the method definition", + taxa.KeywordOption, taxa.Braces, + ), + // TODO: Generate a suggestion for this. + ) + } +} diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go new file mode 100644 index 00000000..bc9934d7 --- /dev/null +++ b/experimental/parser/legalize_file.go @@ -0,0 +1,347 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "fmt" + "regexp" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ast/syntax" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/seq" + "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" +) + +// isOrdinaryFilePath matches a "normal looking" file path, for the purposes +// of emitting warnings. +var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") + +// legalizeFile is the entry-point for legalizing a parsed Protobuf file. +func legalizeFile(p *parser, file ast.File) { + var ( + pkg ast.DeclPackage + imports = make(map[string][]ast.DeclImport) + ) + seq.All(file.Decls())(func(i int, decl ast.DeclAny) bool { + file := classified{file, taxa.TopLevel} + switch decl.Kind() { + case ast.DeclKindSyntax: + legalizeSyntax(p, file, i, &p.syntax, decl.AsSyntax()) + case ast.DeclKindPackage: + legalizePackage(p, file, i, &pkg, decl.AsPackage()) + case ast.DeclKindImport: + legalizeImport(p, file, decl.AsImport(), imports) + default: + legalizeDecl(p, file, decl) + } + + return true + }) + + if pkg.IsZero() { + p.Warnf("missing %s", taxa.Package).Apply( + report.InFile(p.Stream().Path()), + report.Notef( + "not explicitly specifying a package places the file in the "+ + "unnamed package; using it strongly is discouraged"), + ) + } +} + +// legalizeSyntax legalizes a DeclSyntax. +// +// idx is the index of this declaration within its parent; first is a pointer to +// a slot where we can store the first DeclSyntax seen, so we can legalize +// against duplicates. +func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax, decl ast.DeclSyntax) { + in := taxa.Syntax + if decl.IsEdition() { + in = taxa.Edition + } + + if parent.what != taxa.TopLevel || first == nil { + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) + return + } + + file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. + switch { + case !first.IsZero(): + p.Errorf("unexpected %s", in).Apply( + report.Snippet(decl), + report.Snippetf(*first, "previous declaration is here"), + report.SuggestEdits( + decl, + "remove this", + report.Edit{Start: 0, End: decl.Span().Len()}, + ), + report.Notef("a file may contain at most one `syntax` or `edition` declaration"), + ) + return + case idx > 0: + p.Errorf("unexpected %s", in).Apply( + report.Snippet(decl), + report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"), + // TODO: Add a suggestion to move this up. + report.Notef("a %s must be the first declaration in a file", in), + ) + *first = decl + return + default: + *first = decl + } + + if !decl.Options().IsZero() { + p.Error(errHasOptions{decl}) + } + + expr := decl.Value() + var name string + switch expr.Kind() { + case ast.ExprKindLiteral: + if text, ok := expr.AsLiteral().AsString(); ok { + name = text + break + } + + fallthrough + case ast.ExprKindPath: + name = expr.Span().Text() + + case ast.ExprKindInvalid: + return + default: + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: taxa.String.AsSet(), + }) + return + } + + permitted := func() report.DiagnosticOption { + values := iterx.Join(iterx.FilterMap(syntax.All(), func(s syntax.Syntax) (string, bool) { + if s.IsEdition() != (in == taxa.Edition) { + return "", false + } + + return fmt.Sprintf("%q", s), true + }), ", ") + + return report.Notef("permitted values: %s", values) + } + + value := syntax.Lookup(name) + lit := expr.AsLiteral() + switch { + case value == syntax.Unknown: + p.Errorf("unrecognized %s value", in).Apply( + report.Snippet(expr), + permitted(), + ) + case value.IsEdition() && in == taxa.Syntax: + p.Errorf("unexpected edition in %s", in).Apply( + report.Snippet(expr), + permitted(), + ) + case !value.IsEdition() && in == taxa.Edition: + p.Errorf("unexpected syntax in %s", in).Apply( + report.Snippet(expr), + permitted(), + ) + + case lit.Kind() != token.String: + span := expr.Span() + p.Errorf("the value of a %s must be a string literal", in).Apply( + report.Snippet(span), + report.SuggestEdits( + span, + "add quotes to make this a string literal", + report.Edit{Start: 0, End: 0, Replace: `"`}, + report.Edit{Start: span.Len(), End: span.Len(), Replace: `"`}, + ), + ) + + case !lit.IsZero() && !lit.IsPureString(): + p.Warn(errImpureString{lit.Token, in.In()}) + } +} + +// legalizePackage legalizes a DeclPackage. +// +// idx is the index of this declaration within its parent; first is a pointer to +// a slot where we can store the first DeclPackage seen, so we can legalize +// against duplicates. +func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPackage, decl ast.DeclPackage) { + if parent.what != taxa.TopLevel || first == nil { + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) + return + } + + file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. + switch { + case !first.IsZero(): + p.Errorf("unexpected %s", taxa.Package).Apply( + report.Snippet(decl), + report.Snippetf(*first, "previous declaration is here"), + report.SuggestEdits( + decl, + "remove this", + report.Edit{Start: 0, End: decl.Span().Len()}, + ), + report.Notef("a file must contain exactly one %s", taxa.Package), + ) + return + case idx > 0: + if idx > 1 || file.Decls().At(0).Kind() != ast.DeclKindSyntax { + p.Warnf("the %s should be placed at the top of the file", taxa.Package).Apply( + report.Snippet(decl), + report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"), + // TODO: Add a suggestion to move this up. + report.Helpf( + "a file's %s should immediately follow the `syntax` or `edition` declaration", + taxa.Package, + ), + ) + return + } + fallthrough + default: + *first = decl + } + + if !decl.Options().IsZero() { + p.Error(errHasOptions{decl}) + } + + if decl.Path().IsZero() { + p.Errorf("missing path in %s", taxa.Package).Apply( + report.Snippet(decl), + report.Helpf( + "to place a file in the unnamed package, omit the %s; however, "+ + "using the unnamed package is discouraged", + taxa.Package, + ), + ) + } + + legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{ + MaxBytes: 512, + MaxComponents: 101, + }) +} + +// legalizeImport legalizes a DeclImport. +// +// imports is a map that classifies DeclImports by the contents of their import string. +// This populates it and uses it to detect duplicates. +func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports map[string][]ast.DeclImport) { + in := taxa.Import + if decl.IsPublic() { + in = taxa.PublicImport + } else if decl.IsWeak() { + in = taxa.WeakImport + } + + if parent.what != taxa.TopLevel { + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) + return + } + + if !decl.Options().IsZero() { + p.Error(errHasOptions{decl}) + } + + expr := decl.ImportPath() + switch expr.Kind() { + case ast.ExprKindLiteral: + lit := expr.AsLiteral() + if file, ok := lit.AsString(); ok { + if imports != nil { + prev := imports[file] + imports[file] = append(prev, decl) + if len(prev) == 1 { // Do not bother diagnosing this more than once. + p.Errorf("file %q imported multiple times", file).Apply( + report.Snippet(decl), + report.Snippetf(prev[0], "first imported here"), + ) + } + if prev != nil { + return + } + } + + if !expr.AsLiteral().IsPureString() { + // Only warn for cases where the import is alphanumeric. + if isOrdinaryFilePath.MatchString(file) { + p.Warn(errImpureString{lit.Token, in.In()}) + } + } + break + } + + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: taxa.String.AsSet(), + }) + return + + case ast.ExprKindPath: + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: taxa.String.AsSet(), + }).Apply( + // TODO: potentially defer this diagnostic to later, when we can + // perform symbol lookup and figure out what the correct file to + // import is. + report.Notef("Protobuf does not support importing symbols by name, instead, " + + "try importing a file, e.g. `import \"google/protobuf/descriptor.proto\";`"), + ) + return + + case ast.ExprKindInvalid: + if decl.Semicolon().IsZero() { + // If there is a missing semicolon, this is some other kind of syntax error + // so we should avoid diagnosing it twice. + return + } + + p.Errorf("missing import path in %s", in).Apply( + report.Snippet(decl), + ) + return + + default: + p.Error(errUnexpected{ + what: expr, + where: in.In(), + want: taxa.String.AsSet(), + }) + return + } + + if in == taxa.WeakImport { + p.Warnf("use of `import weak`").Apply( + report.Snippet(report.Join(decl.Keyword(), decl.Modifier())), + report.Notef("`import weak` is deprecated and not supported correctly "+ + "in most Protobuf implementations"), + ) + } +} diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go new file mode 100644 index 00000000..252c841b --- /dev/null +++ b/experimental/parser/legalize_option.go @@ -0,0 +1,332 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "fmt" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ast/predeclared" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/seq" + "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/ext/slicesx" +) + +// legalizeCompactOptions legalizes a [...] of options. +// +// All this really does is check that opt is non-empty and then forwards each +// entry to [legalizeOptionEntry]. +func legalizeCompactOptions(p *parser, opts ast.CompactOptions) { + entries := opts.Entries() + if entries.Len() == 0 { + p.Errorf("%s cannot be empty", taxa.CompactOptions).Apply( + report.Snippetf(opts, "help: remove this"), + ) + return + } + + seq.Values(entries)(func(opt ast.Option) bool { + legalizeOptionEntry(p, opt, opt.Span()) + return true + }) +} + +// legalizeCompactOptions is the common path for legalizing options, either +// from an option def or from compact options. +// +// We can't perform type-checking yet, so all we can really do here +// is check that the path is ok for an option. Legalizing the value cannot +// happen until type-checking in IR construction. +func legalizeOptionEntry(p *parser, opt ast.Option, decl report.Span) { + if opt.Path.IsZero() { + p.Errorf("missing %v path", taxa.Option).Apply( + report.Snippet(decl), + ) + + // Don't bother legalizing if the value is zero. That can only happen + // when the user writes just option;, which will produce two very + // similar diagnostics. + return + } + + legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{ + AllowExts: true, + }) + + if opt.Value.IsZero() { + p.Errorf("missing %v", taxa.OptionValue).Apply( + report.Snippet(decl), + ) + } else { + legalizeOptionValue(p, decl, ast.ExprAny{}, opt.Value) + } +} + +// legalizeOptionValue conservatively legalizes an option value. +func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value ast.ExprAny) { + // TODO: Some diagnostics emitted by this function must be suppressed by type + // checking, which generates more precise diagnostics. + + if slicesx.Among(value.Kind(), ast.ExprKindInvalid, ast.ExprKindError) { + // Diagnosed elsewhere. + return + } + + switch value.Kind() { + case ast.ExprKindLiteral: + // All literals are allowed. + case ast.ExprKindPath: + if value.AsPath().AsIdent().IsZero() { + p.Error(errUnexpected{ + what: value, + where: taxa.OptionValue.In(), + want: taxa.Ident.AsSet(), + }) + } + case ast.ExprKindPrefixed: + value := value.AsPrefixed() + if value.Expr().IsZero() { + return + } + + //nolint:gocritic // Intentional single-case switch. + switch value.Prefix() { + case ast.ExprPrefixMinus: + ok := value.Expr().AsLiteral().Kind() == token.Number + if path := value.Expr().AsPath(); !path.IsZero() { + // A minus sign may precede inf or nan, but it may also precede + // any identifier when inside of a message literal. + ok = (parent.Kind() == ast.ExprKindField && !path.AsIdent().IsZero()) || + slicesx.Among(path.AsPredeclared(), predeclared.Inf, predeclared.NAN) + } + + if !ok { + p.Error(errUnexpected{ + what: value.Expr(), + where: taxa.Minus.After(), + want: taxa.NewSet(taxa.Int, taxa.Float), + }) + } + } + case ast.ExprKindArray: + array := value.AsArray().Elements() + switch { + case parent.IsZero(): + err := p.Error(errUnexpected{ + what: value, + where: taxa.OptionValue.In(), + }).Apply( + report.Notef("%ss can only appear inside of %ss", taxa.Array, taxa.Dict), + ) + + switch array.Len() { + case 0: + err.Apply(report.SuggestEdits( + decl, + fmt.Sprintf("delete this option; an empty %s has no effect", taxa.Array), + report.Edit{Start: 0, End: decl.Len()}, + )) + case 1: + elem := array.At(0) + if !slicesx.Among(elem.Kind(), + // This check avoids making nonsensical suggestions. + ast.ExprKindInvalid, ast.ExprKindError, + ast.ExprKindRange, ast.ExprKindField) { + err.Apply(report.SuggestEdits( + value, + "delete the brackets; this is equivalent for repeated fields", + report.Edit{Start: 0, End: 1}, + report.Edit{Start: value.Span().Len() - 1, End: value.Span().Len()}, + )) + break + } + fallthrough + default: + err.Apply(report.Helpf("break this %s into one per element", taxa.Option)) + } + + case parent.Kind() == ast.ExprKindArray: + p.Errorf("nested %ss are not allowed", taxa.Array).Apply( + report.Snippetf(value, "cannot nest this %s...", taxa.Array), + report.Snippetf(parent, "...within this %s", taxa.Array), + ) + + default: + seq.Values(array)(func(e ast.ExprAny) bool { + legalizeOptionValue(p, decl, value, e) + return true + }) + + if parent.Kind() == ast.ExprKindField && array.Len() == 0 { + p.Warnf("empty %s has no effect", taxa.Array).Apply( + report.Snippet(value), + report.SuggestEdits( + parent, + fmt.Sprintf("delete this %s", taxa.DictField), + report.Edit{Start: 0, End: parent.Span().Len()}, + ), + report.Notef(`repeated fields do not distinguish "empty" and "missing" states`), + ) + } + } + case ast.ExprKindDict: + dict := value.AsDict() + + // Legalize against <...> in all cases, but only emit a warning when they + // are not strictly illegal. + if dict.Braces().Text() == "<" { + var err *report.Diagnostic + if parent.IsZero() { + err = p.Errorf("cannot use %s for %s here", taxa.Angles, taxa.Dict) + } else { + err = p.Warnf("using %s for %s is not recommended", taxa.Angles, taxa.Dict) + } + + err.Apply( + report.Snippet(value), + report.SuggestEdits( + dict, + fmt.Sprintf("use %s instead", taxa.Braces), + report.Edit{Start: 0, End: 1, Replace: "{"}, + report.Edit{Start: dict.Span().Len() - 1, End: dict.Span().Len(), Replace: "}"}, + ), + report.Notef("%s are only permitted for sub-messages within a %s, but as top-level option values", taxa.Angles, taxa.Dict), + report.Helpf("%s %ss are an obscure feature and not recommended", taxa.Angles, taxa.Dict), + ) + } + + seq.Values(value.AsDict().Elements())(func(kv ast.ExprField) bool { + want := taxa.NewSet(taxa.FieldName, taxa.ExtensionName, taxa.TypeURL) + switch kv.Key().Kind() { + case ast.ExprKindLiteral: + lit := kv.Key().AsLiteral() + err := p.Error(errUnexpected{ + what: lit, + where: taxa.DictField.In(), + want: want, + }) + + if name, _ := lit.AsString(); isASCIIIdent(name) { + err.Apply(report.SuggestEdits( + lit, + "remove the quotes", + report.Edit{ + Start: 0, End: lit.Span().Len(), + Replace: name, + }, + )) + } + + case ast.ExprKindPath: + path := kv.Key().AsPath() + first, ok := iterx.OnlyOne(path.Components) + if !ok || !first.Separator().IsZero() { + p.Error(errUnexpected{ + what: path, + where: taxa.DictField.In(), + want: want, + }) + break + } + if !first.AsExtension().IsZero() { + p.Errorf("cannot name extension field using %s in %s", taxa.Parens, taxa.Dict).Apply( + report.Snippetf(path, "expected this to be wrapped in %s instead", taxa.Brackets), + report.SuggestEdits( + path, + fmt.Sprintf("replace the %s with %s", taxa.Parens, taxa.Brackets), + report.Edit{Start: 0, End: 1, Replace: "["}, + report.Edit{Start: path.Span().Len() - 1, End: path.Span().Len(), Replace: "]"}, + ), + ) + } + + case ast.ExprKindArray: + elem, ok := iterx.OnlyOne(seq.Values(kv.Key().AsArray().Elements())) + path := elem.AsPath().Path + if !ok || path.IsZero() { + p.Error(errUnexpected{ + what: kv.Key(), + where: taxa.DictField.In(), + want: want, + }) + break + } + + _, isURL := iterx.Find(path.Components, func(pc ast.PathComponent) bool { + return pc.Separator().Text() == "/" + }) + if isURL { + legalizePath(p, taxa.TypeURL.In(), path, pathOptions{AllowSlash: true}) + } else { + legalizePath(p, taxa.ExtensionName.In(), path, pathOptions{ + // Surprisingly, this extension path cannot be an absolute + // path! + AllowAbsolute: false, + }) + } + default: + if !kv.Key().IsZero() { + p.Error(errUnexpected{ + what: kv.Key(), + where: taxa.DictField.In(), + want: want, + }) + } + } + + if kv.Colon().IsZero() && kv.Value().Kind() == ast.ExprKindArray { + // When the user writes {a [ ... ]}, every element of the array + // must be a dict. + // + // TODO: There is a version of this diagnostic that requires type + // information. Namely, {a []} is not allowed if a is not of message + // type. Arguably, because this syntax does nothing, it should + // be disallowed... + seq.Values(kv.Value().AsArray().Elements())(func(e ast.ExprAny) bool { + if e.Kind() != ast.ExprKindDict { + p.Error(errUnexpected{ + what: e, + where: taxa.Array.In(), + want: taxa.Dict.AsSet(), + }).Apply( + report.Snippetf(kv.Key(), + "because this %s is missing a %s", + taxa.DictField, taxa.Colon), + report.Notef( + "the %s can be omitted in a %s, but only if the value is a %s or a %s of them", + taxa.Colon, taxa.DictField, + taxa.Dict, taxa.Array), + ) + + return false // Only diagnose the first one. + } + + return true + }) + } + + legalizeOptionValue(p, decl, kv.AsAny(), kv.Value()) + return true + }) + default: + p.Error(errUnexpected{ + what: value, + where: taxa.OptionValue.In(), + }) + } +} diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go new file mode 100644 index 00000000..4f74eb02 --- /dev/null +++ b/experimental/parser/legalize_path.go @@ -0,0 +1,119 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" +) + +// pathOptions is configuration for [legalizePath]. +type pathOptions struct { + // If set, the path must be relative. + AllowAbsolute bool + + // If set, the path may contain precisely one `/` separator. + AllowSlash bool + + // If set, the path may contain extension components. + AllowExts bool + + // If nonzero, the maximum number of bytes in the path. + MaxBytes int + + // If nonzero, the maximum number of components in the path. + MaxComponents int +} + +// legalizePath legalizes a path to satisfy the configuration in opts. +func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) (ok bool) { + ok = true + + var bytes, components int + var slash token.Token + iterx.Enumerate(path.Components)(func(i int, pc ast.PathComponent) bool { + bytes += pc.Separator().Span().Len() + // Just Len() here is technically incorrect, because it could be an + // extension, but MaxBytes is never used with AllowExts. + bytes += pc.Name().Span().Len() + components++ + + if i == 0 && !opts.AllowAbsolute && !pc.Separator().IsZero() { + p.Errorf("unexpected absolute path %s", where).Apply( + report.Snippetf(path, "expected a path without a leading `%s`", pc.Separator().Text()), + ) + ok = false + return true + } + + if pc.Separator().Text() == "/" { + if !opts.AllowSlash { + p.Errorf("unexpected `/` in path %s", where).Apply( + report.Snippetf(pc.Separator(), "help: replace this with a `.`"), + ) + ok = false + return true + } else if !slash.IsZero() { + p.Errorf("type URL can only contain a single `/`").Apply( + report.Snippet(pc.Separator()), + report.Snippetf(slash, "first one is here"), + ) + ok = false + return true + } + slash = pc.Separator() + } + + if ext := pc.AsExtension(); !ext.IsZero() { + if opts.AllowExts { + ok = legalizePath(p, where, ext, pathOptions{ + AllowAbsolute: true, + AllowExts: false, + }) + if !ok { + return true + } + } else { + p.Errorf("unexpected nested extension path %s", where).Apply( + // Use Name() here so we get the outer parens of the extension. + report.Snippet(pc.Name()), + ) + ok = false + return true + } + } + + return true + }) + + if ok { + if opts.MaxBytes > 0 && bytes > opts.MaxBytes { + p.Errorf("path %s is too large", where).Apply( + report.Snippet(path), + report.Notef("Protobuf imposes a limit of %v bytes here", opts.MaxBytes), + ) + } else if opts.MaxComponents > 0 && components > opts.MaxComponents { + p.Errorf("path %s is too large", where).Apply( + report.Snippet(path), + report.Notef("Protobuf imposes a limit of %v components here", opts.MaxComponents), + ) + } + } + + return ok +} diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go new file mode 100644 index 00000000..bcc61a61 --- /dev/null +++ b/experimental/parser/legalize_type.go @@ -0,0 +1,137 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ast/predeclared" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/internal/ext/iterx" +) + +// legalizeMethodParams legalizes part of the signature of a method. +func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { + if list.Len() != 1 { + p.Errorf("expected exactly one type in %s, got %d", what, list.Len()).Apply( + report.Snippet(list), + ) + return + } + + ty := list.At(0) + switch ty.Kind() { + case ast.TypeKindPath: + legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) + case ast.TypeKindPrefixed: + prefixed := ty.AsPrefixed() + if prefixed.Prefix() != ast.TypePrefixStream { + p.Errorf("only the %s modifier may appear in %s", taxa.KeywordStream, what).Apply( + report.Snippet(prefixed.PrefixToken()), + ) + } + + if prefixed.Type().Kind() == ast.TypeKindPath { + legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) + break + } + + ty = prefixed.Type() + fallthrough + default: + p.Errorf("only message types may appear in %s", what).Apply( + report.Snippet(ty), + ) + } +} + +// legalizeFieldType legalizes the type of a message field. +func legalizeFieldType(p *parser, ty ast.TypeAny) { + switch ty.Kind() { + case ast.TypeKindPath: + legalizePath(p, taxa.Field.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) + + case ast.TypeKindPrefixed: + ty := ty.AsPrefixed() + if ty.Prefix() == ast.TypePrefixStream { + p.Errorf("the %s modifier may only appear in a %s", taxa.KeywordStream, taxa.Signature).Apply( + report.Snippet(ty.PrefixToken()), + ) + } + inner := ty.Type() + switch inner.Kind() { + case ast.TypeKindPath: + legalizeFieldType(p, inner) + case ast.TypeKindPrefixed: + p.Error(errMoreThanOne{ + first: ty.PrefixToken(), + second: inner.AsPrefixed().PrefixToken(), + what: taxa.TypePrefix, + }) + default: + p.Error(errUnexpected{ + what: inner, + where: taxa.Classify(ty.PrefixToken()).After(), + want: taxa.TypePath.AsSet(), + }) + } + + case ast.TypeKindGeneric: + ty := ty.AsGeneric() + switch { + case ty.Path().AsPredeclared() != predeclared.Map: + p.Errorf("generic types other than `map` are not supported").Apply( + report.Snippet(ty.Path()), + ) + case ty.Args().Len() != 2: + p.Errorf("expected exactly two type arguments, got %d", ty.Args().Len()).Apply( + report.Snippet(ty.Args()), + ) + default: + k, v := ty.AsMap() + if !k.AsPath().AsPredeclared().IsMapKey() { + p.Error(errUnexpected{ + what: k, + where: taxa.MapKey.In(), + got: "non-comparable type", + }).Apply( + report.Helpf( + "a map key must be one of the following types: %s", + iterx.Join(iterx.Filter( + predeclared.All(), + func(p predeclared.Name) bool { return p.IsMapKey() }, + ), ", "), + ), + ) + } + + switch v.Kind() { + case ast.TypeKindPath: + legalizeFieldType(p, v) + case ast.TypeKindPrefixed: + p.Error(errUnexpected{ + what: v.AsPrefixed().PrefixToken(), + where: taxa.MapValue.In(), + }) + default: + p.Error(errUnexpected{ + what: v, + where: taxa.MapValue.In(), + want: taxa.TypePath.AsSet(), + }) + } + } + } +} diff --git a/experimental/parser/lex.go b/experimental/parser/lex.go index c97c1315..07defcdb 100644 --- a/experimental/parser/lex.go +++ b/experimental/parser/lex.go @@ -360,15 +360,18 @@ func bracePair(s string) (string, string) { } func isASCIIIdent(s string) bool { - for _, r := range s { + for i, r := range s { switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': case r >= '0' && r <= '9': + if i == 0 { + return false + } case r == '_': default: return false } } - return true + return len(s) > 0 } diff --git a/experimental/parser/parse.go b/experimental/parser/parse.go index 965b4082..f1b162df 100644 --- a/experimental/parser/parse.go +++ b/experimental/parser/parse.go @@ -67,6 +67,9 @@ func parse(ctx ast.Context, errs *report.Report) { seq.Append(root.Decls(), node) } } + + p.parseComplete = true + legalizeFile(p, root) } // ensureProgress is used to make sure that the parser makes progress on each diff --git a/experimental/parser/parse_decl.go b/experimental/parser/parse_decl.go index 506bd6b7..e34ca48f 100644 --- a/experimental/parser/parse_decl.go +++ b/experimental/parser/parse_decl.go @@ -186,7 +186,8 @@ func parseDecl(p *parser, c *token.Cursor, in taxa.Noun) ast.DeclAny { // // TODO: this treats import public inside of a message as a field, which // may result in worse diagnostics. - if in != taxa.TopLevel && !path.AsIdent().IsZero() { + if in != taxa.TopLevel && + (!path.AsIdent().IsZero() && next.Kind() != token.String) { break } // This is definitely a field. @@ -371,6 +372,7 @@ func parseRange(p *parser, c *token.Cursor) ast.DeclRange { // parseTypeList parses a type list out of a bracket token. func parseTypeList(p *parser, parens token.Token, types ast.TypeList, in taxa.Noun) { + types.SetBrackets(parens) delimited[ast.TypeAny]{ p: p, c: parens.Children(), diff --git a/experimental/parser/parse_starts.go b/experimental/parser/parse_starts.go index 33d4cc39..90c8571d 100644 --- a/experimental/parser/parse_starts.go +++ b/experimental/parser/parse_starts.go @@ -43,7 +43,7 @@ func canStartExpr(tok token.Token) bool { return canStartPath(tok) || tok.Kind() == token.Number || tok.Kind() == token.String || tok.Text() == "-" || - ((tok.Text() == "{" || tok.Text() == "[") && !tok.IsLeaf()) + ((tok.Text() == "{" || tok.Text() == "<" || tok.Text() == "[") && !tok.IsLeaf()) } func canStartOptions(tok token.Token) bool { diff --git a/experimental/parser/parse_state.go b/experimental/parser/parse_state.go index bd3b5953..4dbeb76a 100644 --- a/experimental/parser/parse_state.go +++ b/experimental/parser/parse_state.go @@ -26,6 +26,54 @@ type parser struct { ast.Context *ast.Nodes *report.Report + + parseComplete bool + + syntax ast.DeclSyntax + cachedMode taxa.Noun +} + +// Mode returns whether or not the parser believes it is in editions +// mode. This function must not be called until AST construction is complete +// and legalization begins. +// +// This function will return the same answer every time it is called. This is to +// avoid diagnostics depending on where the editions keyword appears. For +// example, consider: +// +// message Foo { +// reserved foo; +// } +// +// edition = "2023"; +// +// message Bar { +// reserved "foo"; +// } +// +// If we only referenced p.syntax, we get into a situation where we diagnose +// *both* reserved ranges, rather than just the one in Foo, which is potentially +// confusing, and suggests that the order of declarations in Protobuf is +// semantically meaningful. +func (p *parser) Mode() taxa.Noun { + if !p.parseComplete { + panic("called parser.Mode() outside of the legalizer; this is a bug") + } + + if p.cachedMode == taxa.Unknown { + p.cachedMode = taxa.SyntaxMode + if !p.syntax.IsZero() && p.syntax.IsEdition() { + p.cachedMode = taxa.EditionMode + } + } + + return p.cachedMode +} + +// classified is a spanner that has been classified by taxa. +type classified struct { + report.Spanner + what taxa.Noun } // parsePunct attempts to unconditionally parse some punctuation. diff --git a/experimental/parser/parse_type.go b/experimental/parser/parse_type.go index 5ecfa815..466072dc 100644 --- a/experimental/parser/parse_type.go +++ b/experimental/parser/parse_type.go @@ -114,8 +114,9 @@ func parseTypeImpl(p *parser, c *token.Cursor, where taxa.Place, pathAfter bool) // This case applies to the keywords: // - package // - extend + // - option if !isList && len(mods) == 0 && - slicesx.Among(ident.Text(), "package", "extend") && + slicesx.Among(ident.Text(), "package", "extend", "option") && !canStartPath(c.Peek()) { kw, path := tyPath.Split(1) if !path.IsZero() { diff --git a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt index 0f05af55..f2ecd152 100644 --- a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt +++ b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt @@ -3,6 +3,10 @@ error: unterminated string literal | 1 | '\' | ^^^ expected to be terminated by `'` - = note: this string appears to end in an escaped quote; replace `\'` with `\\''` + help: this string appears to end in an escaped quote + | + 1 | - '\' + 1 | + '\\'' + | encountered 1 error diff --git a/experimental/parser/testdata/parser/def/bad_path.proto b/experimental/parser/testdata/parser/def/bad_path.proto new file mode 100644 index 00000000..048acbbb --- /dev/null +++ b/experimental/parser/testdata/parser/def/bad_path.proto @@ -0,0 +1,32 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message foo.Bar { + oneof foo.Bar {} + oneof foo.(bar.baz).Bar {} +} +message foo.(bar.baz).Bar {} + +enum foo.Bar {} +enum foo.(bar.baz).Bar {} + +extend foo.Bar {} +extend foo.(bar.baz).Bar {} + +service foo.Bar {} +service foo.(bar.baz).Bar {} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt new file mode 100644 index 00000000..0d5e265d --- /dev/null +++ b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt @@ -0,0 +1,63 @@ +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:19:12 + | +19 | message foo.Bar { + | ^ expected identifier + = note: the name of a message definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:20:14 + | +20 | oneof foo.Bar {} + | ^ expected identifier + = note: the name of a oneof definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:21:14 + | +21 | oneof foo.(bar.baz).Bar {} + | ^ expected identifier + = note: the name of a oneof definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:23:12 + | +23 | message foo.(bar.baz).Bar {} + | ^ expected identifier + = note: the name of a message definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:25:9 + | +25 | enum foo.Bar {} + | ^ expected identifier + = note: the name of a enum definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:26:9 + | +26 | enum foo.(bar.baz).Bar {} + | ^ expected identifier + = note: the name of a enum definition must be a single identifier + +error: unexpected nested extension path in message extension block + --> testdata/parser/def/bad_path.proto:29:12 + | +29 | extend foo.(bar.baz).Bar {} + | ^^^^^^^^^ + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:31:12 + | +31 | service foo.Bar {} + | ^ expected identifier + = note: the name of a service definition must be a single identifier + +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:32:12 + | +32 | service foo.(bar.baz).Bar {} + | ^ expected identifier + = note: the name of a service definition must be a single identifier + +encountered 9 errors diff --git a/experimental/parser/testdata/parser/def/bad_path.proto.yaml b/experimental/parser/testdata/parser/def/bad_path.proto.yaml new file mode 100644 index 00000000..eab67751 --- /dev/null +++ b/experimental/parser/testdata/parser/def/bad_path.proto.yaml @@ -0,0 +1,63 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + name.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + type.path.components: [{ ident: "message" }] + body.decls: + - def: + name.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + type.path.components: [{ ident: "oneof" }] + body: {} + - def: + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "Bar", separator: SEPARATOR_DOT } + type.path.components: [{ ident: "oneof" }] + body: {} + - def: + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "Bar", separator: SEPARATOR_DOT } + type.path.components: [{ ident: "message" }] + body: {} + - def: + name.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + type.path.components: [{ ident: "enum" }] + body: {} + - def: + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "Bar", separator: SEPARATOR_DOT } + type.path.components: [{ ident: "enum" }] + body: {} + - def: + kind: KIND_EXTEND + name.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + body: {} + - def: + kind: KIND_EXTEND + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "Bar", separator: SEPARATOR_DOT } + body: {} + - def: + name.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + type.path.components: [{ ident: "service" }] + body: {} + - def: + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "Bar", separator: SEPARATOR_DOT } + type.path.components: [{ ident: "service" }] + body: {} diff --git a/experimental/parser/testdata/parser/def/bare_bodies.proto b/experimental/parser/testdata/parser/def/bare_bodies.proto new file mode 100644 index 00000000..26e84d38 --- /dev/null +++ b/experimental/parser/testdata/parser/def/bare_bodies.proto @@ -0,0 +1,30 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message M { + int32 x = 1; + { + int32 y = 2; + } +} + +{ + message N { + int32 y = 2; + } +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt b/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt new file mode 100644 index 00000000..a5a0529b --- /dev/null +++ b/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt @@ -0,0 +1,31 @@ +error: unexpected definition body in message definition + --> testdata/parser/def/bare_bodies.proto:21:5 + | +21 | / { +22 | | int32 y = 2; +23 | | } + | \_____^ + help: remove these braces + | +21 | - { +22 | int32 y = 2; +23 | - } + | + +error: unexpected definition body in file scope + --> testdata/parser/def/bare_bodies.proto:26:1 + | +26 | / { +... | +30 | | } + | \_^ + help: remove these braces + | +26 | - { +27 | message N { +28 | int32 y = 2; +29 | } +30 | - } + | + +encountered 2 errors diff --git a/experimental/parser/testdata/parser/def/bare_bodies.proto.yaml b/experimental/parser/testdata/parser/def/bare_bodies.proto.yaml new file mode 100644 index 00000000..fadbb3e5 --- /dev/null +++ b/experimental/parser/testdata/parser/def/bare_bodies.proto.yaml @@ -0,0 +1,28 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "M" }] + body.decls: + - def: + kind: KIND_FIELD + name.components: [{ ident: "x" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 1 + - body.decls: + - def: + kind: KIND_FIELD + name.components: [{ ident: "y" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 2 + - body.decls: + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "N" }] + body.decls: + - def: + kind: KIND_FIELD + name.components: [{ ident: "y" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 2 diff --git a/experimental/parser/testdata/parser/def/mixed.proto b/experimental/parser/testdata/parser/def/mixed.proto new file mode 100644 index 00000000..350aab5f --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto @@ -0,0 +1,43 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message Foo [foo=bar] { + enum Foo [foo=bar] {} + oneof Foo [foo=bar] {} +} +extend bar.Foo [foo=bar] {} + +service FooService [foo=bar] {} + +message Foo = 1 { + enum Foo = 1 {} + oneof Foo = 1 {} +} +extend bar.Foo = 1 {} + +service FooService = 1 {} + +message Foo(X) returns (X) { + enum Foo(X) returns (X) {} + oneof Foo(X) returns (X) {} +} +extend bar.Foo(X) returns (X) {} + +service FooService(X) returns (X) {} + +message Foo = "bar" {} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt b/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt new file mode 100644 index 00000000..e1563c06 --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt @@ -0,0 +1,97 @@ +error: message definition cannot specify compact options + --> testdata/parser/def/mixed.proto:19:13 + | +19 | message Foo [foo=bar] { + | ^^^^^^^^^ help: remove this + +error: enum definition cannot specify compact options + --> testdata/parser/def/mixed.proto:20:14 + | +20 | enum Foo [foo=bar] {} + | ^^^^^^^^^ help: remove this + +error: oneof definition cannot specify compact options + --> testdata/parser/def/mixed.proto:21:15 + | +21 | oneof Foo [foo=bar] {} + | ^^^^^^^^^ help: remove this + +error: message extension block cannot specify compact options + --> testdata/parser/def/mixed.proto:23:16 + | +23 | extend bar.Foo [foo=bar] {} + | ^^^^^^^^^ help: remove this + +error: service definition cannot specify compact options + --> testdata/parser/def/mixed.proto:25:20 + | +25 | service FooService [foo=bar] {} + | ^^^^^^^^^ help: remove this + +error: unexpected integer literal in message definition + --> testdata/parser/def/mixed.proto:27:13 + | +27 | message Foo = 1 { + | ^^^ + +error: unexpected integer literal in enum definition + --> testdata/parser/def/mixed.proto:28:14 + | +28 | enum Foo = 1 {} + | ^^^ + +error: unexpected integer literal in oneof definition + --> testdata/parser/def/mixed.proto:29:15 + | +29 | oneof Foo = 1 {} + | ^^^ + +error: unexpected integer literal in message extension block + --> testdata/parser/def/mixed.proto:31:16 + | +31 | extend bar.Foo = 1 {} + | ^^^ + +error: unexpected integer literal in service definition + --> testdata/parser/def/mixed.proto:33:20 + | +33 | service FooService = 1 {} + | ^^^ + +error: message definition appears to have method signature + --> testdata/parser/def/mixed.proto:35:12 + | +35 | message Foo(X) returns (X) { + | ^^^^^^^^^^^^^^^ help: remove this + +error: enum definition appears to have method signature + --> testdata/parser/def/mixed.proto:36:13 + | +36 | enum Foo(X) returns (X) {} + | ^^^^^^^^^^^^^^^ help: remove this + +error: oneof definition appears to have method signature + --> testdata/parser/def/mixed.proto:37:14 + | +37 | oneof Foo(X) returns (X) {} + | ^^^^^^^^^^^^^^^ help: remove this + +error: message extension block appears to have method signature + --> testdata/parser/def/mixed.proto:39:15 + | +39 | extend bar.Foo(X) returns (X) {} + | ^^^^^^^^^^^^^^^ help: remove this + +error: service definition appears to have method signature + --> testdata/parser/def/mixed.proto:41:19 + | +41 | service FooService(X) returns (X) {} + | ^^^^^^^^^^^^^^^ help: remove this + +error: unexpected string literal in message definition + --> testdata/parser/def/mixed.proto:43:13 + | +43 | message Foo = "bar" {} + | ^^^^^^^ + +encountered 16 errors diff --git a/experimental/parser/testdata/parser/def/mixed.proto.yaml b/experimental/parser/testdata/parser/def/mixed.proto.yaml new file mode 100644 index 00000000..92b8be84 --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto.yaml @@ -0,0 +1,103 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + options.entries: + - path.components: [{ ident: "foo" }] + value.path.components: [{ ident: "bar" }] + body.decls: + - def: + kind: KIND_ENUM + name.components: [{ ident: "Foo" }] + options.entries: + - path.components: [{ ident: "foo" }] + value.path.components: [{ ident: "bar" }] + body: {} + - def: + kind: KIND_ONEOF + name.components: [{ ident: "Foo" }] + options.entries: + - path.components: [{ ident: "foo" }] + value.path.components: [{ ident: "bar" }] + body: {} + - def: + kind: KIND_EXTEND + name.components: [{ ident: "bar" }, { ident: "Foo", separator: SEPARATOR_DOT }] + options.entries: + - path.components: [{ ident: "foo" }] + value.path.components: [{ ident: "bar" }] + body: {} + - def: + kind: KIND_SERVICE + name.components: [{ ident: "FooService" }] + options.entries: + - path.components: [{ ident: "foo" }] + value.path.components: [{ ident: "bar" }] + body: {} + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + value.literal.int_value: 1 + body.decls: + - def: + kind: KIND_ENUM + name.components: [{ ident: "Foo" }] + value.literal.int_value: 1 + body: {} + - def: + kind: KIND_ONEOF + name.components: [{ ident: "Foo" }] + value.literal.int_value: 1 + body: {} + - def: + kind: KIND_EXTEND + name.components: [{ ident: "bar" }, { ident: "Foo", separator: SEPARATOR_DOT }] + value.literal.int_value: 1 + body: {} + - def: + kind: KIND_SERVICE + name.components: [{ ident: "FooService" }] + value.literal.int_value: 1 + body: {} + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + signature: + inputs: [{ path.components: [{ ident: "X" }] }] + outputs: [{ path.components: [{ ident: "X" }] }] + body.decls: + - def: + kind: KIND_ENUM + name.components: [{ ident: "Foo" }] + signature: + inputs: [{ path.components: [{ ident: "X" }] }] + outputs: [{ path.components: [{ ident: "X" }] }] + body: {} + - def: + kind: KIND_ONEOF + name.components: [{ ident: "Foo" }] + signature: + inputs: [{ path.components: [{ ident: "X" }] }] + outputs: [{ path.components: [{ ident: "X" }] }] + body: {} + - def: + kind: KIND_EXTEND + name.components: [{ ident: "bar" }, { ident: "Foo", separator: SEPARATOR_DOT }] + signature: + inputs: [{ path.components: [{ ident: "X" }] }] + outputs: [{ path.components: [{ ident: "X" }] }] + body: {} + - def: + kind: KIND_SERVICE + name.components: [{ ident: "FooService" }] + signature: + inputs: [{ path.components: [{ ident: "X" }] }] + outputs: [{ path.components: [{ ident: "X" }] }] + body: {} + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + value.literal.string_value: "bar" + body: {} diff --git a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt new file mode 100644 index 00000000..a65f6752 --- /dev/null +++ b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt @@ -0,0 +1,297 @@ +error: unexpected service definition within message definition + --> testdata/parser/def/nesting.proto:22:5 + | +19 | / message M { +20 | | message M {} +21 | | enum E {} +22 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +23 | | extend E {} +24 | | oneof O {} +25 | | } + | \_- ...cannot be declared within this message definition + = help: a service definition can only appear at file scope + +error: unexpected message definition within enum definition + --> testdata/parser/def/nesting.proto:28:5 + | +27 | / enum E { +28 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +29 | | enum E {} +... | +33 | | } + | \_- ...cannot be declared within this enum definition + = help: a message definition can only appear within one of file scope, + message definition, or group definition + +error: unexpected enum definition within enum definition + --> testdata/parser/def/nesting.proto:29:5 + | +27 | / enum E { +28 | | message M {} +29 | | enum E {} + | | ^^^^^^^^^ this enum definition... +30 | | service S {} +... | +33 | | } + | \_- ...cannot be declared within this enum definition + = help: a enum definition can only appear within one of file scope, message + definition, or group definition + +error: unexpected service definition within enum definition + --> testdata/parser/def/nesting.proto:30:5 + | +27 | / enum E { +28 | | message M {} +29 | | enum E {} +30 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +31 | | extend E {} +32 | | oneof O {} +33 | | } + | \_- ...cannot be declared within this enum definition + = help: a service definition can only appear at file scope + +error: unexpected message extension block within enum definition + --> testdata/parser/def/nesting.proto:31:5 + | +27 | / enum E { +... | +30 | | service S {} +31 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +32 | | oneof O {} +33 | | } + | \_- ...cannot be declared within this enum definition + = help: a message extension block can only appear within one of file scope, + message definition, or group definition + +error: unexpected oneof definition within enum definition + --> testdata/parser/def/nesting.proto:32:5 + | +27 | / enum E { +... | +31 | | extend E {} +32 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +33 | | } + | \_- ...cannot be declared within this enum definition + = help: a oneof definition can only appear within one of message definition + or group definition + +error: unexpected message definition within service definition + --> testdata/parser/def/nesting.proto:36:5 + | +35 | / service S { +36 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +37 | | enum E {} +... | +41 | | } + | \_- ...cannot be declared within this service definition + = help: a message definition can only appear within one of file scope, + message definition, or group definition + +error: unexpected enum definition within service definition + --> testdata/parser/def/nesting.proto:37:5 + | +35 | / service S { +36 | | message M {} +37 | | enum E {} + | | ^^^^^^^^^ this enum definition... +38 | | service S {} +... | +41 | | } + | \_- ...cannot be declared within this service definition + = help: a enum definition can only appear within one of file scope, message + definition, or group definition + +error: unexpected service definition within service definition + --> testdata/parser/def/nesting.proto:38:5 + | +35 | / service S { +36 | | message M {} +37 | | enum E {} +38 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +39 | | extend E {} +40 | | oneof O {} +41 | | } + | \_- ...cannot be declared within this service definition + = help: a service definition can only appear at file scope + +error: unexpected message extension block within service definition + --> testdata/parser/def/nesting.proto:39:5 + | +35 | / service S { +... | +38 | | service S {} +39 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +40 | | oneof O {} +41 | | } + | \_- ...cannot be declared within this service definition + = help: a message extension block can only appear within one of file scope, + message definition, or group definition + +error: unexpected oneof definition within service definition + --> testdata/parser/def/nesting.proto:40:5 + | +35 | / service S { +... | +39 | | extend E {} +40 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +41 | | } + | \_- ...cannot be declared within this service definition + = help: a oneof definition can only appear within one of message definition + or group definition + +error: unexpected message definition within message extension block + --> testdata/parser/def/nesting.proto:44:5 + | +43 | / extend E { +44 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +45 | | enum E {} +... | +49 | | } + | \_- ...cannot be declared within this message extension block + = help: a message definition can only appear within one of file scope, + message definition, or group definition + +error: unexpected enum definition within message extension block + --> testdata/parser/def/nesting.proto:45:5 + | +43 | / extend E { +44 | | message M {} +45 | | enum E {} + | | ^^^^^^^^^ this enum definition... +46 | | service S {} +... | +49 | | } + | \_- ...cannot be declared within this message extension block + = help: a enum definition can only appear within one of file scope, message + definition, or group definition + +error: unexpected service definition within message extension block + --> testdata/parser/def/nesting.proto:46:5 + | +43 | / extend E { +44 | | message M {} +45 | | enum E {} +46 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +47 | | extend E {} +48 | | oneof O {} +49 | | } + | \_- ...cannot be declared within this message extension block + = help: a service definition can only appear at file scope + +error: unexpected message extension block within message extension block + --> testdata/parser/def/nesting.proto:47:5 + | +43 | / extend E { +... | +46 | | service S {} +47 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +48 | | oneof O {} +49 | | } + | \_- ...cannot be declared within this message extension block + = help: a message extension block can only appear within one of file scope, + message definition, or group definition + +error: unexpected oneof definition within message extension block + --> testdata/parser/def/nesting.proto:48:5 + | +43 | / extend E { +... | +47 | | extend E {} +48 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +49 | | } + | \_- ...cannot be declared within this message extension block + = help: a oneof definition can only appear within one of message definition + or group definition + +error: unexpected oneof definition at file scope + --> testdata/parser/def/nesting.proto:51:1 + | +51 | / oneof O { +... | +57 | | } + | \_^ this oneof definition cannot be declared here + = help: a oneof definition can only appear within one of message definition + or group definition + +error: unexpected message definition within oneof definition + --> testdata/parser/def/nesting.proto:52:5 + | +51 | / oneof O { +52 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +53 | | enum E {} +... | +57 | | } + | \_- ...cannot be declared within this oneof definition + = help: a message definition can only appear within one of file scope, + message definition, or group definition + +error: unexpected enum definition within oneof definition + --> testdata/parser/def/nesting.proto:53:5 + | +51 | / oneof O { +52 | | message M {} +53 | | enum E {} + | | ^^^^^^^^^ this enum definition... +54 | | service S {} +... | +57 | | } + | \_- ...cannot be declared within this oneof definition + = help: a enum definition can only appear within one of file scope, message + definition, or group definition + +error: unexpected service definition within oneof definition + --> testdata/parser/def/nesting.proto:54:5 + | +51 | / oneof O { +52 | | message M {} +53 | | enum E {} +54 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +55 | | extend E {} +56 | | oneof O {} +57 | | } + | \_- ...cannot be declared within this oneof definition + = help: a service definition can only appear at file scope + +error: unexpected message extension block within oneof definition + --> testdata/parser/def/nesting.proto:55:5 + | +51 | / oneof O { +... | +54 | | service S {} +55 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +56 | | oneof O {} +57 | | } + | \_- ...cannot be declared within this oneof definition + = help: a message extension block can only appear within one of file scope, + message definition, or group definition + +error: unexpected oneof definition within oneof definition + --> testdata/parser/def/nesting.proto:56:5 + | +51 | / oneof O { +... | +55 | | extend E {} +56 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +57 | | } + | \_- ...cannot be declared within this oneof definition + = help: a oneof definition can only appear within one of message definition + or group definition + +encountered 22 errors diff --git a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index c1ccc797..1cb418c2 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt @@ -1,3 +1,15 @@ +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:20:5 + | +20 | M x (T) (T); + | ^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:20:9 + | +20 | M x (T) (T); + | ^^^ help: remove this + error: encountered more than one method parameter list --> testdata/parser/def/ordering.proto:20:13 | @@ -6,6 +18,18 @@ error: encountered more than one method parameter list | | | first one is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:21:5 + | +21 | M x returns (T) (T); + | ^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:21:9 + | +21 | M x returns (T) (T); + | ^^^^^^^^^^^^^^^ help: remove this + error: unexpected method parameter list after method return type --> testdata/parser/def/ordering.proto:21:21 | @@ -14,6 +38,18 @@ error: unexpected method parameter list after method return type | | | previous method return type is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:22:5 + | +22 | M x returns T (T); + | ^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:22:9 + | +22 | M x returns T (T); + | ^^^^^^^^^^^^^ help: remove this + error: missing `(...)` around method return type --> testdata/parser/def/ordering.proto:22:17 | @@ -28,6 +64,18 @@ error: unexpected method parameter list after method return type | | | previous method return type is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:23:5 + | +23 | M x [foo = bar] (T); + | ^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:23:21 + | +23 | M x [foo = bar] (T); + | ^^^ help: remove this + error: unexpected method parameter list after compact options --> testdata/parser/def/ordering.proto:23:21 | @@ -36,6 +84,48 @@ error: unexpected method parameter list after compact options | | | previous compact options is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:24:5 + | +24 | M x { /* ... */ } (T); + | ^^^^^^^^^^^^^^^^^ + +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:24:9 + | +24 | M x { /* ... */ } (T); + | ^^^^^^^^^^^^^ + +error: unexpected nested extension path in message field + --> testdata/parser/def/ordering.proto:24:23 + | +24 | M x { /* ... */ } (T); + | ^^^ + +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:24:23 + | +24 | M x { /* ... */ } (T); + | ^^^^ + +error: missing name in message field + --> testdata/parser/def/ordering.proto:24:23 + | +24 | M x { /* ... */ } (T); + | ^^^^ + +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:26:5 + | +26 | M x returns (T) returns (T); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:26:9 + | +26 | M x returns (T) returns (T); + | ^^^^^^^^^^^ help: remove this + error: encountered more than one method return type --> testdata/parser/def/ordering.proto:26:21 | @@ -44,6 +134,18 @@ error: encountered more than one method return type | | | first one is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:27:5 + | +27 | M x [foo = bar] returns (T); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:27:21 + | +27 | M x [foo = bar] returns (T); + | ^^^^^^^^^^^ help: remove this + error: unexpected method return type after compact options --> testdata/parser/def/ordering.proto:27:21 | @@ -52,6 +154,42 @@ error: unexpected method return type after compact options | | | previous compact options is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:28:5 + | +28 | M x { /* ... */ } returns (T); + | ^^^^^^^^^^^^^^^^^ + +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:28:9 + | +28 | M x { /* ... */ } returns (T); + | ^^^^^^^^^^^^^ + +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:28:23 + | +28 | M x { /* ... */ } returns (T); + | ^^^^^^^^^^^^ + +error: unexpected extension name in message field + --> testdata/parser/def/ordering.proto:28:31 + | +28 | M x { /* ... */ } returns (T); + | ^^^ expected identifier + +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:30:5 + | +30 | M x returns T returns T; + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:30:9 + | +30 | M x returns T returns T; + | ^^^^^^^^^ help: remove this + error: missing `(...)` around method return type --> testdata/parser/def/ordering.proto:30:17 | @@ -66,12 +204,30 @@ error: encountered more than one method return type | | | first one is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:31:5 + | +31 | M x returns T [] returns T; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:31:9 + | +31 | M x returns T [] returns T; + | ^^^^^^^^^ help: remove this + error: missing `(...)` around method return type --> testdata/parser/def/ordering.proto:31:17 | 31 | M x returns T [] returns T; | ^ help: replace this with `(T)` +error: compact options cannot be empty + --> testdata/parser/def/ordering.proto:31:19 + | +31 | M x returns T [] returns T; + | ^^ help: remove this + error: unexpected method return type after compact options --> testdata/parser/def/ordering.proto:31:22 | @@ -80,6 +236,18 @@ error: unexpected method return type after compact options | | | previous compact options is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:32:5 + | +32 | M x [foo = bar] returns T; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: message field appears to have method signature + --> testdata/parser/def/ordering.proto:32:21 + | +32 | M x [foo = bar] returns T; + | ^^^^^^^^^ help: remove this + error: unexpected method return type after compact options --> testdata/parser/def/ordering.proto:32:21 | @@ -94,6 +262,24 @@ error: missing `(...)` around method return type 32 | M x [foo = bar] returns T; | ^ help: replace this with `(T)` +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:33:5 + | +33 | M x { /* ... */ } returns T; + | ^^^^^^^^^^^^^^^^^ + +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:33:9 + | +33 | M x { /* ... */ } returns T; + | ^^^^^^^^^^^^^ + +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:33:23 + | +33 | M x { /* ... */ } returns T; + | ^^^^^^^^^^ + error: encountered more than one message field tag --> testdata/parser/def/ordering.proto:35:13 | @@ -110,12 +296,30 @@ error: unexpected message field tag after compact options | | | previous compact options is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:37:5 + | +37 | M x { /* ... */ } = 1; + | ^^^^^^^^^^^^^^^^^ + +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:37:9 + | +37 | M x { /* ... */ } = 1; + | ^^^^^^^^^^^^^ + error: unexpected tokens in message definition --> testdata/parser/def/ordering.proto:37:23 | 37 | M x { /* ... */ } = 1; | ^^^ expected identifier, `;`, `.`, `(...)`, or `{...}` +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:39:5 + | +39 | M x [foo = bar] [foo = bar]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + error: encountered more than one compact options --> testdata/parser/def/ordering.proto:39:21 | @@ -124,10 +328,22 @@ error: encountered more than one compact options | | | first one is here +error: missing message field tag in declaration + --> testdata/parser/def/ordering.proto:40:5 + | +40 | M x { /* ... */ } [foo = bar]; + | ^^^^^^^^^^^^^^^^^ + +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:40:9 + | +40 | M x { /* ... */ } [foo = bar]; + | ^^^^^^^^^^^^^ + error: unexpected `[...]` in message definition --> testdata/parser/def/ordering.proto:40:23 | 40 | M x { /* ... */ } [foo = bar]; | ^^^^^^^^^^^ expected identifier, `;`, `.`, `(...)`, or `{...}` -encountered 18 errors +encountered 54 errors diff --git a/experimental/parser/testdata/parser/def/ordering.proto.yaml b/experimental/parser/testdata/parser/def/ordering.proto.yaml index f87ef26b..adf4aff4 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.yaml +++ b/experimental/parser/testdata/parser/def/ordering.proto.yaml @@ -37,9 +37,7 @@ decls: name.components: [{ ident: "x" }] type.path.components: [{ ident: "M" }] body: {} - - def: - kind: KIND_FIELD - type.path.components: [{ extension.components: [{ ident: "T" }] }] + - def.type.path.components: [{ extension.components: [{ ident: "T" }] }] - def: kind: KIND_FIELD name.components: [{ ident: "x" }] @@ -59,7 +57,6 @@ decls: type.path.components: [{ ident: "M" }] body: {} - def: - kind: KIND_FIELD name.components: [{ extension.components: [{ ident: "T" }] }] type.path.components: [{ ident: "returns" }] - def: diff --git a/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt new file mode 100644 index 00000000..8ceda0bd --- /dev/null +++ b/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt @@ -0,0 +1,19 @@ +error: unexpected qualified name in enum value + --> testdata/parser/enum/bad-path.proto:20:5 + | +20 | foo.bar = 1; + | ^^^^^^^ expected identifier + +error: unexpected extension name in enum value + --> testdata/parser/enum/bad-path.proto:21:5 + | +21 | (foo) = 2; + | ^^^^^ expected identifier + +error: unexpected qualified name in enum value + --> testdata/parser/enum/bad-path.proto:22:5 + | +22 | foo/bar = 3; + | ^^^^^^^ expected identifier + +encountered 3 errors diff --git a/experimental/parser/testdata/parser/enum/bad-path.proto.yaml b/experimental/parser/testdata/parser/enum/bad-path.proto.yaml index 29238454..81d87eb3 100644 --- a/experimental/parser/testdata/parser/enum/bad-path.proto.yaml +++ b/experimental/parser/testdata/parser/enum/bad-path.proto.yaml @@ -6,14 +6,11 @@ decls: name.components: [{ ident: "E" }] body.decls: - def: - kind: KIND_ENUM_VALUE name.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_ENUM_VALUE name.components: [{ extension.components: [{ ident: "foo" }] }] value.literal.int_value: 2 - def: - kind: KIND_ENUM_VALUE name.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_SLASH }] value.literal.int_value: 3 diff --git a/experimental/parser/testdata/parser/enum/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/enum/incomplete.proto.stderr.txt index 65a25684..06efbd49 100644 --- a/experimental/parser/testdata/parser/enum/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/enum/incomplete.proto.stderr.txt @@ -1,7 +1,19 @@ +error: missing enum value in declaration + --> testdata/parser/enum/incomplete.proto:20:5 + | +20 | NO_TAG; + | ^^^^^^^ + +error: missing enum value in declaration + --> testdata/parser/enum/incomplete.proto:21:5 + | +21 | NO_TAG2 + | ^^^^^^^ + error: unexpected `}` after definition --> testdata/parser/enum/incomplete.proto:22:1 | 22 | } | ^ expected `;` -encountered 1 error +encountered 3 errors diff --git a/experimental/parser/testdata/parser/expr.proto.stderr.txt b/experimental/parser/testdata/parser/expr.proto.stderr.txt index b3f835ca..b0f6f856 100644 --- a/experimental/parser/testdata/parser/expr.proto.stderr.txt +++ b/experimental/parser/testdata/parser/expr.proto.stderr.txt @@ -1,3 +1,71 @@ +error: unexpected range expression in option setting value + --> testdata/parser/expr.proto:21:21 + | +21 | option (test.any) = 1 to 100; + | ^^^^^^^^ + +error: unexpected array expression in option setting value + --> testdata/parser/expr.proto:22:21 + | +22 | option (test.any) = [1, 2, 3]; + | ^^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + +error: unexpected qualified name in message field value + --> testdata/parser/expr.proto:29:5 + | +29 | foo.bar: "x", + | ^^^^^^^ expected message field name, extension name, or `Any` type URL + +error: unexpected qualified name in message field value + --> testdata/parser/expr.proto:31:5 + | +31 | foo.bar {}, + | ^^^^^^^ expected message field name, extension name, or `Any` type URL + +error: unexpected integer literal in message field value + --> testdata/parser/expr.proto:33:9 + | +33 | 1: "x", + | ^ expected message field name, extension name, or `Any` type URL + +error: unexpected string literal in message field value + --> testdata/parser/expr.proto:34:9 + | +34 | "foo": "x" + | ^^^^^ expected message field name, extension name, or `Any` type URL + help: remove the quotes + | +34 | - "foo": "x" +34 | + foo: "x" + | + +error: unexpected integer literal in message field value + --> testdata/parser/expr.proto:35:9 + | +35 | 1 { + | ^ expected message field name, extension name, or `Any` type URL + +error: unexpected string literal in message field value + --> testdata/parser/expr.proto:36:13 + | +36 | "foo" { + | ^^^^^ expected message field name, extension name, or `Any` type URL + help: remove the quotes + | +36 | - "foo" { +36 | + foo { + | + +error: unexpected array expression in option setting value + --> testdata/parser/expr.proto:44:21 + | +44 | option (test.bad) = [1: 2]; + | ^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected integer literal in message expression --> testdata/parser/expr.proto:45:22 | @@ -22,10 +90,16 @@ error: unexpected `;` after `-` 46 | option (test.bad) = -; | ^ expected expression +error: unexpected range expression in option setting value + --> testdata/parser/expr.proto:47:21 + | +47 | option (test.bad) = 1 to; + | ^^^^ + error: unexpected `;` after `to` --> testdata/parser/expr.proto:47:25 | 47 | option (test.bad) = 1 to; | ^ expected expression -encountered 5 errors +encountered 15 errors diff --git a/experimental/parser/testdata/parser/field/bad-path.proto b/experimental/parser/testdata/parser/field/bad-path.proto index 7b88c615..d1c65324 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto +++ b/experimental/parser/testdata/parser/field/bad-path.proto @@ -21,11 +21,14 @@ message M { repeated Type path.name = 1; required Type path.name = 1; Type path.name = 1; + Type path/name = 1; optional package.Type path.name = 1; repeated package.Type path.name = 1; required package.Type path.name = 1; package.Type name = 1; + package/Type name = 1; + optional package/Type path.name = 1; optional (foo.bar).Type name = 1; repeated (foo.bar).Type name = 1; @@ -38,4 +41,7 @@ message M { package.Type (foo.bar).name = 1; (foo) (bar) = 1; + + map foo = 1; + map foo = 1; } \ No newline at end of file diff --git a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt new file mode 100644 index 00000000..c23ffc39 --- /dev/null +++ b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt @@ -0,0 +1,139 @@ +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:20:19 + | +20 | optional Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:21:19 + | +21 | repeated Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:22:19 + | +22 | required Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:23:10 + | +23 | Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:24:10 + | +24 | Type path/name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:26:27 + | +26 | optional package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:27:27 + | +27 | repeated package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:28:27 + | +28 | required package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected `/` in path in message field + --> testdata/parser/field/bad-path.proto:30:12 + | +30 | package/Type name = 1; + | ^ help: replace this with a `.` + +error: unexpected `/` in path in message field + --> testdata/parser/field/bad-path.proto:31:21 + | +31 | optional package/Type path.name = 1; + | ^ help: replace this with a `.` + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:31:27 + | +31 | optional package/Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:33:14 + | +33 | optional (foo.bar).Type name = 1; + | ^^^^^^^^^ + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:34:14 + | +34 | repeated (foo.bar).Type name = 1; + | ^^^^^^^^^ + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:35:14 + | +35 | required (foo.bar).Type name = 1; + | ^^^^^^^^^ + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:36:5 + | +36 | (foo.bar).Type name = 1; + | ^^^^^^^^^ + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:38:27 + | +38 | optional package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:39:27 + | +39 | repeated package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:40:27 + | +40 | required package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:41:18 + | +41 | package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:43:5 + | +43 | (foo) (bar) = 1; + | ^^^^^ + +error: unexpected extension name in message field + --> testdata/parser/field/bad-path.proto:43:11 + | +43 | (foo) (bar) = 1; + | ^^^^^ expected identifier + +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:45:21 + | +45 | map foo = 1; + | ^^^^^ + +error: unexpected `/` in path in message field + --> testdata/parser/field/bad-path.proto:46:20 + | +46 | map foo = 1; + | ^ help: replace this with a `.` + +encountered 23 errors diff --git a/experimental/parser/testdata/parser/field/bad-path.proto.yaml b/experimental/parser/testdata/parser/field/bad-path.proto.yaml index 0f9f114a..64fade41 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.yaml +++ b/experimental/parser/testdata/parser/field/bad-path.proto.yaml @@ -6,47 +6,44 @@ decls: name.components: [{ ident: "M" }] body.decls: - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_OPTIONAL type.path.components: [{ ident: "Type" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_REPEATED type.path.components: [{ ident: "Type" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_REQUIRED type.path.components: [{ ident: "Type" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.path.components: [{ ident: "Type" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD + name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_SLASH }] + type.path.components: [{ ident: "Type" }] + value.literal.int_value: 1 + - def: name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_OPTIONAL type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_REPEATED type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.prefixed: prefix: PREFIX_REQUIRED @@ -57,6 +54,17 @@ decls: name.components: [{ ident: "name" }] type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 + - def: + kind: KIND_FIELD + name.components: [{ ident: "name" }] + type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_SLASH }] + value.literal.int_value: 1 + - def: + name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] + type.prefixed: + prefix: PREFIX_OPTIONAL + type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_SLASH }] + value.literal.int_value: 1 - def: kind: KIND_FIELD name.components: [{ ident: "name" }] @@ -92,7 +100,6 @@ decls: - { ident: "Type", separator: SEPARATOR_DOT } value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] - { ident: "name", separator: SEPARATOR_DOT } @@ -101,7 +108,6 @@ decls: type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] - { ident: "name", separator: SEPARATOR_DOT } @@ -110,7 +116,6 @@ decls: type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] - { ident: "name", separator: SEPARATOR_DOT } @@ -119,14 +124,33 @@ decls: type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] - { ident: "name", separator: SEPARATOR_DOT } type.path.components: [{ ident: "package" }, { ident: "Type", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: - kind: KIND_FIELD name.components: [{ extension.components: [{ ident: "bar" }] }] type.path.components: [{ extension.components: [{ ident: "foo" }] }] value.literal.int_value: 1 + - def: + kind: KIND_FIELD + name.components: [{ ident: "foo" }] + type.generic: + path.components: [{ ident: "map" }] + args: + - path.components: [{ ident: "string" }] + - path.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }] + separator: SEPARATOR_DOT + value.literal.int_value: 1 + - def: + kind: KIND_FIELD + name.components: [{ ident: "foo" }] + type.generic: + path.components: [{ ident: "map" }] + args: + - path.components: [{ ident: "string" }] + - path.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_SLASH }] + value.literal.int_value: 1 diff --git a/experimental/parser/testdata/parser/field/group.proto.yaml b/experimental/parser/testdata/parser/field/group.proto.yaml index f40a17f2..f10a3afa 100644 --- a/experimental/parser/testdata/parser/field/group.proto.yaml +++ b/experimental/parser/testdata/parser/field/group.proto.yaml @@ -8,6 +8,7 @@ decls: - def: kind: KIND_GROUP name.components: [{ ident: "foo" }] + type.path.components: [{ ident: "group" }] value.literal.int_value: 1 body.decls: - def: @@ -20,7 +21,7 @@ decls: type.path.components: [{ ident: "Foo" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD + kind: KIND_GROUP name.components: [{ ident: "bar" }] type.prefixed: prefix: PREFIX_OPTIONAL @@ -35,6 +36,7 @@ decls: - def: kind: KIND_GROUP name.components: [{ ident: "x" }] + type.path.components: [{ ident: "group" }] value.literal.int_value: 3 options.entries: - path.components: [{ ident: "bar" }] diff --git a/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt index a2e66554..683435f3 100644 --- a/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt @@ -1,9 +1,27 @@ +error: missing name in message field + --> testdata/parser/field/incomplete.proto:20:5 + | +20 | name = 1; + | ^^^^^^^^^ + +error: missing name in message field + --> testdata/parser/field/incomplete.proto:21:5 + | +21 | foo.Bar = 1; + | ^^^^^^^^^^^^ + error: unexpected identifier after definition --> testdata/parser/field/incomplete.proto:24:5 | 24 | foo.Bar name; | ^^^ expected `;` +error: missing message field tag in declaration + --> testdata/parser/field/incomplete.proto:24:5 + | +24 | foo.Bar name; + | ^^^^^^^^^^^^^ + error: unexpected integer literal in definition --> testdata/parser/field/incomplete.proto:25:18 | @@ -16,4 +34,4 @@ error: unexpected `}` after definition 27 | } | ^ expected `;` -encountered 3 errors +encountered 6 errors diff --git a/experimental/parser/testdata/parser/field/incomplete.proto.yaml b/experimental/parser/testdata/parser/field/incomplete.proto.yaml index 8b9aa4b6..0615d8b8 100644 --- a/experimental/parser/testdata/parser/field/incomplete.proto.yaml +++ b/experimental/parser/testdata/parser/field/incomplete.proto.yaml @@ -6,11 +6,9 @@ decls: name.components: [{ ident: "M" }] body.decls: - def: - kind: KIND_FIELD type.path.components: [{ ident: "name" }] value.literal.int_value: 1 - def: - kind: KIND_FIELD type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] value.literal.int_value: 1 - def: diff --git a/experimental/parser/testdata/parser/field/options.proto.stderr.txt b/experimental/parser/testdata/parser/field/options.proto.stderr.txt index dd855fdd..3b4f4b00 100644 --- a/experimental/parser/testdata/parser/field/options.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/options.proto.stderr.txt @@ -1,3 +1,9 @@ +error: compact options cannot be empty + --> testdata/parser/field/options.proto:21:15 + | +21 | M bar = 2 []; + | ^^ help: remove this + error: unexpected `:` in compact option --> testdata/parser/field/options.proto:27:19 | @@ -5,4 +11,4 @@ error: unexpected `:` in compact option | ^ help: replace this with `=` = note: top-level `option` assignment uses `=`, not `:` -encountered 1 error +encountered 2 errors diff --git a/experimental/parser/testdata/parser/import/42.proto.stderr.txt b/experimental/parser/testdata/parser/import/42.proto.stderr.txt new file mode 100644 index 00000000..7c2e92a2 --- /dev/null +++ b/experimental/parser/testdata/parser/import/42.proto.stderr.txt @@ -0,0 +1,7 @@ +error: unexpected integer literal in import + --> testdata/parser/import/42.proto:19:8 + | +19 | import 42; + | ^^ expected string literal + +encountered 1 error diff --git a/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt b/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt new file mode 100644 index 00000000..d53e2b0a --- /dev/null +++ b/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt @@ -0,0 +1,25 @@ +warning: non-canonical string literal in import + --> testdata/parser/import/escapes.proto:19:8 + | +19 | import "foo\x2eproto"; + | ^^^^^^^^^^^^^^ + help: replace it with a canonical string + | +19 | - import "foo\x2eproto"; +19 | + import "foo.proto"; + | + +warning: non-canonical string literal in import + --> testdata/parser/import/escapes.proto:20:8 + | +20 | import "bar" ".proto"; + | ^^^^^^^^^^^^^^ + help: replace it with a canonical string + | +20 | - import "bar" ".proto"; +20 | + import "bar.proto"; + | + = note: Protobuf implicitly concatenates adjacent string literals, like C or + Python; this can lead to surprising behavior + +encountered 2 warnings diff --git a/experimental/parser/testdata/parser/import/in_message.proto b/experimental/parser/testdata/parser/import/in_message.proto new file mode 100644 index 00000000..c8c2d0c2 --- /dev/null +++ b/experimental/parser/testdata/parser/import/in_message.proto @@ -0,0 +1,25 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message M { + import "foo.proto"; + import public "foo.proto"; + import weak "foo.proto"; + + import foo.proto; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt new file mode 100644 index 00000000..d669f74b --- /dev/null +++ b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt @@ -0,0 +1,50 @@ +error: unexpected import within message definition + --> testdata/parser/import/in_message.proto:20:5 + | +19 | / message M { +20 | | import "foo.proto"; + | | ^^^^^^^^^^^^^^^^^^^ this import... +21 | | import public "foo.proto"; +... | +25 | | } + | \_- ...cannot be declared within this message definition + = help: a import can only appear at file scope + +error: unexpected public import within message definition + --> testdata/parser/import/in_message.proto:21:5 + | +19 | / message M { +20 | | import "foo.proto"; +21 | | import public "foo.proto"; + | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ this public import... +22 | | import weak "foo.proto"; +... | +25 | | } + | \_- ...cannot be declared within this message definition + = help: a public import can only appear at file scope + +error: unexpected weak import within message definition + --> testdata/parser/import/in_message.proto:22:5 + | +19 | / message M { +20 | | import "foo.proto"; +21 | | import public "foo.proto"; +22 | | import weak "foo.proto"; + | | ^^^^^^^^^^^^^^^^^^^^^^^^ this weak import... +... | +25 | | } + | \_- ...cannot be declared within this message definition + = help: a weak import can only appear at file scope + +error: unexpected import within message definition + --> testdata/parser/import/in_message.proto:24:5 + | +19 | / message M { +... | +24 | | import foo.proto; + | | ^^^^^^^^^^^^^^^^^ this import... +25 | | } + | \_- ...cannot be declared within this message definition + = help: a import can only appear at file scope + +encountered 4 errors diff --git a/experimental/parser/testdata/parser/import/in_message.proto.yaml b/experimental/parser/testdata/parser/import/in_message.proto.yaml new file mode 100644 index 00000000..300a7f44 --- /dev/null +++ b/experimental/parser/testdata/parser/import/in_message.proto.yaml @@ -0,0 +1,15 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "M" }] + body.decls: + - import.import_path.literal.string_value: "foo.proto" + - import: + modifier: MODIFIER_PUBLIC + import_path.literal.string_value: "foo.proto" + - import: + modifier: MODIFIER_WEAK + import_path.literal.string_value: "foo.proto" + - import.import_path.path.components: [{ ident: "foo" }, { ident: "proto", separator: SEPARATOR_DOT }] diff --git a/experimental/parser/testdata/parser/import/no_path.proto.stderr.txt b/experimental/parser/testdata/parser/import/no_path.proto.stderr.txt new file mode 100644 index 00000000..4f4e9d56 --- /dev/null +++ b/experimental/parser/testdata/parser/import/no_path.proto.stderr.txt @@ -0,0 +1,19 @@ +error: missing import path in import + --> testdata/parser/import/no_path.proto:19:1 + | +19 | import; + | ^^^^^^^ + +error: missing import path in weak import + --> testdata/parser/import/no_path.proto:20:1 + | +20 | import weak; + | ^^^^^^^^^^^^ + +error: missing import path in public import + --> testdata/parser/import/no_path.proto:21:1 + | +21 | import public; + | ^^^^^^^^^^^^^^ + +encountered 3 errors diff --git a/experimental/parser/testdata/parser/import/ok.proto.stderr.txt b/experimental/parser/testdata/parser/import/ok.proto.stderr.txt new file mode 100644 index 00000000..4987c246 --- /dev/null +++ b/experimental/parser/testdata/parser/import/ok.proto.stderr.txt @@ -0,0 +1,9 @@ +warning: use of `import weak` + --> testdata/parser/import/ok.proto:20:1 + | +20 | import weak "weak.proto"; + | ^^^^^^^^^^^ + = note: `import weak` is deprecated and not supported correctly in most + Protobuf implementations + +encountered 1 warning diff --git a/experimental/parser/testdata/parser/import/options.proto.stderr.txt b/experimental/parser/testdata/parser/import/options.proto.stderr.txt new file mode 100644 index 00000000..6af9290f --- /dev/null +++ b/experimental/parser/testdata/parser/import/options.proto.stderr.txt @@ -0,0 +1,27 @@ +error: import cannot specify compact options + --> testdata/parser/import/options.proto:19:20 + | +19 | import "foo.proto" [(not.allowed) = "here"]; + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + +warning: use of `import weak` + --> testdata/parser/import/options.proto:20:1 + | +20 | import weak "weak.proto" [(not.allowed) = "here"]; + | ^^^^^^^^^^^ + = note: `import weak` is deprecated and not supported correctly in most + Protobuf implementations + +error: weak import cannot specify compact options + --> testdata/parser/import/options.proto:20:26 + | +20 | import weak "weak.proto" [(not.allowed) = "here"]; + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + +error: public import cannot specify compact options + --> testdata/parser/import/options.proto:21:30 + | +21 | import public "public.proto" [(not.allowed) = "here"]; + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + +encountered 3 errors and 1 warning diff --git a/experimental/parser/testdata/parser/import/repeated.proto b/experimental/parser/testdata/parser/import/repeated.proto index 4467f12e..b5de08ae 100644 --- a/experimental/parser/testdata/parser/import/repeated.proto +++ b/experimental/parser/testdata/parser/import/repeated.proto @@ -17,6 +17,6 @@ syntax = "proto2"; package test; import "foo.proto"; -import "foo.proto"; // Second +import "foo\x2eproto"; -import "foo\x2eproto"; \ No newline at end of file +import "foo.proto"; // This should not trip the diagnostic again. diff --git a/experimental/parser/testdata/parser/import/repeated.proto.stderr.txt b/experimental/parser/testdata/parser/import/repeated.proto.stderr.txt new file mode 100644 index 00000000..6f7f8f39 --- /dev/null +++ b/experimental/parser/testdata/parser/import/repeated.proto.stderr.txt @@ -0,0 +1,9 @@ +error: file "foo.proto" imported multiple times + --> testdata/parser/import/repeated.proto:20:1 + | +19 | import "foo.proto"; + | ------------------- first imported here +20 | import "foo\x2eproto"; + | ^^^^^^^^^^^^^^^^^^^^^^ + +encountered 1 error diff --git a/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt b/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt new file mode 100644 index 00000000..f7b1821e --- /dev/null +++ b/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt @@ -0,0 +1,25 @@ +error: unexpected qualified name in import + --> testdata/parser/import/symbol.proto:19:8 + | +19 | import my.Proto; + | ^^^^^^^^ expected string literal + = note: Protobuf does not support importing symbols by name, instead, try + importing a file, e.g. `import "google/protobuf/descriptor.proto";` + +error: unexpected qualified name in weak import + --> testdata/parser/import/symbol.proto:20:13 + | +20 | import weak my.Proto; + | ^^^^^^^^ expected string literal + = note: Protobuf does not support importing symbols by name, instead, try + importing a file, e.g. `import "google/protobuf/descriptor.proto";` + +error: unexpected qualified name in public import + --> testdata/parser/import/symbol.proto:21:15 + | +21 | import public my.Proto; + | ^^^^^^^^ expected string literal + = note: Protobuf does not support importing symbols by name, instead, try + importing a file, e.g. `import "google/protobuf/descriptor.proto";` + +encountered 3 errors diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 7cdfb903..848d7dc1 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -1,3 +1,47 @@ +warning: missing `package` declaration + --> testdata/parser/lists.proto + = note: not explicitly specifying a package places the file in the unnamed + package; using it strongly is discouraged + +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:17:14 + | +17 | option foo = []; + | ^^ + help: delete this option; an empty array expression has no effect + | +17 | - option foo = []; + | + = note: array expressions can only appear inside of message expressions + +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:18:14 + | +18 | option foo = [1]; + | ^^^ + help: delete the brackets; this is equivalent for repeated fields + | +18 | - option foo = [1]; +18 | + option foo = 1; + | + = note: array expressions can only appear inside of message expressions + +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:19:14 + | +19 | option foo = [1, 2]; + | ^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:20:14 + | +20 | option foo = [1, 2 3]; + | ^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected integer literal in array expression --> testdata/parser/lists.proto:20:20 | @@ -6,6 +50,14 @@ error: unexpected integer literal in array expression | | | note: assuming a missing `,` here +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:21:14 + | +21 | option foo = [1, 2,, 3]; + | ^^^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected extra `,` in array expression --> testdata/parser/lists.proto:21:20 | @@ -14,6 +66,14 @@ error: unexpected extra `,` in array expression | | | first delimiter is here +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:22:14 + | +22 | option foo = [1, 2,, 3,]; + | ^^^^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected extra `,` in array expression --> testdata/parser/lists.proto:22:20 | @@ -28,6 +88,14 @@ error: unexpected trailing `,` in array expression 22 | option foo = [1, 2,, 3,]; | ^ +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:23:14 + | +23 | option foo = [,1 2,, 3,]; + | ^^^^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected leading `,` in array expression --> testdata/parser/lists.proto:23:15 | @@ -56,6 +124,14 @@ error: unexpected trailing `,` in array expression 23 | option foo = [,1 2,, 3,]; | ^ +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:24:14 + | +24 | option foo = [1; 2; 3]; + | ^^^^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected `;` in array expression --> testdata/parser/lists.proto:24:16 | @@ -68,6 +144,14 @@ error: unexpected `;` in array expression 24 | option foo = [1; 2; 3]; | ^ expected `,` +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:25:14 + | +25 | option foo = [a {}]; + | ^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + error: unexpected message expression in array expression --> testdata/parser/lists.proto:25:17 | @@ -76,6 +160,17 @@ error: unexpected message expression in array expression | | | note: assuming a missing `,` here +error: unexpected array expression in option setting value + --> testdata/parser/lists.proto:26:14 + | +26 | option foo = [,]; + | ^^^ + help: delete this option; an empty array expression has no effect + | +26 | - option foo = [,]; + | + = note: array expressions can only appear inside of message expressions + error: unexpected leading `,` in array expression --> testdata/parser/lists.proto:26:15 | @@ -124,6 +219,24 @@ error: unexpected leading `,` in message expression 39 | bar {,} | ^ expected message field value +error: expected exactly one type in method parameter list, got 2 + --> testdata/parser/lists.proto:44:12 + | +44 | rpc Foo(int, int) returns (int, int); + | ^^^^^^^^^^ + +error: expected exactly one type in method return type, got 2 + --> testdata/parser/lists.proto:44:31 + | +44 | rpc Foo(int, int) returns (int, int); + | ^^^^^^^^^^ + +error: expected exactly one type in method parameter list, got 2 + --> testdata/parser/lists.proto:45:12 + | +45 | rpc Foo(int int) returns (int int); + | ^^^^^^^^^ + error: unexpected type name in method parameter list --> testdata/parser/lists.proto:45:17 | @@ -132,6 +245,12 @@ error: unexpected type name in method parameter list | | | note: assuming a missing `,` here +error: expected exactly one type in method return type, got 2 + --> testdata/parser/lists.proto:45:30 + | +45 | rpc Foo(int int) returns (int int); + | ^^^^^^^^^ + error: unexpected type name in method return type --> testdata/parser/lists.proto:45:35 | @@ -140,24 +259,48 @@ error: unexpected type name in method return type | | | note: assuming a missing `,` here +error: expected exactly one type in method parameter list, got 2 + --> testdata/parser/lists.proto:46:12 + | +46 | rpc Foo(int; int) returns (int, int,); + | ^^^^^^^^^^ + error: unexpected `;` in method parameter list --> testdata/parser/lists.proto:46:16 | 46 | rpc Foo(int; int) returns (int, int,); | ^ expected `,` +error: expected exactly one type in method return type, got 2 + --> testdata/parser/lists.proto:46:31 + | +46 | rpc Foo(int; int) returns (int, int,); + | ^^^^^^^^^^^ + error: unexpected trailing `,` in method return type --> testdata/parser/lists.proto:46:40 | 46 | rpc Foo(int; int) returns (int, int,); | ^ +error: expected exactly one type in method parameter list, got 2 + --> testdata/parser/lists.proto:47:12 + | +47 | rpc Foo(, int, int) returns (int,, int,); + | ^^^^^^^^^^^^ + error: unexpected leading `,` in method parameter list --> testdata/parser/lists.proto:47:13 | 47 | rpc Foo(, int, int) returns (int,, int,); | ^ expected type +error: expected exactly one type in method return type, got 2 + --> testdata/parser/lists.proto:47:33 + | +47 | rpc Foo(, int, int) returns (int,, int,); + | ^^^^^^^^^^^^ + error: unexpected extra `,` in method return type --> testdata/parser/lists.proto:47:38 | @@ -172,18 +315,84 @@ error: unexpected trailing `,` in method return type 47 | rpc Foo(, int, int) returns (int,, int,); | ^ +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/lists.proto:48:12 + | +48 | rpc Foo(;) returns (,); + | ^^^ + error: unexpected `;` in method parameter list --> testdata/parser/lists.proto:48:13 | 48 | rpc Foo(;) returns (,); | ^ expected type +error: expected exactly one type in method return type, got 0 + --> testdata/parser/lists.proto:48:24 + | +48 | rpc Foo(;) returns (,); + | ^^^ + error: unexpected leading `,` in method return type --> testdata/parser/lists.proto:48:25 | 48 | rpc Foo(;) returns (,); | ^ expected type +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/lists.proto:49:12 + | +49 | rpc Foo() returns (); + | ^^ + +error: expected exactly one type in method return type, got 0 + --> testdata/parser/lists.proto:49:23 + | +49 | rpc Foo() returns (); + | ^^ + +error: missing message field tag in declaration + --> testdata/parser/lists.proto:53:5 + | +53 | map x; + | ^^^^^^^^^^^ + +error: expected exactly two type arguments, got 1 + --> testdata/parser/lists.proto:53:8 + | +53 | map x; + | ^^^^^ + +error: missing message field tag in declaration + --> testdata/parser/lists.proto:54:5 + | +54 | map x; + | ^^^^^^^^^^^^^^^^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:54:9 + | +54 | map x; + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + +error: missing message field tag in declaration + --> testdata/parser/lists.proto:55:5 + | +55 | map x; + | ^^^^^^^^^^^^^^^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:55:9 + | +55 | map x; + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + error: unexpected type name in type parameters --> testdata/parser/lists.proto:55:13 | @@ -192,6 +401,21 @@ error: unexpected type name in type parameters | | | note: assuming a missing `,` here +error: missing message field tag in declaration + --> testdata/parser/lists.proto:56:5 + | +56 | map x; + | ^^^^^^^^^^^^^^^^^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:56:9 + | +56 | map x; + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + error: unexpected extra `,` in type parameters --> testdata/parser/lists.proto:56:13 | @@ -200,24 +424,95 @@ error: unexpected extra `,` in type parameters | | | first delimiter is here +error: missing message field tag in declaration + --> testdata/parser/lists.proto:57:5 + | +57 | map<,> x; + | ^^^^^^^^^ + +error: expected exactly two type arguments, got 0 + --> testdata/parser/lists.proto:57:8 + | +57 | map<,> x; + | ^^^ + error: unexpected leading `,` in type parameters --> testdata/parser/lists.proto:57:9 | 57 | map<,> x; | ^ expected type +error: missing message field tag in declaration + --> testdata/parser/lists.proto:58:5 + | +58 | map<> x; + | ^^^^^^^^ + +error: expected exactly two type arguments, got 0 + --> testdata/parser/lists.proto:58:8 + | +58 | map<> x; + | ^^ + +error: missing message field tag in declaration + --> testdata/parser/lists.proto:59:5 + | +59 | map<,int, int> x; + | ^^^^^^^^^^^^^^^^^ + error: unexpected leading `,` in type parameters --> testdata/parser/lists.proto:59:9 | 59 | map<,int, int> x; | ^ expected type +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:59:10 + | +59 | map<,int, int> x; + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + +error: missing message field tag in declaration + --> testdata/parser/lists.proto:60:5 + | +60 | map x; + | ^^^^^^^^^^^^^^^^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:60:9 + | +60 | map x; + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + error: unexpected `;` in type parameters --> testdata/parser/lists.proto:60:12 | 60 | map x; | ^ expected `,` +error: missing message field tag in declaration + --> testdata/parser/lists.proto:61:5 + | +61 | / map< +... | +64 | | > x; + | \________^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/lists.proto:62:9 + | +62 | int, + | ^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + error: unexpected trailing `,` in type parameters --> testdata/parser/lists.proto:63:12 | @@ -282,6 +577,22 @@ error: unexpected trailing `,` in reserved range 71 | reserved ,1 2,, 3,; | ^ +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:72:14 + | +72 | reserved a {}; + | ^ + help: quote it to make it into a string literal + | +72 | reserved "a" {}; + | + + + +error: unexpected message expression in reserved range + --> testdata/parser/lists.proto:72:16 + | +72 | reserved a {}; + | ^^ expected range expression, string literal, or integer literal + error: unexpected message expression in reserved range --> testdata/parser/lists.proto:72:16 | @@ -296,6 +607,36 @@ error: unexpected leading `,` in reserved range 73 | reserved ,; | ^ expected expression +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:75:14 + | +75 | reserved a, b c; + | ^ + help: quote it to make it into a string literal + | +75 | reserved "a", b c; + | + + + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:75:17 + | +75 | reserved a, b c; + | ^ + help: quote it to make it into a string literal + | +75 | reserved a, "b" c; + | + + + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:75:19 + | +75 | reserved a, b c; + | ^ + help: quote it to make it into a string literal + | +75 | reserved a, b "c"; + | + + + error: unexpected identifier in reserved range --> testdata/parser/lists.proto:75:19 | @@ -304,6 +645,36 @@ error: unexpected identifier in reserved range | | | note: assuming a missing `,` here +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:76:14 + | +76 | reserved a, b c + | ^ + help: quote it to make it into a string literal + | +76 | reserved "a", b c + | + + + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:76:17 + | +76 | reserved a, b c + | ^ + help: quote it to make it into a string literal + | +76 | reserved a, "b" c + | + + + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:76:19 + | +76 | reserved a, b c + | ^ + help: quote it to make it into a string literal + | +76 | reserved a, b "c" + | + + + error: unexpected identifier in reserved range --> testdata/parser/lists.proto:76:19 | @@ -318,4 +689,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 46 errors +encountered 94 errors and 1 warning diff --git a/experimental/parser/testdata/parser/method/bad_type.proto b/experimental/parser/testdata/parser/method/bad_type.proto new file mode 100644 index 00000000..82a2c1da --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto @@ -0,0 +1,30 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +service Foo { + rpc Bar1(optional foo.Bar) returns (foo.Bar); + rpc Bar2(foo.Bar) returns (repeated foo.Bar); + rpc Bar2(foo.Bar) returns repeated foo.Bar; + rpc Bar3(map) returns (foo.Bar); + rpc Bar4(string, foo.Bar) returns (foo.Bar); + rpc Bar5(foo.Bar) returns (foo.Bar, stream string); + rpc Bar6(stream repeated foo.Bar) returns (foo.Bar); + rpc Bar7(stream map) returns (foo.Bar); + + rpc Bar8(foo.(bar.baz)) returns (buf.build/x.y); +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt new file mode 100644 index 00000000..f7ad1e56 --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt @@ -0,0 +1,67 @@ +error: only the `stream` modifier may appear in method parameter list + --> testdata/parser/method/bad_type.proto:20:14 + | +20 | rpc Bar1(optional foo.Bar) returns (foo.Bar); + | ^^^^^^^^ + +error: only the `stream` modifier may appear in method return type + --> testdata/parser/method/bad_type.proto:21:32 + | +21 | rpc Bar2(foo.Bar) returns (repeated foo.Bar); + | ^^^^^^^^ + +error: only the `stream` modifier may appear in method return type + --> testdata/parser/method/bad_type.proto:22:31 + | +22 | rpc Bar2(foo.Bar) returns repeated foo.Bar; + | ^^^^^^^^ + +error: missing `(...)` around method return type + --> testdata/parser/method/bad_type.proto:22:31 + | +22 | rpc Bar2(foo.Bar) returns repeated foo.Bar; + | ^^^^^^^^^^^^^^^^ help: replace this with `(repeated foo.Bar)` + +error: only message types may appear in method parameter list + --> testdata/parser/method/bad_type.proto:23:14 + | +23 | rpc Bar3(map) returns (foo.Bar); + | ^^^^^^^^^^^^^^^^^^^^ + +error: expected exactly one type in method parameter list, got 2 + --> testdata/parser/method/bad_type.proto:24:13 + | +24 | rpc Bar4(string, foo.Bar) returns (foo.Bar); + | ^^^^^^^^^^^^^^^^^ + +error: expected exactly one type in method return type, got 2 + --> testdata/parser/method/bad_type.proto:25:31 + | +25 | rpc Bar5(foo.Bar) returns (foo.Bar, stream string); + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/method/bad_type.proto:26:21 + | +26 | rpc Bar6(stream repeated foo.Bar) returns (foo.Bar); + | ^^^^^^^^^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/method/bad_type.proto:27:21 + | +27 | rpc Bar7(stream map) returns (foo.Bar); + | ^^^^^^^^^^^^^^^^^^^^ + +error: unexpected nested extension path in method parameter list + --> testdata/parser/method/bad_type.proto:29:18 + | +29 | rpc Bar8(foo.(bar.baz)) returns (buf.build/x.y); + | ^^^^^^^^^ + +error: unexpected `/` in path in method return type + --> testdata/parser/method/bad_type.proto:29:47 + | +29 | rpc Bar8(foo.(bar.baz)) returns (buf.build/x.y); + | ^ help: replace this with a `.` + +encountered 11 errors diff --git a/experimental/parser/testdata/parser/method/bad_type.proto.yaml b/experimental/parser/testdata/parser/method/bad_type.proto.yaml new file mode 100644 index 00000000..fb8cf498 --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto.yaml @@ -0,0 +1,118 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_SERVICE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar1" }] + signature: + inputs: + - prefixed: + prefix: PREFIX_OPTIONAL + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar2" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - prefixed: + prefix: PREFIX_REPEATED + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar2" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - prefixed: + prefix: PREFIX_REPEATED + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar3" }] + signature: + inputs: + - generic: + path.components: [{ ident: "map" }] + args: + - path.components: [{ ident: "string" }] + - path.components: + - ident: "foo" + - { ident: "Bar", separator: SEPARATOR_DOT } + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar4" }] + signature: + inputs: + - path.components: [{ ident: "string" }] + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar5" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "string" }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar6" }] + signature: + inputs: + - prefixed: + prefix: PREFIX_STREAM + type.prefixed: + prefix: PREFIX_REPEATED + type.path.components: + - ident: "foo" + - { ident: "Bar", separator: SEPARATOR_DOT } + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar7" }] + signature: + inputs: + - prefixed: + prefix: PREFIX_STREAM + type.generic: + path.components: [{ ident: "map" }] + args: + - path.components: [{ ident: "string" }] + - path.components: + - ident: "foo" + - { ident: "Bar", separator: SEPARATOR_DOT } + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar8" }] + signature: + inputs: + - path.components: + - ident: "foo" + - extension.components: + - ident: "bar" + - { ident: "baz", separator: SEPARATOR_DOT } + separator: SEPARATOR_DOT + outputs: + - path.components: + - ident: "buf" + - { ident: "build", separator: SEPARATOR_DOT } + - { ident: "x", separator: SEPARATOR_SLASH } + - { ident: "y", separator: SEPARATOR_DOT } diff --git a/experimental/parser/testdata/parser/method/incomplete.proto b/experimental/parser/testdata/parser/method/incomplete.proto new file mode 100644 index 00000000..d33ee08b --- /dev/null +++ b/experimental/parser/testdata/parser/method/incomplete.proto @@ -0,0 +1,27 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +service Foo { + rpc Bar1(foo.Bar) returns foo.Bar; + rpc Bar2(foo.Bar); + rpc Bar3 returns (foo.Bar); + rpc Bar4(foo.Bar) returns () {} + rpc Bar5() returns (stream foo.Bar); + rpc Bar6() returns; + rpc Bar7() returns stream foo.Bar; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt new file mode 100644 index 00000000..03751705 --- /dev/null +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -0,0 +1,61 @@ +error: missing `(...)` around method return type + --> testdata/parser/method/incomplete.proto:20:31 + | +20 | rpc Bar1(foo.Bar) returns foo.Bar; + | ^^^^^^^ help: replace this with `(foo.Bar)` + +error: missing method return type in service method + --> testdata/parser/method/incomplete.proto:21:13 + | +21 | rpc Bar2(foo.Bar); + | ^^^^^^^^^ expected type in `(...)` after this + +error: missing method parameter list in service method + --> testdata/parser/method/incomplete.proto:22:9 + | +22 | rpc Bar3 returns (foo.Bar); + | ^^^^ expected type in `(...)` after this + +error: expected exactly one type in method return type, got 0 + --> testdata/parser/method/incomplete.proto:23:31 + | +23 | rpc Bar4(foo.Bar) returns () {} + | ^^ + +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/method/incomplete.proto:24:13 + | +24 | rpc Bar5() returns (stream foo.Bar); + | ^^ + +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/method/incomplete.proto:25:13 + | +25 | rpc Bar6() returns; + | ^^ + +error: missing method return type in service method + --> testdata/parser/method/incomplete.proto:25:13 + | +25 | rpc Bar6() returns; + | ^^ expected type in `(...)` after this + +error: unexpected `;` after `returns` + --> testdata/parser/method/incomplete.proto:25:23 + | +25 | rpc Bar6() returns; + | ^ expected `(` + +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/method/incomplete.proto:26:13 + | +26 | rpc Bar7() returns stream foo.Bar; + | ^^ + +error: missing `(...)` around method return type + --> testdata/parser/method/incomplete.proto:26:24 + | +26 | rpc Bar7() returns stream foo.Bar; + | ^^^^^^^^^^^^^^ help: replace this with `(stream foo.Bar)` + +encountered 10 errors diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.yaml b/experimental/parser/testdata/parser/method/incomplete.proto.yaml new file mode 100644 index 00000000..eb7143f0 --- /dev/null +++ b/experimental/parser/testdata/parser/method/incomplete.proto.yaml @@ -0,0 +1,49 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_SERVICE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar1" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + name.components: [{ ident: "Bar2" }] + type.path.components: [{ ident: "rpc" }] + signature.inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + name.components: [{ ident: "Bar3" }] + type.path.components: [{ ident: "rpc" }] + signature.outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar4" }] + signature.inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + body: {} + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar5" }] + signature.outputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + name.components: [{ ident: "Bar6" }] + type.path.components: [{ ident: "rpc" }] + signature: {} + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar7" }] + signature.outputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] diff --git a/experimental/parser/testdata/parser/method/ok.proto b/experimental/parser/testdata/parser/method/ok.proto new file mode 100644 index 00000000..715fd319 --- /dev/null +++ b/experimental/parser/testdata/parser/method/ok.proto @@ -0,0 +1,25 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +service Foo { + rpc Bar1(foo.Bar) returns (foo.Bar); + rpc Bar2(foo.Bar) returns (foo.Bar) {} + rpc Bar3(stream foo.Bar) returns (foo.Bar); + rpc Bar4(foo.Bar) returns (stream foo.Bar) {} + rpc Bar5(stream foo.Bar) returns (stream foo.Bar); +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/method/ok.proto.yaml b/experimental/parser/testdata/parser/method/ok.proto.yaml new file mode 100644 index 00000000..c89aae6e --- /dev/null +++ b/experimental/parser/testdata/parser/method/ok.proto.yaml @@ -0,0 +1,57 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_SERVICE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar1" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar2" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + body: {} + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar3" }] + signature: + inputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar4" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + body: {} + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar5" }] + signature: + inputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - prefixed: + prefix: PREFIX_STREAM + type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] diff --git a/experimental/parser/testdata/parser/method/options.proto b/experimental/parser/testdata/parser/method/options.proto new file mode 100644 index 00000000..cf1f7912 --- /dev/null +++ b/experimental/parser/testdata/parser/method/options.proto @@ -0,0 +1,24 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +service Foo { + rpc Bar1(foo.Bar) returns (foo.Bar) [not.(allowed).here = 42]; + rpc Bar2(foo.Bar) returns (foo.Bar) { + option (allowed).here = 42; + } +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/method/options.proto.stderr.txt b/experimental/parser/testdata/parser/method/options.proto.stderr.txt new file mode 100644 index 00000000..433b1ba2 --- /dev/null +++ b/experimental/parser/testdata/parser/method/options.proto.stderr.txt @@ -0,0 +1,9 @@ +error: service method cannot specify compact options + --> testdata/parser/method/options.proto:20:41 + | +20 | rpc Bar1(foo.Bar) returns (foo.Bar) [not.(allowed).here = 42]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + = note: service method options are applied using `option`; declarations in + the `{...}` following the method definition + +encountered 1 error diff --git a/experimental/parser/testdata/parser/method/options.proto.yaml b/experimental/parser/testdata/parser/method/options.proto.yaml new file mode 100644 index 00000000..bc8fd584 --- /dev/null +++ b/experimental/parser/testdata/parser/method/options.proto.yaml @@ -0,0 +1,37 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_SERVICE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar1" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + options.entries: + - path.components: + - ident: "not" + - extension.components: [{ ident: "allowed" }] + separator: SEPARATOR_DOT + - { ident: "here", separator: SEPARATOR_DOT } + value.literal.int_value: 42 + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar2" }] + signature: + inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + outputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + body.decls: + - def: + kind: KIND_OPTION + name.components: + - extension.components: [{ ident: "allowed" }] + - { ident: "here", separator: SEPARATOR_DOT } + value.literal.int_value: 42 diff --git a/experimental/parser/testdata/parser/option/bad_path.proto b/experimental/parser/testdata/parser/option/bad_path.proto new file mode 100644 index 00000000..3a9f470f --- /dev/null +++ b/experimental/parser/testdata/parser/option/bad_path.proto @@ -0,0 +1,31 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +option; +option foo.bar; +option = 2; +option .(foo.bar).baz = 3; +option foo/(bar.baz).foo = 4; + +message Foo { + int32 x = 1 [ + .(foo.bar).baz = 3, + foo/(bar.baz).foo = 4 + ]; + int32 y = 2 []; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt new file mode 100644 index 00000000..e33dd65a --- /dev/null +++ b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt @@ -0,0 +1,49 @@ +error: missing option setting path + --> testdata/parser/option/bad_path.proto:19:1 + | +19 | option; + | ^^^^^^^ + +error: missing option setting value + --> testdata/parser/option/bad_path.proto:20:1 + | +20 | option foo.bar; + | ^^^^^^^^^^^^^^^ + +error: missing option setting path + --> testdata/parser/option/bad_path.proto:21:1 + | +21 | option = 2; + | ^^^^^^^^^^^ + +error: unexpected absolute path in option setting + --> testdata/parser/option/bad_path.proto:22:8 + | +22 | option .(foo.bar).baz = 3; + | ^^^^^^^^^^^^^^ expected a path without a leading `.` + +error: unexpected `/` in path in option setting + --> testdata/parser/option/bad_path.proto:23:11 + | +23 | option foo/(bar.baz).foo = 4; + | ^ help: replace this with a `.` + +error: unexpected absolute path in option setting + --> testdata/parser/option/bad_path.proto:27:9 + | +27 | .(foo.bar).baz = 3, + | ^^^^^^^^^^^^^^ expected a path without a leading `.` + +error: unexpected `/` in path in option setting + --> testdata/parser/option/bad_path.proto:28:12 + | +28 | foo/(bar.baz).foo = 4 + | ^ help: replace this with a `.` + +error: compact options cannot be empty + --> testdata/parser/option/bad_path.proto:30:17 + | +30 | int32 y = 2 []; + | ^^ help: remove this + +encountered 8 errors diff --git a/experimental/parser/testdata/parser/option/bad_path.proto.yaml b/experimental/parser/testdata/parser/option/bad_path.proto.yaml new file mode 100644 index 00000000..53faa49d --- /dev/null +++ b/experimental/parser/testdata/parser/option/bad_path.proto.yaml @@ -0,0 +1,50 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def.kind: KIND_OPTION + - def: + kind: KIND_OPTION + name.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + - def: { kind: KIND_OPTION, value.literal.int_value: 2 } + - def: + kind: KIND_OPTION + name.components: + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "baz", separator: SEPARATOR_DOT } + value.literal.int_value: 3 + - def: + kind: KIND_OPTION + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_SLASH + - { ident: "foo", separator: SEPARATOR_DOT } + value.literal.int_value: 4 + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_FIELD + name.components: [{ ident: "x" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 1 + options.entries: + - path.components: + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "baz", separator: SEPARATOR_DOT } + value.literal.int_value: 3 + - path.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_SLASH + - { ident: "foo", separator: SEPARATOR_DOT } + value.literal.int_value: 4 + - def: + kind: KIND_FIELD + name.components: [{ ident: "y" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 2 + options: {} diff --git a/experimental/parser/testdata/parser/option/ok.proto b/experimental/parser/testdata/parser/option/ok.proto new file mode 100644 index 00000000..5f75ab47 --- /dev/null +++ b/experimental/parser/testdata/parser/option/ok.proto @@ -0,0 +1,31 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +option foo = 1; +option bar.baz = 2; +option (foo.bar).baz = 3; +option foo.(bar.baz).foo = 4; + +message Foo { + int32 x = 1 [ + foo = 1, + bar.baz = 2, + (foo.bar).baz = 3, + foo.(bar.baz).foo = 4 + ]; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/option/ok.proto.yaml b/experimental/parser/testdata/parser/option/ok.proto.yaml new file mode 100644 index 00000000..6bb709fd --- /dev/null +++ b/experimental/parser/testdata/parser/option/ok.proto.yaml @@ -0,0 +1,49 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "foo" }] + value.literal.int_value: 1 + - def: + kind: KIND_OPTION + name.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + value.literal.int_value: 2 + - def: + kind: KIND_OPTION + name.components: + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + - { ident: "baz", separator: SEPARATOR_DOT } + value.literal.int_value: 3 + - def: + kind: KIND_OPTION + name.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "foo", separator: SEPARATOR_DOT } + value.literal.int_value: 4 + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - def: + kind: KIND_FIELD + name.components: [{ ident: "x" }] + type.path.components: [{ ident: "int32" }] + value.literal.int_value: 1 + options.entries: + - path.components: [{ ident: "foo" }] + value.literal.int_value: 1 + - path.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + value.literal.int_value: 2 + - path.components: + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + - { ident: "baz", separator: SEPARATOR_DOT } + value.literal.int_value: 3 + - path.components: + - ident: "foo" + - extension.components: [{ ident: "bar" }, { ident: "baz", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "foo", separator: SEPARATOR_DOT } + value.literal.int_value: 4 diff --git a/experimental/parser/testdata/parser/option/values.proto b/experimental/parser/testdata/parser/option/values.proto new file mode 100644 index 00000000..538fb586 --- /dev/null +++ b/experimental/parser/testdata/parser/option/values.proto @@ -0,0 +1,90 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +option x = 0; +option x = 42.4; +option x = inf; +option x = nan; +option x = -inf; +option x = -nan; +option x = true; +option x = false; +option x = Infinity; +option x = -Infinity; +option x = foo.bar; +option x = foo.(foo.bar).bar; +option x = .foo; +option x = x to y; + +option x = []; +option x = [1]; +option x = [1, 2]; + +option x = <>: +option x = ; + +option x = {}; +option x = { + x: 0 + x: 42.4 + x: inf + x: nan + x: -inf + x: -nan + x: true + x: false + x: Infinity + x: -Infinity + x: foo.bar + x: foo.(foo.bar).bar + x: .foo + + x: x to y + + x + + x: [] + x: [1] + x: [1, 2] + x: [1, 2, 3, [4, 5, [6]]] + x: [ + [1], + ] + + x: <> + x: + x: > + + "ident": 42 + "???": 42 + 42: 42 + x.y: 42 + (x.y): 42 + .x: 42 + + [x]: 42 + [x.y]: 42 + [.x.y]: 42 + [x, y, z]: 42 + []: 42 + [buf.build/x.y]: 42 + [buf.build/x/y]: 42 + + x [{x: 5}, 1, , 2, 3], +}; + diff --git a/experimental/parser/testdata/parser/option/values.proto.stderr.txt b/experimental/parser/testdata/parser/option/values.proto.stderr.txt new file mode 100644 index 00000000..ae3bc7fd --- /dev/null +++ b/experimental/parser/testdata/parser/option/values.proto.stderr.txt @@ -0,0 +1,326 @@ +error: unexpected identifier after `-` + --> testdata/parser/option/values.proto:28:13 + | +28 | option x = -Infinity; + | ^^^^^^^^ expected floating-point literal or integer literal + +error: unexpected qualified name in option setting value + --> testdata/parser/option/values.proto:29:12 + | +29 | option x = foo.bar; + | ^^^^^^^ expected identifier + +error: unexpected qualified name in option setting value + --> testdata/parser/option/values.proto:30:12 + | +30 | option x = foo.(foo.bar).bar; + | ^^^^^^^^^^^^^^^^^ expected identifier + +error: unexpected fully qualified name in option setting value + --> testdata/parser/option/values.proto:31:12 + | +31 | option x = .foo; + | ^^^^ expected identifier + +error: unexpected range expression in option setting value + --> testdata/parser/option/values.proto:32:12 + | +32 | option x = x to y; + | ^^^^^^ + +error: unexpected array expression in option setting value + --> testdata/parser/option/values.proto:34:12 + | +34 | option x = []; + | ^^ + help: delete this option; an empty array expression has no effect + | +34 | - option x = []; + | + = note: array expressions can only appear inside of message expressions + +error: unexpected array expression in option setting value + --> testdata/parser/option/values.proto:35:12 + | +35 | option x = [1]; + | ^^^ + help: delete the brackets; this is equivalent for repeated fields + | +35 | - option x = [1]; +35 | + option x = 1; + | + = note: array expressions can only appear inside of message expressions + +error: unexpected array expression in option setting value + --> testdata/parser/option/values.proto:36:12 + | +36 | option x = [1, 2]; + | ^^^^^^ + = note: array expressions can only appear inside of message expressions + = help: break this option setting into one per element + +error: cannot use `<...>` for message expression here + --> testdata/parser/option/values.proto:38:12 + | +38 | option x = <>: + | ^^ + help: use `{...}` instead + | +38 | - option x = <>: +38 | + option x = {}: + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +error: unexpected `:` after definition + --> testdata/parser/option/values.proto:38:14 + | +38 | option x = <>: + | ^ expected `;` + +error: unexpected `:` in file scope + --> testdata/parser/option/values.proto:38:14 + | +38 | option x = <>: + | ^ expected identifier, `;`, `.`, `(...)`, or `{...}` + +error: cannot use `<...>` for message expression here + --> testdata/parser/option/values.proto:39:12 + | +39 | option x = ; + | ^^^^^^^ + help: use `{...}` instead + | +39 | - option x = ; +39 | + option x = {a: 42}; + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +error: unexpected qualified name in option setting value + --> testdata/parser/option/values.proto:53:8 + | +53 | x: foo.bar + | ^^^^^^^ expected identifier + +error: unexpected qualified name in option setting value + --> testdata/parser/option/values.proto:54:8 + | +54 | x: foo.(foo.bar).bar + | ^^^^^^^^^^^^^^^^^ expected identifier + +error: unexpected fully qualified name in option setting value + --> testdata/parser/option/values.proto:55:8 + | +55 | x: .foo + | ^^^^ expected identifier + +error: unexpected range expression in option setting value + --> testdata/parser/option/values.proto:57:8 + | +57 | x: x to y + | ^^^^^^ + +error: unexpected identifier in message expression + --> testdata/parser/option/values.proto:59:5 + | +59 | x + | ^ expected message field value + +warning: empty array expression has no effect + --> testdata/parser/option/values.proto:61:8 + | +61 | x: [] + | ^^ + help: delete this message field value + | +61 | - x: [] + | + = note: repeated fields do not distinguish "empty" and "missing" states + +error: nested array expressions are not allowed + --> testdata/parser/option/values.proto:64:18 + | +64 | x: [1, 2, 3, [4, 5, [6]]] + | ----------^^^^^^^^^^^- ...within this array expression + | | + | cannot nest this array expression... + +error: nested array expressions are not allowed + --> testdata/parser/option/values.proto:66:9 + | +65 | x: [ + | _______- +66 | / [1], + | | ^^^ cannot nest this array expression... +67 | | ] + | \_____- ...within this array expression + +error: unexpected trailing `,` in array expression + --> testdata/parser/option/values.proto:66:12 + | +66 | [1], + | ^ + +warning: using `<...>` for message expression is not recommended + --> testdata/parser/option/values.proto:69:8 + | +69 | x: <> + | ^^ + help: use `{...}` instead + | +69 | - x: <> +69 | + x: {} + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +warning: using `<...>` for message expression is not recommended + --> testdata/parser/option/values.proto:70:8 + | +70 | x: + | ^^^^^^^ + help: use `{...}` instead + | +70 | - x: +70 | + x: {a: 42} + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +warning: using `<...>` for message expression is not recommended + --> testdata/parser/option/values.proto:71:8 + | +71 | x: > + | ^^^^^^^^^^^^ + help: use `{...}` instead + | +71 | - x: > +71 | + x: {a: } + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +warning: using `<...>` for message expression is not recommended + --> testdata/parser/option/values.proto:71:12 + | +71 | x: > + | ^^^^^^^ + help: use `{...}` instead + | +71 | - x: > +71 | + x: + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +error: unexpected string literal in message field value + --> testdata/parser/option/values.proto:73:5 + | +73 | "ident": 42 + | ^^^^^^^ expected message field name, extension name, or `Any` type URL + help: remove the quotes + | +73 | - "ident": 42 +73 | + ident: 42 + | + +error: unexpected string literal in message field value + --> testdata/parser/option/values.proto:74:5 + | +74 | "???": 42 + | ^^^^^ expected message field name, extension name, or `Any` type URL + +error: unexpected integer literal in message field value + --> testdata/parser/option/values.proto:75:5 + | +75 | 42: 42 + | ^^ expected message field name, extension name, or `Any` type URL + +error: unexpected qualified name in message field value + --> testdata/parser/option/values.proto:76:5 + | +76 | x.y: 42 + | ^^^ expected message field name, extension name, or `Any` type URL + +error: cannot name extension field using `(...)` in message expression + --> testdata/parser/option/values.proto:77:5 + | +77 | (x.y): 42 + | ^^^^^ expected this to be wrapped in `[...]` instead + help: replace the `(...)` with `[...]` + | +77 | - (x.y): 42 +77 | + [x.y]: 42 + | + +error: unexpected fully qualified name in message field value + --> testdata/parser/option/values.proto:78:5 + | +78 | .x: 42 + | ^^ expected message field name, extension name, or `Any` type URL + +error: unexpected absolute path in extension name + --> testdata/parser/option/values.proto:82:6 + | +82 | [.x.y]: 42 + | ^^^^ expected a path without a leading `.` + +error: unexpected array expression in message field value + --> testdata/parser/option/values.proto:83:5 + | +83 | [x, y, z]: 42 + | ^^^^^^^^^ expected message field name, extension name, or `Any` type URL + +error: unexpected array expression in message field value + --> testdata/parser/option/values.proto:84:5 + | +84 | []: 42 + | ^^ expected message field name, extension name, or `Any` type URL + +error: type URL can only contain a single `/` + --> testdata/parser/option/values.proto:86:17 + | +86 | [buf.build/x/y]: 42 + | - ^ + | | + | first one is here + +error: unexpected integer literal in array expression + --> testdata/parser/option/values.proto:88:16 + | +88 | x [{x: 5}, 1, , 2, 3], + | - ^ expected message expression + | | + | because this message field value is missing a `:` + = note: the `:` can be omitted in a message field value, but only if the + value is a message expression or a array expression of them + +warning: using `<...>` for message expression is not recommended + --> testdata/parser/option/values.proto:88:19 + | +88 | x [{x: 5}, 1, , 2, 3], + | ^^^^^^ + help: use `{...}` instead + | +88 | - x [{x: 5}, 1, , 2, 3], +88 | + x [{x: 5}, 1, {x: 5}, 2, 3], + | + = note: `<...>` are only permitted for sub-messages within a message + expression, but as top-level option values + = help: `<...>` message expressions are an obscure feature and not + recommended + +encountered 31 errors and 6 warnings diff --git a/experimental/parser/testdata/parser/option/values.proto.yaml b/experimental/parser/testdata/parser/option/values.proto.yaml new file mode 100644 index 00000000..d399f903 --- /dev/null +++ b/experimental/parser/testdata/parser/option/values.proto.yaml @@ -0,0 +1,223 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.literal.int_value: 0 + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.literal.float_value: 42.4 + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "inf" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "nan" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "inf" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "nan" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "true" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "false" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "Infinity" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "Infinity" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: + - ident: "foo" + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "bar", separator: SEPARATOR_DOT } + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.path.components: [{ ident: "foo", separator: SEPARATOR_DOT }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.range: + start.path.components: [{ ident: "x" }] + end.path.components: [{ ident: "y" }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.array: {} + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.array.elements: [{ literal.int_value: 1 }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.array.elements: [{ literal.int_value: 1 }, { literal.int_value: 2 }] + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.dict: {} + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.dict.entries: + - key.path.components: [{ ident: "a" }] + value.literal.int_value: 42 + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.dict: {} + - def: + kind: KIND_OPTION + name.components: [{ ident: "x" }] + value.dict.entries: + - key.path.components: [{ ident: "x" }] + value.literal.int_value: 0 + - key.path.components: [{ ident: "x" }] + value.literal.float_value: 42.4 + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "inf" }] + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "nan" }] + - key.path.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "inf" }] + - key.path.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "nan" }] + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "true" }] + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "false" }] + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "Infinity" }] + - key.path.components: [{ ident: "x" }] + value.prefixed: + prefix: PREFIX_MINUS + expr.path.components: [{ ident: "Infinity" }] + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + - key.path.components: [{ ident: "x" }] + value.path.components: + - ident: "foo" + - extension.components: [{ ident: "foo" }, { ident: "bar", separator: SEPARATOR_DOT }] + separator: SEPARATOR_DOT + - { ident: "bar", separator: SEPARATOR_DOT } + - key.path.components: [{ ident: "x" }] + value.path.components: [{ ident: "foo", separator: SEPARATOR_DOT }] + - key.path.components: [{ ident: "x" }] + value.range: + start.path.components: [{ ident: "x" }] + end.path.components: [{ ident: "y" }] + - value.path.components: [{ ident: "x" }] + - { key.path.components: [{ ident: "x" }], value.array: {} } + - key.path.components: [{ ident: "x" }] + value.array.elements: [{ literal.int_value: 1 }] + - key.path.components: [{ ident: "x" }] + value.array.elements: [{ literal.int_value: 1 }, { literal.int_value: 2 }] + - key.path.components: [{ ident: "x" }] + value.array.elements: + - literal.int_value: 1 + - literal.int_value: 2 + - literal.int_value: 3 + - array.elements: + - literal.int_value: 4 + - literal.int_value: 5 + - array.elements: [{ literal.int_value: 6 }] + - key.path.components: [{ ident: "x" }] + value.array.elements: [{ array.elements: [{ literal.int_value: 1 }] }] + - { key.path.components: [{ ident: "x" }], value.dict: {} } + - key.path.components: [{ ident: "x" }] + value.dict.entries: + - key.path.components: [{ ident: "a" }] + value.literal.int_value: 42 + - key.path.components: [{ ident: "x" }] + value.dict.entries: + - key.path.components: [{ ident: "a" }] + value.dict.entries: + - key.path.components: [{ ident: "a" }] + value.literal.int_value: 42 + - key.literal.string_value: "ident" + value.literal.int_value: 42 + - key.literal.string_value: "???" + value.literal.int_value: 42 + - key.literal.int_value: 42 + value.literal.int_value: 42 + - key.path.components: [{ ident: "x" }, { ident: "y", separator: SEPARATOR_DOT }] + value.literal.int_value: 42 + - key.path.components: + - extension.components: [{ ident: "x" }, { ident: "y", separator: SEPARATOR_DOT }] + value.literal.int_value: 42 + - key.path.components: [{ ident: "x", separator: SEPARATOR_DOT }] + value.literal.int_value: 42 + - key.array.elements: [{ path.components: [{ ident: "x" }] }] + value.literal.int_value: 42 + - key.array.elements: + - path.components: [{ ident: "x" }, { ident: "y", separator: SEPARATOR_DOT }] + value.literal.int_value: 42 + - key.array.elements: + - path.components: + - { ident: "x", separator: SEPARATOR_DOT } + - { ident: "y", separator: SEPARATOR_DOT } + value.literal.int_value: 42 + - key.array.elements: + - path.components: [{ ident: "x" }] + - path.components: [{ ident: "y" }] + - path.components: [{ ident: "z" }] + value.literal.int_value: 42 + - { key.array: {}, value.literal.int_value: 42 } + - key.array.elements: + - path.components: + - ident: "buf" + - { ident: "build", separator: SEPARATOR_DOT } + - { ident: "x", separator: SEPARATOR_SLASH } + - { ident: "y", separator: SEPARATOR_DOT } + value.literal.int_value: 42 + - key.array.elements: + - path.components: + - ident: "buf" + - { ident: "build", separator: SEPARATOR_DOT } + - { ident: "x", separator: SEPARATOR_SLASH } + - { ident: "y", separator: SEPARATOR_SLASH } + value.literal.int_value: 42 + - key.path.components: [{ ident: "x" }] + value.array.elements: + - dict.entries: + - key.path.components: [{ ident: "x" }] + value.literal.int_value: 5 + - literal.int_value: 1 + - dict.entries: + - key.path.components: [{ ident: "x" }] + value.literal.int_value: 5 + - literal.int_value: 2 + - literal.int_value: 3 diff --git a/experimental/parser/testdata/parser/package/42.proto b/experimental/parser/testdata/parser/package/42.proto index a7a2448f..38205e3c 100644 --- a/experimental/parser/testdata/parser/package/42.proto +++ b/experimental/parser/testdata/parser/package/42.proto @@ -14,4 +14,8 @@ syntax = "proto2"; +// TODO: This produces a less-than-ideal diagnostic, but it's not an +// especially reasonable-to-expect case. +// +// See https://github.com/bufbuild/protocompile/pull/438#discussion_r1947046609 package 42; diff --git a/experimental/parser/testdata/parser/package/42.proto.stderr.txt b/experimental/parser/testdata/parser/package/42.proto.stderr.txt index 068c4268..024d087e 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -1,13 +1,21 @@ +error: missing path in `package` declaration + --> testdata/parser/package/42.proto:21:1 + | +21 | package 42; + | ^^^^^^^ + = help: to place a file in the unnamed package, omit the `package` + declaration; however, using the unnamed package is discouraged + error: unexpected integer literal after `package` declaration - --> testdata/parser/package/42.proto:17:9 + --> testdata/parser/package/42.proto:21:9 | -17 | package 42; +21 | package 42; | ^^ expected `;` error: unexpected integer literal in file scope - --> testdata/parser/package/42.proto:17:9 + --> testdata/parser/package/42.proto:21:9 | -17 | package 42; +21 | package 42; | ^^ expected identifier, `;`, `.`, `(...)`, or `{...}` -encountered 2 errors +encountered 3 errors diff --git a/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt new file mode 100644 index 00000000..69b5f891 --- /dev/null +++ b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt @@ -0,0 +1,7 @@ +error: unexpected absolute path in `package` declaration + --> testdata/parser/package/absolute.proto:17:9 + | +17 | package .test.test2; + | ^^^^^^^^^^^ expected a path without a leading `.` + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/empty.proto.stderr.txt b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt new file mode 100644 index 00000000..df40ec41 --- /dev/null +++ b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt @@ -0,0 +1,6 @@ +warning: missing `package` declaration + --> testdata/parser/package/empty.proto + = note: not explicitly specifying a package places the file in the unnamed + package; using it strongly is discouraged + +encountered 1 warning diff --git a/experimental/parser/testdata/parser/package/eof_after_kw.proto.stderr.txt b/experimental/parser/testdata/parser/package/eof_after_kw.proto.stderr.txt index ef9ef905..239924db 100644 --- a/experimental/parser/testdata/parser/package/eof_after_kw.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/eof_after_kw.proto.stderr.txt @@ -1,7 +1,15 @@ +error: missing path in `package` declaration + --> testdata/parser/package/eof_after_kw.proto:17:1 + | +17 | package + | ^^^^^^^ + = help: to place a file in the unnamed package, omit the `package` + declaration; however, using the unnamed package is discouraged + error: unexpected end-of-file after `package` declaration --> testdata/parser/package/eof_after_kw.proto:17:8 | 17 | package | ^ expected `;` -encountered 1 error +encountered 2 errors diff --git a/experimental/parser/testdata/parser/package/extension.proto.stderr.txt b/experimental/parser/testdata/parser/package/extension.proto.stderr.txt new file mode 100644 index 00000000..206ecb4e --- /dev/null +++ b/experimental/parser/testdata/parser/package/extension.proto.stderr.txt @@ -0,0 +1,7 @@ +error: unexpected nested extension path in `package` declaration + --> testdata/parser/package/extension.proto:17:14 + | +17 | package test.(extension.path).test; + | ^^^^^^^^^^^^^^^^ + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/host_qualified.proto.stderr.txt b/experimental/parser/testdata/parser/package/host_qualified.proto.stderr.txt new file mode 100644 index 00000000..f4cb132c --- /dev/null +++ b/experimental/parser/testdata/parser/package/host_qualified.proto.stderr.txt @@ -0,0 +1,7 @@ +error: unexpected `/` in path in `package` declaration + --> testdata/parser/package/host_qualified.proto:17:18 + | +17 | package buf.build/test.test2; + | ^ help: replace this with a `.` + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt new file mode 100644 index 00000000..15a1e99f --- /dev/null +++ b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt @@ -0,0 +1,9 @@ +error: missing path in `package` declaration + --> testdata/parser/package/no_path.proto:17:1 + | +17 | package; + | ^^^^^^^^ + = help: to place a file in the unnamed package, omit the `package` + declaration; however, using the unnamed package is discouraged + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/options.proto.stderr.txt b/experimental/parser/testdata/parser/package/options.proto.stderr.txt new file mode 100644 index 00000000..512000c8 --- /dev/null +++ b/experimental/parser/testdata/parser/package/options.proto.stderr.txt @@ -0,0 +1,7 @@ +error: `package` declaration cannot specify compact options + --> testdata/parser/package/options.proto:17:14 + | +17 | package test [(not.allowed) = "here"]; + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/too_big.proto b/experimental/parser/testdata/parser/package/too_big.proto new file mode 100644 index 00000000..7b5794e9 --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_big.proto @@ -0,0 +1,18 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package thispackagenameconsistsof513bytes. + x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; \ No newline at end of file diff --git a/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt b/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt new file mode 100644 index 00000000..418a1a4f --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt @@ -0,0 +1,10 @@ +error: path in `package` declaration is too large + --> testdata/parser/package/too_big.proto:17:9 + | +17 | package thispackagenameconsistsof513bytes. + | ________^ +18 | / x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; + | \___________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________^ + = note: Protobuf imposes a limit of 512 bytes here + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/too_big.proto.yaml b/experimental/parser/testdata/parser/package/too_big.proto.yaml new file mode 100644 index 00000000..0a7d863e --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_big.proto.yaml @@ -0,0 +1,6 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: + - ident: "thispackagenameconsistsof513bytes" + - ident: "x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + separator: SEPARATOR_DOT diff --git a/experimental/parser/testdata/parser/package/too_long.proto b/experimental/parser/testdata/parser/package/too_long.proto new file mode 100644 index 00000000..89167650 --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_long.proto @@ -0,0 +1,27 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package thispathhas102components + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a + .a.a.a.a.a.a.a.a.a.a.x; \ No newline at end of file diff --git a/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt new file mode 100644 index 00000000..9c4b8d1b --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt @@ -0,0 +1,12 @@ +error: path in `package` declaration is too large + --> testdata/parser/package/too_long.proto:17:9 + | +17 | package thispathhas102components + | ________^ +... / +26 | | .a.a.a.a.a.a.a.a.a.a +27 | | .a.a.a.a.a.a.a.a.a.a.x; + | \__________________________^ + = note: Protobuf imposes a limit of 101 components here + +encountered 1 error diff --git a/experimental/parser/testdata/parser/package/too_long.proto.yaml b/experimental/parser/testdata/parser/package/too_long.proto.yaml new file mode 100644 index 00000000..0447b457 --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_long.proto.yaml @@ -0,0 +1,105 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: + - ident: "thispathhas102components" + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "a", separator: SEPARATOR_DOT } + - { ident: "x", separator: SEPARATOR_DOT } diff --git a/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt b/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt new file mode 100644 index 00000000..57f30cff --- /dev/null +++ b/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt @@ -0,0 +1,31 @@ +warning: non-canonical string literal in reserved range + --> testdata/parser/range/escapes.proto:20:14 + | +20 | reserved "foo" "bar"; + | ^^^^^^^^^^^ + help: replace it with a canonical string + | +20 | - reserved "foo" "bar"; +20 | + reserved "foobar"; + | + = note: Protobuf implicitly concatenates adjacent string literals, like C or + Python; this can lead to surprising behavior + +error: reserved message field name is not a valid identifier + --> testdata/parser/range/escapes.proto:21:14 + | +21 | reserved "foo\n", "b\x61r"; + | ^^^^^^^ + +warning: non-canonical string literal in reserved range + --> testdata/parser/range/escapes.proto:21:23 + | +21 | reserved "foo\n", "b\x61r"; + | ^^^^^^^^ + help: replace it with a canonical string + | +21 | - reserved "foo\n", "b\x61r"; +21 | + reserved "foo\n", "bar"; + | + +encountered 1 error and 2 warnings diff --git a/experimental/parser/testdata/parser/range/extension_names.proto.stderr.txt b/experimental/parser/testdata/parser/range/extension_names.proto.stderr.txt new file mode 100644 index 00000000..d65164e2 --- /dev/null +++ b/experimental/parser/testdata/parser/range/extension_names.proto.stderr.txt @@ -0,0 +1,13 @@ +error: unexpected identifier in extension range + --> testdata/parser/range/extension_names.proto:20:16 + | +20 | extensions foo, "bar"; + | ^^^ expected range expression or integer literal + +error: unexpected string literal in extension range + --> testdata/parser/range/extension_names.proto:20:21 + | +20 | extensions foo, "bar"; + | ^^^^^ expected range expression or integer literal + +encountered 2 errors diff --git a/experimental/parser/testdata/parser/range/invalid_exprs.proto b/experimental/parser/testdata/parser/range/invalid_exprs.proto index 5675edd1..699b4dcc 100644 --- a/experimental/parser/testdata/parser/range/invalid_exprs.proto +++ b/experimental/parser/testdata/parser/range/invalid_exprs.proto @@ -19,4 +19,6 @@ package test; message Foo { extensions {}; reserved {}; + + extensions "foo", -"bar", "foo" to "bar"; } \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/invalid_exprs.proto.stderr.txt b/experimental/parser/testdata/parser/range/invalid_exprs.proto.stderr.txt new file mode 100644 index 00000000..b5e437d6 --- /dev/null +++ b/experimental/parser/testdata/parser/range/invalid_exprs.proto.stderr.txt @@ -0,0 +1,37 @@ +error: unexpected message expression in extension range + --> testdata/parser/range/invalid_exprs.proto:20:16 + | +20 | extensions {}; + | ^^ expected range expression or integer literal + +error: unexpected message expression in reserved range + --> testdata/parser/range/invalid_exprs.proto:21:14 + | +21 | reserved {}; + | ^^ expected range expression, string literal, or integer literal + +error: unexpected string literal in extension range + --> testdata/parser/range/invalid_exprs.proto:23:16 + | +23 | extensions "foo", -"bar", "foo" to "bar"; + | ^^^^^ expected range expression or integer literal + +error: unexpected string literal after `-` + --> testdata/parser/range/invalid_exprs.proto:23:24 + | +23 | extensions "foo", -"bar", "foo" to "bar"; + | ^^^^^ expected integer literal + +error: unexpected string literal in extension range + --> testdata/parser/range/invalid_exprs.proto:23:31 + | +23 | extensions "foo", -"bar", "foo" to "bar"; + | ^^^^^ expected integer literal + +error: unexpected string literal in extension range + --> testdata/parser/range/invalid_exprs.proto:23:40 + | +23 | extensions "foo", -"bar", "foo" to "bar"; + | ^^^^^ expected integer literal or `max` + +encountered 6 errors diff --git a/experimental/parser/testdata/parser/range/invalid_exprs.proto.yaml b/experimental/parser/testdata/parser/range/invalid_exprs.proto.yaml index 823897ba..62987f4f 100644 --- a/experimental/parser/testdata/parser/range/invalid_exprs.proto.yaml +++ b/experimental/parser/testdata/parser/range/invalid_exprs.proto.yaml @@ -7,3 +7,13 @@ decls: body.decls: - range: { kind: KIND_EXTENSIONS, ranges: [{ dict: {} }] } - range: { kind: KIND_RESERVED, ranges: [{ dict: {} }] } + - range: + kind: KIND_EXTENSIONS + ranges: + - literal.string_value: "foo" + - prefixed: + prefix: PREFIX_MINUS + expr.literal.string_value: "bar" + - range: + start.literal.string_value: "foo" + end.literal.string_value: "bar" diff --git a/experimental/parser/testdata/parser/range/invalid_parent.proto b/experimental/parser/testdata/parser/range/invalid_parent.proto new file mode 100644 index 00000000..6554872e --- /dev/null +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto @@ -0,0 +1,31 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +service Foo { + extensions 1; + reserved 1; +} + +extend Foo { + extensions 1; + reserved 1; +} + +enum Foo { + extensions 1; +} diff --git a/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt new file mode 100644 index 00000000..dcc18e26 --- /dev/null +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt @@ -0,0 +1,57 @@ +error: unexpected extension range within service definition + --> testdata/parser/range/invalid_parent.proto:20:5 + | +19 | / service Foo { +20 | | extensions 1; + | | ^^^^^^^^^^^^^ this extension range... +21 | | reserved 1; +22 | | } + | \_- ...cannot be declared within this service definition + = help: a extension range can only appear within a message definition + +error: unexpected reserved range within service definition + --> testdata/parser/range/invalid_parent.proto:21:5 + | +19 | / service Foo { +20 | | extensions 1; +21 | | reserved 1; + | | ^^^^^^^^^^^ this reserved range... +22 | | } + | \_- ...cannot be declared within this service definition + = help: a reserved range can only appear within one of message definition or + enum definition + +error: unexpected extension range within message extension block + --> testdata/parser/range/invalid_parent.proto:25:5 + | +24 | / extend Foo { +25 | | extensions 1; + | | ^^^^^^^^^^^^^ this extension range... +26 | | reserved 1; +27 | | } + | \_- ...cannot be declared within this message extension block + = help: a extension range can only appear within a message definition + +error: unexpected reserved range within message extension block + --> testdata/parser/range/invalid_parent.proto:26:5 + | +24 | / extend Foo { +25 | | extensions 1; +26 | | reserved 1; + | | ^^^^^^^^^^^ this reserved range... +27 | | } + | \_- ...cannot be declared within this message extension block + = help: a reserved range can only appear within one of message definition or + enum definition + +error: unexpected extension range within enum definition + --> testdata/parser/range/invalid_parent.proto:30:5 + | +29 | / enum Foo { +30 | | extensions 1; + | | ^^^^^^^^^^^^^ this extension range... +31 | | } + | \_- ...cannot be declared within this enum definition + = help: a extension range can only appear within a message definition + +encountered 5 errors diff --git a/experimental/parser/testdata/parser/range/invalid_parent.proto.yaml b/experimental/parser/testdata/parser/range/invalid_parent.proto.yaml new file mode 100644 index 00000000..e6a0312e --- /dev/null +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto.yaml @@ -0,0 +1,20 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_SERVICE + name.components: [{ ident: "Foo" }] + body.decls: + - range: { kind: KIND_EXTENSIONS, ranges: [{ literal.int_value: 1 }] } + - range: { kind: KIND_RESERVED, ranges: [{ literal.int_value: 1 }] } + - def: + kind: KIND_EXTEND + name.components: [{ ident: "Foo" }] + body.decls: + - range: { kind: KIND_EXTENSIONS, ranges: [{ literal.int_value: 1 }] } + - range: { kind: KIND_RESERVED, ranges: [{ literal.int_value: 1 }] } + - def: + kind: KIND_ENUM + name.components: [{ ident: "Foo" }] + body.decls: + - range: { kind: KIND_EXTENSIONS, ranges: [{ literal.int_value: 1 }] } diff --git a/experimental/parser/testdata/parser/range/ok.proto b/experimental/parser/testdata/parser/range/ok.proto index 6ce5e087..4111f636 100644 --- a/experimental/parser/testdata/parser/range/ok.proto +++ b/experimental/parser/testdata/parser/range/ok.proto @@ -23,17 +23,13 @@ message Foo { extensions 0 to max; extensions 1, 2, 3, 4 to 5, 6; - reserved 1, "foo"; - reserved 2, 3, 5 to 7, foo, "bar"; + reserved 1; + reserved 2, 3, 5 to 7; + reserved 10 to max; } enum Foo { - extensions 1; - extensions 1 to 2; - extensions -5 to 0x20; - extensions 0 to max; - extensions 1, 2, 3, 4 to 5, 6; - - reserved 1, "foo"; - reserved 2, 3, 5 to 7, foo, "bar"; + reserved 1; + reserved 2, 3, 5 to 7; + reserved 10 to max; } \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/ok.proto.yaml b/experimental/parser/testdata/parser/range/ok.proto.yaml index a6d48419..21052b15 100644 --- a/experimental/parser/testdata/parser/range/ok.proto.yaml +++ b/experimental/parser/testdata/parser/range/ok.proto.yaml @@ -34,9 +34,7 @@ decls: start.literal.int_value: 4 end.literal.int_value: 5 - literal.int_value: 6 - - range: - kind: KIND_RESERVED - ranges: [{ literal.int_value: 1 }, { literal.string_value: "foo" }] + - range: { kind: KIND_RESERVED, ranges: [{ literal.int_value: 1 }] } - range: kind: KIND_RESERVED ranges: @@ -45,51 +43,28 @@ decls: - range: start.literal.int_value: 5 end.literal.int_value: 7 - - path.components: [{ ident: "foo" }] - - literal.string_value: "bar" - - def: - kind: KIND_ENUM - name.components: [{ ident: "Foo" }] - body.decls: - - range: { kind: KIND_EXTENSIONS, ranges: [{ literal.int_value: 1 }] } - range: - kind: KIND_EXTENSIONS - ranges: - - range: - start.literal.int_value: 1 - end.literal.int_value: 2 - - range: - kind: KIND_EXTENSIONS - ranges: - - range: - start.prefixed: { prefix: PREFIX_MINUS, expr.literal.int_value: 5 } - end.literal.int_value: 32 - - range: - kind: KIND_EXTENSIONS + kind: KIND_RESERVED ranges: - range: - start.literal.int_value: 0 + start.literal.int_value: 10 end.path.components: [{ ident: "max" }] + - def: + kind: KIND_ENUM + name.components: [{ ident: "Foo" }] + body.decls: + - range: { kind: KIND_RESERVED, ranges: [{ literal.int_value: 1 }] } - range: - kind: KIND_EXTENSIONS + kind: KIND_RESERVED ranges: - - literal.int_value: 1 - literal.int_value: 2 - literal.int_value: 3 - range: - start.literal.int_value: 4 - end.literal.int_value: 5 - - literal.int_value: 6 - - range: - kind: KIND_RESERVED - ranges: [{ literal.int_value: 1 }, { literal.string_value: "foo" }] + start.literal.int_value: 5 + end.literal.int_value: 7 - range: kind: KIND_RESERVED ranges: - - literal.int_value: 2 - - literal.int_value: 3 - range: - start.literal.int_value: 5 - end.literal.int_value: 7 - - path.components: [{ ident: "foo" }] - - literal.string_value: "bar" + start.literal.int_value: 10 + end.path.components: [{ ident: "max" }] diff --git a/experimental/parser/testdata/parser/range/options.proto.stderr.txt b/experimental/parser/testdata/parser/range/options.proto.stderr.txt new file mode 100644 index 00000000..c11ac973 --- /dev/null +++ b/experimental/parser/testdata/parser/range/options.proto.stderr.txt @@ -0,0 +1,7 @@ +error: reserved range cannot specify compact options + --> testdata/parser/range/options.proto:21:16 + | +21 | reserved 1 [(allowed) = false]; + | ^^^^^^^^^^^^^^^^^^^ help: remove this + +encountered 1 error diff --git a/experimental/parser/testdata/parser/range/reserved_default_syntax.proto b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto new file mode 100644 index 00000000..7738047d --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test; + +message Foo { + reserved foo, "foo"; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt new file mode 100644 index 00000000..4d5e0b73 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt @@ -0,0 +1,11 @@ +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/range/reserved_default_syntax.proto:18:14 + | +18 | reserved foo, "foo"; + | ^^^ + help: quote it to make it into a string literal + | +18 | reserved "foo", "foo"; + | + + + +encountered 1 error diff --git a/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.yaml b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.yaml new file mode 100644 index 00000000..a45b138f --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.yaml @@ -0,0 +1,11 @@ +decls: + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - range: + kind: KIND_RESERVED + ranges: + - path.components: [{ ident: "foo" }] + - literal.string_value: "foo" diff --git a/experimental/parser/testdata/parser/range/reserved_edition.proto b/experimental/parser/testdata/parser/range/reserved_edition.proto new file mode 100644 index 00000000..820ce86d --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto @@ -0,0 +1,21 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2023"; + +package test; + +message Foo { + reserved foo, "foo"; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt new file mode 100644 index 00000000..a37d6a93 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt @@ -0,0 +1,15 @@ +error: cannot use string literals in reserved range in editions mode + --> testdata/parser/range/reserved_edition.proto:20:19 + | +15 | edition = "2023"; + | ----------------- editions mode is specified here +... +20 | reserved foo, "foo"; + | ^^^^^ + help: replace this with an identifier + | +20 | - reserved foo, "foo"; +20 | + reserved foo, foo; + | + +encountered 1 error diff --git a/experimental/parser/testdata/parser/range/reserved_edition.proto.yaml b/experimental/parser/testdata/parser/range/reserved_edition.proto.yaml new file mode 100644 index 00000000..341b2885 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto.yaml @@ -0,0 +1,12 @@ +decls: + - syntax: { kind: KIND_EDITION, value.literal.string_value: "2023" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - range: + kind: KIND_RESERVED + ranges: + - path.components: [{ ident: "foo" }] + - literal.string_value: "foo" diff --git a/experimental/parser/testdata/parser/range/reserved_mixed.proto b/experimental/parser/testdata/parser/range/reserved_mixed.proto new file mode 100644 index 00000000..07925dc1 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_mixed.proto @@ -0,0 +1,25 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message Foo { + reserved 5, "foo"; + reserved "foo", 5; + reserved 5, "foo", 5; + reserved "foo", 5, "foo"; + reserved 5, "foo", 5, "foo", 5, 5; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt new file mode 100644 index 00000000..7e61a418 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt @@ -0,0 +1,71 @@ +error: cannot mix tags and names in reserved range + --> testdata/parser/range/reserved_mixed.proto:20:17 + | +20 | reserved 5, "foo"; + | - ^^^^^ this field name must go in its own reserved range + | | + | but expected a field tag because of this + help: split the reserved range + | +20 | - reserved 5, "foo"; +20 | + reserved 5; +21 | + reserved "foo"; + | + +error: cannot mix tags and names in reserved range + --> testdata/parser/range/reserved_mixed.proto:21:21 + | +21 | reserved "foo", 5; + | ----- ^ this field tag must go in its own reserved range + | | + | but expected a field name because of this + help: split the reserved range + | +21 | - reserved "foo", 5; +21 | + reserved "foo"; +22 | + reserved 5; + | + +error: cannot mix tags and names in reserved range + --> testdata/parser/range/reserved_mixed.proto:22:17 + | +22 | reserved 5, "foo", 5; + | - ^^^^^ this field name must go in its own reserved range + | | + | but expected a field tag because of this + help: split the reserved range + | +22 | - reserved 5, "foo", 5; +22 | + reserved 5, 5; +23 | + reserved "foo"; + | + +error: cannot mix tags and names in reserved range + --> testdata/parser/range/reserved_mixed.proto:23:21 + | +23 | reserved "foo", 5, "foo"; + | ----- ^ this field tag must go in its own reserved range + | | + | but expected a field name because of this + help: split the reserved range + | +23 | - reserved "foo", 5, "foo"; +23 | + reserved "foo", "foo"; +24 | + reserved 5; + | + +error: cannot mix tags and names in reserved range + --> testdata/parser/range/reserved_mixed.proto:24:17 + | +24 | reserved 5, "foo", 5, "foo", 5, 5; + | - ^^^^^ this field name must go in its own reserved range + | | + | but expected a field tag because of this + help: split the reserved range + | +24 | - reserved 5, "foo", 5, "foo", 5, 5; +24 | + reserved 5, 5, 5, 5; +25 | + reserved "foo", "foo"; + | + +encountered 5 errors diff --git a/experimental/parser/testdata/parser/range/reserved_mixed.proto.yaml b/experimental/parser/testdata/parser/range/reserved_mixed.proto.yaml new file mode 100644 index 00000000..d8cda2c8 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_mixed.proto.yaml @@ -0,0 +1,34 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - range: + kind: KIND_RESERVED + ranges: [{ literal.int_value: 5 }, { literal.string_value: "foo" }] + - range: + kind: KIND_RESERVED + ranges: [{ literal.string_value: "foo" }, { literal.int_value: 5 }] + - range: + kind: KIND_RESERVED + ranges: + - literal.int_value: 5 + - literal.string_value: "foo" + - literal.int_value: 5 + - range: + kind: KIND_RESERVED + ranges: + - literal.string_value: "foo" + - literal.int_value: 5 + - literal.string_value: "foo" + - range: + kind: KIND_RESERVED + ranges: + - literal.int_value: 5 + - literal.string_value: "foo" + - literal.int_value: 5 + - literal.string_value: "foo" + - literal.int_value: 5 + - literal.int_value: 5 diff --git a/experimental/parser/testdata/parser/range/reserved_syntax.proto b/experimental/parser/testdata/parser/range/reserved_syntax.proto new file mode 100644 index 00000000..605ea40d --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto @@ -0,0 +1,21 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package test; + +message Foo { + reserved foo, "foo"; +} \ No newline at end of file diff --git a/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt new file mode 100644 index 00000000..7b783f53 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt @@ -0,0 +1,14 @@ +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/range/reserved_syntax.proto:20:14 + | +15 | syntax = "proto2"; + | ------------------ syntax mode is specified here +... +20 | reserved foo, "foo"; + | ^^^ + help: quote it to make it into a string literal + | +20 | reserved "foo", "foo"; + | + + + +encountered 1 error diff --git a/experimental/parser/testdata/parser/range/reserved_syntax.proto.yaml b/experimental/parser/testdata/parser/range/reserved_syntax.proto.yaml new file mode 100644 index 00000000..5f3f5b17 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto.yaml @@ -0,0 +1,12 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "proto2" } + - package.path.components: [{ ident: "test" }] + - def: + kind: KIND_MESSAGE + name.components: [{ ident: "Foo" }] + body.decls: + - range: + kind: KIND_RESERVED + ranges: + - path.components: [{ ident: "foo" }] + - literal.string_value: "foo" diff --git a/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt new file mode 100644 index 00000000..5995d368 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt @@ -0,0 +1,8 @@ +error: unrecognized `edition` declaration value + --> testdata/parser/syntax/2024.proto:15:11 + | +15 | edition = "2024"; + | ^^^^^^ + = note: permitted values: "2023" + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt new file mode 100644 index 00000000..d25bb14b --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt @@ -0,0 +1,8 @@ +error: unexpected syntax in `edition` declaration + --> testdata/parser/syntax/edition_proto2.proto:15:11 + | +15 | edition = "proto2"; + | ^^^^^^^^ + = note: permitted values: "2023" + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt index 311e556b..3a85151c 100644 --- a/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt @@ -1,7 +1,12 @@ +warning: missing `package` declaration + --> testdata/parser/syntax/eof_after_eq.proto + = note: not explicitly specifying a package places the file in the unnamed + package; using it strongly is discouraged + error: unexpected end-of-file in expression --> testdata/parser/syntax/eof_after_eq.proto:15:9 | 15 | syntax = | ^ expected expression -encountered 1 error +encountered 1 error and 1 warning diff --git a/experimental/parser/testdata/parser/syntax/eof_after_kw.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/eof_after_kw.proto.stderr.txt index df781754..5780764f 100644 --- a/experimental/parser/testdata/parser/syntax/eof_after_kw.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/eof_after_kw.proto.stderr.txt @@ -1,7 +1,12 @@ +warning: missing `package` declaration + --> testdata/parser/syntax/eof_after_kw.proto + = note: not explicitly specifying a package places the file in the unnamed + package; using it strongly is discouraged + error: unexpected end-of-file in `syntax` declaration --> testdata/parser/syntax/eof_after_kw.proto:15:7 | 15 | syntax | ^ expected `=` -encountered 1 error +encountered 1 error and 1 warning diff --git a/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt new file mode 100644 index 00000000..e66e6c71 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt @@ -0,0 +1,8 @@ +error: unrecognized `syntax` declaration value + --> testdata/parser/syntax/invalid.proto:15:10 + | +15 | syntax = invalid; + | ^^^^^^^ + = note: permitted values: "proto2", "proto3" + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt index 03b49403..5f42e7fe 100644 --- a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt @@ -1,13 +1,43 @@ +warning: missing `package` declaration + --> testdata/parser/syntax/lonely.proto + = note: not explicitly specifying a package places the file in the unnamed + package; using it strongly is discouraged + error: unexpected `;` in `edition` declaration --> testdata/parser/syntax/lonely.proto:15:8 | 15 | edition; | ^ expected `=` +error: unexpected `syntax` declaration + --> testdata/parser/syntax/lonely.proto:17:1 + | +15 | edition; + | -------- previous declaration is here +16 | +17 | syntax = ; + | ^^^^^^^^^^ + help: remove this + | +17 | - syntax = ; + | + = note: a file may contain at most one `syntax` or `edition` declaration + error: unexpected `;` in `syntax` declaration --> testdata/parser/syntax/lonely.proto:17:10 | 17 | syntax = ; | ^ expected expression -encountered 2 errors +warning: the `package` declaration should be placed at the top of the file + --> testdata/parser/syntax/lonely.proto:19:1 + | +17 | syntax = ; + | ---------- previous declaration is here +18 | +19 | package test; + | ^^^^^^^^^^^^^ + = help: a file's `package` declaration should immediately follow the `syntax` + or `edition` declaration + +encountered 3 errors and 2 warnings diff --git a/experimental/parser/testdata/parser/syntax/not_first.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/not_first.proto.stderr.txt new file mode 100644 index 00000000..01e09049 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/not_first.proto.stderr.txt @@ -0,0 +1,11 @@ +error: unexpected `syntax` declaration + --> testdata/parser/syntax/not_first.proto:17:1 + | +15 | package test; + | ------------- previous declaration is here +16 | +17 | syntax = "proto2"; + | ^^^^^^^^^^^^^^^^^^ + = note: a `syntax` declaration must be the first declaration in a file + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/options.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/options.proto.stderr.txt new file mode 100644 index 00000000..55bb58e6 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/options.proto.stderr.txt @@ -0,0 +1,7 @@ +error: `syntax` declaration cannot specify compact options + --> testdata/parser/syntax/options.proto:15:19 + | +15 | syntax = "proto2" [(not.allowed) = "here"]; + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/proto2_escaped.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/proto2_escaped.proto.stderr.txt new file mode 100644 index 00000000..736f68c4 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/proto2_escaped.proto.stderr.txt @@ -0,0 +1,12 @@ +warning: non-canonical string literal in `syntax` declaration + --> testdata/parser/syntax/proto2_escaped.proto:15:10 + | +15 | syntax = "proto\x32"; + | ^^^^^^^^^^^ + help: replace it with a canonical string + | +15 | - syntax = "proto\x32"; +15 | + syntax = "proto2"; + | + +encountered 1 warning diff --git a/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt new file mode 100644 index 00000000..9e58fb7d --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt @@ -0,0 +1,14 @@ +warning: non-canonical string literal in `syntax` declaration + --> testdata/parser/syntax/proto2_split.proto:15:10 + | +15 | syntax = "proto" "2"; + | ^^^^^^^^^^^ + help: replace it with a canonical string + | +15 | - syntax = "proto" "2"; +15 | + syntax = "proto2"; + | + = note: Protobuf implicitly concatenates adjacent string literals, like C or + Python; this can lead to surprising behavior + +encountered 1 warning diff --git a/experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt new file mode 100644 index 00000000..ec4683ae --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt @@ -0,0 +1,8 @@ +error: unrecognized `syntax` declaration value + --> testdata/parser/syntax/proto4.proto:15:10 + | +15 | syntax = "proto4"; + | ^^^^^^^^ + = note: permitted values: "proto2", "proto3" + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/syntax_2023.proto b/experimental/parser/testdata/parser/syntax/syntax_2023.proto new file mode 100644 index 00000000..7991c836 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/syntax_2023.proto @@ -0,0 +1,17 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "2023"; + +package test; diff --git a/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt new file mode 100644 index 00000000..c6190ae9 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt @@ -0,0 +1,8 @@ +error: unexpected edition in `syntax` declaration + --> testdata/parser/syntax/syntax_2023.proto:15:10 + | +15 | syntax = "2023"; + | ^^^^^^ + = note: permitted values: "proto2", "proto3" + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/syntax_2023.proto.yaml b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.yaml new file mode 100644 index 00000000..7e4a4e97 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.yaml @@ -0,0 +1,3 @@ +decls: + - syntax: { kind: KIND_SYNTAX, value.literal.string_value: "2023" } + - package.path.components: [{ ident: "test" }] diff --git a/experimental/parser/testdata/parser/syntax/unquoted.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/unquoted.proto.stderr.txt new file mode 100644 index 00000000..7c33bcf0 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/unquoted.proto.stderr.txt @@ -0,0 +1,11 @@ +error: the value of a `syntax` declaration must be a string literal + --> testdata/parser/syntax/unquoted.proto:15:10 + | +15 | syntax = proto2; + | ^^^^^^ + help: add quotes to make this a string literal + | +15 | syntax = "proto2"; + | + + + +encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/unquoted_edition.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/unquoted_edition.proto.stderr.txt new file mode 100644 index 00000000..366132b3 --- /dev/null +++ b/experimental/parser/testdata/parser/syntax/unquoted_edition.proto.stderr.txt @@ -0,0 +1,11 @@ +error: the value of a `edition` declaration must be a string literal + --> testdata/parser/syntax/unquoted_edition.proto:15:11 + | +15 | edition = 2023; + | ^^^^ + help: add quotes to make this a string literal + | +15 | edition = "2023"; + | + + + +encountered 1 error diff --git a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt index a8b5c7b4..2f5c7593 100644 --- a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt +++ b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt @@ -1,3 +1,90 @@ +error: unexpected non-comparable type in map key type + --> testdata/parser/type/generic.proto:21:9 + | +21 | map x2 = 2; + | ^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + +error: unexpected non-comparable type in map key type + --> testdata/parser/type/generic.proto:22:9 + | +22 | map x3 = 3; + | ^^^^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + +error: unexpected type in map value type + --> testdata/parser/type/generic.proto:23:17 + | +23 | map> x4 = 4; + | ^^^^^^^^^^^^^^^^^^^ expected type name + +error: generic types other than `map` are not supported + --> testdata/parser/type/generic.proto:25:5 + | +25 | list x5 = 5; + | ^^^^ + +error: generic types other than `map` are not supported + --> testdata/parser/type/generic.proto:26:5 + | +26 | void<> x6 = 6; + | ^^^^ + +error: generic types other than `map` are not supported + --> testdata/parser/type/generic.proto:28:5 + | +28 | my.Map x7 = 7; + | ^^^^^^ + +error: unexpected type after `optional` + --> testdata/parser/type/generic.proto:30:14 + | +30 | optional map x8 = 8; + | ^^^^^^^^^^^^^^^^^^^ expected type name + +error: unexpected type after `repeated` + --> testdata/parser/type/generic.proto:31:14 + | +31 | repeated map x9 = 9; + | ^^^^^^^^^^^^^^^^^^^ expected type name + +error: unexpected type after `required` + --> testdata/parser/type/generic.proto:32:14 + | +32 | required map x10 = 10; + | ^^^^^^^^^^^^^^^^^^^ expected type name + +error: unexpected `repeated` in map value type + --> testdata/parser/type/generic.proto:34:17 + | +34 | map x11 = 11; + | ^^^^^^^^ + +error: unexpected non-comparable type in map key type + --> testdata/parser/type/generic.proto:35:9 + | +35 | map x12 = 12; + | ^^^^^^^^^^^^^^^^ + = help: a map key must be one of the following types: int32, int64, uint32, + uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, + string + +error: unexpected `required` in map value type + --> testdata/parser/type/generic.proto:35:27 + | +35 | map x12 = 12; + | ^^^^^^^^ + +error: generic types other than `map` are not supported + --> testdata/parser/type/generic.proto:37:5 + | +37 | set x13 = 13; + | ^^^ + error: unexpected type name in type parameters --> testdata/parser/type/generic.proto:37:13 | @@ -6,4 +93,46 @@ error: unexpected type name in type parameters | | | note: assuming a missing `,` here -encountered 1 error +error: generic types other than `map` are not supported + --> testdata/parser/type/generic.proto:38:5 + | +38 | set x14 = 14; + | ^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/generic.proto:42:12 + | +42 | rpc X1(map) returns (map) {} + | ^^^^^^^^^^^^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/generic.proto:42:42 + | +42 | rpc X1(map) returns (map) {} + | ^^^^^^^^^^^^^^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/generic.proto:43:12 + | +43 | rpc X2(list) returns (stream .void) {} + | ^^^^^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/generic.proto:43:42 + | +43 | rpc X2(list) returns (stream .void) {} + | ^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/generic.proto:44:12 + | +44 | rpc X3(map) returns (stream map) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/generic.proto:44:58 + | +44 | rpc X3(map) returns (stream map) {} + | ^^^^^^^^^^^^^^^^^^^ + +encountered 21 errors diff --git a/experimental/parser/testdata/parser/type/repeated.proto.stderr.txt b/experimental/parser/testdata/parser/type/repeated.proto.stderr.txt index b096bd89..6372e38e 100644 --- a/experimental/parser/testdata/parser/type/repeated.proto.stderr.txt +++ b/experimental/parser/testdata/parser/type/repeated.proto.stderr.txt @@ -1,19 +1,181 @@ +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:20:14 + | +20 | optional optional M x1 = 1; + | -------- ^^^^^^^^ help: consider removing this + | | + | first one is here + +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:21:14 + | +21 | repeated optional M x2 = 2; + | -------- ^^^^^^^^ help: consider removing this + | | + | first one is here + +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:22:14 + | +22 | required optional M x3 = 3; + | -------- ^^^^^^^^ help: consider removing this + | | + | first one is here + +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:23:14 + | +23 | repeated repeated M x4 = 4; + | -------- ^^^^^^^^ help: consider removing this + | | + | first one is here + +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:24:14 + | +24 | repeated stream M x5 = 5; + | -------- ^^^^^^ help: consider removing this + | | + | first one is here + +error: the `stream` modifier may only appear in a method signature + --> testdata/parser/type/repeated.proto:25:5 + | +25 | stream stream M x6 = 6; + | ^^^^^^ + +error: encountered more than one type modifier + --> testdata/parser/type/repeated.proto:25:12 + | +25 | stream stream M x6 = 6; + | ------ ^^^^^^ help: consider removing this + | | + | first one is here + +error: only the `stream` modifier may appear in method parameter list + --> testdata/parser/type/repeated.proto:29:12 + | +29 | rpc X1(required optional M) returns (stream optional M) {} + | ^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:29:21 + | +29 | rpc X1(required optional M) returns (stream optional M) {} + | ^^^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:29:49 + | +29 | rpc X1(required optional M) returns (stream optional M) {} + | ^^^^^^^^^^ + +error: only the `stream` modifier may appear in method parameter list + --> testdata/parser/type/repeated.proto:30:12 + | +30 | rpc X2(repeated repeated test.M) returns (repeated stream .test.M) {} + | ^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:30:21 + | +30 | rpc X2(repeated repeated test.M) returns (repeated stream .test.M) {} + | ^^^^^^^^^^^^^^^ + +error: only the `stream` modifier may appear in method return type + --> testdata/parser/type/repeated.proto:30:47 + | +30 | rpc X2(repeated repeated test.M) returns (repeated stream .test.M) {} + | ^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:30:56 + | +30 | rpc X2(repeated repeated test.M) returns (repeated stream .test.M) {} + | ^^^^^^^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:31:19 + | +31 | rpc X3(stream stream .test.M) returns (stream repeated M) {} + | ^^^^^^^^^^^^^^ + +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:31:51 + | +31 | rpc X3(stream stream .test.M) returns (stream repeated M) {} + | ^^^^^^^^^^ + +error: only the `stream` modifier may appear in method parameter list + --> testdata/parser/type/repeated.proto:33:12 + | +33 | rpc X4(required optional M) returns stream optional M {} + | ^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:33:21 + | +33 | rpc X4(required optional M) returns stream optional M {} + | ^^^^^^^^^^ + error: missing `(...)` around method return type --> testdata/parser/type/repeated.proto:33:41 | 33 | rpc X4(required optional M) returns stream optional M {} | ^^^^^^^^^^^^^^^^^ help: replace this with `(stream optional M)` +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:33:48 + | +33 | rpc X4(required optional M) returns stream optional M {} + | ^^^^^^^^^^ + +error: only the `stream` modifier may appear in method parameter list + --> testdata/parser/type/repeated.proto:34:12 + | +34 | rpc X5(repeated repeated test.M) returns repeated stream .test.M {} + | ^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:34:21 + | +34 | rpc X5(repeated repeated test.M) returns repeated stream .test.M {} + | ^^^^^^^^^^^^^^^ + +error: only the `stream` modifier may appear in method return type + --> testdata/parser/type/repeated.proto:34:46 + | +34 | rpc X5(repeated repeated test.M) returns repeated stream .test.M {} + | ^^^^^^^^ + error: missing `(...)` around method return type --> testdata/parser/type/repeated.proto:34:46 | 34 | rpc X5(repeated repeated test.M) returns repeated stream .test.M {} | ^^^^^^^^^^^^^^^^^^^^^^^ help: replace this with `(repeated stream .test.M)` +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:34:55 + | +34 | rpc X5(repeated repeated test.M) returns repeated stream .test.M {} + | ^^^^^^^^^^^^^^ + +error: only message types may appear in method parameter list + --> testdata/parser/type/repeated.proto:35:19 + | +35 | rpc X6(stream stream .test.M) returns stream repeated M {} + | ^^^^^^^^^^^^^^ + error: missing `(...)` around method return type --> testdata/parser/type/repeated.proto:35:43 | 35 | rpc X6(stream stream .test.M) returns stream repeated M {} | ^^^^^^^^^^^^^^^^^ help: replace this with `(stream repeated M)` -encountered 3 errors +error: only message types may appear in method return type + --> testdata/parser/type/repeated.proto:35:50 + | +35 | rpc X6(stream stream .test.M) returns stream repeated M {} + | ^^^^^^^^^^ + +encountered 28 errors diff --git a/experimental/report/diff.go b/experimental/report/diff.go index a451908b..9b8da82b 100644 --- a/experimental/report/diff.go +++ b/experimental/report/diff.go @@ -87,24 +87,24 @@ func unifiedDiff(span Span, edits []Edit) (Span, []hunk) { // Partition offsets into overlapping lines. That is, this connects together // all edit spans whose end and start are not separated by a newline. - prev := &edits[0] - parts := slicesx.Partition(edits, func(_, next *Edit) bool { - if next == prev { + prev := 0 + parts := slicesx.SplitFunc(edits, func(i int, next Edit) bool { + if i == prev { return false } - chunk := src[prev.End:next.Start] + chunk := src[edits[i-1].End:next.Start] if !strings.Contains(chunk, "\n") { return false } - prev = next + prev = i return true }) var out []hunk var prevHunk int - parts(func(_ int, edits []Edit) bool { + parts(func(edits []Edit) bool { // First, figure out the start and end of the modified region. start, end := edits[0].Start, edits[0].End for _, edit := range edits[1:] { diff --git a/experimental/report/renderer.go b/experimental/report/renderer.go index f35e7fc6..ee36d9f0 100644 --- a/experimental/report/renderer.go +++ b/experimental/report/renderer.go @@ -18,12 +18,14 @@ import ( "bytes" "fmt" "io" + "math" "math/bits" "slices" "strconv" "strings" "unicode" + "github.com/bufbuild/protocompile/internal/ext/iterx" "github.com/bufbuild/protocompile/internal/ext/slicesx" "github.com/bufbuild/protocompile/internal/ext/stringsx" ) @@ -186,7 +188,8 @@ func (r *renderer) diagnostic(report *Report, d Diagnostic) { // For the other styles, we imitate the Rust compiler. See // https://github.com/rust-lang/rustc-dev-guide/blob/master/src/diagnostics.md - fmt.Fprint(r, r.ss.BoldForLevel(d.level), level, ": ", d.message, r.ss.reset) + fmt.Fprint(r, r.ss.BoldForLevel(d.level), level, ": ") + r.WriteWrapped(d.message, MaxMessageWidth) locations := make([][2]Location, len(d.snippets)) for i, snip := range d.snippets { @@ -210,13 +213,14 @@ func (r *renderer) diagnostic(report *Report, d Diagnostic) { r.margin = max(2, len(strconv.Itoa(greatestLine))) // Easier than messing with math.Log10() // Render all the diagnostic windows. - parts := slicesx.Partition(d.snippets, func(a, b *snippet) bool { - if len(a.edits) > 0 || len(b.edits) > 0 { + parts := slicesx.PartitionKey(d.snippets, func(snip snippet) any { + if len(snip.edits) > 0 { // Suggestions are always rendered in their own windows. - return true + // Return a fresh pointer, since that will always compare as + // distinct. + return new(int) } - - return a.Path() != b.Path() + return snip.Path() }) parts(func(i int, snippets []snippet) bool { @@ -261,34 +265,33 @@ func (r *renderer) diagnostic(report *Report, d Diagnostic) { fmt.Fprintf(r, "--> %s", d.inFile) } - // Render the footers. For simplicity we collect them into an array first. - footers := make([][3]string, 0, len(d.notes)+len(d.help)+len(d.debug)) - for _, note := range d.notes { - footers = append(footers, [3]string{r.ss.bRemark, "note", note}) + type footer struct { + color, label, text string } - for _, help := range d.help { - footers = append(footers, [3]string{r.ss.bRemark, "help", help}) - } - if r.ShowDebug { - for _, debug := range d.debug { - footers = append(footers, [3]string{r.ss.bError, "debug", debug}) + footers := iterx.Chain( + slicesx.Map(d.notes, func(s string) footer { return footer{r.ss.bRemark, "note", s} }), + slicesx.Map(d.help, func(s string) footer { return footer{r.ss.bRemark, "help", s} }), + slicesx.Map(d.debug, func(s string) footer { return footer{r.ss.bError, "debug", s} }), + ) + + footers(func(f footer) bool { + isDebug := f.label == "debug" + if isDebug && !r.ShowDebug { + return true } - } - for _, footer := range footers { + r.WriteString("\n") - r.WriteString(r.ss.nAccent) r.WriteSpaces(r.margin) - r.WriteString(" = ") - fmt.Fprint(r, footer[0], footer[1], ": ", r.ss.reset) - for i, line := range strings.Split(footer[2], "\n") { - if i > 0 { - r.WriteString("\n") - margin := r.margin + 3 + len(footer[1]) + 2 - r.WriteSpaces(margin) - } - r.WriteString(line) + fmt.Fprintf(r, "%s = %s%s: %s", r.ss.nAccent, f.color, f.label, r.ss.reset) + + if isDebug { + r.WriteWrapped(f.text, math.MaxInt) + } else { + r.WriteWrapped(f.text, MaxMessageWidth) } - } + + return true + }) r.WriteString(r.ss.reset) r.WriteString("\n\n") @@ -482,7 +485,7 @@ func (r *renderer) window(w *window) { // Next, we can render the underline parts. This aggregates all underlines // for the same line into rendered chunks - parts := slicesx.Partition(w.underlines, func(a, b *underline) bool { return a.line != b.line }) + parts := slicesx.PartitionKey(w.underlines, func(u underline) int { return u.line }) parts(func(_ int, part []underline) bool { cur := &info[part[0].line-w.start] cur.shouldEmit = true @@ -517,8 +520,7 @@ func (r *renderer) window(w *window) { // Now, convert the buffer into a proper string. var out strings.Builder - parts := slicesx.Partition(buf, func(a, b *byte) bool { return *a != *b }) - parts(func(_ int, line []byte) bool { + slicesx.Partition(buf)(func(_ int, line []byte) bool { level := Level(line[0]) if line[0] == 0 { out.WriteString(r.ss.reset) @@ -750,23 +752,51 @@ func (r *renderer) window(w *window) { } } for i := range info { + printable := func(r rune) bool { return !unicode.IsSpace(r) } + // At least two of the below conditions must be true for // this line to be shown. Annoyingly, go does not have a conversion // from bool to int... var score int - if strings.IndexFunc(lines[i], unicode.IsGraphic) != 0 { + if strings.IndexFunc(lines[i], printable) != -1 { score++ } - if mustEmit[i-1] { + + sameIndent := func(a, b string) bool { + if a == "" || b == "" { + return true + } + d1 := strings.IndexFunc(a, printable) + if d1 == -1 { + d1 = len(a) + } + d2 := strings.IndexFunc(b, printable) + if d2 == -1 { + d2 = len(b) + } + return a[:d1] == b[:d2] + } + + if mustEmit[i-1] && sameIndent(lines[i-1], lines[i]) { score++ } - if mustEmit[i+1] { + if mustEmit[i+1] && sameIndent(lines[i+1], lines[i]) { score++ } if score >= 2 { info[i].shouldEmit = true } } + // Ensure that there are no single-line elided chunks. + // This necessarily results in a fixed point after one iteration. + for i := range info { + mustEmit[i] = info[i].shouldEmit + } + for i := range info { + if mustEmit[i-1] && mustEmit[i+1] { + info[i].shouldEmit = true + } + } lastEmit := w.start for i, line := range lines { @@ -807,7 +837,7 @@ func (r *renderer) window(w *window) { slashAt = len(prevSidebar) - 1 } - r.WriteString(r.sidebar(sidebarLen, lineno, slashAt, cur.sidebar)) + r.WriteString(r.sidebar(sidebarLen, lastEmit+1, slashAt, info[lastEmit-w.start].sidebar)) } // Ok, we are definitely printing this line out. @@ -906,7 +936,7 @@ func (r *renderer) suggestion(snip snippet) { r.WriteString(r.ss.nAccent) r.WriteSpaces(r.margin) r.WriteString("help: ") - r.WriteString(snip.message) + r.WriteWrapped(snip.message, MaxMessageWidth) // Add a blank line after the file. This gives the diagnostic window some // visual breathing room. diff --git a/experimental/report/span.go b/experimental/report/span.go index 56afa12d..48a95410 100644 --- a/experimental/report/span.go +++ b/experimental/report/span.go @@ -27,9 +27,6 @@ import ( "github.com/bufbuild/protocompile/internal/iter" ) -// TabstopWidth is the size we render all tabstops as. -const TabstopWidth int = 4 - // Spanner is any type with a [Span]. type Spanner interface { // Should return the zero [Span] to indicate that it does not contribute @@ -66,6 +63,59 @@ func (s Span) Text() string { return s.File.Text()[s.Start:s.End] } +// Indentation calculates the indentation at this span. +// +// Indentation is defined as the substring between the last newline in +// [Span.Before] and the first non-Pattern_White_Space after that newline. +func (s Span) Indentation() string { + nl := strings.LastIndexByte(s.Before(), '\n') + 1 + margin := strings.IndexFunc(s.File.Text()[nl:], func(r rune) bool { + return !unicode.In(r, unicode.Pattern_White_Space) + }) + return s.File.Text()[nl : nl+margin] +} + +// Before returns all text before this span. +func (s Span) Before() string { + return s.File.Text()[:s.Start] +} + +// Before returns all text after this span. +func (s Span) After() string { + return s.File.Text()[s.End:] +} + +// GrowLeft returns a new span which contains the largest suffix of [Span.Before] +// which match p. +func (s Span) GrowLeft(p func(r rune) bool) Span { + for { + r, sz := utf8.DecodeLastRuneInString(s.Before()) + if r == utf8.RuneError || !p(r) { + break + } + s.Start -= sz + } + return s +} + +// GrowLeft returns a new span which contains the largest prefix of [Span.After] +// which match p. +func (s Span) GrowRight(p func(r rune) bool) Span { + for { + r, sz := utf8.DecodeRuneInString(s.After()) + if r == utf8.RuneError || !p(r) { + break + } + s.End += sz + } + return s +} + +// Len returns the length of this span, in bytes. +func (s Span) Len() int { + return s.End - s.Start +} + // StartLoc returns the start location for this span. func (s Span) StartLoc() Location { return s.Location(s.Start) diff --git a/experimental/report/testdata/i18n.yaml.color.txt b/experimental/report/testdata/i18n.yaml.color.txt index ba46dde7..a260171f 100755 --- a/experimental/report/testdata/i18n.yaml.color.txt +++ b/experimental/report/testdata/i18n.yaml.color.txt @@ -1,4 +1,4 @@ -⟨b.red⟩error: emoji, CJK, bidi⟨reset⟩ +⟨b.red⟩error: emoji, CJK, bidi ⟨blu⟩ --> foo.proto:5:9 | ⟨blu⟩ 5 | ⟨reset⟩message 🐈⬛ { @@ -10,7 +10,7 @@ ⟨blu⟩ 1 | ⟨reset⟩import "חתול שחור.proto"; ⟨blu⟩ | ⟨reset⟩ ⟨b.blu⟩---------------⟨reset⟩ ⟨b.blu⟩bidi works if it's quoted, at least⟨reset⟩ -⟨b.red⟩error: bidi (Arabic, Hebrew, Farsi, etc) is broken in some contexts⟨reset⟩ +⟨b.red⟩error: bidi (Arabic, Hebrew, Farsi, etc) is broken in some contexts ⟨blu⟩ --> foo.proto:7:10 | ⟨blu⟩ 7 | ⟨reset⟩ string القطة السوداء = 2; diff --git a/experimental/report/testdata/multi-file.yaml.color.txt b/experimental/report/testdata/multi-file.yaml.color.txt index 282eeedf..c6735fdc 100755 --- a/experimental/report/testdata/multi-file.yaml.color.txt +++ b/experimental/report/testdata/multi-file.yaml.color.txt @@ -1,4 +1,4 @@ -⟨b.red⟩error: two files⟨reset⟩ +⟨b.red⟩error: two files ⟨blu⟩ --> foo.proto:3:9 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; @@ -11,7 +11,7 @@ ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz2; ⟨blu⟩ | ⟨reset⟩ ⟨b.blu⟩-------⟨reset⟩ ⟨b.blu⟩baz⟨reset⟩ -⟨b.red⟩error: three files⟨reset⟩ +⟨b.red⟩error: three files ⟨blu⟩ --> foo.proto:3:9 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; diff --git a/experimental/report/testdata/multi-underline.yaml.color.txt b/experimental/report/testdata/multi-underline.yaml.color.txt index 29dd3a16..03ef0457 100755 --- a/experimental/report/testdata/multi-underline.yaml.color.txt +++ b/experimental/report/testdata/multi-underline.yaml.color.txt @@ -1,14 +1,13 @@ -⟨b.red⟩error: `size_t` is not a built-in Protobuf type⟨reset⟩ +⟨b.red⟩error: `size_t` is not a built-in Protobuf type ⟨blu⟩ --> foo.proto:6:12 | ⟨blu⟩ 1 | ⟨reset⟩syntax = "proto4" ⟨blu⟩ | ⟨reset⟩ ⟨b.blu⟩--------⟨reset⟩ ⟨b.blu⟩syntax version specified here -⟨blu⟩ 2 | ⟨reset⟩ ⟨blu⟩... ⟨blu⟩ 6 | ⟨reset⟩ required size_t x = 0; ⟨blu⟩ | ⟨reset⟩ ⟨b.red⟩^^^^^⟨reset⟩ ⟨b.red⟩⟨reset⟩ -⟨b.ylw⟩warning: these are pretty bad names⟨reset⟩ +⟨b.ylw⟩warning: these are pretty bad names ⟨blu⟩ --> foo.proto:3:9 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; diff --git a/experimental/report/testdata/multi-underline.yaml.fancy.txt b/experimental/report/testdata/multi-underline.yaml.fancy.txt index 3c824c10..256bf7b6 100755 --- a/experimental/report/testdata/multi-underline.yaml.fancy.txt +++ b/experimental/report/testdata/multi-underline.yaml.fancy.txt @@ -3,7 +3,6 @@ error: `size_t` is not a built-in Protobuf type | 1 | syntax = "proto4" | -------- syntax version specified here - 2 | ... 6 | required size_t x = 0; | ^^^^^ diff --git a/experimental/report/testdata/multiline.yaml.color.txt b/experimental/report/testdata/multiline.yaml.color.txt index b6d8c717..556de145 100755 --- a/experimental/report/testdata/multiline.yaml.color.txt +++ b/experimental/report/testdata/multiline.yaml.color.txt @@ -1,4 +1,4 @@ -⟨b.ylw⟩warning: whole block⟨reset⟩ +⟨b.ylw⟩warning: whole block ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨reset⟩message Blah { @@ -6,7 +6,7 @@ ⟨blu⟩12 | ⟨b.ylw⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.ylw⟩\_^ this block⟨reset⟩ -⟨b.ylw⟩warning: nested blocks⟨reset⟩ +⟨b.ylw⟩warning: nested blocks ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨reset⟩message Blah { @@ -18,20 +18,20 @@ ⟨blu⟩12 | ⟨b.ylw⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.ylw⟩\___^ this block⟨reset⟩ -⟨b.ylw⟩warning: parallel blocks⟨reset⟩ +⟨b.ylw⟩warning: parallel blocks ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨reset⟩message Blah { ⟨blu⟩ 6 | ⟨b.ylw⟩| ⟨reset⟩ required size_t x = 0; ⟨blu⟩ 7 | ⟨b.ylw⟩| ⟨reset⟩ message Bonk { ⟨blu⟩ | ⟨b.ylw⟩\__^ this block -⟨blu⟩... ⟨b.blu⟩ +⟨blu⟩... ⟨b.ylw⟩| ⟨blu⟩11 | ⟨b.blu⟩ ⟨reset⟩ } ⟨blu⟩ | ⟨b.blu⟩ ___- ⟨blu⟩12 | ⟨b.blu⟩/ ⟨reset⟩} ⟨blu⟩ | ⟨b.blu⟩\_- and this block⟨reset⟩ -⟨b.ylw⟩warning: nested blocks same start⟨reset⟩ +⟨b.ylw⟩warning: nested blocks same start ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨b.blu⟩/ ⟨reset⟩message Blah { @@ -41,7 +41,7 @@ ⟨blu⟩12 | ⟨b.ylw⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.ylw⟩\___^ this block⟨reset⟩ -⟨b.ylw⟩warning: nested blocks same end⟨reset⟩ +⟨b.ylw⟩warning: nested blocks same end ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨reset⟩message Blah { @@ -52,7 +52,7 @@ ⟨blu⟩ | ⟨b.ylw⟩\___^ this block ⟨blu⟩ | ⟨b.ylw⟩ ⟨b.blu⟩\_- and this block⟨reset⟩ -⟨b.ylw⟩warning: nested overlap⟨reset⟩ +⟨b.ylw⟩warning: nested overlap ⟨blu⟩ --> foo.proto:5:1 | ⟨blu⟩ 5 | ⟨b.ylw⟩/ ⟨reset⟩message Blah { @@ -64,7 +64,7 @@ ⟨blu⟩12 | ⟨b.blu⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.blu⟩\_- and this block⟨reset⟩ -⟨b.ylw⟩warning: nesting just the braces⟨reset⟩ +⟨b.ylw⟩warning: nesting just the braces ⟨blu⟩ --> foo.proto:5:15 | ⟨blu⟩ 5 | ⟨b.ylw⟩ ⟨reset⟩message Blah { @@ -78,7 +78,7 @@ ⟨blu⟩12 | ⟨b.ylw⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.ylw⟩\___^ this block⟨reset⟩ -⟨b.ylw⟩warning: nesting just the braces same start⟨reset⟩ +⟨b.ylw⟩warning: nesting just the braces same start ⟨blu⟩ --> foo.proto:5:15 | ⟨blu⟩ 5 | ⟨b.ylw⟩ ⟨b.blu⟩ ⟨reset⟩message Blah { @@ -90,7 +90,7 @@ ⟨blu⟩12 | ⟨b.ylw⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.ylw⟩\___^ this block⟨reset⟩ -⟨b.ylw⟩warning: nesting just the braces same start (2)⟨reset⟩ +⟨b.ylw⟩warning: nesting just the braces same start (2) ⟨blu⟩ --> foo.proto:5:15 | ⟨blu⟩ 5 | ⟨b.blu⟩ ⟨b.ylw⟩ ⟨reset⟩message Blah { @@ -102,7 +102,7 @@ ⟨blu⟩12 | ⟨b.blu⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.blu⟩\___- this block⟨reset⟩ -⟨b.ylw⟩warning: braces nesting overlap⟨reset⟩ +⟨b.ylw⟩warning: braces nesting overlap ⟨blu⟩ --> foo.proto:5:15 | ⟨blu⟩ 5 | ⟨b.ylw⟩ ⟨reset⟩message Blah { @@ -116,7 +116,7 @@ ⟨blu⟩12 | ⟨b.blu⟩| ⟨reset⟩} ⟨blu⟩ | ⟨b.blu⟩\_- and this block⟨reset⟩ -⟨b.ylw⟩warning: braces nesting overlap (2)⟨reset⟩ +⟨b.ylw⟩warning: braces nesting overlap (2) ⟨blu⟩ --> foo.proto:7:17 | ⟨blu⟩ 5 | ⟨b.blu⟩ ⟨reset⟩message Blah { diff --git a/experimental/report/testdata/multiline.yaml.fancy.txt b/experimental/report/testdata/multiline.yaml.fancy.txt index 765b768c..a97257c8 100755 --- a/experimental/report/testdata/multiline.yaml.fancy.txt +++ b/experimental/report/testdata/multiline.yaml.fancy.txt @@ -25,7 +25,7 @@ warning: parallel blocks 6 | | required size_t x = 0; 7 | | message Bonk { | \__^ this block -... +... | 11 | } | ___- 12 | / } diff --git a/experimental/report/testdata/no-snippets.yaml b/experimental/report/testdata/no-snippets.yaml index 62360465..6b718a9c 100644 --- a/experimental/report/testdata/no-snippets.yaml +++ b/experimental/report/testdata/no-snippets.yaml @@ -18,6 +18,9 @@ diagnostics: - message: "system not supported" level: LEVEL_ERROR + - message: "this diagnostic message is comically long to illustrate message wrapping; real diagnostics should probably avoid doing this" + level: LEVEL_ERROR + - message: 'could not open file "foo.proto": os error 2: no such file or directory' level: LEVEL_ERROR in_file: foo.proto @@ -28,3 +31,15 @@ diagnostics: notes: ["that means that the file is screaming"] help: ["you should delete it to put it out of its misery"] debug: ["0xaaaaaaaaaaaaaaaa"] + + - message: "very long footers" + level: LEVEL_REMARK + in_file: foo.proto + notes: + - "this footer is a very very very very very very very very very very very very very very very very very very very very very very long footer" + - "this one is also long, and it's also supercalifragilistcexpialidocious, leading to a very early break" + help: + - "this help is very long (and triggers the same word-wrapping code path)" + - "this one contains a newline\nwhich overrides the default word wrap behavior (but this line is wrapped naturally)" + debug: + - "debug lines are never wrapped, no matter how crazy long they are, since they can contain stack traces" diff --git a/experimental/report/testdata/no-snippets.yaml.color.txt b/experimental/report/testdata/no-snippets.yaml.color.txt index 10b9e866..1e5a4b27 100755 --- a/experimental/report/testdata/no-snippets.yaml.color.txt +++ b/experimental/report/testdata/no-snippets.yaml.color.txt @@ -1,13 +1,30 @@ -⟨b.red⟩error: system not supported⟨reset⟩⟨reset⟩ +⟨b.red⟩error: system not supported⟨reset⟩ -⟨b.red⟩error: could not open file "foo.proto": os error 2: no such file or directory⟨reset⟩ +⟨b.red⟩error: this diagnostic message is comically long to illustrate message wrapping; + real diagnostics should probably avoid doing this⟨reset⟩ + +⟨b.red⟩error: could not open file "foo.proto": os error 2: no such file or directory ⟨blu⟩ --> foo.proto⟨reset⟩ -⟨b.ylw⟩warning: file consists only of the byte `0xaa`⟨reset⟩ +⟨b.ylw⟩warning: file consists only of the byte `0xaa` +⟨blu⟩ --> foo.proto + ⟨blu⟩ = ⟨b.cyn⟩note: ⟨reset⟩that means that the file is screaming + ⟨blu⟩ = ⟨b.cyn⟩help: ⟨reset⟩you should delete it to put it out of its misery + ⟨blu⟩ = ⟨b.red⟩debug: ⟨reset⟩0xaaaaaaaaaaaaaaaa⟨reset⟩ + +⟨b.cyn⟩remark: very long footers ⟨blu⟩ --> foo.proto -⟨blu⟩ = ⟨b.cyn⟩note: ⟨reset⟩that means that the file is screaming -⟨blu⟩ = ⟨b.cyn⟩help: ⟨reset⟩you should delete it to put it out of its misery -⟨blu⟩ = ⟨b.red⟩debug: ⟨reset⟩0xaaaaaaaaaaaaaaaa⟨reset⟩ + ⟨blu⟩ = ⟨b.cyn⟩note: ⟨reset⟩this footer is a very very very very very very very very very very + very very very very very very very very very very very very long + footer + ⟨blu⟩ = ⟨b.cyn⟩note: ⟨reset⟩this one is also long, and it's also + supercalifragilistcexpialidocious, leading to a very early break + ⟨blu⟩ = ⟨b.cyn⟩help: ⟨reset⟩this help is very long (and triggers the same word-wrapping code + path) + ⟨blu⟩ = ⟨b.cyn⟩help: ⟨reset⟩this one contains a newline + which overrides the default word wrap behavior (but this line is + wrapped naturally) + ⟨blu⟩ = ⟨b.red⟩debug: ⟨reset⟩debug lines are never wrapped, no matter how crazy long they are, since they can contain stack traces⟨reset⟩ -⟨b.red⟩encountered 2 errors and 1 warning +⟨b.red⟩encountered 3 errors and 1 warning ⟨reset⟩ \ No newline at end of file diff --git a/experimental/report/testdata/no-snippets.yaml.fancy.txt b/experimental/report/testdata/no-snippets.yaml.fancy.txt index d5640c18..689ecb2d 100755 --- a/experimental/report/testdata/no-snippets.yaml.fancy.txt +++ b/experimental/report/testdata/no-snippets.yaml.fancy.txt @@ -1,5 +1,8 @@ error: system not supported +error: this diagnostic message is comically long to illustrate message wrapping; + real diagnostics should probably avoid doing this + error: could not open file "foo.proto": os error 2: no such file or directory --> foo.proto @@ -9,4 +12,18 @@ warning: file consists only of the byte `0xaa` = help: you should delete it to put it out of its misery = debug: 0xaaaaaaaaaaaaaaaa -encountered 2 errors and 1 warning +remark: very long footers + --> foo.proto + = note: this footer is a very very very very very very very very very very + very very very very very very very very very very very very long + footer + = note: this one is also long, and it's also + supercalifragilistcexpialidocious, leading to a very early break + = help: this help is very long (and triggers the same word-wrapping code + path) + = help: this one contains a newline + which overrides the default word wrap behavior (but this line is + wrapped naturally) + = debug: debug lines are never wrapped, no matter how crazy long they are, since they can contain stack traces + +encountered 3 errors and 1 warning diff --git a/experimental/report/testdata/no-snippets.yaml.simple.txt b/experimental/report/testdata/no-snippets.yaml.simple.txt index fb79beac..ea6c45eb 100755 --- a/experimental/report/testdata/no-snippets.yaml.simple.txt +++ b/experimental/report/testdata/no-snippets.yaml.simple.txt @@ -1,3 +1,5 @@ error: system not supported +error: this diagnostic message is comically long to illustrate message wrapping; real diagnostics should probably avoid doing this error: foo.proto: could not open file "foo.proto": os error 2: no such file or directory warning: foo.proto: file consists only of the byte `0xaa` +remark: foo.proto: very long footers diff --git a/experimental/report/testdata/single-line.yaml.color.txt b/experimental/report/testdata/single-line.yaml.color.txt index 31bcd0a9..0dbb754b 100755 --- a/experimental/report/testdata/single-line.yaml.color.txt +++ b/experimental/report/testdata/single-line.yaml.color.txt @@ -1,22 +1,22 @@ -⟨b.cyn⟩remark: "proto4" isn't real, it can't hurt you⟨reset⟩ +⟨b.cyn⟩remark: "proto4" isn't real, it can't hurt you ⟨blu⟩ --> foo.proto:1:10 | ⟨blu⟩ 1 | ⟨reset⟩syntax = "proto4" ⟨blu⟩ | ⟨reset⟩ ⟨b.cyn⟩^^^^^^^^^⟨reset⟩ ⟨b.cyn⟩help: change this to "proto5"⟨reset⟩ -⟨b.red⟩error: missing `;`⟨reset⟩ +⟨b.red⟩error: missing `;` ⟨blu⟩ --> foo.proto:1:18 | ⟨blu⟩ 1 | ⟨reset⟩syntax = "proto4" ⟨blu⟩ | ⟨reset⟩ ⟨b.red⟩^⟨reset⟩ ⟨b.red⟩here⟨reset⟩ -⟨b.cyn⟩remark: EOF⟨reset⟩ +⟨b.cyn⟩remark: EOF ⟨blu⟩ --> foo.proto:7:2 | ⟨blu⟩ 7 | ⟨reset⟩} ⟨blu⟩ | ⟨reset⟩ ⟨b.cyn⟩^⟨reset⟩ ⟨b.cyn⟩here⟨reset⟩ -⟨b.red⟩error: package⟨reset⟩ +⟨b.red⟩error: package ⟨blu⟩ --> foo.proto:3:1 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; @@ -24,7 +24,7 @@ ⟨blu⟩ | ⟨b.red⟩| ⟨blu⟩ | ⟨b.red⟩package⟨reset⟩ -⟨b.red⟩error: this is an overlapping error⟨reset⟩ +⟨b.red⟩error: this is an overlapping error ⟨blu⟩ --> foo.proto:3:1 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; @@ -32,7 +32,7 @@ ⟨blu⟩ | ⟨b.red⟩| ⟨blu⟩ | ⟨b.red⟩package⟨reset⟩ -⟨b.red⟩error: P A C K A G E⟨reset⟩ +⟨b.red⟩error: P A C K A G E ⟨blu⟩ --> foo.proto:3:1 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; @@ -42,7 +42,7 @@ ⟨blu⟩ | ⟨b.blu⟩| ⟨blu⟩ | ⟨b.blu⟩help: ck⟨reset⟩ -⟨b.red⟩error: P A C K A G E (different order)⟨reset⟩ +⟨b.red⟩error: P A C K A G E (different order) ⟨blu⟩ --> foo.proto:3:3 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; @@ -52,7 +52,7 @@ ⟨blu⟩ | ⟨b.blu⟩| ⟨blu⟩ | ⟨b.blu⟩help: p⟨reset⟩ -⟨b.red⟩error: P A C K A G E (single letters)⟨reset⟩ +⟨b.red⟩error: P A C K A G E (single letters) ⟨blu⟩ --> foo.proto:3:1 | ⟨blu⟩ 3 | ⟨reset⟩package abc.xyz; diff --git a/experimental/report/testdata/suggestions.yaml.color.txt b/experimental/report/testdata/suggestions.yaml.color.txt index 3e2017c6..79d478f3 100644 --- a/experimental/report/testdata/suggestions.yaml.color.txt +++ b/experimental/report/testdata/suggestions.yaml.color.txt @@ -1,11 +1,11 @@ -⟨b.cyn⟩remark: let protocompile pick a syntax for you⟨reset⟩ +⟨b.cyn⟩remark: let protocompile pick a syntax for you ⟨blu⟩ --> foo.proto:1:1 ⟨blu⟩ help: delete this | ⟨blu⟩ 1 | ⟨b.red⟩-⟨red⟩ syntax = "proto3"; ⟨blu⟩ | ⟨reset⟩ -⟨b.cyn⟩remark: let protocompile pick a syntax for you⟨reset⟩ +⟨b.cyn⟩remark: let protocompile pick a syntax for you ⟨blu⟩ --> foo.proto:1:10 ⟨blu⟩ help: delete this | @@ -13,7 +13,7 @@ ⟨blu⟩ 1 | ⟨b.grn⟩+⟨grn⟩ syntax = ; ⟨blu⟩ | ⟨reset⟩ -⟨b.ylw⟩warning: services should have a `Service` suffix⟨reset⟩ +⟨b.ylw⟩warning: services should have a `Service` suffix ⟨blu⟩ --> foo.proto:5:9 | ⟨blu⟩ 5 | ⟨reset⟩service Foo { @@ -23,7 +23,7 @@ ⟨blu⟩ 5 | ⟨reset⟩service Foo⟨grn⟩Service⟨reset⟩ { ⟨blu⟩ | ⟨reset⟩ ⟨b.grn⟩+++++++⟨reset⟩ ⟨reset⟩ -⟨b.red⟩error: missing (...) around return type⟨reset⟩ +⟨b.red⟩error: missing (...) around return type ⟨blu⟩ --> foo.proto:6:31 | ⟨blu⟩ 6 | ⟨reset⟩ rpc Get(GetRequest) returns GetResponse; @@ -33,7 +33,7 @@ ⟨blu⟩ 6 | ⟨reset⟩ rpc Get(GetRequest) returns ⟨grn⟩(⟨reset⟩GetResponse⟨grn⟩)⟨reset⟩; ⟨blu⟩ | ⟨reset⟩ ⟨b.grn⟩+⟨reset⟩ ⟨b.grn⟩+⟨reset⟩ ⟨reset⟩ -⟨b.red⟩error: method options must go in a block⟨reset⟩ +⟨b.red⟩error: method options must go in a block ⟨blu⟩ --> foo.proto:7:45 | ⟨blu⟩ 7 | ⟨reset⟩ rpc Put(PutRequest) returns (PutResponse) [foo = bar]; @@ -46,7 +46,7 @@ ⟨blu⟩ 9 | ⟨b.grn⟩+⟨grn⟩ } ⟨blu⟩ | ⟨reset⟩ -⟨b.red⟩error: delete some stuff⟨reset⟩ +⟨b.red⟩error: delete some stuff ⟨blu⟩ --> foo.proto:5:1 ⟨blu⟩ help: | @@ -56,7 +56,7 @@ ⟨blu⟩ 8 | ⟨b.red⟩-⟨red⟩ } ⟨blu⟩ | ⟨reset⟩ -⟨b.red⟩error: delete this method⟨reset⟩ +⟨b.red⟩error: delete this method ⟨blu⟩ --> foo.proto:5:1 ⟨blu⟩ help: | diff --git a/experimental/report/testdata/tabstops.yaml.color.txt b/experimental/report/testdata/tabstops.yaml.color.txt index 4de37670..2a64ae06 100755 --- a/experimental/report/testdata/tabstops.yaml.color.txt +++ b/experimental/report/testdata/tabstops.yaml.color.txt @@ -1,4 +1,4 @@ -⟨b.ylw⟩warning: tabstop⟨reset⟩ +⟨b.ylw⟩warning: tabstop ⟨blu⟩ --> foo.proto:6:9 | ⟨blu⟩ 6 | ⟨reset⟩ field @@ -6,7 +6,7 @@ ⟨blu⟩ | ⟨b.blu⟩| ⟨blu⟩ | ⟨b.blu⟩specifically these⟨reset⟩ -⟨b.ylw⟩warning: partial tabstop⟨reset⟩ +⟨b.ylw⟩warning: partial tabstop ⟨blu⟩ --> foo.proto:7:2 | ⟨blu⟩ 7 | ⟨reset⟩ field diff --git a/experimental/report/width.go b/experimental/report/width.go index 44f14c31..5b1e8481 100644 --- a/experimental/report/width.go +++ b/experimental/report/width.go @@ -21,6 +21,17 @@ import ( "unicode/utf8" "github.com/rivo/uniseg" + + "github.com/bufbuild/protocompile/internal/ext/stringsx" + "github.com/bufbuild/protocompile/internal/iter" +) + +const ( + // TabstopWidth is the size we render all tabstops as. + TabstopWidth int = 4 + // MaxMessageWidth is the maximum width of a diagnostic message before it is + // word-wrapped, to try to keep everything within the bounds of a terminal. + MaxMessageWidth int = 80 ) // NonPrint defines whether or not a rune is considered "unprintable for the @@ -30,6 +41,50 @@ func NonPrint(r rune) bool { return !strings.ContainsRune(" \r\t\n", r) && !unicode.IsPrint(r) } +// wordWrap returns an iterator over chunks of s that are no wider than width, +// which can be printed as their own lines. +func wordWrap(text string, width int) iter.Seq[string] { + return func(yield func(string) bool) { + // Split along lines first, since those are hard breaks we don't plan + // to change. + stringsx.Lines(text)(func(line string) bool { + var nextIsSpace bool + var column, cursor int + + stringsx.PartitionKey(line, unicode.IsSpace)(func(start int, chunk string) bool { + isSpace := nextIsSpace + nextIsSpace = !nextIsSpace + + if isSpace && column == 0 { + return true + } + + w := stringWidth(column, chunk, true, nil) - column + if column+w <= width { + column += w + return true + } + + if !yield(strings.TrimSpace(line[cursor:start])) { + return false + } + + if isSpace { + cursor = start + len(chunk) + column = 0 + } else { + cursor = start + column = w + } + return true + }) + + rest := line[cursor:] + return rest == "" || yield(rest) + }) + } +} + // stringWidth calculates the rendered width of text if placed at the given column, // accounting for tabstops. func stringWidth(column int, text string, allowNonPrint bool, out *writer) int { diff --git a/experimental/report/writer.go b/experimental/report/writer.go index 996785a4..f7ef1057 100644 --- a/experimental/report/writer.go +++ b/experimental/report/writer.go @@ -17,6 +17,7 @@ package report import ( "bytes" "io" + "regexp" "slices" "unicode" @@ -62,6 +63,36 @@ func (w *writer) WriteString(data string) { }) } +var ansiEscapePat = regexp.MustCompile("^\033\\[([\\d;]*)m") + +// WriteWrapped writes a string to w, taking care to wrap data such that a line +// is (ideally) never wider than width. +func (w *writer) WriteWrapped(data string, width int) { + // NOTE: We currently assume that WriteWrapped is never called with user- + // provided text as a prefix; this avoids a fussy call to stringWidth. + var margin int + for i := 0; i < len(w.buf); i++ { + // Need to skip any ANSI color codes. + if esc := ansiEscapePat.Find(w.buf[i:]); esc != nil { + i += len(esc) - 1 + continue + } + + margin++ + } + + first := true + wordWrap(data, width-margin)(func(line string) bool { + if !first { + w.WriteString("\n") + w.WriteSpaces(margin) + } + first = false + w.WriteString(line) + return true + }) +} + // Flush flushes the buffer to the writer's output. func (w *writer) Flush() error { defer func() { w.err = nil }() diff --git a/experimental/token/cursor.go b/experimental/token/cursor.go index f3d090a5..836ca589 100644 --- a/experimental/token/cursor.go +++ b/experimental/token/cursor.go @@ -60,6 +60,7 @@ func NewCursorAt(tok Token) *Cursor { return &Cursor{ withContext: tok.withContext, idx: tok.ID().naturalIndex(), // Convert to 0-based index. + isBackwards: tok.nat().IsClose(), // Set the direction to calculate the offset. } } diff --git a/experimental/token/cursor_test.go b/experimental/token/cursor_test.go index 39334ce7..64cc235a 100644 --- a/experimental/token/cursor_test.go +++ b/experimental/token/cursor_test.go @@ -116,4 +116,20 @@ func TestCursor(t *testing.T) { assert.Len(t, text, span.Start) assert.Len(t, text, span.End) }) + + // Test setting the cursor at the open brace + t.Run("open", func(t *testing.T) { + t.Parallel() + cursor := token.NewCursorAt(open) + tokenEq(t, open, cursor.NextSkippable()) + tokenEq(t, token.Zero, cursor.NextSkippable()) + }) + + // Test setting the cursor at the close brace + t.Run("close", func(t *testing.T) { + t.Parallel() + cursor := token.NewCursorAt(close) + tokenEq(t, close, cursor.NextSkippable()) + tokenEq(t, token.Zero, cursor.NextSkippable()) + }) } diff --git a/go.mod b/go.mod index 0e4fc6e1..4d0cc1c9 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 github.com/rivo/uniseg v0.4.7 github.com/stretchr/testify v1.10.0 - golang.org/x/sync v0.10.0 - google.golang.org/protobuf v1.36.4 + golang.org/x/sync v0.11.0 + google.golang.org/protobuf v1.36.5 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 49fb94d3..d73b7f15 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,10 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.work.sum b/go.work.sum index 85b723a6..60873d96 100644 --- a/go.work.sum +++ b/go.work.sum @@ -89,6 +89,7 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= @@ -161,6 +162,7 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/internal/benchmarks/go.mod b/internal/benchmarks/go.mod index ea62ece1..40bfeae7 100644 --- a/internal/benchmarks/go.mod +++ b/internal/benchmarks/go.mod @@ -7,10 +7,10 @@ require ( github.com/igrmk/treemap/v2 v2.0.1 github.com/jhump/protoreflect v1.14.1 // MUST NOT be updated to v1.15 or higher github.com/stretchr/testify v1.10.0 - google.golang.org/protobuf v1.36.4 + google.golang.org/protobuf v1.36.5 ) -require golang.org/x/sync v0.10.0 +require golang.org/x/sync v0.11.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/internal/benchmarks/go.sum b/internal/benchmarks/go.sum index ea39f913..2dc39fb2 100644 --- a/internal/benchmarks/go.sum +++ b/internal/benchmarks/go.sum @@ -76,8 +76,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -117,8 +117,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 5f12e0f5..e45f7b76 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -15,7 +15,12 @@ // package iterx contains extensions to Go's package iter. package iterx -import "github.com/bufbuild/protocompile/internal/iter" +import ( + "fmt" + "strings" + + "github.com/bufbuild/protocompile/internal/iter" +) // Limit limits a sequence to only yield at most limit times. func Limit[T any](limit uint, seq iter.Seq[T]) iter.Seq[T] { @@ -40,6 +45,30 @@ func First[T any](seq iter.Seq[T]) (v T, ok bool) { return v, ok } +// OnlyOne retrieved the only element of an iterator. +func OnlyOne[T any](seq iter.Seq[T]) (v T, ok bool) { + seq(func(x T) bool { + if !ok { + v = x + } + ok = !ok + return ok + }) + return v, ok +} + +// Find returns the first element that matches a predicate. +func Find[T any](seq iter.Seq[T], p func(T) bool) (v T, ok bool) { + seq(func(x T) bool { + if p(x) { + v, ok = x, true + return false + } + return true + }) + return v, ok +} + // All returns whether every element of an iterator satisfies the given // predicate. Returns true if seq yields no values. func All[T any](seq iter.Seq[T], p func(T) bool) bool { @@ -51,11 +80,109 @@ func All[T any](seq iter.Seq[T], p func(T) bool) bool { return all } +// Count counts the number of elements in seq that match the given predicate. +// +// If p is nil, it is treated as func(_ T) bool { return true }. +func Count[T any](seq iter.Seq[T], p func(T) bool) int { + var total int + seq(func(v T) bool { + if p == nil || p(v) { + total++ + } + return true + }) + return total +} + +// Strings maps an iterator with [fmt.Sprint], yielding an iterator of strings. +func Strings[T any](seq iter.Seq[T]) iter.Seq[string] { + return Map(seq, func(v T) string { + if s, ok := any(v).(string); ok { + return s // Avoid dumb copies. + } + return fmt.Sprint(v) + }) +} + // Map returns a new iterator applying f to each element of seq. func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] { + return FilterMap(seq, func(v T) (U, bool) { return f(v), true }) +} + +// Filter returns a new iterator that only includes values satisfying p. +func Filter[T any](seq iter.Seq[T], p func(T) bool) iter.Seq[T] { + return FilterMap(seq, func(v T) (T, bool) { return v, p(v) }) +} + +// FilterMap combines the operations of [Map] and [Filter]. +func FilterMap[T, U any](seq iter.Seq[T], f func(T) (U, bool)) iter.Seq[U] { return func(yield func(U) bool) { - seq(func(value T) bool { - return yield(f(value)) + seq(func(v T) bool { + v2, ok := f(v) + return !ok || yield(v2) }) } } + +// FilterMap1To2 is like [FilterMap], but it also acts a Y pipe for converting a one-element +// iterator into a two-element iterator. +func Map1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V)) iter.Seq2[U, V] { + return FilterMap1To2(seq, func(v T) (U, V, bool) { + x1, x2 := f(v) + return x1, x2, true + }) +} + +// FilterMap1To2 is like [FilterMap], but it also acts a Y pipe for converting a one-element +// iterator into a two-element iterator. +func FilterMap1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V, bool)) iter.Seq2[U, V] { + return func(yield func(U, V) bool) { + seq(func(v T) bool { + x1, x2, ok := f(v) + return !ok || yield(x1, x2) + }) + } +} + +// Enumerate adapts an iterator to yield an incrementing index each iteration +// step. +func Enumerate[T any](seq iter.Seq[T]) iter.Seq2[int, T] { + var i int + return Map1To2(seq, func(v T) (int, T) { + i++ + return i - 1, v + }) +} + +// Join is like [strings.Join], but works on an iterator. Elements are +// stringified as if by [fmt.Print]. +func Join[T any](seq iter.Seq[T], sep string) string { + var out strings.Builder + first := true + seq(func(v T) bool { + if !first { + out.WriteString(sep) + } + first = false + + fmt.Fprint(&out, v) + return true + }) + return out.String() +} + +// Chain returns an iterator that calls a sequence of iterators in sequence. +func Chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] { + return func(yield func(T) bool) { + var done bool + for _, seq := range seqs { + if done { + return + } + seq(func(v T) bool { + done = !yield(v) + return !done + }) + } + } +} diff --git a/internal/ext/slicesx/collect.go b/internal/ext/mapsx/collect.go similarity index 57% rename from internal/ext/slicesx/collect.go rename to internal/ext/mapsx/collect.go index 082bbaff..54f55f21 100644 --- a/internal/ext/slicesx/collect.go +++ b/internal/ext/mapsx/collect.go @@ -12,30 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -// package iters contains helpers for working with iterators. -package slicesx +package mapsx import "github.com/bufbuild/protocompile/internal/iter" -// Collect polyfills [slices.Collect]. -func Collect[E any](seq iter.Seq[E]) []E { - return AppendSeq[[]E](nil, seq) -} - -// AppendSeq polyfills [slices.AppendSeq]. -func AppendSeq[S ~[]E, E any](s S, seq iter.Seq[E]) []E { - seq(func(v E) bool { - s = append(s, v) +// Collect polyfills [maps.Collect]. +func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]V { + out := make(map[K]V) + seq(func(k K, v V) bool { + out[k] = v return true }) - return s + return out } -// Map constructs a new slice by applying f to each element. -func Map[S ~[]E, E, R any](s S, f func(E) R) []R { - out := make([]R, len(s)) - for i, e := range s { - out[i] = f(e) - } +// CollectSet is like [Collect], but it implicitly fills in each map value +// with a struct{} value. +func CollectSet[K comparable](seq iter.Seq[K]) map[K]struct{} { + out := make(map[K]struct{}) + seq(func(k K) bool { + out[k] = struct{}{} + return true + }) return out } diff --git a/internal/ext/mapsx/mapsx.go b/internal/ext/mapsx/mapsx.go new file mode 100644 index 00000000..34c47475 --- /dev/null +++ b/internal/ext/mapsx/mapsx.go @@ -0,0 +1,34 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package mapsx contains extensions to Go's package maps. +package mapsx + +import "github.com/bufbuild/protocompile/internal/iter" + +// Keys polyfills [maps.Keys]. +func Keys[M ~map[K]V, K comparable, V any](m M) iter.Seq[K] { + return func(yield func(k K) bool) { + for k := range m { + if !yield(k) { + return + } + } + } +} + +// KeySet returns a copy of m, with its values replaced with empty structs. +func KeySet[M ~map[K]V, K comparable, V any](m M) map[K]struct{} { + return CollectSet(Keys(m)) +} diff --git a/internal/ext/slicesx/iter.go b/internal/ext/slicesx/iter.go new file mode 100644 index 00000000..6d90660e --- /dev/null +++ b/internal/ext/slicesx/iter.go @@ -0,0 +1,121 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slicesx + +import ( + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/iter" +) + +// Collect polyfills [slices.Collect]. +func Collect[E any](seq iter.Seq[E]) []E { + return AppendSeq[[]E](nil, seq) +} + +// AppendSeq polyfills [slices.AppendSeq]. +func AppendSeq[S ~[]E, E any](s S, seq iter.Seq[E]) []E { + seq(func(v E) bool { + s = append(s, v) + return true + }) + return s +} + +// Map is a helper for generating a mapped iterator over a slice, to avoid +// a noisy call to [Values]. +func Map[S ~[]E, E, U any](s S, f func(E) U) iter.Seq[U] { + return iterx.Map(Values(s), f) +} + +// PartitionFunc returns an iterator of the largest substrings of s of equal +// elements. +// +// In other words, suppose key is the identity function. Then, the slice +// [a a a b c c] is yielded as the subslices [a a a], [b], and [c c c]. +// +// The iterator also yields the index at which each subslice begins. +// +// Will never yield an empty slice. +// +//nolint:dupword +func Partition[S ~[]E, E comparable](s S) iter.Seq2[int, S] { + return PartitionKey(s, func(e E) E { return e }) +} + +// PartitionKey is like [Partition], but instead the subslices are all such +// that ever element has the same value for key(e). +// +// [Partition] is equivalent to PartitionKey with the identity function. +func PartitionKey[S ~[]E, E any, K comparable](s S, key func(E) K) iter.Seq2[int, S] { + return func(yield func(int, S) bool) { + var start int + var prev K + for i, r := range s { + next := key(r) + if i == 0 { + prev = next + continue + } + + if prev == next { + continue + } + + if !yield(start, s[start:i]) { + return + } + + start = i + prev = next + } + + if start < len(s) { + yield(start, s[start:]) + } + } +} + +// SplitFunc splits a slice according to the given predicate. +// +// Whenever p returns true, this function will yield all prior elements not +// yet yielded. +func SplitFunc[S ~[]E, E any](s S, p func(int, E) bool) iter.Seq[S] { + return func(yield func(S) bool) { + var start int + for i, r := range s { + if !p(i, r) { + continue + } + if !yield(s[start:i]) { + return + } + start = i + } + if start < len(s) { + yield(s[start:]) + } + } +} + +// Values is a polyfill for [slices.Values]. +func Values[S ~[]E, E any](s S) iter.Seq[E] { + return func(yield func(E) bool) { + for _, v := range s { + if !yield(v) { + return + } + } + } +} diff --git a/internal/ext/slicesx/partition.go b/internal/ext/slicesx/partition.go deleted file mode 100644 index acd9366a..00000000 --- a/internal/ext/slicesx/partition.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020-2025 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package slicesx - -import "github.com/bufbuild/protocompile/internal/iter" - -// Partition returns an iterator of subslices of s such that each yielded -// slice is delimited according to delimit. Also yields the starting index of -// the subslice. -// -// In other words, suppose delimit is !=. Then, the slice [a a a b c c] is yielded -// as the subslices [a a a], [b], and [c c c]. -// -// Will never yield an empty slice. -// -//nolint:dupword -func Partition[T any](s []T, delimit func(a, b *T) bool) iter.Seq2[int, []T] { - return func(yield func(int, []T) bool) { - var start int - for i := 1; i < len(s); i++ { - if delimit(&s[i-1], &s[i]) { - if !yield(start, s[start:i]) { - return - } - start = i - } - } - rest := s[start:] - if len(rest) > 0 { - yield(start, rest) - } - } -} diff --git a/internal/ext/slicesx/partition_test.go b/internal/ext/slicesx/partition_test.go index 792db8a2..c50ead5b 100644 --- a/internal/ext/slicesx/partition_test.go +++ b/internal/ext/slicesx/partition_test.go @@ -76,7 +76,7 @@ func TestPartition(t *testing.T) { ss [][]int count int ) - it := slicesx.Partition(test.slice, func(a, b *int) bool { return *a != *b }) + it := slicesx.Partition(test.slice) it(func(i int, s []int) bool { if test.breakAt == count { return false diff --git a/internal/ext/slicesx/slicesx.go b/internal/ext/slicesx/slicesx.go index 971475bb..64398f62 100644 --- a/internal/ext/slicesx/slicesx.go +++ b/internal/ext/slicesx/slicesx.go @@ -19,7 +19,6 @@ import ( "slices" "github.com/bufbuild/protocompile/internal/ext/unsafex" - "github.com/bufbuild/protocompile/internal/iter" ) // SliceIndex is a type that can be used to index into a slice. @@ -29,10 +28,7 @@ type SliceIndex = unsafex.Int // // If the bounds check fails, returns the zero value and false. func Get[S ~[]E, E any, I SliceIndex](s S, idx I) (element E, ok bool) { - if idx < 0 { - return element, false - } - if uint64(idx) >= uint64(len(s)) { + if !BoundsCheck(idx, len(s)) { return element, false } @@ -44,10 +40,7 @@ func Get[S ~[]E, E any, I SliceIndex](s S, idx I) (element E, ok bool) { // GetPointer is like [Get], but it returns a pointer to the selected element // instead, returning nil on out-of-bounds indices. func GetPointer[S ~[]E, E any, I SliceIndex](s S, idx I) *E { - if idx < 0 { - return nil - } - if uint64(idx) >= uint64(len(s)) { + if !BoundsCheck(idx, len(s)) { return nil } @@ -68,6 +61,20 @@ func LastPointer[S ~[]E, E any](s S) *E { return GetPointer(s, len(s)-1) } +// BoundsCheck performs a generic bounds check as efficiently as possible. +// +// This function assumes that len is the length of a slice, i.e, it is +// non-negative. +// +//nolint:revive,predeclared // len is the right variable name ugh. +func BoundsCheck[I SliceIndex](idx I, len int) bool { + // An unsigned comparison is sufficient. If idx is non-negative, it checks + // that it is less than len. If idx is negative, converting it to uint64 + // will produce a value greater than math.Int64Max, which is greater than + // the positive value we get from casting len. + return uint64(idx) < uint64(len) +} + // Among is like [slices.Contains], but the haystack is passed variadically. // // This makes the common case of using Contains as a variadic (x == y || ...) @@ -75,14 +82,3 @@ func LastPointer[S ~[]E, E any](s S) *E { func Among[E comparable](needle E, haystack ...E) bool { return slices.Contains(haystack, needle) } - -// Values is a polyfill for [slices.Values]. -func Values[S ~[]E, E any](s S) iter.Seq[E] { - return func(yield func(E) bool) { - for _, v := range s { - if !yield(v) { - return - } - } - } -} diff --git a/internal/ext/stringsx/stringsx.go b/internal/ext/stringsx/stringsx.go index 8d2d63f0..3066ecd8 100644 --- a/internal/ext/stringsx/stringsx.go +++ b/internal/ext/stringsx/stringsx.go @@ -17,12 +17,37 @@ package stringsx import ( "strings" + "unicode/utf8" "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/ext/slicesx" "github.com/bufbuild/protocompile/internal/ext/unsafex" "github.com/bufbuild/protocompile/internal/iter" ) +// Rune returns the rune at the given index. +// +// Returns 0, false if out of bounds. Returns U+FFFD, false if rune decoding fails. +func Rune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { + if !slicesx.BoundsCheck(idx, len(s)) { + return 0, false + } + r, _ := utf8.DecodeRuneInString(s[idx:]) + return r, r != utf8.RuneError +} + +// Rune returns the previous rune at the given index. +// +// Returns 0, false if out of bounds. Returns U+FFFD, false if rune decoding fails. +func PrevRune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { + if !slicesx.BoundsCheck(idx-1, len(s)) { + return 0, false + } + + r, _ := utf8.DecodeLastRuneInString(s[:idx]) + return r, r != utf8.RuneError +} + // EveryFunc verifies that all runes in the string satisfy the given predicate. func EveryFunc(s string, p func(rune) bool) bool { return iterx.All(Runes(s), p) @@ -74,3 +99,38 @@ func Split[Sep string | rune](s string, sep Sep) iter.Seq[string] { func Lines(s string) iter.Seq[string] { return Split(s, '\n') } + +// PartitionKey returns an iterator of the largest substrings of s such that +// key(r) for each rune in each substring is the same value. +// +// The iterator also yields the index at which each substring begins. +// +// Will never yield an empty string. +func PartitionKey[K comparable](s string, key func(rune) K) iter.Seq2[int, string] { + return func(yield func(int, string) bool) { + var start int + var prev K + for i, r := range s { + next := key(r) + if i == 0 { + prev = next + continue + } + + if prev == next { + continue + } + + if !yield(start, s[start:i]) { + return + } + + start = i + prev = next + } + + if start < len(s) { + yield(start, s[start:]) + } + } +} diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 644ceadf..f131ea53 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.0 require ( github.com/bufbuild/buf v1.50.0 github.com/golangci/golangci-lint v1.63.4 - golang.org/x/tools v0.29.0 + golang.org/x/tools v0.30.0 ) require ( @@ -188,9 +188,9 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.4-0.20250116160514-2005adbe0cf6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/internal/tools/go.sum b/internal/tools/go.sum index cf967dcb..1bdc3594 100644 --- a/internal/tools/go.sum +++ b/internal/tools/go.sum @@ -665,8 +665,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -708,8 +708,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -731,8 +731,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -787,8 +787,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -875,8 +875,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/linker/descriptors.go b/linker/descriptors.go index 4fdd86fc..826880de 100644 --- a/linker/descriptors.go +++ b/linker/descriptors.go @@ -331,6 +331,7 @@ func (r *result) createImports() fileImports { imps[int(publicIndex)].IsPublic = true } for _, weakIndex := range fd.WeakDependency { + //nolint:staticcheck // yes, is_weak is deprecated; but we still have to set it to compile the file imps[int(weakIndex)].IsWeak = true } return fileImports{files: imps}