Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .protoc_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
32.0
33.0
41 changes: 41 additions & 0 deletions experimental/ast/syntax/name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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

import (
"fmt"
"strconv"
)

var names = func() map[Syntax]string {
names := make(map[Syntax]string)
for syntax := range All() {
if syntax.IsEdition() {
names[syntax] = fmt.Sprintf("Edition %s", syntax)
} else {
names[syntax] = strconv.Quote(syntax.String())
}
}
return names
}()

// Name returns the name of this syntax as it should appear in diagnostics.
func (s Syntax) Name() string {
name, ok := names[s]
if !ok {
return "Edition <?>"
}
return name
}
112 changes: 112 additions & 0 deletions experimental/internal/erredition/erredition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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 erredition defines common diagnostics for issuing errors about
// the wrong edition being used.
package erredition

import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/report"
)

// TooNew diagnoses an edition that is too old for the feature used.
type TooOld struct {
What any
Where report.Spanner
Decl ast.DeclSyntax
Current syntax.Syntax
Intro syntax.Syntax
}

// Diagnose implements [report.Diagnoser].
func (e TooOld) Diagnose(d *report.Diagnostic) {
kind := "syntax"
if e.Current.IsEdition() {
kind = "edition"
}

d.Apply(
report.Message("`%s` is not supported in %s", e.What, e.Current.Name()),
report.Snippet(e.Where),
report.Snippetf(e.Decl.Value(), "%s specified here", kind),
)

if e.Intro != syntax.Unknown {
d.Apply(report.Helpf("`%s` requires at least %s", e.What, e.Intro.Name()))
}
}

// TooNew diagnoses an edition that is too new for the feature used.
type TooNew struct {
Current syntax.Syntax
Decl ast.DeclSyntax

Deprecated, Removed syntax.Syntax
DeprecatedReason, RemovedReason string

What any
Where report.Spanner
}

// Diagnose implements [report.Diagnoser].
func (e TooNew) Diagnose(d *report.Diagnostic) {
kind := "syntax"
if e.Current.IsEdition() {
kind = "edition"
}

err := "not supported"
if !e.isRemoved() {
err = "deprecated"
}

d.Apply(
report.Message("`%s` is %s in %s", e.What, err, e.Current.Name()),
report.Snippet(e.Where),
report.Snippetf(e.Decl.Value(), "%s specified here", kind),
)

if e.isRemoved() {
if e.isDeprecated() {
d.Apply(report.Helpf("deprecated since %s, removed in %s", e.Deprecated.Name(), e.Removed.Name()))
} else {
d.Apply(report.Helpf("removed in %s", e.Removed.Name()))
}

if e.RemovedReason != "" {
d.Apply(report.Helpf("%s", normalizeReason(e.RemovedReason)))
return
}
} else if e.isDeprecated() {
if e.Removed != syntax.Unknown {
d.Apply(report.Helpf("deprecated since %s, to be removed in %s", e.Deprecated.Name(), e.Removed.Name()))
} else {
d.Apply(report.Helpf("deprecated since %s", e.Deprecated.Name()))
}
}

if e.DeprecatedReason != "" {
d.Apply(report.Helpf("%s", normalizeReason(e.DeprecatedReason)))
}
}

func (e TooNew) isDeprecated() bool {
return e.Deprecated != syntax.Unknown && e.Deprecated <= e.Current
}

func (e TooNew) isRemoved() bool {
return e.Removed != syntax.Unknown && e.Removed <= e.Current
}
57 changes: 57 additions & 0 deletions experimental/internal/erredition/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 erredition

import (
"regexp"
"strings"
"unicode"
"unicode/utf8"
)

var (
whitespacePattern = regexp.MustCompile(`[ \t\r\n]+`)
protoDevPattern = regexp.MustCompile(` See http:\/\/protobuf\.dev\/[^ ]+ for more information\.?`)
periodPattern = regexp.MustCompile(`\.( [A-Z]|$)`)
editionPattern = regexp.MustCompile(`edition [0-9]+`)
)

