From 8b3b7d75821674674bdd21f155eac1775e55f18e Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:05:55 -0800 Subject: [PATCH 01/64] PR #437 --- experimental/report/diff.go | 88 +++++++++++-------- experimental/report/renderer.go | 38 +++++--- experimental/report/testdata/suggestions.yaml | 24 ++++- .../testdata/suggestions.yaml.color.txt | 19 +++- .../testdata/suggestions.yaml.fancy.txt | 19 +++- .../testdata/suggestions.yaml.simple.txt | 2 + internal/ext/iterx/iterx.go | 11 +++ internal/ext/stringsx/stringsx.go | 53 +++++++++++ 8 files changed, 205 insertions(+), 49 deletions(-) create mode 100644 internal/ext/stringsx/stringsx.go diff --git a/experimental/report/diff.go b/experimental/report/diff.go index eb7dc166..a451908b 100644 --- a/experimental/report/diff.go +++ b/experimental/report/diff.go @@ -15,6 +15,7 @@ package report import ( + "slices" "sort" "strings" @@ -56,40 +57,44 @@ func (h hunk) bold(ss *styleSheet) string { } // hunkDiff computes edit hunks for a diff. -func hunkDiff(span Span, edits []Edit) []hunk { +func hunkDiff(span Span, edits []Edit) (Span, []hunk) { out := make([]hunk, 0, len(edits)*3+1) var prev int - src, offsets := offsetsForDiffing(span, edits) - for i, edit := range edits { - start := offsets[i][0] - end := offsets[i][1] - + span, edits = offsetsForDiffing(span, edits) + src := span.Text() + for _, edit := range edits { out = append(out, - hunk{hunkUnchanged, src[prev:start]}, - hunk{hunkDelete, src[start:end]}, + hunk{hunkUnchanged, src[prev:edit.Start]}, + hunk{hunkDelete, src[edit.Start:edit.End]}, hunk{hunkAdd, edit.Replace}, ) - prev = end + prev = edit.End } - return append(out, hunk{hunkUnchanged, src[prev:]}) + return span, append(out, hunk{hunkUnchanged, src[prev:]}) } // unifiedDiff computes whole-line hunks for this diff, for producing a unified // edit. // // Each slice will contain one or more lines that should be displayed together. -func unifiedDiff(span Span, edits []Edit) []hunk { +func unifiedDiff(span Span, edits []Edit) (Span, []hunk) { // Sort the edits such that they are ordered by starting offset. - src, offsets := offsetsForDiffing(span, edits) + span, edits = offsetsForDiffing(span, edits) + src := span.Text() sort.Slice(edits, func(i, j int) bool { - return offsets[i][0] < offsets[j][0] + return edits[i].Start < edits[j].End }) // Partition offsets into overlapping lines. That is, this connects together // all edit spans whose end and start are not separated by a newline. - prev := &offsets[0] - parts := slicesx.Partition(offsets, func(_, next *[2]int) bool { - if next == prev || !strings.Contains(src[prev[1]:next[0]], "\n") { + prev := &edits[0] + parts := slicesx.Partition(edits, func(_, next *Edit) bool { + if next == prev { + return false + } + + chunk := src[prev.End:next.Start] + if !strings.Contains(chunk, "\n") { return false } @@ -99,12 +104,12 @@ func unifiedDiff(span Span, edits []Edit) []hunk { var out []hunk var prevHunk int - parts(func(i int, offsets [][2]int) bool { + parts(func(_ int, edits []Edit) bool { // First, figure out the start and end of the modified region. - start, end := offsets[0][0], offsets[0][1] - for _, offset := range offsets[1:] { - start = min(start, offset[0]) - end = max(end, offset[1]) + start, end := edits[0].Start, edits[0].End + for _, edit := range edits[1:] { + start = min(start, edit.Start) + end = max(end, edit.End) } // Then, snap the region to be newline delimited. This is the unedited // lines. @@ -114,10 +119,10 @@ func unifiedDiff(span Span, edits []Edit) []hunk { // Now, apply the edits to original to produce the modified result. var buf strings.Builder prev := 0 - for j, offset := range offsets { - buf.WriteString(original[prev:offset[0]]) - buf.WriteString(edits[i+j].Replace) - prev = offset[1] + for _, edit := range edits { + buf.WriteString(original[prev : edit.Start-start]) + buf.WriteString(edit.Replace) + prev = edit.End - start } buf.WriteString(original[prev:]) @@ -131,20 +136,31 @@ func unifiedDiff(span Span, edits []Edit) []hunk { prevHunk = end return true }) - return append(out, hunk{hunkUnchanged, src[prevHunk:]}) + return span, append(out, hunk{hunkUnchanged, src[prevHunk:]}) } // offsetsForDiffing pre-calculates information needed for diffing: -// the line-snapped span, and the offsetsForDiffing of each edit as indices into -// that span. -func offsetsForDiffing(span Span, edits []Edit) (string, [][2]int) { - start, end := adjustLineOffsets(span.File.Text(), span.Start, span.End) - delta := span.Start - start - - offsets := make([][2]int, len(edits)) - for i, edit := range edits { - offsets[i] = [2]int{edit.Start + delta, edit.End + delta} +// the line-snapped span, and edits which are adjusted to conform to that +// span. +func offsetsForDiffing(span Span, edits []Edit) (Span, []Edit) { + edits = slices.Clone(edits) + var start, end int + for i := range edits { + e := &edits[i] + e.Start += span.Start + e.End += span.Start + if i == 0 { + start, end = e.Start, e.End + } else { + start, end = min(e.Start, start), max(e.End, end) + } + } + + start, end = adjustLineOffsets(span.File.Text(), start, end) + for i := range edits { + edits[i].Start -= start + edits[i].End -= start } - return span.File.Text()[start:end], offsets + return span.File.Span(start, end), edits } diff --git a/experimental/report/renderer.go b/experimental/report/renderer.go index 7114618f..fd5f7d48 100644 --- a/experimental/report/renderer.go +++ b/experimental/report/renderer.go @@ -25,6 +25,7 @@ import ( "unicode" "github.com/bufbuild/protocompile/internal/ext/slicesx" + "github.com/bufbuild/protocompile/internal/ext/stringsx" ) // Renderer configures a diagnostic rendering operation. @@ -246,7 +247,7 @@ func (r Renderer) diagnostic(report *Report, d Diagnostic) string { if i > 0 { out.WriteByte('\n') } - suggestion(snippets[0], locations[i][0].Line, lineBarWidth, &ss, &out) + suggestion(snippets[0], lineBarWidth, &ss, &out) return true } @@ -911,7 +912,7 @@ func renderSidebar(bars, lineno, slashAt int, ss *styleSheet, multis []*multilin } // suggestion renders a single suggestion window. -func suggestion(snip snippet, startLine int, lineBarWidth int, ss *styleSheet, out *strings.Builder) { +func suggestion(snip snippet, lineBarWidth int, ss *styleSheet, out *strings.Builder) { out.WriteString(ss.nAccent) padBy(out, lineBarWidth) out.WriteString("help: ") @@ -933,12 +934,29 @@ func suggestion(snip snippet, startLine int, lineBarWidth int, ss *styleSheet, o strings.Contains(snip.Span.Text(), "\n") if multiline { - aLine := startLine - bLine := startLine - for _, hunk := range unifiedDiff(snip.Span, snip.edits) { + span, hunks := unifiedDiff(snip.Span, snip.edits) + aLine := span.StartLoc().Line + bLine := aLine + for i, hunk := range hunks { + // Trim a single newline before and after hunk. This helps deal with + // cases where a newline gets duplicated across hunks of different + // type. + hunk.content, _ = strings.CutPrefix(hunk.content, "\n") + hunk.content, _ = strings.CutSuffix(hunk.content, "\n") + if hunk.content == "" { continue } + + // Skip addition lines that only contain whitespace, if the previous + // hunk was a deletion. This helps avoid cases where a whole line + // was deleted and some indentation was left over. + if prev, _ := slicesx.Get(hunks, i-1); prev.kind == hunkDelete && + hunk.kind == hunkAdd && + stringsx.EveryFunc(hunk.content, unicode.IsSpace) { + continue + } + for _, line := range strings.Split(hunk.content, "\n") { lineno := aLine if hunk.kind == '+' { @@ -954,12 +972,12 @@ func suggestion(snip snippet, startLine int, lineBarWidth int, ss *styleSheet, o ) switch hunk.kind { - case ' ': + case hunkUnchanged: aLine++ bLine++ - case '-': + case hunkDelete: aLine++ - case '+': + case hunkAdd: bLine++ } } @@ -972,8 +990,8 @@ func suggestion(snip snippet, startLine int, lineBarWidth int, ss *styleSheet, o return } - fmt.Fprintf(out, "\n%s%*d | ", ss.nAccent, lineBarWidth, startLine) - hunks := hunkDiff(snip.Span, snip.edits) + span, hunks := hunkDiff(snip.Span, snip.edits) + fmt.Fprintf(out, "\n%s%*d | ", ss.nAccent, lineBarWidth, span.StartLoc().Line) var column int for _, hunk := range hunks { if hunk.content == "" { diff --git a/experimental/report/testdata/suggestions.yaml b/experimental/report/testdata/suggestions.yaml index a31117e2..210b947a 100644 --- a/experimental/report/testdata/suggestions.yaml +++ b/experimental/report/testdata/suggestions.yaml @@ -104,4 +104,26 @@ diagnostics: replace: "{\n option " - start: 10 end: 12 - replace: ";\n }" \ No newline at end of file + replace: ";\n }" + + - message: 'delete some stuff' + level: LEVEL_ERROR + annotations: + - file: 0 + start: 38 + end: 153 + edits: + - start: 0 + end: 13 + - start: 114 + end: 115 + + - message: 'delete this method' + level: LEVEL_ERROR + annotations: + - file: 0 + start: 38 + end: 153 + edits: + - start: 59 + end: 113 \ No newline at end of file diff --git a/experimental/report/testdata/suggestions.yaml.color.txt b/experimental/report/testdata/suggestions.yaml.color.txt index 5dc0b6fb..f0161e23 100644 --- a/experimental/report/testdata/suggestions.yaml.color.txt +++ b/experimental/report/testdata/suggestions.yaml.color.txt @@ -46,5 +46,22 @@ ⟨blu⟩ 9 | ⟨b.grn⟩+⟨grn⟩ } ⟨blu⟩ | ⟨reset⟩ -⟨b.red⟩encountered 2 errors and 1 warning⟨reset⟩ +⟨b.red⟩error: delete some stuff⟨reset⟩ +⟨blu⟩ --> foo.proto:5:1 +⟨blu⟩ help: + | +⟨blu⟩ 5 | ⟨b.red⟩-⟨red⟩ service Foo { +⟨blu⟩ 6 | ⟨reset⟩ ⟨reset⟩ rpc Get(GetRequest) returns GetResponse; +⟨blu⟩ 7 | ⟨reset⟩ ⟨reset⟩ rpc Put(PutRequest) returns (PutResponse) [foo = bar]; +⟨blu⟩ 8 | ⟨b.red⟩-⟨red⟩ } +⟨blu⟩ | ⟨reset⟩ + +⟨b.red⟩error: delete this method⟨reset⟩ +⟨blu⟩ --> foo.proto:5:1 +⟨blu⟩ help: + | +⟨blu⟩ 7 | ⟨b.red⟩-⟨red⟩ rpc Put(PutRequest) returns (PutResponse) [foo = bar]; +⟨blu⟩ | ⟨reset⟩ + +⟨b.red⟩encountered 4 errors and 1 warning⟨reset⟩ ⟨reset⟩ \ No newline at end of file diff --git a/experimental/report/testdata/suggestions.yaml.fancy.txt b/experimental/report/testdata/suggestions.yaml.fancy.txt index 2a2c4a33..4dc5b31b 100644 --- a/experimental/report/testdata/suggestions.yaml.fancy.txt +++ b/experimental/report/testdata/suggestions.yaml.fancy.txt @@ -46,4 +46,21 @@ error: method options must go in a block 9 | + } | -encountered 2 errors and 1 warning +error: delete some stuff + --> foo.proto:5:1 + help: + | + 5 | - service Foo { + 6 | rpc Get(GetRequest) returns GetResponse; + 7 | rpc Put(PutRequest) returns (PutResponse) [foo = bar]; + 8 | - } + | + +error: delete this method + --> foo.proto:5:1 + help: + | + 7 | - rpc Put(PutRequest) returns (PutResponse) [foo = bar]; + | + +encountered 4 errors and 1 warning diff --git a/experimental/report/testdata/suggestions.yaml.simple.txt b/experimental/report/testdata/suggestions.yaml.simple.txt index 6915c543..864cc1ad 100644 --- a/experimental/report/testdata/suggestions.yaml.simple.txt +++ b/experimental/report/testdata/suggestions.yaml.simple.txt @@ -3,3 +3,5 @@ remark: foo.proto:1:10: let protocompile pick a syntax for you warning: foo.proto:5:9: services should have a `Service` suffix error: foo.proto:6:31: missing (...) around return type error: foo.proto:7:45: method options must go in a block +error: foo.proto:5:1: delete some stuff +error: foo.proto:5:1: delete this method diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index b001c442..5f12e0f5 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -40,6 +40,17 @@ func First[T any](seq iter.Seq[T]) (v T, ok bool) { 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 { + all := true + seq(func(v T) bool { + all = p(v) + return all + }) + return all +} + // 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 func(yield func(U) bool) { diff --git a/internal/ext/stringsx/stringsx.go b/internal/ext/stringsx/stringsx.go new file mode 100644 index 00000000..f8200556 --- /dev/null +++ b/internal/ext/stringsx/stringsx.go @@ -0,0 +1,53 @@ +// 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 stringsx contains extensions to Go's package strings. +package stringsx + +import ( + "github.com/bufbuild/protocompile/internal/ext/iterx" + "github.com/bufbuild/protocompile/internal/ext/unsafex" + "github.com/bufbuild/protocompile/internal/iter" +) + +// 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) +} + +// Runes returns an iterator over the runes in a string. +// +// Each non-UTF-8 byte in the string is yielded as a replacement character (U+FFFD). +func Runes(s string) iter.Seq[rune] { + return func(yield func(r rune) bool) { + for _, r := range s { + if !yield(r) { + return + } + } + } +} + +// Bytes returns an iterator over the bytes in a string. +func Bytes(s string) iter.Seq[byte] { + return func(yield func(byte) bool) { + for i := 0; i < len(s); i++ { + // Avoid performing a bounds check each loop step. + b := *unsafex.Add(unsafex.StringData(s), i) + if !yield(b) { + return + } + } + } +} From c6e445eebcfe20860cc45c2689c459791b22f971 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:48:08 -0800 Subject: [PATCH 02/64] add Span.Len --- experimental/report/span.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/experimental/report/span.go b/experimental/report/span.go index 56afa12d..cc2aedad 100644 --- a/experimental/report/span.go +++ b/experimental/report/span.go @@ -66,6 +66,11 @@ func (s Span) Text() string { return s.File.Text()[s.Start:s.End] } +// 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) From 5a0be56cc27862b4232ab88074b31cb64ef446ef Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:32:54 -0800 Subject: [PATCH 03/64] add new iterator helpers --- internal/ext/iterx/iterx.go | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 5f12e0f5..3672076e 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] { @@ -53,9 +58,37 @@ func All[T any](seq iter.Seq[T], p func(T) bool) bool { // 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) }) } } + +// 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() +} From 5c50385953b2302266da385423ee5c3fb9fb3d01 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:31:56 -0800 Subject: [PATCH 04/64] add classifier methods for predeclared.Name --- experimental/ast/predeclared/methods.go | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 experimental/ast/predeclared/methods.go diff --git a/experimental/ast/predeclared/methods.go b/experimental/ast/predeclared/methods.go new file mode 100644 index 00000000..5569eb44 --- /dev/null +++ b/experimental/ast/predeclared/methods.go @@ -0,0 +1,35 @@ +// 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 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 + +// IsScalar returns whether this predeclared name represents one of the scalar +// types. +func (v Name) IsScalar() bool { + return v >= Int32 && v <= Bytes +} + +// 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 +} From c9309db902cab52e2d5bf4ed1f407f892986e224 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:32:21 -0800 Subject: [PATCH 05/64] add an enum for syntax/edition values --- experimental/ast/syntax/doc.go | 19 ++++++ experimental/ast/syntax/is_edition.go | 20 ++++++ experimental/ast/syntax/syntax.go | 91 +++++++++++++++++++++++++++ experimental/ast/syntax/syntax.yaml | 42 +++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 experimental/ast/syntax/doc.go create mode 100644 experimental/ast/syntax/is_edition.go create mode 100644 experimental/ast/syntax/syntax.go create mode 100644 experimental/ast/syntax/syntax.yaml diff --git a/experimental/ast/syntax/doc.go b/experimental/ast/syntax/doc.go new file mode 100644 index 00000000..5210d59d --- /dev/null +++ b/experimental/ast/syntax/doc.go @@ -0,0 +1,19 @@ +// 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 specifies all of the syntax pragmas (including editions) +// that Protocompile understands. +package syntax + +//go:generate go run github.com/bufbuild/protocompile/internal/enum syntax.yaml 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 From 8dd79ae8fcdd7f8a43f84988b339553c5ba5a85d Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:34:45 -0800 Subject: [PATCH 06/64] add some new taxa.Nouns --- experimental/internal/taxa/classify.go | 18 ++++++++++++++++-- experimental/internal/taxa/noun.go | 21 +++++++++++++++++++++ experimental/internal/taxa/noun.yaml | 9 +++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/experimental/internal/taxa/classify.go b/experimental/internal/taxa/classify.go index f816461b..c67ba010 100644 --- a/experimental/internal/taxa/classify.go +++ b/experimental/internal/taxa/classify.go @@ -114,6 +114,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 +139,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 +201,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 diff --git a/experimental/internal/taxa/noun.go b/experimental/internal/taxa/noun.go index 98dfbcc2..693e06b5 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,6 +59,7 @@ const ( CompactOptions MethodIns MethodOuts + Signature FieldTag OptionValue QualifiedName @@ -69,6 +73,9 @@ const ( Type TypePath TypeParams + TypePrefix + MapKey + MapValue Whitespace Comment Ident @@ -140,6 +147,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 +166,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,6 +175,7 @@ var _table_Noun_String = [...]string{ CompactOptions: "compact options", MethodIns: "method parameter list", MethodOuts: "method return type", + Signature: "method signature", FieldTag: "message field tag", OptionValue: "option setting value", QualifiedName: "qualified name", @@ -178,6 +189,9 @@ var _table_Noun_String = [...]string{ Type: "type", TypePath: "type name", TypeParams: "type parameters", + TypePrefix: "type modifier", + MapKey: "map key", + MapValue: "map value", Whitespace: "whitespace", Comment: "comment", Ident: "identifier", @@ -232,6 +246,8 @@ var _table_Noun_GoString = [...]string{ Unrecognized: "Unrecognized", TopLevel: "TopLevel", EOF: "EOF", + SyntaxMode: "SyntaxMode", + EditionMode: "EditionMode", Decl: "Decl", Empty: "Empty", Syntax: "Syntax", @@ -249,6 +265,7 @@ var _table_Noun_GoString = [...]string{ Service: "Service", Extend: "Extend", Oneof: "Oneof", + Group: "Group", Option: "Option", CustomOption: "CustomOption", Field: "Field", @@ -257,6 +274,7 @@ var _table_Noun_GoString = [...]string{ CompactOptions: "CompactOptions", MethodIns: "MethodIns", MethodOuts: "MethodOuts", + Signature: "Signature", FieldTag: "FieldTag", OptionValue: "OptionValue", QualifiedName: "QualifiedName", @@ -270,6 +288,9 @@ var _table_Noun_GoString = [...]string{ Type: "Type", TypePath: "TypePath", TypeParams: "TypeParams", + TypePrefix: "TypePrefix", + MapKey: "MapKey", + MapValue: "MapValue", Whitespace: "Whitespace", Comment: "Comment", Ident: "Ident", diff --git a/experimental/internal/taxa/noun.yaml b/experimental/internal/taxa/noun.yaml index d9042290..0f2ca658 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,6 +60,7 @@ - {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: OptionValue, string: "option setting value"} @@ -73,6 +78,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"} + - {name: MapValue, string: "map value"} - {name: Whitespace, string: "whitespace"} - {name: Comment, string: "comment"} From f40ec3c189032bb8ccaa6634356d5c862662741d Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:33:56 -0800 Subject: [PATCH 07/64] ast additions --- experimental/ast/decl_body.go | 14 +++++++++++++ experimental/ast/decl_def.go | 21 ++++++++++++++++++- experimental/ast/type_generic.go | 6 ++++++ .../testdata/parser/field/group.proto.yaml | 5 +---- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/experimental/ast/decl_body.go b/experimental/ast/decl_body.go index 0422ee88..9b7c0a99 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..b3336752 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,18 @@ 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 +} + +// MarkCorrupt marks a definition as corrupt, which causes all other parts of +// 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 +426,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/type_generic.go b/experimental/ast/type_generic.go index 961b6e3f..24802c87 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() { diff --git a/experimental/parser/testdata/parser/field/group.proto.yaml b/experimental/parser/testdata/parser/field/group.proto.yaml index f40a17f2..8ce4a5ac 100644 --- a/experimental/parser/testdata/parser/field/group.proto.yaml +++ b/experimental/parser/testdata/parser/field/group.proto.yaml @@ -20,11 +20,8 @@ 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 - type.path.components: [{ ident: "group" }] value.literal.int_value: 2 body.decls: - def: From 453be0c44ab5f4166d4e66dac9f6f75e6dc420b8 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:37:25 -0800 Subject: [PATCH 08/64] fix brackets not being recorded for compact options --- experimental/parser/parse_decl.go | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/parser/parse_decl.go b/experimental/parser/parse_decl.go index 506bd6b7..ad7507b0 100644 --- a/experimental/parser/parse_decl.go +++ b/experimental/parser/parse_decl.go @@ -371,6 +371,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(), From 79bf0e0cb7585386110143c04420e0dcad5f9e2e Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:47:00 -0800 Subject: [PATCH 09/64] update import parsing to catch more nested imports --- experimental/parser/parse_decl.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experimental/parser/parse_decl.go b/experimental/parser/parse_decl.go index ad7507b0..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. From a4693951766a06c7bd108f96f6664183b8cbbfa0 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:47:27 -0800 Subject: [PATCH 10/64] track if we're in edition mode --- experimental/parser/parse_state.go | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/experimental/parser/parse_state.go b/experimental/parser/parse_state.go index bd3b5953..4b6643fb 100644 --- a/experimental/parser/parse_state.go +++ b/experimental/parser/parse_state.go @@ -26,6 +26,48 @@ 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 } // parsePunct attempts to unconditionally parse some punctuation. From 2893908f547b6e45677040f96ddd2e020a230e39 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:48:30 -0800 Subject: [PATCH 11/64] add diagnostics for use in the legalizer --- experimental/parser/diagnostics_internal.go | 44 +++++++++++++++++++++ experimental/parser/diagnostics_string.go | 30 ++++++++++++++ experimental/parser/parse_state.go | 6 +++ 3 files changed, 80 insertions(+) diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index 92e82ab3..1bf43555 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -16,6 +16,8 @@ package parser import ( "github.com/bufbuild/protocompile/experimental/internal/taxa" + + "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/report" ) @@ -85,3 +87,45 @@ 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 +} + +func (e errBadNest) Diagnose(d *report.Diagnostic) { + d.Apply( + report.Message("unexpected %s within %s", taxa.Classify(e.child), e.parent.what), + report.Snippetf(e.child, "this %s...", taxa.Classify(e.child)), + report.Snippetf(e.parent, "...cannot be declared within this %s", e.parent.what), + ) +} diff --git a/experimental/parser/diagnostics_string.go b/experimental/parser/diagnostics_string.go index 33db3614..1631a44f 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" ) @@ -91,3 +93,31 @@ 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,", taxa.String), + report.Notef("like C or Python, which can lead to surprising behavior"), + ) + } +} diff --git a/experimental/parser/parse_state.go b/experimental/parser/parse_state.go index 4b6643fb..4dbeb76a 100644 --- a/experimental/parser/parse_state.go +++ b/experimental/parser/parse_state.go @@ -70,6 +70,12 @@ func (p *parser) Mode() taxa.Noun { 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. // // If the wrong token is encountered, it DOES NOT consume the token, returning a nil From e56474dd9f8eca12c5edb8c134f32dfffd0ecbdb Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:50:13 -0800 Subject: [PATCH 12/64] add new testdata files for use with the legalizer --- .../testdata/parser/def/bare_bodies.proto | 30 ++++++ .../parser/def/bare_bodies.proto.yaml | 28 +++++ .../testdata/parser/import/in_message.proto | 25 +++++ .../parser/import/in_message.proto.yaml | 15 +++ .../testdata/parser/import/repeated.proto | 4 +- .../testdata/parser/method/bad_type.proto | 28 +++++ .../parser/method/bad_type.proto.stderr.txt | 7 ++ .../parser/method/bad_type.proto.yaml | 101 ++++++++++++++++++ .../testdata/parser/method/incomplete.proto | 27 +++++ .../parser/method/incomplete.proto.stderr.txt | 19 ++++ .../parser/method/incomplete.proto.yaml | 49 +++++++++ .../parser/testdata/parser/method/ok.proto | 25 +++++ .../testdata/parser/method/ok.proto.yaml | 57 ++++++++++ .../testdata/parser/method/options.proto | 24 +++++ .../testdata/parser/method/options.proto.yaml | 37 +++++++ .../parser/testdata/parser/package/42.proto | 2 + .../parser/package/42.proto.stderr.txt | 8 +- .../parser/range/invalid_parent.proto | 31 ++++++ .../parser/range/invalid_parent.proto.yaml | 20 ++++ .../parser/testdata/parser/range/ok.proto | 16 ++- .../testdata/parser/range/ok.proto.yaml | 51 +++------ .../range/reserved_default_syntax.proto | 19 ++++ .../range/reserved_default_syntax.proto.yaml | 11 ++ .../parser/range/reserved_edition.proto | 21 ++++ .../parser/range/reserved_edition.proto.yaml | 12 +++ .../parser/range/reserved_syntax.proto | 21 ++++ .../parser/range/reserved_syntax.proto.yaml | 12 +++ .../testdata/parser/syntax/syntax_2023.proto | 17 +++ .../parser/syntax/syntax_2023.proto.yaml | 3 + 29 files changed, 666 insertions(+), 54 deletions(-) create mode 100644 experimental/parser/testdata/parser/def/bare_bodies.proto create mode 100644 experimental/parser/testdata/parser/def/bare_bodies.proto.yaml create mode 100644 experimental/parser/testdata/parser/import/in_message.proto create mode 100644 experimental/parser/testdata/parser/import/in_message.proto.yaml create mode 100644 experimental/parser/testdata/parser/method/bad_type.proto create mode 100644 experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/method/bad_type.proto.yaml create mode 100644 experimental/parser/testdata/parser/method/incomplete.proto create mode 100644 experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/method/incomplete.proto.yaml create mode 100644 experimental/parser/testdata/parser/method/ok.proto create mode 100644 experimental/parser/testdata/parser/method/ok.proto.yaml create mode 100644 experimental/parser/testdata/parser/method/options.proto create mode 100644 experimental/parser/testdata/parser/method/options.proto.yaml create mode 100644 experimental/parser/testdata/parser/range/invalid_parent.proto create mode 100644 experimental/parser/testdata/parser/range/invalid_parent.proto.yaml create mode 100644 experimental/parser/testdata/parser/range/reserved_default_syntax.proto create mode 100644 experimental/parser/testdata/parser/range/reserved_default_syntax.proto.yaml create mode 100644 experimental/parser/testdata/parser/range/reserved_edition.proto create mode 100644 experimental/parser/testdata/parser/range/reserved_edition.proto.yaml create mode 100644 experimental/parser/testdata/parser/range/reserved_syntax.proto create mode 100644 experimental/parser/testdata/parser/range/reserved_syntax.proto.yaml create mode 100644 experimental/parser/testdata/parser/syntax/syntax_2023.proto create mode 100644 experimental/parser/testdata/parser/syntax/syntax_2023.proto.yaml 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.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/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.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/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/method/bad_type.proto b/experimental/parser/testdata/parser/method/bad_type.proto new file mode 100644 index 00000000..32fc255d --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto @@ -0,0 +1,28 @@ +// 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); +} \ 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..72bd6ac6 --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt @@ -0,0 +1,7 @@ +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)` + +encountered 1 error 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..1784624a --- /dev/null +++ b/experimental/parser/testdata/parser/method/bad_type.proto.yaml @@ -0,0 +1,101 @@ +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 }] 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..094e4b74 --- /dev/null +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -0,0 +1,19 @@ +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: unexpected `;` after `returns` + --> testdata/parser/method/incomplete.proto:25:23 + | +25 | rpc Bar6() returns; + | ^ expected `(` + +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 3 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..80ac6545 --- /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: + kind: KIND_METHOD + name.components: [{ ident: "Bar2" }] + signature.inputs: + - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] + - def: + kind: KIND_METHOD + name.components: [{ ident: "Bar3" }] + 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: + kind: KIND_METHOD + name.components: [{ ident: "Bar6" }] + 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.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/package/42.proto b/experimental/parser/testdata/parser/package/42.proto index a7a2448f..d7aedd73 100644 --- a/experimental/parser/testdata/parser/package/42.proto +++ b/experimental/parser/testdata/parser/package/42.proto @@ -14,4 +14,6 @@ syntax = "proto2"; +// FIXME: This produces a less-than-ideal diagnostic, but it's not an +// especially reasonable-to-expect case. 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 5c151d49..1f7730a2 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -1,13 +1,13 @@ error: unexpected integer literal after `package` declaration - --> testdata/parser/package/42.proto:17:9 + --> testdata/parser/package/42.proto:19:9 | -17 | package 42; +19 | package 42; | ^^ expected `;` error: unexpected integer literal in file scope - --> testdata/parser/package/42.proto:17:9 + --> testdata/parser/package/42.proto:19:9 | -17 | package 42; +19 | package 42; | ^^ expected identifier, `;`, `.`, `(...)`, or `{...}` encountered 2 errors 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.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/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.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.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_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.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/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.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" }] From 198a8bc75c346bb45774f90de217840212c10155 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 13:57:17 -0800 Subject: [PATCH 13/64] skeletonize the legalizer this contains all of the functions used in the legalizer as well as the recursive tree walk, but it does not contain the diagnostics, which will come in commits that follow. --- experimental/parser/legalize_decl.go | 58 ++++++++++++++++ experimental/parser/legalize_def.go | 63 +++++++++++++++++ experimental/parser/legalize_file.go | 76 +++++++++++++++++++++ experimental/parser/legalize_option.go | 32 +++++++++ experimental/parser/legalize_path.go | 95 ++++++++++++++++++++++++++ experimental/parser/legalize_type.go | 26 +++++++ experimental/parser/parse.go | 3 + 7 files changed, 353 insertions(+) create mode 100644 experimental/parser/legalize_decl.go create mode 100644 experimental/parser/legalize_def.go create mode 100644 experimental/parser/legalize_file.go create mode 100644 experimental/parser/legalize_option.go create mode 100644 experimental/parser/legalize_path.go create mode 100644 experimental/parser/legalize_type.go diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go new file mode 100644 index 00000000..49b70d6e --- /dev/null +++ b/experimental/parser/legalize_decl.go @@ -0,0 +1,58 @@ +// 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/seq" +) + +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() + 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() + legalizeDef(p, parent, def) + + body := def.Body() + what := classified{def, taxa.Classify(def)} + seq.Values(body.Decls())(func(decl ast.DeclAny) bool { + legalizeDecl(p, what, decl) + return true + }) + } +} + +func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { +} diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go new file mode 100644 index 00000000..82b4e9c2 --- /dev/null +++ b/experimental/parser/legalize_def.go @@ -0,0 +1,63 @@ +// 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" +) + +func legalizeDef(p *parser, parent classified, def ast.DeclDef) { + kind := def.Classify() + + if def.IsCorrupt() { + return + } + + 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) + } +} + +// legalizeMessageLike 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) { +} + +// legalizeMessageLike legalizes something that resembles a field definition: +// namely, fields, groups, and enum values. +func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef) { + if options := def.Options(); !options.IsZero() { + legalizeCompactOptions(p, options) + } + + if what == taxa.Field { + legalizeFieldType(p, def.Type()) + } +} + +func legalizeOption(p *parser, def ast.DeclDef) { + legalizeOptionEntry(p, def.AsOption().Option, def.Span()) +} + +func legalizeMethod(p *parser, def ast.DeclDef) { +} diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go new file mode 100644 index 00000000..940200fd --- /dev/null +++ b/experimental/parser/legalize_file.go @@ -0,0 +1,76 @@ +// 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 ( + "regexp" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/internal/taxa" + "github.com/bufbuild/protocompile/experimental/seq" +) + +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 + }) +} + +// 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) { + if parent.what == taxa.TopLevel && first != nil { + if !first.IsZero() { + *first = decl + } + } +} + +// 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) { +} + +// 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) { +} diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go new file mode 100644 index 00000000..b4c958c2 --- /dev/null +++ b/experimental/parser/legalize_option.go @@ -0,0 +1,32 @@ +// 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/report" + "github.com/bufbuild/protocompile/experimental/seq" +) + +func legalizeCompactOptions(p *parser, opt ast.CompactOptions) { + opts := opt.Entries() + seq.Values(opts)(func(opt ast.Option) bool { + legalizeOptionEntry(p, opt, opt.Span()) + return true + }) +} + +func legalizeOptionEntry(p *parser, opt ast.Option, span report.Span) { +} diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go new file mode 100644 index 00000000..cc4d9c91 --- /dev/null +++ b/experimental/parser/legalize_path.go @@ -0,0 +1,95 @@ +// 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" +) + +// pathOptions is configuration for [legalizePath]. +type pathOptions struct { + // If set, the path must be relative. + Relative bool + + // If set, the path may contain precisely one `/` separator. + AllowSlash bool + + // If set, the path may contain extension components. + AllowExts bool +} + +// 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 i int + var slash token.Token + path.Components(func(pc ast.PathComponent) bool { + if i == 0 && opts.Relative { + if !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 false + } + } + + 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 false + } else if !slash.IsZero() { + p.Errorf("unexpected `/` in path %s", where).Apply( + report.Snippet(pc.Separator()), + report.Snippetf(slash, "previous one is here"), + ) + ok = false + return false + } + slash = pc.Separator() + } + + if ext := pc.AsExtension(); !ext.IsZero() { + if opts.AllowExts { + ok = legalizePath(p, where, ext, pathOptions{ + Relative: false, + AllowExts: false, + }) + if !ok { + return false + } + } 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 false + } + } + + i++ + return true + }) + + return ok +} diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go new file mode 100644 index 00000000..249903e6 --- /dev/null +++ b/experimental/parser/legalize_type.go @@ -0,0 +1,26 @@ +// 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" +) + +func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { +} + +func legalizeFieldType(p *parser, ty ast.TypeAny) { +} 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 From 4c4129ef114772d4b6855b2d2142ee20a3a4d172 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:15:42 -0800 Subject: [PATCH 14/64] legalize syntax --- experimental/parser/legalize_file.go | 108 +++++++++++++++++- .../parser/syntax/2024.proto.stderr.txt | 8 ++ .../syntax/edition_proto2.proto.stderr.txt | 8 ++ .../parser/syntax/invalid.proto.stderr.txt | 8 ++ .../parser/syntax/lonely.proto.stderr.txt | 12 +- .../parser/syntax/not_first.proto.stderr.txt | 11 ++ .../parser/syntax/options.proto.stderr.txt | 7 ++ .../syntax/proto2_escaped.proto.stderr.txt | 12 ++ .../syntax/proto2_split.proto.stderr.txt | 14 +++ .../parser/syntax/proto4.proto.stderr.txt | 8 ++ .../syntax/syntax_2023.proto.stderr.txt | 8 ++ .../parser/syntax/unquoted.proto.stderr.txt | 11 ++ .../syntax/unquoted_edition.proto.stderr.txt | 11 ++ 13 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/not_first.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/options.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/proto2_escaped.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/unquoted.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/syntax/unquoted_edition.proto.stderr.txt diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index 940200fd..d7b8cef3 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -15,11 +15,17 @@ package parser import ( + "fmt" "regexp" "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/seq" + + "github.com/bufbuild/protocompile/experimental/ast/syntax" + "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/iterx" ) var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") @@ -53,10 +59,110 @@ func legalizeFile(p *parser, file ast.File) { // 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 { - if !first.IsZero() { + file := parent.Spanner.(ast.File) + switch { + case !first.IsZero(): + p.Errorf("unexpected %s", in).Apply( + report.Snippetf(decl, "help: remove this"), + report.Snippetf(*first, "previous declaration is here"), + report.Notef("a file may only 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"), + report.Notef("a %s must be the first declaration in a file", in), + ) + *first = decl + return + default: *first = decl } + } else { + p.Error(errBadNest{parent: parent, child: decl}) + return + } + + 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(`"%v"`, 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()}) } } 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..820636b1 --- /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..d7f12d4d --- /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/invalid.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt new file mode 100644 index 00000000..0905b53c --- /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 697e1e55..7c11be9a 100644 --- a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt @@ -4,10 +4,20 @@ error: unexpected `;` in `edition` declaration 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 + = note: a file may only 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 +encountered 3 errors 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..88af301b --- /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..c5d38f06 --- /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..e05999bd --- /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..54711d13 --- /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, + = note: like C or Python, which 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..24a7326a --- /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.stderr.txt b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt new file mode 100644 index 00000000..2d65daa6 --- /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/unquoted.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/unquoted.proto.stderr.txt new file mode 100644 index 00000000..714f46dd --- /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..eac41603 --- /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 From 02ac466db6f27b8e41f602c56c37b71d0e161a44 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:17:49 -0800 Subject: [PATCH 15/64] legalize package --- experimental/parser/legalize_file.go | 54 +++++++++++++++++-- .../testdata/parser/lists.proto.stderr.txt | 7 ++- .../parser/package/42.proto.stderr.txt | 10 +++- .../parser/package/absolute.proto.stderr.txt | 7 +++ .../parser/package/empty.proto.stderr.txt | 6 +++ .../package/eof_after_kw.proto.stderr.txt | 10 +++- .../parser/package/extension.proto.stderr.txt | 7 +++ .../package/host_qualified.proto.stderr.txt | 7 +++ .../parser/package/no_path.proto.stderr.txt | 9 ++++ .../parser/package/options.proto.stderr.txt | 7 +++ .../syntax/eof_after_eq.proto.stderr.txt | 7 ++- .../syntax/eof_after_kw.proto.stderr.txt | 7 ++- .../parser/syntax/lonely.proto.stderr.txt | 17 +++++- 13 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 experimental/parser/testdata/parser/package/absolute.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/empty.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/extension.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/host_qualified.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/no_path.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/options.proto.stderr.txt diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index d7b8cef3..f1a88e6c 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -19,11 +19,10 @@ import ( "regexp" "github.com/bufbuild/protocompile/experimental/ast" - "github.com/bufbuild/protocompile/experimental/internal/taxa" - "github.com/bufbuild/protocompile/experimental/seq" - "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" ) @@ -51,6 +50,14 @@ func legalizeFile(p *parser, file ast.File) { return true }) + + if pkg.IsZero() { + p.Warnf("missing %s", taxa.Package).Apply( + report.InFile(p.Stream().Path()), + report.Notef("failing to specify a package places the file in the empty package"), + report.Notef("using the empty package is discouraged"), + ) + } } // legalizeSyntax legalizes a DeclSyntax. @@ -172,6 +179,47 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax // 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 { + file := parent.Spanner.(ast.File) + switch { + case !first.IsZero(): + p.Errorf("unexpected %s", taxa.Package).Apply( + report.Snippetf(decl, "help: remove this"), + report.Snippetf(*first, "previous declaration is here"), + 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.Errorf("unexpected %s", taxa.Package).Apply( + report.Snippet(decl), + report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"), + report.Notef("a %s must be the first declaration in a file, or follow the `syntax` or `edition` declaration", taxa.Package), + ) + return + } + *first = decl + default: + *first = decl + } + } else { + p.Error(errBadNest{parent: parent, child: decl}) + return + } + + 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 empty package, remove the %s", taxa.Package), + report.Helpf("however, using the empty package is discouraged"), + ) + } + + legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{Relative: true}) } // legalizeImport legalizes a DeclImport. diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index e2c5c2d4..38c1e878 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -1,3 +1,8 @@ +warning: missing `package` declaration + --> testdata/parser/lists.proto + = note: failing to specify a package places the file in the empty package + = note: using the empty package is discouraged + error: unexpected integer literal in array expression --> testdata/parser/lists.proto:20:20 | @@ -318,4 +323,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 46 errors +encountered 46 errors and 1 warning diff --git a/experimental/parser/testdata/parser/package/42.proto.stderr.txt b/experimental/parser/testdata/parser/package/42.proto.stderr.txt index 1f7730a2..49eed374 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -1,3 +1,11 @@ +error: missing path in `package` declaration + --> testdata/parser/package/42.proto:19:1 + | +19 | package 42; + | ^^^^^^^ + = help: to place a file in the empty package, remove the `package` declaration + = help: however, using the empty package is discouraged + error: unexpected integer literal after `package` declaration --> testdata/parser/package/42.proto:19:9 | @@ -10,4 +18,4 @@ error: unexpected integer literal in file scope 19 | 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..7d81eb29 --- /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..2a7657f5 --- /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: failing to specify a package places the file in the empty package + = note: using the empty package 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 1ef240de..10400fe2 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 empty package, remove the `package` declaration + = help: however, using the empty 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..932b6703 --- /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..450895cc --- /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..0130f5d5 --- /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 empty package, remove the `package` declaration + = help: however, using the empty 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..a7a73f73 --- /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/syntax/eof_after_eq.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt index 8c24e2e4..9eb76cef 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: failing to specify a package places the file in the empty package + = note: using the empty package 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 faee0824..1a00a2d5 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: failing to specify a package places the file in the empty package + = note: using the empty package 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/lonely.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt index 7c11be9a..23db3617 100644 --- a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt @@ -1,3 +1,8 @@ +warning: missing `package` declaration + --> testdata/parser/syntax/lonely.proto + = note: failing to specify a package places the file in the empty package + = note: using the empty package is discouraged + error: unexpected `;` in `edition` declaration --> testdata/parser/syntax/lonely.proto:15:8 | @@ -20,4 +25,14 @@ error: unexpected `;` in `syntax` declaration 17 | syntax = ; | ^ expected expression -encountered 3 errors +error: unexpected `package` declaration + --> testdata/parser/syntax/lonely.proto:19:1 + | +17 | syntax = ; + | ---------- previous declaration is here +18 | +19 | package test; + | ^^^^^^^^^^^^^ + = note: a `package` declaration must be the first declaration in a file, or follow the `syntax` or `edition` declaration + +encountered 4 errors and 1 warning From 96efead16ce80b06bc753a413183e7362dba0dbc Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:20:30 -0800 Subject: [PATCH 16/64] legalize imports --- experimental/parser/legalize_file.go | 92 +++++++++++++++++++ .../parser/import/42.proto.stderr.txt | 7 ++ .../parser/import/escapes.proto.stderr.txt | 25 +++++ .../parser/import/in_message.proto.stderr.txt | 45 +++++++++ .../parser/import/no_path.proto.stderr.txt | 19 ++++ .../parser/import/ok.proto.stderr.txt | 8 ++ .../parser/import/options.proto.stderr.txt | 26 ++++++ .../parser/import/repeated.proto.stderr.txt | 9 ++ .../parser/import/symbol.proto.stderr.txt | 25 +++++ 9 files changed, 256 insertions(+) create mode 100644 experimental/parser/testdata/parser/import/42.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/escapes.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/in_message.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/no_path.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/ok.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/options.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/repeated.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/import/symbol.proto.stderr.txt diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index f1a88e6c..0aa32910 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -227,4 +227,96 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka // 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}) + 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 a 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"), + report.Notef("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/testdata/parser/import/42.proto.stderr.txt b/experimental/parser/testdata/parser/import/42.proto.stderr.txt new file mode 100644 index 00000000..770fbf6d --- /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..a7a8b1b4 --- /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, + = note: like C or Python, which can lead to surprising behavior + + encountered 2 warnings 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..601fc97f --- /dev/null +++ b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt @@ -0,0 +1,45 @@ +error: unexpected import within message definition + --> testdata/parser/import/in_message.proto:20:5 + | +19 | / message M { +20 | | import "foo.proto"; + | | ^^^^^^^^^^^^^^^^^^^ this import... +... | +25 | | } + | \_- ...cannot be declared within this message definition + +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... +... | +25 | | } + | \_- ...cannot be declared within this message definition + +error: unexpected weak import within message definition + --> testdata/parser/import/in_message.proto:22:5 + | +19 | / message M { +... | +22 | | import weak "foo.proto"; + | | ^^^^^^^^^^^^^^^^^^^^^^^^ this weak import... +23 | | +... | +25 | | } + | \_- ...cannot be declared within this message definition + +error: unexpected import within message definition + --> testdata/parser/import/in_message.proto:24:5 + | +19 | / message M { +... | +23 | | +24 | | import foo.proto; + | | ^^^^^^^^^^^^^^^^^ this import... +25 | | } + | \_- ...cannot be declared within this message definition + +encountered 4 errors 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..07f66cbd --- /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..e6560c40 --- /dev/null +++ b/experimental/parser/testdata/parser/import/ok.proto.stderr.txt @@ -0,0 +1,8 @@ +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..23c3a609 --- /dev/null +++ b/experimental/parser/testdata/parser/import/options.proto.stderr.txt @@ -0,0 +1,26 @@ +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.stderr.txt b/experimental/parser/testdata/parser/import/repeated.proto.stderr.txt new file mode 100644 index 00000000..b1425045 --- /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..77198636 --- /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 + = note: 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 + = note: 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 + = note: instead, try importing a file, e.g. `import "google/protobuf/descriptor.proto";` + +encountered 3 errors From 86033233834ca1d3da6695a828757afd43d15c1b Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:24:23 -0800 Subject: [PATCH 17/64] legalize against bare braces --- experimental/parser/legalize_decl.go | 12 +++++++ .../parser/def/bare_bodies.proto.stderr.txt | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 49b70d6e..3e92b522 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -17,6 +17,7 @@ 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/seq" ) @@ -34,6 +35,17 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { 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. 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..9e61c73d --- /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 From ce87eb6cd3248151592caf5c8ea777d22426eb73 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:26:25 -0800 Subject: [PATCH 18/64] legalize ranges --- experimental/parser/legalize_decl.go | 79 +++++++++++++++++++ .../testdata/parser/lists.proto.stderr.txt | 44 ++++++++++- .../parser/range/escapes.proto.stderr.txt | 31 ++++++++ .../range/invalid_parent.proto.stderr.txt | 50 ++++++++++++ .../parser/range/options.proto.stderr.txt | 7 ++ .../reserved_default_syntax.proto.stderr.txt | 7 ++ .../range/reserved_edition.proto.stderr.txt | 11 +++ .../range/reserved_syntax.proto.stderr.txt | 11 +++ 8 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 experimental/parser/testdata/parser/range/escapes.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/options.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 3e92b522..86493040 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -67,4 +67,83 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { } func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { + in := taxa.Extensions + if decl.IsReserved() { + in = taxa.Reserved + } + + var validParent bool + var isEnum bool + switch parent.what { + case taxa.Message: + validParent = true + case taxa.Enum: + isEnum = true + validParent = in == taxa.Reserved + } + if !validParent { + p.Error(errBadNest{parent: parent, child: decl}) + return + } + + if options := decl.Options(); !options.IsZero() { + if in == taxa.Reserved { + p.Error(errHasOptions{decl}) + } else { + legalizeCompactOptions(p, options) + } + } + + field := taxa.Field + if isEnum { + field = taxa.EnumValue + } + + // We only legalize reserved name productions here, because that depends on + // the syntax/edition keyword. All other expressions are legalized when we + // do constant evaluation. + + if in != taxa.Reserved { + return + } + + seq.Values(decl.Ranges())(func(expr ast.ExprAny) bool { + switch expr.Kind() { + case ast.ExprKindPath: + path := expr.AsPath() + if in == taxa.Reserved && !path.AsIdent().IsZero() { + if m := p.Mode(); m == taxa.SyntaxMode { + 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), + ) + } + } + + case ast.ExprKindLiteral: + lit := expr.AsLiteral() + if name, ok := lit.AsString(); ok { + if m := p.Mode(); m == taxa.EditionMode { + 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), + ) + return true + } + + if !isASCIIIdent(name) { + p.Errorf("reserved %v name is not a valid identifier", field).Apply( + report.Snippet(expr), + ) + return true + } + + if !lit.IsPureString() { + p.Warn(errImpureString{lit.Token, in.In()}) + } + } + } + + return true + }) } diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 38c1e878..1eeb5105 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -287,6 +287,12 @@ 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 {}; + | ^ + error: unexpected message expression in reserved range --> testdata/parser/lists.proto:72:16 | @@ -301,6 +307,24 @@ 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; + | ^ + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:75:17 + | +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; + | ^ + error: unexpected identifier in reserved range --> testdata/parser/lists.proto:75:19 | @@ -309,6 +333,24 @@ 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 + | ^ + +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/lists.proto:76:17 + | +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 + | ^ + error: unexpected identifier in reserved range --> testdata/parser/lists.proto:76:19 | @@ -323,4 +365,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 46 errors and 1 warning +encountered 53 errors and 1 warning 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..1dd0c07c --- /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, + = note: like C or Python, which 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/invalid_parent.proto.stderr.txt b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt new file mode 100644 index 00000000..f20fe293 --- /dev/null +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt @@ -0,0 +1,50 @@ +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 + +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 + +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 + +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 + +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 + +encountered 5 errors 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..47601631 --- /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.stderr.txt b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt new file mode 100644 index 00000000..e39ab3f1 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt @@ -0,0 +1,7 @@ +error: cannot use identifiers in reserved range in syntax mode + --> testdata/parser/range/reserved_default_syntax.proto:18:14 + | +18 | reserved foo, "foo"; + | ^^^ + +encountered 1 error 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..6d3b5986 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt @@ -0,0 +1,11 @@ +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 +16 | +... +20 | reserved foo, "foo"; + | ^^^^^ + +encountered 1 error 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..bacba80f --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt @@ -0,0 +1,11 @@ +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 +16 | +... +20 | reserved foo, "foo"; + | ^^^ + +encountered 1 error From e1382568a213ec9a4d6ad113e26aed2f80c88b8c Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:40:08 -0800 Subject: [PATCH 19/64] legalize parent relationships for defs --- experimental/parser/legalize_def.go | 26 +- .../parser/def/nesting.proto.stderr.txt | 237 ++++++++++++++++++ 2 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 experimental/parser/testdata/parser/def/nesting.proto.stderr.txt diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 82b4e9c2..311f49b8 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -19,13 +19,35 @@ import ( "github.com/bufbuild/protocompile/experimental/internal/taxa" ) -func legalizeDef(p *parser, parent classified, def ast.DeclDef) { - kind := def.Classify() +// 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, + ), +} +func legalizeDef(p *parser, parent classified, def ast.DeclDef) { if def.IsCorrupt() { return } + kind := def.Classify() + if !validDefParents[kind].Has(parent.what) { + p.Error(errBadNest{parent: parent, child: def}) + } + switch kind { case ast.DefKindMessage, ast.DefKindEnum, ast.DefKindService, ast.DefKindOneof, ast.DefKindExtend: legalizeTypeDefLike(p, taxa.Classify(def), def) 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..8fbf5358 --- /dev/null +++ b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt @@ -0,0 +1,237 @@ +error: unexpected service definition within message definition + --> testdata/parser/def/nesting.proto:22:5 + | +19 | / message M { +... | +22 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +... | +25 | | } + | \_- ...cannot be declared within this message definition + +error: unexpected message definition within enum definition + --> testdata/parser/def/nesting.proto:28:5 + | +27 | / enum E { +28 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +... | +33 | | } + | \_- ...cannot be declared within this enum 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... +... | +33 | | } + | \_- ...cannot be declared within this enum definition + +error: unexpected service definition within enum definition + --> testdata/parser/def/nesting.proto:30:5 + | +27 | / enum E { +... | +30 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +... | +33 | | } + | \_- ...cannot be declared within this enum definition + +error: unexpected message extension block within enum definition + --> testdata/parser/def/nesting.proto:31:5 + | +27 | / enum E { +... | +31 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +32 | | oneof O {} +33 | | } + | \_- ...cannot be declared within this enum definition + +error: unexpected oneof definition within enum definition + --> testdata/parser/def/nesting.proto:32:5 + | +27 | / enum E { +... | +32 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +33 | | } + | \_- ...cannot be declared within this enum definition + +error: unexpected message definition within service definition + --> testdata/parser/def/nesting.proto:36:5 + | +35 | / service S { +36 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +... | +41 | | } + | \_- ...cannot be declared within this service 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... +... | +41 | | } + | \_- ...cannot be declared within this service definition + +error: unexpected service definition within service definition + --> testdata/parser/def/nesting.proto:38:5 + | +35 | / service S { +... | +38 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +... | +41 | | } + | \_- ...cannot be declared within this service definition + +error: unexpected message extension block within service definition + --> testdata/parser/def/nesting.proto:39:5 + | +35 | / service S { +... | +39 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +40 | | oneof O {} +41 | | } + | \_- ...cannot be declared within this service definition + +error: unexpected oneof definition within service definition + --> testdata/parser/def/nesting.proto:40:5 + | +35 | / service S { +... | +40 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +41 | | } + | \_- ...cannot be declared within this service 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... +... | +49 | | } + | \_- ...cannot be declared within this message extension block + +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... +... | +49 | | } + | \_- ...cannot be declared within this message extension block + +error: unexpected service definition within message extension block + --> testdata/parser/def/nesting.proto:46:5 + | +43 | / extend E { +... | +46 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +... | +49 | | } + | \_- ...cannot be declared within this message extension block + +error: unexpected message extension block within message extension block + --> testdata/parser/def/nesting.proto:47:5 + | +43 | / extend E { +... | +47 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +48 | | oneof O {} +49 | | } + | \_- ...cannot be declared within this message extension block + +error: unexpected oneof definition within message extension block + --> testdata/parser/def/nesting.proto:48:5 + | +43 | / extend E { +... | +48 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +49 | | } + | \_- ...cannot be declared within this message extension block + +error: unexpected oneof definition within file scope + --> testdata/parser/def/nesting.proto:51:1 + | +15 | / syntax = "proto2"; +16 | | +... | +50 | | +51 | | / oneof O { +... | | +57 | | | } + | \___- ...cannot be declared within this file scope + | \_^ this oneof definition... + +error: unexpected message definition within oneof definition + --> testdata/parser/def/nesting.proto:52:5 + | +51 | / oneof O { +52 | | message M {} + | | ^^^^^^^^^^^^ this message definition... +... | +57 | | } + | \_- ...cannot be declared within this oneof 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... +... | +57 | | } + | \_- ...cannot be declared within this oneof definition + +error: unexpected service definition within oneof definition + --> testdata/parser/def/nesting.proto:54:5 + | +51 | / oneof O { +... | +54 | | service S {} + | | ^^^^^^^^^^^^ this service definition... +... | +57 | | } + | \_- ...cannot be declared within this oneof definition + +error: unexpected message extension block within oneof definition + --> testdata/parser/def/nesting.proto:55:5 + | +51 | / oneof O { +... | +55 | | extend E {} + | | ^^^^^^^^^^^ this message extension block... +56 | | oneof O {} +57 | | } + | \_- ...cannot be declared within this oneof definition + +error: unexpected oneof definition within oneof definition + --> testdata/parser/def/nesting.proto:56:5 + | +51 | / oneof O { +... | +56 | | oneof O {} + | | ^^^^^^^^^^ this oneof definition... +57 | | } + | \_- ...cannot be declared within this oneof definition + +encountered 22 errors From 35f4923278e5beee28118df47c3a4b7a86870a4b Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 14:54:50 -0800 Subject: [PATCH 20/64] legalize messages and friends --- experimental/parser/legalize_decl.go | 4 +- experimental/parser/legalize_def.go | 50 ++++++++++ .../parser/testdata/parser/def/bad_path.proto | 32 ++++++ .../parser/def/bad_path.proto.stderr.txt | 63 ++++++++++++ .../testdata/parser/def/bad_path.proto.yaml | 63 ++++++++++++ .../parser/testdata/parser/def/mixed.proto | 41 ++++++++ .../parser/def/mixed.proto.stderr.txt | 91 +++++++++++++++++ .../testdata/parser/def/mixed.proto.yaml | 98 +++++++++++++++++++ 8 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 experimental/parser/testdata/parser/def/bad_path.proto create mode 100644 experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/def/bad_path.proto.yaml create mode 100644 experimental/parser/testdata/parser/def/mixed.proto create mode 100644 experimental/parser/testdata/parser/def/mixed.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/def/mixed.proto.yaml diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 86493040..9a012b20 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -55,10 +55,10 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { case ast.DeclKindDef: def := decl.AsDef() - legalizeDef(p, parent, def) - body := def.Body() 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 diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 311f49b8..4b6661b6 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -17,6 +17,7 @@ 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. @@ -63,6 +64,55 @@ func legalizeDef(p *parser, parent classified, def ast.DeclDef) { // legalizeMessageLike 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{}) + + case what != taxa.Extend && def.Name().AsIdent().IsZero(): + def.MarkCorrupt() + kw := taxa.Keyword(def.Keyword().Text()) + p.Error(errUnexpected{ + what: def.Name(), + where: kw.After(), + want: taxa.Ident.AsSet(), + }).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}) + } } // legalizeMessageLike legalizes something that resembles a field definition: 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..06ffd3b1 --- /dev/null +++ b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt @@ -0,0 +1,63 @@ +error: unexpected qualified name after `message` + --> testdata/parser/def/bad_path.proto:19:9 + | +19 | message foo.Bar { + | ^^^^^^^ expected identifier + = note: the name of a message definition must be a single identifier + +error: unexpected qualified name after `oneof` + --> testdata/parser/def/bad_path.proto:20:11 + | +20 | oneof foo.Bar {} + | ^^^^^^^ expected identifier + = note: the name of a oneof definition must be a single identifier + +error: unexpected qualified name after `oneof` + --> testdata/parser/def/bad_path.proto:21:11 + | +21 | oneof foo.(bar.baz).Bar {} + | ^^^^^^^^^^^^^^^^^ expected identifier + = note: the name of a oneof definition must be a single identifier + +error: unexpected qualified name after `message` + --> testdata/parser/def/bad_path.proto:23:9 + | +23 | message foo.(bar.baz).Bar {} + | ^^^^^^^^^^^^^^^^^ expected identifier + = note: the name of a message definition must be a single identifier + +error: unexpected qualified name after `enum` + --> testdata/parser/def/bad_path.proto:25:6 + | +25 | enum foo.Bar {} + | ^^^^^^^ expected identifier + = note: the name of a enum definition must be a single identifier + +error: unexpected qualified name after `enum` + --> testdata/parser/def/bad_path.proto:26:6 + | +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 qualified name after `service` + --> testdata/parser/def/bad_path.proto:31:9 + | +31 | service foo.Bar {} + | ^^^^^^^ expected identifier + = note: the name of a service definition must be a single identifier + +error: unexpected qualified name after `service` + --> testdata/parser/def/bad_path.proto:32:9 + | +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/mixed.proto b/experimental/parser/testdata/parser/def/mixed.proto new file mode 100644 index 00000000..3bdf5fb3 --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto @@ -0,0 +1,41 @@ +// 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) {} \ 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..5324bbc3 --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt @@ -0,0 +1,91 @@ +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 + +encountered 15 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..7d9da1f2 --- /dev/null +++ b/experimental/parser/testdata/parser/def/mixed.proto.yaml @@ -0,0 +1,98 @@ +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: {} From f79602425b344777c835c86d437162564cbe662c Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 15:02:19 -0800 Subject: [PATCH 21/64] legalize fields --- experimental/ast/path.go | 6 +- experimental/parser/legalize_def.go | 39 ++++++++ .../parser/def/ordering.proto.stderr.txt | 98 ++++++++++++++++++- .../testdata/parser/def/ordering.proto.yaml | 5 +- .../parser/enum/bad-path.proto.stderr.txt | 19 ++++ .../testdata/parser/enum/bad-path.proto.yaml | 3 - .../parser/field/bad-path.proto.stderr.txt | 73 ++++++++++++++ .../testdata/parser/field/bad-path.proto.yaml | 12 --- .../parser/field/incomplete.proto.stderr.txt | 14 ++- .../parser/field/incomplete.proto.yaml | 2 - go.work.sum | 2 + 11 files changed, 249 insertions(+), 24 deletions(-) create mode 100644 experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt diff --git a/experimental/ast/path.go b/experimental/ast/path.go index fd100bed..aa94f70d 100644 --- a/experimental/ast/path.go +++ b/experimental/ast/path.go @@ -276,7 +276,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/parser/legalize_def.go b/experimental/parser/legalize_def.go index 4b6661b6..a48867cb 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -118,6 +118,45 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { // legalizeMessageLike 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(), + }) + } + + // NOTE: We do not legalize a missing value for fields and enum values + // here; instead, that happens during IR lowering. This is because we want + // to be able to include a suggested field number, but we cannot do that + // until much later, when we have evaluated expressions. + + 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) } diff --git a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index a9dcddcd..88f098b2 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt @@ -1,3 +1,9 @@ +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 +12,12 @@ error: encountered more than one method parameter list | | | first one is here +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 +26,12 @@ error: unexpected method parameter list after method return type | | | previous method return type is here +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 +46,12 @@ error: unexpected method parameter list after method return type | | | previous method return type is here +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 +60,24 @@ error: unexpected method parameter list after compact options | | | previous compact options is here +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:24:9 + | +24 | M x { /* ... */ } (T); + | ^^^^^^^^^^^^^ + +error: missing name in message field + --> testdata/parser/def/ordering.proto:24:23 + | +24 | M x { /* ... */ } (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 +86,12 @@ error: encountered more than one method return type | | | first one is here +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 +100,24 @@ error: unexpected method return type after compact options | | | previous compact options is here +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:28:9 + | +28 | M x { /* ... */ } returns (T); + | ^^^^^^^^^^^^^ + +error: unexpected qualified name in message field + --> testdata/parser/def/ordering.proto:28:31 + | +28 | M x { /* ... */ } returns (T); + | ^^^ expected identifier + +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,6 +132,12 @@ error: encountered more than one method return type | | | first one is here +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 | @@ -80,6 +152,12 @@ error: unexpected method return type after compact options | | | previous compact options is here +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 +172,12 @@ error: missing `(...)` around method return type 32 | M x [foo = bar] returns T; | ^ help: replace this with `(T)` +error: unexpected definition body in message field + --> testdata/parser/def/ordering.proto:33:9 + | +33 | M x { /* ... */ } returns T; + | ^^^^^^^^^^^^^ + error: encountered more than one message field tag --> testdata/parser/def/ordering.proto:35:13 | @@ -110,6 +194,12 @@ error: unexpected message field tag after compact options | | | previous compact options is here +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 | @@ -124,10 +214,16 @@ error: encountered more than one compact options | | | first one is here +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 34 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..fbc5bafb --- /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 qualified 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/field/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt new file mode 100644 index 00000000..3bab4157 --- /dev/null +++ b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt @@ -0,0 +1,73 @@ +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:25:27 + | +25 | optional package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:26:27 + | +26 | repeated package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:27:27 + | +27 | required package.Type path.name = 1; + | ^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:35:27 + | +35 | optional package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:36:27 + | +36 | repeated package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:37:27 + | +37 | required package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:38:18 + | +38 | package.Type (foo.bar).name = 1; + | ^^^^^^^^^^^^^^ expected identifier + +error: unexpected qualified name in message field + --> testdata/parser/field/bad-path.proto:40:11 + | +40 | (foo) (bar) = 1; + | ^^^^^ expected identifier + +encountered 12 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..dbfd35f2 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.yaml +++ b/experimental/parser/testdata/parser/field/bad-path.proto.yaml @@ -6,47 +6,40 @@ 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_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 @@ -92,7 +85,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 +93,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 +101,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 +109,12 @@ 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 diff --git a/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt index 19e196dd..2eff91cb 100644 --- a/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt @@ -1,3 +1,15 @@ +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 | @@ -16,4 +28,4 @@ error: unexpected `}` after definition 27 | } | ^ expected `;` -encountered 3 errors +encountered 5 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/go.work.sum b/go.work.sum index 04d04b20..85b723a6 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/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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= @@ -160,6 +161,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/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= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= From 0eb82c2546f1bbfb907ee0ac95188b98530620c6 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 15:12:12 -0800 Subject: [PATCH 22/64] legalize options --- experimental/parser/legalize_def.go | 15 ++++++ experimental/parser/legalize_option.go | 29 +++++++++++ experimental/parser/parse_type.go | 3 +- .../parser/def/ordering.proto.stderr.txt | 8 ++- .../parser/field/options.proto.stderr.txt | 8 ++- .../testdata/parser/option/bad_path.proto | 31 ++++++++++++ .../parser/option/bad_path.proto.stderr.txt | 49 ++++++++++++++++++ .../parser/option/bad_path.proto.yaml | 50 +++++++++++++++++++ .../parser/testdata/parser/option/ok.proto | 31 ++++++++++++ .../testdata/parser/option/ok.proto.yaml | 49 ++++++++++++++++++ 10 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 experimental/parser/testdata/parser/option/bad_path.proto create mode 100644 experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/option/bad_path.proto.yaml create mode 100644 experimental/parser/testdata/parser/option/ok.proto create mode 100644 experimental/parser/testdata/parser/option/ok.proto.yaml diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index a48867cb..35494164 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -167,6 +167,21 @@ func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef) { } 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()) } diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index b4c958c2..e7e8f989 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -16,12 +16,20 @@ 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/seq" ) func legalizeCompactOptions(p *parser, opt ast.CompactOptions) { opts := opt.Entries() + if opts.Len() == 0 { + p.Errorf("%s cannot be empty", taxa.CompactOptions).Apply( + report.Snippetf(opt, "help: remove this"), + ) + return + } + seq.Values(opts)(func(opt ast.Option) bool { legalizeOptionEntry(p, opt, opt.Span()) return true @@ -29,4 +37,25 @@ func legalizeCompactOptions(p *parser, opt ast.CompactOptions) { } func legalizeOptionEntry(p *parser, opt ast.Option, span report.Span) { + // 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. + + if opt.Path.IsZero() { + p.Errorf("missing %v path", taxa.Option).Apply( + report.Snippet(span), + ) + return + } else { + legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{ + Relative: true, + AllowExts: true, + }) + } + + if opt.Value.IsZero() { + p.Errorf("missing %v", taxa.OptionValue).Apply( + report.Snippet(span), + ) + } } 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/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index 88f098b2..60a1924d 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt @@ -144,6 +144,12 @@ error: missing `(...)` around method return type 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 | @@ -226,4 +232,4 @@ error: unexpected `[...]` in message definition 40 | M x { /* ... */ } [foo = bar]; | ^^^^^^^^^^^ expected identifier, `;`, `.`, `(...)`, or `{...}` -encountered 34 errors +encountered 35 errors diff --git a/experimental/parser/testdata/parser/field/options.proto.stderr.txt b/experimental/parser/testdata/parser/field/options.proto.stderr.txt index 14238d54..c2a56324 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/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..d1541a5e --- /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 From c7f6c7e0ce11e2c0317985495c9a18ee64dc598b Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 15:14:57 -0800 Subject: [PATCH 23/64] legalize methods --- experimental/parser/legalize_def.go | 63 +++++++++++++++++++ .../parser/method/incomplete.proto.stderr.txt | 20 +++++- .../parser/method/incomplete.proto.yaml | 6 +- .../parser/method/options.proto.stderr.txt | 9 +++ 4 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 experimental/parser/testdata/parser/method/options.proto.stderr.txt diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 35494164..92ef74f6 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -186,4 +186,67 @@ func legalizeOption(p *parser, def ast.DeclDef) { } 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.Snippet(def), + ) + } else { + legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns) + } + + if sig.Outputs().Span().IsZero() { + def.MarkCorrupt() + p.Errorf("missing %v in %v", taxa.MethodOuts, taxa.Method).Apply( + report.Snippet(def), + ) + } else { + legalizeMethodParams(p, sig.Outputs(), taxa.MethodOuts) + } + } + + // Methods are unique in that they can be either end in 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", taxa.KeywordOption), + report.Notef("declarations in the %v following the method definition", taxa.Braces), + // TODO: Generate a suggestion for this. + ) + } } diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index 094e4b74..29c7d285 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -4,6 +4,24 @@ error: missing `(...)` around method return type 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:5 + | +21 | rpc Bar2(foo.Bar); + | ^^^^^^^^^^^^^^^^^^ + +error: missing method parameter list in service method + --> testdata/parser/method/incomplete.proto:22:5 + | +22 | rpc Bar3 returns (foo.Bar); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: missing method return type in service method + --> testdata/parser/method/incomplete.proto:25:5 + | +25 | rpc Bar6() returns; + | ^^^^^^^^^^^^^^^^^^^ + error: unexpected `;` after `returns` --> testdata/parser/method/incomplete.proto:25:23 | @@ -16,4 +34,4 @@ error: missing `(...)` around method return type 26 | rpc Bar7() returns stream foo.Bar; | ^^^^^^^^^^^^^^ help: replace this with `(stream foo.Bar)` -encountered 3 errors +encountered 6 errors diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.yaml b/experimental/parser/testdata/parser/method/incomplete.proto.yaml index 80ac6545..eb7143f0 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.yaml +++ b/experimental/parser/testdata/parser/method/incomplete.proto.yaml @@ -14,13 +14,13 @@ decls: outputs: - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] - def: - kind: KIND_METHOD name.components: [{ ident: "Bar2" }] + type.path.components: [{ ident: "rpc" }] signature.inputs: - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] - def: - kind: KIND_METHOD name.components: [{ ident: "Bar3" }] + type.path.components: [{ ident: "rpc" }] signature.outputs: - path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] - def: @@ -37,8 +37,8 @@ decls: prefix: PREFIX_STREAM type.path.components: [{ ident: "foo" }, { ident: "Bar", separator: SEPARATOR_DOT }] - def: - kind: KIND_METHOD name.components: [{ ident: "Bar6" }] + type.path.components: [{ ident: "rpc" }] signature: {} - def: kind: KIND_METHOD 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..434a6b99 --- /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` + = note: declarations in the `{...}` following the method definition + +encountered 1 error From 6575e524b45d324bdb60330ff0e678af3632d534 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 29 Jan 2025 15:16:04 -0800 Subject: [PATCH 24/64] legalize types --- experimental/parser/legalize_type.go | 107 ++++++++++++ .../testdata/parser/lists.proto.stderr.txt | 140 ++++++++++++++- .../parser/method/bad_type.proto.stderr.txt | 50 +++++- .../parser/method/incomplete.proto.stderr.txt | 26 ++- .../parser/type/generic.proto.stderr.txt | 128 +++++++++++++- .../parser/type/repeated.proto.stderr.txt | 164 +++++++++++++++++- 6 files changed, 610 insertions(+), 5 deletions(-) diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index 249903e6..720b42f9 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -16,11 +16,118 @@ 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" ) 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: + // Allow all path types. We don't know if this is a message or not yet; + // figuring that out is a problem for IR construction. + 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 { + break + } + + ty = prefixed.Type() + fallthrough + default: + p.Errorf("only message types may appear in %s", what).Apply( + report.Snippet(ty), + ) + } } func legalizeFieldType(p *parser, ty ast.TypeAny) { + switch ty.Kind() { + case ast.TypeKindPath: + break + + 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: + break + 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("map keys may be of any of the following types:"), + report.Helpf("%s", iterx.Join(iterx.Filter( + predeclared.All(), + func(p predeclared.Name) bool { return p.IsMapKey() }, + ), ", ")), + ) + } + + switch v.Kind() { + case ast.TypeKindPath: + break + 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/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 1eeb5105..7c8b1f0e 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -129,6 +129,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 | @@ -137,6 +155,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 | @@ -145,24 +169,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 | @@ -177,18 +225,64 @@ 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: expected exactly two type arguments, got 1 + --> testdata/parser/lists.proto:53:8 + | +53 | map x; + | ^^^^^ + +error: unexpected non-comparable type in map key + --> testdata/parser/lists.proto:54:9 + | +54 | map x; + | ^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + +error: unexpected non-comparable type in map key + --> testdata/parser/lists.proto:55:9 + | +55 | map x; + | ^^^ + = help: map keys may be of any of the following types: + = help: 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 | @@ -197,6 +291,14 @@ error: unexpected type name in type parameters | | | note: assuming a missing `,` here +error: unexpected non-comparable type in map key + --> testdata/parser/lists.proto:56:9 + | +56 | map x; + | ^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + error: unexpected extra `,` in type parameters --> testdata/parser/lists.proto:56:13 | @@ -205,24 +307,60 @@ error: unexpected extra `,` in type parameters | | | first delimiter is here +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: expected exactly two type arguments, got 0 + --> testdata/parser/lists.proto:58:8 + | +58 | map<> 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 + --> testdata/parser/lists.proto:59:10 + | +59 | map<,int, int> x; + | ^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + +error: unexpected non-comparable type in map key + --> testdata/parser/lists.proto:60:9 + | +60 | map x; + | ^^^ + = help: map keys may be of any of the following types: + = help: 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: unexpected non-comparable type in map key + --> testdata/parser/lists.proto:62:9 + | +62 | int, + | ^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + error: unexpected trailing `,` in type parameters --> testdata/parser/lists.proto:63:12 | @@ -365,4 +503,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 53 errors and 1 warning +encountered 74 errors and 1 warning diff --git a/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt index 72bd6ac6..ea3b2f5d 100644 --- a/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt @@ -1,7 +1,55 @@ +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)` -encountered 1 error +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); + | ^^^^^^^^^^^^^^^^^^^^ + +encountered 9 errors diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index 29c7d285..2a9b1660 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -16,22 +16,46 @@ error: missing method parameter list in service method 22 | rpc Bar3 returns (foo.Bar); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +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: missing method return type in service method --> testdata/parser/method/incomplete.proto:25:5 | 25 | rpc Bar6() returns; | ^^^^^^^^^^^^^^^^^^^ +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/method/incomplete.proto:25:13 + | +25 | rpc Bar6() returns; + | ^^ + 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 6 errors +encountered 10 errors diff --git a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt index 5a2c5f1a..1b381071 100644 --- a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt +++ b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt @@ -1,3 +1,87 @@ +error: unexpected non-comparable type in map key + --> testdata/parser/type/generic.proto:21:9 + | +21 | map x2 = 2; + | ^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + +error: unexpected non-comparable type in map key + --> testdata/parser/type/generic.proto:22:9 + | +22 | map x3 = 3; + | ^^^^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + +error: unexpected type in map value + --> 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 + --> testdata/parser/type/generic.proto:34:17 + | +34 | map x11 = 11; + | ^^^^^^^^ + +error: unexpected non-comparable type in map key + --> testdata/parser/type/generic.proto:35:9 + | +35 | map x12 = 12; + | ^^^^^^^^^^^^^^^^ + = help: map keys may be of any of the following types: + = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + +error: unexpected `required` in map value + --> 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 +90,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 5a242fe3..8290ac41 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 From 69291760bc154721d232c84d47c7e6564d12f04f Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 30 Jan 2025 10:03:05 -0800 Subject: [PATCH 25/64] fix zero_test --- experimental/ast/zero_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From fc14e67d89c6e78dbe1896d2ddcace7e76497e6d Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 30 Jan 2025 10:14:52 -0800 Subject: [PATCH 26/64] lint --- experimental/ast/decl_body.go | 2 +- experimental/ast/decl_def.go | 3 +-- experimental/parser/diagnostics_internal.go | 3 +-- experimental/parser/legalize_file.go | 6 +++--- experimental/parser/legalize_option.go | 9 ++++----- experimental/parser/legalize_path.go | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/experimental/ast/decl_body.go b/experimental/ast/decl_body.go index 9b7c0a99..611c4715 100644 --- a/experimental/ast/decl_body.go +++ b/experimental/ast/decl_body.go @@ -78,7 +78,7 @@ func (d DeclBody) Span() report.Span { } } -// Body implements [HasBody] +// Body implements [HasBody]. func (d DeclBody) Body() DeclBody { return d } diff --git a/experimental/ast/decl_def.go b/experimental/ast/decl_def.go index b3336752..80b18a67 100644 --- a/experimental/ast/decl_def.go +++ b/experimental/ast/decl_def.go @@ -238,8 +238,7 @@ func (d DeclDef) IsCorrupt() bool { return !d.IsZero() && d.raw.corrupt } -// MarkCorrupt marks a definition as corrupt, which causes all other parts of -// the compiler to ignore it. See [DeclDef.IsCorrupt] +// the compiler to ignore it. See [DeclDef.IsCorrupt]. func (d DeclDef) MarkCorrupt() { d.raw.corrupt = true } diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index 1bf43555..1750dba1 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -15,9 +15,8 @@ package parser import ( - "github.com/bufbuild/protocompile/experimental/internal/taxa" - "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" ) diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index 0aa32910..2de019d3 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -29,7 +29,7 @@ import ( var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") -// legalizeFile is the entry-point for legalizing a parsed Protobuf file +// legalizeFile is the entry-point for legalizing a parsed Protobuf file. func legalizeFile(p *parser, file ast.File) { var ( pkg ast.DeclPackage @@ -72,7 +72,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax } if parent.what == taxa.TopLevel && first != nil { - file := parent.Spanner.(ast.File) + file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. switch { case !first.IsZero(): p.Errorf("unexpected %s", in).Apply( @@ -180,7 +180,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax // against duplicates. func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPackage, decl ast.DeclPackage) { if parent.what == taxa.TopLevel && first != nil { - file := parent.Spanner.(ast.File) + file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. switch { case !first.IsZero(): p.Errorf("unexpected %s", taxa.Package).Apply( diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index e7e8f989..05b08ee6 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -46,12 +46,11 @@ func legalizeOptionEntry(p *parser, opt ast.Option, span report.Span) { report.Snippet(span), ) return - } else { - legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{ - Relative: true, - AllowExts: true, - }) } + legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{ + Relative: true, + AllowExts: true, + }) if opt.Value.IsZero() { p.Errorf("missing %v", taxa.OptionValue).Apply( diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go index cc4d9c91..7aea6fc6 100644 --- a/experimental/parser/legalize_path.go +++ b/experimental/parser/legalize_path.go @@ -33,7 +33,7 @@ type pathOptions struct { AllowExts bool } -// legalizePath legalizes a path to satisfy the configuration in opts +// 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 From 8086d1e4425afc6763c1d98deeaffafbc6a0dade Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 30 Jan 2025 13:32:30 -0800 Subject: [PATCH 27/64] add comments --- experimental/parser/legalize_decl.go | 16 +++++++------- experimental/parser/legalize_def.go | 6 ++++++ experimental/parser/legalize_file.go | 4 ++-- experimental/parser/legalize_option.go | 29 ++++++++++++++++++-------- experimental/parser/legalize_type.go | 2 ++ 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 9a012b20..e9dd7a85 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -21,6 +21,10 @@ import ( "github.com/bufbuild/protocompile/experimental/seq" ) +// 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: @@ -66,6 +70,7 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { } } +// legalizeDecl legalizes an extension or reserved range. func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { in := taxa.Extensions if decl.IsReserved() { @@ -73,12 +78,10 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } var validParent bool - var isEnum bool switch parent.what { case taxa.Message: validParent = true case taxa.Enum: - isEnum = true validParent = in == taxa.Reserved } if !validParent { @@ -94,11 +97,6 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } } - field := taxa.Field - if isEnum { - field = taxa.EnumValue - } - // We only legalize reserved name productions here, because that depends on // the syntax/edition keyword. All other expressions are legalized when we // do constant evaluation. @@ -132,6 +130,10 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } 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), ) diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 92ef74f6..0e2aa398 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -39,6 +39,10 @@ var validDefParents = [...]taxa.Set{ ), } +// 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) { if def.IsCorrupt() { return @@ -166,6 +170,7 @@ func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef) { } } +// 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}) @@ -185,6 +190,7 @@ func legalizeOption(p *parser, def ast.DeclDef) { 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() diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index 2de019d3..86f9c158 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -27,8 +27,6 @@ import ( "github.com/bufbuild/protocompile/internal/ext/iterx" ) -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 ( @@ -222,6 +220,8 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{Relative: true}) } +var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") + // legalizeImport legalizes a DeclImport. // // imports is a map that classifies DeclImports by the contents of their import string. diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index 05b08ee6..9392c099 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -21,32 +21,43 @@ import ( "github.com/bufbuild/protocompile/experimental/seq" ) -func legalizeCompactOptions(p *parser, opt ast.CompactOptions) { - opts := opt.Entries() - if opts.Len() == 0 { +// 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(opt, "help: remove this"), + report.Snippetf(opts, "help: remove this"), ) return } - seq.Values(opts)(func(opt ast.Option) bool { + 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, span report.Span) { - // 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. - if opt.Path.IsZero() { p.Errorf("missing %v path", taxa.Option).Apply( report.Snippet(span), ) + + // 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{ Relative: true, AllowExts: true, diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index 720b42f9..9aa5a588 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -22,6 +22,7 @@ import ( "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( @@ -56,6 +57,7 @@ func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { } } +// legalizeFieldType legalizes the type of a message field. func legalizeFieldType(p *parser, ty ast.TypeAny) { switch ty.Kind() { case ast.TypeKindPath: From 119c667c5a37468b62d2700073a732f7999e0e1e Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 10:41:32 -0800 Subject: [PATCH 28/64] cr for predeclared --- experimental/ast/predeclared/doc.go | 25 --- experimental/ast/predeclared/methods.go | 35 ---- experimental/ast/predeclared/name.go | 187 ++++++++++++++++++ .../{predeclared.yaml => name.yaml} | 0 experimental/ast/predeclared/predeclared.go | 184 ++--------------- .../ast/predeclared/predeclared_test.go | 65 ++++++ 6 files changed, 269 insertions(+), 227 deletions(-) delete mode 100644 experimental/ast/predeclared/doc.go delete mode 100644 experimental/ast/predeclared/methods.go create mode 100644 experimental/ast/predeclared/name.go rename experimental/ast/predeclared/{predeclared.yaml => name.yaml} (100%) create mode 100644 experimental/ast/predeclared/predeclared_test.go diff --git a/experimental/ast/predeclared/doc.go b/experimental/ast/predeclared/doc.go deleted file mode 100644 index b734c66d..00000000 --- a/experimental/ast/predeclared/doc.go +++ /dev/null @@ -1,25 +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 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 - -//go:generate go run github.com/bufbuild/protocompile/internal/enum predeclared.yaml diff --git a/experimental/ast/predeclared/methods.go b/experimental/ast/predeclared/methods.go deleted file mode 100644 index 5569eb44..00000000 --- a/experimental/ast/predeclared/methods.go +++ /dev/null @@ -1,35 +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 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 - -// IsScalar returns whether this predeclared name represents one of the scalar -// types. -func (v Name) IsScalar() bool { - return v >= Int32 && v <= Bytes -} - -// 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 -} diff --git a/experimental/ast/predeclared/name.go b/experimental/ast/predeclared/name.go new file mode 100644 index 00000000..b017d3ee --- /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 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. +// +// 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()) + } +} From d72c0ad1bcdbdb2501704d3d7f5620506d7f9da6 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 11:04:38 -0800 Subject: [PATCH 29/64] add tests for the syntax package --- experimental/ast/syntax/doc.go | 18 ++++++++++++ experimental/ast/syntax/syntax_test.go | 36 ++++++++++++++++++++++++ internal/ext/iterx/iterx.go | 24 ++++++++++++++++ internal/ext/mapsx/collect.go | 38 ++++++++++++++++++++++++++ internal/ext/mapsx/mapsx.go | 34 +++++++++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 experimental/ast/syntax/syntax_test.go create mode 100644 internal/ext/mapsx/collect.go create mode 100644 internal/ext/mapsx/mapsx.go diff --git a/experimental/ast/syntax/doc.go b/experimental/ast/syntax/doc.go index 5210d59d..e74d8f57 100644 --- a/experimental/ast/syntax/doc.go +++ b/experimental/ast/syntax/doc.go @@ -16,4 +16,22 @@ // 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 + } + } + } +} + +var totalEditions = iterx.Count(All(), func(s Syntax) bool { return s.IsEdition() }) diff --git a/experimental/ast/syntax/syntax_test.go b/experimental/ast/syntax/syntax_test.go new file mode 100644 index 00000000..b9ca33fe --- /dev/null +++ b/experimental/ast/syntax/syntax_test.go @@ -0,0 +1,36 @@ +// 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/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" + "github.com/stretchr/testify/assert" +) + +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/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 3672076e..67f67b39 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -56,6 +56,30 @@ 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 }) diff --git a/internal/ext/mapsx/collect.go b/internal/ext/mapsx/collect.go new file mode 100644 index 00000000..54f55f21 --- /dev/null +++ b/internal/ext/mapsx/collect.go @@ -0,0 +1,38 @@ +// 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 + +import "github.com/bufbuild/protocompile/internal/iter" + +// 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 out +} + +// 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)) +} From b1be12f50c430c7a4ac45fc33823cf25a384548c Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 11:07:24 -0800 Subject: [PATCH 30/64] fix group types not being printed --- experimental/internal/astx/encode.go | 4 +++- experimental/parser/testdata/parser/field/group.proto.yaml | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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/parser/testdata/parser/field/group.proto.yaml b/experimental/parser/testdata/parser/field/group.proto.yaml index 8ce4a5ac..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: @@ -22,6 +23,9 @@ decls: - def: kind: KIND_GROUP name.components: [{ ident: "bar" }] + type.prefixed: + prefix: PREFIX_OPTIONAL + type.path.components: [{ ident: "group" }] value.literal.int_value: 2 body.decls: - def: @@ -32,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" }] From d74a61ba5a6f9ce9e5247ad1ee3fea68c01d52bd Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 11:25:01 -0800 Subject: [PATCH 31/64] string helpers --- internal/ext/slicesx/slicesx.go | 24 ++++++++++++++++-------- internal/ext/stringsx/stringsx.go | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/internal/ext/slicesx/slicesx.go b/internal/ext/slicesx/slicesx.go index 971475bb..dada2743 100644 --- a/internal/ext/slicesx/slicesx.go +++ b/internal/ext/slicesx/slicesx.go @@ -29,10 +29,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 +41,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 +62,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 || ...) diff --git a/internal/ext/stringsx/stringsx.go b/internal/ext/stringsx/stringsx.go index f8200556..b616f6e8 100644 --- a/internal/ext/stringsx/stringsx.go +++ b/internal/ext/stringsx/stringsx.go @@ -16,11 +16,37 @@ package stringsx import ( + "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) From f3de41d8e640364eee864109b5ae081ad16b6979 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 12:08:27 -0800 Subject: [PATCH 32/64] add span helpers --- experimental/report/span.go | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/experimental/report/span.go b/experimental/report/span.go index cc2aedad..e1dcfb51 100644 --- a/experimental/report/span.go +++ b/experimental/report/span.go @@ -66,6 +66,54 @@ 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 From dd7fdfbbeb6962e04967a25f2b2c2785a2ff32c3 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 12:08:39 -0800 Subject: [PATCH 33/64] cr --- experimental/parser/legalize_decl.go | 87 ++++++++++++++++++- .../testdata/parser/lists.proto.stderr.txt | 16 +++- .../parser/testdata/parser/package/42.proto | 4 +- .../parser/package/42.proto.stderr.txt | 12 +-- .../parser/range/reserved_mixed.proto | 25 ++++++ .../range/reserved_mixed.proto.stderr.txt | 71 +++++++++++++++ .../parser/range/reserved_mixed.proto.yaml | 34 ++++++++ 7 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 experimental/parser/testdata/parser/range/reserved_mixed.proto create mode 100644 experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/reserved_mixed.proto.yaml diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index e9dd7a85..2b62a161 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -15,10 +15,16 @@ package parser import ( + "fmt" + "unicode" + "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" + "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. @@ -105,11 +111,14 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { return } + var names, tags []ast.ExprAny seq.Values(decl.Ranges())(func(expr ast.ExprAny) bool { + var isName bool switch expr.Kind() { case ast.ExprKindPath: + isName = true path := expr.AsPath() - if in == taxa.Reserved && !path.AsIdent().IsZero() { + if !path.AsIdent().IsZero() { if m := p.Mode(); m == taxa.SyntaxMode { p.Errorf("cannot use %vs in %v in %v", taxa.Ident, in, m).Apply( report.Snippet(expr), @@ -121,6 +130,8 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { case ast.ExprKindLiteral: lit := expr.AsLiteral() if name, ok := lit.AsString(); ok { + isName = true + if m := p.Mode(); m == taxa.EditionMode { p.Errorf("cannot use %vs in %v in %v", taxa.String, in, m).Apply( report.Snippet(expr), @@ -146,6 +157,80 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } } + if isName { + names = append(names, expr) + } else { + tags = append(tags, expr) + } + 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", parentWhat, 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). + delete := expr.Span().GrowLeft(unicode.IsSpace).GrowRight(unicode.IsSpace) + if r, _ := stringsx.Rune(delete.After(), 0); r == ',' { + delete.End++ + } + + edits = append(edits, report.Edit{ + Start: delete.Start - span.Start, + End: delete.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/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 7c8b1f0e..871b8b90 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -431,6 +431,20 @@ error: cannot use identifiers in reserved range in syntax mode 72 | reserved a {}; | ^ +error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) + --> testdata/parser/lists.proto:72:16 + | +72 | reserved a {}; + | - ^^ this field tag must go in its own reserved range + | | + | but expected a field name because of this + help: split the reserved range + | +72 | - reserved a {}; +72 | + reserved a; +73 | + reserved {}; + | + error: unexpected message expression in reserved range --> testdata/parser/lists.proto:72:16 | @@ -503,4 +517,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 74 errors and 1 warning +encountered 75 errors and 1 warning diff --git a/experimental/parser/testdata/parser/package/42.proto b/experimental/parser/testdata/parser/package/42.proto index d7aedd73..38205e3c 100644 --- a/experimental/parser/testdata/parser/package/42.proto +++ b/experimental/parser/testdata/parser/package/42.proto @@ -14,6 +14,8 @@ syntax = "proto2"; -// FIXME: This produces a less-than-ideal diagnostic, but it's not an +// 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 49eed374..d45b44ba 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -1,21 +1,21 @@ error: missing path in `package` declaration - --> testdata/parser/package/42.proto:19:1 + --> testdata/parser/package/42.proto:21:1 | -19 | package 42; +21 | package 42; | ^^^^^^^ = help: to place a file in the empty package, remove the `package` declaration = help: however, using the empty package is discouraged error: unexpected integer literal after `package` declaration - --> testdata/parser/package/42.proto:19:9 + --> testdata/parser/package/42.proto:21:9 | -19 | package 42; +21 | package 42; | ^^ expected `;` error: unexpected integer literal in file scope - --> testdata/parser/package/42.proto:19:9 + --> testdata/parser/package/42.proto:21:9 | -19 | package 42; +21 | package 42; | ^^ expected identifier, `;`, `.`, `(...)`, or `{...}` encountered 3 errors 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..26e0d253 --- /dev/null +++ b/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt @@ -0,0 +1,71 @@ +error: cannot mix tags and names in field%!(EXTRA taxa.Noun=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 field%!(EXTRA taxa.Noun=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 field%!(EXTRA taxa.Noun=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 field%!(EXTRA taxa.Noun=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 field%!(EXTRA taxa.Noun=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 From f7e68d327e787c254ab00a016bf77ddff7cbbff3 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 12:41:46 -0800 Subject: [PATCH 34/64] cr --- experimental/parser/legalize_def.go | 6 +- experimental/parser/legalize_file.go | 119 ++++++++++-------- experimental/parser/legalize_option.go | 1 - experimental/parser/legalize_path.go | 60 ++++++--- experimental/parser/legalize_type.go | 1 + .../parser/def/ordering.proto.stderr.txt | 8 +- .../parser/field/bad-path.proto.stderr.txt | 14 ++- .../testdata/parser/lists.proto.stderr.txt | 4 +- .../parser/option/bad_path.proto.stderr.txt | 14 ++- .../parser/package/42.proto.stderr.txt | 4 +- .../parser/package/absolute.proto.stderr.txt | 8 +- .../parser/package/empty.proto.stderr.txt | 4 +- .../package/eof_after_kw.proto.stderr.txt | 4 +- .../parser/package/no_path.proto.stderr.txt | 4 +- .../testdata/parser/package/too_big.proto | 18 +++ .../parser/package/too_big.proto.stderr.txt | 10 ++ .../parser/package/too_big.proto.yaml | 6 + .../testdata/parser/package/too_long.proto | 27 ++++ .../parser/package/too_long.proto.stderr.txt | 11 ++ .../parser/package/too_long.proto.yaml | 105 ++++++++++++++++ .../syntax/eof_after_eq.proto.stderr.txt | 4 +- .../syntax/eof_after_kw.proto.stderr.txt | 4 +- .../parser/syntax/lonely.proto.stderr.txt | 18 +-- 23 files changed, 355 insertions(+), 99 deletions(-) create mode 100644 experimental/parser/testdata/parser/package/too_big.proto create mode 100644 experimental/parser/testdata/parser/package/too_big.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/too_big.proto.yaml create mode 100644 experimental/parser/testdata/parser/package/too_long.proto create mode 100644 experimental/parser/testdata/parser/package/too_long.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/package/too_long.proto.yaml diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 0e2aa398..9e9f720b 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -65,7 +65,7 @@ func legalizeDef(p *parser, parent classified, def ast.DeclDef) { } } -// legalizeMessageLike legalizes something that resembles a type definition: +// 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 { @@ -77,7 +77,7 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { ) case what == taxa.Extend: - legalizePath(p, what.In(), def.Name(), pathOptions{}) + legalizePath(p, what.In(), def.Name(), pathOptions{AllowAbsolute: true}) case what != taxa.Extend && def.Name().AsIdent().IsZero(): def.MarkCorrupt() @@ -119,7 +119,7 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { } } -// legalizeMessageLike legalizes something that resembles a field definition: +// 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() { diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index 86f9c158..a9f12758 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -52,8 +52,8 @@ func legalizeFile(p *parser, file ast.File) { if pkg.IsZero() { p.Warnf("missing %s", taxa.Package).Apply( report.InFile(p.Stream().Path()), - report.Notef("failing to specify a package places the file in the empty package"), - report.Notef("using the empty package is discouraged"), + report.Notef("not explicitly specifying a package places the file"), + report.Notef("in the unnamed package; using it strongly is discouraged"), ) } } @@ -69,32 +69,38 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax in = taxa.Edition } - if parent.what == taxa.TopLevel && first != nil { - file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. - switch { - case !first.IsZero(): - p.Errorf("unexpected %s", in).Apply( - report.Snippetf(decl, "help: remove this"), - report.Snippetf(*first, "previous declaration is here"), - report.Notef("a file may only 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"), - report.Notef("a %s must be the first declaration in a file", in), - ) - *first = decl - return - default: - *first = decl - } - } else { + if parent.what != taxa.TopLevel || first == nil { p.Error(errBadNest{parent: parent, child: decl}) 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"), + // TOOD: 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}) } @@ -129,7 +135,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax return "", false } - return fmt.Sprintf(`"%v"`, s), true + return fmt.Sprintf("%q", s), true }), ", ") return report.Notef("permitted values: [%s]", values) @@ -177,32 +183,38 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax // 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 { - file := parent.Spanner.(ast.File) //nolint:errcheck // Implied by == taxa.TopLevel. - switch { - case !first.IsZero(): - p.Errorf("unexpected %s", taxa.Package).Apply( - report.Snippetf(decl, "help: remove this"), - report.Snippetf(*first, "previous declaration is here"), - report.Notef("a file must contain exactly one %s", taxa.Package), + if parent.what != taxa.TopLevel || first == nil { + p.Error(errBadNest{parent: parent, child: decl}) + 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"), + // TOOD: Add a suggestion to move this up. + report.Helpf("a file's %s should immediately follow the `syntax` or `edition` declaration", taxa.Package), ) return - case idx > 0: - if idx > 1 || file.Decls().At(0).Kind() != ast.DeclKindSyntax { - p.Errorf("unexpected %s", taxa.Package).Apply( - report.Snippet(decl), - report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"), - report.Notef("a %s must be the first declaration in a file, or follow the `syntax` or `edition` declaration", taxa.Package), - ) - return - } - *first = decl - default: - *first = decl } - } else { - p.Error(errBadNest{parent: parent, child: decl}) - return + fallthrough + default: + *first = decl } if !decl.Options().IsZero() { @@ -212,12 +224,15 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka if decl.Path().IsZero() { p.Errorf("missing path in %s", taxa.Package).Apply( report.Snippet(decl), - report.Helpf("to place a file in the empty package, remove the %s", taxa.Package), - report.Helpf("however, using the empty package is discouraged"), + report.Helpf("to place a file in the unnamed package, remove the %s", taxa.Package), + report.Helpf("however, using the unnamed package is discouraged"), ) } - legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{Relative: true}) + legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{ + MaxBytes: 512, + MaxComponents: 101, + }) } var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") @@ -251,7 +266,7 @@ func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports m if imports != nil { prev := imports[file] imports[file] = append(prev, decl) - if len(prev) == 1 { // Do not bother diagnosing a more than once. + 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"), diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index 9392c099..856a7eb9 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -59,7 +59,6 @@ func legalizeOptionEntry(p *parser, opt ast.Option, span report.Span) { } legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{ - Relative: true, AllowExts: true, }) diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go index 7aea6fc6..dafbb888 100644 --- a/experimental/parser/legalize_path.go +++ b/experimental/parser/legalize_path.go @@ -24,30 +24,40 @@ import ( // pathOptions is configuration for [legalizePath]. type pathOptions struct { // If set, the path must be relative. - Relative bool + 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 i int + var i, bytes, components int var slash token.Token path.Components(func(pc ast.PathComponent) bool { - if i == 0 && opts.Relative { - if !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 false - } + 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() == "/" { @@ -56,14 +66,14 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) report.Snippetf(pc.Separator(), "help: replace this with a `.`"), ) ok = false - return false + return true } else if !slash.IsZero() { - p.Errorf("unexpected `/` in path %s", where).Apply( + p.Errorf("type URL can only contain a single `/`", where).Apply( report.Snippet(pc.Separator()), - report.Snippetf(slash, "previous one is here"), + report.Snippetf(slash, "first one is here"), ) ok = false - return false + return true } slash = pc.Separator() } @@ -71,11 +81,11 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) if ext := pc.AsExtension(); !ext.IsZero() { if opts.AllowExts { ok = legalizePath(p, where, ext, pathOptions{ - Relative: false, - AllowExts: false, + AllowAbsolute: true, + AllowExts: false, }) if !ok { - return false + return true } } else { p.Errorf("unexpected nested extension path %s", where).Apply( @@ -83,7 +93,7 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) report.Snippet(pc.Name()), ) ok = false - return false + return true } } @@ -91,5 +101,19 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) 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, where), + ) + } 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, where), + ) + } + } + return ok } diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index 9aa5a588..34c43c06 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -61,6 +61,7 @@ func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { func legalizeFieldType(p *parser, ty ast.TypeAny) { switch ty.Kind() { case ast.TypeKindPath: + legalizePath(p, taxa.Field.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) break case ast.TypeKindPrefixed: diff --git a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index 60a1924d..fa6a4837 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt @@ -66,6 +66,12 @@ error: unexpected definition body in message field 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 name in message field --> testdata/parser/def/ordering.proto:24:23 | @@ -232,4 +238,4 @@ error: unexpected `[...]` in message definition 40 | M x { /* ... */ } [foo = bar]; | ^^^^^^^^^^^ expected identifier, `;`, `.`, `(...)`, or `{...}` -encountered 35 errors +encountered 36 errors diff --git a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt index 3bab4157..dca3f675 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt @@ -40,6 +40,12 @@ error: unexpected qualified name in message field 27 | required package.Type path.name = 1; | ^^^^^^^^^ expected identifier +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:33:5 + | +33 | (foo.bar).Type name = 1; + | ^^^^^^^^^ + error: unexpected qualified name in message field --> testdata/parser/field/bad-path.proto:35:27 | @@ -64,10 +70,16 @@ error: unexpected qualified name in message field 38 | package.Type (foo.bar).name = 1; | ^^^^^^^^^^^^^^ expected identifier +error: unexpected nested extension path in message field + --> testdata/parser/field/bad-path.proto:40:5 + | +40 | (foo) (bar) = 1; + | ^^^^^ + error: unexpected qualified name in message field --> testdata/parser/field/bad-path.proto:40:11 | 40 | (foo) (bar) = 1; | ^^^^^ expected identifier -encountered 12 errors +encountered 14 errors diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 871b8b90..0f7d1b03 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -1,7 +1,7 @@ warning: missing `package` declaration --> testdata/parser/lists.proto - = note: failing to specify a package places the file in the empty package - = note: using the empty package is discouraged + = note: not explicitly specifying a package places the file + = note: in the unnamed package; using it strongly is discouraged error: unexpected integer literal in array expression --> testdata/parser/lists.proto:20:20 diff --git a/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt index d1541a5e..0f7e5550 100644 --- a/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt @@ -22,6 +22,12 @@ error: unexpected absolute path in option setting 22 | option .(foo.bar).baz = 3; | ^^^^^^^^^^^^^^ expected a path without a leading `.` +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 | @@ -34,6 +40,12 @@ error: unexpected absolute path in option setting 27 | .(foo.bar).baz = 3, | ^^^^^^^^^^^^^^ expected a path without a leading `.` +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 | @@ -46,4 +58,4 @@ error: compact options cannot be empty 30 | int32 y = 2 []; | ^^ help: remove this -encountered 8 errors +encountered 10 errors diff --git a/experimental/parser/testdata/parser/package/42.proto.stderr.txt b/experimental/parser/testdata/parser/package/42.proto.stderr.txt index d45b44ba..bfcc536a 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -3,8 +3,8 @@ error: missing path in `package` declaration | 21 | package 42; | ^^^^^^^ - = help: to place a file in the empty package, remove the `package` declaration - = help: however, using the empty package is discouraged + = help: to place a file in the unnamed package, remove the `package` declaration + = help: however, using the unnamed package is discouraged error: unexpected integer literal after `package` declaration --> testdata/parser/package/42.proto:21:9 diff --git a/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt index 7d81eb29..2a6dc764 100644 --- a/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt @@ -4,4 +4,10 @@ error: unexpected absolute path in `package` declaration 17 | package .test.test2; | ^^^^^^^^^^^ expected a path without a leading `.` -encountered 1 error +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 2 errors diff --git a/experimental/parser/testdata/parser/package/empty.proto.stderr.txt b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt index 2a7657f5..a330951a 100644 --- a/experimental/parser/testdata/parser/package/empty.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt @@ -1,6 +1,6 @@ warning: missing `package` declaration --> testdata/parser/package/empty.proto - = note: failing to specify a package places the file in the empty package - = note: using the empty package is discouraged + = note: not explicitly specifying a package places the file + = note: 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 10400fe2..2af0c304 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 @@ -3,8 +3,8 @@ error: missing path in `package` declaration | 17 | package | ^^^^^^^ - = help: to place a file in the empty package, remove the `package` declaration - = help: however, using the empty package is discouraged + = help: to place a file in the unnamed package, remove the `package` declaration + = help: however, using the unnamed package is discouraged error: unexpected end-of-file after `package` declaration --> testdata/parser/package/eof_after_kw.proto:17:8 diff --git a/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt index 0130f5d5..758e0db8 100644 --- a/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt @@ -3,7 +3,7 @@ error: missing path in `package` declaration | 17 | package; | ^^^^^^^^ - = help: to place a file in the empty package, remove the `package` declaration - = help: however, using the empty package is discouraged + = help: to place a file in the unnamed package, remove the `package` declaration + = help: however, using the unnamed package is discouraged 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..a234762a --- /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%!(EXTRA taxa.Place=in `package` declaration) + +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..56f1950c --- /dev/null +++ b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt @@ -0,0 +1,11 @@ +error: path in `package` declaration is too large + --> testdata/parser/package/too_long.proto:17:9 + | +17 | package thispathhas102components + | ________^ +... / +27 | | .a.a.a.a.a.a.a.a.a.a.x; + | \__________________________^ + = note: Protobuf imposes a limit of 101 components here%!(EXTRA taxa.Place=in `package` declaration) + +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/syntax/eof_after_eq.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/eof_after_eq.proto.stderr.txt index 9eb76cef..105233f9 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,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/eof_after_eq.proto - = note: failing to specify a package places the file in the empty package - = note: using the empty package is discouraged + = note: not explicitly specifying a package places the file + = note: 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 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 1a00a2d5..13d4083a 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,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/eof_after_kw.proto - = note: failing to specify a package places the file in the empty package - = note: using the empty package is discouraged + = note: not explicitly specifying a package places the file + = note: 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 diff --git a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt index 23db3617..f2a7fcca 100644 --- a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt @@ -1,7 +1,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/lonely.proto - = note: failing to specify a package places the file in the empty package - = note: using the empty package is discouraged + = note: not explicitly specifying a package places the file + = note: in the unnamed package; using it strongly is discouraged error: unexpected `;` in `edition` declaration --> testdata/parser/syntax/lonely.proto:15:8 @@ -16,8 +16,12 @@ error: unexpected `syntax` declaration | -------- previous declaration is here 16 | 17 | syntax = ; - | ^^^^^^^^^^ help: remove this - = note: a file may only contain at most one `syntax` or `edition` declaration + | ^^^^^^^^^^ + 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 @@ -25,7 +29,7 @@ error: unexpected `;` in `syntax` declaration 17 | syntax = ; | ^ expected expression -error: unexpected `package` declaration +warning: the `package` declaration should be placed at the top of the file --> testdata/parser/syntax/lonely.proto:19:1 | 17 | syntax = ; @@ -33,6 +37,6 @@ error: unexpected `package` declaration 18 | 19 | package test; | ^^^^^^^^^^^^^ - = note: a `package` declaration must be the first declaration in a file, or follow the `syntax` or `edition` declaration + = help: a file's `package` declaration should immediately follow the `syntax` or `edition` declaration -encountered 4 errors and 1 warning +encountered 3 errors and 2 warnings From d94e8834d530a1cb471139f76ebc0f54354053f2 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 13:22:50 -0800 Subject: [PATCH 35/64] fix a renderer quirk --- .../parser/def/nesting.proto.stderr.txt | 48 ++++++++++++++----- .../parser/import/in_message.proto.stderr.txt | 7 +-- .../parser/package/too_long.proto.stderr.txt | 1 + .../range/reserved_edition.proto.stderr.txt | 1 - .../range/reserved_syntax.proto.stderr.txt | 1 - experimental/report/renderer.go | 37 ++++++++++++-- .../testdata/multi-underline.yaml.color.txt | 1 - .../testdata/multi-underline.yaml.fancy.txt | 1 - 8 files changed, 73 insertions(+), 24 deletions(-) diff --git a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt index 8fbf5358..fa99ee76 100644 --- a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt @@ -2,10 +2,12 @@ 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 @@ -15,6 +17,7 @@ error: unexpected message definition within enum definition 27 | / enum E { 28 | | message M {} | | ^^^^^^^^^^^^ this message definition... +29 | | enum E {} ... | 33 | | } | \_- ...cannot be declared within this enum definition @@ -26,6 +29,7 @@ error: unexpected enum definition within enum definition 28 | | message M {} 29 | | enum E {} | | ^^^^^^^^^ this enum definition... +30 | | service S {} ... | 33 | | } | \_- ...cannot be declared within this enum definition @@ -34,10 +38,12 @@ 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 @@ -46,6 +52,7 @@ error: unexpected message extension block within enum definition | 27 | / enum E { ... | +30 | | service S {} 31 | | extend E {} | | ^^^^^^^^^^^ this message extension block... 32 | | oneof O {} @@ -57,6 +64,7 @@ error: unexpected oneof definition within enum definition | 27 | / enum E { ... | +31 | | extend E {} 32 | | oneof O {} | | ^^^^^^^^^^ this oneof definition... 33 | | } @@ -68,6 +76,7 @@ error: unexpected message definition within service definition 35 | / service S { 36 | | message M {} | | ^^^^^^^^^^^^ this message definition... +37 | | enum E {} ... | 41 | | } | \_- ...cannot be declared within this service definition @@ -79,6 +88,7 @@ error: unexpected enum definition within service definition 36 | | message M {} 37 | | enum E {} | | ^^^^^^^^^ this enum definition... +38 | | service S {} ... | 41 | | } | \_- ...cannot be declared within this service definition @@ -87,10 +97,12 @@ 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 @@ -99,6 +111,7 @@ error: unexpected message extension block within service definition | 35 | / service S { ... | +38 | | service S {} 39 | | extend E {} | | ^^^^^^^^^^^ this message extension block... 40 | | oneof O {} @@ -110,6 +123,7 @@ error: unexpected oneof definition within service definition | 35 | / service S { ... | +39 | | extend E {} 40 | | oneof O {} | | ^^^^^^^^^^ this oneof definition... 41 | | } @@ -121,6 +135,7 @@ error: unexpected message definition within message extension block 43 | / extend E { 44 | | message M {} | | ^^^^^^^^^^^^ this message definition... +45 | | enum E {} ... | 49 | | } | \_- ...cannot be declared within this message extension block @@ -132,6 +147,7 @@ error: unexpected enum definition within message extension block 44 | | message M {} 45 | | enum E {} | | ^^^^^^^^^ this enum definition... +46 | | service S {} ... | 49 | | } | \_- ...cannot be declared within this message extension block @@ -140,10 +156,12 @@ 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 @@ -152,6 +170,7 @@ error: unexpected message extension block within message extension block | 43 | / extend E { ... | +46 | | service S {} 47 | | extend E {} | | ^^^^^^^^^^^ this message extension block... 48 | | oneof O {} @@ -163,6 +182,7 @@ error: unexpected oneof definition within message extension block | 43 | / extend E { ... | +47 | | extend E {} 48 | | oneof O {} | | ^^^^^^^^^^ this oneof definition... 49 | | } @@ -172,9 +192,7 @@ error: unexpected oneof definition within file scope --> testdata/parser/def/nesting.proto:51:1 | 15 | / syntax = "proto2"; -16 | | ... | -50 | | 51 | | / oneof O { ... | | 57 | | | } @@ -187,6 +205,7 @@ error: unexpected message definition within oneof definition 51 | / oneof O { 52 | | message M {} | | ^^^^^^^^^^^^ this message definition... +53 | | enum E {} ... | 57 | | } | \_- ...cannot be declared within this oneof definition @@ -198,6 +217,7 @@ error: unexpected enum definition within oneof definition 52 | | message M {} 53 | | enum E {} | | ^^^^^^^^^ this enum definition... +54 | | service S {} ... | 57 | | } | \_- ...cannot be declared within this oneof definition @@ -206,10 +226,12 @@ 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 @@ -218,6 +240,7 @@ error: unexpected message extension block within oneof definition | 51 | / oneof O { ... | +54 | | service S {} 55 | | extend E {} | | ^^^^^^^^^^^ this message extension block... 56 | | oneof O {} @@ -229,6 +252,7 @@ error: unexpected oneof definition within oneof definition | 51 | / oneof O { ... | +55 | | extend E {} 56 | | oneof O {} | | ^^^^^^^^^^ this oneof definition... 57 | | } diff --git a/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt index 601fc97f..779fc7bf 100644 --- a/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt @@ -4,6 +4,7 @@ error: unexpected import within message definition 19 | / message M { 20 | | import "foo.proto"; | | ^^^^^^^^^^^^^^^^^^^ this import... +21 | | import public "foo.proto"; ... | 25 | | } | \_- ...cannot be declared within this message definition @@ -15,6 +16,7 @@ error: unexpected public import within message definition 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 @@ -23,10 +25,10 @@ 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... -23 | | ... | 25 | | } | \_- ...cannot be declared within this message definition @@ -36,7 +38,6 @@ error: unexpected import within message definition | 19 | / message M { ... | -23 | | 24 | | import foo.proto; | | ^^^^^^^^^^^^^^^^^ this import... 25 | | } diff --git a/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt index 56f1950c..bbf8ea26 100644 --- a/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt @@ -4,6 +4,7 @@ error: path in `package` declaration is too large 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%!(EXTRA taxa.Place=in `package` declaration) diff --git a/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt index 6d3b5986..a9efa2d7 100644 --- a/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt @@ -3,7 +3,6 @@ error: cannot use string literals in reserved range in editions mode | 15 | edition = "2023"; | ----------------- editions mode is specified here -16 | ... 20 | reserved foo, "foo"; | ^^^^^ diff --git a/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt index bacba80f..735ad71d 100644 --- a/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt @@ -3,7 +3,6 @@ error: cannot use identifiers in reserved range in syntax mode | 15 | syntax = "proto2"; | ------------------ syntax mode is specified here -16 | ... 20 | reserved foo, "foo"; | ^^^ diff --git a/experimental/report/renderer.go b/experimental/report/renderer.go index fd5f7d48..fe6b359b 100644 --- a/experimental/report/renderer.go +++ b/experimental/report/renderer.go @@ -759,23 +759,51 @@ func (w *window) Render(lineBarWidth int, ss *styleSheet, out *strings.Builder) } } 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 { @@ -815,8 +843,7 @@ func (w *window) Render(lineBarWidth int, ss *styleSheet, out *strings.Builder) prevSidebar[len(prevSidebar)-1].startWidth > 0 { slashAt = len(prevSidebar) - 1 } - - out.WriteString(renderSidebar(sidebarLen, lineno, slashAt, ss, cur.sidebar)) + out.WriteString(renderSidebar(sidebarLen, lastEmit+1, slashAt, ss, info[lastEmit-w.start].sidebar)) } // Ok, we are definitely printing this line out. diff --git a/experimental/report/testdata/multi-underline.yaml.color.txt b/experimental/report/testdata/multi-underline.yaml.color.txt index 4f35abb7..d4a62859 100755 --- a/experimental/report/testdata/multi-underline.yaml.color.txt +++ b/experimental/report/testdata/multi-underline.yaml.color.txt @@ -3,7 +3,6 @@ | ⟨blu⟩ 1 | ⟨reset⟩syntax = "proto4" ⟨blu⟩ | ⟨reset⟩⟨reset⟩ ⟨b.blu⟩--------⟨reset⟩ ⟨b.blu⟩syntax version specified here -⟨blu⟩ 2 | ⟨reset⟩ ⟨blu⟩... ⟨reset⟩ ⟨blu⟩ 6 | ⟨reset⟩ required size_t x = 0; ⟨blu⟩ | ⟨reset⟩⟨reset⟩ ⟨b.red⟩^^^^^⟨reset⟩ ⟨b.red⟩⟨reset⟩ diff --git a/experimental/report/testdata/multi-underline.yaml.fancy.txt b/experimental/report/testdata/multi-underline.yaml.fancy.txt index 182366b7..50070a16 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; | ^^^^^ From cddd4401a6128e1c4529fe8321194ae864931c02 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 7 Feb 2025 13:28:23 -0800 Subject: [PATCH 36/64] lint --- experimental/ast/syntax/syntax_test.go | 3 ++- experimental/parser/legalize_decl.go | 12 ++++++------ experimental/parser/legalize_file.go | 4 ++-- experimental/parser/legalize_path.go | 6 +++--- experimental/parser/legalize_type.go | 1 - .../parser/testdata/parser/lists.proto.stderr.txt | 2 +- .../testdata/parser/package/too_big.proto.stderr.txt | 2 +- .../parser/package/too_long.proto.stderr.txt | 2 +- .../parser/range/reserved_mixed.proto.stderr.txt | 10 +++++----- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/experimental/ast/syntax/syntax_test.go b/experimental/ast/syntax/syntax_test.go index b9ca33fe..9f0dd60d 100644 --- a/experimental/ast/syntax/syntax_test.go +++ b/experimental/ast/syntax/syntax_test.go @@ -17,12 +17,13 @@ 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" - "github.com/stretchr/testify/assert" ) func TestEditions(t *testing.T) { diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 2b62a161..13b1806e 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -184,7 +184,7 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { leastWhat, mostWhat = mostWhat, leastWhat } - err := p.Errorf("cannot mix tags and names in %s", parentWhat, taxa.Reserved).Apply( + 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), ) @@ -193,14 +193,14 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { var edits []report.Edit for _, expr := range least { // Delete leading whitespace and trailing whitespace (and a comma, too). - delete := expr.Span().GrowLeft(unicode.IsSpace).GrowRight(unicode.IsSpace) - if r, _ := stringsx.Rune(delete.After(), 0); r == ',' { - delete.End++ + 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: delete.Start - span.Start, - End: delete.End - span.Start, + Start: toDelete.Start - span.Start, + End: toDelete.End - span.Start, }) } diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index a9f12758..0d285062 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -92,7 +92,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax p.Errorf("unexpected %s", in).Apply( report.Snippet(decl), report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"), - // TOOD: Add a suggestion to move this up. + // TODO: Add a suggestion to move this up. report.Notef("a %s must be the first declaration in a file", in), ) *first = decl @@ -207,7 +207,7 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka 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"), - // TOOD: Add a suggestion to move this up. + // 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 diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go index dafbb888..6fa50386 100644 --- a/experimental/parser/legalize_path.go +++ b/experimental/parser/legalize_path.go @@ -68,7 +68,7 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) ok = false return true } else if !slash.IsZero() { - p.Errorf("type URL can only contain a single `/`", where).Apply( + p.Errorf("type URL can only contain a single `/`").Apply( report.Snippet(pc.Separator()), report.Snippetf(slash, "first one is here"), ) @@ -105,12 +105,12 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) 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, where), + 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, where), + report.Notef("Protobuf imposes a limit of %v components here", opts.MaxComponents), ) } } diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index 34c43c06..661e55c3 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -62,7 +62,6 @@ func legalizeFieldType(p *parser, ty ast.TypeAny) { switch ty.Kind() { case ast.TypeKindPath: legalizePath(p, taxa.Field.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) - break case ast.TypeKindPrefixed: ty := ty.AsPrefixed() diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 4d502e67..e5519148 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -431,7 +431,7 @@ error: cannot use identifiers in reserved range in syntax mode 72 | reserved a {}; | ^ -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +error: cannot mix tags and names in reserved range --> testdata/parser/lists.proto:72:16 | 72 | reserved a {}; diff --git a/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt b/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt index 5e57d957..418a1a4f 100644 --- a/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/too_big.proto.stderr.txt @@ -5,6 +5,6 @@ error: path in `package` declaration is too large | ________^ 18 | / x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; | \___________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________^ - = note: Protobuf imposes a limit of 512 bytes here%!(EXTRA taxa.Place=in `package` declaration) + = note: Protobuf imposes a limit of 512 bytes here encountered 1 error diff --git a/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt index 8d8c80a0..9c4b8d1b 100644 --- a/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/too_long.proto.stderr.txt @@ -7,6 +7,6 @@ error: path in `package` declaration is too large 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%!(EXTRA taxa.Place=in `package` declaration) + = note: Protobuf imposes a limit of 101 components here encountered 1 error diff --git a/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt index f53699eb..7e61a418 100644 --- a/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_mixed.proto.stderr.txt @@ -1,4 +1,4 @@ -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +error: cannot mix tags and names in reserved range --> testdata/parser/range/reserved_mixed.proto:20:17 | 20 | reserved 5, "foo"; @@ -12,7 +12,7 @@ error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) 21 | + reserved "foo"; | -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +error: cannot mix tags and names in reserved range --> testdata/parser/range/reserved_mixed.proto:21:21 | 21 | reserved "foo", 5; @@ -26,7 +26,7 @@ error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) 22 | + reserved 5; | -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +error: cannot mix tags and names in reserved range --> testdata/parser/range/reserved_mixed.proto:22:17 | 22 | reserved 5, "foo", 5; @@ -40,7 +40,7 @@ error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) 23 | + reserved "foo"; | -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +error: cannot mix tags and names in reserved range --> testdata/parser/range/reserved_mixed.proto:23:21 | 23 | reserved "foo", 5, "foo"; @@ -54,7 +54,7 @@ error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) 24 | + reserved 5; | -error: cannot mix tags and names in field%!(EXTRA taxa.Noun=reserved range) +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; From e321aaabcbb85dac59254475b74dc5be5fb963db Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 13:26:01 -0800 Subject: [PATCH 37/64] suggestions for reserved names --- experimental/parser/legalize_decl.go | 25 ++++++++++++++++- .../testdata/parser/lists.proto.stderr.txt | 28 +++++++++++++++++++ .../reserved_default_syntax.proto.stderr.txt | 4 +++ .../range/reserved_edition.proto.stderr.txt | 5 ++++ .../range/reserved_syntax.proto.stderr.txt | 4 +++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 13b1806e..6fd3b8b1 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -123,6 +123,17 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { 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: `"`, + }, + ), ) } } @@ -133,10 +144,22 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { isName = true if m := p.Mode(); m == taxa.EditionMode { - p.Errorf("cannot use %vs in %v in %v", taxa.String, in, m).Apply( + 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, + }, + )) + } + return true } diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index e5519148..a8f741cf 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -430,6 +430,10 @@ error: cannot use identifiers in reserved range in syntax mode | 72 | reserved a {}; | ^ + help: quote it to make it into a string literal + | +72 | reserved "a" {}; + | + + error: cannot mix tags and names in reserved range --> testdata/parser/lists.proto:72:16 @@ -464,18 +468,30 @@ error: cannot use identifiers in reserved range in syntax mode | 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 @@ -490,18 +506,30 @@ error: cannot use identifiers in reserved range in syntax mode | 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 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 index 315e054b..4d5e0b73 100644 --- a/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_default_syntax.proto.stderr.txt @@ -3,5 +3,9 @@ error: cannot use identifiers in reserved range in syntax mode | 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_edition.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt index d5dc74f4..a37d6a93 100644 --- a/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_edition.proto.stderr.txt @@ -6,5 +6,10 @@ error: cannot use string literals in reserved range in editions mode ... 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_syntax.proto.stderr.txt b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt index 3e809389..7b783f53 100644 --- a/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/reserved_syntax.proto.stderr.txt @@ -6,5 +6,9 @@ error: cannot use identifiers in reserved range in syntax mode ... 20 | reserved foo, "foo"; | ^^^ + help: quote it to make it into a string literal + | +20 | reserved "foo", "foo"; + | + + encountered 1 error From e8e6a03758f7dc20a30aa3873b657309f039defc Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 13:42:19 -0800 Subject: [PATCH 38/64] add more context to bad-nest errors --- experimental/parser/diagnostics_internal.go | 40 +++++++++++++++---- experimental/parser/legalize_decl.go | 13 ++---- experimental/parser/legalize_def.go | 2 +- experimental/parser/legalize_file.go | 6 +-- .../parser/def/nesting.proto.stderr.txt | 33 +++++++++++---- .../parser/import/in_message.proto.stderr.txt | 4 ++ .../range/invalid_parent.proto.stderr.txt | 5 +++ 7 files changed, 76 insertions(+), 27 deletions(-) diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index 1750dba1..edda1645 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -18,6 +18,7 @@ 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 @@ -117,14 +118,39 @@ func (e errHasSignature) Diagnose(d *report.Diagnostic) { // errBadNest diagnoses bad nesting: parent should not contain child. type errBadNest struct { - parent classified - child report.Spanner + parent classified + child report.Spanner + validParents taxa.Set } func (e errBadNest) Diagnose(d *report.Diagnostic) { - d.Apply( - report.Message("unexpected %s within %s", taxa.Classify(e.child), e.parent.what), - report.Snippetf(e.child, "this %s...", taxa.Classify(e.child)), - report.Snippetf(e.parent, "...cannot be declared within this %s", e.parent.what), - ) + 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/legalize_decl.go b/experimental/parser/legalize_decl.go index 6fd3b8b1..69497b58 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -79,19 +79,14 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { // 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) } - var validParent bool - switch parent.what { - case taxa.Message: - validParent = true - case taxa.Enum: - validParent = in == taxa.Reserved - } - if !validParent { - p.Error(errBadNest{parent: parent, child: decl}) + if !validParents.Has(parent.what) { + p.Error(errBadNest{parent: parent, child: decl, validParents: validParents}) return } diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 9e9f720b..76434771 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -50,7 +50,7 @@ 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}) + p.Error(errBadNest{parent: parent, child: def, validParents: validDefParents[kind]}) } switch kind { diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index 0d285062..b0090367 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -70,7 +70,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax } if parent.what != taxa.TopLevel || first == nil { - p.Error(errBadNest{parent: parent, child: decl}) + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) return } @@ -184,7 +184,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax // 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}) + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) return } @@ -250,7 +250,7 @@ func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports m } if parent.what != taxa.TopLevel { - p.Error(errBadNest{parent: parent, child: decl}) + p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()}) return } diff --git a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt index c447a69d..165e7fcb 100644 --- a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt @@ -10,6 +10,7 @@ error: unexpected service definition within message definition 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 @@ -21,6 +22,7 @@ error: unexpected message definition within enum definition ... | 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 @@ -33,6 +35,7 @@ error: unexpected enum definition within enum definition ... | 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 @@ -46,6 +49,7 @@ error: unexpected service definition within enum definition 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 @@ -58,6 +62,7 @@ error: unexpected message extension block within enum definition 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 @@ -69,6 +74,7 @@ error: unexpected oneof definition within enum definition | | ^^^^^^^^^^ 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 @@ -80,6 +86,7 @@ error: unexpected message definition within service definition ... | 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 @@ -92,6 +99,7 @@ error: unexpected enum definition within service definition ... | 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 @@ -105,6 +113,7 @@ error: unexpected service definition within service definition 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 @@ -117,6 +126,7 @@ error: unexpected message extension block within service definition 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 @@ -128,6 +138,7 @@ error: unexpected oneof definition within service definition | | ^^^^^^^^^^ 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 @@ -139,6 +150,7 @@ error: unexpected message definition within message extension block ... | 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 @@ -151,6 +163,7 @@ error: unexpected enum definition within message extension block ... | 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 @@ -164,6 +177,7 @@ error: unexpected service definition within message extension block 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 @@ -176,6 +190,7 @@ error: unexpected message extension block within 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 @@ -187,17 +202,16 @@ error: unexpected oneof definition within message extension block | | ^^^^^^^^^^ 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 within file scope +error: unexpected oneof definition at file scope --> testdata/parser/def/nesting.proto:51:1 | -15 | / syntax = "proto2"; +51 | / oneof O { ... | -51 | | / oneof O { -... | | -57 | | | } - | \___- ...cannot be declared within this file scope - | \_^ this oneof definition... +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 @@ -209,6 +223,7 @@ error: unexpected message definition within oneof definition ... | 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 @@ -221,6 +236,7 @@ error: unexpected enum definition within oneof definition ... | 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 @@ -234,6 +250,7 @@ error: unexpected service definition within oneof definition 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 @@ -246,6 +263,7 @@ error: unexpected message extension block within oneof definition 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 @@ -257,5 +275,6 @@ error: unexpected oneof definition within oneof definition | | ^^^^^^^^^^ 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/import/in_message.proto.stderr.txt b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt index d12ceb24..d669f74b 100644 --- a/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/in_message.proto.stderr.txt @@ -8,6 +8,7 @@ error: unexpected import within message definition ... | 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 @@ -20,6 +21,7 @@ error: unexpected public import within message definition ... | 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 @@ -32,6 +34,7 @@ error: unexpected weak import within message definition ... | 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 @@ -42,5 +45,6 @@ error: unexpected import within message definition | | ^^^^^^^^^^^^^^^^^ 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/range/invalid_parent.proto.stderr.txt b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt index 2a72f392..b6a905c2 100644 --- a/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt @@ -7,6 +7,7 @@ error: unexpected extension range within service definition 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 @@ -17,6 +18,7 @@ error: unexpected reserved range within service definition | | ^^^^^^^^^^^ 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 @@ -27,6 +29,7 @@ error: unexpected extension range within message extension block 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 @@ -37,6 +40,7 @@ error: unexpected reserved range within message extension block | | ^^^^^^^^^^^ 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 @@ -46,5 +50,6 @@ error: unexpected extension range within enum definition | | ^^^^^^^^^^^^^ 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 From 21f75e54a9bbdc01bfa0fa683f1d5d4c3ce39d08 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 13:52:28 -0800 Subject: [PATCH 39/64] cr --- experimental/parser/legalize_decl.go | 3 ++ experimental/parser/legalize_def.go | 28 ++++++++--- .../parser/def/bad_path.proto.stderr.txt | 48 +++++++++---------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 69497b58..c4430661 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -66,6 +66,9 @@ func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) { 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) diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 76434771..e339c6a6 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -44,10 +44,6 @@ var validDefParents = [...]taxa.Set{ // It will mark the definition as corrupt if it encounters any particularly // egregious problems. func legalizeDef(p *parser, parent classified, def ast.DeclDef) { - if def.IsCorrupt() { - return - } - kind := def.Classify() if !validDefParents[kind].Has(parent.what) { p.Error(errBadNest{parent: parent, child: def, validParents: validDefParents[kind]}) @@ -79,14 +75,32 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { case what == taxa.Extend: legalizePath(p, what.In(), def.Name(), pathOptions{AllowAbsolute: true}) - case what != taxa.Extend && def.Name().AsIdent().IsZero(): + case def.Name().AsIdent().IsZero(): def.MarkCorrupt() kw := taxa.Keyword(def.Keyword().Text()) - p.Error(errUnexpected{ + + err := errUnexpected{ what: def.Name(), where: kw.After(), want: taxa.Ident.AsSet(), - }).Apply( + } + // 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. diff --git a/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt index f91706c6..0d5e265d 100644 --- a/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt @@ -1,43 +1,43 @@ -error: unexpected qualified name after `message` - --> testdata/parser/def/bad_path.proto:19:9 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:19:12 | 19 | message foo.Bar { - | ^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a message definition must be a single identifier -error: unexpected qualified name after `oneof` - --> testdata/parser/def/bad_path.proto:20:11 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:20:14 | 20 | oneof foo.Bar {} - | ^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a oneof definition must be a single identifier -error: unexpected qualified name after `oneof` - --> testdata/parser/def/bad_path.proto:21:11 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:21:14 | 21 | oneof foo.(bar.baz).Bar {} - | ^^^^^^^^^^^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a oneof definition must be a single identifier -error: unexpected qualified name after `message` - --> testdata/parser/def/bad_path.proto:23:9 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:23:12 | 23 | message foo.(bar.baz).Bar {} - | ^^^^^^^^^^^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a message definition must be a single identifier -error: unexpected qualified name after `enum` - --> testdata/parser/def/bad_path.proto:25:6 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:25:9 | 25 | enum foo.Bar {} - | ^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a enum definition must be a single identifier -error: unexpected qualified name after `enum` - --> testdata/parser/def/bad_path.proto:26:6 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:26:9 | 26 | enum foo.(bar.baz).Bar {} - | ^^^^^^^^^^^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a enum definition must be a single identifier error: unexpected nested extension path in message extension block @@ -46,18 +46,18 @@ error: unexpected nested extension path in message extension block 29 | extend foo.(bar.baz).Bar {} | ^^^^^^^^^ -error: unexpected qualified name after `service` - --> testdata/parser/def/bad_path.proto:31:9 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:31:12 | 31 | service foo.Bar {} - | ^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a service definition must be a single identifier -error: unexpected qualified name after `service` - --> testdata/parser/def/bad_path.proto:32:9 +error: unexpected `.` in identifier + --> testdata/parser/def/bad_path.proto:32:12 | 32 | service foo.(bar.baz).Bar {} - | ^^^^^^^^^^^^^^^^^ expected identifier + | ^ expected identifier = note: the name of a service definition must be a single identifier encountered 9 errors From ca538383c1f74f0b9fb139908ae93da49ce847b2 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 14:02:33 -0800 Subject: [PATCH 40/64] be more precise about classifying single-element paths --- experimental/ast/path.go | 8 +++----- experimental/internal/taxa/classify.go | 12 ++++++++++-- .../testdata/parser/def/ordering.proto.stderr.txt | 2 +- .../testdata/parser/enum/bad-path.proto.stderr.txt | 2 +- .../testdata/parser/field/bad-path.proto.stderr.txt | 2 +- internal/ext/iterx/iterx.go | 12 ++++++++++++ 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/experimental/ast/path.go b/experimental/ast/path.go index aa94f70d..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. diff --git a/experimental/internal/taxa/classify.go b/experimental/internal/taxa/classify.go index c67ba010..9f69cc87 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: diff --git a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index ae8d24cd..a5047501 100644 --- a/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt @@ -112,7 +112,7 @@ error: unexpected definition body in message field 28 | M x { /* ... */ } returns (T); | ^^^^^^^^^^^^^ -error: unexpected qualified name in message field +error: unexpected extension name in message field --> testdata/parser/def/ordering.proto:28:31 | 28 | M x { /* ... */ } returns (T); diff --git a/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt index d20aad38..8ceda0bd 100644 --- a/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/enum/bad-path.proto.stderr.txt @@ -4,7 +4,7 @@ error: unexpected qualified name in enum value 20 | foo.bar = 1; | ^^^^^^^ expected identifier -error: unexpected qualified name in enum value +error: unexpected extension name in enum value --> testdata/parser/enum/bad-path.proto:21:5 | 21 | (foo) = 2; diff --git a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt index dfd6aa46..a379f47a 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt @@ -76,7 +76,7 @@ error: unexpected nested extension path in message field 40 | (foo) (bar) = 1; | ^^^^^ -error: unexpected qualified name in message field +error: unexpected extension name in message field --> testdata/parser/field/bad-path.proto:40:11 | 40 | (foo) (bar) = 1; diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 67f67b39..0bb44caf 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -45,6 +45,18 @@ 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 +} + // 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 { From 58ffdcab5188dfc69b66e75a3cacacb4e1a957ee Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 16:14:35 -0800 Subject: [PATCH 41/64] legalize option values --- experimental/ast/predeclared/name.go | 2 +- experimental/internal/taxa/noun.go | 10 +- experimental/internal/taxa/noun.yaml | 6 +- experimental/parser/legalize_def.go | 21 +- experimental/parser/legalize_option.go | 270 ++++++++++++++- experimental/parser/legalize_path.go | 6 +- experimental/parser/lex.go | 7 +- experimental/parser/parse_starts.go | 2 +- .../parser/def/ordering.proto.stderr.txt | 110 +++++- .../parser/enum/incomplete.proto.stderr.txt | 14 +- .../testdata/parser/expr.proto.stderr.txt | 76 ++++- .../parser/field/incomplete.proto.stderr.txt | 8 +- .../testdata/parser/lists.proto.stderr.txt | 160 ++++++++- .../parser/option/bad_path.proto.stderr.txt | 14 +- .../testdata/parser/option/values.proto | 90 +++++ .../parser/option/values.proto.stderr.txt | 319 ++++++++++++++++++ .../testdata/parser/option/values.proto.yaml | 223 ++++++++++++ .../parser/package/absolute.proto.stderr.txt | 8 +- .../parser/type/generic.proto.stderr.txt | 12 +- internal/ext/iterx/iterx.go | 42 +++ 20 files changed, 1344 insertions(+), 56 deletions(-) create mode 100644 experimental/parser/testdata/parser/option/values.proto create mode 100644 experimental/parser/testdata/parser/option/values.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/option/values.proto.yaml diff --git a/experimental/ast/predeclared/name.go b/experimental/ast/predeclared/name.go index b017d3ee..1609185d 100644 --- a/experimental/ast/predeclared/name.go +++ b/experimental/ast/predeclared/name.go @@ -12,7 +12,7 @@ // 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. +// Code generated by github.com/bufbuild/protocompile/internal/enum name.yaml. DO NOT EDIT. package predeclared diff --git a/experimental/internal/taxa/noun.go b/experimental/internal/taxa/noun.go index 693e06b5..a95edacc 100644 --- a/experimental/internal/taxa/noun.go +++ b/experimental/internal/taxa/noun.go @@ -61,10 +61,12 @@ const ( MethodOuts Signature FieldTag + FieldName OptionValue QualifiedName FullyQualifiedName ExtensionName + TypeURL Expr Range Array @@ -177,10 +179,12 @@ var _table_Noun_String = [...]string{ 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", @@ -190,8 +194,8 @@ var _table_Noun_String = [...]string{ TypePath: "type name", TypeParams: "type parameters", TypePrefix: "type modifier", - MapKey: "map key", - MapValue: "map value", + MapKey: "map key type", + MapValue: "map value type", Whitespace: "whitespace", Comment: "comment", Ident: "identifier", @@ -276,10 +280,12 @@ var _table_Noun_GoString = [...]string{ MethodOuts: "MethodOuts", Signature: "Signature", FieldTag: "FieldTag", + FieldName: "FieldName", OptionValue: "OptionValue", QualifiedName: "QualifiedName", FullyQualifiedName: "FullyQualifiedName", ExtensionName: "ExtensionName", + TypeURL: "TypeURL", Expr: "Expr", Range: "Range", Array: "Array", diff --git a/experimental/internal/taxa/noun.yaml b/experimental/internal/taxa/noun.yaml index 0f2ca658..e4510169 100644 --- a/experimental/internal/taxa/noun.yaml +++ b/experimental/internal/taxa/noun.yaml @@ -63,11 +63,13 @@ - {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"} @@ -80,8 +82,8 @@ - {name: TypeParams, string: "type parameters"} - {name: TypePrefix, string: "type modifier"} - - {name: MapKey, string: "map key"} - - {name: MapValue, string: "map value"} + - {name: MapKey, string: "map key type"} + - {name: MapValue, string: "map value type"} - {name: Whitespace, string: "whitespace"} - {name: Comment, string: "comment"} diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index e339c6a6..5f211168 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -149,11 +149,22 @@ func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef) { want: taxa.Ident.AsSet(), }) } - - // NOTE: We do not legalize a missing value for fields and enum values - // here; instead, that happens during IR lowering. This is because we want - // to be able to include a suggested field number, but we cannot do that - // until much later, when we have evaluated expressions. + 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}) diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index 856a7eb9..505885de 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -15,10 +15,16 @@ 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. @@ -46,10 +52,10 @@ func legalizeCompactOptions(p *parser, opts ast.CompactOptions) { // 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, span report.Span) { +func legalizeOptionEntry(p *parser, opt ast.Option, decl report.Span) { if opt.Path.IsZero() { p.Errorf("missing %v path", taxa.Option).Apply( - report.Snippet(span), + report.Snippet(decl), ) // Don't bother legalizing if the value is zero. That can only happen @@ -64,7 +70,265 @@ func legalizeOptionEntry(p *parser, opt ast.Option, span report.Span) { if opt.Value.IsZero() { p.Errorf("missing %v", taxa.OptionValue).Apply( - report.Snippet(span), + 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 + } + + 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.Braces, 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,", taxa.Angles, taxa.Dict), + report.Notef("but as top-level option values"), + 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", + taxa.Colon, taxa.DictField), + report.Notef( + "is a %s or a %s of the same", + 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 index 6fa50386..4f74eb02 100644 --- a/experimental/parser/legalize_path.go +++ b/experimental/parser/legalize_path.go @@ -19,6 +19,7 @@ import ( "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]. @@ -43,9 +44,9 @@ type pathOptions struct { func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) (ok bool) { ok = true - var i, bytes, components int + var bytes, components int var slash token.Token - path.Components(func(pc ast.PathComponent) bool { + 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. @@ -97,7 +98,6 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) } } - i++ return true }) 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_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/testdata/parser/def/ordering.proto.stderr.txt b/experimental/parser/testdata/parser/def/ordering.proto.stderr.txt index a5047501..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,9 @@ +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 | @@ -12,6 +18,12 @@ 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 | @@ -26,6 +38,12 @@ 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 | @@ -46,6 +64,12 @@ 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 | @@ -60,6 +84,12 @@ 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 | @@ -72,12 +102,24 @@ error: unexpected nested extension path in message field 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 | @@ -92,6 +134,12 @@ 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 | @@ -106,18 +154,36 @@ 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 | @@ -138,6 +204,12 @@ 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 | @@ -164,6 +236,12 @@ 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 | @@ -184,12 +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 | @@ -206,6 +296,12 @@ 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 | @@ -218,6 +314,12 @@ error: unexpected tokens in message definition 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 | @@ -226,6 +328,12 @@ 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 | @@ -238,4 +346,4 @@ error: unexpected `[...]` in message definition 40 | M x { /* ... */ } [foo = bar]; | ^^^^^^^^^^^ expected identifier, `;`, `.`, `(...)`, or `{...}` -encountered 36 errors +encountered 54 errors 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/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt index d868a0fc..683435f3 100644 --- a/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/incomplete.proto.stderr.txt @@ -16,6 +16,12 @@ error: unexpected identifier after definition 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 | @@ -28,4 +34,4 @@ error: unexpected `}` after definition 27 | } | ^ expected `;` -encountered 5 errors +encountered 6 errors diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index a8f741cf..20384abb 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -3,6 +3,45 @@ warning: missing `package` declaration = note: not explicitly specifying a package places the file = note: 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 | @@ -11,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 | @@ -19,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 | @@ -33,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 | @@ -61,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 | @@ -73,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 | @@ -81,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 | @@ -261,13 +351,25 @@ error: expected exactly one type in method return type, got 0 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: unexpected non-comparable type in map key +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; @@ -275,7 +377,13 @@ error: unexpected non-comparable type in map key = help: map keys may be of any of the following types: = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string -error: unexpected non-comparable type in map key +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; @@ -291,7 +399,13 @@ error: unexpected type name in type parameters | | | note: assuming a missing `,` here -error: unexpected non-comparable type in map key +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; @@ -307,6 +421,12 @@ 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 | @@ -319,19 +439,31 @@ error: unexpected leading `,` in type parameters 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 +error: unexpected non-comparable type in map key type --> testdata/parser/lists.proto:59:10 | 59 | map<,int, int> x; @@ -339,7 +471,13 @@ error: unexpected non-comparable type in map key = help: map keys may be of any of the following types: = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string -error: unexpected non-comparable type in map key +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; @@ -353,7 +491,15 @@ error: unexpected `;` in type parameters 60 | map x; | ^ expected `,` -error: unexpected non-comparable type in map key +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, @@ -545,4 +691,4 @@ error: unexpected `message` after reserved range 77 | message Foo {} | ^^^^^^^ expected `;` -encountered 75 errors and 1 warning +encountered 94 errors and 1 warning diff --git a/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt index 6c61a7e1..e33dd65a 100644 --- a/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/option/bad_path.proto.stderr.txt @@ -22,12 +22,6 @@ error: unexpected absolute path in option setting 22 | option .(foo.bar).baz = 3; | ^^^^^^^^^^^^^^ expected a path without a leading `.` -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 | @@ -40,12 +34,6 @@ error: unexpected absolute path in option setting 27 | .(foo.bar).baz = 3, | ^^^^^^^^^^^^^^ expected a path without a leading `.` -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 | @@ -58,4 +46,4 @@ error: compact options cannot be empty 30 | int32 y = 2 []; | ^^ help: remove this -encountered 10 errors +encountered 8 errors 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..8c975f07 --- /dev/null +++ b/experimental/parser/testdata/parser/option/values.proto.stderr.txt @@ -0,0 +1,319 @@ +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, + = note: 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, + = note: 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, + = note: 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, + = note: 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, + = note: 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, + = note: 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 + = note: is a message expression or a array expression of the same + +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, + = note: 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/absolute.proto.stderr.txt b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt index 0c678bd8..69b5f891 100644 --- a/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/absolute.proto.stderr.txt @@ -4,10 +4,4 @@ error: unexpected absolute path in `package` declaration 17 | package .test.test2; | ^^^^^^^^^^^ expected a path without a leading `.` -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 2 errors +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 8370aab4..b3a538e1 100644 --- a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt +++ b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt @@ -1,4 +1,4 @@ -error: unexpected non-comparable type in map key +error: unexpected non-comparable type in map key type --> testdata/parser/type/generic.proto:21:9 | 21 | map x2 = 2; @@ -6,7 +6,7 @@ error: unexpected non-comparable type in map key = help: map keys may be of any of the following types: = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string -error: unexpected non-comparable type in map key +error: unexpected non-comparable type in map key type --> testdata/parser/type/generic.proto:22:9 | 22 | map x3 = 3; @@ -14,7 +14,7 @@ error: unexpected non-comparable type in map key = help: map keys may be of any of the following types: = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string -error: unexpected type in map value +error: unexpected type in map value type --> testdata/parser/type/generic.proto:23:17 | 23 | map> x4 = 4; @@ -56,13 +56,13 @@ error: unexpected type after `required` 32 | required map x10 = 10; | ^^^^^^^^^^^^^^^^^^^ expected type name -error: unexpected `repeated` in map value +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 +error: unexpected non-comparable type in map key type --> testdata/parser/type/generic.proto:35:9 | 35 | map x12 = 12; @@ -70,7 +70,7 @@ error: unexpected non-comparable type in map key = help: map keys may be of any of the following types: = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string -error: unexpected `required` in map value +error: unexpected `required` in map value type --> testdata/parser/type/generic.proto:35:27 | 35 | map x12 = 12; diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 0bb44caf..0c5fc110 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -57,6 +57,18 @@ func OnlyOne[T any](seq iter.Seq[T]) (v T, ok bool) { 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 { @@ -112,6 +124,36 @@ func FilterMap[T, U any](seq iter.Seq[T], f func(T) (U, bool)) iter.Seq[U] { } } +// 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 { From da83bd5c2f54c789b2c8ea2cf6b400f4ae0eb813 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 16:20:31 -0800 Subject: [PATCH 42/64] improve span for incomplete signatures --- experimental/parser/legalize_def.go | 16 +++++++++++++--- .../parser/method/incomplete.proto.stderr.txt | 18 +++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 5f211168..528d7404 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -253,7 +253,7 @@ func legalizeMethod(p *parser, def ast.DeclDef) { if sig.Inputs().Span().IsZero() { def.MarkCorrupt() p.Errorf("missing %v in %v", taxa.MethodIns, taxa.Method).Apply( - report.Snippet(def), + report.Snippetf(def.Name(), "expected type in %s after this", taxa.Parens), ) } else { legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns) @@ -261,15 +261,25 @@ func legalizeMethod(p *parser, def ast.DeclDef) { 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.Snippet(def), + 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 be either end in a ; or a {}. + // 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. diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index df5ff09d..03751705 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -5,16 +5,16 @@ error: missing `(...)` around method return type | ^^^^^^^ help: replace this with `(foo.Bar)` error: missing method return type in service method - --> testdata/parser/method/incomplete.proto:21:5 + --> 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:5 + --> 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 @@ -28,17 +28,17 @@ error: expected exactly one type in method parameter list, got 0 24 | rpc Bar5() returns (stream foo.Bar); | ^^ -error: missing method return type in service method - --> testdata/parser/method/incomplete.proto:25:5 +error: expected exactly one type in method parameter list, got 0 + --> testdata/parser/method/incomplete.proto:25:13 | 25 | rpc Bar6() returns; - | ^^^^^^^^^^^^^^^^^^^ + | ^^ -error: expected exactly one type in method parameter list, got 0 +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 From 5bdfe905447c20e2a56042199848cbdb4d6fa635 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 16:32:43 -0800 Subject: [PATCH 43/64] add more validation for types --- experimental/parser/legalize_type.go | 8 +- .../parser/testdata/parser/def/mixed.proto | 4 +- .../parser/def/mixed.proto.stderr.txt | 8 +- .../testdata/parser/def/mixed.proto.yaml | 5 + .../testdata/parser/field/bad-path.proto | 6 ++ .../parser/field/bad-path.proto.stderr.txt | 94 +++++++++++++++---- .../testdata/parser/field/bad-path.proto.yaml | 36 +++++++ .../testdata/parser/method/bad_type.proto | 2 + .../parser/method/bad_type.proto.stderr.txt | 14 ++- .../parser/method/bad_type.proto.yaml | 17 ++++ 10 files changed, 167 insertions(+), 27 deletions(-) diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index 661e55c3..cbbae203 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -34,8 +34,7 @@ func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { ty := list.At(0) switch ty.Kind() { case ast.TypeKindPath: - // Allow all path types. We don't know if this is a message or not yet; - // figuring that out is a problem for IR construction. + legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) case ast.TypeKindPrefixed: prefixed := ty.AsPrefixed() if prefixed.Prefix() != ast.TypePrefixStream { @@ -45,6 +44,7 @@ func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { } if prefixed.Type().Kind() == ast.TypeKindPath { + legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) break } @@ -73,7 +73,7 @@ func legalizeFieldType(p *parser, ty ast.TypeAny) { inner := ty.Type() switch inner.Kind() { case ast.TypeKindPath: - break + legalizeFieldType(p, inner) case ast.TypeKindPrefixed: p.Error(errMoreThanOne{ first: ty.PrefixToken(), @@ -117,7 +117,7 @@ func legalizeFieldType(p *parser, ty ast.TypeAny) { switch v.Kind() { case ast.TypeKindPath: - break + legalizeFieldType(p, v) case ast.TypeKindPrefixed: p.Error(errUnexpected{ what: v.AsPrefixed().PrefixToken(), diff --git a/experimental/parser/testdata/parser/def/mixed.proto b/experimental/parser/testdata/parser/def/mixed.proto index 3bdf5fb3..350aab5f 100644 --- a/experimental/parser/testdata/parser/def/mixed.proto +++ b/experimental/parser/testdata/parser/def/mixed.proto @@ -38,4 +38,6 @@ message Foo(X) returns (X) { } extend bar.Foo(X) returns (X) {} -service FooService(X) returns (X) {} \ No newline at end of file +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 index 208e5587..e1563c06 100644 --- a/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/mixed.proto.stderr.txt @@ -88,4 +88,10 @@ error: service definition appears to have method signature 41 | service FooService(X) returns (X) {} | ^^^^^^^^^^^^^^^ help: remove this -encountered 15 errors +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 index 7d9da1f2..92b8be84 100644 --- a/experimental/parser/testdata/parser/def/mixed.proto.yaml +++ b/experimental/parser/testdata/parser/def/mixed.proto.yaml @@ -96,3 +96,8 @@ decls: 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/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 index a379f47a..c23ffc39 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/field/bad-path.proto.stderr.txt @@ -23,63 +23,117 @@ error: unexpected qualified name in message field | ^^^^^^^^^ expected identifier error: unexpected qualified name in message field - --> testdata/parser/field/bad-path.proto:25:27 + --> testdata/parser/field/bad-path.proto:24:10 | -25 | optional package.Type path.name = 1; - | ^^^^^^^^^ expected identifier +24 | Type path/name = 1; + | ^^^^^^^^^ expected identifier error: unexpected qualified name in message field --> testdata/parser/field/bad-path.proto:26:27 | -26 | repeated package.Type path.name = 1; +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 | required package.Type path.name = 1; +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:5 + --> testdata/parser/field/bad-path.proto:33:14 | -33 | (foo.bar).Type name = 1; +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:35:27 + --> testdata/parser/field/bad-path.proto:38:27 | -35 | optional package.Type (foo.bar).name = 1; +38 | optional package.Type (foo.bar).name = 1; | ^^^^^^^^^^^^^^ expected identifier error: unexpected qualified name in message field - --> testdata/parser/field/bad-path.proto:36:27 + --> testdata/parser/field/bad-path.proto:39:27 | -36 | repeated package.Type (foo.bar).name = 1; +39 | repeated package.Type (foo.bar).name = 1; | ^^^^^^^^^^^^^^ expected identifier error: unexpected qualified name in message field - --> testdata/parser/field/bad-path.proto:37:27 + --> testdata/parser/field/bad-path.proto:40:27 | -37 | required package.Type (foo.bar).name = 1; +40 | required package.Type (foo.bar).name = 1; | ^^^^^^^^^^^^^^ expected identifier error: unexpected qualified name in message field - --> testdata/parser/field/bad-path.proto:38:18 + --> testdata/parser/field/bad-path.proto:41:18 | -38 | package.Type (foo.bar).name = 1; +41 | package.Type (foo.bar).name = 1; | ^^^^^^^^^^^^^^ expected identifier error: unexpected nested extension path in message field - --> testdata/parser/field/bad-path.proto:40:5 + --> testdata/parser/field/bad-path.proto:43:5 | -40 | (foo) (bar) = 1; +43 | (foo) (bar) = 1; | ^^^^^ error: unexpected extension name in message field - --> testdata/parser/field/bad-path.proto:40:11 + --> testdata/parser/field/bad-path.proto:43:11 | -40 | (foo) (bar) = 1; +43 | (foo) (bar) = 1; | ^^^^^ expected identifier -encountered 14 errors +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 dbfd35f2..64fade41 100644 --- a/experimental/parser/testdata/parser/field/bad-path.proto.yaml +++ b/experimental/parser/testdata/parser/field/bad-path.proto.yaml @@ -27,6 +27,10 @@ decls: name.components: [{ ident: "path" }, { ident: "name", separator: SEPARATOR_DOT }] type.path.components: [{ ident: "Type" }] value.literal.int_value: 1 + - def: + 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: @@ -50,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" }] @@ -118,3 +133,24 @@ decls: 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/method/bad_type.proto b/experimental/parser/testdata/parser/method/bad_type.proto index 32fc255d..82a2c1da 100644 --- a/experimental/parser/testdata/parser/method/bad_type.proto +++ b/experimental/parser/testdata/parser/method/bad_type.proto @@ -25,4 +25,6 @@ service Foo { 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 index 6d5d1b12..f7ad1e56 100644 --- a/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/bad_type.proto.stderr.txt @@ -52,4 +52,16 @@ error: only message types may appear in method parameter list 27 | rpc Bar7(stream map) returns (foo.Bar); | ^^^^^^^^^^^^^^^^^^^^ -encountered 9 errors +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 index 1784624a..fb8cf498 100644 --- a/experimental/parser/testdata/parser/method/bad_type.proto.yaml +++ b/experimental/parser/testdata/parser/method/bad_type.proto.yaml @@ -99,3 +99,20 @@ decls: - { 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 } From 5d3ddb3360091ca27e4056a72e65a5c3d32ccc33 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 16:33:24 -0800 Subject: [PATCH 44/64] move a var --- experimental/parser/legalize_file.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index b0090367..c6e426ed 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -27,6 +27,10 @@ import ( "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 ( @@ -235,8 +239,6 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka }) } -var isOrdinaryFilePath = regexp.MustCompile("[0-9a-zA-Z./_-]*") - // legalizeImport legalizes a DeclImport. // // imports is a map that classifies DeclImports by the contents of their import string. From e3318a45c8c23987eab268c03a2725e47f215c19 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 17:08:47 -0800 Subject: [PATCH 45/64] legalize extn ranges --- experimental/internal/taxa/classify.go | 5 + experimental/internal/taxa/noun.go | 6 + experimental/internal/taxa/noun.yaml | 5 +- experimental/parser/legalize_decl.go | 140 ++++++++++++++---- .../testdata/parser/lists.proto.stderr.txt | 12 +- .../range/extension_names.proto.stderr.txt | 13 ++ .../testdata/parser/range/invalid_exprs.proto | 2 + .../range/invalid_exprs.proto.stderr.txt | 37 +++++ .../parser/range/invalid_exprs.proto.yaml | 10 ++ 9 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 experimental/parser/testdata/parser/range/extension_names.proto.stderr.txt create mode 100644 experimental/parser/testdata/parser/range/invalid_exprs.proto.stderr.txt diff --git a/experimental/internal/taxa/classify.go b/experimental/internal/taxa/classify.go index 9f69cc87..67f080d9 100644 --- a/experimental/internal/taxa/classify.go +++ b/experimental/internal/taxa/classify.go @@ -300,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 a95edacc..d33978e9 100644 --- a/experimental/internal/taxa/noun.go +++ b/experimental/internal/taxa/noun.go @@ -125,6 +125,8 @@ const ( KeywordRequired KeywordGroup KeywordStream + PredeclaredMap + PredeclaredMax total int = iota ) @@ -243,6 +245,8 @@ var _table_Noun_String = [...]string{ KeywordRequired: "`required`", KeywordGroup: "`group`", KeywordStream: "`stream`", + PredeclaredMap: "`map`", + PredeclaredMax: "`max`", } var _table_Noun_GoString = [...]string{ @@ -344,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 e4510169..cb189e7f 100644 --- a/experimental/internal/taxa/noun.yaml +++ b/experimental/internal/taxa/noun.yaml @@ -139,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/legalize_decl.go b/experimental/parser/legalize_decl.go index c4430661..3e9be04e 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -16,12 +16,15 @@ 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" @@ -101,46 +104,104 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } } - // We only legalize reserved name productions here, because that depends on - // the syntax/edition keyword. All other expressions are legalized when we - // do constant evaluation. + 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) + } + } - if in != taxa.Reserved { - return + 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() + 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 { - var isName bool switch expr.Kind() { case ast.ExprKindPath: - isName = true path := expr.AsPath() - if !path.AsIdent().IsZero() { - if m := p.Mode(); m == taxa.SyntaxMode { - 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: `"`, - }, - ), - ) - } + 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 { - isName = true + 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), @@ -158,7 +219,7 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { )) } - return true + break } if !isASCIIIdent(name) { @@ -169,19 +230,34 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { p.Errorf("reserved %v name is not a valid identifier", field).Apply( report.Snippet(expr), ) - return true + break } if !lit.IsPureString() { p.Warn(errImpureString{lit.Token, in.In()}) } + + break } - } - if isName { - names = append(names, expr) - } else { + 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 diff --git a/experimental/parser/testdata/parser/lists.proto.stderr.txt b/experimental/parser/testdata/parser/lists.proto.stderr.txt index 20384abb..583568dd 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -581,19 +581,11 @@ error: cannot use identifiers in reserved range in syntax mode 72 | reserved "a" {}; | + + -error: cannot mix tags and names in reserved range +error: unexpected message expression in reserved range --> testdata/parser/lists.proto:72:16 | 72 | reserved a {}; - | - ^^ this field tag must go in its own reserved range - | | - | but expected a field name because of this - help: split the reserved range - | -72 | - reserved a {}; -72 | + reserved a; -73 | + reserved {}; - | + | ^^ expected range expression, string literal, or integer literal error: unexpected message expression in reserved range --> testdata/parser/lists.proto:72:16 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" From 2c9bfc943bdda9b79d65507e7ca2c408b7b65979 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 10 Feb 2025 17:11:30 -0800 Subject: [PATCH 46/64] lint --- experimental/parser/legalize_decl.go | 1 + experimental/parser/legalize_option.go | 1 + 2 files changed, 2 insertions(+) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 3e9be04e..8adad3e2 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -127,6 +127,7 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } case ast.ExprKindPrefixed: expr := expr.AsPrefixed() + //nolint:gocritic // Intentional single-case switch. switch expr.Prefix() { case ast.ExprPrefixMinus: lit := expr.Expr().AsLiteral() diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index 505885de..2dc3f39a 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -104,6 +104,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value return } + //nolint:gocritic // Intentional single-case switch. switch value.Prefix() { case ast.ExprPrefixMinus: ok := value.Expr().AsLiteral().Kind() == token.Number From b3b3c6b7e478bd70334e79d9f3964b55c863be32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:39:25 -0500 Subject: [PATCH 47/64] Bump golang.org/x/tools from 0.29.0 to 0.30.0 in /internal/tools (#440) --- internal/tools/go.mod | 8 ++++---- internal/tools/go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) 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= From 308341c48461ff5d1d7c12d7920cb63a10fba37e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:41:47 -0500 Subject: [PATCH 48/64] Bump golang.org/x/sync from 0.10.0 to 0.11.0 in /internal/benchmarks (#442) --- internal/benchmarks/go.mod | 2 +- internal/benchmarks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/benchmarks/go.mod b/internal/benchmarks/go.mod index ea62ece1..f2095ef7 100644 --- a/internal/benchmarks/go.mod +++ b/internal/benchmarks/go.mod @@ -10,7 +10,7 @@ require ( google.golang.org/protobuf v1.36.4 ) -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..7f20e4ed 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= From 449dfd8257a0af1051d8e85420610796ede2e61e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:42:05 -0500 Subject: [PATCH 49/64] Bump golang.org/x/sync from 0.10.0 to 0.11.0 (#444) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0e4fc6e1..b975b412 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ 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 + golang.org/x/sync v0.11.0 google.golang.org/protobuf v1.36.4 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 49fb94d3..414ed647 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ 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= +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.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 418a3ac149e711bba5706b7f27ee4efd1511a3b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:55:15 +0000 Subject: [PATCH 50/64] Bump google.golang.org/protobuf from 1.36.4 to 1.36.5 (#443) --- go.mod | 2 +- go.sum | 4 ++-- go.work.sum | 2 ++ internal/benchmarks/go.mod | 2 +- internal/benchmarks/go.sum | 4 ++-- linker/descriptors.go | 1 + 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b975b412..4d0cc1c9 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/rivo/uniseg v0.4.7 github.com/stretchr/testify v1.10.0 golang.org/x/sync v0.11.0 - google.golang.org/protobuf v1.36.4 + google.golang.org/protobuf v1.36.5 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 414ed647..d73b7f15 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.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/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 f2095ef7..40bfeae7 100644 --- a/internal/benchmarks/go.mod +++ b/internal/benchmarks/go.mod @@ -7,7 +7,7 @@ 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.11.0 diff --git a/internal/benchmarks/go.sum b/internal/benchmarks/go.sum index 7f20e4ed..2dc39fb2 100644 --- a/internal/benchmarks/go.sum +++ b/internal/benchmarks/go.sum @@ -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/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} From 5ea57e483b135758cbac52576ff8211880e8e4f9 Mon Sep 17 00:00:00 2001 From: Doria Keung Date: Tue, 11 Feb 2025 18:10:04 -0500 Subject: [PATCH 51/64] Fix cursor-at for close tokens (#445) Since open/close tokens are fused, when we set the `idx` for `NewCursorAt` for close tokens, we need to use the offset to set it to the open token. --- experimental/token/cursor.go | 1 + experimental/token/cursor_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) 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()) + }) } From 8ed3e0ae18edcf44daf0e111f8877ffd228548f7 Mon Sep 17 00:00:00 2001 From: Doria Keung Date: Wed, 12 Feb 2025 17:49:46 -0500 Subject: [PATCH 52/64] Set brackets on TypeList in parser (#448) Add a setter for brackets on `TypeList` and set them in the parser. --- experimental/ast/type_generic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/ast/type_generic.go b/experimental/ast/type_generic.go index 24802c87..d02665f2 100644 --- a/experimental/ast/type_generic.go +++ b/experimental/ast/type_generic.go @@ -146,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 From 901587e294397c6f6f6fae727b0c31f76debcfb8 Mon Sep 17 00:00:00 2001 From: Miguel Young Date: Wed, 12 Feb 2025 17:59:50 -0500 Subject: [PATCH 53/64] Implement text wrapping for most diagnostic messages (#446) This PR adds basic text wrapping to diagnostic messages (except those attached to snippets) to avoid needing to wrap notes/helps by hand when writing diagnostics. --- .../lexer/strings/unclosed2.proto.stderr.txt | 3 +- experimental/report/diff.go | 12 +- experimental/report/renderer.go | 68 +++++----- experimental/report/span.go | 3 - .../report/testdata/i18n.yaml.color.txt | 4 +- .../report/testdata/multi-file.yaml.color.txt | 4 +- .../testdata/multi-underline.yaml.color.txt | 4 +- .../report/testdata/multiline.yaml.color.txt | 22 ++-- experimental/report/testdata/no-snippets.yaml | 15 +++ .../testdata/no-snippets.yaml.color.txt | 30 ++++- .../testdata/no-snippets.yaml.fancy.txt | 18 ++- .../testdata/no-snippets.yaml.simple.txt | 2 + .../testdata/single-line.yaml.color.txt | 16 +-- .../testdata/suggestions.yaml.color.txt | 14 +- .../report/testdata/tabstops.yaml.color.txt | 4 +- experimental/report/width.go | 49 +++++++ experimental/report/writer.go | 31 +++++ internal/ext/iterx/iterx.go | 16 +++ internal/ext/slicesx/collect.go | 41 ------ internal/ext/slicesx/iter.go | 121 ++++++++++++++++++ internal/ext/slicesx/partition.go | 45 ------- internal/ext/slicesx/partition_test.go | 2 +- internal/ext/slicesx/slicesx.go | 12 -- internal/ext/stringsx/stringsx.go | 35 +++++ 24 files changed, 387 insertions(+), 184 deletions(-) delete mode 100644 internal/ext/slicesx/collect.go create mode 100644 internal/ext/slicesx/iter.go delete mode 100644 internal/ext/slicesx/partition.go diff --git a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt index 0f05af55..2ab7e03d 100644 --- a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt +++ b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt @@ -3,6 +3,7 @@ error: unterminated string literal | 1 | '\' | ^^^ expected to be terminated by `'` - = note: this string appears to end in an escaped quote; replace `\'` with `\\''` + = note: this string appears to end in an escaped quote; replace `\'` with + `\\''` encountered 1 error 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 6ea49129..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}) - } - for _, help := range d.help { - footers = append(footers, [3]string{r.ss.bRemark, "help", help}) + type footer struct { + color, label, text string } - 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) @@ -934,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 e1dcfb51..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 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 4cae1390..03ef0457 100755 --- a/experimental/report/testdata/multi-underline.yaml.color.txt +++ b/experimental/report/testdata/multi-underline.yaml.color.txt @@ -1,4 +1,4 @@ -⟨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" @@ -7,7 +7,7 @@ ⟨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/multiline.yaml.color.txt b/experimental/report/testdata/multiline.yaml.color.txt index 8e91453c..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,7 +18,7 @@ ⟨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 { @@ -31,7 +31,7 @@ ⟨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/no-snippets.yaml b/experimental/report/testdata/no-snippets.yaml index 62360465..e5f89a18 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 very very very very very very very very very very very very very very very very very very long" + - "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..1fddb326 100755 --- a/experimental/report/testdata/no-snippets.yaml.color.txt +++ b/experimental/report/testdata/no-snippets.yaml.color.txt @@ -1,13 +1,29 @@ -⟨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 very very very very very very very very very very very + very very very very very very very long + ⟨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..c8d8a43f 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,17 @@ 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 very very very very very very very very very very very + very very very very very very very long + = 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..a6daeafc 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,44 @@ 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 { + line = strings.TrimSpace(line) + var nextIsSpace bool + var column, cursor int + + stringsx.PartitionKey(line, unicode.IsSpace)(func(start int, chunk string) bool { + isSpace := nextIsSpace + nextIsSpace = !nextIsSpace + + column = stringWidth(column, chunk, true, nil) + if column <= width { + return true + } + + line := line[cursor:start] + if !yield(strings.TrimSpace(line)) { + return false + } + cursor = start + if isSpace { + cursor += len(chunk) + } + column = 0 + 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/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 0c5fc110..e45f7b76 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -170,3 +170,19 @@ func Join[T any](seq iter.Seq[T], sep string) string { }) 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/slicesx/collect.go deleted file mode 100644 index 082bbaff..00000000 --- a/internal/ext/slicesx/collect.go +++ /dev/null @@ -1,41 +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 iters contains helpers for working with iterators. -package slicesx - -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) - return true - }) - return s -} - -// 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) - } - return out -} 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 dada2743..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. @@ -83,14 +82,3 @@ func BoundsCheck[I SliceIndex](idx I, len int) bool { 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 c190ad08..3066ecd8 100644 --- a/internal/ext/stringsx/stringsx.go +++ b/internal/ext/stringsx/stringsx.go @@ -99,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:]) + } + } +} From 75102e7aab9dd191c6c329e64bacbf69dbf5cfd6 Mon Sep 17 00:00:00 2001 From: Miguel Young Date: Wed, 12 Feb 2025 18:35:53 -0500 Subject: [PATCH 54/64] Fix minor mis-accounting of column position in `report.wordWrap` (#449) This PR also adds a test to exercise the corner case. --- experimental/report/testdata/no-snippets.yaml | 2 +- .../testdata/no-snippets.yaml.color.txt | 5 +++-- .../testdata/no-snippets.yaml.fancy.txt | 5 +++-- experimental/report/width.go | 22 ++++++++++++------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/experimental/report/testdata/no-snippets.yaml b/experimental/report/testdata/no-snippets.yaml index e5f89a18..6b718a9c 100644 --- a/experimental/report/testdata/no-snippets.yaml +++ b/experimental/report/testdata/no-snippets.yaml @@ -36,7 +36,7 @@ diagnostics: level: LEVEL_REMARK in_file: foo.proto notes: - - "this footer is very very very very very very very very very very very very very very very very very very long" + - "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)" diff --git a/experimental/report/testdata/no-snippets.yaml.color.txt b/experimental/report/testdata/no-snippets.yaml.color.txt index 1fddb326..1e5a4b27 100755 --- a/experimental/report/testdata/no-snippets.yaml.color.txt +++ b/experimental/report/testdata/no-snippets.yaml.color.txt @@ -14,8 +14,9 @@ ⟨b.cyn⟩remark: very long footers ⟨blu⟩ --> foo.proto - ⟨blu⟩ = ⟨b.cyn⟩note: ⟨reset⟩this footer is very very very very very very very very very very very - very very very very very very very long + ⟨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 diff --git a/experimental/report/testdata/no-snippets.yaml.fancy.txt b/experimental/report/testdata/no-snippets.yaml.fancy.txt index c8d8a43f..689ecb2d 100755 --- a/experimental/report/testdata/no-snippets.yaml.fancy.txt +++ b/experimental/report/testdata/no-snippets.yaml.fancy.txt @@ -14,8 +14,9 @@ warning: file consists only of the byte `0xaa` remark: very long footers --> foo.proto - = note: this footer is very very very very very very very very very very very - very very very very very very very long + = 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 diff --git a/experimental/report/width.go b/experimental/report/width.go index a6daeafc..5b1e8481 100644 --- a/experimental/report/width.go +++ b/experimental/report/width.go @@ -48,7 +48,6 @@ func wordWrap(text string, width int) iter.Seq[string] { // Split along lines first, since those are hard breaks we don't plan // to change. stringsx.Lines(text)(func(line string) bool { - line = strings.TrimSpace(line) var nextIsSpace bool var column, cursor int @@ -56,20 +55,27 @@ func wordWrap(text string, width int) iter.Seq[string] { isSpace := nextIsSpace nextIsSpace = !nextIsSpace - column = stringWidth(column, chunk, true, nil) - if column <= width { + if isSpace && column == 0 { return true } - line := line[cursor:start] - if !yield(strings.TrimSpace(line)) { + w := stringWidth(column, chunk, true, nil) - column + if column+w <= width { + column += w + return true + } + + if !yield(strings.TrimSpace(line[cursor:start])) { return false } - cursor = start + if isSpace { - cursor += len(chunk) + cursor = start + len(chunk) + column = 0 + } else { + cursor = start + column = w } - column = 0 return true }) From bbdf2f452af807e2f8404552c4386776e97947fa Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 12 Feb 2025 15:21:48 -0800 Subject: [PATCH 55/64] use automatic word wrapping --- experimental/parser/diagnostics_string.go | 17 +++-- experimental/parser/legalize_def.go | 6 +- experimental/parser/legalize_file.go | 26 +++++--- experimental/parser/legalize_option.go | 11 ++-- experimental/parser/legalize_type.go | 12 ++-- .../lexer/strings/unclosed2.proto.stderr.txt | 7 ++- .../parser/def/nesting.proto.stderr.txt | 51 ++++++++++----- .../parser/import/escapes.proto.stderr.txt | 4 +- .../parser/import/ok.proto.stderr.txt | 3 +- .../parser/import/options.proto.stderr.txt | 3 +- .../parser/import/symbol.proto.stderr.txt | 12 ++-- .../testdata/parser/lists.proto.stderr.txt | 34 +++++----- .../parser/method/options.proto.stderr.txt | 4 +- .../parser/option/values.proto.stderr.txt | 63 ++++++++++--------- .../parser/package/42.proto.stderr.txt | 4 +- .../parser/package/empty.proto.stderr.txt | 4 +- .../package/eof_after_kw.proto.stderr.txt | 4 +- .../parser/package/no_path.proto.stderr.txt | 4 +- .../parser/range/escapes.proto.stderr.txt | 4 +- .../range/invalid_parent.proto.stderr.txt | 6 +- .../parser/syntax/2024.proto.stderr.txt | 2 +- .../syntax/edition_proto2.proto.stderr.txt | 2 +- .../syntax/eof_after_eq.proto.stderr.txt | 4 +- .../syntax/eof_after_kw.proto.stderr.txt | 4 +- .../parser/syntax/invalid.proto.stderr.txt | 2 +- .../parser/syntax/lonely.proto.stderr.txt | 7 ++- .../syntax/proto2_split.proto.stderr.txt | 4 +- .../parser/syntax/proto4.proto.stderr.txt | 2 +- .../syntax/syntax_2023.proto.stderr.txt | 2 +- .../parser/type/generic.proto.stderr.txt | 15 +++-- 30 files changed, 190 insertions(+), 133 deletions(-) diff --git a/experimental/parser/diagnostics_string.go b/experimental/parser/diagnostics_string.go index 1631a44f..cd040332 100644 --- a/experimental/parser/diagnostics_string.go +++ b/experimental/parser/diagnostics_string.go @@ -43,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 @@ -116,8 +121,10 @@ func (e errImpureString) Diagnose(d *report.Diagnostic) { if !e.lit.IsLeaf() { d.Apply( - report.Notef("Protobuf implicitly concatenates adjacent %ss,", taxa.String), - report.Notef("like C or Python, which can lead to surprising behavior"), + 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_def.go b/experimental/parser/legalize_def.go index 528d7404..b2768f12 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -285,8 +285,10 @@ func legalizeMethod(p *parser, def ast.DeclDef) { if options := def.Options(); !options.IsZero() { p.Error(errHasOptions{def}).Apply( - report.Notef("service method options are applied using %v", taxa.KeywordOption), - report.Notef("declarations in the %v following the method definition", taxa.Braces), + 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 index c6e426ed..bc9934d7 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -56,8 +56,9 @@ func legalizeFile(p *parser, file ast.File) { 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"), - report.Notef("in the unnamed package; using it strongly is discouraged"), + report.Notef( + "not explicitly specifying a package places the file in the "+ + "unnamed package; using it strongly is discouraged"), ) } } @@ -142,7 +143,7 @@ func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax return fmt.Sprintf("%q", s), true }), ", ") - return report.Notef("permitted values: [%s]", values) + return report.Notef("permitted values: %s", values) } value := syntax.Lookup(name) @@ -212,7 +213,10 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka 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), + report.Helpf( + "a file's %s should immediately follow the `syntax` or `edition` declaration", + taxa.Package, + ), ) return } @@ -228,8 +232,11 @@ func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPacka 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, remove the %s", taxa.Package), - report.Helpf("however, using the unnamed package is discouraged"), + report.Helpf( + "to place a file in the unnamed package, omit the %s; however, "+ + "using the unnamed package is discouraged", + taxa.Package, + ), ) } @@ -304,8 +311,8 @@ func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports m // 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"), - report.Notef("instead, try importing a file, e.g. `import \"google/protobuf/descriptor.proto\";`"), + report.Notef("Protobuf does not support importing symbols by name, instead, " + + "try importing a file, e.g. `import \"google/protobuf/descriptor.proto\";`"), ) return @@ -333,7 +340,8 @@ func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports m 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"), + 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 index 2dc3f39a..252c841b 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -194,7 +194,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value 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.Braces, taxa.Dict) + err = p.Warnf("using %s for %s is not recommended", taxa.Angles, taxa.Dict) } err.Apply( @@ -205,8 +205,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value 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,", taxa.Angles, taxa.Dict), - report.Notef("but as top-level option values"), + 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), ) } @@ -309,10 +308,8 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value "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", - taxa.Colon, taxa.DictField), - report.Notef( - "is a %s or a %s of the same", + "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), ) diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index cbbae203..bcc61a61 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -107,11 +107,13 @@ func legalizeFieldType(p *parser, ty ast.TypeAny) { where: taxa.MapKey.In(), got: "non-comparable type", }).Apply( - report.Helpf("map keys may be of any of the following types:"), - report.Helpf("%s", iterx.Join(iterx.Filter( - predeclared.All(), - func(p predeclared.Name) bool { return p.IsMapKey() }, - ), ", ")), + 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() }, + ), ", "), + ), ) } diff --git a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt index 2ab7e03d..f2ecd152 100644 --- a/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt +++ b/experimental/parser/testdata/lexer/strings/unclosed2.proto.stderr.txt @@ -3,7 +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/nesting.proto.stderr.txt b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt index 165e7fcb..a65f6752 100644 --- a/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/nesting.proto.stderr.txt @@ -22,7 +22,8 @@ error: unexpected message definition within enum definition ... | 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 + = 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 @@ -35,7 +36,8 @@ error: unexpected enum definition within enum definition ... | 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 + = 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 @@ -62,7 +64,8 @@ error: unexpected message extension block within enum definition 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 + = 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 @@ -74,7 +77,8 @@ error: unexpected oneof definition within enum definition | | ^^^^^^^^^^ 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 + = 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 @@ -86,7 +90,8 @@ error: unexpected message definition within service definition ... | 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 + = 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 @@ -99,7 +104,8 @@ error: unexpected enum definition within service definition ... | 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 + = 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 @@ -126,7 +132,8 @@ error: unexpected message extension block within service definition 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 + = 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 @@ -138,7 +145,8 @@ error: unexpected oneof definition within service definition | | ^^^^^^^^^^ 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 + = 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 @@ -150,7 +158,8 @@ error: unexpected message definition within message extension block ... | 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 + = 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 @@ -163,7 +172,8 @@ error: unexpected enum definition within message extension block ... | 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 + = 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 @@ -190,7 +200,8 @@ error: unexpected message extension block within 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 + = 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 @@ -202,7 +213,8 @@ error: unexpected oneof definition within message extension block | | ^^^^^^^^^^ 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 + = 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 @@ -211,7 +223,8 @@ error: unexpected oneof definition at file scope ... | 57 | | } | \_^ this oneof definition cannot be declared here - = help: a oneof definition can only appear within one of message definition or group definition + = 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 @@ -223,7 +236,8 @@ error: unexpected message definition within oneof definition ... | 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 + = 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 @@ -236,7 +250,8 @@ error: unexpected enum definition within oneof definition ... | 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 + = 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 @@ -263,7 +278,8 @@ error: unexpected message extension block within oneof definition 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 + = 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 @@ -275,6 +291,7 @@ error: unexpected oneof definition within oneof definition | | ^^^^^^^^^^ 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 + = 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/import/escapes.proto.stderr.txt b/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt index 680f6ee0..d53e2b0a 100644 --- a/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/escapes.proto.stderr.txt @@ -19,7 +19,7 @@ warning: non-canonical string literal in import 20 | - import "bar" ".proto"; 20 | + import "bar.proto"; | - = note: Protobuf implicitly concatenates adjacent string literals, - = note: like C or Python, which can lead to surprising behavior + = 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/ok.proto.stderr.txt b/experimental/parser/testdata/parser/import/ok.proto.stderr.txt index dfde688c..4987c246 100644 --- a/experimental/parser/testdata/parser/import/ok.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/ok.proto.stderr.txt @@ -3,6 +3,7 @@ warning: use of `import weak` | 20 | import weak "weak.proto"; | ^^^^^^^^^^^ - = note: `import weak` is deprecated and not supported correctly in most Protobuf implementations + = 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 index 9abe7524..6af9290f 100644 --- a/experimental/parser/testdata/parser/import/options.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/options.proto.stderr.txt @@ -9,7 +9,8 @@ warning: use of `import weak` | 20 | import weak "weak.proto" [(not.allowed) = "here"]; | ^^^^^^^^^^^ - = note: `import weak` is deprecated and not supported correctly in most Protobuf implementations + = 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 diff --git a/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt b/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt index 6c1484a1..f7b1821e 100644 --- a/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt +++ b/experimental/parser/testdata/parser/import/symbol.proto.stderr.txt @@ -3,23 +3,23 @@ error: unexpected qualified name in import | 19 | import my.Proto; | ^^^^^^^^ expected string literal - = note: Protobuf does not support importing symbols by name - = note: instead, try importing a file, e.g. `import "google/protobuf/descriptor.proto";` + = 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 - = note: instead, try importing a file, e.g. `import "google/protobuf/descriptor.proto";` + = 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 - = note: instead, try importing a file, e.g. `import "google/protobuf/descriptor.proto";` + = 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 583568dd..848d7dc1 100644 --- a/experimental/parser/testdata/parser/lists.proto.stderr.txt +++ b/experimental/parser/testdata/parser/lists.proto.stderr.txt @@ -1,7 +1,7 @@ warning: missing `package` declaration --> testdata/parser/lists.proto - = note: not explicitly specifying a package places the file - = note: in the unnamed package; using it strongly is discouraged + = 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 @@ -374,8 +374,9 @@ error: unexpected non-comparable type in map key type | 54 | map x; | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -388,8 +389,9 @@ error: unexpected non-comparable type in map key type | 55 | map x; | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -410,8 +412,9 @@ error: unexpected non-comparable type in map key type | 56 | map x; | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -468,8 +471,9 @@ error: unexpected non-comparable type in map key type | 59 | map<,int, int> x; | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -482,8 +486,9 @@ error: unexpected non-comparable type in map key type | 60 | map x; | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -504,8 +509,9 @@ error: unexpected non-comparable type in map key type | 62 | int, | ^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 diff --git a/experimental/parser/testdata/parser/method/options.proto.stderr.txt b/experimental/parser/testdata/parser/method/options.proto.stderr.txt index 472831af..433b1ba2 100644 --- a/experimental/parser/testdata/parser/method/options.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/options.proto.stderr.txt @@ -3,7 +3,7 @@ error: service method cannot specify compact options | 20 | rpc Bar1(foo.Bar) returns (foo.Bar) [not.(allowed).here = 42]; | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove this - = note: service method options are applied using `option` - = note: declarations in the `{...}` following the method definition + = 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/option/values.proto.stderr.txt b/experimental/parser/testdata/parser/option/values.proto.stderr.txt index 8c975f07..ae3bc7fd 100644 --- a/experimental/parser/testdata/parser/option/values.proto.stderr.txt +++ b/experimental/parser/testdata/parser/option/values.proto.stderr.txt @@ -69,9 +69,10 @@ error: cannot use `<...>` for message expression here 38 | - option x = <>: 38 | + option x = {}: | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 @@ -95,9 +96,10 @@ error: cannot use `<...>` for message expression here 39 | - option x = ; 39 | + option x = {a: 42}; | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 @@ -164,7 +166,7 @@ error: unexpected trailing `,` in array expression 66 | [1], | ^ -warning: using `{...}` for message expression is not recommended +warning: using `<...>` for message expression is not recommended --> testdata/parser/option/values.proto:69:8 | 69 | x: <> @@ -174,11 +176,12 @@ warning: using `{...}` for message expression is not recommended 69 | - x: <> 69 | + x: {} | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 +warning: using `<...>` for message expression is not recommended --> testdata/parser/option/values.proto:70:8 | 70 | x: @@ -188,11 +191,12 @@ warning: using `{...}` for message expression is not recommended 70 | - x: 70 | + x: {a: 42} | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 +warning: using `<...>` for message expression is not recommended --> testdata/parser/option/values.proto:71:8 | 71 | x: > @@ -202,11 +206,12 @@ warning: using `{...}` for message expression is not recommended 71 | - x: > 71 | + x: {a: } | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 +warning: using `<...>` for message expression is not recommended --> testdata/parser/option/values.proto:71:12 | 71 | x: > @@ -216,9 +221,10 @@ warning: using `{...}` for message expression is not recommended 71 | - x: > 71 | + x: | - = note: `<...>` are only permitted for sub-messages within a message expression, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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 @@ -299,10 +305,10 @@ error: unexpected integer literal in array expression | - ^ 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 - = note: is a message expression or a array expression of the same + = 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 +warning: using `<...>` for message expression is not recommended --> testdata/parser/option/values.proto:88:19 | 88 | x [{x: 5}, 1, , 2, 3], @@ -312,8 +318,9 @@ warning: using `{...}` for message expression is not recommended 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, - = note: but as top-level option values - = help: `<...>` message expressions are an obscure feature and not recommended + = 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/package/42.proto.stderr.txt b/experimental/parser/testdata/parser/package/42.proto.stderr.txt index 597701d2..024d087e 100644 --- a/experimental/parser/testdata/parser/package/42.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/42.proto.stderr.txt @@ -3,8 +3,8 @@ error: missing path in `package` declaration | 21 | package 42; | ^^^^^^^ - = help: to place a file in the unnamed package, remove the `package` declaration - = help: however, using the unnamed package is discouraged + = 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:21:9 diff --git a/experimental/parser/testdata/parser/package/empty.proto.stderr.txt b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt index 1d6d79ae..df40ec41 100644 --- a/experimental/parser/testdata/parser/package/empty.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/empty.proto.stderr.txt @@ -1,6 +1,6 @@ warning: missing `package` declaration --> testdata/parser/package/empty.proto - = note: not explicitly specifying a package places the file - = note: in the unnamed package; using it strongly is discouraged + = 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 a42c4628..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 @@ -3,8 +3,8 @@ error: missing path in `package` declaration | 17 | package | ^^^^^^^ - = help: to place a file in the unnamed package, remove the `package` declaration - = help: however, using the unnamed package is discouraged + = 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 diff --git a/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt index cb838b47..15a1e99f 100644 --- a/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/package/no_path.proto.stderr.txt @@ -3,7 +3,7 @@ error: missing path in `package` declaration | 17 | package; | ^^^^^^^^ - = help: to place a file in the unnamed package, remove the `package` declaration - = help: however, using the unnamed package is discouraged + = 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/range/escapes.proto.stderr.txt b/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt index 0b59d74d..57f30cff 100644 --- a/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/escapes.proto.stderr.txt @@ -8,8 +8,8 @@ warning: non-canonical string literal in reserved range 20 | - reserved "foo" "bar"; 20 | + reserved "foobar"; | - = note: Protobuf implicitly concatenates adjacent string literals, - = note: like C or Python, which can lead to surprising behavior + = 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 diff --git a/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt index b6a905c2..dcc18e26 100644 --- a/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt +++ b/experimental/parser/testdata/parser/range/invalid_parent.proto.stderr.txt @@ -18,7 +18,8 @@ error: unexpected reserved range within service definition | | ^^^^^^^^^^^ 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 + = 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 @@ -40,7 +41,8 @@ error: unexpected reserved range within message extension block | | ^^^^^^^^^^^ 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 + = 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 diff --git a/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt index 1166c854..5995d368 100644 --- a/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/2024.proto.stderr.txt @@ -3,6 +3,6 @@ error: unrecognized `edition` declaration value | 15 | edition = "2024"; | ^^^^^^ - = note: permitted values: ["2023"] + = 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 index 92794749..d25bb14b 100644 --- a/experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/edition_proto2.proto.stderr.txt @@ -3,6 +3,6 @@ error: unexpected syntax in `edition` declaration | 15 | edition = "proto2"; | ^^^^^^^^ - = note: permitted values: ["2023"] + = 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 12e2fc42..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,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/eof_after_eq.proto - = note: not explicitly specifying a package places the file - = note: in the unnamed package; using it strongly is discouraged + = 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 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 4c09edd9..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,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/eof_after_kw.proto - = note: not explicitly specifying a package places the file - = note: in the unnamed package; using it strongly is discouraged + = 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 diff --git a/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt index 663015ca..e66e6c71 100644 --- a/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/invalid.proto.stderr.txt @@ -3,6 +3,6 @@ error: unrecognized `syntax` declaration value | 15 | syntax = invalid; | ^^^^^^^ - = note: permitted values: ["proto2", "proto3"] + = 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 0d784ff3..5f42e7fe 100644 --- a/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/lonely.proto.stderr.txt @@ -1,7 +1,7 @@ warning: missing `package` declaration --> testdata/parser/syntax/lonely.proto - = note: not explicitly specifying a package places the file - = note: in the unnamed package; using it strongly is discouraged + = 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 @@ -37,6 +37,7 @@ warning: the `package` declaration should be placed at the top of the file 18 | 19 | package test; | ^^^^^^^^^^^^^ - = help: a file's `package` declaration should immediately follow the `syntax` or `edition` declaration + = 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/proto2_split.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt index e0f1b98f..9e58fb7d 100644 --- a/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/proto2_split.proto.stderr.txt @@ -8,7 +8,7 @@ warning: non-canonical string literal in `syntax` declaration 15 | - syntax = "proto" "2"; 15 | + syntax = "proto2"; | - = note: Protobuf implicitly concatenates adjacent string literals, - = note: like C or Python, which can lead to surprising behavior + = 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 index 018ef0f2..ec4683ae 100644 --- a/experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/proto4.proto.stderr.txt @@ -3,6 +3,6 @@ error: unrecognized `syntax` declaration value | 15 | syntax = "proto4"; | ^^^^^^^^ - = note: permitted values: ["proto2", "proto3"] + = note: permitted values: "proto2", "proto3" encountered 1 error diff --git a/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt index 671b1a57..c6190ae9 100644 --- a/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt +++ b/experimental/parser/testdata/parser/syntax/syntax_2023.proto.stderr.txt @@ -3,6 +3,6 @@ error: unexpected edition in `syntax` declaration | 15 | syntax = "2023"; | ^^^^^^ - = note: permitted values: ["proto2", "proto3"] + = note: permitted values: "proto2", "proto3" 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 b3a538e1..2f5c7593 100644 --- a/experimental/parser/testdata/parser/type/generic.proto.stderr.txt +++ b/experimental/parser/testdata/parser/type/generic.proto.stderr.txt @@ -3,16 +3,18 @@ error: unexpected non-comparable type in map key type | 21 | map x2 = 2; | ^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 @@ -67,8 +69,9 @@ error: unexpected non-comparable type in map key type | 35 | map x12 = 12; | ^^^^^^^^^^^^^^^^ - = help: map keys may be of any of the following types: - = help: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string + = 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 From a88cee7d66cb7ccd5d82f151ae1f27b20f3ea78d Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Tue, 25 Feb 2025 13:11:45 -0800 Subject: [PATCH 56/64] cr --- experimental/ast/syntax/doc.go | 2 +- experimental/internal/taxa/noun.go | 3 +++ experimental/internal/taxa/noun.yaml | 2 ++ experimental/parser/diagnostics_internal.go | 17 ++++++++++++----- experimental/parser/legalize_decl.go | 4 +--- experimental/parser/legalize_def.go | 11 ++++++++--- experimental/parser/legalize_option.go | 1 + experimental/parser/parse_def.go | 4 +++- internal/ext/mapsx/mapsx.go | 9 ++++++++- internal/ext/stringsx/stringsx.go | 4 ++-- internal/tools/go.mod | 10 ---------- 11 files changed, 41 insertions(+), 26 deletions(-) diff --git a/experimental/ast/syntax/doc.go b/experimental/ast/syntax/doc.go index 77b030e6..e5adbf2d 100644 --- a/experimental/ast/syntax/doc.go +++ b/experimental/ast/syntax/doc.go @@ -26,7 +26,7 @@ import ( // 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++ { + for i := range totalEditions { if !yield(Syntax(i + int(Edition2023))) { break } diff --git a/experimental/internal/taxa/noun.go b/experimental/internal/taxa/noun.go index d33978e9..a83322c4 100644 --- a/experimental/internal/taxa/noun.go +++ b/experimental/internal/taxa/noun.go @@ -103,6 +103,7 @@ const ( Brackets Braces Angles + ReturnsParens KeywordSyntax KeywordEdition KeywordImport @@ -223,6 +224,7 @@ var _table_Noun_String = [...]string{ Brackets: "`[...]`", Braces: "`{...}`", Angles: "`<...>`", + ReturnsParens: "`returns (...)`", KeywordSyntax: "`syntax`", KeywordEdition: "`edition`", KeywordImport: "`import`", @@ -326,6 +328,7 @@ var _table_Noun_GoString = [...]string{ Brackets: "Brackets", Braces: "Braces", Angles: "Angles", + ReturnsParens: "ReturnsParens", KeywordSyntax: "KeywordSyntax", KeywordEdition: "KeywordEdition", KeywordImport: "KeywordImport", diff --git a/experimental/internal/taxa/noun.yaml b/experimental/internal/taxa/noun.yaml index cb189e7f..abe215aa 100644 --- a/experimental/internal/taxa/noun.yaml +++ b/experimental/internal/taxa/noun.yaml @@ -115,6 +115,8 @@ - {name: Braces, string: "`{...}`"} - {name: Angles, string: "`<...>`"} + - {name: ReturnsParens, string: "`returns (...)`"} + - {name: KeywordSyntax, string: "`syntax`"} - {name: KeywordEdition, string: "`edition`"} - {name: KeywordImport, string: "`import`"} diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index edda1645..a6ac4335 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -15,6 +15,8 @@ package parser import ( + "fmt" + "github.com/bufbuild/protocompile/experimental/ast" "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" @@ -37,7 +39,10 @@ type errUnexpected struct { // shown, but if it's not set, we call describe(what) to get a user-visible // description. want taxa.Set - got any + // If set and want is empty, the snippet will repeat the "unexpected foo" + // text under the snippet. + repeatUnexpected bool + got any } func (e errUnexpected) Diagnose(d *report.Diagnostic) { @@ -49,20 +54,22 @@ func (e errUnexpected) Diagnose(d *report.Diagnostic) { } } - var message report.DiagnosticOption + var message string if e.where.Subject() == taxa.Unknown { - message = report.Message("unexpected %v", got) + message = fmt.Sprintf("unexpected %v", got) } else { - message = report.Message("unexpected %v %v", got, e.where) + message = fmt.Sprintf("unexpected %v %v", got, e.where) } snippet := report.Snippet(e.what) if e.want.Len() > 0 { snippet = report.Snippetf(e.what, "expected %v", e.want.Join("or")) + } else if e.repeatUnexpected { + snippet = report.Snippetf(e.what, "%v", message) } d.Apply( - message, + report.Message("%v", message), snippet, report.Snippetf(e.prev, "previous %v is here", e.where.Subject()), ) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 8adad3e2..66dcaae1 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -127,9 +127,7 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } case ast.ExprKindPrefixed: expr := expr.AsPrefixed() - //nolint:gocritic // Intentional single-case switch. - switch expr.Prefix() { - case ast.ExprPrefixMinus: + if expr.Prefix() == ast.ExprPrefixMinus { lit := expr.Expr().AsLiteral() if lit.Kind() != token.Number || strings.Contains(lit.Text(), ".") { p.Error(errUnexpected{ diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index b2768f12..9ef3d3c5 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -95,7 +95,8 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { err = errUnexpected{ what: pc.Separator(), where: taxa.Ident.In(), - want: taxa.Ident.AsSet(), + + repeatUnexpected: true, } return false }) @@ -253,7 +254,7 @@ func legalizeMethod(p *parser, def ast.DeclDef) { 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), + report.Snippetf(def.Name(), "expected argument type in %s after this", taxa.Parens), ) } else { legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns) @@ -262,17 +263,21 @@ func legalizeMethod(p *parser, def ast.DeclDef) { if sig.Outputs().Span().IsZero() { def.MarkCorrupt() var after report.Spanner + var expected taxa.Noun switch { case !sig.Returns().IsZero(): after = sig.Returns() + expected = taxa.Parens case !sig.Inputs().IsZero(): after = sig.Inputs() + expected = taxa.ReturnsParens default: after = def.Name() + expected = taxa.ReturnsParens } p.Errorf("missing %v in %v", taxa.MethodOuts, taxa.Method).Apply( - report.Snippetf(after, "expected type in %s after this", taxa.Parens), + report.Snippetf(after, "expected return type in %s after this", expected), ) } else { legalizeMethodParams(p, sig.Outputs(), taxa.MethodOuts) diff --git a/experimental/parser/legalize_option.go b/experimental/parser/legalize_option.go index 252c841b..b98284c2 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -157,6 +157,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value } fallthrough default: + // TODO: generate a suggestion for this. err.Apply(report.Helpf("break this %s into one per element", taxa.Option)) } diff --git a/experimental/parser/parse_def.go b/experimental/parser/parse_def.go index 55322724..89619e5e 100644 --- a/experimental/parser/parse_def.go +++ b/experimental/parser/parse_def.go @@ -206,6 +206,9 @@ func (defOutputs) parse(p *defParser) report.Span { // Note that the inputs and outputs of a method are parsed // separately, so foo(bar) and foo returns (bar) are both possible. returns := p.c.Next() + if p.args.Returns.IsZero() { + p.args.Returns = returns + } var ty ast.TypeAny list, err := p.Punct(p.c, "(", taxa.KeywordReturns.After()) @@ -220,7 +223,6 @@ func (defOutputs) parse(p *defParser) report.Span { } if p.outputs.IsZero() && p.outputTy.IsZero() { - p.args.Returns = returns if !list.IsZero() { p.outputs = list } else { diff --git a/internal/ext/mapsx/mapsx.go b/internal/ext/mapsx/mapsx.go index 34c47475..6ef19cc0 100644 --- a/internal/ext/mapsx/mapsx.go +++ b/internal/ext/mapsx/mapsx.go @@ -30,5 +30,12 @@ func Keys[M ~map[K]V, K comparable, V any](m M) iter.Seq[K] { // 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)) + // return CollectSet(Keys(m)) + // Instead of going through an iterator, inline the loop so that + // we can preallocate and avoid rehashes. + keys := make(map[K]struct{}, len(m)) + for k := range m { + keys[k] = struct{}{} + } + return keys } diff --git a/internal/ext/stringsx/stringsx.go b/internal/ext/stringsx/stringsx.go index b3e36d54..0145bbac 100644 --- a/internal/ext/stringsx/stringsx.go +++ b/internal/ext/stringsx/stringsx.go @@ -26,7 +26,7 @@ import ( "github.com/bufbuild/protocompile/internal/iter" ) -// Rune returns the rune at the given index. +// Rune returns the rune at the given byte 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) { @@ -37,7 +37,7 @@ func Rune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { return r, r != utf8.RuneError } -// Rune returns the previous rune at the given index. +// Rune returns the previous rune at the given byte 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) { diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 7cc75a0b..9f73bca6 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -184,22 +184,12 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect -<<<<<<< HEAD - 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.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 -======= golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // 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.22.0 // indirect google.golang.org/protobuf v1.36.4 // indirect ->>>>>>> origin/main gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect From ad19a2bd7064c57e2fcbb89659ca73e96e498820 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 26 Feb 2025 14:43:50 -0800 Subject: [PATCH 57/64] oops --- .../parser/def/bad_path.proto.stderr.txt | 16 ++++++++-------- .../parser/method/incomplete.proto.stderr.txt | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt index 0d5e265d..9314357f 100644 --- a/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/bad_path.proto.stderr.txt @@ -2,42 +2,42 @@ error: unexpected `.` in identifier --> testdata/parser/def/bad_path.proto:19:12 | 19 | message foo.Bar { - | ^ expected identifier + | ^ unexpected `.` in 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 + | ^ unexpected `.` in 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 + | ^ unexpected `.` in 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 + | ^ unexpected `.` in 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 + | ^ unexpected `.` in 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 + | ^ unexpected `.` in identifier = note: the name of a enum definition must be a single identifier error: unexpected nested extension path in message extension block @@ -50,14 +50,14 @@ error: unexpected `.` in identifier --> testdata/parser/def/bad_path.proto:31:12 | 31 | service foo.Bar {} - | ^ expected identifier + | ^ unexpected `.` in 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 + | ^ unexpected `.` in identifier = note: the name of a service definition must be a single identifier encountered 9 errors diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index 03751705..f600823d 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -8,13 +8,13 @@ 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 + | ^^^^^^^^^ expected return type in `returns (...)` 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 + | ^^^^ expected argument type in `(...)` after this error: expected exactly one type in method return type, got 0 --> testdata/parser/method/incomplete.proto:23:31 @@ -35,10 +35,10 @@ error: expected exactly one type in method parameter list, got 0 | ^^ error: missing method return type in service method - --> testdata/parser/method/incomplete.proto:25:13 + --> testdata/parser/method/incomplete.proto:25:16 | 25 | rpc Bar6() returns; - | ^^ expected type in `(...)` after this + | ^^^^^^^ expected return type in `(...)` after this error: unexpected `;` after `returns` --> testdata/parser/method/incomplete.proto:25:23 From 3d69f92711ad67087c2041e210df639b4029d0b7 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Wed, 26 Feb 2025 17:16:35 -0800 Subject: [PATCH 58/64] unbreak --- experimental/parser/legalize_decl.go | 3 ++- experimental/parser/legalize_def.go | 4 ++-- experimental/parser/legalize_file.go | 2 +- experimental/parser/legalize_option.go | 7 ++++--- experimental/parser/legalize_path.go | 9 +++++---- experimental/parser/legalize_type.go | 5 +++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/experimental/parser/legalize_decl.go b/experimental/parser/legalize_decl.go index 66dcaae1..529fc5da 100644 --- a/experimental/parser/legalize_decl.go +++ b/experimental/parser/legalize_decl.go @@ -25,6 +25,7 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/ext/iterx" "github.com/bufbuild/protocompile/internal/ext/slicesx" "github.com/bufbuild/protocompile/internal/ext/stringsx" @@ -127,7 +128,7 @@ func legalizeRange(p *parser, parent classified, decl ast.DeclRange) { } case ast.ExprKindPrefixed: expr := expr.AsPrefixed() - if expr.Prefix() == ast.ExprPrefixMinus { + if expr.Prefix() == keyword.Minus { lit := expr.Expr().AsLiteral() if lit.Kind() != token.Number || strings.Contains(lit.Text(), ".") { p.Error(errUnexpected{ diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 9ef3d3c5..3dbc1878 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -67,7 +67,7 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { switch { case def.Name().IsZero(): def.MarkCorrupt() - kw := taxa.Keyword(def.Keyword().Text()) + kw := taxa.Keyword(def.Keyword()) p.Errorf("missing name %v", kw.After()).Apply( report.Snippet(def), ) @@ -77,7 +77,7 @@ func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) { case def.Name().AsIdent().IsZero(): def.MarkCorrupt() - kw := taxa.Keyword(def.Keyword().Text()) + kw := taxa.Keyword(def.Keyword()) err := errUnexpected{ what: def.Name(), diff --git a/experimental/parser/legalize_file.go b/experimental/parser/legalize_file.go index bc9934d7..2b5a09c8 100644 --- a/experimental/parser/legalize_file.go +++ b/experimental/parser/legalize_file.go @@ -339,7 +339,7 @@ func legalizeImport(p *parser, parent classified, decl ast.DeclImport, imports m if in == taxa.WeakImport { p.Warnf("use of `import weak`").Apply( - report.Snippet(report.Join(decl.Keyword(), decl.Modifier())), + report.Snippet(report.Join(decl.KeywordToken(), decl.ModifierToken())), 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 index b98284c2..a8a23c30 100644 --- a/experimental/parser/legalize_option.go +++ b/experimental/parser/legalize_option.go @@ -23,6 +23,7 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/ext/iterx" "github.com/bufbuild/protocompile/internal/ext/slicesx" ) @@ -106,7 +107,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value //nolint:gocritic // Intentional single-case switch. switch value.Prefix() { - case ast.ExprPrefixMinus: + case keyword.Minus: 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 @@ -190,7 +191,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value // Legalize against <...> in all cases, but only emit a warning when they // are not strictly illegal. - if dict.Braces().Text() == "<" { + if dict.Braces().Keyword() == keyword.Angles { var err *report.Diagnostic if parent.IsZero() { err = p.Errorf("cannot use %s for %s here", taxa.Angles, taxa.Dict) @@ -269,7 +270,7 @@ func legalizeOptionValue(p *parser, decl report.Span, parent ast.ExprAny, value } _, isURL := iterx.Find(path.Components, func(pc ast.PathComponent) bool { - return pc.Separator().Text() == "/" + return pc.Separator().Keyword() == keyword.Slash }) if isURL { legalizePath(p, taxa.TypeURL.In(), path, pathOptions{AllowSlash: true}) diff --git a/experimental/parser/legalize_path.go b/experimental/parser/legalize_path.go index 4f74eb02..3ddb1d05 100644 --- a/experimental/parser/legalize_path.go +++ b/experimental/parser/legalize_path.go @@ -19,6 +19,7 @@ import ( "github.com/bufbuild/protocompile/experimental/internal/taxa" "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/ext/iterx" ) @@ -61,15 +62,15 @@ func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) return true } - if pc.Separator().Text() == "/" { + if pc.Separator().Keyword() == keyword.Slash { if !opts.AllowSlash { - p.Errorf("unexpected `/` in path %s", where).Apply( - report.Snippetf(pc.Separator(), "help: replace this with a `.`"), + p.Errorf("unexpected %s in path %s", taxa.Slash, where).Apply( + report.Snippetf(pc.Separator(), "help: replace this with a %s", taxa.Dot), ) ok = false return true } else if !slash.IsZero() { - p.Errorf("type URL can only contain a single `/`").Apply( + p.Errorf("type URL can only contain a single %s", taxa.Slash).Apply( report.Snippet(pc.Separator()), report.Snippetf(slash, "first one is here"), ) diff --git a/experimental/parser/legalize_type.go b/experimental/parser/legalize_type.go index bcc61a61..212e3483 100644 --- a/experimental/parser/legalize_type.go +++ b/experimental/parser/legalize_type.go @@ -19,6 +19,7 @@ import ( "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/token/keyword" "github.com/bufbuild/protocompile/internal/ext/iterx" ) @@ -37,7 +38,7 @@ func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) { legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true}) case ast.TypeKindPrefixed: prefixed := ty.AsPrefixed() - if prefixed.Prefix() != ast.TypePrefixStream { + if prefixed.Prefix() != keyword.Stream { p.Errorf("only the %s modifier may appear in %s", taxa.KeywordStream, what).Apply( report.Snippet(prefixed.PrefixToken()), ) @@ -65,7 +66,7 @@ func legalizeFieldType(p *parser, ty ast.TypeAny) { case ast.TypeKindPrefixed: ty := ty.AsPrefixed() - if ty.Prefix() == ast.TypePrefixStream { + if ty.Prefix() == keyword.Stream { p.Errorf("the %s modifier may only appear in a %s", taxa.KeywordStream, taxa.Signature).Apply( report.Snippet(ty.PrefixToken()), ) From 294ee4da74a05fd7ab39ef52d72ce8cdf5fc901f Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 27 Feb 2025 12:12:32 -0800 Subject: [PATCH 59/64] unbreak2 --- .../parser/testdata/parser/method/incomplete.proto.stderr.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index f600823d..a74d3ce0 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -44,7 +44,7 @@ error: unexpected `;` after `returns` --> testdata/parser/method/incomplete.proto:25:23 | 25 | rpc Bar6() returns; - | ^ expected `(` + | ^ expected `(...)` error: expected exactly one type in method parameter list, got 0 --> testdata/parser/method/incomplete.proto:26:13 From 3447b40b129d7a37ea6b0ea25c544dde9a64fe05 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 27 Feb 2025 14:23:38 -0800 Subject: [PATCH 60/64] fix diagnostics broken by merge --- experimental/parser/diagnostics_internal.go | 18 ++++++++++++------ experimental/parser/parse_def.go | 2 +- .../parser/def/bare_bodies.proto.stderr.txt | 12 ++++++------ .../parser/option/values.proto.stderr.txt | 2 +- experimental/report/diff.go | 6 +++++- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index 1470765f..edd17845 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -364,10 +364,13 @@ func doJustifyLeft(stream *token.Stream, span report.Span, e *report.Edit) { return } - // Seek to the previous unskippable token, and use its end as - // the start of the justification. - e.Start = token.NewCursorAt(start).Prev().Span().End - span.Start + if start.Kind().IsSkippable() { + // Seek to the previous unskippable token, and use its end as + // the start of the justification. + start = token.NewCursorAt(start).Prev() + } + e.Start = start.Span().End - span.Start if !wasDelete { e.End = e.Start } @@ -384,10 +387,13 @@ func doJustifyRight(stream *token.Stream, span report.Span, e *report.Edit) { return } - // Seek to the next unskippable token, and use its start as - // the start of the justification. - e.End = token.NewCursorAt(end).Next().Span().Start - span.Start + if end.Kind().IsSkippable() { + // Seek to the next unskippable token, and use its start as + // the start of the justification. + end = token.NewCursorAt(end).Next() + } + e.End = end.Span().Start - span.Start if !wasDelete { e.Start = e.End } diff --git a/experimental/parser/parse_def.go b/experimental/parser/parse_def.go index 928125a7..b1c5eae7 100644 --- a/experimental/parser/parse_def.go +++ b/experimental/parser/parse_def.go @@ -286,7 +286,7 @@ func (defValue) canStart(p *defParser) bool { // If the next "expression" looks like a path, this likelier to be // due to a missing semicolon than a missing =. return false - case slicesx.Among(next.Keyword(), keyword.Brackets, keyword.Braces): + case slicesx.Among(next.Keyword(), keyword.Brackets, keyword.Braces, keyword.Angles): // Exclude the two followers after this one. return false case canStartExpr(next): diff --git a/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt b/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt index f2055bb9..a5a0529b 100644 --- a/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt +++ b/experimental/parser/testdata/parser/def/bare_bodies.proto.stderr.txt @@ -8,8 +8,8 @@ error: unexpected definition body in message definition help: remove these braces | 21 | - { -23 | int32 y = 2; -25 | - } +22 | int32 y = 2; +23 | - } | error: unexpected definition body in file scope @@ -22,10 +22,10 @@ error: unexpected definition body in file scope help: remove these braces | 26 | - { -28 | message N { -29 | int32 y = 2; -30 | } -32 | - } +27 | message N { +28 | int32 y = 2; +29 | } +30 | - } | encountered 2 errors diff --git a/experimental/parser/testdata/parser/option/values.proto.stderr.txt b/experimental/parser/testdata/parser/option/values.proto.stderr.txt index 4b0828a1..dc3a4931 100644 --- a/experimental/parser/testdata/parser/option/values.proto.stderr.txt +++ b/experimental/parser/testdata/parser/option/values.proto.stderr.txt @@ -82,7 +82,7 @@ error: unexpected `:` after definition help: consider inserting a `;` | 38 | option x = <>;: - | + + | + error: unexpected `:` in file scope --> testdata/parser/option/values.proto:38:14 diff --git a/experimental/report/diff.go b/experimental/report/diff.go index fbaca36f..98ea263c 100644 --- a/experimental/report/diff.go +++ b/experimental/report/diff.go @@ -121,9 +121,13 @@ func unifiedDiff(span Span, edits []Edit) (Span, []hunk) { } buf.WriteString(original[prev:]) + unchanged := src[prevHunk:start] + unchanged = strings.TrimPrefix(unchanged, "\n") + unchanged = strings.TrimSuffix(unchanged, "\n") + // Dump the result into the output. out = append(out, - hunk{hunkUnchanged, src[prevHunk:start]}, + hunk{hunkUnchanged, unchanged}, hunk{hunkDelete, src[start:end]}, hunk{hunkAdd, buf.String()}, ) From 1209df46e1b0f6438644aa62a87642374dd367e8 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Thu, 27 Feb 2025 14:24:14 -0800 Subject: [PATCH 61/64] lint --- experimental/parser/diagnostics_internal.go | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/parser/diagnostics_internal.go b/experimental/parser/diagnostics_internal.go index edd17845..a243e846 100644 --- a/experimental/parser/diagnostics_internal.go +++ b/experimental/parser/diagnostics_internal.go @@ -16,7 +16,6 @@ package parser import ( "fmt" - "unicode" "unicode/utf8" From 317be102e6531b8108f28eb1633976c59de54998 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 28 Feb 2025 10:59:08 -0800 Subject: [PATCH 62/64] regen tests --- .../report/testdata/suggestions.yaml.color.txt | 12 +++++------- .../report/testdata/suggestions.yaml.fancy.txt | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/experimental/report/testdata/suggestions.yaml.color.txt b/experimental/report/testdata/suggestions.yaml.color.txt index 1152f651..bd92e867 100644 --- a/experimental/report/testdata/suggestions.yaml.color.txt +++ b/experimental/report/testdata/suggestions.yaml.color.txt @@ -51,13 +51,11 @@ ⟨blu⟩ help: | ⟨blu⟩ 5 | ⟨b.red⟩-⟨red⟩ service Foo { -⟨blu⟩ 6 | ⟨reset⟩ ⟨reset⟩ -⟨blu⟩ 7 | ⟨reset⟩ ⟨reset⟩ rpc Get(GetRequest) returns GetResponse -⟨blu⟩ 8 | ⟨reset⟩ ⟨reset⟩ rpc Put(PutRequest) returns (PutResponse) [foo = bar]; -⟨blu⟩ 9 | ⟨reset⟩ ⟨reset⟩ -⟨blu⟩10 | ⟨b.red⟩-⟨red⟩ } -⟨blu⟩11 | ⟨b.red⟩-⟨red⟩ -⟨blu⟩ 9 | ⟨b.grn⟩+⟨grn⟩ } +⟨blu⟩ 6 | ⟨reset⟩ ⟨reset⟩ rpc Get(GetRequest) returns GetResponse +⟨blu⟩ 7 | ⟨reset⟩ ⟨reset⟩ rpc Put(PutRequest) returns (PutResponse) [foo = bar]; +⟨blu⟩ 8 | ⟨b.red⟩-⟨red⟩ } +⟨blu⟩ 9 | ⟨b.red⟩-⟨red⟩ +⟨blu⟩ 7 | ⟨b.grn⟩+⟨grn⟩ } ⟨blu⟩ | ⟨reset⟩ ⟨b.red⟩error: delete this method diff --git a/experimental/report/testdata/suggestions.yaml.fancy.txt b/experimental/report/testdata/suggestions.yaml.fancy.txt index 87159097..69301406 100644 --- a/experimental/report/testdata/suggestions.yaml.fancy.txt +++ b/experimental/report/testdata/suggestions.yaml.fancy.txt @@ -51,13 +51,11 @@ error: delete some stuff help: | 5 | - service Foo { - 6 | - 7 | rpc Get(GetRequest) returns GetResponse - 8 | rpc Put(PutRequest) returns (PutResponse) [foo = bar]; - 9 | -10 | - } -11 | - - 9 | + } + 6 | rpc Get(GetRequest) returns GetResponse + 7 | rpc Put(PutRequest) returns (PutResponse) [foo = bar]; + 8 | - } + 9 | - + 7 | + } | error: delete this method From 0adf7e589e5dace9de55e3ac1bd5fe991c292904 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 28 Feb 2025 11:40:55 -0800 Subject: [PATCH 63/64] cr --- experimental/parser/legalize_def.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/parser/legalize_def.go b/experimental/parser/legalize_def.go index 3dbc1878..19c37c14 100644 --- a/experimental/parser/legalize_def.go +++ b/experimental/parser/legalize_def.go @@ -254,7 +254,7 @@ func legalizeMethod(p *parser, def ast.DeclDef) { if sig.Inputs().Span().IsZero() { def.MarkCorrupt() p.Errorf("missing %v in %v", taxa.MethodIns, taxa.Method).Apply( - report.Snippetf(def.Name(), "expected argument type in %s after this", taxa.Parens), + report.Snippetf(def.Name(), "expected %s after this", taxa.Parens), ) } else { legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns) @@ -277,7 +277,7 @@ func legalizeMethod(p *parser, def ast.DeclDef) { } p.Errorf("missing %v in %v", taxa.MethodOuts, taxa.Method).Apply( - report.Snippetf(after, "expected return type in %s after this", expected), + report.Snippetf(after, "expected %s after this", expected), ) } else { legalizeMethodParams(p, sig.Outputs(), taxa.MethodOuts) From bb769e0b5ca5ea7bfa035ed0af64ffdf62398eca Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Fri, 28 Feb 2025 11:43:54 -0800 Subject: [PATCH 64/64] facepalm --- .../testdata/parser/method/incomplete.proto.stderr.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt index 1d63cb4f..d56d855f 100644 --- a/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt +++ b/experimental/parser/testdata/parser/method/incomplete.proto.stderr.txt @@ -12,13 +12,13 @@ error: missing method return type in service method --> testdata/parser/method/incomplete.proto:21:13 | 21 | rpc Bar2(foo.Bar); - | ^^^^^^^^^ expected return type in `returns (...)` after this + | ^^^^^^^^^ expected `returns (...)` after this error: missing method parameter list in service method --> testdata/parser/method/incomplete.proto:22:9 | 22 | rpc Bar3 returns (foo.Bar); - | ^^^^ expected argument type in `(...)` after this + | ^^^^ expected `(...)` after this error: expected exactly one type in method return type, got 0 --> testdata/parser/method/incomplete.proto:23:31 @@ -42,7 +42,7 @@ error: missing method return type in service method --> testdata/parser/method/incomplete.proto:25:16 | 25 | rpc Bar6() returns; - | ^^^^^^^ expected return type in `(...)` after this + | ^^^^^^^ expected `(...)` after this error: unexpected `;` after `returns` --> testdata/parser/method/incomplete.proto:25:23