// normalizeReason canonicalizes the appearance of deprecation reasons.
// Some built-in deprecation warnings have double spaces after periods.
func normalizeReason(text string) string {
// First, normalize all whitespace.
text = whitespacePattern.ReplaceAllString(text, " ")

// Delete protobuf.dev links; these should ideally use our specialized link
// formatting instead.
text = protoDevPattern.ReplaceAllString(text, "")

// Replace all sentence-ending periods with semicolons.
text = periodPattern.ReplaceAllStringFunc(text, func(match string) string {
if match == "." {
return ""
}
return ";" + strings.ToLower(match[1:])
})

// Capitalize "Edition" when followed by an edition number.
text = editionPattern.ReplaceAllStringFunc(text, func(s string) string {
return "E" + s[1:]
})

// Finally, de-capitalize the first letter.
r, n := utf8.DecodeRuneInString(text)
return string(unicode.ToLower(r)) + text[n:]
}
30 changes: 30 additions & 0 deletions experimental/internal/erredition/normalize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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 erredition

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNormalize(t *testing.T) {
t.Parallel()

assert.Equal(t,
"the legacy closed enum behavior in Java is deprecated and is scheduled to be removed in Edition 2025",
normalizeReason("The legacy closed enum behavior in Java is deprecated and is scheduled to be removed in edition 2025. See http://protobuf.dev/programming-guides/enum/#java for more information."),
)
}
49 changes: 32 additions & 17 deletions experimental/ir/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package ir
import (
"fmt"
"reflect"
"strings"

"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/internal/arena"
Expand Down Expand Up @@ -77,14 +78,15 @@ type builtins struct {
EditionSupportWarning Member
EditionSupportRemoved Member

FeatureSet Type
FeaturePresence Member
FeatureEnumType Member
FeaturePacked Member
FeatureUTF8 Member
FeatureGroup Member
FeatureEnum Member
FeatureJSON Member
FeatureSet Type
FeaturePresence Member
FeatureEnumType Member
FeaturePacked Member
FeatureUTF8 Member
FeatureGroup Member
FeatureEnum Member
FeatureJSON Member
FeatureVisibility Member `builtin:"optional"`

FileFeatures Member
MessageFeatures Member
Expand Down Expand Up @@ -162,14 +164,15 @@ type builtinIDs struct {
EditionSupportWarning intern.ID `intern:"google.protobuf.FieldOptions.FeatureSupport.deprecation_warning"`
EditionSupportRemoved intern.ID `intern:"google.protobuf.FieldOptions.FeatureSupport.edition_removed"`

FeatureSet intern.ID `intern:"google.protobuf.FeatureSet"`
FeaturePresence intern.ID `intern:"google.protobuf.FeatureSet.field_presence"`
FeatureEnumType intern.ID `intern:"google.protobuf.FeatureSet.enum_type"`
FeaturePacked intern.ID `intern:"google.protobuf.FeatureSet.repeated_field_encoding"`
FeatureUTF8 intern.ID `intern:"google.protobuf.FeatureSet.utf8_validation"`
FeatureGroup intern.ID `intern:"google.protobuf.FeatureSet.message_encoding"`
FeatureEnum intern.ID `intern:"google.protobuf.FeatureSet.enum_type"`
FeatureJSON intern.ID `intern:"google.protobuf.FeatureSet.json_format"`
FeatureSet intern.ID `intern:"google.protobuf.FeatureSet"`
FeaturePresence intern.ID `intern:"google.protobuf.FeatureSet.field_presence"`
FeatureEnumType intern.ID `intern:"google.protobuf.FeatureSet.enum_type"`
FeaturePacked intern.ID `intern:"google.protobuf.FeatureSet.repeated_field_encoding"`
FeatureUTF8 intern.ID `intern:"google.protobuf.FeatureSet.utf8_validation"`
FeatureGroup intern.ID `intern:"google.protobuf.FeatureSet.message_encoding"`
FeatureEnum intern.ID `intern:"google.protobuf.FeatureSet.enum_type"`
FeatureJSON intern.ID `intern:"google.protobuf.FeatureSet.json_format"`
FeatureVisibility intern.ID `intern:"google.protobuf.FeatureSet.default_symbol_visibility"`

FileFeatures intern.ID `intern:"google.protobuf.FileOptions.features"`
MessageFeatures intern.ID `intern:"google.protobuf.MessageOptions.features"`
Expand Down Expand Up @@ -207,11 +210,23 @@ func resolveBuiltins(file *File) {
ids := reflect.ValueOf(file.session.builtins)
for i := range v.NumField() {
field := v.Field(i)
id := ids.FieldByName(v.Type().Field(i).Name).Interface().(intern.ID) //nolint:errcheck
tyField := v.Type().Field(i)
id := ids.FieldByName(tyField.Name).Interface().(intern.ID) //nolint:errcheck
kind := kinds[field.Type()]

var optional bool
for option := range strings.SplitSeq(tyField.Tag.Get("builtin"), ",") {
if option == "optional" {
optional = true
}
}

ref := file.exported.lookup(file, id)
sym := GetRef(file, ref)
if sym.IsZero() && optional {
continue
}

if sym.Kind() != kind.kind {
panic(fmt.Errorf(
"missing descriptor.proto symbol: %s `%s`; got kind %s",
Expand Down
34 changes: 28 additions & 6 deletions experimental/ir/fdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,16 @@ func (dg *descGenerator) file(file *File, fdp *descriptorpb.FileDescriptorProto)
return imp.Decl.KeywordToken().Span().Start
}))
for i, imp := range imports {
fdp.Dependency = append(fdp.Dependency, imp.Path())
if imp.Public {
fdp.PublicDependency = append(fdp.PublicDependency, int32(i))
}
if imp.Weak {
fdp.WeakDependency = append(fdp.WeakDependency, int32(i))
if !imp.Option {
fdp.Dependency = append(fdp.Dependency, imp.Path())
if imp.Public {
fdp.PublicDependency = append(fdp.PublicDependency, int32(i))
}
if imp.Weak {
fdp.WeakDependency = append(fdp.WeakDependency, int32(i))
}
} else if imp.Option {
fdp.OptionDependency = append(fdp.OptionDependency, imp.Path())
}

if dg.sourceCodeInfoExtn != nil && !imp.Used {
Expand Down Expand Up @@ -274,6 +278,15 @@ func (dg *descGenerator) message(ty Type, mdp *descriptorpb.DescriptorProto) {
mdp.Options = new(descriptorpb.MessageOptions)
dg.options(options, mdp.Options)
}

switch exported, explicit := ty.IsExported(); {
case !explicit:
break
case exported:
mdp.Visibility = descriptorpb.SymbolVisibility_VISIBILITY_EXPORT.Enum()
case !exported:
mdp.Visibility = descriptorpb.SymbolVisibility_VISIBILITY_LOCAL.Enum()
}
}

var predeclaredToFDPType = []descriptorpb.FieldDescriptorProto_Type{
Expand Down Expand Up @@ -402,6 +415,15 @@ func (dg *descGenerator) enum(ty Type, edp *descriptorpb.EnumDescriptorProto) {
edp.Options = new(descriptorpb.EnumOptions)
dg.options(options, edp.Options)
}

switch exported, explicit := ty.IsExported(); {
case !explicit:
break
case exported:
edp.Visibility = descriptorpb.SymbolVisibility_VISIBILITY_EXPORT.Enum()
case !exported:
edp.Visibility = descriptorpb.SymbolVisibility_VISIBILITY_LOCAL.Enum()
}
}

func (dg *descGenerator) enumValue(f Member, evdp *descriptorpb.EnumValueDescriptorProto) {
Expand Down
Loading
Loading