// 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 ast
import "fmt"
// EnumNode represents an enum declaration. Example:
//
// enum Foo { BAR = 0; BAZ = 1 }
//
// In edition 2024, enums can have export/local modifiers:
//
// local enum LocalEnum { ... }
// export enum ExportedEnum { ... }
type EnumNode struct {
compositeNode
// Optional; if present indicates visibility modifier (export/local) for edition 2024
Visibility *KeywordNode
Keyword *KeywordNode
Name *IdentNode
OpenBrace *RuneNode
Decls []EnumElement
CloseBrace *RuneNode
}
func (*EnumNode) fileElement() {}
func (*EnumNode) msgElement() {}
// NewEnumNode creates a new *EnumNode. All arguments must be non-nil. While
// it is technically allowed for decls to be nil or empty, the resulting node
// will not be a valid enum, which must have at least one value.
// - keyword: The token corresponding to the "enum" keyword.
// - name: The token corresponding to the enum's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the enum body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewEnumNode(keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []EnumElement, closeBrace *RuneNode) *EnumNode {
return NewEnumNodeWithVisibility(nil, keyword, name, openBrace, decls, closeBrace)
}
// NewEnumNodeWithVisibility creates a new *EnumNode with optional visibility modifier.
// While it is technically allowed for decls to be nil or empty, the resulting node
// will not be a valid enum, which must have at least one value.
// - visibility: Optional visibility modifier token ("export" or "local") for edition 2024.
// - keyword: The token corresponding to the "enum" keyword.
// - name: The token corresponding to the enum's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the enum body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewEnumNodeWithVisibility(visibility *KeywordNode, keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []EnumElement, closeBrace *RuneNode) *EnumNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
numChildren := 4 + len(decls) // keyword, name, openBrace, closeBrace + decls
if visibility != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
if visibility != nil {
children = append(children, visibility)
}
children = append(children, keyword, name, openBrace)
for _, decl := range decls {
switch decl.(type) {
case *OptionNode, *EnumValueNode, *ReservedNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid EnumElement type: %T", decl))
}
children = append(children, decl)
}
children = append(children, closeBrace)
return &EnumNode{
compositeNode: compositeNode{
children: children,
},
Visibility: visibility,
Keyword: keyword,
Name: name,
OpenBrace: openBrace,
CloseBrace: closeBrace,
Decls: decls,
}
}
func (n *EnumNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// EnumElement is an interface implemented by all AST nodes that can
// appear in the body of an enum declaration.
type EnumElement interface {
Node
enumElement()
}
var _ EnumElement = (*OptionNode)(nil)
var _ EnumElement = (*EnumValueNode)(nil)
var _ EnumElement = (*ReservedNode)(nil)
var _ EnumElement = (*EmptyDeclNode)(nil)
// EnumValueDeclNode is a placeholder interface for AST nodes that represent
// enum values. This allows NoSourceNode to be used in place of *EnumValueNode
// for some usages.
type EnumValueDeclNode interface {
NodeWithOptions
GetName() Node
GetNumber() Node
}
var _ EnumValueDeclNode = (*EnumValueNode)(nil)
var _ EnumValueDeclNode = (*NoSourceNode)(nil)
// EnumValueNode represents an enum declaration. Example:
//
// UNSET = 0 [deprecated = true];
type EnumValueNode struct {
compositeNode
Name *IdentNode
Equals *RuneNode
Number IntValueNode
Options *CompactOptionsNode
Semicolon *RuneNode
}
func (*EnumValueNode) enumElement() {}
// NewEnumValueNode creates a new *EnumValueNode. All arguments must be non-nil
// except opts which is only non-nil if the declaration included options.
// - name: The token corresponding to the enum value's name.
// - equals: The token corresponding to the '=' rune after the name.
// - number: The token corresponding to the enum value's number.
// - opts: Optional set of enum value options.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewEnumValueNode(name *IdentNode, equals *RuneNode, number IntValueNode, opts *CompactOptionsNode, semicolon *RuneNode) *EnumValueNode {
if name == nil {
panic("name is nil")
}
if equals == nil {
panic("equals is nil")
}
if number == nil {
panic("number is nil")
}
numChildren := 3
if semicolon != nil {
numChildren++
}
if opts != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, name, equals, number)
if opts != nil {
children = append(children, opts)
}
if semicolon != nil {
children = append(children, semicolon)
}
return &EnumValueNode{
compositeNode: compositeNode{
children: children,
},
Name: name,
Equals: equals,
Number: number,
Options: opts,
Semicolon: semicolon,
}
}
func (e *EnumValueNode) GetName() Node {
return e.Name
}
func (e *EnumValueNode) GetNumber() Node {
return e.Number
}
func (e *EnumValueNode) RangeOptions(fn func(*OptionNode) bool) {
for _, opt := range e.Options.Options {
if !fn(opt) {
return
}
}
}
// 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 ast
import "fmt"
// FieldDeclNode is a node in the AST that defines a field. This includes
// normal message fields as well as extensions. There are multiple types
// of AST nodes that declare fields:
// - *FieldNode
// - *GroupNode
// - *MapFieldNode
// - *SyntheticMapField
//
// This also allows NoSourceNode and SyntheticMapField to be used in place of
// one of the above for some usages.
type FieldDeclNode interface {
NodeWithOptions
FieldLabel() Node
FieldName() Node
FieldType() Node
FieldTag() Node
FieldExtendee() Node
GetGroupKeyword() Node
GetOptions() *CompactOptionsNode
}
var _ FieldDeclNode = (*FieldNode)(nil)
var _ FieldDeclNode = (*GroupNode)(nil)
var _ FieldDeclNode = (*MapFieldNode)(nil)
var _ FieldDeclNode = (*SyntheticMapField)(nil)
var _ FieldDeclNode = (*NoSourceNode)(nil)
// FieldNode represents a normal field declaration (not groups or maps). It
// can represent extension fields as well as non-extension fields (both inside
// of messages and inside of one-ofs). Example:
//
// optional string foo = 1;
type FieldNode struct {
compositeNode
Label FieldLabel
FldType IdentValueNode
Name *IdentNode
Equals *RuneNode
Tag *UintLiteralNode
Options *CompactOptionsNode
Semicolon *RuneNode
// This is an up-link to the containing *ExtendNode for fields
// that are defined inside of "extend" blocks.
Extendee *ExtendNode
}
func (*FieldNode) msgElement() {}
func (*FieldNode) oneofElement() {}
func (*FieldNode) extendElement() {}
// NewFieldNode creates a new *FieldNode. The label and options arguments may be
// nil but the others must be non-nil.
// - label: The token corresponding to the label keyword if present ("optional",
// "required", or "repeated").
// - fieldType: The token corresponding to the field's type.
// - name: The token corresponding to the field's name.
// - equals: The token corresponding to the '=' rune after the name.
// - tag: The token corresponding to the field's tag number.
// - opts: Optional set of field options.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewFieldNode(label *KeywordNode, fieldType IdentValueNode, name *IdentNode, equals *RuneNode, tag *UintLiteralNode, opts *CompactOptionsNode, semicolon *RuneNode) *FieldNode {
if fieldType == nil {
panic("fieldType is nil")
}
if name == nil {
panic("name is nil")
}
numChildren := 2
if equals != nil {
numChildren++
}
if tag != nil {
numChildren++
}
if semicolon != nil {
numChildren++
}
if label != nil {
numChildren++
}
if opts != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
if label != nil {
children = append(children, label)
}
children = append(children, fieldType, name)
if equals != nil {
children = append(children, equals)
}
if tag != nil {
children = append(children, tag)
}
if opts != nil {
children = append(children, opts)
}
if semicolon != nil {
children = append(children, semicolon)
}
return &FieldNode{
compositeNode: compositeNode{
children: children,
},
Label: newFieldLabel(label),
FldType: fieldType,
Name: name,
Equals: equals,
Tag: tag,
Options: opts,
Semicolon: semicolon,
}
}
func (n *FieldNode) FieldLabel() Node {
// proto3 fields and fields inside one-ofs will not have a label and we need
// this check in order to return a nil node -- otherwise we'd return a
// non-nil node that has a nil pointer value in it :/
if n.Label.KeywordNode == nil {
return nil
}
return n.Label.KeywordNode
}
func (n *FieldNode) FieldName() Node {
return n.Name
}
func (n *FieldNode) FieldType() Node {
return n.FldType
}
func (n *FieldNode) FieldTag() Node {
if n.Tag == nil {
return n
}
return n.Tag
}
func (n *FieldNode) FieldExtendee() Node {
if n.Extendee != nil {
return n.Extendee.Extendee
}
return nil
}
func (n *FieldNode) GetGroupKeyword() Node {
return nil
}
func (n *FieldNode) GetOptions() *CompactOptionsNode {
return n.Options
}
func (n *FieldNode) RangeOptions(fn func(*OptionNode) bool) {
for _, opt := range n.Options.Options {
if !fn(opt) {
return
}
}
}
// FieldLabel represents the label of a field, which indicates its cardinality
// (i.e. whether it is optional, required, or repeated).
type FieldLabel struct {
*KeywordNode
Repeated bool
Required bool
}
func newFieldLabel(lbl *KeywordNode) FieldLabel {
repeated, required := false, false
if lbl != nil {
repeated = lbl.Val == "repeated"
required = lbl.Val == "required"
}
return FieldLabel{
KeywordNode: lbl,
Repeated: repeated,
Required: required,
}
}
// IsPresent returns true if a label keyword was present in the declaration
// and false if it was absent.
func (f *FieldLabel) IsPresent() bool {
return f.KeywordNode != nil
}
// GroupNode represents a group declaration, which doubles as a field and inline
// message declaration. It can represent extension fields as well as
// non-extension fields (both inside of messages and inside of one-ofs).
// Example:
//
// optional group Key = 4 {
// optional uint64 id = 1;
// optional string name = 2;
// }
type GroupNode struct {
compositeNode
Label FieldLabel
Keyword *KeywordNode
Name *IdentNode
Equals *RuneNode
Tag *UintLiteralNode
Options *CompactOptionsNode
MessageBody
// This is an up-link to the containing *ExtendNode for groups
// that are defined inside of "extend" blocks.
Extendee *ExtendNode
}
func (*GroupNode) msgElement() {}
func (*GroupNode) oneofElement() {}
func (*GroupNode) extendElement() {}
// NewGroupNode creates a new *GroupNode. The label and options arguments may be
// nil but the others must be non-nil.
// - label: The token corresponding to the label keyword if present ("optional",
// "required", or "repeated").
// - keyword: The token corresponding to the "group" keyword.
// - name: The token corresponding to the field's name.
// - equals: The token corresponding to the '=' rune after the name.
// - tag: The token corresponding to the field's tag number.
// - opts: Optional set of field options.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the group body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewGroupNode(label *KeywordNode, keyword *KeywordNode, name *IdentNode, equals *RuneNode, tag *UintLiteralNode, opts *CompactOptionsNode, openBrace *RuneNode, decls []MessageElement, closeBrace *RuneNode) *GroupNode {
if keyword == nil {
panic("fieldType is nil")
}
if name == nil {
panic("name is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
numChildren := 4 + len(decls)
if label != nil {
numChildren++
}
if equals != nil {
numChildren++
}
if tag != nil {
numChildren++
}
if opts != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
if label != nil {
children = append(children, label)
}
children = append(children, keyword, name)
if equals != nil {
children = append(children, equals)
}
if tag != nil {
children = append(children, tag)
}
if opts != nil {
children = append(children, opts)
}
children = append(children, openBrace)
for _, decl := range decls {
children = append(children, decl)
}
children = append(children, closeBrace)
ret := &GroupNode{
compositeNode: compositeNode{
children: children,
},
Label: newFieldLabel(label),
Keyword: keyword,
Name: name,
Equals: equals,
Tag: tag,
Options: opts,
}
populateMessageBody(&ret.MessageBody, openBrace, decls, closeBrace)
return ret
}
func (n *GroupNode) FieldLabel() Node {
if n.Label.KeywordNode == nil {
// return nil interface to indicate absence, not a typed nil
return nil
}
return n.Label.KeywordNode
}
func (n *GroupNode) FieldName() Node {
return n.Name
}
func (n *GroupNode) FieldType() Node {
return n.Keyword
}
func (n *GroupNode) FieldTag() Node {
if n.Tag == nil {
return n
}
return n.Tag
}
func (n *GroupNode) FieldExtendee() Node {
if n.Extendee != nil {
return n.Extendee.Extendee
}
return nil
}
func (n *GroupNode) GetGroupKeyword() Node {
return n.Keyword
}
func (n *GroupNode) GetOptions() *CompactOptionsNode {
return n.Options
}
func (n *GroupNode) RangeOptions(fn func(*OptionNode) bool) {
for _, opt := range n.Options.Options {
if !fn(opt) {
return
}
}
}
func (n *GroupNode) AsMessage() *SyntheticGroupMessageNode {
return (*SyntheticGroupMessageNode)(n)
}
// SyntheticGroupMessageNode is a view of a GroupNode that implements MessageDeclNode.
// Since a group field implicitly defines a message type, this node represents
// that message type while the corresponding GroupNode represents the field.
//
// This type is considered synthetic since it never appears in a file's AST, but
// is only returned from other accessors (e.g. GroupNode.AsMessage).
type SyntheticGroupMessageNode GroupNode
func (n *SyntheticGroupMessageNode) MessageName() Node {
return n.Name
}
func (n *SyntheticGroupMessageNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// OneofDeclNode is a node in the AST that defines a oneof. There are
// multiple types of AST nodes that declare oneofs:
// - *OneofNode
// - *SyntheticOneof
//
// This also allows NoSourceNode to be used in place of one of the above
// for some usages.
type OneofDeclNode interface {
NodeWithOptions
OneofName() Node
}
var _ OneofDeclNode = (*OneofNode)(nil)
var _ OneofDeclNode = (*SyntheticOneof)(nil)
var _ OneofDeclNode = (*NoSourceNode)(nil)
// OneofNode represents a one-of declaration. Example:
//
// oneof query {
// string by_name = 2;
// Type by_type = 3;
// Address by_address = 4;
// Labels by_label = 5;
// }
type OneofNode struct {
compositeNode
Keyword *KeywordNode
Name *IdentNode
OpenBrace *RuneNode
Decls []OneofElement
CloseBrace *RuneNode
}
func (*OneofNode) msgElement() {}
// NewOneofNode creates a new *OneofNode. All arguments must be non-nil. While
// it is technically allowed for decls to be nil or empty, the resulting node
// will not be a valid oneof, which must have at least one field.
// - keyword: The token corresponding to the "oneof" keyword.
// - name: The token corresponding to the oneof's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the oneof body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewOneofNode(keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []OneofElement, closeBrace *RuneNode) *OneofNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
children := make([]Node, 0, 4+len(decls))
children = append(children, keyword, name, openBrace)
for _, decl := range decls {
children = append(children, decl)
}
children = append(children, closeBrace)
for _, decl := range decls {
switch decl := decl.(type) {
case *OptionNode, *FieldNode, *GroupNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid OneofElement type: %T", decl))
}
}
return &OneofNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
OpenBrace: openBrace,
Decls: decls,
CloseBrace: closeBrace,
}
}
func (n *OneofNode) OneofName() Node {
return n.Name
}
func (n *OneofNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// OneofElement is an interface implemented by all AST nodes that can
// appear in the body of a oneof declaration.
type OneofElement interface {
Node
oneofElement()
}
var _ OneofElement = (*OptionNode)(nil)
var _ OneofElement = (*FieldNode)(nil)
var _ OneofElement = (*GroupNode)(nil)
var _ OneofElement = (*EmptyDeclNode)(nil)
// SyntheticOneof is not an actual node in the AST but a synthetic node
// that represents the oneof implied by a proto3 optional field.
//
// This type is considered synthetic since it never appears in a file's AST,
// but is only returned from other functions (e.g. NewSyntheticOneof).
type SyntheticOneof struct {
// The proto3 optional field that implies the presence of this oneof.
Field *FieldNode
}
var _ Node = (*SyntheticOneof)(nil)
// NewSyntheticOneof creates a new *SyntheticOneof that corresponds to the
// given proto3 optional field.
func NewSyntheticOneof(field *FieldNode) *SyntheticOneof {
return &SyntheticOneof{Field: field}
}
func (n *SyntheticOneof) Start() Token {
return n.Field.Start()
}
func (n *SyntheticOneof) End() Token {
return n.Field.End()
}
func (n *SyntheticOneof) LeadingComments() []Comment {
return nil
}
func (n *SyntheticOneof) TrailingComments() []Comment {
return nil
}
func (n *SyntheticOneof) OneofName() Node {
return n.Field.FieldName()
}
func (n *SyntheticOneof) RangeOptions(_ func(*OptionNode) bool) {
}
// MapTypeNode represents the type declaration for a map field. It defines
// both the key and value types for the map. Example:
//
// map<string, Values>
type MapTypeNode struct {
compositeNode
Keyword *KeywordNode
OpenAngle *RuneNode
KeyType *IdentNode
Comma *RuneNode
ValueType IdentValueNode
CloseAngle *RuneNode
}
// NewMapTypeNode creates a new *MapTypeNode. All arguments must be non-nil.
// - keyword: The token corresponding to the "map" keyword.
// - openAngle: The token corresponding to the "<" rune after the keyword.
// - keyType: The token corresponding to the key type for the map.
// - comma: The token corresponding to the "," rune between key and value types.
// - valType: The token corresponding to the value type for the map.
// - closeAngle: The token corresponding to the ">" rune that ends the declaration.
func NewMapTypeNode(keyword *KeywordNode, openAngle *RuneNode, keyType *IdentNode, comma *RuneNode, valType IdentValueNode, closeAngle *RuneNode) *MapTypeNode {
if keyword == nil {
panic("keyword is nil")
}
if openAngle == nil {
panic("openAngle is nil")
}
if keyType == nil {
panic("keyType is nil")
}
if comma == nil {
panic("comma is nil")
}
if valType == nil {
panic("valType is nil")
}
if closeAngle == nil {
panic("closeAngle is nil")
}
children := []Node{keyword, openAngle, keyType, comma, valType, closeAngle}
return &MapTypeNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
OpenAngle: openAngle,
KeyType: keyType,
Comma: comma,
ValueType: valType,
CloseAngle: closeAngle,
}
}
// MapFieldNode represents a map field declaration. Example:
//
// map<string,string> replacements = 3 [deprecated = true];
type MapFieldNode struct {
compositeNode
MapType *MapTypeNode
Name *IdentNode
Equals *RuneNode
Tag *UintLiteralNode
Options *CompactOptionsNode
Semicolon *RuneNode
}
func (*MapFieldNode) msgElement() {}
// NewMapFieldNode creates a new *MapFieldNode. All arguments must be non-nil
// except opts, which may be nil.
// - mapType: The token corresponding to the map type.
// - name: The token corresponding to the field's name.
// - equals: The token corresponding to the '=' rune after the name.
// - tag: The token corresponding to the field's tag number.
// - opts: Optional set of field options.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewMapFieldNode(mapType *MapTypeNode, name *IdentNode, equals *RuneNode, tag *UintLiteralNode, opts *CompactOptionsNode, semicolon *RuneNode) *MapFieldNode {
if mapType == nil {
panic("mapType is nil")
}
if name == nil {
panic("name is nil")
}
numChildren := 2
if equals != nil {
numChildren++
}
if tag != nil {
numChildren++
}
if opts != nil {
numChildren++
}
if semicolon != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, mapType, name)
if equals != nil {
children = append(children, equals)
}
if tag != nil {
children = append(children, tag)
}
if opts != nil {
children = append(children, opts)
}
if semicolon != nil {
children = append(children, semicolon)
}
return &MapFieldNode{
compositeNode: compositeNode{
children: children,
},
MapType: mapType,
Name: name,
Equals: equals,
Tag: tag,
Options: opts,
Semicolon: semicolon,
}
}
func (n *MapFieldNode) FieldLabel() Node {
return nil
}
func (n *MapFieldNode) FieldName() Node {
return n.Name
}
func (n *MapFieldNode) FieldType() Node {
return n.MapType
}
func (n *MapFieldNode) FieldTag() Node {
if n.Tag == nil {
return n
}
return n.Tag
}
func (n *MapFieldNode) FieldExtendee() Node {
return nil
}
func (n *MapFieldNode) GetGroupKeyword() Node {
return nil
}
func (n *MapFieldNode) GetOptions() *CompactOptionsNode {
return n.Options
}
func (n *MapFieldNode) RangeOptions(fn func(*OptionNode) bool) {
for _, opt := range n.Options.Options {
if !fn(opt) {
return
}
}
}
func (n *MapFieldNode) AsMessage() *SyntheticMapEntryNode {
return (*SyntheticMapEntryNode)(n)
}
func (n *MapFieldNode) KeyField() *SyntheticMapField {
return NewSyntheticMapField(n.MapType.KeyType, 1)
}
func (n *MapFieldNode) ValueField() *SyntheticMapField {
return NewSyntheticMapField(n.MapType.ValueType, 2)
}
// SyntheticMapEntryNode is a view of a MapFieldNode that implements MessageDeclNode.
// Since a map field implicitly defines a message type for the map entry,
// this node represents that message type.
//
// This type is considered synthetic since it never appears in a file's AST, but
// is only returned from other accessors (e.g. MapFieldNode.AsMessage).
type SyntheticMapEntryNode MapFieldNode
func (n *SyntheticMapEntryNode) MessageName() Node {
return n.Name
}
func (n *SyntheticMapEntryNode) RangeOptions(_ func(*OptionNode) bool) {
}
// SyntheticMapField is not an actual node in the AST but a synthetic node
// that implements FieldDeclNode. These are used to represent the implicit
// field declarations of the "key" and "value" fields in a map entry.
//
// This type is considered synthetic since it never appears in a file's AST,
// but is only returned from other accessors and functions (e.g.
// MapFieldNode.KeyField, MapFieldNode.ValueField, and NewSyntheticMapField).
type SyntheticMapField struct {
Ident IdentValueNode
Tag *UintLiteralNode
}
// NewSyntheticMapField creates a new *SyntheticMapField for the given
// identifier (either a key or value type in a map declaration) and tag
// number (1 for key, 2 for value).
func NewSyntheticMapField(ident IdentValueNode, tagNum uint64) *SyntheticMapField {
tag := &UintLiteralNode{
terminalNode: ident.Start().asTerminalNode(),
Val: tagNum,
}
return &SyntheticMapField{Ident: ident, Tag: tag}
}
func (n *SyntheticMapField) Start() Token {
return n.Ident.Start()
}
func (n *SyntheticMapField) End() Token {
return n.Ident.End()
}
func (n *SyntheticMapField) LeadingComments() []Comment {
return nil
}
func (n *SyntheticMapField) TrailingComments() []Comment {
return nil
}
func (n *SyntheticMapField) FieldLabel() Node {
return n.Ident
}
func (n *SyntheticMapField) FieldName() Node {
return n.Ident
}
func (n *SyntheticMapField) FieldType() Node {
return n.Ident
}
func (n *SyntheticMapField) FieldTag() Node {
if n.Tag == nil {
return n
}
return n.Tag
}
func (n *SyntheticMapField) FieldExtendee() Node {
return nil
}
func (n *SyntheticMapField) GetGroupKeyword() Node {
return nil
}
func (n *SyntheticMapField) GetOptions() *CompactOptionsNode {
return nil
}
func (n *SyntheticMapField) RangeOptions(_ func(*OptionNode) bool) {
}
// 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 ast
import "fmt"
// FileDeclNode is a placeholder interface for AST nodes that represent files.
// This allows NoSourceNode to be used in place of *FileNode for some usages.
type FileDeclNode interface {
NodeWithOptions
Name() string
NodeInfo(n Node) NodeInfo
}
var _ FileDeclNode = (*FileNode)(nil)
var _ FileDeclNode = (*NoSourceNode)(nil)
// FileNode is the root of the AST hierarchy. It represents an entire
// protobuf source file.
type FileNode struct {
compositeNode
fileInfo *FileInfo
// A file has either a Syntax or Edition node, never both.
// If both are nil, neither declaration is present and the
// file is assumed to use "proto2" syntax.
Syntax *SyntaxNode
Edition *EditionNode
Decls []FileElement
// This synthetic node allows access to final comments and whitespace
EOF *RuneNode
}
// NewFileNode creates a new *FileNode. The syntax parameter is optional. If it
// is absent, it means the file had no syntax declaration.
//
// This function panics if the concrete type of any element of decls is not
// from this package.
func NewFileNode(info *FileInfo, syntax *SyntaxNode, decls []FileElement, eof Token) *FileNode {
return newFileNode(info, syntax, nil, decls, eof)
}
// NewFileNodeWithEdition creates a new *FileNode. The edition parameter is required. If a file
// has no edition declaration, use NewFileNode instead.
//
// This function panics if the concrete type of any element of decls is not
// from this package.
func NewFileNodeWithEdition(info *FileInfo, edition *EditionNode, decls []FileElement, eof Token) *FileNode {
if edition == nil {
panic("edition is nil")
}
return newFileNode(info, nil, edition, decls, eof)
}
func newFileNode(info *FileInfo, syntax *SyntaxNode, edition *EditionNode, decls []FileElement, eof Token) *FileNode {
numChildren := len(decls) + 1
if syntax != nil || edition != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
if syntax != nil {
children = append(children, syntax)
} else if edition != nil {
children = append(children, edition)
}
for _, decl := range decls {
switch decl := decl.(type) {
case *PackageNode, *ImportNode, *OptionNode, *MessageNode,
*EnumNode, *ExtendNode, *ServiceNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid FileElement type: %T", decl))
}
children = append(children, decl)
}
eofNode := NewRuneNode(0, eof)
children = append(children, eofNode)
return &FileNode{
compositeNode: compositeNode{
children: children,
},
fileInfo: info,
Syntax: syntax,
Edition: edition,
Decls: decls,
EOF: eofNode,
}
}
// NewEmptyFileNode returns an empty AST for a file with the given name.
func NewEmptyFileNode(filename string) *FileNode {
fileInfo := NewFileInfo(filename, []byte{})
return NewFileNode(fileInfo, nil, nil, fileInfo.AddToken(0, 0))
}
func (f *FileNode) Name() string {
return f.fileInfo.Name()
}
func (f *FileNode) NodeInfo(n Node) NodeInfo {
return f.fileInfo.NodeInfo(n)
}
func (f *FileNode) TokenInfo(t Token) NodeInfo {
return f.fileInfo.TokenInfo(t)
}
func (f *FileNode) ItemInfo(i Item) ItemInfo {
return f.fileInfo.ItemInfo(i)
}
func (f *FileNode) GetItem(i Item) (Token, Comment) {
return f.fileInfo.GetItem(i)
}
func (f *FileNode) Items() Sequence[Item] {
return f.fileInfo.Items()
}
func (f *FileNode) Tokens() Sequence[Token] {
return f.fileInfo.Tokens()
}
func (f *FileNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range f.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// FileElement is an interface implemented by all AST nodes that are
// allowed as top-level declarations in the file.
type FileElement interface {
Node
fileElement()
}
var _ FileElement = (*ImportNode)(nil)
var _ FileElement = (*PackageNode)(nil)
var _ FileElement = (*OptionNode)(nil)
var _ FileElement = (*MessageNode)(nil)
var _ FileElement = (*EnumNode)(nil)
var _ FileElement = (*ExtendNode)(nil)
var _ FileElement = (*ServiceNode)(nil)
var _ FileElement = (*EmptyDeclNode)(nil)
// SyntaxNode represents a syntax declaration, which if present must be
// the first non-comment content. Example:
//
// syntax = "proto2";
//
// Files that don't have a syntax node are assumed to use proto2 syntax.
type SyntaxNode struct {
compositeNode
Keyword *KeywordNode
Equals *RuneNode
Syntax StringValueNode
Semicolon *RuneNode
}
// NewSyntaxNode creates a new *SyntaxNode. All four arguments must be non-nil:
// - keyword: The token corresponding to the "syntax" keyword.
// - equals: The token corresponding to the "=" rune.
// - syntax: The actual syntax value, e.g. "proto2" or "proto3".
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewSyntaxNode(keyword *KeywordNode, equals *RuneNode, syntax StringValueNode, semicolon *RuneNode) *SyntaxNode {
if keyword == nil {
panic("keyword is nil")
}
if equals == nil {
panic("equals is nil")
}
if syntax == nil {
panic("syntax is nil")
}
var children []Node
if semicolon == nil {
children = []Node{keyword, equals, syntax}
} else {
children = []Node{keyword, equals, syntax, semicolon}
}
return &SyntaxNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Equals: equals,
Syntax: syntax,
Semicolon: semicolon,
}
}
// EditionNode represents an edition declaration, which if present must be
// the first non-comment content. Example:
//
// edition = "2023";
//
// Files may include either an edition node or a syntax node, but not both.
// If neither are present, the file is assumed to use proto2 syntax.
type EditionNode struct {
compositeNode
Keyword *KeywordNode
Equals *RuneNode
Edition StringValueNode
Semicolon *RuneNode
}
// NewEditionNode creates a new *EditionNode. All four arguments must be non-nil:
// - keyword: The token corresponding to the "edition" keyword.
// - equals: The token corresponding to the "=" rune.
// - edition: The actual edition value, e.g. "2023".
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewEditionNode(keyword *KeywordNode, equals *RuneNode, edition StringValueNode, semicolon *RuneNode) *EditionNode {
if keyword == nil {
panic("keyword is nil")
}
if equals == nil {
panic("equals is nil")
}
if edition == nil {
panic("edition is nil")
}
if semicolon == nil {
panic("semicolon is nil")
}
children := []Node{keyword, equals, edition, semicolon}
return &EditionNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Equals: equals,
Edition: edition,
Semicolon: semicolon,
}
}
// ImportNode represents an import statement. Example:
//
// import "google/protobuf/empty.proto";
type ImportNode struct {
compositeNode
Keyword *KeywordNode
// Optional; if present indicates this is a public import
// Deprecated: Use Modifier field instead.
Public *KeywordNode
// Optional; if present indicates this is a weak import
// Deprecated: Use Modifier field instead.
Weak *KeywordNode
// Optional; if present indicates modifier (public/weak/option)
// If public the Public field will also be populated for backwards compatibility.
// If weak the Weak field will also be populated for backward compatibility.
Modifier *KeywordNode
Name StringValueNode
Semicolon *RuneNode
}
// NewImportNode creates a new *ImportNode. The public and weak arguments are optional
// and only one or the other (or neither) may be specified, not both. When public is
// non-nil, it indicates the "public" keyword in the import statement and means this is
// a public import. When weak is non-nil, it indicates the "weak" keyword in the import
// statement and means this is a weak import. When both are nil, this is a normal import.
// The other arguments must be non-nil:
// - keyword: The token corresponding to the "import" keyword.
// - public: The token corresponding to the optional "public" keyword.
// - weak: The token corresponding to the optional "weak" keyword.
// - name: The actual imported file name.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewImportNode(keyword *KeywordNode, public *KeywordNode, weak *KeywordNode, name StringValueNode, semicolon *RuneNode) *ImportNode {
var modifier *KeywordNode
switch {
case public != nil:
modifier = public
case weak != nil:
modifier = weak
}
return NewImportNodeWithModifier(keyword, modifier, name, semicolon)
}
// NewImportNodeWithModifier creates a new *ImportNode with a single modifier argument.
// The modifier argument is optional and represents either "public", "weak", or "option" keyword.
// For backwards compatibility, the appropriate Public or Weak field will be populated.
// - keyword: The token corresponding to the "import" keyword.
// - modifier: Optional modifier token ("public", "weak", or "option").
// - name: The actual imported file name.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewImportNodeWithModifier(keyword *KeywordNode, modifier *KeywordNode, name StringValueNode, semicolon *RuneNode) *ImportNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
numChildren := 2 // keyword + name
if semicolon != nil {
numChildren++
}
if modifier != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, keyword)
if modifier != nil {
children = append(children, modifier)
}
children = append(children, name)
if semicolon != nil {
children = append(children, semicolon)
}
// For backwards compatibility, populate the appropriate legacy field.
var public, weak *KeywordNode
if modifier != nil {
switch modifier.Val {
case "public":
public = modifier
case "weak":
weak = modifier
}
}
return &ImportNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Public: public,
Weak: weak,
Modifier: modifier,
Name: name,
Semicolon: semicolon,
}
}
func (*ImportNode) fileElement() {}
// PackageNode represents a package declaration. Example:
//
// package foobar.com;
type PackageNode struct {
compositeNode
Keyword *KeywordNode
Name IdentValueNode
Semicolon *RuneNode
}
func (*PackageNode) fileElement() {}
// NewPackageNode creates a new *PackageNode. All three arguments must be non-nil:
// - keyword: The token corresponding to the "package" keyword.
// - name: The package name declared for the file.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewPackageNode(keyword *KeywordNode, name IdentValueNode, semicolon *RuneNode) *PackageNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
var children []Node
if semicolon == nil {
children = []Node{keyword, name}
} else {
children = []Node{keyword, name, semicolon}
}
return &PackageNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
Semicolon: semicolon,
}
}
// 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 ast
import (
"fmt"
"sort"
"unicode/utf8"
)
// FileInfo contains information about the contents of a source file, including
// details about comments and items. A lexer accumulates these details as it
// scans the file contents. This allows efficient representation of things like
// source positions.
type FileInfo struct {
// The name of the source file.
name string
// The raw contents of the source file.
data []byte
// The offsets for each line in the file. The value is the zero-based byte
// offset for a given line. The line is given by its index. So the value at
// index 0 is the offset for the first line (which is always zero). The
// value at index 1 is the offset at which the second line begins. Etc.
lines []int
// The info for every comment in the file. This is empty if the file has no
// comments. The first entry corresponds to the first comment in the file,
// and so on.
comments []commentInfo
// The info for every lexed item in the file. The last item in the slice
// corresponds to the EOF, so every file (even an empty one) should have at
// least one entry. This includes all terminal symbols (tokens) in the AST
// as well as all comments.
items []itemSpan
}
type commentInfo struct {
// the index of the item, in the file's items slice, that represents this
// comment
index int
// the index of the token to which this comment is attributed.
attributedToIndex int
}
type itemSpan struct {
// the offset into the file of the first character of an item.
offset int
// the length of the item
length int
}
// NewFileInfo creates a new instance for the given file.
func NewFileInfo(filename string, contents []byte) *FileInfo {
return &FileInfo{
name: filename,
data: contents,
lines: []int{0},
}
}
func (f *FileInfo) Name() string {
return f.name
}
// AddLine adds the offset representing the beginning of the "next" line in the file.
// The first line always starts at offset 0, the second line starts at offset-of-newline-char+1.
func (f *FileInfo) AddLine(offset int) {
if offset < 0 {
panic(fmt.Sprintf("invalid offset: %d must not be negative", offset))
}
if offset > len(f.data) {
panic(fmt.Sprintf("invalid offset: %d is greater than file size %d", offset, len(f.data)))
}
if len(f.lines) > 0 {
lastOffset := f.lines[len(f.lines)-1]
if offset <= lastOffset {
panic(fmt.Sprintf("invalid offset: %d is not greater than previously observed line offset %d", offset, lastOffset))
}
}
f.lines = append(f.lines, offset)
}
// AddToken adds info about a token at the given location to this file. It
// returns a value that allows access to all of the token's details.
func (f *FileInfo) AddToken(offset, length int) Token {
if offset < 0 {
panic(fmt.Sprintf("invalid offset: %d must not be negative", offset))
}
if length < 0 {
panic(fmt.Sprintf("invalid length: %d must not be negative", length))
}
if offset+length > len(f.data) {
panic(fmt.Sprintf("invalid offset+length: %d is greater than file size %d", offset+length, len(f.data)))
}
tokenID := len(f.items)
if len(f.items) > 0 {
lastToken := f.items[tokenID-1]
lastEnd := lastToken.offset + lastToken.length - 1
if offset <= lastEnd {
panic(fmt.Sprintf("invalid offset: %d is not greater than previously observed token end %d", offset, lastEnd))
}
}
f.items = append(f.items, itemSpan{offset: offset, length: length})
return Token(tokenID)
}
// AddComment adds info about a comment to this file. Comments must first be
// added as items via f.AddToken(). The given comment argument is the Token
// from that step. The given attributedTo argument indicates another token in the
// file with which the comment is associated. If comment's offset is before that
// of attributedTo, then this is a leading comment. Otherwise, it is a trailing
// comment.
func (f *FileInfo) AddComment(comment, attributedTo Token) Comment {
if len(f.comments) > 0 {
lastComment := f.comments[len(f.comments)-1]
if int(comment) <= lastComment.index {
panic(fmt.Sprintf("invalid index: %d is not greater than previously observed comment index %d", comment, lastComment.index))
}
if int(attributedTo) < lastComment.attributedToIndex {
panic(fmt.Sprintf("invalid attribution: %d is not greater than previously observed comment attribution index %d", attributedTo, lastComment.attributedToIndex))
}
}
f.comments = append(f.comments, commentInfo{index: int(comment), attributedToIndex: int(attributedTo)})
return Comment{
fileInfo: f,
index: len(f.comments) - 1,
}
}
// NodeInfo returns details from the original source for the given AST node.
//
// If the given n is out of range, this returns an invalid NodeInfo (i.e.
// nodeInfo.IsValid() returns false). If the given n is not out of range but
// also from a different file than f, then the result is undefined.
func (f *FileInfo) NodeInfo(n Node) NodeInfo {
return f.nodeInfo(int(n.Start()), int(n.End()))
}
// TokenInfo returns details from the original source for the given token.
//
// If the given t is out of range, this returns an invalid NodeInfo (i.e.
// nodeInfo.IsValid() returns false). If the given t is not out of range but
// also from a different file than f, then the result is undefined.
func (f *FileInfo) TokenInfo(t Token) NodeInfo {
return f.nodeInfo(int(t), int(t))
}
func (f *FileInfo) nodeInfo(start, end int) NodeInfo {
if start < 0 || start >= len(f.items) {
return NodeInfo{fileInfo: f}
}
if end < 0 || end >= len(f.items) {
return NodeInfo{fileInfo: f}
}
return NodeInfo{fileInfo: f, startIndex: start, endIndex: end}
}
// ItemInfo returns details from the original source for the given item.
//
// If the given i is out of range, this returns nil. If the given i is not
// out of range but also from a different file than f, then the result is
// undefined.
func (f *FileInfo) ItemInfo(i Item) ItemInfo {
tok, cmt := f.GetItem(i)
if tok != TokenError {
return f.TokenInfo(tok)
}
if cmt.IsValid() {
return cmt
}
return nil
}
// GetItem returns the token or comment represented by the given item. Only one
// of the return values will be valid. If the item is a token then the returned
// comment will be a zero value and thus invalid (i.e. comment.IsValid() returns
// false). If the item is a comment then the returned token will be TokenError.
//
// If the given i is out of range, this returns (TokenError, Comment{}). If the
// given i is not out of range but also from a different file than f, then
// the result is undefined.
func (f *FileInfo) GetItem(i Item) (Token, Comment) {
if i < 0 || int(i) >= len(f.items) {
return TokenError, Comment{}
}
if !f.isComment(i) {
return Token(i), Comment{}
}
// It's a comment, so find its location in f.comments
c := sort.Search(len(f.comments), func(c int) bool {
return f.comments[c].index >= int(i)
})
if c < len(f.comments) && f.comments[c].index == int(i) {
return TokenError, Comment{fileInfo: f, index: c}
}
// f.isComment(i) returned true, but we couldn't find it
// in f.comments? Uh oh... that shouldn't be possible.
return TokenError, Comment{}
}
func (f *FileInfo) isDummyFile() bool {
return f == nil || f.lines == nil
}
// Sequence represents a navigable sequence of elements.
type Sequence[T any] interface {
// First returns the first element in the sequence. The bool return
// is false if this sequence contains no elements. For example, an
// empty file has no items or tokens.
First() (T, bool)
// Next returns the next element in the sequence that comes after
// the given element. The bool returns is false if there is no next
// item (i.e. the given element is the last one). It also returns
// false if the given element is invalid.
Next(T) (T, bool)
// Last returns the last element in the sequence. The bool return
// is false if this sequence contains no elements. For example, an
// empty file has no items or tokens.
Last() (T, bool)
// Previous returns the previous element in the sequence that comes
// before the given element. The bool returns is false if there is no
// previous item (i.e. the given element is the first one). It also
// returns false if the given element is invalid.
Previous(T) (T, bool)
}
func (f *FileInfo) Items() Sequence[Item] {
return items{fileInfo: f}
}
func (f *FileInfo) Tokens() Sequence[Token] {
return tokens{fileInfo: f}
}
type items struct {
fileInfo *FileInfo
}
func (i items) First() (Item, bool) {
if len(i.fileInfo.items) == 0 {
return 0, false
}
return 0, true
}
func (i items) Next(item Item) (Item, bool) {
if item < 0 || int(item) >= len(i.fileInfo.items)-1 {
return 0, false
}
return i.fileInfo.itemForward(item+1, true)
}
func (i items) Last() (Item, bool) {
if len(i.fileInfo.items) == 0 {
return 0, false
}
return Item(len(i.fileInfo.items) - 1), true
}
func (i items) Previous(item Item) (Item, bool) {
if item <= 0 || int(item) >= len(i.fileInfo.items) {
return 0, false
}
return i.fileInfo.itemBackward(item-1, true)
}
type tokens struct {
fileInfo *FileInfo
}
func (t tokens) First() (Token, bool) {
i, ok := t.fileInfo.itemForward(0, false)
return Token(i), ok
}
func (t tokens) Next(tok Token) (Token, bool) {
if tok < 0 || int(tok) >= len(t.fileInfo.items)-1 {
return 0, false
}
i, ok := t.fileInfo.itemForward(Item(tok+1), false)
return Token(i), ok
}
func (t tokens) Last() (Token, bool) {
i, ok := t.fileInfo.itemBackward(Item(len(t.fileInfo.items))-1, false)
return Token(i), ok
}
func (t tokens) Previous(tok Token) (Token, bool) {
if tok <= 0 || int(tok) >= len(t.fileInfo.items) {
return 0, false
}
i, ok := t.fileInfo.itemBackward(Item(tok-1), false)
return Token(i), ok
}
func (f *FileInfo) itemForward(i Item, allowComment bool) (Item, bool) {
end := Item(len(f.items))
for i < end {
if allowComment || !f.isComment(i) {
return i, true
}
i++
}
return 0, false
}
func (f *FileInfo) itemBackward(i Item, allowComment bool) (Item, bool) {
for i >= 0 {
if allowComment || !f.isComment(i) {
return i, true
}
i--
}
return 0, false
}
// isComment is comment returns true if i refers to a comment.
// (If it returns false, i refers to a token.)
func (f *FileInfo) isComment(i Item) bool {
item := f.items[i]
if item.length < 2 {
return false
}
// see if item text starts with "//" or "/*"
if f.data[item.offset] != '/' {
return false
}
c := f.data[item.offset+1]
return c == '/' || c == '*'
}
func (f *FileInfo) SourcePos(offset int) SourcePos {
lineNumber := sort.Search(len(f.lines), func(n int) bool {
return f.lines[n] > offset
})
// If it weren't for tabs and multibyte unicode characters, we
// could trivially compute the column just based on offset and the
// starting offset of lineNumber :(
// Wish this were more efficient... that would require also storing
// computed line+column information, which would triple the size of
// f's items slice...
col := 0
for i := f.lines[lineNumber-1]; i < offset; i++ {
if f.data[i] == '\t' {
nextTabStop := 8 - (col % 8)
col += nextTabStop
} else if utf8.RuneStart(f.data[i]) {
col++
}
}
return SourcePos{
Filename: f.name,
Offset: offset,
Line: lineNumber,
// Columns are 1-indexed in this AST
Col: col + 1,
}
}
// Token represents a single lexed token.
type Token int
// TokenError indicates an invalid token. It is returned from query
// functions when no valid token satisfies the request.
const TokenError = Token(-1)
// AsItem returns the Item that corresponds to t.
func (t Token) AsItem() Item {
return Item(t)
}
func (t Token) asTerminalNode() terminalNode {
return terminalNode(t)
}
// Item represents an item lexed from source. It represents either
// a Token or a Comment.
type Item int
// ItemInfo provides details about an item's location in the source file and
// its contents.
type ItemInfo interface {
SourceSpan
LeadingWhitespace() string
RawText() string
}
// NodeInfo represents the details for a node or token in the source file's AST.
// It provides access to information about the node's location in the source
// file. It also provides access to the original text in the source file (with
// all the original formatting intact) and also provides access to surrounding
// comments.
type NodeInfo struct {
fileInfo *FileInfo
startIndex, endIndex int
}
var _ ItemInfo = NodeInfo{}
// IsValid returns true if this node info is valid. If n is a zero-value struct,
// it is not valid.
func (n NodeInfo) IsValid() bool {
return n.fileInfo != nil
}
// Start returns the starting position of the element. This is the first
// character of the node or token.
func (n NodeInfo) Start() SourcePos {
if n.fileInfo.isDummyFile() {
return UnknownPos(n.fileInfo.name)
}
tok := n.fileInfo.items[n.startIndex]
return n.fileInfo.SourcePos(tok.offset)
}
// End returns the ending position of the element, exclusive. This is the
// location after the last character of the node or token. If n returns
// the same position for Start() and End(), the element in source had a
// length of zero (which should only happen for the special EOF token
// that designates the end of the file).
func (n NodeInfo) End() SourcePos {
if n.fileInfo.isDummyFile() {
return UnknownPos(n.fileInfo.name)
}
tok := n.fileInfo.items[n.endIndex]
// find offset of last character in the span
offset := tok.offset
if tok.length > 0 {
offset += tok.length - 1
}
pos := n.fileInfo.SourcePos(offset)
if tok.length > 0 {
// We return "open range", so end is the position *after* the
// last character in the span. So we adjust
pos.Col++
}
return pos
}
// LeadingWhitespace returns any whitespace prior to the element. If there
// were comments in between this element and the previous one, this will
// return the whitespace between the last such comment in the element. If
// there were no such comments, this returns the whitespace between the
// previous element and the current one.
func (n NodeInfo) LeadingWhitespace() string {
if n.fileInfo.isDummyFile() {
return ""
}
tok := n.fileInfo.items[n.startIndex]
var prevEnd int
if n.startIndex > 0 {
prevTok := n.fileInfo.items[n.startIndex-1]
prevEnd = prevTok.offset + prevTok.length
}
return string(n.fileInfo.data[prevEnd:tok.offset])
}
// LeadingComments returns all comments in the source that exist between the
// element and the previous element, except for any trailing comment on the
// previous element.
func (n NodeInfo) LeadingComments() Comments {
if n.fileInfo.isDummyFile() {
return EmptyComments
}
start := sort.Search(len(n.fileInfo.comments), func(i int) bool {
return n.fileInfo.comments[i].attributedToIndex >= n.startIndex
})
if start == len(n.fileInfo.comments) || n.fileInfo.comments[start].attributedToIndex != n.startIndex {
// no comments associated with this token
return EmptyComments
}
numComments := 0
for i := start; i < len(n.fileInfo.comments); i++ {
comment := n.fileInfo.comments[i]
if comment.attributedToIndex == n.startIndex &&
comment.index < n.startIndex {
numComments++
} else {
break
}
}
return Comments{
fileInfo: n.fileInfo,
first: start,
num: numComments,
}
}
// TrailingComments returns the trailing comment for the element, if any.
// An element will have a trailing comment only if it is the last token
// on a line and is followed by a comment on the same line. Typically, the
// following comment is a line-style comment (starting with "//").
//
// If the following comment is a block-style comment that spans multiple
// lines, and the next token is on the same line as the end of the comment,
// the comment is NOT considered a trailing comment.
//
// Examples:
//
// foo // this is a trailing comment for foo
//
// bar /* this is a trailing comment for bar */
//
// baz /* this is a trailing
// comment for baz */
//
// fizz /* this is NOT a trailing
// comment for fizz because
// its on the same line as the
// following token buzz */ buzz
func (n NodeInfo) TrailingComments() Comments {
if n.fileInfo.isDummyFile() {
return EmptyComments
}
start := sort.Search(len(n.fileInfo.comments), func(i int) bool {
comment := n.fileInfo.comments[i]
return comment.attributedToIndex >= n.endIndex &&
comment.index > n.endIndex
})
if start == len(n.fileInfo.comments) || n.fileInfo.comments[start].attributedToIndex != n.endIndex {
// no comments associated with this token
return EmptyComments
}
numComments := 0
for i := start; i < len(n.fileInfo.comments); i++ {
comment := n.fileInfo.comments[i]
if comment.attributedToIndex == n.endIndex {
numComments++
} else {
break
}
}
return Comments{
fileInfo: n.fileInfo,
first: start,
num: numComments,
}
}
// RawText returns the actual text in the source file that corresponds to the
// element. If the element is a node in the AST that encompasses multiple
// items (like an entire declaration), the full text of all items is returned
// including any interior whitespace and comments.
func (n NodeInfo) RawText() string {
startTok := n.fileInfo.items[n.startIndex]
endTok := n.fileInfo.items[n.endIndex]
return string(n.fileInfo.data[startTok.offset : endTok.offset+endTok.length])
}
// SourcePos identifies a location in a proto source file.
type SourcePos struct {
Filename string
// The line and column numbers for this position. These are
// one-based, so the first line and column is 1 (not zero). If
// either is zero, then the line and column are unknown and
// only the file name is known.
Line, Col int
// The offset, in bytes, from the beginning of the file. This
// is zero-based: the first character in the file is offset zero.
Offset int
}
func (pos SourcePos) String() string {
if pos.Line <= 0 || pos.Col <= 0 {
return pos.Filename
}
return fmt.Sprintf("%s:%d:%d", pos.Filename, pos.Line, pos.Col)
}
// SourceSpan represents a range of source positions.
type SourceSpan interface {
Start() SourcePos
End() SourcePos
}
// NewSourceSpan creates a new span that covers the given range.
func NewSourceSpan(start SourcePos, end SourcePos) SourceSpan {
return sourceSpan{StartPos: start, EndPos: end}
}
type sourceSpan struct {
StartPos SourcePos
EndPos SourcePos
}
func (p sourceSpan) Start() SourcePos {
return p.StartPos
}
func (p sourceSpan) End() SourcePos {
return p.EndPos
}
var _ SourceSpan = sourceSpan{}
// Comments represents a range of sequential comments in a source file
// (e.g. no interleaving items or AST nodes).
type Comments struct {
fileInfo *FileInfo
first, num int
}
// EmptyComments is an empty set of comments.
var EmptyComments = Comments{}
// Len returns the number of comments in c.
func (c Comments) Len() int {
return c.num
}
func (c Comments) Index(i int) Comment {
if i < 0 || i >= c.num {
panic(fmt.Sprintf("index %d out of range (len = %d)", i, c.num))
}
return Comment{
fileInfo: c.fileInfo,
index: c.first + i,
}
}
// Comment represents a single comment in a source file. It indicates
// the position of the comment and its contents. A single comment means
// one line-style comment ("//" to end of line) or one block comment
// ("/*" through "*/"). If a longer comment uses multiple line comments,
// each line is considered to be a separate comment. For example:
//
// // This is a single comment, and
// // this is a separate comment.
type Comment struct {
fileInfo *FileInfo
index int
}
var _ ItemInfo = Comment{}
// IsValid returns true if this comment is valid. If this comment is
// a zero-value struct, it is not valid.
func (c Comment) IsValid() bool {
return c.fileInfo != nil && c.index >= 0
}
// AsItem returns the Item that corresponds to c.
func (c Comment) AsItem() Item {
return Item(c.fileInfo.comments[c.index].index)
}
func (c Comment) Start() SourcePos {
span := c.fileInfo.items[c.AsItem()]
return c.fileInfo.SourcePos(span.offset)
}
func (c Comment) End() SourcePos {
span := c.fileInfo.items[c.AsItem()]
return c.fileInfo.SourcePos(span.offset + span.length - 1)
}
func (c Comment) LeadingWhitespace() string {
item := c.AsItem()
span := c.fileInfo.items[item]
var prevEnd int
if item > 0 {
prevItem := c.fileInfo.items[item-1]
prevEnd = prevItem.offset + prevItem.length
}
return string(c.fileInfo.data[prevEnd:span.offset])
}
func (c Comment) RawText() string {
span := c.fileInfo.items[c.AsItem()]
return string(c.fileInfo.data[span.offset : span.offset+span.length])
}
// 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 ast
import (
"fmt"
"strings"
)
// Identifier is a possibly-qualified name. This is used to distinguish
// ValueNode values that are references/identifiers vs. those that are
// string literals.
type Identifier string
// IdentValueNode is an AST node that represents an identifier.
type IdentValueNode interface {
ValueNode
AsIdentifier() Identifier
}
var _ IdentValueNode = (*IdentNode)(nil)
var _ IdentValueNode = (*CompoundIdentNode)(nil)
// IdentNode represents a simple, unqualified identifier. These are used to name
// elements declared in a protobuf file or to refer to elements. Example:
//
// foobar
type IdentNode struct {
terminalNode
Val string
}
// NewIdentNode creates a new *IdentNode. The given val is the identifier text.
func NewIdentNode(val string, tok Token) *IdentNode {
return &IdentNode{
terminalNode: tok.asTerminalNode(),
Val: val,
}
}
func (n *IdentNode) Value() any {
return n.AsIdentifier()
}
func (n *IdentNode) AsIdentifier() Identifier {
return Identifier(n.Val)
}
// ToKeyword is used to convert identifiers to keywords. Since keywords are not
// reserved in the protobuf language, they are initially lexed as identifiers
// and then converted to keywords based on context.
func (n *IdentNode) ToKeyword() *KeywordNode {
return (*KeywordNode)(n)
}
// CompoundIdentNode represents a qualified identifier. A qualified identifier
// has at least one dot and possibly multiple identifier names (all separated by
// dots). If the identifier has a leading dot, then it is a *fully* qualified
// identifier. Example:
//
// .com.foobar.Baz
type CompoundIdentNode struct {
compositeNode
// Optional leading dot, indicating that the identifier is fully qualified.
LeadingDot *RuneNode
Components []*IdentNode
// Dots[0] is the dot after Components[0]. The length of Dots is always
// one less than the length of Components.
Dots []*RuneNode
// The text value of the identifier, with all components and dots
// concatenated.
Val string
}
// NewCompoundIdentNode creates a *CompoundIdentNode. The leadingDot may be nil.
// The dots arg must have a length that is one less than the length of
// components. The components arg must not be empty.
func NewCompoundIdentNode(leadingDot *RuneNode, components []*IdentNode, dots []*RuneNode) *CompoundIdentNode {
if len(components) == 0 {
panic("must have at least one component")
}
if len(dots) != len(components)-1 && len(dots) != len(components) {
panic(fmt.Sprintf("%d components requires %d dots, not %d", len(components), len(components)-1, len(dots)))
}
numChildren := len(components) + len(dots)
if leadingDot != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
var b strings.Builder
if leadingDot != nil {
children = append(children, leadingDot)
b.WriteRune(leadingDot.Rune)
}
for i, comp := range components {
if i > 0 {
dot := dots[i-1]
children = append(children, dot)
b.WriteRune(dot.Rune)
}
children = append(children, comp)
b.WriteString(comp.Val)
}
if len(dots) == len(components) {
dot := dots[len(dots)-1]
children = append(children, dot)
b.WriteRune(dot.Rune)
}
return &CompoundIdentNode{
compositeNode: compositeNode{
children: children,
},
LeadingDot: leadingDot,
Components: components,
Dots: dots,
Val: b.String(),
}
}
func (n *CompoundIdentNode) Value() any {
return n.AsIdentifier()
}
func (n *CompoundIdentNode) AsIdentifier() Identifier {
return Identifier(n.Val)
}
// KeywordNode is an AST node that represents a keyword. Keywords are
// like identifiers, but they have special meaning in particular contexts.
// Example:
//
// message
type KeywordNode IdentNode
// NewKeywordNode creates a new *KeywordNode. The given val is the keyword.
func NewKeywordNode(val string, tok Token) *KeywordNode {
return &KeywordNode{
terminalNode: tok.asTerminalNode(),
Val: val,
}
}
// 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 ast
import "fmt"
// MessageDeclNode is a node in the AST that defines a message type. This
// includes normal message fields as well as implicit messages:
// - *MessageNode
// - *SyntheticGroupMessageNode (the group is a field and inline message type)
// - *SyntheticMapEntryNode (map fields implicitly define a MapEntry message type)
//
// This also allows NoSourceNode to be used in place of one of the above
// for some usages.
type MessageDeclNode interface {
NodeWithOptions
MessageName() Node
}
var _ MessageDeclNode = (*MessageNode)(nil)
var _ MessageDeclNode = (*SyntheticGroupMessageNode)(nil)
var _ MessageDeclNode = (*SyntheticMapEntryNode)(nil)
var _ MessageDeclNode = (*NoSourceNode)(nil)
// MessageNode represents a message declaration. Example:
//
// message Foo {
// string name = 1;
// repeated string labels = 2;
// bytes extra = 3;
// }
//
// In edition 2024, messages can have export/local modifiers:
//
// local message LocalMessage { ... }
// export message ExportedMessage { ... }
type MessageNode struct {
compositeNode
// Optional; if present indicates visibility modifier ("export" or "local") for edition 2024
Visibility *KeywordNode
Keyword *KeywordNode
Name *IdentNode
MessageBody
}
func (*MessageNode) fileElement() {}
func (*MessageNode) msgElement() {}
// NewMessageNode creates a new *MessageNode. All arguments must be non-nil.
// - keyword: The token corresponding to the "message" keyword.
// - name: The token corresponding to the message's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the message body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewMessageNode(keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []MessageElement, closeBrace *RuneNode) *MessageNode {
return NewMessageNodeWithVisibility(nil, keyword, name, openBrace, decls, closeBrace)
}
// NewMessageNodeWithVisibility creates a new *MessageNode with optional visibility modifier.
// - visibility: Optional visibility modifier token ("export" or "local") for edition 2024.
// - keyword: The token corresponding to the "message" keyword.
// - name: The token corresponding to the message's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the message body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewMessageNodeWithVisibility(visibility *KeywordNode, keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []MessageElement, closeBrace *RuneNode) *MessageNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
numChildren := 4 + len(decls) // keyword, name, openBrace, closeBrace + decls
if visibility != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
if visibility != nil {
children = append(children, visibility)
}
children = append(children, keyword, name, openBrace)
for _, decl := range decls {
children = append(children, decl)
}
children = append(children, closeBrace)
ret := &MessageNode{
compositeNode: compositeNode{
children: children,
},
Visibility: visibility,
Keyword: keyword,
Name: name,
}
populateMessageBody(&ret.MessageBody, openBrace, decls, closeBrace)
return ret
}
func (n *MessageNode) MessageName() Node {
return n.Name
}
func (n *MessageNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// MessageBody represents the body of a message. It is used by both
// MessageNodes and GroupNodes.
type MessageBody struct {
OpenBrace *RuneNode
Decls []MessageElement
CloseBrace *RuneNode
}
func populateMessageBody(m *MessageBody, openBrace *RuneNode, decls []MessageElement, closeBrace *RuneNode) {
m.OpenBrace = openBrace
m.Decls = decls
for _, decl := range decls {
switch decl.(type) {
case *OptionNode, *FieldNode, *MapFieldNode, *GroupNode, *OneofNode,
*MessageNode, *EnumNode, *ExtendNode, *ExtensionRangeNode,
*ReservedNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid MessageElement type: %T", decl))
}
}
m.CloseBrace = closeBrace
}
// MessageElement is an interface implemented by all AST nodes that can
// appear in a message body.
type MessageElement interface {
Node
msgElement()
}
var _ MessageElement = (*OptionNode)(nil)
var _ MessageElement = (*FieldNode)(nil)
var _ MessageElement = (*MapFieldNode)(nil)
var _ MessageElement = (*OneofNode)(nil)
var _ MessageElement = (*GroupNode)(nil)
var _ MessageElement = (*MessageNode)(nil)
var _ MessageElement = (*EnumNode)(nil)
var _ MessageElement = (*ExtendNode)(nil)
var _ MessageElement = (*ExtensionRangeNode)(nil)
var _ MessageElement = (*ReservedNode)(nil)
var _ MessageElement = (*EmptyDeclNode)(nil)
// ExtendNode represents a declaration of extension fields. Example:
//
// extend google.protobuf.FieldOptions {
// bool redacted = 33333;
// }
type ExtendNode struct {
compositeNode
Keyword *KeywordNode
Extendee IdentValueNode
OpenBrace *RuneNode
Decls []ExtendElement
CloseBrace *RuneNode
}
func (*ExtendNode) fileElement() {}
func (*ExtendNode) msgElement() {}
// NewExtendNode creates a new *ExtendNode. All arguments must be non-nil.
// - keyword: The token corresponding to the "extend" keyword.
// - extendee: The token corresponding to the name of the extended message.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the message body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewExtendNode(keyword *KeywordNode, extendee IdentValueNode, openBrace *RuneNode, decls []ExtendElement, closeBrace *RuneNode) *ExtendNode {
if keyword == nil {
panic("keyword is nil")
}
if extendee == nil {
panic("extendee is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
children := make([]Node, 0, 4+len(decls))
children = append(children, keyword, extendee, openBrace)
for _, decl := range decls {
children = append(children, decl)
}
children = append(children, closeBrace)
ret := &ExtendNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Extendee: extendee,
OpenBrace: openBrace,
Decls: decls,
CloseBrace: closeBrace,
}
for _, decl := range decls {
switch decl := decl.(type) {
case *FieldNode:
decl.Extendee = ret
case *GroupNode:
decl.Extendee = ret
case *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid ExtendElement type: %T", decl))
}
}
return ret
}
// ExtendElement is an interface implemented by all AST nodes that can
// appear in the body of an extends declaration.
type ExtendElement interface {
Node
extendElement()
}
var _ ExtendElement = (*FieldNode)(nil)
var _ ExtendElement = (*GroupNode)(nil)
var _ ExtendElement = (*EmptyDeclNode)(nil)
// 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 ast
// UnknownPos is a placeholder position when only the source file
// name is known.
func UnknownPos(filename string) SourcePos {
return SourcePos{Filename: filename}
}
// UnknownSpan is a placeholder span when only the source file
// name is known.
func UnknownSpan(filename string) SourceSpan {
return unknownSpan{filename: filename}
}
type unknownSpan struct {
filename string
}
func (s unknownSpan) Start() SourcePos {
return UnknownPos(s.filename)
}
func (s unknownSpan) End() SourcePos {
return UnknownPos(s.filename)
}
// NoSourceNode is a placeholder AST node that implements numerous
// interfaces in this package. It can be used to represent an AST
// element for a file whose source is not available.
type NoSourceNode FileInfo
// NewNoSourceNode creates a new NoSourceNode for the given filename.
func NewNoSourceNode(filename string) *NoSourceNode {
return &NoSourceNode{name: filename}
}
func (n *NoSourceNode) Name() string {
return n.name
}
func (n *NoSourceNode) Start() Token {
return 0
}
func (n *NoSourceNode) End() Token {
return 0
}
func (n *NoSourceNode) NodeInfo(Node) NodeInfo {
return NodeInfo{
fileInfo: (*FileInfo)(n),
}
}
func (n *NoSourceNode) GetSyntax() Node {
return n
}
func (n *NoSourceNode) GetName() Node {
return n
}
func (n *NoSourceNode) GetValue() ValueNode {
return n
}
func (n *NoSourceNode) FieldLabel() Node {
return n
}
func (n *NoSourceNode) FieldName() Node {
return n
}
func (n *NoSourceNode) FieldType() Node {
return n
}
func (n *NoSourceNode) FieldTag() Node {
return n
}
func (n *NoSourceNode) FieldExtendee() Node {
return n
}
func (n *NoSourceNode) GetGroupKeyword() Node {
return n
}
func (n *NoSourceNode) GetOptions() *CompactOptionsNode {
return nil
}
func (n *NoSourceNode) RangeStart() Node {
return n
}
func (n *NoSourceNode) RangeEnd() Node {
return n
}
func (n *NoSourceNode) GetNumber() Node {
return n
}
func (n *NoSourceNode) MessageName() Node {
return n
}
func (n *NoSourceNode) OneofName() Node {
return n
}
func (n *NoSourceNode) GetInputType() Node {
return n
}
func (n *NoSourceNode) GetOutputType() Node {
return n
}
func (n *NoSourceNode) Value() any {
return nil
}
func (n *NoSourceNode) RangeOptions(func(*OptionNode) bool) {
}
// 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 ast
// Node is the interface implemented by all nodes in the AST. It
// provides information about the span of this AST node in terms
// of location in the source file. It also provides information
// about all prior comments (attached as leading comments) and
// optional subsequent comments (attached as trailing comments).
type Node interface {
Start() Token
End() Token
}
// TerminalNode represents a leaf in the AST. These represent
// the items/lexemes in the protobuf language. Comments and
// whitespace are accumulated by the lexer and associated with
// the following lexed token.
type TerminalNode interface {
Node
Token() Token
}
var _ TerminalNode = (*StringLiteralNode)(nil)
var _ TerminalNode = (*UintLiteralNode)(nil)
var _ TerminalNode = (*FloatLiteralNode)(nil)
var _ TerminalNode = (*IdentNode)(nil)
var _ TerminalNode = (*SpecialFloatLiteralNode)(nil)
var _ TerminalNode = (*KeywordNode)(nil)
var _ TerminalNode = (*RuneNode)(nil)
// CompositeNode represents any non-terminal node in the tree. These
// are interior or root nodes and have child nodes.
type CompositeNode interface {
Node
// Children contains all AST nodes that are immediate children of this one.
Children() []Node
}
// terminalNode contains bookkeeping shared by all TerminalNode
// implementations. It is embedded in all such node types in this
// package. It provides the implementation of the TerminalNode
// interface.
type terminalNode Token
func (n terminalNode) Start() Token {
return Token(n)
}
func (n terminalNode) End() Token {
return Token(n)
}
func (n terminalNode) Token() Token {
return Token(n)
}
// compositeNode contains bookkeeping shared by all CompositeNode
// implementations. It is embedded in all such node types in this
// package. It provides the implementation of the CompositeNode
// interface.
type compositeNode struct {
children []Node
}
func (n *compositeNode) Children() []Node {
return n.children
}
func (n *compositeNode) Start() Token {
return n.children[0].Start()
}
func (n *compositeNode) End() Token {
return n.children[len(n.children)-1].End()
}
// RuneNode represents a single rune in protobuf source. Runes
// are typically collected into items, but some runes stand on
// their own, such as punctuation/symbols like commas, semicolons,
// equals signs, open and close symbols (braces, brackets, angles,
// and parentheses), and periods/dots.
// TODO: make this more compact; if runes don't have attributed comments
// then we don't need a Token to represent them and only need an offset
// into the file's contents.
type RuneNode struct {
terminalNode
Rune rune
}
// NewRuneNode creates a new *RuneNode with the given properties.
func NewRuneNode(r rune, tok Token) *RuneNode {
return &RuneNode{
terminalNode: tok.asTerminalNode(),
Rune: r,
}
}
// EmptyDeclNode represents an empty declaration in protobuf source.
// These amount to extra semicolons, with no actual content preceding
// the semicolon.
type EmptyDeclNode struct {
compositeNode
Semicolon *RuneNode
}
// NewEmptyDeclNode creates a new *EmptyDeclNode. The one argument must
// be non-nil.
func NewEmptyDeclNode(semicolon *RuneNode) *EmptyDeclNode {
if semicolon == nil {
panic("semicolon is nil")
}
return &EmptyDeclNode{
compositeNode: compositeNode{
children: []Node{semicolon},
},
Semicolon: semicolon,
}
}
func (e *EmptyDeclNode) fileElement() {}
func (e *EmptyDeclNode) msgElement() {}
func (e *EmptyDeclNode) extendElement() {}
func (e *EmptyDeclNode) oneofElement() {}
func (e *EmptyDeclNode) enumElement() {}
func (e *EmptyDeclNode) serviceElement() {}
func (e *EmptyDeclNode) methodElement() {}
// 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 ast
import "fmt"
// OptionDeclNode is a placeholder interface for AST nodes that represent
// options. This allows NoSourceNode to be used in place of *OptionNode
// for some usages.
type OptionDeclNode interface {
Node
GetName() Node
GetValue() ValueNode
}
var _ OptionDeclNode = (*OptionNode)(nil)
var _ OptionDeclNode = (*NoSourceNode)(nil)
// OptionNode represents the declaration of a single option for an element.
// It is used both for normal option declarations (start with "option" keyword
// and end with semicolon) and for compact options found in fields, enum values,
// and extension ranges. Example:
//
// option (custom.option) = "foo";
type OptionNode struct {
compositeNode
Keyword *KeywordNode // absent for compact options
Name *OptionNameNode
Equals *RuneNode
Val ValueNode
Semicolon *RuneNode // absent for compact options
}
func (*OptionNode) fileElement() {}
func (*OptionNode) msgElement() {}
func (*OptionNode) oneofElement() {}
func (*OptionNode) enumElement() {}
func (*OptionNode) serviceElement() {}
func (*OptionNode) methodElement() {}
// NewOptionNode creates a new *OptionNode for a full option declaration (as
// used in files, messages, oneofs, enums, services, and methods). All arguments
// must be non-nil. (Also see NewCompactOptionNode.)
// - keyword: The token corresponding to the "option" keyword.
// - name: The token corresponding to the name of the option.
// - equals: The token corresponding to the "=" rune after the name.
// - val: The token corresponding to the option value.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewOptionNode(keyword *KeywordNode, name *OptionNameNode, equals *RuneNode, val ValueNode, semicolon *RuneNode) *OptionNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if equals == nil {
panic("equals is nil")
}
if val == nil {
panic("val is nil")
}
var children []Node
if semicolon == nil {
children = []Node{keyword, name, equals, val}
} else {
children = []Node{keyword, name, equals, val, semicolon}
}
return &OptionNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
Equals: equals,
Val: val,
Semicolon: semicolon,
}
}
// NewCompactOptionNode creates a new *OptionNode for a full compact declaration
// (as used in fields, enum values, and extension ranges). All arguments must be
// non-nil.
// - name: The token corresponding to the name of the option.
// - equals: The token corresponding to the "=" rune after the name.
// - val: The token corresponding to the option value.
func NewCompactOptionNode(name *OptionNameNode, equals *RuneNode, val ValueNode) *OptionNode {
if name == nil {
panic("name is nil")
}
if equals == nil && val != nil {
panic("equals is nil but val is not")
}
if val == nil && equals != nil {
panic("val is nil but equals is not")
}
var children []Node
if equals == nil && val == nil {
children = []Node{name}
} else {
children = []Node{name, equals, val}
}
return &OptionNode{
compositeNode: compositeNode{
children: children,
},
Name: name,
Equals: equals,
Val: val,
}
}
func (n *OptionNode) GetName() Node {
return n.Name
}
func (n *OptionNode) GetValue() ValueNode {
return n.Val
}
// OptionNameNode represents an option name or even a traversal through message
// types to name a nested option field. Example:
//
// (foo.bar).baz.(bob)
type OptionNameNode struct {
compositeNode
Parts []*FieldReferenceNode
// Dots represent the separating '.' characters between name parts. The
// length of this slice must be exactly len(Parts)-1, each item in Parts
// having a corresponding item in this slice *except the last* (since a
// trailing dot is not allowed).
//
// These do *not* include dots that are inside of an extension name. For
// example: (foo.bar).baz.(bob) has three parts:
// 1. (foo.bar) - an extension name
// 2. baz - a regular field in foo.bar
// 3. (bob) - an extension field in baz
// Note that the dot in foo.bar will thus not be present in Dots but is
// instead in Parts[0].
Dots []*RuneNode
}
// NewOptionNameNode creates a new *OptionNameNode. The dots arg must have a
// length that is one less than the length of parts. The parts arg must not be
// empty.
func NewOptionNameNode(parts []*FieldReferenceNode, dots []*RuneNode) *OptionNameNode {
if len(parts) == 0 {
panic("must have at least one part")
}
if len(dots) != len(parts)-1 && len(dots) != len(parts) {
panic(fmt.Sprintf("%d parts requires %d dots, not %d", len(parts), len(parts)-1, len(dots)))
}
children := make([]Node, 0, len(parts)+len(dots))
for i, part := range parts {
if part == nil {
panic(fmt.Sprintf("parts[%d] is nil", i))
}
if i > 0 {
if dots[i-1] == nil {
panic(fmt.Sprintf("dots[%d] is nil", i-1))
}
children = append(children, dots[i-1])
}
children = append(children, part)
}
if len(dots) == len(parts) { // Add the erroneous, but tolerated trailing dot.
if dots[len(dots)-1] == nil {
panic(fmt.Sprintf("dots[%d] is nil", len(dots)-1))
}
children = append(children, dots[len(dots)-1])
}
return &OptionNameNode{
compositeNode: compositeNode{
children: children,
},
Parts: parts,
Dots: dots,
}
}
// FieldReferenceNode is a reference to a field name. It can indicate a regular
// field (simple unqualified name), an extension field (possibly-qualified name
// that is enclosed either in brackets or parentheses), or an "any" type
// reference (a type URL in the form "server.host/fully.qualified.Name" that is
// enclosed in brackets).
//
// Extension names are used in options to refer to custom options (which are
// actually extensions), in which case the name is enclosed in parentheses "("
// and ")". They can also be used to refer to extension fields of options.
//
// Extension names are also used in message literals to set extension fields,
// in which case the name is enclosed in square brackets "[" and "]".
//
// "Any" type references can only be used in message literals, and are not
// allowed in option names. They are always enclosed in square brackets. An
// "any" type reference is distinguished from an extension name by the presence
// of a slash, which must be present in an "any" type reference and must be
// absent in an extension name.
//
// Examples:
//
// foobar
// (foo.bar)
// [foo.bar]
// [type.googleapis.com/foo.bar]
type FieldReferenceNode struct {
compositeNode
Open *RuneNode // only present for extension names and "any" type references
// only present for "any" type references
URLPrefix IdentValueNode
Slash *RuneNode
Name IdentValueNode
Close *RuneNode // only present for extension names and "any" type references
}
// NewFieldReferenceNode creates a new *FieldReferenceNode for a regular field.
// The name arg must not be nil.
func NewFieldReferenceNode(name *IdentNode) *FieldReferenceNode {
if name == nil {
panic("name is nil")
}
children := []Node{name}
return &FieldReferenceNode{
compositeNode: compositeNode{
children: children,
},
Name: name,
}
}
// NewExtensionFieldReferenceNode creates a new *FieldReferenceNode for an
// extension field. All args must be non-nil. The openSym and closeSym runes
// should be "(" and ")" or "[" and "]".
func NewExtensionFieldReferenceNode(openSym *RuneNode, name IdentValueNode, closeSym *RuneNode) *FieldReferenceNode {
if name == nil {
panic("name is nil")
}
if openSym == nil {
panic("openSym is nil")
}
if closeSym == nil {
panic("closeSym is nil")
}
children := []Node{openSym, name, closeSym}
return &FieldReferenceNode{
compositeNode: compositeNode{
children: children,
},
Open: openSym,
Name: name,
Close: closeSym,
}
}
// NewAnyTypeReferenceNode creates a new *FieldReferenceNode for an "any"
// type reference. All args must be non-nil. The openSym and closeSym runes
// should be "[" and "]". The slashSym run should be "/".
func NewAnyTypeReferenceNode(openSym *RuneNode, urlPrefix IdentValueNode, slashSym *RuneNode, name IdentValueNode, closeSym *RuneNode) *FieldReferenceNode {
if name == nil {
panic("name is nil")
}
if openSym == nil {
panic("openSym is nil")
}
if closeSym == nil {
panic("closeSym is nil")
}
if urlPrefix == nil {
panic("urlPrefix is nil")
}
if slashSym == nil {
panic("slashSym is nil")
}
children := []Node{openSym, urlPrefix, slashSym, name, closeSym}
return &FieldReferenceNode{
compositeNode: compositeNode{
children: children,
},
Open: openSym,
URLPrefix: urlPrefix,
Slash: slashSym,
Name: name,
Close: closeSym,
}
}
// IsExtension reports if this is an extension name or not (e.g. enclosed in
// punctuation, such as parentheses or brackets).
func (a *FieldReferenceNode) IsExtension() bool {
return a.Open != nil && a.Slash == nil
}
// IsAnyTypeReference reports if this is an Any type reference.
func (a *FieldReferenceNode) IsAnyTypeReference() bool {
return a.Slash != nil
}
func (a *FieldReferenceNode) Value() string {
if a.Open != nil {
if a.Slash != nil {
return string(a.Open.Rune) + string(a.URLPrefix.AsIdentifier()) + string(a.Slash.Rune) + string(a.Name.AsIdentifier()) + string(a.Close.Rune)
}
return string(a.Open.Rune) + string(a.Name.AsIdentifier()) + string(a.Close.Rune)
}
return string(a.Name.AsIdentifier())
}
// CompactOptionsNode represents a compact options declaration, as used with
// fields, enum values, and extension ranges. Example:
//
// [deprecated = true, json_name = "foo_bar"]
type CompactOptionsNode struct {
compositeNode
OpenBracket *RuneNode
Options []*OptionNode
// Commas represent the separating ',' characters between options. The
// length of this slice must be exactly len(Options)-1, with each item
// in Options having a corresponding item in this slice *except the last*
// (since a trailing comma is not allowed).
Commas []*RuneNode
CloseBracket *RuneNode
}
// NewCompactOptionsNode creates a *CompactOptionsNode. All args must be
// non-nil. The commas arg must have a length that is one less than the
// length of opts. The opts arg must not be empty.
func NewCompactOptionsNode(openBracket *RuneNode, opts []*OptionNode, commas []*RuneNode, closeBracket *RuneNode) *CompactOptionsNode {
if openBracket == nil {
panic("openBracket is nil")
}
if closeBracket == nil {
panic("closeBracket is nil")
}
if len(opts) == 0 && len(commas) != 0 {
panic("opts is empty but commas is not")
}
if len(opts) != len(commas) && len(opts) != len(commas)+1 {
panic(fmt.Sprintf("%d opts requires %d commas, not %d", len(opts), len(opts)-1, len(commas)))
}
children := make([]Node, 0, len(opts)+len(commas)+2)
children = append(children, openBracket)
if len(opts) > 0 {
for i, opt := range opts {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if opt == nil {
panic(fmt.Sprintf("opts[%d] is nil", i))
}
children = append(children, opt)
}
if len(opts) == len(commas) { // Add the erroneous, but tolerated trailing comma.
if commas[len(commas)-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", len(commas)-1))
}
children = append(children, commas[len(commas)-1])
}
}
children = append(children, closeBracket)
return &CompactOptionsNode{
compositeNode: compositeNode{
children: children,
},
OpenBracket: openBracket,
Options: opts,
Commas: commas,
CloseBracket: closeBracket,
}
}
func (e *CompactOptionsNode) GetElements() []*OptionNode {
if e == nil {
return nil
}
return e.Options
}
// NodeWithOptions represents a node in the AST that contains
// option statements.
type NodeWithOptions interface {
Node
RangeOptions(func(*OptionNode) bool)
}
var _ NodeWithOptions = FileDeclNode(nil)
var _ NodeWithOptions = MessageDeclNode(nil)
var _ NodeWithOptions = OneofDeclNode(nil)
var _ NodeWithOptions = (*EnumNode)(nil)
var _ NodeWithOptions = (*ServiceNode)(nil)
var _ NodeWithOptions = RPCDeclNode(nil)
var _ NodeWithOptions = FieldDeclNode(nil)
var _ NodeWithOptions = EnumValueDeclNode(nil)
var _ NodeWithOptions = (*ExtensionRangeNode)(nil)
var _ NodeWithOptions = (*NoSourceNode)(nil)
// 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 ast
import "fmt"
// ExtensionRangeNode represents an extension range declaration in an extendable
// message. Example:
//
// extensions 100 to max;
type ExtensionRangeNode struct {
compositeNode
Keyword *KeywordNode
Ranges []*RangeNode
// Commas represent the separating ',' characters between ranges. The
// length of this slice must be exactly len(Ranges)-1, each item in Ranges
// having a corresponding item in this slice *except the last* (since a
// trailing comma is not allowed).
Commas []*RuneNode
Options *CompactOptionsNode
Semicolon *RuneNode
}
func (*ExtensionRangeNode) msgElement() {}
// NewExtensionRangeNode creates a new *ExtensionRangeNode. All args must be
// non-nil except opts, which may be nil.
// - keyword: The token corresponding to the "extends" keyword.
// - ranges: One or more range expressions.
// - commas: Tokens that represent the "," runes that delimit the range expressions.
// The length of commas must be one less than the length of ranges.
// - opts: The node corresponding to options that apply to each of the ranges.
// - semicolon The token corresponding to the ";" rune that ends the declaration.
func NewExtensionRangeNode(keyword *KeywordNode, ranges []*RangeNode, commas []*RuneNode, opts *CompactOptionsNode, semicolon *RuneNode) *ExtensionRangeNode {
if keyword == nil {
panic("keyword is nil")
}
if semicolon == nil {
panic("semicolon is nil")
}
if len(ranges) == 0 {
panic("must have at least one range")
}
if len(commas) != len(ranges)-1 {
panic(fmt.Sprintf("%d ranges requires %d commas, not %d", len(ranges), len(ranges)-1, len(commas)))
}
numChildren := len(ranges)*2 + 1
if opts != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, keyword)
for i, rng := range ranges {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if rng == nil {
panic(fmt.Sprintf("ranges[%d] is nil", i))
}
children = append(children, rng)
}
if opts != nil {
children = append(children, opts)
}
children = append(children, semicolon)
return &ExtensionRangeNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Ranges: ranges,
Commas: commas,
Options: opts,
Semicolon: semicolon,
}
}
func (e *ExtensionRangeNode) RangeOptions(fn func(*OptionNode) bool) {
for _, opt := range e.Options.Options {
if !fn(opt) {
return
}
}
}
// RangeDeclNode is a placeholder interface for AST nodes that represent
// numeric values. This allows NoSourceNode to be used in place of *RangeNode
// for some usages.
type RangeDeclNode interface {
Node
RangeStart() Node
RangeEnd() Node
}
var _ RangeDeclNode = (*RangeNode)(nil)
var _ RangeDeclNode = (*NoSourceNode)(nil)
// RangeNode represents a range expression, used in both extension ranges and
// reserved ranges. Example:
//
// 1000 to max
type RangeNode struct {
compositeNode
StartVal IntValueNode
// if To is non-nil, then exactly one of EndVal or Max must also be non-nil
To *KeywordNode
// EndVal and Max are mutually exclusive
EndVal IntValueNode
Max *KeywordNode
}
// NewRangeNode creates a new *RangeNode. The start argument must be non-nil.
// The to argument represents the "to" keyword. If present (i.e. if it is non-nil),
// then so must be exactly one of end or max. If max is non-nil, it indicates a
// "100 to max" style range. But if end is non-nil, the end of the range is a
// literal, such as "100 to 200".
func NewRangeNode(start IntValueNode, to *KeywordNode, end IntValueNode, maxEnd *KeywordNode) *RangeNode {
if start == nil {
panic("start is nil")
}
numChildren := 1
if to != nil {
if end == nil && maxEnd == nil {
panic("to is not nil, but end and max both are")
}
if end != nil && maxEnd != nil {
panic("end and max cannot be both non-nil")
}
numChildren = 3
} else {
if end != nil {
panic("to is nil, but end is not")
}
if maxEnd != nil {
panic("to is nil, but max is not")
}
}
children := make([]Node, 0, numChildren)
children = append(children, start)
if to != nil {
children = append(children, to)
if end != nil {
children = append(children, end)
} else {
children = append(children, maxEnd)
}
}
return &RangeNode{
compositeNode: compositeNode{
children: children,
},
StartVal: start,
To: to,
EndVal: end,
Max: maxEnd,
}
}
func (n *RangeNode) RangeStart() Node {
return n.StartVal
}
func (n *RangeNode) RangeEnd() Node {
if n.Max != nil {
return n.Max
}
if n.EndVal != nil {
return n.EndVal
}
return n.StartVal
}
func (n *RangeNode) StartValue() any {
return n.StartVal.Value()
}
func (n *RangeNode) StartValueAsInt32(minVal, maxVal int32) (int32, bool) {
return AsInt32(n.StartVal, minVal, maxVal)
}
func (n *RangeNode) EndValue() any {
if n.EndVal == nil {
return nil
}
return n.EndVal.Value()
}
func (n *RangeNode) EndValueAsInt32(minVal, maxVal int32) (int32, bool) {
if n.Max != nil {
return maxVal, true
}
if n.EndVal == nil {
return n.StartValueAsInt32(minVal, maxVal)
}
return AsInt32(n.EndVal, minVal, maxVal)
}
// ReservedNode represents reserved declaration, which can be used to reserve
// either names or numbers. Examples:
//
// reserved 1, 10-12, 15;
// reserved "foo", "bar", "baz";
// reserved foo, bar, baz;
type ReservedNode struct {
compositeNode
Keyword *KeywordNode
// If non-empty, this node represents reserved ranges, and Names and Identifiers
// will be empty.
Ranges []*RangeNode
// If non-empty, this node represents reserved names as string literals, and
// Ranges and Identifiers will be empty. String literals are used for reserved
// names in proto2 and proto3 syntax.
Names []StringValueNode
// If non-empty, this node represents reserved names as identifiers, and Ranges
// and Names will be empty. Identifiers are used for reserved names in editions.
Identifiers []*IdentNode
// Commas represent the separating ',' characters between options. The
// length of this slice must be exactly len(Ranges)-1 or len(Names)-1, depending
// on whether this node represents reserved ranges or reserved names. Each item
// in Ranges or Names has a corresponding item in this slice *except the last*
// (since a trailing comma is not allowed).
Commas []*RuneNode
Semicolon *RuneNode
}
func (*ReservedNode) msgElement() {}
func (*ReservedNode) enumElement() {}
// NewReservedRangesNode creates a new *ReservedNode that represents reserved
// numeric ranges. All args must be non-nil.
// - keyword: The token corresponding to the "reserved" keyword.
// - ranges: One or more range expressions.
// - commas: Tokens that represent the "," runes that delimit the range expressions.
// The length of commas must be one less than the length of ranges.
// - semicolon The token corresponding to the ";" rune that ends the declaration.
func NewReservedRangesNode(keyword *KeywordNode, ranges []*RangeNode, commas []*RuneNode, semicolon *RuneNode) *ReservedNode {
if keyword == nil {
panic("keyword is nil")
}
if semicolon == nil {
panic("semicolon is nil")
}
if len(ranges) == 0 {
panic("must have at least one range")
}
if len(commas) != len(ranges)-1 {
panic(fmt.Sprintf("%d ranges requires %d commas, not %d", len(ranges), len(ranges)-1, len(commas)))
}
children := make([]Node, 0, len(ranges)*2+1)
children = append(children, keyword)
for i, rng := range ranges {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if rng == nil {
panic(fmt.Sprintf("ranges[%d] is nil", i))
}
children = append(children, rng)
}
children = append(children, semicolon)
return &ReservedNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Ranges: ranges,
Commas: commas,
Semicolon: semicolon,
}
}
// NewReservedNamesNode creates a new *ReservedNode that represents reserved
// names. All args must be non-nil.
// - keyword: The token corresponding to the "reserved" keyword.
// - names: One or more names.
// - commas: Tokens that represent the "," runes that delimit the names.
// The length of commas must be one less than the length of names.
// - semicolon The token corresponding to the ";" rune that ends the declaration.
func NewReservedNamesNode(keyword *KeywordNode, names []StringValueNode, commas []*RuneNode, semicolon *RuneNode) *ReservedNode {
if keyword == nil {
panic("keyword is nil")
}
if len(names) == 0 {
panic("must have at least one name")
}
if len(commas) != len(names)-1 {
panic(fmt.Sprintf("%d names requires %d commas, not %d", len(names), len(names)-1, len(commas)))
}
numChildren := len(names) * 2
if semicolon != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, keyword)
for i, name := range names {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if name == nil {
panic(fmt.Sprintf("names[%d] is nil", i))
}
children = append(children, name)
}
if semicolon != nil {
children = append(children, semicolon)
}
return &ReservedNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Names: names,
Commas: commas,
Semicolon: semicolon,
}
}
// NewReservedIdentifiersNode creates a new *ReservedNode that represents reserved
// names. All args must be non-nil.
// - keyword: The token corresponding to the "reserved" keyword.
// - names: One or more names.
// - commas: Tokens that represent the "," runes that delimit the names.
// The length of commas must be one less than the length of names.
// - semicolon The token corresponding to the ";" rune that ends the declaration.
func NewReservedIdentifiersNode(keyword *KeywordNode, names []*IdentNode, commas []*RuneNode, semicolon *RuneNode) *ReservedNode {
if keyword == nil {
panic("keyword is nil")
}
if len(names) == 0 {
panic("must have at least one name")
}
if len(commas) != len(names)-1 {
panic(fmt.Sprintf("%d names requires %d commas, not %d", len(names), len(names)-1, len(commas)))
}
numChildren := len(names) * 2
if semicolon != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, keyword)
for i, name := range names {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if name == nil {
panic(fmt.Sprintf("names[%d] is nil", i))
}
children = append(children, name)
}
if semicolon != nil {
children = append(children, semicolon)
}
return &ReservedNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Identifiers: names,
Commas: commas,
Semicolon: semicolon,
}
}
// 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 ast
import "fmt"
// ServiceNode represents a service declaration. Example:
//
// service Foo {
// rpc Bar (Baz) returns (Bob);
// rpc Frobnitz (stream Parts) returns (Gyzmeaux);
// }
type ServiceNode struct {
compositeNode
Keyword *KeywordNode
Name *IdentNode
OpenBrace *RuneNode
Decls []ServiceElement
CloseBrace *RuneNode
}
func (*ServiceNode) fileElement() {}
// NewServiceNode creates a new *ServiceNode. All arguments must be non-nil.
// - keyword: The token corresponding to the "service" keyword.
// - name: The token corresponding to the service's name.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the service body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewServiceNode(keyword *KeywordNode, name *IdentNode, openBrace *RuneNode, decls []ServiceElement, closeBrace *RuneNode) *ServiceNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
children := make([]Node, 0, 4+len(decls))
children = append(children, keyword, name, openBrace)
for _, decl := range decls {
switch decl := decl.(type) {
case *OptionNode, *RPCNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid ServiceElement type: %T", decl))
}
children = append(children, decl)
}
children = append(children, closeBrace)
return &ServiceNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
OpenBrace: openBrace,
Decls: decls,
CloseBrace: closeBrace,
}
}
func (n *ServiceNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// ServiceElement is an interface implemented by all AST nodes that can
// appear in the body of a service declaration.
type ServiceElement interface {
Node
serviceElement()
}
var _ ServiceElement = (*OptionNode)(nil)
var _ ServiceElement = (*RPCNode)(nil)
var _ ServiceElement = (*EmptyDeclNode)(nil)
// RPCDeclNode is a placeholder interface for AST nodes that represent RPC
// declarations. This allows NoSourceNode to be used in place of *RPCNode
// for some usages.
type RPCDeclNode interface {
NodeWithOptions
GetName() Node
GetInputType() Node
GetOutputType() Node
}
var _ RPCDeclNode = (*RPCNode)(nil)
var _ RPCDeclNode = (*NoSourceNode)(nil)
// RPCNode represents an RPC declaration. Example:
//
// rpc Foo (Bar) returns (Baz);
type RPCNode struct {
compositeNode
Keyword *KeywordNode
Name *IdentNode
Input *RPCTypeNode
Returns *KeywordNode
Output *RPCTypeNode
Semicolon *RuneNode
OpenBrace *RuneNode
Decls []RPCElement
CloseBrace *RuneNode
}
func (n *RPCNode) serviceElement() {}
// NewRPCNode creates a new *RPCNode with no body. All arguments must be non-nil.
// - keyword: The token corresponding to the "rpc" keyword.
// - name: The token corresponding to the RPC's name.
// - input: The token corresponding to the RPC input message type.
// - returns: The token corresponding to the "returns" keyword that precedes the output type.
// - output: The token corresponding to the RPC output message type.
// - semicolon: The token corresponding to the ";" rune that ends the declaration.
func NewRPCNode(keyword *KeywordNode, name *IdentNode, input *RPCTypeNode, returns *KeywordNode, output *RPCTypeNode, semicolon *RuneNode) *RPCNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if input == nil {
panic("input is nil")
}
if returns == nil {
panic("returns is nil")
}
if output == nil {
panic("output is nil")
}
var children []Node
if semicolon == nil {
children = []Node{keyword, name, input, returns, output}
} else {
children = []Node{keyword, name, input, returns, output, semicolon}
}
return &RPCNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
Input: input,
Returns: returns,
Output: output,
Semicolon: semicolon,
}
}
// NewRPCNodeWithBody creates a new *RPCNode that includes a body (and possibly
// options). All arguments must be non-nil.
// - keyword: The token corresponding to the "rpc" keyword.
// - name: The token corresponding to the RPC's name.
// - input: The token corresponding to the RPC input message type.
// - returns: The token corresponding to the "returns" keyword that precedes the output type.
// - output: The token corresponding to the RPC output message type.
// - openBrace: The token corresponding to the "{" rune that starts the body.
// - decls: All declarations inside the RPC body.
// - closeBrace: The token corresponding to the "}" rune that ends the body.
func NewRPCNodeWithBody(keyword *KeywordNode, name *IdentNode, input *RPCTypeNode, returns *KeywordNode, output *RPCTypeNode, openBrace *RuneNode, decls []RPCElement, closeBrace *RuneNode) *RPCNode {
if keyword == nil {
panic("keyword is nil")
}
if name == nil {
panic("name is nil")
}
if input == nil {
panic("input is nil")
}
if returns == nil {
panic("returns is nil")
}
if output == nil {
panic("output is nil")
}
if openBrace == nil {
panic("openBrace is nil")
}
if closeBrace == nil {
panic("closeBrace is nil")
}
children := make([]Node, 0, 7+len(decls))
children = append(children, keyword, name, input, returns, output, openBrace)
for _, decl := range decls {
switch decl := decl.(type) {
case *OptionNode, *EmptyDeclNode:
default:
panic(fmt.Sprintf("invalid RPCElement type: %T", decl))
}
children = append(children, decl)
}
children = append(children, closeBrace)
return &RPCNode{
compositeNode: compositeNode{
children: children,
},
Keyword: keyword,
Name: name,
Input: input,
Returns: returns,
Output: output,
OpenBrace: openBrace,
Decls: decls,
CloseBrace: closeBrace,
}
}
func (n *RPCNode) GetName() Node {
return n.Name
}
func (n *RPCNode) GetInputType() Node {
return n.Input.MessageType
}
func (n *RPCNode) GetOutputType() Node {
return n.Output.MessageType
}
func (n *RPCNode) RangeOptions(fn func(*OptionNode) bool) {
for _, decl := range n.Decls {
if opt, ok := decl.(*OptionNode); ok {
if !fn(opt) {
return
}
}
}
}
// RPCElement is an interface implemented by all AST nodes that can
// appear in the body of an rpc declaration (aka method).
type RPCElement interface {
Node
methodElement()
}
var _ RPCElement = (*OptionNode)(nil)
var _ RPCElement = (*EmptyDeclNode)(nil)
// RPCTypeNode represents the declaration of a request or response type for an
// RPC. Example:
//
// (stream foo.Bar)
type RPCTypeNode struct {
compositeNode
OpenParen *RuneNode
Stream *KeywordNode
MessageType IdentValueNode
CloseParen *RuneNode
}
// NewRPCTypeNode creates a new *RPCTypeNode. All arguments must be non-nil
// except stream, which may be nil.
// - openParen: The token corresponding to the "(" rune that starts the declaration.
// - stream: The token corresponding to the "stream" keyword or nil if not present.
// - msgType: The token corresponding to the message type's name.
// - closeParen: The token corresponding to the ")" rune that ends the declaration.
func NewRPCTypeNode(openParen *RuneNode, stream *KeywordNode, msgType IdentValueNode, closeParen *RuneNode) *RPCTypeNode {
if openParen == nil {
panic("openParen is nil")
}
if msgType == nil {
panic("msgType is nil")
}
if closeParen == nil {
panic("closeParen is nil")
}
var children []Node
if stream != nil {
children = []Node{openParen, stream, msgType, closeParen}
} else {
children = []Node{openParen, msgType, closeParen}
}
return &RPCTypeNode{
compositeNode: compositeNode{
children: children,
},
OpenParen: openParen,
Stream: stream,
MessageType: msgType,
CloseParen: closeParen,
}
}
// 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 ast
import (
"fmt"
"math"
"strings"
)
// ValueNode is an AST node that represents a literal value.
//
// It also includes references (e.g. IdentifierValueNode), which can be
// used as values in some contexts, such as describing the default value
// for a field, which can refer to an enum value.
//
// This also allows NoSourceNode to be used in place of a real value node
// for some usages.
type ValueNode interface {
Node
// Value returns a Go representation of the value. For scalars, this
// will be a string, int64, uint64, float64, or bool. This could also
// be an Identifier (e.g. IdentValueNodes). It can also be a composite
// literal:
// * For array literals, the type returned will be []ValueNode
// * For message literals, the type returned will be []*MessageFieldNode
//
// If the ValueNode is a NoSourceNode, indicating that there is no actual
// source code (and thus not AST information), then this method always
// returns nil.
Value() any
}
var _ ValueNode = (*IdentNode)(nil)
var _ ValueNode = (*CompoundIdentNode)(nil)
var _ ValueNode = (*StringLiteralNode)(nil)
var _ ValueNode = (*CompoundStringLiteralNode)(nil)
var _ ValueNode = (*UintLiteralNode)(nil)
var _ ValueNode = (*NegativeIntLiteralNode)(nil)
var _ ValueNode = (*FloatLiteralNode)(nil)
var _ ValueNode = (*SpecialFloatLiteralNode)(nil)
var _ ValueNode = (*SignedFloatLiteralNode)(nil)
var _ ValueNode = (*ArrayLiteralNode)(nil)
var _ ValueNode = (*MessageLiteralNode)(nil)
var _ ValueNode = (*NoSourceNode)(nil)
// StringValueNode is an AST node that represents a string literal.
// Such a node can be a single literal (*StringLiteralNode) or a
// concatenation of multiple literals (*CompoundStringLiteralNode).
type StringValueNode interface {
ValueNode
AsString() string
}
var _ StringValueNode = (*StringLiteralNode)(nil)
var _ StringValueNode = (*CompoundStringLiteralNode)(nil)
// StringLiteralNode represents a simple string literal. Example:
//
// "proto2"
type StringLiteralNode struct {
terminalNode
// Val is the actual string value that the literal indicates.
Val string
}
// NewStringLiteralNode creates a new *StringLiteralNode with the given val.
func NewStringLiteralNode(val string, tok Token) *StringLiteralNode {
return &StringLiteralNode{
terminalNode: tok.asTerminalNode(),
Val: val,
}
}
func (n *StringLiteralNode) Value() any {
return n.AsString()
}
func (n *StringLiteralNode) AsString() string {
return n.Val
}
// CompoundStringLiteralNode represents a compound string literal, which is
// the concatenaton of adjacent string literals. Example:
//
// "this " "is" " all one " "string"
type CompoundStringLiteralNode struct {
compositeNode
Val string
}
// NewCompoundLiteralStringNode creates a new *CompoundStringLiteralNode that
// consists of the given string components. The components argument may not be
// empty.
func NewCompoundLiteralStringNode(components ...*StringLiteralNode) *CompoundStringLiteralNode {
if len(components) == 0 {
panic("must have at least one component")
}
children := make([]Node, len(components))
var b strings.Builder
for i, comp := range components {
children[i] = comp
b.WriteString(comp.Val)
}
return &CompoundStringLiteralNode{
compositeNode: compositeNode{
children: children,
},
Val: b.String(),
}
}
func (n *CompoundStringLiteralNode) Value() any {
return n.AsString()
}
func (n *CompoundStringLiteralNode) AsString() string {
return n.Val
}
// IntValueNode is an AST node that represents an integer literal. If
// an integer literal is too large for an int64 (or uint64 for
// positive literals), it is represented instead by a FloatValueNode.
type IntValueNode interface {
ValueNode
AsInt64() (int64, bool)
AsUint64() (uint64, bool)
}
// AsInt32 range checks the given int value and returns its value is
// in the range or 0, false if it is outside the range.
func AsInt32(n IntValueNode, minVal, maxVal int32) (int32, bool) {
i, ok := n.AsInt64()
if !ok {
return 0, false
}
if i < int64(minVal) || i > int64(maxVal) {
return 0, false
}
return int32(i), true
}
var _ IntValueNode = (*UintLiteralNode)(nil)
var _ IntValueNode = (*NegativeIntLiteralNode)(nil)
// UintLiteralNode represents a simple integer literal with no sign character.
type UintLiteralNode struct {
terminalNode
// Val is the numeric value indicated by the literal
Val uint64
}
// NewUintLiteralNode creates a new *UintLiteralNode with the given val.
func NewUintLiteralNode(val uint64, tok Token) *UintLiteralNode {
return &UintLiteralNode{
terminalNode: tok.asTerminalNode(),
Val: val,
}
}
func (n *UintLiteralNode) Value() any {
return n.Val
}
func (n *UintLiteralNode) AsInt64() (int64, bool) {
if n.Val > math.MaxInt64 {
return 0, false
}
return int64(n.Val), true
}
func (n *UintLiteralNode) AsUint64() (uint64, bool) {
return n.Val, true
}
func (n *UintLiteralNode) AsFloat() float64 {
return float64(n.Val)
}
// NegativeIntLiteralNode represents an integer literal with a negative (-) sign.
type NegativeIntLiteralNode struct {
compositeNode
Minus *RuneNode
Uint *UintLiteralNode
Val int64
}
// NewNegativeIntLiteralNode creates a new *NegativeIntLiteralNode. Both
// arguments must be non-nil.
func NewNegativeIntLiteralNode(sign *RuneNode, i *UintLiteralNode) *NegativeIntLiteralNode {
if sign == nil {
panic("sign is nil")
}
if i == nil {
panic("i is nil")
}
children := []Node{sign, i}
return &NegativeIntLiteralNode{
compositeNode: compositeNode{
children: children,
},
Minus: sign,
Uint: i,
Val: -int64(i.Val),
}
}
func (n *NegativeIntLiteralNode) Value() any {
return n.Val
}
func (n *NegativeIntLiteralNode) AsInt64() (int64, bool) {
return n.Val, true
}
func (n *NegativeIntLiteralNode) AsUint64() (uint64, bool) {
if n.Val < 0 {
return 0, false
}
return uint64(n.Val), true
}
// FloatValueNode is an AST node that represents a numeric literal with
// a floating point, in scientific notation, or too large to fit in an
// int64 or uint64.
type FloatValueNode interface {
ValueNode
AsFloat() float64
}
var _ FloatValueNode = (*FloatLiteralNode)(nil)
var _ FloatValueNode = (*SpecialFloatLiteralNode)(nil)
var _ FloatValueNode = (*UintLiteralNode)(nil)
// FloatLiteralNode represents a floating point numeric literal.
type FloatLiteralNode struct {
terminalNode
// Val is the numeric value indicated by the literal
Val float64
}
// NewFloatLiteralNode creates a new *FloatLiteralNode with the given val.
func NewFloatLiteralNode(val float64, tok Token) *FloatLiteralNode {
return &FloatLiteralNode{
terminalNode: tok.asTerminalNode(),
Val: val,
}
}
func (n *FloatLiteralNode) Value() any {
return n.AsFloat()
}
func (n *FloatLiteralNode) AsFloat() float64 {
return n.Val
}
// SpecialFloatLiteralNode represents a special floating point numeric literal
// for "inf" and "nan" values.
type SpecialFloatLiteralNode struct {
*KeywordNode
Val float64
}
// NewSpecialFloatLiteralNode returns a new *SpecialFloatLiteralNode for the
// given keyword. The given keyword should be "inf", "infinity", or "nan"
// in any case.
func NewSpecialFloatLiteralNode(name *KeywordNode) *SpecialFloatLiteralNode {
var f float64
switch strings.ToLower(name.Val) {
case "inf", "infinity":
f = math.Inf(1)
default:
f = math.NaN()
}
return &SpecialFloatLiteralNode{
KeywordNode: name,
Val: f,
}
}
func (n *SpecialFloatLiteralNode) Value() any {
return n.AsFloat()
}
func (n *SpecialFloatLiteralNode) AsFloat() float64 {
return n.Val
}
// SignedFloatLiteralNode represents a signed floating point number.
type SignedFloatLiteralNode struct {
compositeNode
Sign *RuneNode
Float FloatValueNode
Val float64
}
// NewSignedFloatLiteralNode creates a new *SignedFloatLiteralNode. Both
// arguments must be non-nil.
func NewSignedFloatLiteralNode(sign *RuneNode, f FloatValueNode) *SignedFloatLiteralNode {
if sign == nil {
panic("sign is nil")
}
if f == nil {
panic("f is nil")
}
children := []Node{sign, f}
val := f.AsFloat()
if sign.Rune == '-' {
val = -val
}
return &SignedFloatLiteralNode{
compositeNode: compositeNode{
children: children,
},
Sign: sign,
Float: f,
Val: val,
}
}
func (n *SignedFloatLiteralNode) Value() any {
return n.Val
}
func (n *SignedFloatLiteralNode) AsFloat() float64 {
return n.Val
}
// ArrayLiteralNode represents an array literal, which is only allowed inside of
// a MessageLiteralNode, to indicate values for a repeated field. Example:
//
// ["foo", "bar", "baz"]
type ArrayLiteralNode struct {
compositeNode
OpenBracket *RuneNode
Elements []ValueNode
// Commas represent the separating ',' characters between elements. The
// length of this slice must be exactly len(Elements)-1, with each item
// in Elements having a corresponding item in this slice *except the last*
// (since a trailing comma is not allowed).
Commas []*RuneNode
CloseBracket *RuneNode
}
// NewArrayLiteralNode creates a new *ArrayLiteralNode. The openBracket and
// closeBracket args must be non-nil and represent the "[" and "]" runes that
// surround the array values. The given commas arg must have a length that is
// one less than the length of the vals arg. However, vals may be empty, in
// which case commas must also be empty.
func NewArrayLiteralNode(openBracket *RuneNode, vals []ValueNode, commas []*RuneNode, closeBracket *RuneNode) *ArrayLiteralNode {
if openBracket == nil {
panic("openBracket is nil")
}
if closeBracket == nil {
panic("closeBracket is nil")
}
if len(vals) == 0 && len(commas) != 0 {
panic("vals is empty but commas is not")
}
if len(vals) > 0 && len(commas) != len(vals)-1 {
panic(fmt.Sprintf("%d vals requires %d commas, not %d", len(vals), len(vals)-1, len(commas)))
}
children := make([]Node, 0, len(vals)*2+1)
children = append(children, openBracket)
for i, val := range vals {
if i > 0 {
if commas[i-1] == nil {
panic(fmt.Sprintf("commas[%d] is nil", i-1))
}
children = append(children, commas[i-1])
}
if val == nil {
panic(fmt.Sprintf("vals[%d] is nil", i))
}
children = append(children, val)
}
children = append(children, closeBracket)
return &ArrayLiteralNode{
compositeNode: compositeNode{
children: children,
},
OpenBracket: openBracket,
Elements: vals,
Commas: commas,
CloseBracket: closeBracket,
}
}
func (n *ArrayLiteralNode) Value() any {
return n.Elements
}
// MessageLiteralNode represents a message literal, which is compatible with the
// protobuf text format and can be used for custom options with message types.
// Example:
//
// { foo:1 foo:2 foo:3 bar:<name:"abc" id:123> }
type MessageLiteralNode struct {
compositeNode
Open *RuneNode // should be '{' or '<'
Elements []*MessageFieldNode
// Separator characters between elements, which can be either ','
// or ';' if present. This slice must be exactly len(Elements) in
// length, with each item in Elements having one corresponding item
// in Seps. Separators in message literals are optional, so a given
// item in this slice may be nil to indicate absence of a separator.
Seps []*RuneNode
Close *RuneNode // should be '}' or '>', depending on Open
}
// NewMessageLiteralNode creates a new *MessageLiteralNode. The openSym and
// closeSym runes must not be nil and should be "{" and "}" or "<" and ">".
//
// Unlike separators (dots and commas) used for other AST nodes that represent
// a list of elements, the seps arg must be the SAME length as vals, and it may
// contain nil values to indicate absence of a separator (in fact, it could be
// all nils).
func NewMessageLiteralNode(openSym *RuneNode, vals []*MessageFieldNode, seps []*RuneNode, closeSym *RuneNode) *MessageLiteralNode {
if openSym == nil {
panic("openSym is nil")
}
if closeSym == nil {
panic("closeSym is nil")
}
if len(seps) != len(vals) {
panic(fmt.Sprintf("%d vals requires %d commas, not %d", len(vals), len(vals), len(seps)))
}
numChildren := len(vals) + 2
for _, sep := range seps {
if sep != nil {
numChildren++
}
}
children := make([]Node, 0, numChildren)
children = append(children, openSym)
for i, val := range vals {
if val == nil {
panic(fmt.Sprintf("vals[%d] is nil", i))
}
children = append(children, val)
if seps[i] != nil {
children = append(children, seps[i])
}
}
children = append(children, closeSym)
return &MessageLiteralNode{
compositeNode: compositeNode{
children: children,
},
Open: openSym,
Elements: vals,
Seps: seps,
Close: closeSym,
}
}
func (n *MessageLiteralNode) Value() any {
return n.Elements
}
// MessageFieldNode represents a single field (name and value) inside of a
// message literal. Example:
//
// foo:"bar"
type MessageFieldNode struct {
compositeNode
Name *FieldReferenceNode
// Sep represents the ':' separator between the name and value. If
// the value is a message or list literal (and thus starts with '<',
// '{', or '['), then the separator may be omitted and this field may
// be nil.
Sep *RuneNode
Val ValueNode
}
// NewMessageFieldNode creates a new *MessageFieldNode. All args except sep
// must be non-nil.
func NewMessageFieldNode(name *FieldReferenceNode, sep *RuneNode, val ValueNode) *MessageFieldNode {
if name == nil {
panic("name is nil")
}
if val == nil {
panic("val is nil")
}
numChildren := 2
if sep != nil {
numChildren++
}
children := make([]Node, 0, numChildren)
children = append(children, name)
if sep != nil {
children = append(children, sep)
}
children = append(children, val)
return &MessageFieldNode{
compositeNode: compositeNode{
children: children,
},
Name: name,
Sep: sep,
Val: val,
}
}
// 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 ast
import "fmt"
// Walk conducts a walk of the AST rooted at the given root using the
// given visitor. It performs a "pre-order traversal", visiting a
// given AST node before it visits that node's descendants.
//
// If a visitor returns an error while walking the tree, the entire
// operation is aborted and that error is returned.
func Walk(root Node, v Visitor, opts ...WalkOption) error {
var wOpts walkOptions
for _, opt := range opts {
opt(&wOpts)
}
return walk(root, v, wOpts)
}
// WalkOption represents an option used with the Walk function. These
// allow optional before and after hooks to be invoked as each node in
// the tree is visited.
type WalkOption func(*walkOptions)
type walkOptions struct {
before, after func(Node) error
}
// WithBefore returns a WalkOption that will cause the given function to be
// invoked before a node is visited during a walk operation. If this hook
// returns an error, the node is not visited and the walk operation is aborted.
func WithBefore(fn func(Node) error) WalkOption {
return func(options *walkOptions) {
options.before = fn
}
}
// WithAfter returns a WalkOption that will cause the given function to be
// invoked after a node (as well as any descendants) is visited during a walk
// operation. If this hook returns an error, the node is not visited and the
// walk operation is aborted.
//
// If the walk is aborted due to some other visitor or before hook returning an
// error, the after hook is still called for all nodes that have been visited.
// However, the walk operation fails with the first error it encountered, so any
// error returned from an after hook is effectively ignored.
func WithAfter(fn func(Node) error) WalkOption {
return func(options *walkOptions) {
options.after = fn
}
}
func walk(root Node, v Visitor, opts walkOptions) (err error) {
if opts.before != nil {
if err := opts.before(root); err != nil {
return err
}
}
if opts.after != nil {
defer func() {
if afterErr := opts.after(root); afterErr != nil {
// if another call already returned an error then we
// have to ignore the error from the after hook
if err == nil {
err = afterErr
}
}
}()
}
if err := Visit(root, v); err != nil {
return err
}
if comp, ok := root.(CompositeNode); ok {
for _, child := range comp.Children() {
if err := walk(child, v, opts); err != nil {
return err
}
}
}
return nil
}
// Visit implements the double-dispatch idiom and visits the given node by
// calling the appropriate method of the given visitor.
func Visit(n Node, v Visitor) error {
switch n := n.(type) {
case *FileNode:
return v.VisitFileNode(n)
case *SyntaxNode:
return v.VisitSyntaxNode(n)
case *EditionNode:
return v.VisitEditionNode(n)
case *PackageNode:
return v.VisitPackageNode(n)
case *ImportNode:
return v.VisitImportNode(n)
case *OptionNode:
return v.VisitOptionNode(n)
case *OptionNameNode:
return v.VisitOptionNameNode(n)
case *FieldReferenceNode:
return v.VisitFieldReferenceNode(n)
case *CompactOptionsNode:
return v.VisitCompactOptionsNode(n)
case *MessageNode:
return v.VisitMessageNode(n)
case *ExtendNode:
return v.VisitExtendNode(n)
case *ExtensionRangeNode:
return v.VisitExtensionRangeNode(n)
case *ReservedNode:
return v.VisitReservedNode(n)
case *RangeNode:
return v.VisitRangeNode(n)
case *FieldNode:
return v.VisitFieldNode(n)
case *GroupNode:
return v.VisitGroupNode(n)
case *MapFieldNode:
return v.VisitMapFieldNode(n)
case *MapTypeNode:
return v.VisitMapTypeNode(n)
case *OneofNode:
return v.VisitOneofNode(n)
case *EnumNode:
return v.VisitEnumNode(n)
case *EnumValueNode:
return v.VisitEnumValueNode(n)
case *ServiceNode:
return v.VisitServiceNode(n)
case *RPCNode:
return v.VisitRPCNode(n)
case *RPCTypeNode:
return v.VisitRPCTypeNode(n)
case *IdentNode:
return v.VisitIdentNode(n)
case *CompoundIdentNode:
return v.VisitCompoundIdentNode(n)
case *StringLiteralNode:
return v.VisitStringLiteralNode(n)
case *CompoundStringLiteralNode:
return v.VisitCompoundStringLiteralNode(n)
case *UintLiteralNode:
return v.VisitUintLiteralNode(n)
case *NegativeIntLiteralNode:
return v.VisitNegativeIntLiteralNode(n)
case *FloatLiteralNode:
return v.VisitFloatLiteralNode(n)
case *SpecialFloatLiteralNode:
return v.VisitSpecialFloatLiteralNode(n)
case *SignedFloatLiteralNode:
return v.VisitSignedFloatLiteralNode(n)
case *ArrayLiteralNode:
return v.VisitArrayLiteralNode(n)
case *MessageLiteralNode:
return v.VisitMessageLiteralNode(n)
case *MessageFieldNode:
return v.VisitMessageFieldNode(n)
case *KeywordNode:
return v.VisitKeywordNode(n)
case *RuneNode:
return v.VisitRuneNode(n)
case *EmptyDeclNode:
return v.VisitEmptyDeclNode(n)
default:
panic(fmt.Sprintf("unexpected type of node: %T", n))
}
}
// AncestorTracker is used to track the path of nodes during a walk operation.
// By passing AsWalkOptions to a call to Walk, a visitor can inspect the path to
// the node being visited using this tracker.
type AncestorTracker struct {
ancestors []Node
}
// AsWalkOptions returns WalkOption values that will cause this ancestor tracker
// to track the path through the AST during the walk operation.
func (t *AncestorTracker) AsWalkOptions() []WalkOption {
return []WalkOption{
WithBefore(func(n Node) error {
t.ancestors = append(t.ancestors, n)
return nil
}),
WithAfter(func(_ Node) error {
t.ancestors = t.ancestors[:len(t.ancestors)-1]
return nil
}),
}
}
// Path returns a slice of nodes that represents the path from the root of the
// walk operation to the currently visited node. The first element in the path
// is the root supplied to Walk. The last element in the path is the currently
// visited node.
//
// The returned slice is not a defensive copy; so callers should NOT mutate it.
func (t *AncestorTracker) Path() []Node {
return t.ancestors
}
// Parent returns the parent node of the currently visited node. If the node
// currently being visited is the root supplied to Walk then nil is returned.
func (t *AncestorTracker) Parent() Node {
if len(t.ancestors) <= 1 {
return nil
}
return t.ancestors[len(t.ancestors)-2]
}
// VisitChildren visits all direct children of the given node using the given
// visitor. If visiting a child returns an error, that error is immediately
// returned, and other children will not be visited.
func VisitChildren(n CompositeNode, v Visitor) error {
for _, ch := range n.Children() {
if err := Visit(ch, v); err != nil {
return err
}
}
return nil
}
// Visitor provides a technique for walking the AST that allows for
// dynamic dispatch, where a particular function is invoked based on
// the runtime type of the argument.
//
// It consists of a number of functions, each of which matches a
// concrete Node type.
//
// NOTE: As the language evolves, new methods may be added to this
// interface to correspond to new grammar elements. That is why it
// cannot be directly implemented outside this package. Visitor
// implementations must embed NoOpVisitor and then implement the
// subset of methods of interest. If such an implementation is used
// with an AST that has newer elements, the visitor will not do
// anything in response to the new node types.
//
// An alternative to embedding NoOpVisitor is to use an instance of
// SimpleVisitor.
//
// Visitors can be supplied to a Walk operation or passed to a call
// to Visit or VisitChildren.
//
// Note that there are some AST node types defined in this package
// that do not have corresponding visit methods. These are synthetic
// node types, that have specialized use from the parser, but never
// appear in an actual AST (which is always rooted at FileNode).
// These include SyntheticMapField, SyntheticOneof,
// SyntheticGroupMessageNode, and SyntheticMapEntryNode.
type Visitor interface {
// VisitFileNode is invoked when visiting a *FileNode in the AST.
VisitFileNode(*FileNode) error
// VisitSyntaxNode is invoked when visiting a *SyntaxNode in the AST.
VisitSyntaxNode(*SyntaxNode) error
// VisitEditionNode is invoked when visiting an *EditionNode in the AST.
VisitEditionNode(*EditionNode) error
// VisitPackageNode is invoked when visiting a *PackageNode in the AST.
VisitPackageNode(*PackageNode) error
// VisitImportNode is invoked when visiting an *ImportNode in the AST.
VisitImportNode(*ImportNode) error
// VisitOptionNode is invoked when visiting an *OptionNode in the AST.
VisitOptionNode(*OptionNode) error
// VisitOptionNameNode is invoked when visiting an *OptionNameNode in the AST.
VisitOptionNameNode(*OptionNameNode) error
// VisitFieldReferenceNode is invoked when visiting a *FieldReferenceNode in the AST.
VisitFieldReferenceNode(*FieldReferenceNode) error
// VisitCompactOptionsNode is invoked when visiting a *CompactOptionsNode in the AST.
VisitCompactOptionsNode(*CompactOptionsNode) error
// VisitMessageNode is invoked when visiting a *MessageNode in the AST.
VisitMessageNode(*MessageNode) error
// VisitExtendNode is invoked when visiting an *ExtendNode in the AST.
VisitExtendNode(*ExtendNode) error
// VisitExtensionRangeNode is invoked when visiting an *ExtensionRangeNode in the AST.
VisitExtensionRangeNode(*ExtensionRangeNode) error
// VisitReservedNode is invoked when visiting a *ReservedNode in the AST.
VisitReservedNode(*ReservedNode) error
// VisitRangeNode is invoked when visiting a *RangeNode in the AST.
VisitRangeNode(*RangeNode) error
// VisitFieldNode is invoked when visiting a *FieldNode in the AST.
VisitFieldNode(*FieldNode) error
// VisitGroupNode is invoked when visiting a *GroupNode in the AST.
VisitGroupNode(*GroupNode) error
// VisitMapFieldNode is invoked when visiting a *MapFieldNode in the AST.
VisitMapFieldNode(*MapFieldNode) error
// VisitMapTypeNode is invoked when visiting a *MapTypeNode in the AST.
VisitMapTypeNode(*MapTypeNode) error
// VisitOneofNode is invoked when visiting a *OneofNode in the AST.
VisitOneofNode(*OneofNode) error
// VisitEnumNode is invoked when visiting an *EnumNode in the AST.
VisitEnumNode(*EnumNode) error
// VisitEnumValueNode is invoked when visiting an *EnumValueNode in the AST.
VisitEnumValueNode(*EnumValueNode) error
// VisitServiceNode is invoked when visiting a *ServiceNode in the AST.
VisitServiceNode(*ServiceNode) error
// VisitRPCNode is invoked when visiting an *RPCNode in the AST.
VisitRPCNode(*RPCNode) error
// VisitRPCTypeNode is invoked when visiting an *RPCTypeNode in the AST.
VisitRPCTypeNode(*RPCTypeNode) error
// VisitIdentNode is invoked when visiting an *IdentNode in the AST.
VisitIdentNode(*IdentNode) error
// VisitCompoundIdentNode is invoked when visiting a *CompoundIdentNode in the AST.
VisitCompoundIdentNode(*CompoundIdentNode) error
// VisitStringLiteralNode is invoked when visiting a *StringLiteralNode in the AST.
VisitStringLiteralNode(*StringLiteralNode) error
// VisitCompoundStringLiteralNode is invoked when visiting a *CompoundStringLiteralNode in the AST.
VisitCompoundStringLiteralNode(*CompoundStringLiteralNode) error
// VisitUintLiteralNode is invoked when visiting a *UintLiteralNode in the AST.
VisitUintLiteralNode(*UintLiteralNode) error
// VisitNegativeIntLiteralNode is invoked when visiting a *NegativeIntLiteralNode in the AST.
VisitNegativeIntLiteralNode(*NegativeIntLiteralNode) error
// VisitFloatLiteralNode is invoked when visiting a *FloatLiteralNode in the AST.
VisitFloatLiteralNode(*FloatLiteralNode) error
// VisitSpecialFloatLiteralNode is invoked when visiting a *SpecialFloatLiteralNode in the AST.
VisitSpecialFloatLiteralNode(*SpecialFloatLiteralNode) error
// VisitSignedFloatLiteralNode is invoked when visiting a *SignedFloatLiteralNode in the AST.
VisitSignedFloatLiteralNode(*SignedFloatLiteralNode) error
// VisitArrayLiteralNode is invoked when visiting an *ArrayLiteralNode in the AST.
VisitArrayLiteralNode(*ArrayLiteralNode) error
// VisitMessageLiteralNode is invoked when visiting a *MessageLiteralNode in the AST.
VisitMessageLiteralNode(*MessageLiteralNode) error
// VisitMessageFieldNode is invoked when visiting a *MessageFieldNode in the AST.
VisitMessageFieldNode(*MessageFieldNode) error
// VisitKeywordNode is invoked when visiting a *KeywordNode in the AST.
VisitKeywordNode(*KeywordNode) error
// VisitRuneNode is invoked when visiting a *RuneNode in the AST.
VisitRuneNode(*RuneNode) error
// VisitEmptyDeclNode is invoked when visiting a *EmptyDeclNode in the AST.
VisitEmptyDeclNode(*EmptyDeclNode) error
// Unexported method prevents callers from directly implementing.
isVisitor()
}
// NoOpVisitor is a visitor implementation that does nothing. All methods
// unconditionally return nil. This can be embedded into a struct to make that
// struct implement the Visitor interface, and only the relevant visit methods
// then need to be implemented on the struct.
type NoOpVisitor struct{}
var _ Visitor = NoOpVisitor{}
func (n NoOpVisitor) isVisitor() {}
func (n NoOpVisitor) VisitFileNode(_ *FileNode) error {
return nil
}
func (n NoOpVisitor) VisitSyntaxNode(_ *SyntaxNode) error {
return nil
}
func (n NoOpVisitor) VisitEditionNode(_ *EditionNode) error {
return nil
}
func (n NoOpVisitor) VisitPackageNode(_ *PackageNode) error {
return nil
}
func (n NoOpVisitor) VisitImportNode(_ *ImportNode) error {
return nil
}
func (n NoOpVisitor) VisitOptionNode(_ *OptionNode) error {
return nil
}
func (n NoOpVisitor) VisitOptionNameNode(_ *OptionNameNode) error {
return nil
}
func (n NoOpVisitor) VisitFieldReferenceNode(_ *FieldReferenceNode) error {
return nil
}
func (n NoOpVisitor) VisitCompactOptionsNode(_ *CompactOptionsNode) error {
return nil
}
func (n NoOpVisitor) VisitMessageNode(_ *MessageNode) error {
return nil
}
func (n NoOpVisitor) VisitExtendNode(_ *ExtendNode) error {
return nil
}
func (n NoOpVisitor) VisitExtensionRangeNode(_ *ExtensionRangeNode) error {
return nil
}
func (n NoOpVisitor) VisitReservedNode(_ *ReservedNode) error {
return nil
}
func (n NoOpVisitor) VisitRangeNode(_ *RangeNode) error {
return nil
}
func (n NoOpVisitor) VisitFieldNode(_ *FieldNode) error {
return nil
}
func (n NoOpVisitor) VisitGroupNode(_ *GroupNode) error {
return nil
}
func (n NoOpVisitor) VisitMapFieldNode(_ *MapFieldNode) error {
return nil
}
func (n NoOpVisitor) VisitMapTypeNode(_ *MapTypeNode) error {
return nil
}
func (n NoOpVisitor) VisitOneofNode(_ *OneofNode) error {
return nil
}
func (n NoOpVisitor) VisitEnumNode(_ *EnumNode) error {
return nil
}
func (n NoOpVisitor) VisitEnumValueNode(_ *EnumValueNode) error {
return nil
}
func (n NoOpVisitor) VisitServiceNode(_ *ServiceNode) error {
return nil
}
func (n NoOpVisitor) VisitRPCNode(_ *RPCNode) error {
return nil
}
func (n NoOpVisitor) VisitRPCTypeNode(_ *RPCTypeNode) error {
return nil
}
func (n NoOpVisitor) VisitIdentNode(_ *IdentNode) error {
return nil
}
func (n NoOpVisitor) VisitCompoundIdentNode(_ *CompoundIdentNode) error {
return nil
}
func (n NoOpVisitor) VisitStringLiteralNode(_ *StringLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitCompoundStringLiteralNode(_ *CompoundStringLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitUintLiteralNode(_ *UintLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitNegativeIntLiteralNode(_ *NegativeIntLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitFloatLiteralNode(_ *FloatLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitSpecialFloatLiteralNode(_ *SpecialFloatLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitSignedFloatLiteralNode(_ *SignedFloatLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitArrayLiteralNode(_ *ArrayLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitMessageLiteralNode(_ *MessageLiteralNode) error {
return nil
}
func (n NoOpVisitor) VisitMessageFieldNode(_ *MessageFieldNode) error {
return nil
}
func (n NoOpVisitor) VisitKeywordNode(_ *KeywordNode) error {
return nil
}
func (n NoOpVisitor) VisitRuneNode(_ *RuneNode) error {
return nil
}
func (n NoOpVisitor) VisitEmptyDeclNode(_ *EmptyDeclNode) error {
return nil
}
// SimpleVisitor is a visitor implementation that uses numerous function fields.
// If a relevant function field is not nil, then it will be invoked when a node
// is visited.
//
// In addition to a function for each concrete node type (and thus for each
// Visit* method of the Visitor interface), it also has function fields that
// accept interface types. So a visitor can, for example, easily treat all
// ValueNodes uniformly by providing a non-nil value for DoVisitValueNode
// instead of having to supply values for the various DoVisit*Node methods
// corresponding to all types that implement ValueNode.
//
// The most specific function provided that matches a given node is the one that
// will be invoked. For example, DoVisitStringValueNode will be called if
// present and applicable before DoVisitValueNode. Similarly, DoVisitValueNode
// would be called before DoVisitTerminalNode or DoVisitCompositeNode. The
// DoVisitNode is the most generic function and is called only if no more
// specific function is present for a given node type.
//
// The *UintLiteralNode type implements both IntValueNode and FloatValueNode.
// In this case, the DoVisitIntValueNode function is considered more specific
// than DoVisitFloatValueNode, so will be preferred if present.
//
// Similarly, *MapFieldNode and *GroupNode implement both FieldDeclNode and
// MessageDeclNode. In this case, the DoVisitFieldDeclNode function is
// treated as more specific than DoVisitMessageDeclNode, so will be preferred
// if both are present.
type SimpleVisitor struct {
DoVisitFileNode func(*FileNode) error
DoVisitSyntaxNode func(*SyntaxNode) error
DoVisitEditionNode func(*EditionNode) error
DoVisitPackageNode func(*PackageNode) error
DoVisitImportNode func(*ImportNode) error
DoVisitOptionNode func(*OptionNode) error
DoVisitOptionNameNode func(*OptionNameNode) error
DoVisitFieldReferenceNode func(*FieldReferenceNode) error
DoVisitCompactOptionsNode func(*CompactOptionsNode) error
DoVisitMessageNode func(*MessageNode) error
DoVisitExtendNode func(*ExtendNode) error
DoVisitExtensionRangeNode func(*ExtensionRangeNode) error
DoVisitReservedNode func(*ReservedNode) error
DoVisitRangeNode func(*RangeNode) error
DoVisitFieldNode func(*FieldNode) error
DoVisitGroupNode func(*GroupNode) error
DoVisitMapFieldNode func(*MapFieldNode) error
DoVisitMapTypeNode func(*MapTypeNode) error
DoVisitOneofNode func(*OneofNode) error
DoVisitEnumNode func(*EnumNode) error
DoVisitEnumValueNode func(*EnumValueNode) error
DoVisitServiceNode func(*ServiceNode) error
DoVisitRPCNode func(*RPCNode) error
DoVisitRPCTypeNode func(*RPCTypeNode) error
DoVisitIdentNode func(*IdentNode) error
DoVisitCompoundIdentNode func(*CompoundIdentNode) error
DoVisitStringLiteralNode func(*StringLiteralNode) error
DoVisitCompoundStringLiteralNode func(*CompoundStringLiteralNode) error
DoVisitUintLiteralNode func(*UintLiteralNode) error
DoVisitNegativeIntLiteralNode func(*NegativeIntLiteralNode) error
DoVisitFloatLiteralNode func(*FloatLiteralNode) error
DoVisitSpecialFloatLiteralNode func(*SpecialFloatLiteralNode) error
DoVisitSignedFloatLiteralNode func(*SignedFloatLiteralNode) error
DoVisitArrayLiteralNode func(*ArrayLiteralNode) error
DoVisitMessageLiteralNode func(*MessageLiteralNode) error
DoVisitMessageFieldNode func(*MessageFieldNode) error
DoVisitKeywordNode func(*KeywordNode) error
DoVisitRuneNode func(*RuneNode) error
DoVisitEmptyDeclNode func(*EmptyDeclNode) error
DoVisitFieldDeclNode func(FieldDeclNode) error
DoVisitMessageDeclNode func(MessageDeclNode) error
DoVisitIdentValueNode func(IdentValueNode) error
DoVisitStringValueNode func(StringValueNode) error
DoVisitIntValueNode func(IntValueNode) error
DoVisitFloatValueNode func(FloatValueNode) error
DoVisitValueNode func(ValueNode) error
DoVisitTerminalNode func(TerminalNode) error
DoVisitCompositeNode func(CompositeNode) error
DoVisitNode func(Node) error
}
var _ Visitor = (*SimpleVisitor)(nil)
func (v *SimpleVisitor) isVisitor() {}
func (v *SimpleVisitor) visitInterface(node Node) error {
switch n := node.(type) {
case FieldDeclNode:
if v.DoVisitFieldDeclNode != nil {
return v.DoVisitFieldDeclNode(n)
}
// *MapFieldNode and *GroupNode both implement both FieldDeclNode and
// MessageDeclNode, so handle other case here
if fn, ok := n.(MessageDeclNode); ok && v.DoVisitMessageDeclNode != nil {
return v.DoVisitMessageDeclNode(fn)
}
case MessageDeclNode:
if v.DoVisitMessageDeclNode != nil {
return v.DoVisitMessageDeclNode(n)
}
case IdentValueNode:
if v.DoVisitIdentValueNode != nil {
return v.DoVisitIdentValueNode(n)
}
case StringValueNode:
if v.DoVisitStringValueNode != nil {
return v.DoVisitStringValueNode(n)
}
case IntValueNode:
if v.DoVisitIntValueNode != nil {
return v.DoVisitIntValueNode(n)
}
// *UintLiteralNode implements both IntValueNode and FloatValueNode,
// so handle other case here
if fn, ok := n.(FloatValueNode); ok && v.DoVisitFloatValueNode != nil {
return v.DoVisitFloatValueNode(fn)
}
case FloatValueNode:
if v.DoVisitFloatValueNode != nil {
return v.DoVisitFloatValueNode(n)
}
}
if n, ok := node.(ValueNode); ok && v.DoVisitValueNode != nil {
return v.DoVisitValueNode(n)
}
switch n := node.(type) {
case TerminalNode:
if v.DoVisitTerminalNode != nil {
return v.DoVisitTerminalNode(n)
}
case CompositeNode:
if v.DoVisitCompositeNode != nil {
return v.DoVisitCompositeNode(n)
}
}
if v.DoVisitNode != nil {
return v.DoVisitNode(node)
}
return nil
}
func (v *SimpleVisitor) VisitFileNode(node *FileNode) error {
if v.DoVisitFileNode != nil {
return v.DoVisitFileNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitSyntaxNode(node *SyntaxNode) error {
if v.DoVisitSyntaxNode != nil {
return v.DoVisitSyntaxNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitEditionNode(node *EditionNode) error {
if v.DoVisitEditionNode != nil {
return v.DoVisitEditionNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitPackageNode(node *PackageNode) error {
if v.DoVisitPackageNode != nil {
return v.DoVisitPackageNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitImportNode(node *ImportNode) error {
if v.DoVisitImportNode != nil {
return v.DoVisitImportNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitOptionNode(node *OptionNode) error {
if v.DoVisitOptionNode != nil {
return v.DoVisitOptionNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitOptionNameNode(node *OptionNameNode) error {
if v.DoVisitOptionNameNode != nil {
return v.DoVisitOptionNameNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitFieldReferenceNode(node *FieldReferenceNode) error {
if v.DoVisitFieldReferenceNode != nil {
return v.DoVisitFieldReferenceNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitCompactOptionsNode(node *CompactOptionsNode) error {
if v.DoVisitCompactOptionsNode != nil {
return v.DoVisitCompactOptionsNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitMessageNode(node *MessageNode) error {
if v.DoVisitMessageNode != nil {
return v.DoVisitMessageNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitExtendNode(node *ExtendNode) error {
if v.DoVisitExtendNode != nil {
return v.DoVisitExtendNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitExtensionRangeNode(node *ExtensionRangeNode) error {
if v.DoVisitExtensionRangeNode != nil {
return v.DoVisitExtensionRangeNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitReservedNode(node *ReservedNode) error {
if v.DoVisitReservedNode != nil {
return v.DoVisitReservedNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitRangeNode(node *RangeNode) error {
if v.DoVisitRangeNode != nil {
return v.DoVisitRangeNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitFieldNode(node *FieldNode) error {
if v.DoVisitFieldNode != nil {
return v.DoVisitFieldNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitGroupNode(node *GroupNode) error {
if v.DoVisitGroupNode != nil {
return v.DoVisitGroupNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitMapFieldNode(node *MapFieldNode) error {
if v.DoVisitMapFieldNode != nil {
return v.DoVisitMapFieldNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitMapTypeNode(node *MapTypeNode) error {
if v.DoVisitMapTypeNode != nil {
return v.DoVisitMapTypeNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitOneofNode(node *OneofNode) error {
if v.DoVisitOneofNode != nil {
return v.DoVisitOneofNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitEnumNode(node *EnumNode) error {
if v.DoVisitEnumNode != nil {
return v.DoVisitEnumNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitEnumValueNode(node *EnumValueNode) error {
if v.DoVisitEnumValueNode != nil {
return v.DoVisitEnumValueNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitServiceNode(node *ServiceNode) error {
if v.DoVisitServiceNode != nil {
return v.DoVisitServiceNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitRPCNode(node *RPCNode) error {
if v.DoVisitRPCNode != nil {
return v.DoVisitRPCNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitRPCTypeNode(node *RPCTypeNode) error {
if v.DoVisitRPCTypeNode != nil {
return v.DoVisitRPCTypeNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitIdentNode(node *IdentNode) error {
if v.DoVisitIdentNode != nil {
return v.DoVisitIdentNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitCompoundIdentNode(node *CompoundIdentNode) error {
if v.DoVisitCompoundIdentNode != nil {
return v.DoVisitCompoundIdentNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitStringLiteralNode(node *StringLiteralNode) error {
if v.DoVisitStringLiteralNode != nil {
return v.DoVisitStringLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitCompoundStringLiteralNode(node *CompoundStringLiteralNode) error {
if v.DoVisitCompoundStringLiteralNode != nil {
return v.DoVisitCompoundStringLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitUintLiteralNode(node *UintLiteralNode) error {
if v.DoVisitUintLiteralNode != nil {
return v.DoVisitUintLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitNegativeIntLiteralNode(node *NegativeIntLiteralNode) error {
if v.DoVisitNegativeIntLiteralNode != nil {
return v.DoVisitNegativeIntLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitFloatLiteralNode(node *FloatLiteralNode) error {
if v.DoVisitFloatLiteralNode != nil {
return v.DoVisitFloatLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitSpecialFloatLiteralNode(node *SpecialFloatLiteralNode) error {
if v.DoVisitSpecialFloatLiteralNode != nil {
return v.DoVisitSpecialFloatLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitSignedFloatLiteralNode(node *SignedFloatLiteralNode) error {
if v.DoVisitSignedFloatLiteralNode != nil {
return v.DoVisitSignedFloatLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitArrayLiteralNode(node *ArrayLiteralNode) error {
if v.DoVisitArrayLiteralNode != nil {
return v.DoVisitArrayLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitMessageLiteralNode(node *MessageLiteralNode) error {
if v.DoVisitMessageLiteralNode != nil {
return v.DoVisitMessageLiteralNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitMessageFieldNode(node *MessageFieldNode) error {
if v.DoVisitMessageFieldNode != nil {
return v.DoVisitMessageFieldNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitKeywordNode(node *KeywordNode) error {
if v.DoVisitKeywordNode != nil {
return v.DoVisitKeywordNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitRuneNode(node *RuneNode) error {
if v.DoVisitRuneNode != nil {
return v.DoVisitRuneNode(node)
}
return v.visitInterface(node)
}
func (v *SimpleVisitor) VisitEmptyDeclNode(node *EmptyDeclNode) error {
if v.DoVisitEmptyDeclNode != nil {
return v.DoVisitEmptyDeclNode(node)
}
return v.visitInterface(node)
}
// 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 protocompile
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"runtime"
"runtime/debug"
"strings"
"sync"
"golang.org/x/sync/semaphore"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/linker"
"github.com/bufbuild/protocompile/options"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/sourceinfo"
)
// Compiler handles compilation tasks, to turn protobuf source files, or other
// intermediate representations, into fully linked descriptors.
//
// The compilation process involves five steps for each protobuf source file:
// 1. Parsing the source into an AST (abstract syntax tree).
// 2. Converting the AST into descriptor protos.
// 3. Linking descriptor protos into fully linked descriptors.
// 4. Interpreting options.
// 5. Computing source code information.
//
// With fully linked descriptors, code generators and protoc plugins could be
// invoked (though that step is not implemented by this package and not a
// responsibility of this type).
type Compiler struct {
// Resolves path/file names into source code or intermediate representations
// for protobuf source files. This is how the compiler loads the files to
// be compiled as well as all dependencies. This field is the only required
// field.
Resolver Resolver
// The maximum parallelism to use when compiling. If unspecified or set to
// a non-positive value, then min(runtime.NumCPU(), runtime.GOMAXPROCS(-1))
// will be used.
MaxParallelism int
// A custom error and warning reporter. If unspecified a default reporter
// is used. A default reporter fails the compilation after encountering any
// errors and ignores all warnings.
Reporter reporter.Reporter
// If unspecified or set to SourceInfoNone, source code information will not
// be included in the resulting descriptors. Source code information is
// metadata in the file descriptor that provides position information (i.e.
// the line and column where file elements were defined) as well as comments.
//
// If set to SourceInfoStandard, normal source code information will be
// included in the resulting descriptors. This matches the output of protoc
// (the reference compiler for Protocol Buffers). If set to
// SourceInfoMoreComments, the resulting descriptor will attempt to preserve
// as many comments as possible, for all elements in the file, not just for
// complete declarations.
//
// If Resolver returns descriptors or descriptor protos for a file, then
// those descriptors will not be modified. If they do not already include
// source code info, they will be left that way when the compile operation
// concludes. Similarly, if they already have source code info but this flag
// is false, existing info will be left in place.
SourceInfoMode SourceInfoMode
// If true, ASTs are retained in compilation results for which an AST was
// constructed. So any linker.Result value in the resulting compiled files
// will have an AST, in addition to descriptors. If left false, the AST
// will be removed as soon as it's no longer needed. This can help reduce
// total memory usage for operations involving a large number of files.
RetainASTs bool
// If non-nil, the set of symbols already known. Any symbols in the current
// compilation will be added to it. If the compilation tries to redefine any
// of these symbols, it will be reported as a collision.
//
// This allows a large compilation to be split up into multiple, smaller
// operations and still be able to identify naming collisions and extension
// number collisions across all operations.
Symbols *linker.Symbols
}
// SourceInfoMode indicates how source code info is generated by a Compiler.
type SourceInfoMode int
const (
// SourceInfoNone indicates that no source code info is generated.
SourceInfoNone = SourceInfoMode(0)
// SourceInfoStandard indicates that the standard source code info is
// generated, which includes comments only for complete declarations.
SourceInfoStandard = SourceInfoMode(1)
// SourceInfoExtraComments indicates that source code info is generated
// and will include comments for all elements (more comments than would
// be found in a descriptor produced by protoc).
SourceInfoExtraComments = SourceInfoMode(2)
// SourceInfoExtraOptionLocations indicates that source code info is
// generated with additional locations for elements inside of message
// literals in option values. This can be combined with the above by
// bitwise-OR'ing it with SourceInfoExtraComments.
SourceInfoExtraOptionLocations = SourceInfoMode(4)
)
// Compile compiles the given file names into fully-linked descriptors. The
// compiler's resolver is used to locate source code (or intermediate artifacts
// such as parsed ASTs or descriptor protos) and then do what is necessary to
// transform that into descriptors (parsing, linking, etc).
//
// Elements in the given returned files will implement [linker.Result] if the
// compiler had to link it (i.e. the resolver provided either a descriptor proto
// or source code). That result will contain a full AST for the file if the
// compiler had to parse it (i.e. the resolver provided source code for that
// file).
func (c *Compiler) Compile(ctx context.Context, files ...string) (linker.Files, error) {
if len(files) == 0 {
return nil, nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
par := c.MaxParallelism
if par <= 0 {
par = runtime.GOMAXPROCS(-1)
cpus := runtime.NumCPU()
if par > cpus {
par = cpus
}
}
h := reporter.NewHandler(c.Reporter)
sym := c.Symbols
if sym == nil {
sym = &linker.Symbols{}
}
e := executor{
c: c,
h: h,
s: semaphore.NewWeighted(int64(par)),
cancel: cancel,
sym: sym,
results: map[string]*result{},
}
// We lock now and create all tasks under lock to make sure that no
// async task can create a duplicate result. For example, if files
// contains both "foo.proto" and "bar.proto", then there is a race
// after we start compiling "foo.proto" between this loop and the
// async compilation task to create the result for "bar.proto". But
// we need to know if the file is directly requested for compilation,
// so we need this loop to define the result. So this loop holds the
// lock the whole time so async tasks can't create a result first.
results := make([]*result, len(files))
func() {
e.mu.Lock()
defer e.mu.Unlock()
for i, f := range files {
results[i] = e.compileLocked(ctx, f, true)
}
}()
descs := make([]linker.File, len(files))
var firstError error
for i, r := range results {
select {
case <-r.ready:
case <-ctx.Done():
return nil, ctx.Err()
}
if r.err != nil {
if firstError == nil {
firstError = r.err
}
}
descs[i] = r.res
}
if err := h.Error(); err != nil {
return descs, err
}
// this should probably never happen; if any task returned an
// error, h.Error() should be non-nil
return descs, firstError
}
type result struct {
name string
ready chan struct{}
// true if this file was explicitly provided to the compiler; otherwise
// this file is an import that is implicitly included
explicitFile bool
// produces a linker.File or error, only available when ready is closed
res linker.File
err error
mu sync.Mutex
// the results that are dependencies of this result; this result is
// blocked, waiting on these dependencies to complete
blockedOn []string
}
func (r *result) fail(err error) {
r.err = err
close(r.ready)
}
func (r *result) complete(f linker.File) {
r.res = f
close(r.ready)
}
func (r *result) setBlockedOn(deps []string) {
r.mu.Lock()
defer r.mu.Unlock()
r.blockedOn = deps
}
func (r *result) getBlockedOn() []string {
r.mu.Lock()
defer r.mu.Unlock()
return r.blockedOn
}
type executor struct {
c *Compiler
h *reporter.Handler
s *semaphore.Weighted
cancel context.CancelFunc
sym *linker.Symbols
descriptorProtoCheck sync.Once
descriptorProtoIsCustom bool
mu sync.Mutex
results map[string]*result
}
func (e *executor) compile(ctx context.Context, file string) *result {
e.mu.Lock()
defer e.mu.Unlock()
return e.compileLocked(ctx, file, false)
}
func (e *executor) compileLocked(ctx context.Context, file string, explicitFile bool) *result {
r := e.results[file]
if r != nil {
return r
}
r = &result{
name: file,
ready: make(chan struct{}),
explicitFile: explicitFile,
}
e.results[file] = r
go func() {
defer func() {
if p := recover(); p != nil {
if r.err == nil {
// TODO: strip top frames from stack trace so that the panic is
// the top of the trace?
panicErr := PanicError{File: file, Value: p, Stack: string(debug.Stack())}
r.fail(panicErr)
}
// TODO: if r.err != nil, then this task has already
// failed and there's nothing we can really do to
// communicate this panic to parent goroutine. This
// means the panic must have happened *after* the
// failure was already recorded (or during?)
// It would be nice to do something else here, like
// send the compiler an out-of-band error? Or log?
}
}()
e.doCompile(ctx, file, r)
}()
return r
}
// PanicError is an error value that represents a recovered panic. It includes
// the value returned by recover() as well as the stack trace.
//
// This should generally only be seen if a Resolver implementation panics.
//
// An error returned by a Compiler may wrap a PanicError, so you may need to
// use errors.As(...) to access panic details.
type PanicError struct {
// The file that was being processed when the panic occurred
File string
// The value returned by recover()
Value any
// A formatted stack trace
Stack string
}
// Error implements the error interface. It does NOT include the stack trace.
// Use a type assertion and query the Stack field directly to access that.
func (p PanicError) Error() string {
return fmt.Sprintf("panic handling %q: %v", p.File, p.Value)
}
type errFailedToResolve struct {
err error
path string
}
func (e errFailedToResolve) Error() string {
errMsg := e.err.Error()
if strings.Contains(errMsg, e.path) {
// underlying error already refers to path in question, so we don't need to add more context
return errMsg
}
return fmt.Sprintf("could not resolve path %q: %s", e.path, e.err.Error())
}
func (e errFailedToResolve) Unwrap() error {
return e.err
}
func (e *executor) hasOverrideDescriptorProto() bool {
e.descriptorProtoCheck.Do(func() {
defer func() {
// ignore a panic here; just assume no custom descriptor.proto
_ = recover()
}()
res, err := e.c.Resolver.FindFileByPath(descriptorProtoPath)
e.descriptorProtoIsCustom = err == nil && res.Desc != standardImports[descriptorProtoPath]
})
return e.descriptorProtoIsCustom
}
func (e *executor) doCompile(ctx context.Context, file string, r *result) {
t := task{e: e, h: e.h.SubHandler(), r: r}
if err := e.s.Acquire(ctx, 1); err != nil {
r.fail(err)
return
}
defer t.release()
sr, err := e.c.Resolver.FindFileByPath(file)
if err != nil {
r.fail(errFailedToResolve{err: err, path: file})
return
}
defer func() {
// if results included a result, don't leave it open if it can be closed
if sr.Source == nil {
return
}
if c, ok := sr.Source.(io.Closer); ok {
_ = c.Close()
}
}()
desc, err := t.asFile(ctx, file, sr)
if err != nil {
r.fail(err)
return
}
r.complete(desc)
}
// A compilation task. The executor has a semaphore that limits the number
// of concurrent, running tasks.
type task struct {
e *executor
// handler for this task
h *reporter.Handler
// If true, this task needs to acquire a semaphore permit before running.
// If false, this task needs to release its semaphore permit on completion.
released bool
// the result that is populated by this task
r *result
}
func (t *task) release() {
if !t.released {
t.e.s.Release(1)
t.released = true
}
}
const descriptorProtoPath = "google/protobuf/descriptor.proto"
func (t *task) asFile(ctx context.Context, name string, r SearchResult) (linker.File, error) {
if r.Desc != nil {
if r.Desc.Path() != name {
return nil, fmt.Errorf("search result for %q returned descriptor for %q", name, r.Desc.Path())
}
return linker.NewFileRecursive(r.Desc)
}
parseRes, err := t.asParseResult(name, r)
if err != nil {
return nil, err
}
if linkRes, ok := parseRes.(linker.Result); ok {
// if resolver returned a parse result that was actually a link result,
// use the link result directly (no other steps needed)
return linkRes, nil
}
var deps []linker.File
fileDescriptorProto := parseRes.FileDescriptorProto()
var wantsDescriptorProto bool
imports := fileDescriptorProto.Dependency
if t.e.hasOverrideDescriptorProto() {
// we only consider implicitly including descriptor.proto if it's overridden
if name != descriptorProtoPath {
var includesDescriptorProto bool
for _, dep := range fileDescriptorProto.Dependency {
if dep == descriptorProtoPath {
includesDescriptorProto = true
break
}
}
if !includesDescriptorProto {
wantsDescriptorProto = true
// make a defensive copy so we don't inadvertently mutate
// slice's backing array when adding this implicit dep
importsCopy := make([]string, len(imports)+1)
copy(importsCopy, imports)
importsCopy[len(imports)] = descriptorProtoPath
imports = importsCopy
}
}
}
var overrideDescriptorProto linker.File
if len(imports) > 0 {
t.r.setBlockedOn(imports)
results := make([]*result, len(fileDescriptorProto.Dependency))
checked := map[string]struct{}{}
for i, dep := range fileDescriptorProto.Dependency {
span := findImportSpan(parseRes, dep)
if name == dep {
// doh! file imports itself
handleImportCycle(t.h, span, []string{name}, dep)
return nil, t.h.Error()
}
res := t.e.compile(ctx, dep)
// check for dependency cycle to prevent deadlock
if err := t.e.checkForDependencyCycle(res, []string{name, dep}, span, checked); err != nil {
return nil, err
}
results[i] = res
}
deps = make([]linker.File, len(results))
var descriptorProtoRes *result
if wantsDescriptorProto {
descriptorProtoRes = t.e.compile(ctx, descriptorProtoPath)
}
// release our semaphore so dependencies can be processed w/out risk of deadlock
t.e.s.Release(1)
t.released = true
// now we wait for them all to be computed
for i, res := range results {
select {
case <-res.ready:
if res.err != nil {
if rerr, ok := res.err.(errFailedToResolve); ok {
// We don't report errors to get file from resolver to handler since
// it's usually considered immediately fatal. However, if the reason
// we were resolving is due to an import, turn this into an error with
// source position that pinpoints the import statement and report it.
return nil, reporter.Error(findImportSpan(parseRes, res.name), rerr)
}
return nil, res.err
}
deps[i] = res.res
case <-ctx.Done():
return nil, ctx.Err()
}
}
if descriptorProtoRes != nil {
select {
case <-descriptorProtoRes.ready:
// descriptor.proto wasn't explicitly imported, so we can ignore a failure
if descriptorProtoRes.err == nil {
overrideDescriptorProto = descriptorProtoRes.res
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
// all deps resolved
t.r.setBlockedOn(nil)
// reacquire semaphore so we can proceed
if err := t.e.s.Acquire(ctx, 1); err != nil {
return nil, err
}
t.released = false
}
return t.link(parseRes, deps, overrideDescriptorProto)
}
func (e *executor) checkForDependencyCycle(res *result, sequence []string, span ast.SourceSpan, checked map[string]struct{}) error {
if _, ok := checked[res.name]; ok {
// already checked this one
return nil
}
checked[res.name] = struct{}{}
deps := res.getBlockedOn()
for _, dep := range deps {
// is this a cycle?
for _, file := range sequence {
if file == dep {
handleImportCycle(e.h, span, sequence, dep)
return e.h.Error()
}
}
e.mu.Lock()
depRes := e.results[dep]
e.mu.Unlock()
if depRes == nil {
continue
}
if err := e.checkForDependencyCycle(depRes, append(sequence, dep), span, checked); err != nil {
return err
}
}
return nil
}
func handleImportCycle(h *reporter.Handler, span ast.SourceSpan, importSequence []string, dep string) {
var buf bytes.Buffer
buf.WriteString("cycle found in imports: ")
for _, imp := range importSequence {
_, _ = fmt.Fprintf(&buf, "%q -> ", imp)
}
_, _ = fmt.Fprintf(&buf, "%q", dep)
// error is saved and returned in caller
_ = h.HandleErrorWithPos(span, errors.New(buf.String()))
}
func findImportSpan(res parser.Result, dep string) ast.SourceSpan {
root := res.AST()
if root == nil {
return ast.UnknownSpan(res.FileNode().Name())
}
for _, decl := range root.Decls {
if imp, ok := decl.(*ast.ImportNode); ok {
if imp.Name.AsString() == dep {
return root.NodeInfo(imp.Name)
}
}
}
// this should never happen...
return ast.UnknownSpan(res.FileNode().Name())
}
func (t *task) link(parseRes parser.Result, deps linker.Files, overrideDescriptorProtoRes linker.File) (linker.File, error) {
file, err := linker.Link(parseRes, deps, t.e.sym, t.h)
if err != nil {
return nil, err
}
var interpretOpts []options.InterpreterOption
if overrideDescriptorProtoRes != nil {
interpretOpts = []options.InterpreterOption{options.WithOverrideDescriptorProto(overrideDescriptorProtoRes)}
}
optsIndex, err := options.InterpretOptions(file, t.h, interpretOpts...)
if err != nil {
return nil, err
}
// now that options are interpreted, we can do some additional checks
if err := file.ValidateOptions(t.h, t.e.sym); err != nil {
return nil, err
}
if t.r.explicitFile {
file.CheckForUnusedImports(t.h)
}
if err := t.h.Error(); err != nil {
return nil, err
}
if needsSourceInfo(parseRes, t.e.c.SourceInfoMode) {
var srcInfoOpts []sourceinfo.GenerateOption
if t.e.c.SourceInfoMode&SourceInfoExtraComments != 0 {
srcInfoOpts = append(srcInfoOpts, sourceinfo.WithExtraComments())
}
if t.e.c.SourceInfoMode&SourceInfoExtraOptionLocations != 0 {
srcInfoOpts = append(srcInfoOpts, sourceinfo.WithExtraOptionLocations())
}
parseRes.FileDescriptorProto().SourceCodeInfo = sourceinfo.GenerateSourceInfo(parseRes.AST(), optsIndex, srcInfoOpts...)
} else if t.e.c.SourceInfoMode == SourceInfoNone {
// If results came from unlinked FileDescriptorProto, it could have
// source info that we should strip.
parseRes.FileDescriptorProto().SourceCodeInfo = nil
}
if len(parseRes.FileDescriptorProto().GetSourceCodeInfo().GetLocation()) > 0 {
// If we have source code info in the descriptor proto at this point,
// we have to build the index of locations.
file.PopulateSourceCodeInfo()
}
if !t.e.c.RetainASTs {
file.RemoveAST()
}
return file, nil
}
func needsSourceInfo(parseRes parser.Result, mode SourceInfoMode) bool {
return mode != SourceInfoNone && parseRes.AST() != nil && parseRes.FileDescriptorProto().SourceCodeInfo == nil
}
func (t *task) asParseResult(name string, r SearchResult) (parser.Result, error) {
if r.ParseResult != nil {
if r.ParseResult.FileDescriptorProto().GetName() != name {
return nil, fmt.Errorf("search result for %q returned descriptor for %q", name, r.ParseResult.FileDescriptorProto().GetName())
}
// If the file descriptor needs linking, it will be mutated during the
// next stage. So to make anu mutations thread-safe, we must make a
// defensive copy.
res := parser.Clone(r.ParseResult)
return res, nil
}
if r.Proto != nil {
if r.Proto.GetName() != name {
return nil, fmt.Errorf("search result for %q returned descriptor for %q", name, r.Proto.GetName())
}
// If the file descriptor needs linking, it will be mutated during the
// next stage. So to make any mutations thread-safe, we must make a
// defensive copy.
descProto := proto.Clone(r.Proto).(*descriptorpb.FileDescriptorProto) //nolint:errcheck
return parser.ResultWithoutAST(descProto), nil
}
file, err := t.asAST(name, r)
if err != nil {
return nil, err
}
return parser.ResultFromAST(file, true, t.h)
}
func (t *task) asAST(name string, r SearchResult) (*ast.FileNode, error) {
if r.AST != nil {
if r.AST.Name() != name {
return nil, fmt.Errorf("search result for %q returned descriptor for %q", name, r.AST.Name())
}
return r.AST, nil
}
return parser.Parse(name, r.Source, t.h)
}
// 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 ast
import (
"slices"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/token"
)
// Commas is like [Slice], but it's for a comma-delimited list of some kind.
//
// This makes it easy to work with the list as though it's a slice, while also
// allowing access to the commas.
type Commas[T any] interface {
seq.Inserter[T]
// Comma is like [seq.Indexer.At] but returns the comma that follows the nth
// element.
//
// May be [token.Zero], either because it's the last element
// (a common situation where there is no comma) or it was added with
// Insert() rather than InsertComma().
Comma(n int) token.Token
// AppendComma is like [seq.Append], but includes an explicit comma.
AppendComma(value T, comma token.Token)
// InsertComma is like [seq.Inserter.Insert], but includes an explicit comma.
InsertComma(n int, value T, comma token.Token)
}
type withComma[T any] struct {
Value T
Comma token.ID
}
type commas[T, E any] struct {
seq.SliceInserter[T, withComma[E]]
file *File
}
func (c commas[T, _]) Comma(n int) token.Token {
return id.Wrap(c.file.Stream(), (*c.SliceInserter.Slice)[n].Comma)
}
func (c commas[T, _]) AppendComma(value T, comma token.Token) {
c.InsertComma(c.Len(), value, comma)
}
func (c commas[T, _]) InsertComma(n int, value T, comma token.Token) {
c.file.Nodes().panicIfNotOurs(comma)
v := c.SliceInserter.Unwrap(n, value)
v.Comma = comma.ID()
*c.Slice = slices.Insert(*c.Slice, n, v)
}
// 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 ast
import (
"iter"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// File is the top-level AST node for a Protobuf file.
//
// A file is a list of declarations (in other words, it is a [DeclBody]). The
// File type provides convenience functions for extracting salient elements,
// such as the [DeclSyntax] and the [DeclPackage].
//
// # Grammar
//
// File := DeclAny*
type File struct {
_ unsafex.NoCopy
stream *token.Stream
path string
decls decls
types types
exprs exprs
options arena.Arena[rawCompactOptions]
// A cache of raw paths that have been converted into parenthesized
// components in NewExtensionComponent.
extnPathCache map[PathID]token.ID
}
type withContext = id.HasContext[*File]
// New creates a fresh context for a file.
//
// path is the semantic import path of this file, which may not be the same as
// file.Path, which is used for diagnostics.
func New(path string, stream *token.Stream) *File {
f := &File{
stream: stream,
path: path,
}
_ = f.Nodes().NewDeclBody(token.Zero) // This is the rawBody for the whole file.
return f
}
// Syntax returns this file's declaration, if it has one.
func (f *File) Syntax() DeclSyntax {
for d := range seq.Values(f.Decls()) {
if s := d.AsSyntax(); !s.IsZero() {
return s
}
}
return DeclSyntax{}
}
// Package returns this file's package declaration, if it has one.
func (f *File) Package() DeclPackage {
for d := range seq.Values(f.Decls()) {
if p := d.AsPackage(); !p.IsZero() {
return p
}
}
return DeclPackage{}
}
// Imports returns an iterator over this file's import declarations.
func (f *File) Imports() iter.Seq[DeclImport] {
return iterx.FilterMap(seq.Values(f.Decls()), func(d DeclAny) (DeclImport, bool) {
if imp := d.AsImport(); !imp.IsZero() {
return imp, true
}
return DeclImport{}, false
})
}
// Options returns an iterator over this file's option definitions.
func (f *File) Options() iter.Seq[DefOption] {
return iterx.FilterMap(seq.Values(f.Decls()), func(d DeclAny) (DefOption, bool) {
if def := d.AsDef(); !def.IsZero() {
if def.Classify() == DefKindOption {
return def.AsOption(), true
}
}
return DefOption{}, false
})
}
// Path returns the semantic import path of this file.
func (f *File) Path() string {
if f == nil {
return ""
}
return f.path
}
// Decls returns all of the top-level declarations in this file.
func (f *File) Decls() seq.Inserter[DeclAny] {
return id.Wrap(f, id.ID[DeclBody](1)).Decls()
}
// Stream returns the underlying token stream.
func (f *File) Stream() *token.Stream {
if f == nil {
return nil
}
return f.stream
}
// Nodes returns the node arena for this file, which can be used to allocate
// new AST nodes.
func (f *File) Nodes() *Nodes {
return (*Nodes)(f)
}
// Stream returns the underlying token stream.
func (f *File) Span() source.Span {
return id.Wrap(f, id.ID[DeclBody](1)).Span()
}
// FromID implements [id.Context].
func (f *File) FromID(id uint64, want any) any {
switch want.(type) {
case **rawDeclBody:
return f.decls.bodies.Deref(arena.Pointer[rawDeclBody](id))
case **rawDeclDef:
return f.decls.defs.Deref(arena.Pointer[rawDeclDef](id))
case **rawDeclEmpty:
return f.decls.empties.Deref(arena.Pointer[rawDeclEmpty](id))
case **rawDeclImport:
return f.decls.imports.Deref(arena.Pointer[rawDeclImport](id))
case **rawDeclPackage:
return f.decls.packages.Deref(arena.Pointer[rawDeclPackage](id))
case **rawDeclRange:
return f.decls.ranges.Deref(arena.Pointer[rawDeclRange](id))
case **rawDeclSyntax:
return f.decls.syntaxes.Deref(arena.Pointer[rawDeclSyntax](id))
case **rawExprError:
return f.exprs.errors.Deref(arena.Pointer[rawExprError](id))
case **rawExprArray:
return f.exprs.arrays.Deref(arena.Pointer[rawExprArray](id))
case **rawExprDict:
return f.exprs.dicts.Deref(arena.Pointer[rawExprDict](id))
case **rawExprField:
return f.exprs.fields.Deref(arena.Pointer[rawExprField](id))
case **rawExprPrefixed:
return f.exprs.prefixes.Deref(arena.Pointer[rawExprPrefixed](id))
case **rawExprRange:
return f.exprs.ranges.Deref(arena.Pointer[rawExprRange](id))
case **rawTypeError:
return f.types.errors.Deref(arena.Pointer[rawTypeError](id))
case **rawTypeGeneric:
return f.types.generics.Deref(arena.Pointer[rawTypeGeneric](id))
case **rawTypePrefixed:
return f.types.prefixes.Deref(arena.Pointer[rawTypePrefixed](id))
case **rawCompactOptions:
return f.options.Deref(arena.Pointer[rawCompactOptions](id))
default:
return f.stream.FromID(id, want)
}
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/arena"
)
// DeclAny is any Decl* type in this package.
//
// Values of this type can be obtained by calling an AsAny method on a Decl*
// type, such as [DeclSyntax.AsAny]. It can be type-asserted back to any of
// the concrete Decl* types using its own As* methods.
//
// This type is used in lieu of a putative Decl interface type to avoid heap
// allocations in functions that would return one of many different Decl*
// types.
//
// # Grammar
//
// DeclAny := DeclEmpty | DeclSyntax | DeclPackage | DeclImport | DeclDef | DeclBody | DeclRange
//
// Note that this grammar is highly ambiguous. TODO: document the rules under
// which parse DeclSyntax, DeclPackage, DeclImport, and DeclRange.
type DeclAny id.DynNode[DeclAny, DeclKind, *File]
// AsEmpty converts a DeclAny into a DeclEmpty, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsEmpty() DeclEmpty {
if d.Kind() != DeclKindEmpty {
return DeclEmpty{}
}
return id.Wrap(d.Context(), id.ID[DeclEmpty](d.ID().Value()))
}
// AsSyntax converts a DeclAny into a DeclSyntax, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsSyntax() DeclSyntax {
if d.Kind() != DeclKindSyntax {
return DeclSyntax{}
}
return id.Wrap(d.Context(), id.ID[DeclSyntax](d.ID().Value()))
}
// AsPackage converts a DeclAny into a DeclPackage, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsPackage() DeclPackage {
if d.Kind() != DeclKindPackage {
return DeclPackage{}
}
return id.Wrap(d.Context(), id.ID[DeclPackage](d.ID().Value()))
}
// AsImport converts a DeclAny into a DeclImport, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsImport() DeclImport {
if d.Kind() != DeclKindImport {
return DeclImport{}
}
return id.Wrap(d.Context(), id.ID[DeclImport](d.ID().Value()))
}
// AsDef converts a DeclAny into a DeclDef, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsDef() DeclDef {
if d.Kind() != DeclKindDef {
return DeclDef{}
}
return id.Wrap(d.Context(), id.ID[DeclDef](d.ID().Value()))
}
// AsBody converts a DeclAny into a DeclBody, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsBody() DeclBody {
if d.Kind() != DeclKindBody {
return DeclBody{}
}
return id.Wrap(d.Context(), id.ID[DeclBody](d.ID().Value()))
}
// AsRange converts a DeclAny into a DeclRange, if that is the declaration
// it contains.
//
// Otherwise, returns zero.
func (d DeclAny) AsRange() DeclRange {
if d.Kind() != DeclKindRange {
return DeclRange{}
}
return id.Wrap(d.Context(), id.ID[DeclRange](d.ID().Value()))
}
// Span implements [source.Spanner].
func (d DeclAny) Span() source.Span {
// At most one of the below will produce a non-zero decl, and that will be
// the span selected by source.Join. If all of them are zero, this produces
// the zero span.
return source.Join(
d.AsEmpty(),
d.AsSyntax(),
d.AsPackage(),
d.AsImport(),
d.AsDef(),
d.AsBody(),
d.AsRange(),
)
}
func (DeclKind) DecodeDynID(lo, _ int32) DeclKind {
return DeclKind(lo)
}
func (k DeclKind) EncodeDynID(value int32) (int32, int32, bool) {
return int32(k), value, true
}
// decls is storage for every kind of Decl in a Context.
type decls struct {
empties arena.Arena[rawDeclEmpty]
syntaxes arena.Arena[rawDeclSyntax]
packages arena.Arena[rawDeclPackage]
imports arena.Arena[rawDeclImport]
defs arena.Arena[rawDeclDef]
bodies arena.Arena[rawDeclBody]
ranges arena.Arena[rawDeclRange]
}
// 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 ast
import (
"iter"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// DeclBody is the body of a [DeclBody], or the whole contents of a [File]. The
// protocompile AST is very lenient, and allows any declaration to exist anywhere, for the
// benefit of rich diagnostics and refactorings. For example, it is possible to represent an
// "orphaned" field or oneof outside of a message, or an RPC method inside of an enum, and
// so on.
//
// # Grammar
//
// DeclBody := `{` DeclAny* `}`
//
// Note that a [File] is simply a DeclBody that is delimited by the bounds of
// the source file, rather than braces.
type DeclBody id.Node[DeclBody, *File, *rawDeclBody]
// HasBody is an AST node that contains a [Body].
//
// [File], [DeclBody], and [DeclDef] all implement this interface.
type HasBody interface {
source.Spanner
Body() DeclBody
}
type rawDeclBody struct {
braces token.ID
// These slices are co-indexed; they are parallelizes to save
// three bytes per decl (declKind is 1 byte, but decl is 4; if
// they're stored in AOS format, we waste 3 bytes of padding).
kinds []DeclKind
ptrs []id.ID[DeclAny]
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclBody) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindBody, id.ID[DeclAny](d.ID())))
}
// Braces returns this body's surrounding braces, if it has any.
func (d DeclBody) Braces() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().braces)
}
// Span implements [source.Spanner].
func (d DeclBody) Span() source.Span {
decls := d.Decls()
switch {
case d.IsZero():
return source.Span{}
case !d.Braces().IsZero():
return d.Braces().Span()
case decls.Len() == 0:
return source.Span{}
default:
return source.Join(decls.At(0), decls.At(decls.Len()-1))
}
}
// 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] {
if d.IsZero() {
return seq.SliceInserter2[DeclAny, DeclKind, id.ID[DeclAny]]{}
}
return seq.NewSliceInserter2(
&d.Raw().kinds,
&d.Raw().ptrs,
func(_ int, k DeclKind, p id.ID[DeclAny]) DeclAny {
return id.WrapDyn(d.Context(), id.NewDyn(k, p))
},
func(_ int, d DeclAny) (DeclKind, id.ID[DeclAny]) {
d.Context().Nodes().panicIfNotOurs(d)
return d.ID().Kind(), d.ID().Value()
},
)
}
// Options returns an iterator over the option definitions in this body.
func (d DeclBody) Options() iter.Seq[DefOption] {
return iterx.FilterMap(seq.Values(d.Decls()), func(d DeclAny) (DefOption, bool) {
if def := d.AsDef(); !def.IsZero() {
if def.Classify() == DefKindOption {
return def.AsOption(), true
}
}
return DefOption{}, false
})
}
// 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 ast
import (
"iter"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// DeclDef is a general Protobuf definition.
//
// This [Decl] represents the union of several similar AST nodes, to aid in permissive
// parsing and precise diagnostics.
//
// This node represents messages, enums, services, extend blocks, fields, enum values,
// oneofs, groups, service methods, and options. It also permits nonsensical syntax, such as a
// message with a tag number.
//
// Generally, you should not need to work with DeclDef directly; instead, use the As* methods
// to access the correct concrete syntax production a DeclDef represents.
//
// # Grammar
//
// DeclDef := (Type Path | Type | Ident) followers* (`;` | DeclBody)?
//
// followers := inputs | outputs | value | CompactOptions
// inputs := `(` (Type `,`?)* `)`
// outputs := `returns` (Type | inputs)?
// value := (`=` Expr) | ExprPath | ExprLiteral | ExprRange | ExprField
//
// Note that this type will only record the first appearance of any follower.
type DeclDef id.Node[DeclDef, *File, *rawDeclDef]
type rawDeclDef struct {
ty id.Dyn[TypeAny, TypeKind] // Not present for enum fields.
name PathID
signature *rawSignature
equals token.ID
value id.Dyn[ExprAny, ExprKind]
options id.ID[CompactOptions]
body id.ID[DeclBody]
semi token.ID
corrupt bool
}
// DeclDefArgs is arguments for creating a [DeclDef] with [Context.NewDeclDef].
type DeclDefArgs struct {
// If both Keyword and Type are set, Type will be prioritized.
Keyword token.Token
Type TypeAny
Name Path
// NOTE: the values for the type signature are not provided at
// construction time, and should be added by mutating through
// DeclDef.Signature.
Returns token.Token
Equals token.Token
Value ExprAny
Options CompactOptions
Body DeclBody
Semicolon token.Token
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclDef) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindDef, id.ID[DeclAny](d.ID())))
}
// Type returns the "prefix" type of this definition.
//
// This type may coexist with a [Signature] in this definition.
//
// May be zero, such as for enum values. For messages and other productions
// introduced by a special keyword, this will be a [TypePath] whose single
// identifier is that keyword.
//
// See [DeclDef.KeywordToken].
func (d DeclDef) Type() TypeAny {
if d.IsZero() {
return TypeAny{}
}
return id.WrapDyn(d.Context(), d.Raw().ty)
}
// SetType sets the "prefix" type of this definition.
func (d DeclDef) SetType(ty TypeAny) {
d.Raw().ty = ty.ID()
}
// KeywordToken returns the introducing keyword for this definition, if
// there is one.
//
// See [DeclDef.Type] for details on where this keyword comes from.
func (d DeclDef) Keyword() keyword.Keyword {
return d.KeywordToken().Keyword()
}
// KeywordToken returns the introducing keyword token for this definition, if
// there is one.
//
// See [DeclDef.Type] for details on where this keyword comes from.
func (d DeclDef) KeywordToken() token.Token {
// Begin by removing all modifiers. Certain kinds of defs can have
// modifiers, such as groups and types. Any def that can have a body
// is permitted to have modifiers, because that is unambiguous with a field.
mods := false
ty := d.Type()
for ty.Kind() == TypeKindPrefixed {
mods = true
ty = ty.AsPrefixed().Type()
}
path := ty.AsPath()
if path.IsZero() {
return token.Zero
}
ident := path.Path.AsIdent()
switch ident.Keyword() {
case keyword.Option:
if !mods { // NOTE: Options with modifiers are treated as fields by protoc.
return ident
}
case keyword.RPC:
if !d.Signature().IsZero() {
return ident
}
case keyword.Message, keyword.Enum, keyword.Service, keyword.Extend,
keyword.Oneof, keyword.Group:
if !d.Body().IsZero() {
return ident
}
}
return token.Zero
}
// Prefixes returns an iterator over the modifiers on this def, expressed as
// [TypePrefixed] nodes.
func (d DeclDef) Prefixes() iter.Seq[TypePrefixed] {
return func(yield func(TypePrefixed) bool) {
ty := d.Type()
for ty.Kind() == TypeKindPrefixed {
prefixed := ty.AsPrefixed()
if !yield(prefixed) {
break
}
ty = prefixed.Type()
}
}
}
// Name returns this definition's declared name.
func (d DeclDef) Name() Path {
if d.IsZero() {
return Path{}
}
return d.Raw().name.In(d.Context())
}
// Stem returns a span that contains both this definition's type and name.
//
// For e.g. a message, this is the "message Foo" part.
func (d DeclDef) Stem() source.Span {
return source.Join(d.Type(), d.Name())
}
// Signature returns this definition's type signature, if it has one.
//
// Note that this is distinct from the type returned by [DeclDef.Type], which
// is the "prefix" type for the definition (such as for a field). This is a
// signature for e.g. a method.
//
// Not all defs have a signature, so this function may return a zero Signature.q
// If you want to add one, use [DeclDef.WithSignature].
func (d DeclDef) Signature() Signature {
if d.IsZero() || d.Raw().signature == nil {
return Signature{}
}
return Signature{
id.WrapContext(d.Context()),
d.Raw().signature,
}
}
// WithSignature is like Signature, but it adds an empty signature if it would
// return zero.
func (d DeclDef) WithSignature() Signature {
if !d.IsZero() && d.Signature().IsZero() {
d.Raw().signature = new(rawSignature)
}
return d.Signature()
}
// Equals returns this definitions = token, before the value.
// May be zero.
func (d DeclDef) Equals() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().equals)
}
// Value returns this definition's value. For a field, this will be the
// tag number, while for an option, this will be the complex expression
// representing its value.
func (d DeclDef) Value() ExprAny {
if d.IsZero() {
return ExprAny{}
}
return id.WrapDyn(d.Context(), d.Raw().value)
}
// SetValue sets the value of this definition.
//
// See [DeclDef.Value].
func (d DeclDef) SetValue(expr ExprAny) {
d.Raw().value = expr.ID()
}
// Options returns the compact options list for this definition.
func (d DeclDef) Options() CompactOptions {
if d.IsZero() {
return CompactOptions{}
}
return id.Wrap(d.Context(), d.Raw().options)
}
// SetOptions sets the compact options list for this definition.
//
// Setting it to a zero Options clears it.
func (d DeclDef) SetOptions(opts CompactOptions) {
d.Raw().options = opts.ID()
}
// Body returns this definition's body, if it has one.
func (d DeclDef) Body() DeclBody {
if d.IsZero() {
return DeclBody{}
}
return id.Wrap(d.Context(), d.Raw().body)
}
// SetBody sets the body for this definition.
func (d DeclDef) SetBody(b DeclBody) {
d.Raw().body = b.ID()
}
// Semicolon returns the ending semicolon token for this definition.
// May be zero.
func (d DeclDef) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// IsCorrupt reports whether or not some part of the parser decided that this
// definition is not interpretable as any specific kind of definition.
func (d DeclDef) IsCorrupt() bool {
return !d.IsZero() && d.Raw().corrupt
}
// the compiler to ignore it. See [DeclDef.IsCorrupt].
func (d DeclDef) MarkCorrupt() {
d.Raw().corrupt = true
}
// AsMessage extracts the fields from this definition relevant to interpreting
// it as a message.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsMessage() DefMessage {
return DefMessage{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Body: d.Body(),
Decl: d,
}
}
// AsEnum extracts the fields from this definition relevant to interpreting
// it as an enum.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsEnum() DefEnum {
return DefEnum{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Body: d.Body(),
Decl: d,
}
}
// AsService extracts the fields from this definition relevant to interpreting
// it as a service.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsService() DefService {
return DefService{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Body: d.Body(),
Decl: d,
}
}
// AsExtend extracts the fields from this definition relevant to interpreting
// it as a service.
//
// The return value's fields may be zero if they are not present.
//
// See [DeclDef.Classify].
func (d DeclDef) AsExtend() DefExtend {
return DefExtend{
Keyword: d.KeywordToken(),
Extendee: d.Name(),
Body: d.Body(),
Decl: d,
}
}
// AsField extracts the fields from this definition relevant to interpreting
// it as a message field.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsField() DefField {
return DefField{
Type: d.Type(),
Name: d.Name().AsIdent(),
Equals: d.Equals(),
Tag: d.Value(),
Options: d.Options(),
Semicolon: d.Semicolon(),
Decl: d,
}
}
// AsOneof extracts the fields from this definition relevant to interpreting
// it as a oneof.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsOneof() DefOneof {
return DefOneof{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Body: d.Body(),
Decl: d,
}
}
// AsGroup extracts the fields from this definition relevant to interpreting
// it as a group.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsGroup() DefGroup {
return DefGroup{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Equals: d.Equals(),
Tag: d.Value(),
Options: d.Options(),
Decl: d,
}
}
// AsEnumValue extracts the fields from this definition relevant to interpreting
// it as an enum value.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsEnumValue() DefEnumValue {
return DefEnumValue{
Name: d.Name().AsIdent(),
Equals: d.Equals(),
Tag: d.Value(),
Options: d.Options(),
Semicolon: d.Semicolon(),
Decl: d,
}
}
// AsMethod extracts the fields from this definition relevant to interpreting
// it as a service method.
//
// The return value's fields may be zero if they are not present (in particular,
// Name will be zero if d.Name() is not an identifier).
//
// See [DeclDef.Classify].
func (d DeclDef) AsMethod() DefMethod {
return DefMethod{
Keyword: d.KeywordToken(),
Name: d.Name().AsIdent(),
Signature: d.Signature(),
Body: d.Body(),
Decl: d,
}
}
// AsMethod extracts the fields from this definition relevant to interpreting
// it as an option.
//
// The return value's fields may be zero if they are not present.
//
// See [DeclDef.Classify].
func (d DeclDef) AsOption() DefOption {
return DefOption{
Keyword: d.KeywordToken(),
Option: Option{
Path: d.Name(),
Equals: d.Equals(),
Value: d.Value(),
},
Semicolon: d.Semicolon(),
Decl: d,
}
}
// Classify looks at all the fields in this definition and decides what kind of
// definition it's supposed to represent.
//
// To select which definition this probably is, this function looks at
// [DeclDef.KeywordToken]. If there is no keyword or it isn't something that it
// recognizes, it is classified as either an enum value or a field, depending on
// whether this definition has a type.
//
// The correct way to use this function is as the input value for a switch. The
// 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() || d.IsCorrupt() {
return DefKindInvalid
}
switch d.Keyword() {
case keyword.Message:
return DefKindMessage
case keyword.Enum:
return DefKindEnum
case keyword.Service:
return DefKindService
case keyword.Extend:
return DefKindExtend
case keyword.Oneof:
return DefKindOneof
case keyword.Group:
return DefKindGroup
case keyword.RPC:
return DefKindMethod
case keyword.Option:
return DefKindOption
}
if d.Type().IsZero() {
return DefKindEnumValue
}
return DefKindField
}
// Span implements [source.Spanner].
func (d DeclDef) Span() source.Span {
if d.IsZero() {
return source.Span{}
}
return source.Join(
d.Type(),
d.Name(),
d.Signature(),
d.Equals(),
d.Value(),
d.Options(),
d.Body(),
d.Semicolon(),
)
}
// Signature is a type signature of the form (types) returns (types).
//
// Signatures may have multiple inputs and outputs.
type Signature struct {
withContext
raw *rawSignature
}
type rawSignature struct {
input, output rawTypeList
returns token.ID
}
// Returns returns (lol) the "returns" token that separates the input and output
// type lists.
func (s Signature) Returns() token.Token {
if s.IsZero() {
return token.Zero
}
return id.Wrap(s.Context().Stream(), s.raw.returns)
}
// Inputs returns the input argument list for this signature.
func (s Signature) Inputs() TypeList {
if s.IsZero() {
return TypeList{}
}
return TypeList{
id.WrapContext(s.Context()),
&s.raw.input,
}
}
// Outputs returns the output argument list for this signature.
func (s Signature) Outputs() TypeList {
if s.IsZero() {
return TypeList{}
}
return TypeList{
id.WrapContext(s.Context()),
&s.raw.output,
}
}
// Span implemented [source.Spanner].
func (s Signature) Span() source.Span {
if s.IsZero() {
return source.Span{}
}
return source.Join(s.Inputs(), s.Returns(), s.Outputs())
}
// Def is the return type of [DeclDef.Classify].
//
// This interface is implemented by all the Def* types in this package, and
// can be type-asserted to any of them, usually in a type switch.
//
// A [DeclDef] can't be mutated through a Def; instead, you will need to mutate
// the general structure instead.
type Def interface {
source.Spanner
isDef()
}
// DefMessage is a [DeclDef] projected into a message definition.
//
// See [DeclDef.Classify].
type DefMessage struct {
Keyword token.Token
Name token.Token
Body DeclBody
Decl DeclDef
}
func (DefMessage) isDef() {}
func (d DefMessage) Span() source.Span { return d.Decl.Span() }
func (d DefMessage) Context() *File { return d.Decl.Context() }
// DefEnum is a [DeclDef] projected into an enum definition.
//
// See [DeclDef.Classify].
type DefEnum struct {
Keyword token.Token
Name token.Token
Body DeclBody
Decl DeclDef
}
func (DefEnum) isDef() {}
func (d DefEnum) Span() source.Span { return d.Decl.Span() }
func (d DefEnum) Context() *File { return d.Decl.Context() }
// DefService is a [DeclDef] projected into a service definition.
//
// See [DeclDef.Classify].
type DefService struct {
Keyword token.Token
Name token.Token
Body DeclBody
Decl DeclDef
}
func (DefService) isDef() {}
func (d DefService) Span() source.Span { return d.Decl.Span() }
func (d DefService) Context() *File { return d.Decl.Context() }
// DefExtend is a [DeclDef] projected into an extension definition.
//
// See [DeclDef.Classify].
type DefExtend struct {
Keyword token.Token
Extendee Path
Body DeclBody
Decl DeclDef
}
func (DefExtend) isDef() {}
func (d DefExtend) Span() source.Span { return d.Decl.Span() }
func (d DefExtend) Context() *File { return d.Decl.Context() }
// DefField is a [DeclDef] projected into a field definition.
//
// See [DeclDef.Classify].
type DefField struct {
Type TypeAny
Name token.Token
Equals token.Token
Tag ExprAny
Options CompactOptions
Semicolon token.Token
Decl DeclDef
}
func (DefField) isDef() {}
func (d DefField) Span() source.Span { return d.Decl.Span() }
func (d DefField) Context() *File { return d.Decl.Context() }
// DefEnumValue is a [DeclDef] projected into an enum value definition.
//
// See [DeclDef.Classify].
type DefEnumValue struct {
Name token.Token
Equals token.Token
Tag ExprAny
Options CompactOptions
Semicolon token.Token
Decl DeclDef
}
func (DefEnumValue) isDef() {}
func (d DefEnumValue) Span() source.Span { return d.Decl.Span() }
func (d DefEnumValue) Context() *File { return d.Decl.Context() }
// DefEnumValue is a [DeclDef] projected into a oneof definition.
//
// See [DeclDef.Classify].
type DefOneof struct {
Keyword token.Token
Name token.Token
Body DeclBody
Decl DeclDef
}
func (DefOneof) isDef() {}
func (d DefOneof) Span() source.Span { return d.Decl.Span() }
func (d DefOneof) Context() *File { return d.Decl.Context() }
// DefGroup is a [DeclDef] projected into a group definition.
//
// See [DeclDef.Classify].
type DefGroup struct {
Keyword token.Token
Name token.Token
Equals token.Token
Tag ExprAny
Options CompactOptions
Body DeclBody
Decl DeclDef
}
func (DefGroup) isDef() {}
func (d DefGroup) Span() source.Span { return d.Decl.Span() }
func (d DefGroup) Context() *File { return d.Decl.Context() }
// DefMethod is a [DeclDef] projected into a method definition.
//
// See [DeclDef.Classify].
type DefMethod struct {
Keyword token.Token
Name token.Token
Signature Signature
Body DeclBody
Decl DeclDef
}
func (DefMethod) isDef() {}
func (d DefMethod) Span() source.Span { return d.Decl.Span() }
func (d DefMethod) Context() *File { return d.Decl.Context() }
// DefOption is a [DeclDef] projected into a method definition.
//
// Yes, an option is technically not defining anything, just setting a value.
// However, it's syntactically analogous to a definition!
//
// See [DeclDef.Classify].
type DefOption struct {
Option
Keyword token.Token
Semicolon token.Token
Decl DeclDef
}
func (DefOption) isDef() {}
func (d DefOption) Span() source.Span { return d.Decl.Span() }
func (d DefOption) Context() *File { return d.Decl.Context() }
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// DeclEmpty is an empty declaration, a lone ;.
//
// # Grammar
//
// DeclEmpty := `;`
type DeclEmpty id.Node[DeclEmpty, *File, *rawDeclEmpty]
type rawDeclEmpty struct {
semi token.ID
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclEmpty) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindEmpty, id.ID[DeclAny](d.ID())))
}
// Semicolon returns this field's ending semicolon.
//
// May be [token.Zero], if not present.
func (d DeclEmpty) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// Span implements [source.Spanner].
func (d DeclEmpty) Span() source.Span {
if d.IsZero() {
return source.Span{}
}
return d.Semicolon().Span()
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// DeclSyntax represents a language declaration, such as the syntax or edition
// keywords.
//
// # Grammar
//
// DeclSyntax := (`syntax` | `edition`) (`=`? Expr)? CompactOptions? `;`?
//
// Note: options are not permitted on syntax declarations in Protobuf, but we
// parse them for diagnosis.
type DeclSyntax id.Node[DeclSyntax, *File, *rawDeclSyntax]
type rawDeclSyntax struct {
value id.Dyn[ExprAny, ExprKind]
keyword token.ID
equals token.ID
semi token.ID
options id.ID[CompactOptions]
}
// DeclSyntaxArgs is arguments for [Context.NewDeclSyntax].
type DeclSyntaxArgs struct {
// Must be "syntax" or "edition".
Keyword token.Token
Equals token.Token
Value ExprAny
Options CompactOptions
Semicolon token.Token
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclSyntax) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindSyntax, id.ID[DeclAny](d.ID())))
}
// Keyword returns the keyword for this declaration.
func (d DeclSyntax) Keyword() keyword.Keyword {
return d.KeywordToken().Keyword()
}
// KeywordToken returns the keyword token for this declaration.
func (d DeclSyntax) KeywordToken() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().keyword)
}
// IsSyntax checks whether this is an OG syntax declaration.
func (d DeclSyntax) IsSyntax() bool {
return d.Keyword() == keyword.Syntax
}
// IsEdition checks whether this is a new-style edition declaration.
func (d DeclSyntax) IsEdition() bool {
return d.Keyword() == keyword.Edition
}
// Equals returns the equals sign after the keyword.
//
// May be zero, if the user wrote something like syntax "proto2";.
func (d DeclSyntax) Equals() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().equals)
}
// Value returns the value expression of this declaration.
//
// May be zero, if the user wrote something like syntax;. It can also be
// a number or an identifier, for cases like edition = 2024; or syntax = proto2;.
func (d DeclSyntax) Value() ExprAny {
if d.IsZero() {
return ExprAny{}
}
return id.WrapDyn(d.Context(), d.Raw().value)
}
// SetValue sets the expression for this declaration's value.
//
// If passed zero, this clears the value (e.g., for syntax = ;).
func (d DeclSyntax) SetValue(expr ExprAny) {
d.Raw().value = expr.ID()
}
// Options returns the compact options list for this declaration.
//
// Syntax declarations cannot have options, but we parse them anyways.
func (d DeclSyntax) Options() CompactOptions {
if d.IsZero() {
return CompactOptions{}
}
return id.Wrap(d.Context(), d.Raw().options)
}
// SetOptions sets the compact options list for this declaration.
//
// Setting it to a zero Options clears it.
func (d DeclSyntax) SetOptions(opts CompactOptions) {
d.Raw().options = opts.ID()
}
// Semicolon returns this declaration's ending semicolon.
//
// May be zero, if the user forgot it.
func (d DeclSyntax) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// source.Span implements [source.Spanner].
func (d DeclSyntax) Span() source.Span {
if d.IsZero() {
return source.Span{}
}
return source.Join(d.KeywordToken(), d.Equals(), d.Value(), d.Semicolon())
}
// DeclPackage is the package declaration for a file.
//
// # Grammar
//
// DeclPackage := `package` Path? CompactOptions? `;`?
//
// Note: options are not permitted on package declarations in Protobuf, but we
// parse them for diagnosis.
type DeclPackage id.Node[DeclPackage, *File, *rawDeclPackage]
type rawDeclPackage struct {
keyword token.ID
path PathID
semi token.ID
options id.ID[CompactOptions]
}
// DeclPackageArgs is arguments for [Context.NewDeclPackage].
type DeclPackageArgs struct {
Keyword token.Token
Path Path
Options CompactOptions
Semicolon token.Token
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclPackage) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindPackage, id.ID[DeclAny](d.ID())))
}
// Keyword returns the keyword for this declaration.
func (d DeclPackage) Keyword() keyword.Keyword {
return d.KeywordToken().Keyword()
}
// KeywordToken returns the "package" token for this declaration.
func (d DeclPackage) KeywordToken() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().keyword)
}
// Path returns this package's path.
//
// May be zero, if the user wrote something like package;.
func (d DeclPackage) Path() Path {
if d.IsZero() {
return Path{}
}
return d.Raw().path.In(d.Context())
}
// Options returns the compact options list for this declaration.
//
// Package declarations cannot have options, but we parse them anyways.
func (d DeclPackage) Options() CompactOptions {
if d.IsZero() {
return CompactOptions{}
}
return id.Wrap(d.Context(), d.Raw().options)
}
// SetOptions sets the compact options list for this declaration.
//
// Setting it to a zero Options clears it.
func (d DeclPackage) SetOptions(opts CompactOptions) {
d.Raw().options = opts.ID()
}
// Semicolon returns this package's ending semicolon.
//
// May be zero, if the user forgot it.
func (d DeclPackage) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// source.Span implements [source.Spanner].
func (d DeclPackage) Span() source.Span {
if d.IsZero() {
return source.Span{}
}
return source.Join(d.KeywordToken(), d.Path(), d.Semicolon())
}
// DeclImport is an import declaration within a file.
//
// # Grammar
//
// DeclImport := `import` (`weak` | `public`)? Expr? CompactOptions? `;`?
//
// Note: options are not permitted on import declarations in Protobuf, but we
// parse them for diagnosis.
type DeclImport id.Node[DeclImport, *File, *rawDeclImport]
type rawDeclImport struct {
keyword, semi token.ID
modifiers []token.ID
importPath id.Dyn[ExprAny, ExprKind]
options id.ID[CompactOptions]
}
// DeclImportArgs is arguments for [Context.NewDeclImport].
type DeclImportArgs struct {
Keyword token.Token
Modifiers []token.Token
ImportPath ExprAny
Options CompactOptions
Semicolon token.Token
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclImport) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindImport, id.ID[DeclAny](d.ID())))
}
// Keyword returns the keyword for this declaration.
func (d DeclImport) Keyword() keyword.Keyword {
return d.KeywordToken().Keyword()
}
// KeywordToken returns the "import" keyword for this declaration.
func (d DeclImport) KeywordToken() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().keyword)
}
// Modifiers returns the modifiers for this declaration.
func (d DeclImport) Modifiers() seq.Indexer[keyword.Keyword] {
var slice []token.ID
if !d.IsZero() {
slice = d.Raw().modifiers
}
return seq.NewFixedSlice(slice, func(_ int, t token.ID) keyword.Keyword {
return id.Wrap(d.Context().Stream(), t).Keyword()
})
}
// ModifierTokens returns the modifier tokens for this declaration.
func (d DeclImport) ModifierTokens() seq.Inserter[token.Token] {
if d.IsZero() {
return seq.EmptySliceInserter[token.Token, token.ID]()
}
return seq.NewSliceInserter(&d.Raw().modifiers,
func(_ int, e token.ID) token.Token { return id.Wrap(d.Context().Stream(), e) },
func(_ int, t token.Token) token.ID {
d.Context().Nodes().panicIfNotOurs(t)
return t.ID()
},
)
}
// IsPublic checks whether this is an "import public".
func (d DeclImport) IsPublic() bool {
return iterx.Contains(seq.Values(d.Modifiers()), func(k keyword.Keyword) bool {
return k == keyword.Public
})
}
// IsWeak checks whether this is an "import weak".
func (d DeclImport) IsWeak() bool {
return iterx.Contains(seq.Values(d.Modifiers()), func(k keyword.Keyword) bool {
return k == keyword.Weak
})
}
// IsOption checks whether this is an "import option".
func (d DeclImport) IsOption() bool {
return iterx.Contains(seq.Values(d.Modifiers()), func(k keyword.Keyword) bool {
return k == keyword.Option
})
}
// ImportPath returns the file path for this import as a string.
//
// May be zero, if the user forgot it.
func (d DeclImport) ImportPath() ExprAny {
if d.IsZero() {
return ExprAny{}
}
return id.WrapDyn(d.Context(), d.Raw().importPath)
}
// SetValue sets the expression for this import's file path.
//
// If passed zero, this clears the path expression.
func (d DeclImport) SetImportPath(expr ExprAny) {
d.Raw().importPath = expr.ID()
}
// Options returns the compact options list for this declaration.
//
// Imports cannot have options, but we parse them anyways.
func (d DeclImport) Options() CompactOptions {
if d.IsZero() {
return CompactOptions{}
}
return id.Wrap(d.Context(), d.Raw().options)
}
// SetOptions sets the compact options list for this declaration.
//
// Setting it to a zero Options clears it.
func (d DeclImport) SetOptions(opts CompactOptions) {
d.Raw().options = opts.ID()
}
// Semicolon returns this import's ending semicolon.
//
// May be zero, if the user forgot it.
func (d DeclImport) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// source.Span implements [source.Spanner].
func (d DeclImport) Span() source.Span {
if d.IsZero() {
return source.Span{}
}
return source.Join(d.KeywordToken(), d.ImportPath(), d.Semicolon())
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// DeclRange represents an extension or reserved range declaration. They are almost identical
// syntactically so they use the same AST node.
//
// # Grammar
//
// DeclRange := (`extensions` | `reserved`) (Expr `,`)* Expr? CompactOptions? `;`?
type DeclRange id.Node[DeclRange, *File, *rawDeclRange]
type rawDeclRange struct {
keyword token.ID
args []withComma[id.Dyn[ExprAny, ExprKind]]
options id.ID[CompactOptions]
semi token.ID
}
// DeclRangeArgs is arguments for [Context.NewDeclRange].
type DeclRangeArgs struct {
Keyword token.Token
Options CompactOptions
Semicolon token.Token
}
// AsAny type-erases this declaration value.
//
// See [DeclAny] for more information.
func (d DeclRange) AsAny() DeclAny {
if d.IsZero() {
return DeclAny{}
}
return id.WrapDyn(d.Context(), id.NewDyn(DeclKindRange, id.ID[DeclAny](d.ID())))
}
// Keyword returns the keyword for this range.
func (d DeclRange) Keyword() keyword.Keyword {
return d.KeywordToken().Keyword()
}
// KeywordToken returns the keyword token for this range.
func (d DeclRange) KeywordToken() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().keyword)
}
// IsExtensions checks whether this is an extension range.
func (d DeclRange) IsExtensions() bool {
return d.Keyword() == keyword.Extensions
}
// IsReserved checks whether this is a reserved range.
func (d DeclRange) IsReserved() bool {
return d.Keyword() == keyword.Reserved
}
// Ranges returns the sequence of expressions denoting the ranges in this
// range declaration.
func (d DeclRange) Ranges() Commas[ExprAny] {
type slice = commas[ExprAny, id.Dyn[ExprAny, ExprKind]]
if d.IsZero() {
return slice{}
}
return slice{
file: d.Context(),
SliceInserter: seq.NewSliceInserter(
&d.Raw().args,
func(_ int, c withComma[id.Dyn[ExprAny, ExprKind]]) ExprAny {
return id.WrapDyn(d.Context(), c.Value)
},
func(_ int, e ExprAny) withComma[id.Dyn[ExprAny, ExprKind]] {
d.Context().Nodes().panicIfNotOurs(e)
return withComma[id.Dyn[ExprAny, ExprKind]]{Value: e.ID()}
},
),
}
}
// Options returns the compact options list for this range.
func (d DeclRange) Options() CompactOptions {
if d.IsZero() {
return CompactOptions{}
}
return id.Wrap(d.Context(), d.Raw().options)
}
// SetOptions sets the compact options list for this definition.
//
// Setting it to a nil Options clears it.
func (d DeclRange) SetOptions(opts CompactOptions) {
d.Raw().options = opts.ID()
}
// Semicolon returns this range's ending semicolon.
//
// May be nil, if not present.
func (d DeclRange) Semicolon() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.Raw().semi)
}
// Span implements [source.Spanner].
func (d DeclRange) Span() source.Span {
r := d.Ranges()
switch {
case d.IsZero():
return source.Span{}
case r.Len() == 0:
return source.Join(d.KeywordToken(), d.Semicolon(), d.Options())
default:
return source.Join(
d.KeywordToken(), d.Semicolon(), d.Options(),
r.At(0),
r.At(r.Len()-1),
)
}
}
// 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 enums.yaml. DO NOT EDIT.
package ast
import (
"fmt"
"iter"
)
// DeclKind is a kind of declaration. There is one value of DeclKind for each
// Decl* type in this package.
type DeclKind int8
const (
DeclKindInvalid DeclKind = iota
DeclKindEmpty
DeclKindSyntax
DeclKindPackage
DeclKindImport
DeclKindDef
DeclKindBody
DeclKindRange
)
// String implements [fmt.Stringer].
func (v DeclKind) String() string {
if int(v) < 0 || int(v) > len(_table_DeclKind_String) {
return fmt.Sprintf("DeclKind(%v)", int(v))
}
return _table_DeclKind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v DeclKind) GoString() string {
if int(v) < 0 || int(v) > len(_table_DeclKind_GoString) {
return fmt.Sprintf("ast.DeclKind(%v)", int(v))
}
return _table_DeclKind_GoString[v]
}
// DefKind is the kind of definition a [DeclDef] contains.
//
// See [DeclDef.Classify].
type DefKind int8
const (
DefKindInvalid DefKind = iota
DefKindMessage
DefKindEnum
DefKindService
DefKindExtend
DefKindField
DefKindOneof
DefKindGroup
DefKindEnumValue
DefKindMethod
DefKindOption
)
// String implements [fmt.Stringer].
func (v DefKind) String() string {
if int(v) < 0 || int(v) > len(_table_DefKind_String) {
return fmt.Sprintf("DefKind(%v)", int(v))
}
return _table_DefKind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v DefKind) GoString() string {
if int(v) < 0 || int(v) > len(_table_DefKind_GoString) {
return fmt.Sprintf("ast.DefKind(%v)", int(v))
}
return _table_DefKind_GoString[v]
}
// ExprKind is a kind of expression. There is one value of ExprKind for each
// Expr* type in this package.
type ExprKind int8
const (
ExprKindInvalid ExprKind = iota
ExprKindError
ExprKindLiteral
ExprKindPrefixed
ExprKindPath
ExprKindRange
ExprKindArray
ExprKindDict
ExprKindField
)
// String implements [fmt.Stringer].
func (v ExprKind) String() string {
if int(v) < 0 || int(v) > len(_table_ExprKind_String) {
return fmt.Sprintf("ExprKind(%v)", int(v))
}
return _table_ExprKind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v ExprKind) GoString() string {
if int(v) < 0 || int(v) > len(_table_ExprKind_GoString) {
return fmt.Sprintf("ast.ExprKind(%v)", int(v))
}
return _table_ExprKind_GoString[v]
}
// TypeKind is a kind of type. There is one value of TypeKind for each
// Type* type in this package.
type TypeKind int8
const (
TypeKindInvalid TypeKind = iota
TypeKindError
TypeKindPath
TypeKindPrefixed
TypeKindGeneric
)
// String implements [fmt.Stringer].
func (v TypeKind) String() string {
if int(v) < 0 || int(v) > len(_table_TypeKind_String) {
return fmt.Sprintf("TypeKind(%v)", int(v))
}
return _table_TypeKind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v TypeKind) GoString() string {
if int(v) < 0 || int(v) > len(_table_TypeKind_GoString) {
return fmt.Sprintf("ast.TypeKind(%v)", int(v))
}
return _table_TypeKind_GoString[v]
}
var _table_DeclKind_String = [...]string{
DeclKindInvalid: "DeclKindInvalid",
DeclKindEmpty: "DeclKindEmpty",
DeclKindSyntax: "DeclKindSyntax",
DeclKindPackage: "DeclKindPackage",
DeclKindImport: "DeclKindImport",
DeclKindDef: "DeclKindDef",
DeclKindBody: "DeclKindBody",
DeclKindRange: "DeclKindRange",
}
var _table_DeclKind_GoString = [...]string{
DeclKindInvalid: "ast.DeclKindInvalid",
DeclKindEmpty: "ast.DeclKindEmpty",
DeclKindSyntax: "ast.DeclKindSyntax",
DeclKindPackage: "ast.DeclKindPackage",
DeclKindImport: "ast.DeclKindImport",
DeclKindDef: "ast.DeclKindDef",
DeclKindBody: "ast.DeclKindBody",
DeclKindRange: "ast.DeclKindRange",
}
var _table_DefKind_String = [...]string{
DefKindInvalid: "DefKindInvalid",
DefKindMessage: "DefKindMessage",
DefKindEnum: "DefKindEnum",
DefKindService: "DefKindService",
DefKindExtend: "DefKindExtend",
DefKindField: "DefKindField",
DefKindOneof: "DefKindOneof",
DefKindGroup: "DefKindGroup",
DefKindEnumValue: "DefKindEnumValue",
DefKindMethod: "DefKindMethod",
DefKindOption: "DefKindOption",
}
var _table_DefKind_GoString = [...]string{
DefKindInvalid: "ast.DefKindInvalid",
DefKindMessage: "ast.DefKindMessage",
DefKindEnum: "ast.DefKindEnum",
DefKindService: "ast.DefKindService",
DefKindExtend: "ast.DefKindExtend",
DefKindField: "ast.DefKindField",
DefKindOneof: "ast.DefKindOneof",
DefKindGroup: "ast.DefKindGroup",
DefKindEnumValue: "ast.DefKindEnumValue",
DefKindMethod: "ast.DefKindMethod",
DefKindOption: "ast.DefKindOption",
}
var _table_ExprKind_String = [...]string{
ExprKindInvalid: "ExprKindInvalid",
ExprKindError: "ExprKindError",
ExprKindLiteral: "ExprKindLiteral",
ExprKindPrefixed: "ExprKindPrefixed",
ExprKindPath: "ExprKindPath",
ExprKindRange: "ExprKindRange",
ExprKindArray: "ExprKindArray",
ExprKindDict: "ExprKindDict",
ExprKindField: "ExprKindField",
}
var _table_ExprKind_GoString = [...]string{
ExprKindInvalid: "ast.ExprKindInvalid",
ExprKindError: "ast.ExprKindError",
ExprKindLiteral: "ast.ExprKindLiteral",
ExprKindPrefixed: "ast.ExprKindPrefixed",
ExprKindPath: "ast.ExprKindPath",
ExprKindRange: "ast.ExprKindRange",
ExprKindArray: "ast.ExprKindArray",
ExprKindDict: "ast.ExprKindDict",
ExprKindField: "ast.ExprKindField",
}
var _table_TypeKind_String = [...]string{
TypeKindInvalid: "TypeKindInvalid",
TypeKindError: "TypeKindError",
TypeKindPath: "TypeKindPath",
TypeKindPrefixed: "TypeKindPrefixed",
TypeKindGeneric: "TypeKindGeneric",
}
var _table_TypeKind_GoString = [...]string{
TypeKindInvalid: "ast.TypeKindInvalid",
TypeKindError: "ast.TypeKindError",
TypeKindPath: "ast.TypeKindPath",
TypeKindPrefixed: "ast.TypeKindPrefixed",
TypeKindGeneric: "ast.TypeKindGeneric",
}
var _ iter.Seq[int] // Mark iter as used.
// 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.
//nolint:dupword // Disable for whole file, because the error is in a comment.
package ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
)
// ExprAny is any ExprAny* type in this package.
//
// Values of this type can be obtained by calling an AsAny method on a ExprAny*
// type, such as [ExprPath.AsAny]. It can be type-asserted back to any of
// the concrete ExprAny* types using its own As* methods.
//
// This type is used in lieu of a putative ExprAny interface type to avoid heap
// allocations in functions that would return one of many different ExprAny*
// types.
//
// # Grammar
//
// In addition to the Expr type, we define some exported productions for
// handling operator precedence.
//
// Expr := ExprField | ExprOp
// ExprJuxta := ExprFieldWithColon | ExprOp
// ExprOp := ExprRange | ExprPrefix | ExprSolo
// ExprSolo := ExprLiteral | ExprPath | ExprArray | ExprDict
//
// Note: ExprJuxta is the expression production that is unambiguous when
// expressions are juxtaposed with each other; i.e., ExprJuxta* does not make
// e.g. "foo {}" ambiguous between an [ExprField] or an [ExprPath] followed by
// an [ExprDict].
type ExprAny id.DynNode[ExprAny, ExprKind, *File]
// AsError converts a ExprAny into a ExprError, if that is the type
// it contains.
//
// Otherwise, returns nil.
func (e ExprAny) AsError() ExprError {
if e.Kind() != ExprKindError {
return ExprError{}
}
return id.Wrap(e.Context(), id.ID[ExprError](e.ID().Value()))
}
// AsLiteral converts a ExprAny into a ExprLiteral, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsLiteral() ExprLiteral {
if e.Kind() != ExprKindLiteral {
return ExprLiteral{}
}
return ExprLiteral{
File: e.Context(),
Token: id.Wrap(e.Context().Stream(), id.ID[token.Token](e.ID().Value())),
}
}
// AsPath converts a ExprAny into a ExprPath, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsPath() ExprPath {
if e.Kind() != ExprKindPath {
return ExprPath{}
}
start, end := e.ID().Raw()
return ExprPath{Path: PathID{start: token.ID(start), end: token.ID(end)}.In(e.Context())}
}
// AsPrefixed converts a ExprAny into a ExprPrefixed, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsPrefixed() ExprPrefixed {
if e.Kind() != ExprKindPrefixed {
return ExprPrefixed{}
}
return id.Wrap(e.Context(), id.ID[ExprPrefixed](e.ID().Value()))
}
// AsRange converts a ExprAny into a ExprRange, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsRange() ExprRange {
if e.Kind() != ExprKindRange {
return ExprRange{}
}
return id.Wrap(e.Context(), id.ID[ExprRange](e.ID().Value()))
}
// AsArray converts a ExprAny into a ExprArray, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsArray() ExprArray {
if e.Kind() != ExprKindArray {
return ExprArray{}
}
return id.Wrap(e.Context(), id.ID[ExprArray](e.ID().Value()))
}
// AsDict converts a ExprAny into a ExprDict, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsDict() ExprDict {
if e.Kind() != ExprKindDict {
return ExprDict{}
}
return id.Wrap(e.Context(), id.ID[ExprDict](e.ID().Value()))
}
// AsField converts a ExprAny into a ExprKV, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (e ExprAny) AsField() ExprField {
if e.Kind() != ExprKindField {
return ExprField{}
}
return id.Wrap(e.Context(), id.ID[ExprField](e.ID().Value()))
}
// Span implements [source.Spanner].
func (e ExprAny) Span() source.Span {
// At most one of the below will produce a non-nil type, and that will be
// the span selected by source.Join. If all of them are nil, this produces
// the nil span.
return source.Join(
e.AsLiteral(),
e.AsPath(),
e.AsPrefixed(),
e.AsRange(),
e.AsArray(),
e.AsDict(),
e.AsField(),
)
}
// ExprError represents an unrecoverable parsing error in an expression context.
type ExprError id.Node[ExprError, *File, *rawExprError]
type rawExprError source.Span
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprError) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindError, id.ID[ExprAny](e.ID())))
}
// Span implements [source.Spanner].
func (e ExprError) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
return source.Span(*e.Raw())
}
func (ExprKind) DecodeDynID(lo, hi int32) ExprKind {
switch {
case lo == 0:
return ExprKindInvalid
case lo < 0 && hi > 0:
return ExprKind(^lo)
default:
return ExprKindPath
}
}
func (k ExprKind) EncodeDynID(value int32) (int32, int32, bool) {
return ^int32(k), value, true
}
// exprs is storage for the various kinds of Exprs in a Context.
type exprs struct {
errors arena.Arena[rawExprError]
prefixes arena.Arena[rawExprPrefixed]
ranges arena.Arena[rawExprRange]
arrays arena.Arena[rawExprArray]
dicts arena.Arena[rawExprDict]
fields arena.Arena[rawExprField]
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// ExprArray represents an array of expressions between square brackets.
//
// # Grammar
//
// ExprArray := `[` (ExprJuxta `,`?)*`]`
type ExprArray id.Node[ExprArray, *File, *rawExprArray]
type rawExprArray struct {
brackets token.ID
args []withComma[id.Dyn[ExprAny, ExprKind]]
}
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprArray) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindArray, id.ID[ExprAny](e.ID())))
}
// Brackets returns the token tree corresponding to the whole [...].
//
// May be missing for a synthetic expression.
func (e ExprArray) Brackets() token.Token {
if e.IsZero() {
return token.Zero
}
return id.Wrap(e.Context().Stream(), e.Raw().brackets)
}
// Elements returns the sequence of expressions in this array.
func (e ExprArray) Elements() Commas[ExprAny] {
type slice = commas[ExprAny, id.Dyn[ExprAny, ExprKind]]
if e.IsZero() {
return slice{}
}
return slice{
file: e.Context(),
SliceInserter: seq.NewSliceInserter(
&e.Raw().args,
func(_ int, c withComma[id.Dyn[ExprAny, ExprKind]]) ExprAny {
return id.WrapDyn(e.Context(), c.Value)
},
func(_ int, e ExprAny) withComma[id.Dyn[ExprAny, ExprKind]] {
e.Context().Nodes().panicIfNotOurs(e)
return withComma[id.Dyn[ExprAny, ExprKind]]{Value: e.ID()}
},
),
}
}
// Span implements [source.Spanner].
func (e ExprArray) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
return e.Brackets().Span()
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// ExprDict represents a an array of message fields between curly braces.
//
// # Grammar
//
// ExprDict := `{` fields `}` | `<` fields `>`
// fields := (Expr (`,` | `;`)?)*
//
// Note that if a non-[ExprField] occurs as a field of a dict, the parser will
// rewrite it into an [ExprField] with a missing key.
type ExprDict id.Node[ExprDict, *File, *rawExprDict]
type rawExprDict struct {
braces token.ID
fields []withComma[id.ID[ExprField]]
}
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprDict) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindDict, id.ID[ExprAny](e.ID())))
}
// Braces returns the token tree corresponding to the whole {...}.
//
// May be missing for a synthetic expression.
func (e ExprDict) Braces() token.Token {
if e.IsZero() {
return token.Zero
}
return id.Wrap(e.Context().Stream(), e.Raw().braces)
}
// Elements returns the sequence of expressions in this array.
func (e ExprDict) Elements() Commas[ExprField] {
type slice = commas[ExprField, id.ID[ExprField]]
if e.IsZero() {
return slice{}
}
return slice{
file: e.Context(),
SliceInserter: seq.NewSliceInserter(
&e.Raw().fields,
func(_ int, c withComma[id.ID[ExprField]]) ExprField {
return id.Wrap(e.Context(), c.Value)
},
func(_ int, e ExprField) withComma[id.ID[ExprField]] {
e.Context().Nodes().panicIfNotOurs(e)
return withComma[id.ID[ExprField]]{Value: e.ID()}
},
),
}
}
// Span implements [source.Spanner].
func (e ExprDict) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
return e.Braces().Span()
}
// ExprField is a key-value pair within an [ExprDict].
//
// It implements [ExprAny], since it can appear inside of e.g. an array if the
// user incorrectly writes [foo: bar].
//
// # Grammar
//
// ExprField := ExprFieldWithColon | Expr (ExprDict | ExprArray)
// ExprFieldWithColon := Expr (`:` | `=`) Expr
//
// Note: ExprFieldWithColon appears in ExprJuxta, the expression production that
// is unambiguous when expressions are juxtaposed with each other.
type ExprField id.Node[ExprField, *File, *rawExprField]
type rawExprField struct {
key, value id.Dyn[ExprAny, ExprKind]
colon token.ID
}
// ExprFieldArgs is arguments for [Context.NewExprKV].
type ExprFieldArgs struct {
Key ExprAny
Colon token.Token
Value ExprAny
}
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprField) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindField, id.ID[ExprAny](e.ID())))
}
// Key returns the key for this field.
//
// May be zero if the parser encounters a message expression with a missing field, e.g. {foo, bar: baz}.
func (e ExprField) Key() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), e.Raw().key)
}
// SetKey sets the key for this field.
//
// If passed zero, this clears the key.
func (e ExprField) SetKey(expr ExprAny) {
e.Raw().key = expr.ID()
}
// Colon returns the colon between Key() and Value().
//
// May be zero: it is valid for a field name to be immediately followed by its value and be syntactically
// valid (unlike most "optional" punctuation, this is permitted by Protobuf, not just our permissive AST).
func (e ExprField) Colon() token.Token {
if e.IsZero() {
return token.Zero
}
return id.Wrap(e.Context().Stream(), e.Raw().colon)
}
// Value returns the value for this field.
func (e ExprField) Value() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), e.Raw().value)
}
// SetValue sets the value for this field.
//
// If passed zero, this clears the expression.
func (e ExprField) SetValue(expr ExprAny) {
e.Raw().value = expr.ID()
}
// Span implements [source.Spanner].
func (e ExprField) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
return source.Join(e.Key(), e.Colon(), e.Value())
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/token"
)
// ExprLiteral is an expression corresponding to a string or number literal.
//
// # Grammar
//
// ExprLiteral := token.Number | token.String
type ExprLiteral struct {
File *File
// The token backing this expression. Must be [token.String] or [token.Number].
token.Token
}
// Context returns this literal's context.
//
// This returns a [File] rather than a [token.Stream], which would otherwise
// be returned because ExprLiteral embeds [token.Token].
func (e ExprLiteral) Context() *File {
return e.File
}
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (e ExprLiteral) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(
e.File,
id.NewDyn(ExprKindLiteral, id.ID[ExprAny](e.ID())),
)
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// ExprPrefixed is an expression prefixed with an operator.
//
// # Grammar
//
// ExprPrefix := `-` ExprSolo
type ExprPrefixed id.Node[ExprPrefixed, *File, *rawExprPrefixed]
type rawExprPrefixed struct {
prefix token.ID
expr id.Dyn[ExprAny, ExprKind]
}
// ExprPrefixedArgs is arguments for [Context.NewExprPrefixed].
type ExprPrefixedArgs struct {
Prefix token.Token
Expr ExprAny
}
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprPrefixed) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindPrefixed, id.ID[ExprAny](e.ID())))
}
// Prefix returns this expression's prefix.
//
// Returns [keyword.Unknown] if [TypePrefixed.PrefixToken] does not contain
// a known prefix.
func (e ExprPrefixed) Prefix() keyword.Keyword {
return e.PrefixToken().Keyword()
}
// PrefixToken returns the token representing this expression's prefix.
func (e ExprPrefixed) PrefixToken() token.Token {
if e.IsZero() {
return token.Zero
}
return id.Wrap(e.Context().Stream(), e.Raw().prefix)
}
// Expr returns the expression the prefix is applied to.
func (e ExprPrefixed) Expr() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), e.Raw().expr)
}
// SetExpr sets the expression that the prefix is applied to.
//
// If passed zero, this clears the expression.
func (e ExprPrefixed) SetExpr(expr ExprAny) {
e.Raw().expr = expr.ID()
}
// source.Span implements [source.Spanner].
func (e ExprPrefixed) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
return source.Join(e.PrefixToken(), e.Expr())
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// ExprRange represents a range of values, such as 1 to 4 or 5 to max.
//
// Note that max is not special syntax; it will appear as an [ExprPath] with the name "max".
//
// # Grammar
//
// ExprRange := ExprPrefixed `to` ExprOp
type ExprRange id.Node[ExprRange, *File, *rawExprRange]
type rawExprRange struct {
start, end id.Dyn[ExprAny, ExprKind]
to token.ID
}
// ExprRangeArgs is arguments for [Context.NewExprRange].
type ExprRangeArgs struct {
Start ExprAny
To token.Token
End ExprAny
}
// AsAny type-erases this expression value.
//
// See [ExprAny] for more information.
func (e ExprRange) AsAny() ExprAny {
if e.IsZero() {
return ExprAny{}
}
return id.WrapDyn(e.Context(), id.NewDyn(ExprKindRange, id.ID[ExprAny](e.ID())))
}
// Bounds returns this range's bounds. These are inclusive bounds.
func (e ExprRange) Bounds() (start, end ExprAny) {
if e.IsZero() {
return ExprAny{}, ExprAny{}
}
return id.WrapDyn(e.Context(), e.Raw().start), id.WrapDyn(e.Context(), e.Raw().end)
}
// SetBounds set the expressions for this range's bounds.
//
// Clears the respective expressions when passed a zero expression.
func (e ExprRange) SetBounds(start, end ExprAny) {
e.Raw().start = start.ID()
e.Raw().end = end.ID()
}
// Keyword returns the "to" keyword for this range.
func (e ExprRange) Keyword() token.Token {
if e.IsZero() {
return token.Zero
}
return id.Wrap(e.Context().Stream(), e.Raw().to)
}
// Span implements [source.Spanner].
func (e ExprRange) Span() source.Span {
if e.IsZero() {
return source.Span{}
}
lo, hi := e.Bounds()
return source.Join(lo, e.Keyword(), hi)
}
// 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 ast
import (
"fmt"
"math"
"slices"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// Nodes provides storage for the various AST node types, and can be used
// to construct new ones.
type Nodes File
// File returns the [File] that this Nodes adds nodes to.
func (n *Nodes) File() *File {
return (*File)(n)
}
// NewPathComponent returns a new path component with the given separator and
// name.
//
// sep must be a [token.Keyword] whose value is either '.' or '/'. name must be
// a [token.Ident]. This function will panic if either condition does not
// hold.
//
// To create a path component with an extension value, see [Nodes.NewExtensionComponent].
func (n *Nodes) NewPathComponent(separator, name token.Token) PathComponent {
n.panicIfNotOurs(separator, name)
if !separator.IsZero() {
if separator.Kind() != token.Keyword || (separator.Text() != "." && separator.Text() != "/") {
panic(fmt.Sprintf("protocompile/ast: passed non '.' or '/' separator to NewPathComponent: %s", separator))
}
}
if name.Kind() != token.Ident {
panic("protocompile/ast: passed non-identifier name to NewPathComponent")
}
return PathComponent{
withContext: id.WrapContext(n.File()),
separator: separator.ID(),
name: name.ID(),
}
}
// NewExtensionComponent returns a new extension path component containing the
// given path.
func (n *Nodes) NewExtensionComponent(separator token.Token, path Path) PathComponent {
n.panicIfNotOurs(separator, path)
if !separator.IsZero() {
if separator.Kind() != token.Keyword || (separator.Text() != "." && separator.Text() != "/") {
panic(fmt.Sprintf("protocompile/ast: passed non '.' or '/' separator to NewPathComponent: %s", separator))
}
}
name, ok := n.extnPathCache[path.raw]
if !ok {
stream := n.stream
start := stream.NewPunct("(")
end := stream.NewPunct(")")
var children []token.Token
path.Components(func(pc PathComponent) bool {
if !pc.Separator().IsZero() {
children = append(children, pc.Separator())
}
if !pc.Name().IsZero() {
children = append(children, pc.Name())
}
return true
})
stream.NewFused(start, end, children...)
name = start.ID()
if n.extnPathCache == nil {
n.extnPathCache = make(map[PathID]token.ID)
}
n.extnPathCache[path.raw] = name
}
return PathComponent{
withContext: id.WrapContext(n.File()),
separator: separator.ID(),
name: name,
}
}
// NewPath creates a new synthetic Path.
func (n *Nodes) NewPath(components ...PathComponent) Path {
if len(components) > math.MaxInt16 {
panic("protocompile/ast: cannot build path with more than 2^15 components")
}
for _, t := range components {
n.panicIfNotOurs(t)
}
stream := n.stream
// Every synthetic path looks like a (a.b.c) token tree. Users can't see the
// parens here.
start := stream.NewPunct("(")
end := stream.NewPunct(")")
var children []token.Token
for _, pc := range components {
if !pc.Separator().IsZero() {
children = append(children, pc.Separator())
}
if !pc.Name().IsZero() {
children = append(children, pc.Name())
}
}
stream.NewFused(start, end, children...)
path := PathID{start: start.ID()}.withSynthRange(0, len(children))
if n.extnPathCache == nil {
n.extnPathCache = make(map[PathID]token.ID)
}
n.extnPathCache[path] = path.start
return path.In(n.File())
}
// NewDeclEmpty creates a new DeclEmpty node.
func (n *Nodes) NewDeclEmpty(semicolon token.Token) DeclEmpty {
n.panicIfNotOurs(semicolon)
decl := id.Wrap(n.File(), id.ID[DeclEmpty](n.decls.empties.NewCompressed(rawDeclEmpty{
semi: semicolon.ID(),
})))
return decl
}
// NewDeclSyntax creates a new DeclSyntax node.
func (n *Nodes) NewDeclSyntax(args DeclSyntaxArgs) DeclSyntax {
n.panicIfNotOurs(args.Keyword, args.Equals, args.Value, args.Options, args.Semicolon)
return id.Wrap(n.File(), id.ID[DeclSyntax](n.decls.syntaxes.NewCompressed(rawDeclSyntax{
keyword: args.Keyword.ID(),
equals: args.Equals.ID(),
value: args.Value.ID(),
options: args.Options.ID(),
semi: args.Semicolon.ID(),
})))
}
// NewDeclPackage creates a new DeclPackage node.
func (n *Nodes) NewDeclPackage(args DeclPackageArgs) DeclPackage {
n.panicIfNotOurs(args.Keyword, args.Path, args.Options, args.Semicolon)
return id.Wrap(n.File(), id.ID[DeclPackage](n.decls.packages.NewCompressed(rawDeclPackage{
keyword: args.Keyword.ID(),
path: args.Path.raw,
options: args.Options.ID(),
semi: args.Semicolon.ID(),
})))
}
// NewDeclImport creates a new DeclImport node.
func (n *Nodes) NewDeclImport(args DeclImportArgs) DeclImport {
n.panicIfNotOurs(args.Keyword, args.ImportPath, args.Options, args.Semicolon)
return id.Wrap(n.File(), id.ID[DeclImport](n.decls.imports.NewCompressed(rawDeclImport{
keyword: args.Keyword.ID(),
modifiers: slices.Collect(iterx.Map(
slices.Values(args.Modifiers),
func(t token.Token) token.ID {
n.panicIfNotOurs(t)
return t.ID()
}),
),
importPath: args.ImportPath.ID(),
options: args.Options.ID(),
semi: args.Semicolon.ID(),
})))
}
// NewDeclDef creates a new DeclDef node.
func (n *Nodes) NewDeclDef(args DeclDefArgs) DeclDef {
n.panicIfNotOurs(
args.Keyword, args.Type, args.Name, args.Returns,
args.Equals, args.Value, args.Options, args.Body, args.Semicolon)
raw := rawDeclDef{
name: args.Name.raw,
equals: args.Equals.ID(),
value: args.Value.ID(),
options: args.Options.ID(),
body: args.Body.ID(),
semi: args.Semicolon.ID(),
}
if !args.Type.IsZero() {
raw.ty = args.Type.ID()
} else {
kw := PathID{args.Keyword.ID(), args.Keyword.ID()}.In(n.File())
raw.ty = TypePath{Path: kw}.AsAny().ID()
}
if !args.Returns.IsZero() {
raw.signature = &rawSignature{
returns: args.Returns.ID(),
}
}
return id.Wrap(n.File(), id.ID[DeclDef](n.decls.defs.NewCompressed(raw)))
}
// NewDeclBody creates a new DeclBody node.
//
// To add declarations to the returned body, use [DeclBody.Append].
func (n *Nodes) NewDeclBody(braces token.Token) DeclBody {
n.panicIfNotOurs(braces)
return id.Wrap(n.File(), id.ID[DeclBody](n.decls.bodies.NewCompressed(rawDeclBody{
braces: braces.ID(),
})))
}
// NewDeclRange creates a new DeclRange node.
//
// To add ranges to the returned declaration, use [DeclRange.Append].
func (n *Nodes) NewDeclRange(args DeclRangeArgs) DeclRange {
n.panicIfNotOurs(args.Keyword, args.Options, args.Semicolon)
return id.Wrap(n.File(), id.ID[DeclRange](n.decls.ranges.NewCompressed(rawDeclRange{
keyword: args.Keyword.ID(),
options: args.Options.ID(),
semi: args.Semicolon.ID(),
})))
}
// NewExprPrefixed creates a new ExprPrefixed node.
func (n *Nodes) NewExprPrefixed(args ExprPrefixedArgs) ExprPrefixed {
n.panicIfNotOurs(args.Prefix, args.Expr)
return id.Wrap(n.File(), id.ID[ExprPrefixed](n.exprs.prefixes.NewCompressed(rawExprPrefixed{
prefix: args.Prefix.ID(),
expr: args.Expr.ID(),
})))
}
// NewExprRange creates a new ExprRange node.
func (n *Nodes) NewExprRange(args ExprRangeArgs) ExprRange {
n.panicIfNotOurs(args.Start, args.To, args.End)
return id.Wrap(n.File(), id.ID[ExprRange](n.exprs.ranges.NewCompressed(rawExprRange{
to: args.To.ID(),
start: args.Start.ID(),
end: args.End.ID(),
})))
}
// NewExprArray creates a new ExprArray node.
//
// To add elements to the returned expression, use [ExprArray.Append].
func (n *Nodes) NewExprArray(brackets token.Token) ExprArray {
n.panicIfNotOurs(brackets)
return id.Wrap(n.File(), id.ID[ExprArray](n.exprs.arrays.NewCompressed(rawExprArray{
brackets: brackets.ID(),
})))
}
// NewExprDict creates a new ExprDict node.
//
// To add elements to the returned expression, use [ExprDict.Append].
func (n *Nodes) NewExprDict(braces token.Token) ExprDict {
n.panicIfNotOurs(braces)
return id.Wrap(n.File(), id.ID[ExprDict](n.exprs.dicts.NewCompressed(rawExprDict{
braces: braces.ID(),
})))
}
// NewExprField creates a new ExprPrefixed node.
func (n *Nodes) NewExprField(args ExprFieldArgs) ExprField {
n.panicIfNotOurs(args.Key, args.Colon, args.Value)
return id.Wrap(n.File(), id.ID[ExprField](n.exprs.fields.NewCompressed(rawExprField{
key: args.Key.ID(),
colon: args.Colon.ID(),
value: args.Value.ID(),
})))
}
// NewTypePrefixed creates a new TypePrefixed node.
func (n *Nodes) NewTypePrefixed(args TypePrefixedArgs) TypePrefixed {
n.panicIfNotOurs(args.Prefix, args.Type)
return id.Wrap(n.File(), id.ID[TypePrefixed](n.types.prefixes.NewCompressed(rawTypePrefixed{
prefix: args.Prefix.ID(),
ty: args.Type.ID(),
})))
}
// NewTypeGeneric creates a new TypeGeneric node.
//
// To add arguments to the returned type, use [TypeGeneric.Append].
func (n *Nodes) NewTypeGeneric(args TypeGenericArgs) TypeGeneric {
n.panicIfNotOurs(args.Path, args.AngleBrackets)
return id.Wrap(n.File(), id.ID[TypeGeneric](n.types.generics.NewCompressed(rawTypeGeneric{
path: args.Path.raw,
args: rawTypeList{brackets: args.AngleBrackets.ID()},
})))
}
// NewCompactOptions creates a new CompactOptions node.
func (n *Nodes) NewCompactOptions(brackets token.Token) CompactOptions {
n.panicIfNotOurs(brackets)
return id.Wrap(n.File(), id.ID[CompactOptions](n.options.NewCompressed(rawCompactOptions{
brackets: brackets.ID(),
})))
}
// panicIfNotOurs checks that a contextual value is owned by this context, and panics if not.
//
// Does not panic if that is zero or has a zero context. Panics if n is zero.
func (n *Nodes) panicIfNotOurs(that ...any) {
for _, that := range that {
if that == nil {
continue
}
var path string
switch that := that.(type) {
case interface{ Context() *token.Stream }:
ctx := that.Context()
if ctx == nil || ctx == n.File().Stream() {
continue
}
path = ctx.Path()
case interface{ Context() *File }:
ctx := that.Context()
if ctx == nil || ctx == n.File() {
continue
}
path = ctx.Stream().Path()
default:
continue
}
panic(fmt.Sprintf(
"protocompile/ast: attempt to mix different contexts: %q vs %q",
n.stream.Path(),
path,
))
}
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// CompactOptions represents the collection of options attached to a [DeclAny],
// contained within square brackets.
//
// # Grammar
//
// CompactOptions := `[` (option `,`?)? `]`
// option := Path [:=]? Expr?
type CompactOptions id.Node[CompactOptions, *File, *rawCompactOptions]
type rawCompactOptions struct {
brackets token.ID
options []withComma[rawOption]
}
// Option is a key-value pair inside of a [CompactOptions] or a [DefOption].
type Option struct {
Path Path
Equals token.Token
Value ExprAny
}
// Span implements [source.Spanner].
func (o Option) Span() source.Span {
return source.Join(o.Path, o.Equals, o.Value)
}
type rawOption struct {
path PathID
equals token.ID
value id.Dyn[ExprAny, ExprKind]
}
// Brackets returns the token tree corresponding to the whole [...].
func (o CompactOptions) Brackets() token.Token {
if o.IsZero() {
return token.Zero
}
return id.Wrap(o.Context().Stream(), o.Raw().brackets)
}
// Entries returns the sequence of options in this CompactOptions.
func (o CompactOptions) Entries() Commas[Option] {
type slice = commas[Option, rawOption]
if o.IsZero() {
return slice{}
}
return slice{
file: o.Context(),
SliceInserter: seq.NewSliceInserter(
&o.Raw().options,
func(_ int, c withComma[rawOption]) Option {
return c.Value.With(o.Context())
},
func(_ int, v Option) withComma[rawOption] {
o.Context().Nodes().panicIfNotOurs(v.Path, v.Equals, v.Value)
return withComma[rawOption]{Value: rawOption{
path: v.Path.ID(),
equals: v.Equals.ID(),
value: v.Value.ID(),
}}
},
),
}
}
// Span implements [source.Spanner].
func (o CompactOptions) Span() source.Span {
if o.IsZero() {
return source.Span{}
}
return o.Brackets().Span()
}
func (o *rawOption) With(f *File) Option {
if o == nil {
return Option{}
}
return Option{
Path: o.path.In(f),
Equals: id.Wrap(f.Stream(), o.equals),
Value: id.WrapDyn(f, o.value),
}
}
// 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 ast
import (
"fmt"
"strings"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// Path represents a multi-part identifier.
//
// This includes single identifiers like foo, references like foo.bar,
// and fully-qualified names like .foo.bar.
//
// # Grammar
//
// Path := `.`? component (sep component)*
//
// component := token.Ident | `(` Path `)`
// sep := `.` | `/`
type Path struct {
// The layout of this type is depended on in ast2/path.go
withContext
raw PathID
}
// PathID identifies a [Path] in a [Context].
type PathID struct {
// This has one of the following configurations.
//
// 1. Two zero tokens. This is the zero path.
//
// 2. Two natural tokens. This means the path is all tokens between them,
// including the end-point.
//
// 3. Two synthetic tokens. The former is a an actual token, whose children
// are the path tokens. The latter is a packed pair of uint16s representing
// the subslice of Start.children that the path uses. This is necessary to
// implement Split() for synthetic paths.
//
// The case Start < 0 && End > 0 is reserved for use by pathLike. The case
// Start < 0 && End == 0 is currently unused.
start, end token.ID
}
// In wraps this ID with a context.
func (p PathID) In(f *File) Path {
if p.start.IsZero() {
return Path{}
}
if p.end.IsZero() {
panic(fmt.Sprintf("protocompile/ast: invalid ast.Path representation %v; this is a bug in protocompile", p))
}
return Path{id.WrapContext(f), p}
}
// ID returns this path's ID.
func (p Path) ID() PathID {
return p.raw
}
// Absolute returns whether this path starts with a dot.
func (p Path) Absolute() bool {
first, ok := iterx.First(p.Components)
return ok && !first.Separator().IsZero()
}
// IsSynthetic returns whether this path was created with [Nodes.NewPath].
func (p Path) IsSynthetic() bool {
return p.raw.start < 0
}
// ToRelative converts this path into a relative path, by deleting all leading
// separators. In particular, the path "..foo", which contains empty components,
// will be converted into "foo".
//
// If called on zero or a relative path, returns p.
func (p Path) ToRelative() Path {
for pc := range p.Components {
if !pc.IsEmpty() {
p.raw.start = pc.name
break
}
}
return p
}
// AsIdent returns the single identifier that comprises this path, or
// the zero token.
func (p Path) AsIdent() token.Token {
first, _ := iterx.OnlyOne(p.Components)
if !first.Separator().IsZero() {
return token.Zero
}
return first.AsIdent()
}
// AsPredeclared returns the [predeclared.Name] that this path represents.
//
// If this path does not represent a builtin, returns [predeclared.Unknown].
func (p Path) AsPredeclared() predeclared.Name {
return predeclared.FromKeyword(p.AsKeyword())
}
// AsKeyword returns the [keyword.Keyword] that this path represents.
//
// If this path does not represent a builtin, returns [keyword.Unknown].
func (p Path) AsKeyword() keyword.Keyword {
return p.AsIdent().Keyword()
}
// IsIdents returns whether p is a sequence of exactly the given identifiers.
func (p Path) IsIdents(idents ...string) bool {
for i, pc := range iterx.Enumerate(p.Components) {
if i >= len(idents) || pc.AsIdent().Text() != idents[i] {
break
}
if i == len(idents)-1 {
return true
}
}
return false
}
// source.Span implements [source.Spanner].
func (p Path) Span() source.Span {
// No need to check for zero here, if p is zero both start and end will be
// zero tokens.
return source.Join(
id.Wrap(p.Context().Stream(), p.raw.start),
id.Wrap(p.Context().Stream(), p.raw.end),
)
}
// Components is an [iter.Seq] that ranges over each component in this path.
// Specifically, it yields the (possibly zero) dot that precedes the component,
// and the identifier token.
func (p Path) Components(yield func(PathComponent) bool) {
if p.IsZero() {
return
}
var cursor *token.Cursor
first := id.Wrap(p.Context().Stream(), p.raw.start)
if p.IsSynthetic() {
cursor = first.SyntheticChildren(p.raw.synthRange())
} else {
cursor = token.NewCursorAt(first)
}
var sep token.Token
var idx uint32
for tok := range cursor.Rest() {
if !p.IsSynthetic() && tok.ID() > p.raw.end {
// We've reached the end of the path.
break
}
if tok.Text() == "." || tok.Text() == "/" {
if !sep.IsZero() {
// Uh-oh, empty path component!
if !yield(PathComponent{p.withContext, p.raw, sep.ID(), 0, idx}) {
return
}
idx++
}
sep = tok
continue
}
if !yield(PathComponent{p.withContext, p.raw, sep.ID(), tok.ID(), idx}) {
return
}
idx++
sep = token.Zero
}
if !sep.IsZero() {
yield(PathComponent{p.withContext, p.raw, sep.ID(), 0, idx})
}
}
// Split splits a path at the given path component index, producing two
// new paths where the first contains the first n components and the second
// contains the rest. If n is negative or greater than the number of components
// in p, both returned paths will be zero.
//
// The suffix will be absolute, except in the following cases:
// 1. n == 0 and p is not absolute (prefix will be zero and suffix will be p).
// 2. n is equal to the length of p (suffix will be zero and prefix will be p).
//
// This operation runs in O(n) time.
func (p Path) Split(n int) (prefix, suffix Path) {
if n < 0 || p.IsZero() {
return Path{}, Path{}
}
if n == 0 {
return Path{}, p
}
var i int
var prev PathComponent
var found bool
for pc := range p.Components {
if n > 0 {
prev = pc
n--
if !pc.Separator().IsZero() {
i++
}
if !pc.Name().IsZero() {
i++
}
continue
}
prefix, suffix = p, p
found = true
if p.IsSynthetic() {
a, _ := prefix.raw.synthRange()
prefix.raw = prefix.raw.withSynthRange(a, a+i)
a, b := suffix.raw.synthRange()
a += i
suffix.raw = suffix.raw.withSynthRange(a, b)
continue
}
if !prev.name.IsZero() {
prefix.raw.end = prev.name
} else {
prefix.raw.end = prev.separator
}
if !pc.separator.IsZero() {
suffix.raw.start = pc.separator
} else {
suffix.raw.start = pc.name
}
break
}
if !found {
return p, Path{}
}
return prefix, suffix
}
// Canonicalized returns a string containing this path's value after
// canonicalization.
//
// Canonicalization converts a path into something that can be used for name
// resolution. This includes removing extra separators and deleting whitespace
// and comments.
func (p Path) Canonicalized() string {
// Most paths are already in canonical form. Verify this before allocating
// a fresh string.
if id := p.AsIdent(); !id.IsZero() {
return id.Name()
} else if p.isCanonical() {
return p.Span().Text()
}
var out strings.Builder
p.canonicalized(&out)
return out.String()
}
func (p Path) canonicalized(out *strings.Builder) {
for i, pc := range iterx.Enumerate(p.Components) {
if pc.Name().IsZero() {
continue
}
if i > 0 || !pc.Separator().IsZero() {
out.WriteString(pc.Separator().Text())
}
if id := pc.Name(); !id.IsZero() {
out.WriteString(id.Name())
} else {
out.WriteByte('(')
pc.AsExtension().canonicalized(out)
out.WriteByte(')')
}
}
}
func (p Path) isCanonical() bool {
var prev PathComponent
for pc := range p.Components {
sep := pc.Separator()
name := pc.Name()
if name.IsZero() {
return false
}
if !sep.IsZero() && sep.Span().End != name.Span().Start {
return false
}
if extn := pc.AsExtension(); !extn.IsZero() {
if !extn.isCanonical() {
return false
}
// Ensure that the parens tightly wrap extn.
parens := name.Span()
extn := extn.Span()
if parens.Start+1 != extn.Start || parens.End-1 != extn.End {
return false
}
} else if pc.AsIdent().Text() != pc.AsIdent().Name() {
return false
}
if !prev.IsZero() {
if sep.IsZero() {
return false
}
if prev.Name().Span().End != sep.Span().Start {
return false
}
}
prev = pc
}
return true
}
// trim discards any skippable tokens before and after the start of this path.
func (p Path) trim() Path {
for p.raw.start < p.raw.end &&
id.Wrap(p.Context().Stream(), p.raw.start).Kind().IsSkippable() {
p.raw.start++
}
for p.raw.start < p.raw.end &&
id.Wrap(p.Context().Stream(), p.raw.end).Kind().IsSkippable() {
p.raw.end--
}
if p.raw.start <= p.raw.end {
return p
}
return Path{}
}
// TypePath is a simple path reference as a type.
//
// # Grammar
//
// TypePath := Path
type TypePath struct {
// The path that refers to this type.
Path
}
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (t TypePath) AsAny() TypeAny {
return id.WrapDyn(t.Context(), id.NewDynFromRaw[TypeAny, TypeKind](int32(t.raw.start), int32(t.raw.end)))
}
// ExprPath is a simple path reference in expression position.
//
// # Grammar
//
// ExprPath := Path
type ExprPath struct {
// The path backing this expression.
Path
}
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (e ExprPath) AsAny() ExprAny {
return id.WrapDyn(e.Context(), id.NewDynFromRaw[ExprAny, ExprKind](int32(e.raw.start), int32(e.raw.end)))
}
// PathComponent is a piece of a path. This is either an identifier or a nested path
// (for an extension name).
type PathComponent struct {
withContext
path PathID
separator, name token.ID
idx uint32
}
// Path returns the path that this component is part of.
func (p PathComponent) Path() Path {
return Path{p.withContext, p.path}
}
// IsFirst returns whether this is the first component of its path.
func (p PathComponent) IsFirst() bool {
if p.Path().IsSynthetic() {
return p.idx == 0
}
return p.separator == p.path.start || p.name == p.path.start
}
// IsLast returns whether this is the last component of its path.
func (p PathComponent) IsLast() bool {
if p.Path().IsSynthetic() {
i, j := p.path.synthRange()
return int(p.idx) == j-i
}
return p.separator == p.path.end || p.name == p.path.end
}
// SplitBefore splits the path that this component came from around the
// component boundary before this component.
//
// after's first component will be this component.
//
// Not currently implemented for synthetic paths.
func (p PathComponent) SplitBefore() (before, after Path) {
if p.IsFirst() {
return Path{}, p.Path()
}
if p.Path().IsSynthetic() {
panic("protocompile/ast: called PathComponent.SplitBefore with synthetic path")
}
prefix, suffix := p.Path(), p.Path()
if p.separator.IsZero() {
prefix.raw.end = id.Wrap(p.Context().Stream(), p.name).Prev().ID()
suffix.raw.start = p.name
} else {
prefix.raw.end = id.Wrap(p.Context().Stream(), p.separator).Prev().ID()
suffix.raw.start = p.separator
}
return prefix.trim(), suffix.trim()
}
// SplitAfter splits the path that this component came from around the
// component boundary after this component.
//
// before's last component will be this component.
func (p PathComponent) SplitAfter() (before, after Path) {
if p.IsLast() {
return p.Path(), Path{}
}
if p.Path().IsSynthetic() {
panic("protocompile/ast: called PathComponent.SplitAfter with synthetic path")
}
prefix, suffix := p.Path(), p.Path()
if !p.name.IsZero() {
prefix.raw.end = p.name
suffix.raw.start = id.Wrap(p.Context().Stream(), p.name).Next().ID()
} else {
prefix.raw.end = p.separator
suffix.raw.start = id.Wrap(p.Context().Stream(), p.separator).Next().ID()
}
return prefix.trim(), suffix.trim()
}
// Separator is the token that separates this component from the previous one, if
// any. This may be a dot or a slash.
func (p PathComponent) Separator() token.Token {
return id.Wrap(p.Context().Stream(), p.separator)
}
// Name is the token that represents this component's name. THis is either an
// identifier or a (...) token containing a path.
func (p PathComponent) Name() token.Token {
return id.Wrap(p.Context().Stream(), p.name)
}
// Returns whether this is an empty path component. Such components are not allowed
// in the grammar but may occur in invalid inputs nonetheless.
func (p PathComponent) IsEmpty() bool {
return p.Name().IsZero()
}
// Next returns the next path component after this one, if there is one.
func (p PathComponent) Next() PathComponent {
_, after := p.SplitAfter()
next, _ := iterx.First(after.Components)
return next
}
// AsExtension returns the Path inside of this path component, if it is an extension
// path component, i.e. (a.b.c).
//
// This is unrelated to the [foo.bar/my.Type] URL-like Any paths that appear in
// some expressions. Those are represented by allowing / as an alternative
// separator to . in paths.
func (p PathComponent) AsExtension() Path {
if p.Name().IsZero() || p.Name().IsLeaf() {
return Path{}
}
// If this is a synthetic token, its children are already precisely a path,
// so we can use the "synthetic with children" form of Path.
if p.Name().IsSynthetic() {
return Path{p.withContext, PathID{p.Name().ID(), 0}}
}
// Find the first and last non-skippable tokens to be the bounds.
var first, last token.Token
for tok := range p.Name().Children().Rest() {
if first.IsZero() {
first = tok
}
last = tok
}
return PathID{first.ID(), last.ID()}.In(p.Context())
}
// AsIdent returns the single identifier that makes up this path component, if
// it is not an extension path component.
//
// May be zero, in the case of e.g. the second component of foo..bar.
func (p PathComponent) AsIdent() token.Token {
tok := id.Wrap(p.Context().Stream(), p.name)
if tok.Kind() == token.Ident {
return tok
}
return token.Zero
}
// Span implements [source.Spanner].
func (p PathComponent) Span() source.Span {
return source.Join(p.Separator(), p.Name())
}
func (p PathID) synthRange() (start, end int) {
return int(^uint16(p.end)), int(^uint16(p.end >> 16))
}
func (p PathID) withSynthRange(start, end int) PathID {
p.end = token.ID(^uint16(start)) | (token.ID(^uint16(end)) << 16)
return p
}
// 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 a subset of the [keyword.Keyword] enum which are names that are
// special for name resolution. 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
import (
"fmt"
"iter"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// 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 keyword.Keyword
const (
Unknown = Name(keyword.Unknown)
Int32 = Name(keyword.Int32)
Int64 = Name(keyword.Int64)
UInt32 = Name(keyword.Uint32)
UInt64 = Name(keyword.Uint64)
SInt32 = Name(keyword.Sint32)
SInt64 = Name(keyword.Sint64)
Fixed32 = Name(keyword.Fixed32)
Fixed64 = Name(keyword.Fixed64)
SFixed32 = Name(keyword.Sfixed32)
SFixed64 = Name(keyword.Sfixed64)
Float = Name(keyword.Float)
Double = Name(keyword.Double)
Bool = Name(keyword.Bool)
String = Name(keyword.String)
Bytes = Name(keyword.Bytes)
Inf = Name(keyword.Inf)
NAN = Name(keyword.NaN)
True = Name(keyword.True)
False = Name(keyword.False)
Map = Name(keyword.Map)
Max = Name(keyword.Max)
Float32 = Float
Float64 = Double
)
// predeclaredToFDPType maps the scalar predeclared [Name]s to their respective
// [descriptorpb.FieldDescriptorProto_Type].
var predeclaredToFDPType = []descriptorpb.FieldDescriptorProto_Type{
Int32: descriptorpb.FieldDescriptorProto_TYPE_INT32,
Int64: descriptorpb.FieldDescriptorProto_TYPE_INT64,
UInt32: descriptorpb.FieldDescriptorProto_TYPE_UINT32,
UInt64: descriptorpb.FieldDescriptorProto_TYPE_UINT64,
SInt32: descriptorpb.FieldDescriptorProto_TYPE_SINT32,
SInt64: descriptorpb.FieldDescriptorProto_TYPE_SINT64,
Fixed32: descriptorpb.FieldDescriptorProto_TYPE_FIXED32,
Fixed64: descriptorpb.FieldDescriptorProto_TYPE_FIXED64,
SFixed32: descriptorpb.FieldDescriptorProto_TYPE_SFIXED32,
SFixed64: descriptorpb.FieldDescriptorProto_TYPE_SFIXED64,
Float32: descriptorpb.FieldDescriptorProto_TYPE_FLOAT,
Float64: descriptorpb.FieldDescriptorProto_TYPE_DOUBLE,
Bool: descriptorpb.FieldDescriptorProto_TYPE_BOOL,
String: descriptorpb.FieldDescriptorProto_TYPE_STRING,
Bytes: descriptorpb.FieldDescriptorProto_TYPE_BYTES,
}
// FromKeyword performs a vast from a [keyword.Keyword], but also validates
// that it is in-range. If it isn't, returns [Unknown].
func FromKeyword(kw keyword.Keyword) Name {
n := Name(kw)
if n.InRange() {
return n
}
return Unknown
}
// String implements [fmt.Stringer].
func (n Name) String() string {
if !n.InRange() {
return fmt.Sprintf("Name(%d)", int(n))
}
return keyword.Keyword(n).String()
}
// GoString implements [fmt.GoStringer].
func (n Name) GoString() string {
if !n.InRange() {
return fmt.Sprintf("predeclared.Name(%d)", int(n))
}
return keyword.Keyword(n).GoString()
}
// FDPType returns the [descriptorpb.FieldDescriptorProto_Type] for the predeclared name,
// if it is a scalar type. Otherwise, it returns 0.
func (n Name) FDPType() descriptorpb.FieldDescriptorProto_Type {
kind, _ := slicesx.Get(predeclaredToFDPType, n)
return kind
}
// InRange returns whether this name value is within the range of declared
// values.
func (n Name) InRange() bool {
return n == Unknown || (n >= Int32 && n <= Max)
}
// IsScalar returns whether a predeclared name corresponds to one of the
// primitive scalar types.
func (n Name) IsScalar() bool {
return n >= Int32 && n <= Bytes
}
// IsInt returns whether this is an integer type.
func (n Name) IsInt() bool {
return n >= Int32 && n <= SFixed64
}
// IsNumber returns whether this is a numeric type.
func (n Name) IsNumber() bool {
return n >= Int32 && n <= Double
}
// IsPackable returns whether this is a type that can go in a packed repeated
// field.
func (n Name) IsPackable() bool {
return n >= Int32 && n <= Bool
}
// IsVarint returns whether this is a varint-encoded type.
func (n Name) IsVarint() bool {
return n >= Int32 && n <= SInt64 || n == Bool
}
// IsZigZag returns whether this is a ZigZag varint-encoded type.
func (n Name) IsZigZag() bool {
return n == SInt32 || n == SInt64
}
// IsFixed returns whether this is a fixed-width type.
func (n Name) IsFixed() bool {
return n >= Fixed32 && n <= Double
}
// IsUnsigned returns whether this is an unsigned integer type.
func (n Name) IsUnsigned() bool {
switch n {
case UInt32, UInt64, Fixed32, Fixed64:
return true
default:
return false
}
}
// IsSigned returns whether this is a signed integer type.
func (n Name) IsSigned() bool {
return n.IsInt() && !n.IsUnsigned()
}
// IsFloat returns whether this is a floating-point type.
func (n Name) IsFloat() bool {
return n == Float32 || n == Float64
}
// IsString returns whether this is a string type (string or bytes).
func (n Name) IsString() bool {
return n == String || n == Bytes
}
// Bits returns the bit size of a name satisfying [Name.IsNumber].
//
// Return 0 for all other names.
func (n Name) Bits() int {
switch n {
case Int32, UInt32, SInt32, Fixed32, SFixed32, Float32:
return 32
case Int64, UInt64, SInt64, Fixed64, SFixed64, Float64:
return 64
default:
return 0
}
}
// IsMapKey returns whether this predeclared name represents one of the map key
// types.
func (n Name) IsMapKey() bool {
return (n >= Int32 && n <= SFixed64) || n == Bool || n == String
}
// Lookup looks up a predefined identifier by name.
//
// If name does not name a predefined identifier, returns [Unknown].
func Lookup(s string) Name {
return FromKeyword(keyword.Lookup(s))
}
// All returns an iterator over all distinct [Name] values.
func All() iter.Seq[Name] {
return func(yield func(Name) bool) {
if !yield(Unknown) {
return
}
for i := Int32; i <= Max; i++ {
if !yield(i) {
return
}
}
}
}
// 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 (
"iter"
"strconv"
)
// LatestImplementedEdition is the most recent edition that the compiler
// implements.
const LatestImplementedEdition = Edition2024
// All returns an iterator over all known [Syntax] values.
func All() iter.Seq[Syntax] {
return func(yield func(Syntax) bool) {
_ = yield(Proto2) &&
yield(Proto3) &&
yield(Edition2023) &&
yield(Edition2024)
}
}
// Editions returns an iterator over all the editions in this package.
func Editions() iter.Seq[Syntax] {
return func(yield func(Syntax) bool) {
_ = yield(Edition2023) &&
yield(Edition2024)
}
}
// IsEdition returns whether this represents an edition.
func (s Syntax) IsEdition() bool {
return s != Proto2 && s != Proto3
}
// IsSupported returns whether this syntax is fully supported.
func (s Syntax) IsSupported() bool {
switch s {
case Proto2, Proto3, Edition2023, Edition2024:
return true
default:
return false
}
}
// IsValid returns whether this syntax is valid (i.e., it can appear in a
// syntax/edition declaration).
func (s Syntax) IsValid() bool {
switch s {
case Proto2, Proto3, Edition2023, Edition2024:
return true
default:
return false
}
}
// IsKnown returns whether this syntax is a known value in google.protobuf.Edition.
func (s Syntax) IsKnown() bool {
switch s {
case Unknown, EditionLegacy,
Proto2, Proto3, Edition2023, Edition2024,
EditionTest1, EditionTest2, EditionTest99997, EditionTest99998, EditionTest99999,
EditionMax:
return true
default:
return false
}
}
// IsConstraint returns whether this syntax can be used as a constraint in
// google.protobuf.FieldOptions.feature_support.
func (s Syntax) IsConstraint() bool {
switch s {
case Proto2, Proto3, Edition2023, Edition2024,
EditionLegacy:
return true
default:
return false
}
}
// DescriptorName converts a syntax into the corresponding google.protobuf.Edition name.
//
// Returns a stringified digit if it is not a named edition value.
func (s Syntax) DescriptorName() string {
name := descriptorNames[s]
if name != "" {
return name
}
return strconv.Itoa(int(s))
}
var descriptorNames = map[Syntax]string{
Unknown: "EDITION_UNKNOWN",
EditionLegacy: "EDITION_LEGACY",
Proto2: "EDITION_PROTO2",
Proto3: "EDITION_PROTO3",
Edition2023: "EDITION_2023",
Edition2024: "EDITION_2024",
EditionTest1: "EDITION_1_TEST_ONLY",
EditionTest2: "EDITION_2_TEST_ONLY",
EditionTest99997: "EDITION_99997_TEST_ONLY",
EditionTest99998: "EDITION_99998_TEST_ONLY",
EditionTest99999: "EDITION_99999_TEST_ONLY",
EditionMax: "EDITION_MAX",
}
// 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
}
// 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"
"iter"
)
// Syntax is a known syntax pragma.
//
// Not only does this include "proto2" and "proto3", but also all of the
// editions.
//
// The integer values of the constants correspond to their the values of
// google.protobuf.Edition.
type Syntax int32
const (
Unknown Syntax = 0
Proto2 Syntax = 998
Proto3 Syntax = 999
Edition2023 Syntax = 1000
Edition2024 Syntax = 1001
EditionLegacy Syntax = 900
EditionTest1 Syntax = 1
EditionTest2 Syntax = 2
EditionTest99997 Syntax = 99997
EditionTest99998 Syntax = 99998
EditionTest99999 Syntax = 99999
EditionMax Syntax = 0x7fff_ffff
)
// String implements [fmt.Stringer].
func (v Syntax) String() string {
s, ok := _table_Syntax_String[v]
if !ok {
return fmt.Sprintf("Syntax(%v)", int(v))
}
return s
}
// GoString implements [fmt.GoStringer].
func (v Syntax) GoString() string {
s, ok := _table_Syntax_GoString[v]
if !ok {
return fmt.Sprintf("syntax.Syntax(%v)", int(v))
}
return s
}
// 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]
}
var _table_Syntax_String = map[Syntax]string{
Unknown: "<unknown>",
Proto2: "proto2",
Proto3: "proto3",
Edition2023: "2023",
Edition2024: "2024",
EditionLegacy: "buf/legacy",
EditionTest1: "buf/1",
EditionTest2: "buf/2",
EditionTest99997: "buf/99997",
EditionTest99998: "buf/99998",
EditionTest99999: "buf/99999",
EditionMax: "buf/max",
}
var _table_Syntax_GoString = map[Syntax]string{
Unknown: "syntax.Unknown",
Proto2: "syntax.Proto2",
Proto3: "syntax.Proto3",
Edition2023: "syntax.Edition2023",
Edition2024: "syntax.Edition2024",
EditionLegacy: "syntax.EditionLegacy",
EditionTest1: "syntax.EditionTest1",
EditionTest2: "syntax.EditionTest2",
EditionTest99997: "syntax.EditionTest99997",
EditionTest99998: "syntax.EditionTest99998",
EditionTest99999: "syntax.EditionTest99999",
EditionMax: "syntax.EditionMax",
}
var _table_Syntax_Lookup = map[string]Syntax{
"proto2": Proto2,
"proto3": Proto3,
"2023": Edition2023,
"2024": Edition2024,
"buf/legacy": EditionLegacy,
"buf/1": EditionTest1,
"buf/2": EditionTest2,
"buf/99997": EditionTest99997,
"buf/99998": EditionTest99998,
"buf/99999": EditionTest99999,
"buf/max": EditionMax,
}
var _ iter.Seq[int] // Mark iter as used.
// 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.
//nolint:dupword // Disable for whole file, because the error is in a comment.
package ast
import (
"iter"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
)
// TypeAny is any Type* type in this package.
//
// Values of this type can be obtained by calling an AsAny method on a Type*
// type, such as [TypePath.AsAny]. It can be type-asserted back to any of
// the concrete Type* types using its own As* methods.
//
// This type is used in lieu of a putative Type interface type to avoid heap
// allocations in functions that would return one of many different Type*
// types.
//
// # Grammar
//
// Type := TypePath | TypePrefixed | TypeGeneric
//
// Note that parsing a type cannot always be greedy. Consider that, if parsed
// as a type, "optional optional foo" could be parsed as:
//
// TypePrefix{Optional, TypePrefix{Optional, TypePath("foo")}}
//
// However, if we want to parse a type followed by a [Path], it needs to parse
// as follows:
//
// TypePrefix{Optional, TypePath("optional")}, Path("foo")
//
// Thus, parsing a type is greedy except when the containing production contains
// "Type Path?" or similar, in which case parsing must be greedy up to the last
// [Path] it would otherwise consume.
type TypeAny id.DynNode[TypeAny, TypeKind, *File]
// AsError converts a TypeAny into a TypeError, if that is the type
// it contains.
//
// Otherwise, returns nil.
func (t TypeAny) AsError() TypeError {
if t.Kind() != TypeKindError {
return TypeError{}
}
return id.Wrap(t.Context(), id.ID[TypeError](t.ID().Value()))
}
// AsPath converts a TypeAny into a TypePath, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (t TypeAny) AsPath() TypePath {
if t.Kind() != TypeKindPath {
return TypePath{}
}
start, end := t.ID().Raw()
return TypePath{Path: PathID{start: token.ID(start), end: token.ID(end)}.In(t.Context())}
}
// AsPrefixed converts a TypeAny into a TypePrefix, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (t TypeAny) AsPrefixed() TypePrefixed {
if t.Kind() != TypeKindPrefixed {
return TypePrefixed{}
}
return id.Wrap(t.Context(), id.ID[TypePrefixed](t.ID().Value()))
}
// AsGeneric converts a TypeAny into a TypePrefix, if that is the type
// it contains.
//
// Otherwise, returns zero.
func (t TypeAny) AsGeneric() TypeGeneric {
if t.Kind() != TypeKindGeneric {
return TypeGeneric{}
}
return id.Wrap(t.Context(), id.ID[TypeGeneric](t.ID().Value()))
}
// Prefixes is an iterator over all [TypePrefix]es wrapping this type.
func (t TypeAny) Prefixes() iter.Seq[TypePrefixed] {
return func(yield func(TypePrefixed) bool) {
for t.Kind() == TypeKindPrefixed {
prefixed := t.AsPrefixed()
if !yield(prefixed) {
return
}
t = prefixed.Type()
}
}
}
// RemovePrefixes removes all [TypePrefix] values wrapping this type.
func (t TypeAny) RemovePrefixes() TypeAny {
for t.Kind() == TypeKindPrefixed {
t = t.AsPrefixed().Type()
}
return t
}
// source.Span implements [source.Spanner].
func (t TypeAny) Span() source.Span {
// At most one of the below will produce a non-zero type, and that will be
// the span selected by source.Join. If all of them are zero, this produces
// the zero span.
return source.Join(
t.AsPath(),
t.AsPrefixed(),
t.AsGeneric(),
)
}
// TypeError represents an unrecoverable parsing error in a type context.
//
// This type is so named to adhere to package ast's naming convention. It does
// not represent a "type error" as in "type-checking failure".
type TypeError id.Node[TypeError, *File, *rawTypeError]
type rawTypeError source.Span
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (t TypeError) AsAny() TypeAny {
if t.IsZero() {
return TypeAny{}
}
return id.WrapDyn(t.Context(), id.NewDyn(TypeKindError, id.ID[TypeAny](t.ID())))
}
// Span implements [source.Spanner].
func (t TypeError) Span() source.Span {
if t.IsZero() {
return source.Span{}
}
return source.Span(*t.Raw())
}
func (TypeKind) DecodeDynID(lo, hi int32) TypeKind {
switch {
case lo == 0:
return TypeKindInvalid
case lo < 0 && hi > 0:
return TypeKind(^lo)
default:
return TypeKindPath
}
}
func (k TypeKind) EncodeDynID(value int32) (int32, int32, bool) {
return ^int32(k), value, true
}
// types is storage for every kind of Type in a Context.Raw().
type types struct {
prefixes arena.Arena[rawTypePrefixed]
generics arena.Arena[rawTypeGeneric]
errors arena.Arena[rawTypeError]
}
// 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 ast
import (
"slices"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// TypeGeneric is a type with generic arguments.
//
// Protobuf does not have generics... mostly. It has the map<K, V> production,
// which looks like something that generalizes, but doesn't. It is useful to parse
// when users mistakenly think this generalizes or provide the incorrect number
// of arguments.
//
// You will usually want to immediately call [TypeGeneric.Map] to codify the assumption
// that all generic types understood by your code are maps.
//
// TypeGeneric implements [Commas[TypeAny]] for accessing its arguments.
//
// # Grammar
//
// TypeGeneric := TypePath `<` (Type `,`?`)* `>`
type TypeGeneric id.Node[TypeGeneric, *File, *rawTypeGeneric]
type rawTypeGeneric struct {
path PathID
args rawTypeList
}
// TypeGenericArgs is the arguments for [Context.NewTypeGeneric].
//
// Generic arguments should be added after construction with [TypeGeneric.AppendComma].
type TypeGenericArgs struct {
Path Path
AngleBrackets token.Token
}
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (t TypeGeneric) AsAny() TypeAny {
if t.IsZero() {
return TypeAny{}
}
return id.WrapDyn(t.Context(), id.NewDyn(TypeKindGeneric, id.ID[TypeAny](t.ID())))
}
// Path returns the path of the "type constructor". For example, for
// my.Map<K, V>, this would return the path my.Map.
func (t TypeGeneric) Path() Path {
if t.IsZero() {
return Path{}
}
return t.Raw().path.In(t.Context())
}
// AsMap extracts the key/value types out of this generic type, checking that it's actually a
// map<K, V>. This is intended for asserting the extremely common case of "the only generic
// type is map".
//
// Returns zeros if this is not a map, or it has the wrong number of generic arguments.
func (t TypeGeneric) AsMap() (key, value TypeAny) {
if t.Path().AsPredeclared() != predeclared.Map || t.Args().Len() != 2 {
return TypeAny{}, TypeAny{}
}
return t.Args().At(0), t.Args().At(1)
}
// Args returns the argument list for this generic type.
func (t TypeGeneric) Args() TypeList {
if t.IsZero() {
return TypeList{}
}
return TypeList{
id.WrapContext(t.Context()),
&t.Raw().args,
}
}
// Span implements [source.Spanner].
func (t TypeGeneric) Span() source.Span {
if t.IsZero() {
return source.Span{}
}
return source.Join(t.Path(), t.Args())
}
// TypeList is a [Commas] over a list of types surrounded by some kind of brackets.
//
// Despite the name, TypeList does not implement [TypeAny] because it is not a type.
type TypeList struct {
withContext
raw *rawTypeList
}
var (
_ Commas[TypeAny] = TypeList{}
_ source.Spanner = TypeList{}
)
type rawTypeList struct {
brackets token.ID
args []withComma[id.Dyn[TypeAny, TypeKind]]
}
// Brackets returns the token tree for the brackets wrapping the argument list.
//
// May be zero, if the user forgot to include brackets.
func (d TypeList) Brackets() token.Token {
if d.IsZero() {
return token.Zero
}
return id.Wrap(d.Context().Stream(), d.raw.brackets)
}
// 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() {
return 0
}
return len(d.raw.args)
}
// At implements [seq.Indexer].
func (d TypeList) At(n int) TypeAny {
return id.WrapDyn(d.Context(), d.raw.args[n].Value)
}
// SetAt implements [seq.Setter].
func (d TypeList) SetAt(n int, ty TypeAny) {
d.Context().Nodes().panicIfNotOurs(ty)
d.raw.args[n].Value = ty.ID()
}
// Insert implements [seq.Inserter].
func (d TypeList) Insert(n int, ty TypeAny) {
d.InsertComma(n, ty, token.Zero)
}
// Delete implements [seq.Inserter].
func (d TypeList) Delete(n int) {
d.raw.args = slices.Delete(d.raw.args, n, n+1)
}
// Comma implements [Commas].
func (d TypeList) Comma(n int) token.Token {
return id.Wrap(d.Context().Stream(), d.raw.args[n].Comma)
}
// AppendComma implements [Commas].
func (d TypeList) AppendComma(value TypeAny, comma token.Token) {
d.InsertComma(d.Len(), value, comma)
}
// InsertComma implements [Commas].
func (d TypeList) InsertComma(n int, ty TypeAny, comma token.Token) {
d.Context().Nodes().panicIfNotOurs(ty, comma)
d.raw.args = slices.Insert(d.raw.args, n, withComma[id.Dyn[TypeAny, TypeKind]]{ty.ID(), comma.ID()})
}
// Span implements [source.Spanner].
func (d TypeList) Span() source.Span {
switch {
case d.IsZero():
return source.Span{}
case !d.Brackets().IsZero():
return d.Brackets().Span()
case d.Len() == 0:
return source.Span{}
default:
return source.Join(d.At(0), d.At(d.Len()-1))
}
}
// 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 ast
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// TypePrefixed is a type with a [TypePrefix].
//
// Unlike in ordinary Protobuf, the Protocompile AST permits arbitrary nesting
// of modifiers.
//
// # Grammar
//
// TypePrefixed := (`optional` | `repeated` | `required` | `stream`) Type
//
// Note that there are ambiguities when Type is an absolute [TypePath].
// The source "optional .foo" names the type "optional.foo" only when inside
// of a [TypeGeneric]'s brackets or a [Signature]'s method parameters.
//
// Also, the `stream` prefix may only occur inside of a [Signature].
type TypePrefixed id.Node[TypePrefixed, *File, *rawTypePrefixed]
type rawTypePrefixed struct {
prefix token.ID
ty id.Dyn[TypeAny, TypeKind]
}
// TypePrefixedArgs is the arguments for [Context.NewTypePrefixed].
type TypePrefixedArgs struct {
Prefix token.Token
Type TypeAny
}
// AsAny type-erases this type value.
//
// See [TypeAny] for more information.
func (t TypePrefixed) AsAny() TypeAny {
if t.IsZero() {
return TypeAny{}
}
return id.WrapDyn(t.Context(), id.NewDyn(TypeKindPrefixed, id.ID[TypeAny](t.ID())))
}
// Prefix extracts the modifier out of this type.
//
// Returns [keyword.Unknown] if [TypePrefixed.PrefixToken] does not contain
// a known prefix.
func (t TypePrefixed) Prefix() keyword.Keyword {
return t.PrefixToken().Keyword()
}
// PrefixToken returns the token representing this type's prefix.
func (t TypePrefixed) PrefixToken() token.Token {
if t.IsZero() {
return token.Zero
}
return id.Wrap(t.Context().Stream(), t.Raw().prefix)
}
// Type returns the type that is being prefixed.
func (t TypePrefixed) Type() TypeAny {
if t.IsZero() {
return TypeAny{}
}
return id.WrapDyn(t.Context(), t.Raw().ty)
}
// SetType sets the expression that is being prefixed.
//
// If passed zero, this clears the type.
func (t TypePrefixed) SetType(ty TypeAny) {
t.Raw().ty = ty.ID()
}
// Span implements [source.Spanner].
func (t TypePrefixed) Span() source.Span {
if t.IsZero() {
return source.Span{}
}
return source.Join(t.PrefixToken(), t.Type())
}
// 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 dom
import (
"iter"
)
const (
kindNone kind = iota //nolint:unused
kindText // Ordinary text.
kindSpace // All spaces (U+0020).
kindBreak // All newlines (U+000A).
kindGroup // See [Group].
kindIndent // See [Indent].
kindUnindent // See [Unindent].
)
// kind is a kind of [tag].
type kind byte
// dom is a source code DOM that contains formatting tags.
type dom []tag
// cursor is a recursive iterator over a [dom].
//
// See [dom.cursor].
type cursor iter.Seq2[*tag, cursor]
// tag is a single tag within a [doc].
type tag struct {
text string
limit int // Used by kind == tagGroup.
kind kind
cond Cond
broken bool
width, column int // See layout.go.
children int // Number of children that follow in a [dom].
}
// add applies a set of tag funcs to this doc.
func (d *dom) add(tags ...Tag) {
for _, tag := range tags {
if tag != nil {
tag(d)
}
}
}
// push appends a tag with children.
func (d *dom) push(tag tag, body func(Sink)) {
*d = append(*d, tag)
if body != nil {
n := len(*d)
body(d.add)
(*d)[n-1].children = len(*d) - n
}
}
// cursor returns an iterator over the top-level tags of this doc.
//
// The iterator yields tags along with another iterator over that tag's
// children.
func (d *dom) cursor() cursor {
return func(yield func(*tag, cursor) bool) {
d := *d
for i := 0; i < len(d); i++ {
tag := &d[i]
children := d[i+1 : i+tag.children+1]
i += len(children)
if !yield(tag, children.cursor()) {
return
}
}
}
}
// renderIf returns whether a condition is true.
func (t *tag) renderIf(cond Cond) bool {
return t.cond == Always || t.cond == cond
}
// shouldMerge calculates whether adjacent tags should be merged together.
//
// Returns which of the tags should be kept based on whitespace merge semantics.
//
// Never returns false, false.
func shouldMerge(a, b *tag) (keepA, keepB bool) {
switch {
case a.kind == kindSpace && b.kind == kindBreak:
return false, true
case a.kind == kindBreak && b.kind == kindSpace:
return true, false
case a.kind == kindSpace && b.kind == kindSpace,
a.kind == kindBreak && b.kind == kindBreak:
bIsWider := len(a.text) < len(b.text)
return !bIsWider, bIsWider
}
return true, true
}
// 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 dom
import (
"strings"
"github.com/rivo/uniseg"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
)
type layout struct {
Options
indent []int
column int
prevText *tag
}
func (l *layout) layout(doc dom) {
l.layoutFlat(doc.cursor())
l.prevText = nil
l.layoutBroken(doc.cursor())
}
// layoutFlat calculates the potential layoutFlat width of an element.
func (l *layout) layoutFlat(cursor cursor) (total int, broken bool) {
for tag, cursor := range cursor {
switch tag.kind {
case kindText, kindSpace, kindBreak:
if l.prevText != nil {
prev, next := shouldMerge(l.prevText, tag)
if !prev {
total -= l.prevText.width
l.prevText = nil
} else if !next {
continue
}
}
tag.broken = strings.Contains(tag.text, "\n")
// With tabs, we need to be pessimistic, because we don't
// know whether groups are broken yet.
tag.width = stringWidth(l.Options, -1, tag.text)
if tag.renderIf(Flat) {
l.prevText = tag
}
}
n, br := l.layoutFlat(cursor)
tag.width += n
tag.broken = tag.broken || br
if tag.renderIf(Flat) {
total += tag.width
broken = broken || tag.broken
}
}
return total, broken
}
// layoutBroken calculates the layout of a group we have decided to break.
func (l *layout) layoutBroken(cursor cursor) {
for tag, cursor := range cursor {
if !tag.renderIf(Broken) {
continue
}
tag.column = l.column
switch tag.kind {
case kindText, kindSpace, kindBreak:
if l.prevText != nil {
prev, next := shouldMerge(l.prevText, tag)
if !prev {
if !l.prevText.broken {
l.column -= l.prevText.width
}
l.prevText = nil
} else if !next {
continue
}
}
if l.column == 0 {
l.column, _ = slicesx.Last(l.indent)
}
last := stringsx.LastLine(tag.text)
if len(last) < len(tag.text) {
l.column = 0
}
l.column = stringWidth(l.Options, l.column, last)
case kindGroup:
// This enforces that groups break if:
//
// 1. The would cause overflow of the global max width.
//
// 2. The group itself is too wide.
tag.broken = tag.broken ||
tag.column+tag.width > l.MaxWidth ||
tag.width > tag.limit
if !tag.broken {
// No need to recurse; we are leaving this group unbroken.
l.column += tag.width
} else {
l.layoutBroken(cursor)
}
case kindIndent:
prev, _ := slicesx.Last(l.indent)
next := stringWidth(l.Options, prev, tag.text)
l.indent = append(l.indent, next)
l.layoutBroken(cursor)
l.indent = l.indent[:len(l.indent)-1]
case kindUnindent:
prev, ok := slicesx.Pop(&l.indent)
l.layoutBroken(cursor)
if ok {
l.indent = append(l.indent, prev)
}
}
}
}
// stringWidth calculates the rendered width of text if placed at the given
// column, accounting for tabstops.
//
// If column is -1, all tabstops are given their maximum width. This is used for
// cases where we are forced to be conservative because we do not know the
// column we will be rendering at.
func stringWidth(options Options, column int, text string) int {
maxWidth := column < 0
column = max(0, column)
// We can't just use StringWidth, because that doesn't respect tabstops
// correctly.
for i, next := range iterx.Enumerate(stringsx.Split(text, '\t')) {
if i > 0 {
tab := options.TabstopWidth
if !maxWidth {
tab -= (column % options.TabstopWidth)
}
column += tab
}
column += uniseg.StringWidth(next)
}
return column
}
// 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 dom
import (
"fmt"
"math"
"slices"
"strings"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// printer holds state for converting a laid-out [dom] into a string.
type printer struct {
Options
out strings.Builder
// Buffered spaces and newlines, for whitespace merging in write().
spaces, newlines int
// Indentation state. See indentBy() for usage.
indent []byte
indents []string
// Whether a value has been popped from indent. This is used to handle the
// relatively rare case where indentBy is called inside of unindentBy.
popped bool
}
// render renders a dom with the given options.
func render(options Options, doc *dom) string {
options = options.WithDefaults()
l := layout{Options: options}
l.layout(*doc)
p := printer{Options: options}
if options.HTML {
p.html(doc.cursor())
} else {
// Top level group is always broken.
p.print(Broken, doc.cursor())
}
if !strings.HasSuffix(p.out.String(), "\n") {
p.out.WriteByte('\n')
}
return p.out.String()
}
// print prints all of the elements of a cursor that are conditioned on cond.
//
// In other words, this function is called with cond set to whether the
// containing group is broken.
func (p *printer) print(cond Cond, cursor cursor) {
for tag, cursor := range cursor {
if !tag.renderIf(cond) {
continue
}
switch tag.kind {
case kindText:
p.write(tag.text)
p.spaces = 0
p.newlines = 0
case kindSpace:
p.spaces = max(p.spaces, len(tag.text))
case kindBreak:
p.newlines = max(p.newlines, len(tag.text))
case kindGroup:
ourCond := Flat
if tag.broken {
ourCond = Broken
}
p.print(ourCond, cursor)
case kindIndent:
p.withIndent(tag.text, func(p *printer) {
p.print(cond, cursor)
})
case kindUnindent:
p.withUnindent(func(p *printer) {
p.print(cond, cursor)
})
}
}
}
// html renders the contents of cursor as pseudo-HTML.
func (p *printer) html(cursor cursor) {
for tag, cursor := range cursor {
var cond string
switch tag.cond {
case Flat:
cond = " if=flat"
case Broken:
cond = " if=broken"
}
switch tag.kind {
case kindText:
if cond != "" {
fmt.Fprintf(&p.out, "<p%v>%q</p>", cond, tag.text)
} else {
fmt.Fprintf(&p.out, "%q", tag.text)
}
case kindSpace:
fmt.Fprintf(&p.out, "<sp count=%v%v>", len(tag.text), cond)
case kindBreak:
fmt.Fprintf(&p.out, "<br count=%v%v>", len(tag.text), cond)
case kindGroup:
name := "span"
if tag.broken {
name = "div"
}
var limit string
if tag.limit != math.MaxInt {
limit = fmt.Sprintf(" limit=%v", tag.limit)
}
fmt.Fprintf(&p.out,
"<%v%v width=%v col=%v%v>",
name, limit, tag.width, tag.column, cond)
p.newlines++
p.withIndent(" ", func(p *printer) { p.html(cursor) })
fmt.Fprintf(&p.out, "</%v>", name)
case kindIndent:
fmt.Fprintf(&p.out, "<indent by=%q%v>", tag.text, cond)
p.newlines++
p.withIndent(" ", func(p *printer) { p.html(cursor) })
fmt.Fprintf(&p.out, "</indent>")
case kindUnindent:
fmt.Fprintf(&p.out, "<unindent%v>", cond)
p.newlines++
p.withIndent(" ", func(p *printer) { p.html(cursor) })
fmt.Fprintf(&p.out, "</unindent>")
}
p.newlines++
}
}
// write appends data to the output buffer.
//
// This function automatically handles newline/space merging and indentation.
func (p *printer) write(data string) {
if p.newlines > 0 {
for range p.newlines {
p.out.WriteByte('\n')
}
p.newlines = 0
p.spaces = 0
p.out.Write(p.indent)
}
for range p.spaces {
p.out.WriteByte(' ')
}
p.spaces = 0
p.out.WriteString(data)
}
// withIndent pushes an indentation string onto the indentation stack for
// the duration of body.
func (p *printer) withIndent(by string, body func(*printer)) {
prev := p.indent
if p.popped {
p.popped = false
// Need to make a defensive copy here to avoid clobbering any
// indent popped by withUnindent. Doing this here avoids needing to
// do the copy except in the case of an indent/unindent/indent sequence.
//
// Force a copy in append() below by clipping the slice.
p.indent = slices.Clip(p.indent)
}
p.indent = append(p.indent, by...)
p.indents = append(p.indents, by)
body(p)
if slicesx.PointerEqual(prev, p.indent) {
// Retain any capacity added by downstream indent calls.
p.indent = p.indent[:len(prev)]
} else {
p.indent = prev
}
slicesx.Pop(&p.indents)
}
// withUnindent undoes the most recent call to [printer.withIndent] for the
// duration of body.
func (p *printer) withUnindent(body func(*printer)) {
if len(p.indents) == 0 {
body(p)
return
}
prev := p.indent
popped, _ := slicesx.Pop(&p.indents)
p.indent = p.indent[:len(p.indent)-len(popped)]
p.popped = true
body(p)
p.popped = false
p.indent = prev
p.indents = append(p.indents, popped)
}
// 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 dom is a Go port of https://github.com/mcy/strings/tree/main/allman,
// a high-performance meta-formatting library.
//
// The function [Render] is the primary entry point. It is given a collection
// of [Tag]s, which represent various formatting directives, such as indentation
// and grouping.
//
// The main benefit of using this package is the ability to perform smart line
// wrapping of code. The [Group] tag can be used to group a collection of tags
// together that may be rendered in either a "flat" or "broken" orientation. The
// layout engine will determine whether this element could be laid out flat
// without going over a configured column limit, and if it would, the group is
// marked as broken. This can be combined with conditioned tags (viz. [Cond])
// to insert e.g. line breaks at strategic points.
package dom
import (
"math"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
)
// Render renders a document consisting of the given sequence of tags.
func Render(options Options, content func(push Sink)) string {
d := new(dom)
content(d.add)
return render(options, d)
}
// Options specifies configuration for [Render].
type Options struct {
// The maximum number of columns to render before triggering
// a break. A value of zero implies an infinite width.
MaxWidth int
// The number of columns a tab character counts as. Defaults to 1.
TabstopWidth int
// If true, prints all of the tags in an HTML-like format. Intended for
// debugging.
HTML bool
}
// WithDefaults replaces any unset (read: zero value) fields of an Options which
// specify a default value with that default value.
func (o Options) WithDefaults() Options {
if o.MaxWidth == 0 {
o.MaxWidth = math.MaxInt
}
if o.TabstopWidth == 0 {
o.TabstopWidth = 1
}
return o
}
// Tag is data passed to a rendering function.
//
// The various factory functions in this package can be used to construct tags.
// See their documentation for more information on what tags are available.
//
// The nil tag is equivalent to Text("").
type Tag func(*dom)
// Sink is a place to append tags. The given tags will be appended to whatever
// context the sink was created for.
//
// Many functions in this package take a func(push Sink) as an argument. This
// callback is executed in the context of that tag, and must not be used after
// the callback returns.
type Sink func(...Tag)
const (
Always Cond = iota
Flat // Render only in a flat group.
Broken // Render only in a broken group.
)
// Cond is a condition for a tag.
//
// Tags can be conditioned on whether or not they are rendered if the group
// that they are being rendered in is flat or broken.
type Cond byte
// Text returns a tag that emits its text exactly.
//
// If text consists only of spaces (U+0020) or newlines (U+000A), it will be
// treated specially:
//
// - Space tags adjacent to a newline will be deleted, so that lines do not
// have trailing whitespace.
//
// - If two space or newline tags of the same rune are adjacent, the shorter
// one is deleted.
func Text(text string) Tag {
return TextIf(Always, text)
}
// TextIf is like [Text], but with a condition attached.
//
// If the condition does not hold in the containing tag, this tag expands to
// nothing. The outermost level is treated as always broken.
func TextIf(cond Cond, text string) Tag {
return func(d *dom) {
if text == "" {
return
}
var kind kind
switch {
case stringsx.Every(text, ' '):
kind = kindSpace
case stringsx.Every(text, '\n'):
kind = kindBreak
default:
kind = kindText
}
d.push(tag{kind: kind, text: text, cond: cond}, nil)
}
}
// Group returns a tag that groups together a collection of child tags.
//
// Each group in a document can be broken or flat. Flat groups contain only
// other flat groups. Groups are broken when:
//
// 1. They contain a tag that contains a newline.
//
// 2. They contain a broken group.
//
// 3. The width of the group when flat is greater than maxWidth (a value of
// zero implies no limit).
//
// 4. If the group was laid out flat, the current line would exceed the maximum
// configured column length in [Options].
func Group(maxWidth int, content func(push Sink)) Tag {
return GroupIf(Always, maxWidth, content)
}
// GroupIf is like [Group], but with a condition attached.
//
// If the condition does not hold in the containing tag, this tag expands to
// nothing. The outermost level is treated as always broken.
func GroupIf(cond Cond, maxWidth int, content func(push Sink)) Tag {
return func(d *dom) {
if maxWidth == 0 {
maxWidth = math.MaxInt
}
d.push(tag{kind: kindGroup, limit: maxWidth, cond: cond}, content)
}
}
// Indent pushes by to the indentation stack for all of the given tags.
//
// The indentation stack consists of strings printed on each new line, if that
// line is otherwise not empty.
//
// Indent cannot be conditioned, because it already has no effect in a flat
// group.
func Indent(by string, content func(push Sink)) Tag {
return func(d *dom) {
if by == "" {
content(d.add)
return
}
d.push(tag{kind: kindIndent, text: by}, content)
}
}
// Unindent pops the last [Indent] for all of the given tags.
//
// Unindent cannot be conditioned, because it already has no effect in a flat
// group.
func Unindent(content func(push Sink)) Tag {
return func(d *dom) {
d.push(tag{kind: kindUnindent}, content)
}
}
// 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 fdp
import (
"fmt"
"slices"
"strings"
"unicode"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// commentTracker is used to track and attribute comments in a token stream. All attributed
// comments are stored in [commentTracker].attributed for easy look-up by [token.ID].
type commentTracker struct {
cursor *token.Cursor
attributed map[token.ID]*comments // [token.ID] and its attributed comments.
tracked []paragraph
current []token.Token
prev token.ID // The last non-skippable token.
// The first line of the current comment tokens is on the same line as the last non-skippable token.
firstCommentOnSameLine bool
}
// A paragraph is a group of comment and whitespace tokens that make up a single paragraph comment.
type paragraph []token.Token
// stringify returns the paragraph is a single string. It also trims off the leading "//"
// for line comments, and enclosing "/* */" for block comments.
func (p paragraph) stringify() string {
var str strings.Builder
for _, t := range p {
text := t.Text()
if t.Kind() != token.Comment {
fmt.Fprint(&str, text)
continue
}
switch {
case strings.HasPrefix(text, "//"):
// For line comments, the leading "//" needs to be trimmed off.
fmt.Fprint(&str, strings.TrimPrefix(text, "//"))
case strings.HasPrefix(text, "/*"):
// For block comments, we iterate through each line and trim the leading "/*",
// "*", and "*/".
for _, line := range strings.SplitAfter(text, "\n") {
switch {
case strings.HasPrefix(line, "/*"):
fmt.Fprint(&str, strings.TrimPrefix(line, "/*"))
case strings.HasSuffix(line, "*/"):
fmt.Fprint(&str, strings.TrimSuffix(line, "*/"))
case strings.HasPrefix(strings.TrimSpace(line), "*"):
// We check the line with all spaces trimmed because of leading whitespace.
fmt.Fprint(&str, strings.TrimPrefix(strings.TrimLeftFunc(line, unicode.IsSpace), "*"))
}
}
}
}
return str.String()
}
// Comments are the leading, trailing, and detached comments associated with a token.
type comments struct {
leading paragraph
trailing paragraph
detached []paragraph
}
// leadingComment returns the leading comment string.
func (c comments) leadingComment() string {
return c.leading.stringify()
}
// trailingComment returns the trailing comment string.
func (c comments) trailingComment() string {
return c.trailing.stringify()
}
// detachedComments returns a slice of detached comment strings.
func (c comments) detachedComments() []string {
detached := make([]string, len(c.detached))
for i, paragraph := range c.detached {
detached[i] = paragraph.stringify()
}
return detached
}
// attributeComments walks the given token stream and groups comment and space tokens
// into [paragraph]s and attributes them to non-skippable tokens as leading, trailing, and
// detached comments.
func (ct *commentTracker) attributeComments(cursor *token.Cursor) {
ct.cursor = cursor
t := cursor.NextSkippable()
for !t.IsZero() {
switch t.Kind() {
case token.Comment:
ct.handleCommentToken(t)
case token.Space:
ct.handleSpaceToken(t)
default:
ct.handleNonSkippableToken(t)
}
if !t.IsLeaf() {
ct.attributeComments(t.Children())
_, end := t.StartEnd()
ct.handleNonSkippableToken(end)
ct.cursor = cursor
}
t = cursor.NextSkippable()
}
}
// handleCommentToken looks at the current comment [token.Token] and determines whether to
// start tracking a new comment paragraph or track it as part of an existing paragraph.
//
// For line comments, if it is on the same line as the previous non-skippable token, it is
// always considered its own paragraph.
//
// A block comment cannot be made into a paragraph with other tokens, so the currently
// tracked paragraph is closed out, and the block comment is also closed out as its own
// paragraph.
//
// The first comment token since the last non-skippable token is always tracked.
func (ct *commentTracker) handleCommentToken(t token.Token) {
prev := id.Wrap(ct.cursor.Context(), ct.prev)
isLineComment := strings.HasPrefix(t.Text(), "//")
if !isLineComment {
// Block comments are their own paragraph, close the current paragraph and track the
// current block comment as its own paragraph.
ct.closeParagraph()
ct.current = append(ct.current, t)
ct.closeParagraph()
return
}
ct.current = append(ct.current, t)
// If this is not the first comment in the current paragraph, move on.
if len(ct.current) > 1 {
return
}
if !prev.IsZero() && ct.cursor.NewLinesBetween(prev, t, 1) == 0 {
// This first comment is always in a paragraph by itself if there are no newlines
// between it and the previous non-skippable token.
ct.closeParagraph()
ct.firstCommentOnSameLine = true
}
}
// handleSpaceToken looks at the current space [token.Token] and determines whether this
// space token is part of the current comment paragraph or if the current paragraph needs
// to be closed.
//
// If there are no currently tracked paragraphs, then the space token is thrown away,
// paragraphs are not started with space tokens.
//
// If the current space token is a newline, and is preceded by another token that ends with
// a newline, then the current paragraph is closed, and the current newline token is dropped.
// Otherwise, the newline token is attached to the current paragraph.
//
// All other space tokens are thrown away.
func (ct *commentTracker) handleSpaceToken(t token.Token) {
if !strings.HasSuffix(t.Text(), "\n") || len(ct.current) == 0 {
return
}
if strings.HasSuffix(ct.current[len(ct.current)-1].Text(), "\n") {
ct.closeParagraph()
} else {
ct.current = append(ct.current, t)
}
}
// handleNonSkippableToken looks at the current non-skippable [token.Token], closes out the
// currently tracked paragraph, and determines attributions for the tracked comment paragraphs.
//
// Comments are either attributed as leading or detached leading comments on the current
// token or as trailing comments on the last seen non-skippable token.
func (ct *commentTracker) handleNonSkippableToken(t token.Token) {
ct.closeParagraph()
prev := id.Wrap(ct.cursor.Context(), ct.prev)
// Set new non-skippable token
ct.prev = t.ID()
if len(ct.tracked) == 0 {
return
}
var donate bool // Donate the first tracked paragraph as a trailing comment to prev
switch {
case prev.IsZero():
donate = false
case ct.firstCommentOnSameLine:
donate = true
// Check if there are more than 2 newlines between the previous non-skippable token
// and the first line of the first tracked paragraph.
case ct.cursor.NewLinesBetween(prev, ct.tracked[0][0], 2) < 2:
// If yes, check the remaining criteria for donation:
//
// 1. Is there more than one comment? If not, donate.
// 2. Is the current token one of the closers, ), ], or } (but not >). If yes, donate
// the currently tracked paragraphs because a body is closed.
// 3. Is there more than one newline between the current token and the end of the
// first tracked paragraph? If yes, donate.
switch {
case len(ct.tracked) > 1 && ct.tracked[1] != nil:
donate = true
case slicesx.Among(
t.Text(),
keyword.LParen.String(),
keyword.LBracket.String(),
keyword.LBrace.String(),
):
donate = true
case ct.cursor.NewLinesBetween(ct.tracked[0][len(ct.tracked[0])-1], t, 2) > 1:
donate = true
}
}
if donate {
ct.setTrailing(ct.tracked[0], prev)
ct.tracked = ct.tracked[1:]
}
if len(ct.tracked) > 0 {
// The leading comment must have precisely one new line between it and the current token.
if last := ct.tracked[len(ct.tracked)-1]; ct.cursor.NewLinesBetween(last[len(last)-1], t, 2) == 1 {
ct.setLeading(last, t)
ct.tracked = ct.tracked[:len(ct.tracked)-1]
}
}
// Check the remaining tracked comments to see if they are detached comments.
// Detached comments must be separated from other non-space tokens by at least 2
// newlines (unless they are at the top of the file), e.g. a file with contents:
//
// // This is a detached comment at the top of the file.
//
// edition = "2023";
//
// message Foo {}
// // This is neither a detached nor trailing comment, since it is not separated from
// // the closing brace above by an empty line.
//
// // This IS a detached comment for Bar.
//
// // A leading comment for Bar.
// message Bar {}
//
for i, remaining := range ct.tracked {
prev := remaining[0].Prev()
for prev.Kind() == token.Space {
prev = prev.Prev()
}
next := remaining[len(remaining)-1].Next()
for next.Kind() == token.Space {
next = next.Next()
}
if !prev.IsZero() && ct.cursor.NewLinesBetween(prev, remaining[0], 2) < 2 {
continue
}
if !next.IsZero() && ct.cursor.NewLinesBetween(remaining[len(remaining)-1], next, 2) == 2 {
ct.setDetached(ct.tracked[i:], t)
break
}
}
// Reset tracked comment information
ct.firstCommentOnSameLine = false
ct.tracked = nil
}
// closeParagraph takes the currently tracked paragraph, closes it, and tracks it.
func (ct *commentTracker) closeParagraph() {
// If the current paragraph only contains whitespace tokens, then throw it away.
if slices.ContainsFunc(ct.current, func(t token.Token) bool {
return t.Kind() == token.Comment
}) {
ct.tracked = append(ct.tracked, ct.current)
}
ct.current = nil
}
// setLeading sets the given paragraph as the leading comment on the given token.
func (ct *commentTracker) setLeading(leading paragraph, t token.Token) {
ct.mutateComment(t, func(c *comments) {
c.leading = leading
})
}
// setTrailing sets the given paragraph as the trailing comment on the given token.
func (ct *commentTracker) setTrailing(trailing paragraph, t token.Token) {
ct.mutateComment(t, func(c *comments) {
c.trailing = trailing
})
}
// setDetached sets the given slice of paragraphs as the detached comments on the given token.
func (ct *commentTracker) setDetached(detached []paragraph, t token.Token) {
ct.mutateComment(t, func(c *comments) {
c.detached = detached
})
}
// mutateComment mutates the attributed comments on the given token.
func (ct *commentTracker) mutateComment(t token.Token, mutate func(*comments)) {
if ct.attributed == nil {
ct.attributed = make(map[token.ID]*comments)
}
if ct.attributed[t.ID()] == nil {
ct.attributed[t.ID()] = &comments{}
}
mutate(ct.attributed[t.ID()])
}
// 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 fdp provides functionality for lowering the IR to a FileDescriptorSet.
package fdp
import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/experimental/ir"
)
// DescriptorSetBytes generates a FileDescriptorSet for the given files, and returns the
// result as an encoded byte slice.
//
// The resulting FileDescriptorSet is always fully linked: it contains all dependencies except
// the WKTs, and all names are fully-qualified.
func DescriptorSetBytes(files []*ir.File, options ...DescriptorOption) ([]byte, error) {
var g generator
for _, opt := range options {
if opt != nil {
opt(&g)
}
}
fds := new(descriptorpb.FileDescriptorSet)
g.files(files, fds)
return proto.Marshal(fds)
}
// DescriptorProtoBytes generates a single FileDescriptorProto for file, and returns the
// result as an encoded byte slice.
//
// The resulting FileDescriptorProto is fully linked: all names are fully-qualified.
func DescriptorProtoBytes(file *ir.File, options ...DescriptorOption) ([]byte, error) {
var g generator
for _, opt := range options {
if opt != nil {
opt(&g)
}
}
fdp := new(descriptorpb.FileDescriptorProto)
g.file(file, fdp)
return proto.Marshal(fdp)
}
// DescriptorOption is an option to pass to [DescriptorSetBytes] or [DescriptorProtoBytes].
type DescriptorOption func(*generator)
// IncludeDebugInfo sets whether or not to include google.protobuf.SourceCodeInfo in
// the output.
func IncludeSourceCodeInfo(flag bool) DescriptorOption {
return func(g *generator) {
g.includeDebugInfo = flag
}
}
// ExcludeFiles excludes the given files from the output of [DescriptorSetBytes].
func ExcludeFiles(exclude func(*ir.File) bool) DescriptorOption {
return func(g *generator) {
g.exclude = exclude
}
}
// 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 fdp
import (
"math"
"slices"
"strconv"
descriptorv1 "buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go/buf/descriptor/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/ir"
"github.com/bufbuild/protocompile/experimental/ir/presence"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
type generator struct {
currentFile *ir.File
includeDebugInfo bool
exclude func(*ir.File) bool
path *path
sourceCodeInfo *descriptorpb.SourceCodeInfo
sourceCodeInfoExtn *descriptorv1.SourceCodeInfoExtension
commentTracker *commentTracker
}
func (g *generator) files(files []*ir.File, fds *descriptorpb.FileDescriptorSet) {
// Build up all of the imported files. We can't just pull out the transitive
// imports for each file because we want the result to be sorted
// topologically.
for file := range ir.TopoSort(files) {
if g.exclude != nil && g.exclude(file) {
continue
}
fdp := new(descriptorpb.FileDescriptorProto)
fds.File = append(fds.File, fdp)
g.file(file, fdp)
}
}
func (g *generator) file(file *ir.File, fdp *descriptorpb.FileDescriptorProto) {
g.currentFile = file
fdp.Name = addr(file.Path())
g.path = new(path)
if g.includeDebugInfo {
g.sourceCodeInfo = new(descriptorpb.SourceCodeInfo)
fdp.SourceCodeInfo = g.sourceCodeInfo
ct := new(commentTracker)
g.commentTracker = ct
ct.attributeComments(g.currentFile.AST().Stream().Cursor())
g.sourceCodeInfoExtn = new(descriptorv1.SourceCodeInfoExtension)
proto.SetExtension(g.sourceCodeInfo, descriptorv1.E_BufSourceCodeInfoExtension, g.sourceCodeInfoExtn)
}
fdp.Package = addr(string(file.Package()))
g.addSourceLocationWithSourcePathElements(
file.AST().Package().Span(),
[]int32{internal.FilePackageTag},
file.AST().Package().KeywordToken().ID(),
file.AST().Package().Semicolon().ID(),
)
if file.Syntax().IsEdition() {
fdp.Syntax = addr("editions")
fdp.Edition = descriptorpb.Edition(file.Syntax()).Enum()
} else {
fdp.Syntax = addr(file.Syntax().String())
}
g.addSourceLocationWithSourcePathElements(
file.AST().Syntax().Span(),
// According to descriptor.proto and protoc behavior, the path is always set to [12]
// for both syntax and editions.
[]int32{internal.FileSyntaxTag},
file.AST().Syntax().KeywordToken().ID(),
file.AST().Syntax().Semicolon().ID(),
)
if g.sourceCodeInfoExtn != nil {
g.sourceCodeInfoExtn.IsSyntaxUnspecified = file.AST().Syntax().IsZero()
}
// Canonicalize import order so that it does not change whenever we refactor
// internal structures.
imports := seq.ToSlice(file.Imports())
slices.SortFunc(imports, cmpx.Key(func(imp ir.Import) int {
return imp.Decl.KeywordToken().Span().Start
}))
var publicDepIndex, weakDepIndex, optionDepIndex int32
for i, imp := range imports {
if !imp.Option {
fdp.Dependency = append(fdp.Dependency, imp.Path())
g.addSourceLocationWithSourcePathElements(
imp.Decl.Span(),
[]int32{internal.FileDependencyTag, int32(i)},
imp.Decl.KeywordToken().ID(),
imp.Decl.Semicolon().ID(),
)
if imp.Public {
fdp.PublicDependency = append(fdp.PublicDependency, int32(i))
_, public := iterx.Find(seq.Values(imp.Decl.ModifierTokens()), func(t token.Token) bool {
return t.Keyword() == keyword.Public
})
g.addSourceLocationWithSourcePathElements(
public.Span(),
[]int32{internal.FilePublicDependencyTag, publicDepIndex},
)
publicDepIndex++
}
if imp.Weak {
fdp.WeakDependency = append(fdp.WeakDependency, int32(i))
_, weak := iterx.Find(seq.Values(imp.Decl.ModifierTokens()), func(t token.Token) bool {
return t.Keyword() == keyword.Weak
})
g.addSourceLocationWithSourcePathElements(
weak.Span(),
[]int32{internal.FileWeakDependencyTag, weakDepIndex},
)
}
} else if imp.Option {
fdp.OptionDependency = append(fdp.OptionDependency, imp.Path())
g.addSourceLocationWithSourcePathElements(
imp.Decl.Span(),
[]int32{internal.FileOptionDependencyTag, optionDepIndex},
imp.Decl.KeywordToken().ID(),
imp.Decl.Semicolon().ID(),
)
}
if g.sourceCodeInfoExtn != nil && !imp.Used {
g.sourceCodeInfoExtn.UnusedDependency = append(g.sourceCodeInfoExtn.UnusedDependency, int32(i))
}
}
var msgIndex, enumIndex int32
for ty := range seq.Values(file.Types()) {
if ty.IsEnum() {
edp := new(descriptorpb.EnumDescriptorProto)
fdp.EnumType = append(fdp.EnumType, edp)
g.enum(ty, edp, internal.FileEnumsTag, enumIndex)
enumIndex++
continue
}
mdp := new(descriptorpb.DescriptorProto)
fdp.MessageType = append(fdp.MessageType, mdp)
g.message(ty, mdp, internal.FileMessagesTag, msgIndex)
msgIndex++
}
for i, service := range seq.All(file.Services()) {
sdp := new(descriptorpb.ServiceDescriptorProto)
fdp.Service = append(fdp.Service, sdp)
g.service(service, sdp, internal.FileServicesTag, int32(i))
}
var extnIndex int32
for extend := range seq.Values(file.Extends()) {
g.addSourceLocationWithSourcePathElements(
extend.AST().Span(),
[]int32{internal.FileExtensionsTag},
extend.AST().KeywordToken().ID(),
extend.AST().Body().Braces().ID(),
)
for extn := range seq.Values(extend.Extensions()) {
fd := new(descriptorpb.FieldDescriptorProto)
fdp.Extension = append(fdp.Extension, fd)
g.field(extn, fd, internal.FileExtensionsTag, extnIndex)
extnIndex++
}
}
if options := file.Options(); !iterx.Empty(options.Fields()) {
for option := range file.AST().Options() {
g.addSourceLocationWithSourcePathElements(option.Span(), []int32{internal.FileOptionsTag})
}
fdp.Options = new(descriptorpb.FileOptions)
g.options(options, fdp.Options, internal.FileOptionsTag)
}
if g.sourceCodeInfoExtn != nil && iterx.Empty2(g.sourceCodeInfoExtn.ProtoReflect().Range) {
proto.ClearExtension(g.sourceCodeInfo, descriptorv1.E_BufSourceCodeInfoExtension)
}
if g.sourceCodeInfo != nil {
slices.SortFunc(g.sourceCodeInfo.Location, func(a, b *descriptorpb.SourceCodeInfo_Location) int {
return slices.Compare(a.Span, b.Span)
})
g.sourceCodeInfo.Location = append(
[]*descriptorpb.SourceCodeInfo_Location{{Span: locationSpan(file.AST().Span())}},
g.sourceCodeInfo.Location...,
)
}
}
func (g *generator) message(ty ir.Type, mdp *descriptorpb.DescriptorProto, sourcePath ...int32) {
reset := g.path.with(sourcePath...)
defer reset()
messageAST := ty.AST().AsMessage()
g.addSourceLocation(messageAST.Span(), messageAST.Keyword.ID(), messageAST.Body.Braces().ID())
mdp.Name = addr(ty.Name())
g.addSourceLocationWithSourcePathElements(messageAST.Name.Span(), []int32{internal.MessageNameTag})
for i, field := range seq.All(ty.Members()) {
fd := new(descriptorpb.FieldDescriptorProto)
mdp.Field = append(mdp.Field, fd)
g.field(field, fd, internal.MessageFieldsTag, int32(i))
}
var extnIndex int32
for extend := range seq.Values(ty.Extends()) {
g.addSourceLocationWithSourcePathElements(
extend.AST().Span(),
[]int32{internal.MessageExtensionsTag},
extend.AST().KeywordToken().ID(),
extend.AST().Body().Braces().ID(),
)
for extn := range seq.Values(extend.Extensions()) {
fd := new(descriptorpb.FieldDescriptorProto)
mdp.Extension = append(mdp.Extension, fd)
g.field(extn, fd, internal.MessageExtensionsTag, extnIndex)
extnIndex++
}
}
var enumIndex, nestedMsgIndex int32
for ty := range seq.Values(ty.Nested()) {
if ty.IsEnum() {
edp := new(descriptorpb.EnumDescriptorProto)
mdp.EnumType = append(mdp.EnumType, edp)
g.enum(ty, edp, internal.MessageEnumsTag, enumIndex)
enumIndex++
continue
}
nested := new(descriptorpb.DescriptorProto)
mdp.NestedType = append(mdp.NestedType, nested)
g.message(ty, nested, internal.MessageNestedMessagesTag, nestedMsgIndex)
nestedMsgIndex++
}
for i, extensions := range seq.All(ty.ExtensionRanges()) {
er := new(descriptorpb.DescriptorProto_ExtensionRange)
mdp.ExtensionRange = append(mdp.ExtensionRange, er)
start, end := extensions.Range()
er.Start = addr(start)
er.End = addr(end + 1) // Exclusive.
g.addSourceLocationWithSourcePathElements(
extensions.DeclAST().Span(),
[]int32{internal.MessageExtensionRangesTag},
extensions.DeclAST().KeywordToken().ID(),
extensions.DeclAST().Semicolon().ID(),
)
g.rangeSourceCodeInfo(
extensions.AST(),
internal.MessageExtensionRangesTag,
internal.ExtensionRangeStartTag,
internal.ExtensionRangeEndTag,
int32(i),
)
if options := extensions.Options(); !iterx.Empty(options.Fields()) {
g.addSourceLocationWithSourcePathElements(
extensions.DeclAST().Options().Span(),
[]int32{internal.ExtensionRangeOptionsTag},
)
er.Options = new(descriptorpb.ExtensionRangeOptions)
g.options(options, er.Options, internal.ExtensionRangeOptionsTag)
}
}
var topLevelSourceLocation bool
for i, reserved := range seq.All(ty.ReservedRanges()) {
if !topLevelSourceLocation {
g.addSourceLocationWithSourcePathElements(
reserved.DeclAST().Span(),
[]int32{internal.MessageReservedRangesTag},
reserved.DeclAST().KeywordToken().ID(),
reserved.DeclAST().Semicolon().ID(),
)
topLevelSourceLocation = true
}
rr := new(descriptorpb.DescriptorProto_ReservedRange)
mdp.ReservedRange = append(mdp.ReservedRange, rr)
start, end := reserved.Range()
rr.Start = addr(start)
rr.End = addr(end + 1) // Exclusive.
g.rangeSourceCodeInfo(
reserved.AST(),
internal.MessageReservedRangesTag,
internal.ReservedRangeStartTag,
internal.ReservedRangeEndTag,
int32(i),
)
}
topLevelSourceLocation = false
for i, name := range seq.All(ty.ReservedNames()) {
if !topLevelSourceLocation {
g.addSourceLocationWithSourcePathElements(
name.DeclAST().Span(),
[]int32{internal.MessageReservedNamesTag},
name.DeclAST().KeywordToken().ID(),
name.DeclAST().Semicolon().ID(),
)
topLevelSourceLocation = true
}
mdp.ReservedName = append(mdp.ReservedName, name.Name())
g.addSourceLocationWithSourcePathElements(
name.AST().Span(),
[]int32{internal.MessageReservedNamesTag, int32(i)},
)
}
for i, oneof := range seq.All(ty.Oneofs()) {
odp := new(descriptorpb.OneofDescriptorProto)
mdp.OneofDecl = append(mdp.OneofDecl, odp)
g.oneof(oneof, odp, internal.MessageOneofsTag, int32(i))
}
if g.currentFile.Syntax() == syntax.Proto3 {
// Only now that we have added all of the normal oneofs do we add the
// synthetic oneofs.
for i, field := range seq.All(ty.Members()) {
if field.SyntheticOneofName() == "" {
continue
}
fdp := mdp.Field[i]
fdp.Proto3Optional = addr(true)
fdp.OneofIndex = addr(int32(len(mdp.OneofDecl)))
mdp.OneofDecl = append(mdp.OneofDecl, &descriptorpb.OneofDescriptorProto{
Name: addr(field.SyntheticOneofName()),
})
}
}
if options := ty.Options(); !iterx.Empty(options.Fields()) {
for option := range messageAST.Body.Options() {
g.addSourceLocationWithSourcePathElements(option.Span(), []int32{internal.MessageOptionsTag})
}
mdp.Options = new(descriptorpb.MessageOptions)
g.options(options, mdp.Options, internal.MessageOptionsTag)
}
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()
}
}
func (g *generator) field(f ir.Member, fdp *descriptorpb.FieldDescriptorProto, sourcePath ...int32) {
reset := g.path.with(sourcePath...)
defer reset()
fieldAST := f.AST().AsField()
g.addSourceLocation(fieldAST.Span(), token.ID(fieldAST.Type.ID()), fieldAST.Semicolon.ID())
fdp.Name = addr(f.Name())
g.addSourceLocationWithSourcePathElements(fieldAST.Name.Span(), []int32{internal.FieldNameTag})
fdp.Number = addr(f.Number())
g.addSourceLocationWithSourcePathElements(fieldAST.Tag.Span(), []int32{internal.FieldNumberTag})
switch f.Presence() {
case presence.Explicit, presence.Implicit, presence.Shared:
fdp.Label = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
case presence.Repeated:
fdp.Label = descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
case presence.Required:
fdp.Label = descriptorpb.FieldDescriptorProto_LABEL_REQUIRED.Enum()
}
// Note: for specifically protobuf fields, we expect a single prefix. The protocompile
// AST allows for arbitrary nesting of prefixes, so the API returns an iterator, but
// [descriptorpb.FieldDescriptorProto] expects a single label.
for prefix := range fieldAST.Type.Prefixes() {
g.addSourceLocationWithSourcePathElements(
prefix.PrefixToken().Span(),
[]int32{internal.FieldLabelTag},
)
}
fieldTypeSourcePathElement := internal.FieldTypeNameTag
if ty := f.Element(); !ty.IsZero() {
if kind := ty.Predeclared().FDPType(); kind != 0 {
fdp.Type = kind.Enum()
fieldTypeSourcePathElement = internal.FieldTypeTag
} else {
fdp.TypeName = addr(string(ty.FullName().ToAbsolute()))
switch {
case ty.IsEnum():
fdp.Type = descriptorpb.FieldDescriptorProto_TYPE_ENUM.Enum()
case f.IsGroup():
fdp.Type = descriptorpb.FieldDescriptorProto_TYPE_GROUP.Enum()
default:
fdp.Type = descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
}
}
}
g.addSourceLocationWithSourcePathElements(
fieldAST.Type.RemovePrefixes().Span(),
[]int32{int32(fieldTypeSourcePathElement)},
)
if f.IsExtension() && f.Container().FullName() != "" {
fdp.Extendee = addr(string(f.Container().FullName().ToAbsolute()))
g.addSourceLocationWithSourcePathElements(
f.Extend().AST().Name().Span(),
[]int32{internal.FieldExtendeeTag},
)
}
if oneof := f.Oneof(); !oneof.IsZero() {
fdp.OneofIndex = addr(int32(oneof.Index()))
}
if options := f.Options(); !iterx.Empty(options.Fields()) {
g.addSourceLocationWithSourcePathElements(
fieldAST.Options.Span(),
[]int32{internal.FieldOptionsTag},
)
fdp.Options = new(descriptorpb.FieldOptions)
g.options(options, fdp.Options, internal.FieldOptionsTag)
}
fdp.JsonName = addr(f.JSONName())
d := f.PseudoOptions().Default
if !d.IsZero() {
if v, ok := d.AsBool(); ok {
fdp.DefaultValue = addr(strconv.FormatBool(v))
} else if v, ok := d.AsInt(); ok {
fdp.DefaultValue = addr(strconv.FormatInt(v, 10))
} else if v, ok := d.AsUInt(); ok {
fdp.DefaultValue = addr(strconv.FormatUint(v, 10))
} else if v, ok := d.AsFloat(); ok {
switch {
case math.IsInf(v, 1):
fdp.DefaultValue = addr("inf")
case math.IsInf(v, -1):
fdp.DefaultValue = addr("-inf")
case math.IsNaN(v):
fdp.DefaultValue = addr("nan") // Goodbye NaN payload. :(
default:
fdp.DefaultValue = addr(strconv.FormatFloat(v, 'g', -1, 64))
}
} else if v, ok := d.AsString(); ok {
fdp.DefaultValue = addr(v)
}
}
}
func (g *generator) oneof(o ir.Oneof, odp *descriptorpb.OneofDescriptorProto, sourcePath ...int32) {
topLevelReset := g.path.with(sourcePath...)
defer topLevelReset()
oneofAST := o.AST().AsOneof()
g.addSourceLocation(oneofAST.Span(), oneofAST.Keyword.ID(), oneofAST.Body.Braces().ID())
odp.Name = addr(o.Name())
reset := g.path.with(internal.OneofNameTag)
g.addSourceLocation(oneofAST.Name.Span())
reset()
if options := o.Options(); !iterx.Empty(options.Fields()) {
for option := range oneofAST.Body.Options() {
reset := g.path.with(internal.OneofOptionsTag)
g.addSourceLocation(option.Span())
reset()
}
odp.Options = new(descriptorpb.OneofOptions)
g.options(options, odp.Options, internal.OneofOptionsTag)
}
}
func (g *generator) enum(ty ir.Type, edp *descriptorpb.EnumDescriptorProto, sourcePath ...int32) {
topLevelReset := g.path.with(sourcePath...)
defer topLevelReset()
enumAST := ty.AST().AsEnum()
g.addSourceLocation(enumAST.Span(), enumAST.Keyword.ID(), enumAST.Body.Braces().ID())
edp.Name = addr(ty.Name())
reset := g.path.with(internal.EnumNameTag)
g.addSourceLocation(enumAST.Name.Span())
reset()
for i, enumValue := range seq.All(ty.Members()) {
evd := new(descriptorpb.EnumValueDescriptorProto)
edp.Value = append(edp.Value, evd)
g.enumValue(enumValue, evd, internal.EnumValuesTag, int32(i))
}
var topLevelSourceLocation bool
for i, reserved := range seq.All(ty.ReservedRanges()) {
if !topLevelSourceLocation {
reset := g.path.with(internal.EnumReservedRangesTag)
g.addSourceLocation(
reserved.DeclAST().Span(),
reserved.DeclAST().KeywordToken().ID(),
reserved.DeclAST().Semicolon().ID(),
)
reset()
topLevelSourceLocation = true
}
rr := new(descriptorpb.EnumDescriptorProto_EnumReservedRange)
edp.ReservedRange = append(edp.ReservedRange, rr)
start, end := reserved.Range()
rr.Start = addr(start)
rr.End = addr(end) // Inclusive, not exclusive like the one for messages!
g.rangeSourceCodeInfo(
reserved.AST(),
internal.EnumReservedRangesTag,
internal.ReservedRangeStartTag,
internal.ReservedRangeEndTag,
int32(i),
)
}
topLevelSourceLocation = false
for i, name := range seq.All(ty.ReservedNames()) {
if !topLevelSourceLocation {
reset := g.path.with(internal.EnumReservedNamesTag)
g.addSourceLocation(
name.DeclAST().Span(),
name.DeclAST().KeywordToken().ID(),
name.DeclAST().Semicolon().ID(),
)
reset()
topLevelSourceLocation = true
}
edp.ReservedName = append(edp.ReservedName, name.Name())
reset := g.path.with(internal.EnumReservedNamesTag, int32(i))
g.addSourceLocation(name.AST().Span())
reset()
}
if options := ty.Options(); !iterx.Empty(options.Fields()) {
for option := range enumAST.Body.Options() {
reset := g.path.with(internal.EnumOptionsTag)
g.addSourceLocation(option.Span())
reset()
}
edp.Options = new(descriptorpb.EnumOptions)
g.options(options, edp.Options, internal.EnumOptionsTag)
}
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 (g *generator) enumValue(f ir.Member, evdp *descriptorpb.EnumValueDescriptorProto, sourcePath ...int32) {
topLevelReset := g.path.with(sourcePath...)
defer topLevelReset()
enumValueAST := f.AST().AsEnumValue()
g.addSourceLocation(enumValueAST.Span(), enumValueAST.Name.ID(), enumValueAST.Semicolon.ID())
evdp.Name = addr(f.Name())
reset := g.path.with(internal.EnumValNameTag)
g.addSourceLocation(enumValueAST.Name.Span())
reset()
evdp.Number = addr(f.Number())
reset = g.path.with(internal.EnumValNumberTag)
g.addSourceLocation(enumValueAST.Tag.Span())
reset()
if options := f.Options(); !iterx.Empty(options.Fields()) {
reset := g.path.with(internal.EnumValOptionsTag)
g.addSourceLocation(enumValueAST.Options.Span())
reset()
evdp.Options = new(descriptorpb.EnumValueOptions)
g.options(options, evdp.Options, internal.EnumValOptionsTag)
}
}
func (g *generator) service(s ir.Service, sdp *descriptorpb.ServiceDescriptorProto, sourcePath ...int32) {
topLevelReset := g.path.with(sourcePath...)
defer topLevelReset()
serviceAST := s.AST().AsService()
g.addSourceLocation(serviceAST.Span(), serviceAST.Keyword.ID(), serviceAST.Body.Braces().ID())
sdp.Name = addr(s.Name())
reset := g.path.with(internal.ServiceNameTag)
g.addSourceLocation(serviceAST.Name.Span())
reset()
for i, method := range seq.All(s.Methods()) {
mdp := new(descriptorpb.MethodDescriptorProto)
sdp.Method = append(sdp.Method, mdp)
g.method(method, mdp, internal.ServiceMethodsTag, int32(i))
}
if options := s.Options(); !iterx.Empty(options.Fields()) {
sdp.Options = new(descriptorpb.ServiceOptions)
for option := range serviceAST.Body.Options() {
reset := g.path.with(internal.ServiceOptionsTag)
g.addSourceLocation(option.Span())
reset()
}
g.options(options, sdp.Options, internal.ServiceOptionsTag)
}
}
func (g *generator) method(m ir.Method, mdp *descriptorpb.MethodDescriptorProto, sourcePath ...int32) {
topLevelReset := g.path.with(sourcePath...)
defer topLevelReset()
methodAST := m.AST().AsMethod()
// Comment attribution for tokens is unique. The behavior in protoc for method leading
// comments is as follows for methods without a body:
//
// service FooService {
// // I'm the leading comment for GetFoo
// rpc GetFoo (GetFooRequest) returns (GetFooResponse); // I'm the trailing comment for GetFoo
// }
//
// And for methods with a body:
//
// service FooService {
// // I'm still the leading comment for GetFoo
// rpc GetFoo (GetFooRequest) returns (GetFooResponse) { // I'm the trailing comment for GetFoo
// }; // I am NOT the trailing comment for GetFoo, and am instead dropped.
// }
//
closingToken := m.AST().Semicolon().ID()
if !methodAST.Body.Braces().IsZero() {
closingToken = methodAST.Body.Braces().ID()
}
g.addSourceLocation(methodAST.Span(), methodAST.Keyword.ID(), closingToken)
mdp.Name = addr(m.Name())
reset := g.path.with(internal.MethodNameTag)
g.addSourceLocation(methodAST.Name.Span())
reset()
in, inStream := m.Input()
mdp.InputType = addr(string(in.FullName()))
mdp.ClientStreaming = addr(inStream)
// Methods only have a single input, see [descriptorpb.MethodDescriptorProto].
inputAST := methodAST.Signature.Inputs().At(0)
if prefixed := inputAST.AsPrefixed(); !prefixed.IsZero() {
reset := g.path.with(internal.MethodInputStreamTag)
g.addSourceLocation(prefixed.PrefixToken().Span())
reset()
}
reset = g.path.with(internal.MethodInputTag)
g.addSourceLocation(inputAST.RemovePrefixes().Span())
reset()
out, outStream := m.Output()
mdp.OutputType = addr(string(out.FullName()))
mdp.ServerStreaming = addr(outStream)
// Methods only have a single output, see [descriptorpb.MethodDescriptorProto].
outputAST := methodAST.Signature.Outputs().At(0)
if prefixed := outputAST.AsPrefixed(); !prefixed.IsZero() {
reset := g.path.with(internal.MethodOutputStreamTag)
g.addSourceLocation(prefixed.PrefixToken().Span())
reset()
}
reset = g.path.with(internal.MethodOutputTag)
g.addSourceLocation(outputAST.RemovePrefixes().Span())
reset()
if options := m.Options(); !iterx.Empty(options.Fields()) {
mdp.Options = new(descriptorpb.MethodOptions)
for option := range methodAST.Body.Options() {
reset := g.path.with(internal.MethodOptionsTag)
g.addSourceLocation(option.Span())
reset()
}
g.options(options, mdp.Options, internal.MethodOptionsTag)
}
}
func (g *generator) options(v ir.MessageValue, target proto.Message, sourcePathElement int32) {
target.ProtoReflect().SetUnknown(v.Marshal(nil, nil))
g.messageValueSourceCodeInfo(v, sourcePathElement)
}
func (g *generator) messageValueSourceCodeInfo(v ir.MessageValue, sourcePath ...int32) {
for field := range v.Fields() {
var optionSpanIndex int32
for optionSpan := range seq.Values(field.OptionSpans()) {
if optionSpan == nil {
continue
}
if messageField := field.AsMessage(); !messageField.IsZero() {
g.messageValueSourceCodeInfo(messageField, append(sourcePath, field.Field().Number())...)
continue
}
span := optionSpan.Span()
// For declarations with bodies, e.g. messages, enums, services, methods, files,
// leading and trailing comments are attributed on the option declarations based on
// the option keyword and semicolon, respectively, e.g.
//
// message Foo {
// // Leading comment for the following option declaration, (a) = 10.
// option (a) = 10;
// option (b) = 20; // Trailing comment for the option declaration (b) = 20.
// }
//
// However, the optionSpan in the IR does not capture the keyword and semicolon
// tokens. In addition to the comments, the span including the option keyword and
// semicolon is needed for the source location.
//
// So this hack checks the non-skippable token directly before and after the
// optionSpan for the option keyword and semicolon tokens respectively.
//
// For declarations with compact options, e.g. fields, enum values, there are no
// comments attributed to the option spans, e.g.
//
// message Foo {
// string name = 1 [
// // This is dropped.
// (c) = 15, // This is also dropped.
// ]
// }
//
var checkCommentTokens []token.ID
keyword, semicolon := g.optionKeywordAndSemicolon(span)
if !keyword.IsZero() && !semicolon.IsZero() {
checkCommentTokens = []token.ID{keyword.ID(), semicolon.ID()}
span = source.Between(keyword.Span(), semicolon.Span())
}
if field.Field().IsRepeated() {
reset := g.path.with(append(sourcePath, field.Field().Number(), optionSpanIndex)...)
g.addSourceLocation(span, checkCommentTokens...)
reset()
optionSpanIndex++
} else {
reset := g.path.with(append(sourcePath, field.Field().Number())...)
g.addSourceLocation(span, checkCommentTokens...)
reset()
}
}
}
}
// optionKeywordAndSemicolon is a helper function that checks the non-skippable tokens
// before and after the given span. If the non-skippable token before is the option keyword
// and the non-skippable token after is the semicolon, then both are returned.
func (g *generator) optionKeywordAndSemicolon(optionSpan source.Span) (token.Token, token.Token) {
_, start := g.currentFile.AST().Stream().Around(optionSpan.Start)
before := token.NewCursorAt(start)
prev := before.Prev()
if prev.Keyword() != keyword.Option {
return token.Zero, token.Zero
}
_, end := g.currentFile.AST().Stream().Around(optionSpan.End)
after := token.NewCursorAt(end)
next := after.Next()
if next.Keyword() != keyword.Semi {
return token.Zero, token.Zero
}
return prev, next
}
func (g *generator) rangeSourceCodeInfo(rangeAST ast.ExprAny, baseTag, startTag, endTag, index int32) {
reset := g.path.with(baseTag, index)
defer reset()
g.addSourceLocation(rangeAST.Span())
var startSpan, endSpan source.Span
switch rangeAST.Kind() {
case ast.ExprKindLiteral, ast.ExprKindPath:
startSpan = rangeAST.Span()
endSpan = rangeAST.Span()
case ast.ExprKindRange:
start, end := rangeAST.AsRange().Bounds()
startSpan = start.Span()
endSpan = end.Span()
}
if startTag != 0 {
reset := g.path.with(startTag)
g.addSourceLocation(startSpan)
reset()
}
if endTag != 0 {
reset := g.path.with(endTag)
g.addSourceLocation(endSpan)
reset()
}
}
// addSourceLocationWithSourcePathElements is a helper that adds a new source location for
// the given span, source path elements, and comment tokens, then resets the path immediately.
func (g *generator) addSourceLocationWithSourcePathElements(
span source.Span,
sourcePathElements []int32,
checkForComments ...token.ID,
) {
reset := g.path.with(sourcePathElements...)
defer reset()
g.addSourceLocation(span, checkForComments...)
}
// addSourceLocation adds the source code info location based on the current path tracked
// by the [generator]. It also checks the given token IDs for comments.
func (g *generator) addSourceLocation(span source.Span, checkForComments ...token.ID) {
if g.sourceCodeInfo == nil || span.IsZero() {
return
}
location := new(descriptorpb.SourceCodeInfo_Location)
g.sourceCodeInfo.Location = append(g.sourceCodeInfo.Location, location)
location.Span = locationSpan(span)
location.Path = g.path.clone()
// Comments are merged across the provided [token.ID]s.
for _, id := range checkForComments {
comments, ok := g.commentTracker.attributed[id]
if !ok {
continue
}
if leadingComment := comments.leadingComment(); leadingComment != "" {
location.LeadingComments = addr(leadingComment)
}
if trailingComment := comments.trailingComment(); trailingComment != "" {
location.TrailingComments = addr(trailingComment)
}
if detachedComments := comments.detachedComments(); len(detachedComments) > 0 {
location.LeadingDetachedComments = detachedComments
}
}
}
// addr is a helper for creating a pointer out of any type, because Go is
// missing the syntax &"foo", etc.
func addr[T any](v T) *T { return &v }
// locationSpan is a helper function for returning the [descriptorpb.SourceCodeInfo_Location]
// span for the given [source.Span].
//
// The span for [descriptorpb.SourceCodeInfo_Location] always has exactly three or four:
// start line, start column, end line (optional, otherwise assumed same as start line),
// and end column. The line and column numbers are zero-based.
func locationSpan(span source.Span) []int32 {
start, end := span.StartLoc(), span.EndLoc()
if start.Line == end.Line {
return []int32{
int32(start.Line) - 1,
int32(start.Column) - 1,
int32(end.Column) - 1,
}
}
return []int32{
int32(start.Line) - 1,
int32(start.Column) - 1,
int32(end.Line) - 1,
int32(end.Column) - 1,
}
}
// 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 fdp
import (
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/bufbuild/protocompile/internal"
)
// path is an extension of [protoreflect.SourcePath] to provide an API for path tracking.
type path protoreflect.SourcePath
// clone returns a copy of the currently tracked source path.
func (p *path) clone() protoreflect.SourcePath {
return internal.ClonePath(protoreflect.SourcePath(*p))
}
// with adds the given elements to the tracked path and returns a reset function. The reset
// trims the length of the given elements off the tracked path. It is the caller's
// responsibility to ensure that reset is called on a valid path length.
func (p *path) with(elements ...int32) func() {
*p = append(*p, elements...)
return func() {
if len(*p) > 0 {
*p = (*p)[:len(*p)-len(elements)]
}
}
}
// 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 id
// Context is an "ID context", which allows converting between IDs and the
// underlying values they represent.
//
// Users of this package should not call the Context methods directly.
type Context interface {
// FromID gets the value for a given ID.
//
// The ID will be passed in as a raw 64-bit value. It is up to the caller
// to interpret it based on the requested type.
//
// The requested type is passed in via the parameter want, which will be
// a nil pointer to a value of the desired type. E.g., if the desired type
// is *int, want will be (**int)(nil).
FromID(id uint64, want any) any
}
// Constraint is a version of [Context] that can be used as a constraint.
type Constraint interface {
comparable
Context
}
// HasContext is a helper for adding IsZero and Context methods to a type.
//
// Simply alias it as an unexported type in your package, and embed it into
// types of interest.
type HasContext[Context comparable] struct {
context Context
}
// For embedding within this package.
type hasContext[Context comparable] = HasContext[Context]
// WrapContext wraps the context c in a [HasContext].
func WrapContext[Context comparable](c Context) HasContext[Context] {
return HasContext[Context]{c}
}
// IsZero returns whether this is a zero value.
func (c HasContext[Context]) IsZero() bool {
var z Context
return z == c.context
}
// Context returns this value's context.
func (c HasContext[Context]) Context() Context {
return c.context
}
// 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 id
import (
"fmt"
"reflect"
)
// Kind is a kind type usable in a [Dyn].
//
// Self should be the type implementing Kind.
type Kind[Self any] interface {
comparable // Make it not useable as an interface.
// Decodes the kind by decoding the low and high parts of a Dyn
// and setting this value to the result. A zero return value is taken to
// mean that decoding failed.
DecodeDynID(lo, hi int32) Self
// EncodeDynID encodes a new Dyn with the given value part. Returns
// arguments for [NewDynFromRaw].
EncodeDynID(value int32) (lo, hi int32, ok bool)
}
// ID is a generic 32-bit identifier for a value of type T. The zero value is
// reserved as a sentinel "no value" ID.
//
// IDs are typed and require a [Context] to be interpreted.
type ID[T any] int32
// IsZero returns whether this is the zero ID.
func (id ID[T]) IsZero() bool {
return id == 0
}
// String implements [fmt.Stringer].
func (id ID[T]) String() string {
ty := reflect.TypeFor[T]()
name := ty.Name()
if id.IsZero() {
return name + "(<nil>)"
}
if id < 0 {
return fmt.Sprintf("%s(^%d)", name, ^int32(id))
}
return fmt.Sprintf("%s(%d)", name, int32(id)-1)
}
// Dyn is a generic 64-bit identifier for a value of type T, intended to
// carry additional dynamic type information than an [ID]. It consists of a
// K and an [ID][T]. However, some uses may make use of the whole 64 bits of
// this ID, which can be accessed with [Dyn.Raw].
//
// DynIDs are typed and require a [Context] to be interpreted.
type Dyn[T any, K Kind[K]] uint64
// NewDyn encodes a new [Dyn] from the given parts.
func NewDyn[T any, K Kind[K]](kind K, id ID[T]) Dyn[T, K] {
lo, hi, ok := kind.EncodeDynID(int32(id))
if !ok {
return 0
}
return NewDynFromRaw[T, K](lo, hi)
}
// NewDynFromRaw encodes a new [Dyn] from the given raw parts.
func NewDynFromRaw[T any, K Kind[K]](lo, hi int32) Dyn[T, K] {
return Dyn[T, K](uint64(uint32(lo)) | (uint64(uint32(hi)) << 32))
}
// IsZero returns whether this is the zero ID.
func (id Dyn[T, K]) IsZero() bool {
return id == 0
}
// Kind returns the kind of this DynID.
func (id Dyn[T, K]) Kind() K {
var kind K
return kind.DecodeDynID(id.Raw())
}
// Value returns the id part of this DynID.
//
// If the resulting ID is not well-formed, returns zero.
func (id Dyn[T, K]) Value() ID[T] {
var z K
if id.Kind() == z {
return 0
}
_, v := id.Raw()
return ID[T](v)
}
// Raw reinterprets this ID as two 32-bit integers.
func (id Dyn[T, K]) Raw() (lo, hi int32) {
return int32(id), int32(id >> 32)
}
// String implements [fmt.Stringer].
func (id Dyn[T, K]) String() string {
ty := reflect.TypeFor[T]()
name := ty.Name()
if id.IsZero() {
return name + "(<nil>)"
}
a, b := id.Raw()
var z K
if k := id.Kind(); k != z {
if b < 0 {
return fmt.Sprintf("%s(%v, ^%d)", name, k, ^b)
}
return fmt.Sprintf("%s(%v, %d)", name, k, b-1)
}
return fmt.Sprintf("%s(%08x %08x)", name, a, b)
}
// 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 id
// Node is a raw node value with an associated context and ID.
//
// Types which are a context/ID pair should be defined as
//
// type MyNode id.Node[MyNode, Context, *rawMyNode]
type Node[T any, C Constraint, Raw any] = struct {
impl[T, C, Raw]
}
// DynNode is the equivalent of [Node] for [Dyn].
//
// Types which are a context/ID pair should be defined as
//
// type MyNode DynNode[MyNode, MyNodeKind, Context]
type DynNode[T any, K Kind[K], C Constraint] = struct {
implDynamic[T, K, C]
}
// Wrap gets the value of type T with the given ID from c.
//
// If c or id is its zero value (e.g. nil), returns a zero value.
func Wrap[T ~Node[T, C, Raw], C Constraint, Raw any](c C, id ID[T]) T {
var z C
if z == c || id.IsZero() {
return T{}
}
raw := c.FromID(uint64(uint32(id)), (*Raw)(nil))
if raw == nil {
return T{}
}
return T{
impl: impl[T, C, Raw]{
hasContext: hasContext[C]{c},
raw: raw.(Raw), //nolint:errcheck
id: int32(id),
},
}
}
// WrapDyn wraps a dynamic ID with a context.
//
// If c or id is its zero value (e.g. nil), returns a zero value.
func WrapDyn[T ~DynNode[T, K, C], K Kind[K], C Constraint](c C, id Dyn[T, K]) T {
var z C
if z == c || id.IsZero() {
return T{}
}
return T{
implDynamic: implDynamic[T, K, C]{
hasContext: hasContext[C]{c},
id: uint64(id),
},
}
}
// WrapRaw constructs a new T using the given raw value.
//
// If c or id is its zero value (e.g. nil), returns a zero value.
func WrapRaw[T ~Node[T, C, Raw], C Constraint, Raw any](c C, id ID[T], raw Raw) T {
var z C
if z == c || id.IsZero() {
return T{}
}
return T{
impl: impl[T, C, Raw]{
hasContext: hasContext[C]{c},
raw: raw,
id: int32(id),
},
}
}
// impl is where we hang the methods associated with Value from.
// These need to be defined in a separate struct, so that we can embed it into
// Value, so that then when named types use Value as their underlying type,
// they pick up those methods.
type impl[T any, C Constraint, Raw any] struct {
hasContext[C]
raw Raw
id int32
}
// Raw returns the wrapped raw value.
func (v impl[T, C, R]) Raw() R {
return v.raw
}
// ID returns this value's ID.
func (v impl[T, C, R]) ID() ID[T] {
return ID[T](v.id)
}
// See impl.
type implDynamic[T any, K Kind[K], C Constraint] struct {
hasContext[C]
id uint64
}
// Kind returns this value's kind.
func (v implDynamic[T, K, C]) Kind() K {
return v.ID().Kind()
}
// ID returns this value's ID.
func (v implDynamic[T, K, C]) ID() Dyn[T, K] {
return Dyn[T, K](v.id)
}
// 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 id
import (
"github.com/bufbuild/protocompile/experimental/seq"
)
// Seq is an array of nodes which uses a compressed representation.
//
// A zero value is empty and ready to use.
type Seq[T ~Node[T, C, Raw], C Constraint, Raw any] struct {
ids []ID[T]
}
// Inserter returns a [seq.Inserter] wrapping this [Seq].
func (s *Seq[T, C, Raw]) Inserter(context C) seq.Inserter[T] {
var ids *[]ID[T]
if s != nil {
ids = &s.ids
}
return seq.NewSliceInserter(
ids,
func(_ int, p ID[T]) T {
return Wrap(context, p)
},
func(_ int, t T) ID[T] {
return Node[T, C, Raw](t).ID()
},
)
}
// DynSeq is an array of dynamic nodes which uses a compressed representation.
//
// A zero value is empty and ready to use.
type DynSeq[T ~DynNode[T, K, C], K Kind[K], C Constraint] struct {
kinds []K
ids []ID[T]
}
// Inserter returns a [seq.Inserter] wrapping this [DynSeq].
func (s *DynSeq[T, K, C]) Inserter(context C) seq.Inserter[T] {
var kinds *[]K
var ids *[]ID[T]
if s != nil {
kinds = &s.kinds
ids = &s.ids
}
return seq.NewSliceInserter2(
kinds,
ids,
func(_ int, k K, p ID[T]) T {
return WrapDyn(context, NewDyn(k, p))
},
func(_ int, t T) (K, ID[T]) {
id := DynNode[T, K, C](t).ID()
return id.Kind(), id.Value()
},
)
}
// 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 incremental
import (
"context"
"fmt"
"runtime"
"slices"
"sync"
"sync/atomic"
"time"
"weak"
"golang.org/x/sync/semaphore"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/internal"
)
// Executor is a caching executor for incremental queries.
//
// See [New], [Run], and [Invalidate].
type Executor struct {
reportOptions report.Options
dirty sync.RWMutex
// TODO: Evaluate alternatives. sync.Map is pretty bad at having predictable
// performance, and we may want to add eviction to keep memoization costs
// in a long-running process (like, say, a language server) down.
// See https://github.com/dgraph-io/ristretto as a potential alternative.
tasks sync.Map // [any, *task]
sema *semaphore.Weighted
// The [time.Duration] to wait before running the GC when debug mode is on. See docs for
// [WithDebugEvict].
evictGCDeadline time.Duration
counter atomic.Uint64 // Used for generating sequence IDs for Result.Unchanged.
}
// ExecutorOption is an option func for [New].
type ExecutorOption func(*Executor)
// New constructs a new executor with the given maximum parallelism.
func New(options ...ExecutorOption) *Executor {
exec := &Executor{
sema: semaphore.NewWeighted(int64(runtime.GOMAXPROCS(0))),
}
for _, opt := range options {
opt(exec)
}
return exec
}
// WithParallelism sets the maximum number of queries that can execute in
// parallel. Defaults to GOMAXPROCS if not set explicitly.
func WithParallelism(n int64) ExecutorOption {
return func(e *Executor) { e.sema = semaphore.NewWeighted(n) }
}
// WithReportOptions sets the report options for reports generated by this
// executor.
func WithReportOptions(options report.Options) ExecutorOption {
return func(e *Executor) { e.reportOptions = options }
}
// WithDebugEvict takes a [time.Duration] and configures debug mode for evictions in the
// executor.
//
// If set and the compiler is built with the debug tag, when [Executor.EvictWithCleanup]
// is called, all evicted keys will be tracked. Then after eviction, a goroutine will be
// kicked off to sleep for the configured duration, force a GC run, and then print out all
// pointers that should be evicted but have not been GC'd.
func WithDebugEvict(wait time.Duration) ExecutorOption {
return func(e *Executor) { e.evictGCDeadline = wait }
}
// Keys returns a snapshot of the keys of which queries are present (and
// memoized) in an Executor.
//
// The returned slice is sorted.
func (e *Executor) Keys() (keys []string) {
e.tasks.Range(func(k, t any) bool {
task := t.(*task) //nolint:errcheck // All values in this map are tasks.
result := task.result.Load()
if result == nil || !closed(result.done) {
return true
}
keys = append(keys, fmt.Sprintf("%#v", k))
return true
})
slices.Sort(keys)
return
}
var runExecutorKey byte
// Run executes a set of queries on this executor in parallel.
//
// This function only returns an error if ctx expires during execution,
// in which case it returns nil and [context.Cause].
//
// Errors that occur during each query are contained within the returned results.
// Unlike [Resolve], these contain the *transitive* errors for each query!
//
// Implementations of [Query].Execute MUST NOT UNDER ANY CIRCUMSTANCES call
// Run. This will result in potential resource starvation or deadlocking, and
// defeats other correctness verification (such as cycle detection). Instead,
// use [Resolve], which takes a [Task] instead of an [Executor].
//
// Note: this function really wants to be a method of [Executor], but it isn't
// because it's generic.
func Run[T any](ctx context.Context, e *Executor, queries ...Query[T]) ([]Result[T], *report.Report, error) {
e.dirty.RLock()
defer e.dirty.RUnlock()
// Verify we haven't reëntrantly called Run.
if callers, ok := ctx.Value(&runExecutorKey).(*[]*Executor); ok {
if slices.Contains(*callers, e) {
panic("protocompile/incremental: reentrant call to Run")
}
*callers = append(*callers, e)
} else {
ctx = context.WithValue(ctx, &runExecutorKey, &[]*Executor{e})
}
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
generation := e.counter.Add(1)
root := &Task{
ctx: ctx,
cancel: cancel,
exec: e,
result: &result{done: make(chan struct{})},
runID: generation,
onRootGoroutine: true,
}
// Need to acquire a hold on the global semaphore to represent the root
// task we're about to execute.
if !root.acquire() {
return nil, nil, context.Cause(ctx)
}
defer root.release()
results, expired := Resolve(root, queries...)
if expired != nil {
if _, aborted := expired.(*errAbort); aborted {
panic(expired)
}
return nil, nil, expired
}
// Record all diagnostics generates by the queries.
report := &report.Report{Options: e.reportOptions}
dedup := make(map[*task]struct{})
tasks := make([]*task, 0, len(queries))
for _, query := range queries {
task, ok := e.getTask(query.Key())
if !ok {
continue // Uncompleted query, can happen due to an abort.
}
tasks = append(tasks, task)
}
for n := len(tasks); n > 0; n = len(tasks) {
node := tasks[n-1]
tasks = tasks[:n-1]
if _, ok := dedup[node]; ok {
continue
}
node.deps.Range(func(depAny any, _ any) bool {
dep := depAny.(*task) //nolint:errcheck
tasks = append(tasks, dep)
return true
})
dedup[node] = struct{}{}
report.Diagnostics = append(report.Diagnostics, node.report.Diagnostics...)
}
report.Canonicalize()
return results, report, nil
}
// Evict marks query keys as invalid, requiring those queries, and their
// dependencies, to be recomputed. keys that are not cached are ignored.
//
// This function cannot execute in parallel with calls to [Run], and will take
// an exclusive lock (note that [Run] calls themselves can be run in parallel).
func (e *Executor) Evict(keys ...any) {
e.EvictWithCleanup(keys, nil)
}
// EvictWithCleanup is like [Executor.Evict], but it executes the given cleanup
// function atomically with the eviction action.
//
// This function can be used to clean up after a query, or modify the result of
// the evicted query by writing to a variable, without risking concurrent calls
// to [Run] seeing inconsistent or stale state across multiple queries.
func (e *Executor) EvictWithCleanup(keys []any, cleanup func()) {
var tasks []*task
for _, key := range keys {
if t, ok := e.getTask(key); ok {
tasks = append(tasks, t)
}
}
if len(tasks) == 0 && cleanup == nil {
return
}
e.dirty.Lock()
defer e.dirty.Unlock()
var evicted []weak.Pointer[task]
logEvictionDebug := internal.Debug && e.evictGCDeadline > 0
for n := len(tasks); n > 0; n = len(tasks) {
next := tasks[n-1]
tasks = tasks[:n-1]
for k := range next.callers.Range {
tasks = append(tasks, k.(*task)) //nolint:errcheck
}
// Remove the task from the map. Syncronized by the dirty lock.
t, _ := e.tasks.LoadAndDelete(next.query.Key())
if logEvictionDebug {
evicted = append(evicted, weak.Make(t.(*task))) //nolint:errcheck
}
// Remove the task from the callers of its deps.
for k := range next.deps.Range {
k.(*task).callers.Delete(next) //nolint:errcheck
}
}
if evicted != nil {
go func() {
time.Sleep(e.evictGCDeadline)
runtime.GC()
evicted = slices.DeleteFunc(evicted, func(e weak.Pointer[task]) bool {
return e.Value() == nil
})
for _, e := range evicted {
internal.DebugLog(
[]any{"exec %p", e},
"EvictWithCleanup",
"failed to GC evicted task %p: %v",
e.Value(),
e.Value().query.Key(),
)
}
}()
}
if cleanup != nil {
cleanup()
}
}
// getTask returns a task pointer for the given key and whether it was found.
// The returned task is nil if found is false.
func (e *Executor) getTask(key any) (_ *task, found bool) {
if t, ok := e.tasks.Load(key); ok {
return t.(*task), true //nolint:errcheck
}
return nil, false
}
// getOrCreateTask returns (and creates if necessary) a task pointer for the given query.
// The returned task is never nil.
func (e *Executor) getOrCreateTask(query *AnyQuery) *task {
// Avoid allocating a new task object in the common case.
key := query.Key()
if t, ok := e.tasks.Load(key); ok {
return t.(*task) //nolint:errcheck
}
t, _ := e.tasks.LoadOrStore(key, &task{
query: query,
report: report.Report{Options: e.reportOptions},
})
return t.(*task) //nolint:errcheck
}
// 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 queries
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/incremental"
"github.com/bufbuild/protocompile/experimental/parser"
"github.com/bufbuild/protocompile/experimental/source"
)
// AST is an [incremental.Query] for the AST of a Protobuf file.
//
// AST queries with different Openers are considered distinct.
type AST struct {
source.Opener // Must be comparable.
Path string
}
var _ incremental.Query[*ast.File] = AST{}
// Key implements [incremental.Query].
//
// The key for a Contents query is the query itself. This means that a single
// [incremental.Executor] can host Contents queries for multiple Openers. It
// also means that the Openers must all be comparable. As the [Opener]
// documentation states, implementations should take a pointer receiver so that
// comparison uses object identity.
func (a AST) Key() any {
return a
}
// Execute implements [incremental.Query].
func (a AST) Execute(t *incremental.Task) (*ast.File, error) {
t.Report().Options.Stage += stageAST
r, err := incremental.Resolve(t, File{
Opener: a.Opener,
Path: a.Path,
ReportError: true,
})
if err != nil {
return nil, err
}
if r[0].Fatal != nil {
return nil, r[0].Fatal
}
file, _ := parser.Parse(a.Path, r[0].Value, t.Report())
return file, nil
}
// 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 queries
import (
"github.com/bufbuild/protocompile/experimental/incremental"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
)
// File is an [incremental.Query] for the contents of a file as provided
// by a [source.Opener].
//
// File queries with different Openers are considered distinct.
type File struct {
source.Opener // Must be comparable.
Path string
// If set, any errors generated from opening the file are logged as
// diagnostics. Setting this to false is useful for cases where the
// caller wants to emit a more general diagnostic.
ReportError bool
}
var _ incremental.Query[*source.File] = File{}
// Key implements [incremental.Query].
//
// The key for a File query is the query itself. This means that a single
// [incremental.Executor] can host File queries for multiple Openers. It
// also means that the Openers must all be comparable. As the [Opener]
// documentation states, implementations should take a pointer receiver so that
// comparison uses object identity.
func (f File) Key() any {
return f
}
// Execute implements [incremental.Query].
func (f File) Execute(t *incremental.Task) (*source.File, error) {
if !f.ReportError {
file, err := f.Open(f.Path)
if err != nil {
return nil, err
}
return file, nil
}
f.ReportError = false
r, err := incremental.Resolve(t, f)
if err != nil {
return nil, err
}
err = r[0].Fatal
if err != nil {
t.Report().Errorf("%v", err).Apply(
report.InFile(f.Path),
)
return nil, err
}
return r[0].Value, nil
}
// 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 queries
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/incremental"
"github.com/bufbuild/protocompile/experimental/ir"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// IR is an [incremental.Query] for the lowered IR of a Protobuf file.
//
// IR queries with different Openers are considered distinct.
type IR struct {
source.Opener // Must be comparable.
*ir.Session
Path string
// Used for tracking if this IR request was triggered by an import, for
// constructing a cycle error. This is not part of the query's key.
request ast.DeclImport
}
var _ incremental.Query[*ir.File] = IR{}
// Key implements [incremental.Query].
func (i IR) Key() any {
type key struct {
o source.Opener
s *ir.Session
path string
}
return key{i.Opener, i.Session, i.Path}
}
// Execute implements [incremental.Query].
func (i IR) Execute(t *incremental.Task) (*ir.File, error) {
t.Report().Options.Stage += stageIR
r, err := incremental.Resolve(t, AST{
Opener: i.Opener,
Path: i.Path,
})
if err != nil {
return nil, err
}
file := r[0].Value
// Check for descriptor.proto in the opener. If it's not present, that's
// going to produce a mess of weird errors, and we want this to be an ICE.
dp, err := incremental.Resolve(t, File{
Opener: i.Opener,
Path: ir.DescriptorProtoPath,
ReportError: false,
})
if err != nil {
return nil, err
}
if dp[0].Fatal != nil {
t.Report().Fatalf("could not import `%s`", ir.DescriptorProtoPath).Apply(
report.Notef("protocompile is not configured correctly"),
report.Helpf("`descriptor.proto` must always be available, since it is "+
"required for correctly implementing Protobuf's semantics. "+
"If you are using protocompile as a library, you may be missing a "+
"source.WKTs() in your source.Opener setup."),
)
return nil, dp[0].Fatal
}
// Resolve all of the imports in the AST.
queries := make([]incremental.Query[*ir.File],
// Preallocate for one extra query here, corresponding to the
// descriptor.proto query.
iterx.Count(file.Imports())+1)
errors := make([]error, len(queries))
for j, decl := range iterx.Enumerate(file.Imports()) {
lit := decl.ImportPath().AsLiteral().AsString()
path := lit.Text()
path = ir.CanonicalizeFilePath(path)
if lit.IsZero() {
// The import path is already legalized in [parser.legalizeImport()], if it is not
// a valid path, we just set a [incremental.ZeroQuery] so that we don't get a nil
// query for index j.
queries[j] = incremental.ZeroQuery[*ir.File]{}
continue
}
r, err := incremental.Resolve(t, File{
Opener: i.Opener,
Path: path,
ReportError: false,
})
if err != nil {
return nil, err
}
if err := r[0].Fatal; err != nil {
queries[j] = incremental.ZeroQuery[*ir.File]{}
errors[j] = r[0].Fatal
continue
}
queries[j] = IR{
Opener: i.Opener,
Session: i.Session,
Path: path,
request: decl,
}
}
queries[len(queries)-1] = IR{
Opener: i.Opener,
Session: i.Session,
Path: ir.DescriptorProtoPath,
}
imports, err := incremental.Resolve(t, queries...)
if err != nil {
return nil, err
}
importer := func(n int, _ string, _ ast.DeclImport) (*ir.File, error) {
if n == -1 {
// The lowering code will call the importer with n == -1 if it needs
// descriptor.proto but it isn't imported (transitively).
n = len(queries) - 1
}
result := imports[n]
switch err := result.Fatal.(type) {
case nil:
return result.Value, errors[n]
case *incremental.ErrCycle:
// We need to walk the cycle and extract which imports are
// responsible for the failure.
cyc := new(ir.ErrCycle)
for _, q := range err.Cycle {
irq, ok := incremental.AsTyped[IR](q)
if !ok {
continue
}
if !irq.request.IsZero() {
cyc.Cycle = append(cyc.Cycle, irq.request)
}
}
return nil, cyc
default:
return nil, err
}
}
ir, _ := i.Session.Lower(file, t.Report(), importer)
return ir, nil
}
// 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 incremental
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/internal/cycle"
)
// Query represents an incremental compilation query.
//
// Types which implement Query can be executed by an [Executor], which
// automatically caches the results of a query.
//
// Nil query values will cause [Run] and [Resolve] to panic.
type Query[T any] interface {
// Returns a unique key representing this query.
//
// This should be a comparable struct type unique to the query type. Failure
// to do so may result in different queries with the same key, which may
// result in incorrect results or panics.
Key() any
// Executes this query. This function will only be called if the result of
// this query is not already in the [Executor]'s cache.
//
// The error return should only be used to signal if the query failed. For
// non-fatal errors, you should record that information with [Task.NonFatal].
//
// Implementations of this function MUST NOT call [Run] on the executor that
// is executing them. This will defeat correctness detection, and lead to
// resource starvation (and potentially deadlocks).
//
// Panicking in execute is not interpreted as a fatal error that should be
// memoized; instead, it is treated as cancellation of the context that
// was passed to [Run].
Execute(*Task) (value T, fatal error)
}
// ErrCycle is an error due to cyclic dependencies.
type ErrCycle = cycle.Error[*AnyQuery]
// ErrPanic is returned by [Run] if any of the queries it executes panic.
// This error is used to cancel the [context.Context] that governs the call to
// [Run].
type ErrPanic struct {
Query *AnyQuery // The query that panicked.
Panic any // The actual value passed to panic().
Backtrace string // A backtrace for the panic.
}
// Error implements [error].
func (e *ErrPanic) Error() string {
return fmt.Sprintf(
"call to Query.Execute (key: %#v) panicked: %v\n%s",
e.Query.Key(), e.Panic, e.Backtrace,
)
}
// ZeroQuery is a [Query] that produces the zero value of T.
//
// This query is useful for cases where you are building a slice of queries out
// of some input slice, but some of the elements of that slice are invalid. This
// can be used as a "placeholder" query so that indices of the input slice
// match the indices of the result slice returned by [Resolve].
type ZeroQuery[T any] struct{}
// Key implements [Query].
func (q ZeroQuery[T]) Key() any { return q }
// Execute implements [Query].
func (q ZeroQuery[T]) Execute(_ *Task) (T, error) {
var zero T
return zero, nil
}
// AnyQuery is a [Query] that has been type-erased.
type AnyQuery struct {
actual, key any
execute func(*Task) (any, error)
}
// AsAny type-erases a [Query].
//
// This is intended to be combined with [Resolve], for cases where queries
// of different types want to be run in parallel.
//
// If q is nil, returns nil.
func AsAny[T any](q Query[T]) *AnyQuery {
if q == nil {
return nil
}
if q, ok := any(q).(*AnyQuery); ok {
return q
}
return &AnyQuery{
actual: q,
key: q.Key(),
execute: func(t *Task) (any, error) { return q.Execute(t) },
}
}
// Underlying returns the original, non-AnyQuery query this query was
// constructed with.
func (q *AnyQuery) Underlying() any {
if q == nil {
return nil
}
return q.actual
}
// Key implements [Query].
func (q *AnyQuery) Key() any { return q.key }
// Execute implements [Query].
func (q *AnyQuery) Execute(t *Task) (any, error) { return q.execute(t) }
// Format implements [fmt.Formatter].
func (q *AnyQuery) Format(state fmt.State, verb rune) {
fmt.Fprintf(state, fmt.FormatString(state, verb), q.Underlying())
}
// AsTyped undoes the effect of [AsAny].
//
// For some Query[any] values, you may be able to use ordinary Go type
// assertions, if the underlying type actually implements Query[any]. However,
// to downcast to a concrete Query[T] type, you must use this function.
func AsTyped[Q Query[T], T any](q Query[any]) (downcast Q, ok bool) {
if downcast, ok := q.(Q); ok {
return downcast, true
}
qAny, ok := q.(*AnyQuery)
if !ok {
var zero Q
return zero, false
}
downcast, ok = qAny.actual.(Q)
return downcast, ok
}
// 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 incremental
import (
"context"
"errors"
"fmt"
"runtime"
"runtime/debug"
"slices"
"sync"
"sync/atomic"
"golang.org/x/sync/semaphore"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
var (
errBadAcquire = errors.New("called acquire() while holding the semaphore")
errBadRelease = errors.New("called release() without holding the semaphore")
)
// Task represents a query that is currently being executed.
//
// Values of type Task are passed to [Query]. The main use of a Task is to
// be passed to [Resolve] to resolve dependencies.
type Task struct {
// We need all of the contexts for a call to [Run] to be the same, so to
// avoid user implementations of Query making this mistake (or inserting
// inappropriate timeouts), we pass the context as part of the task context.
ctx context.Context //nolint:containedctx
cancel func(error)
exec *Executor
task *task
result *result
runID uint64
// Set if we're currently holding the executor's semaphore. This exists to
// ensure that we do not violate concurrency assumptions, and is never
// itself mutated concurrently.
holding bool
// True if this task is intended to execute on the goroutine that called [Run].
onRootGoroutine bool
}
// Context returns the cancellation context for this task.
func (t *Task) Context() context.Context {
t.checkDone()
return t.ctx
}
// Report returns the diagnostic report for this task.
func (t *Task) Report() *report.Report {
t.checkDone()
return &t.task.report
}
// acquire acquires a hold on the global semaphore.
//
// Returns false if the underlying context has timed out.
func (t *Task) acquire() bool {
if t.holding {
t.abort(errBadAcquire)
}
t.holding = t.exec.sema.Acquire(t.ctx, 1) == nil
t.log("acquire", "%[1]v %[2]T/%[2]v", t.holding, t.task.underlying())
return t.holding
}
// release releases a hold on the global semaphore.
func (t *Task) release() {
t.log("release", "%[1]T/%[1]v", t.task.underlying())
if !t.holding {
if context.Cause(t.ctx) != nil {
// This context was cancelled, so acquires prior to this release
// may have failed, in which case we do nothing instead of panic.
return
}
t.abort(errBadRelease)
}
t.exec.sema.Release(1)
t.holding = false
}
// transferFrom acquires a hold on the global semaphore from the given task.
func (t *Task) transferFrom(that *Task) {
if t.holding || !that.holding {
t.abort(errBadAcquire)
}
t.holding, that.holding = that.holding, t.holding
t.log("acquireFrom", "%[1]T/%[1]v -> %[2]T/%[2]v",
that.task.underlying(),
t.task.underlying())
}
// log is used for printf debugging in the task scheduling code.
func (t *Task) log(what string, format string, args ...any) {
internal.DebugLog(
[]any{"%p/%d", t.exec, t.runID},
what, format, args...)
}
type errAbort struct {
err error
}
func (e *errAbort) Unwrap() error {
return e.err
}
func (e *errAbort) Error() string {
return fmt.Sprintf(
"incremental: internal error: %v (this is a bug in protocompile)", e.err,
)
}
// abort aborts the current computation due to an unrecoverable error.
//
// This will cause the outer call to Run() to immediately wake up and panic.
func (t *Task) abort(err error) {
t.log("abort", "%[1]T/%[1]v, %[2]v", t.task.underlying(), err)
if prev := t.aborted(); prev != nil {
// Prevent multiple errors from cascading and getting spammed all over
// the place.
err = prev
} else {
err = &errAbort{err}
t.cancel(err)
}
// Destroy the current task, it's in a broken state.
panic(err)
}
// aborted returns the error passed to [Task.abort] by some task in the current
// Run call.
//
// Returns nil if there is no such error.
func (t *Task) aborted() error {
err, ok := context.Cause(t.ctx).(*errAbort)
if !ok {
return nil
}
return err
}
// Resolve executes a set of queries in parallel. Each query is run on its own
// goroutine.
//
// If the context passed [Executor] expires, this will return [context.Cause].
// The caller must propagate this error to ensure the whole query graph exits
// quickly. Failure to propagate the error will result in incorrect query
// results.
//
// If a cycle is detected for a given query, the query will automatically fail
// and produce an [ErrCycle] for its fatal error. If the call to [Query].Execute
// returns an error, that will be placed into the fatal error value, instead.
//
// Callers should make sure to check each result's fatal error before using
// its value.
//
// Non-fatal errors for each result are only those that occurred as a direct
// result of query execution, and *does not* contain that query's transitive
// errors. This is unlike the behavior of [Run], which only collects errors at
// the very end. This ensures that errors are not duplicated, something that
// is not possible to do mid-query.
//
// Note: this function really wants to be a method of [Task], but it isn't
// because it's generic.
func Resolve[T any](caller *Task, queries ...Query[T]) (results []Result[T], expired error) {
caller.checkDone()
if len(queries) == 0 {
return nil, nil
}
results = make([]Result[T], len(queries))
anyQueries := make([]*AnyQuery, len(queries))
deps := make([]*task, len(queries))
// We use a semaphore here instead of a WaitGroup so that when we block
// on it later in this function, we can bail if caller.ctx is cancelled.
join := semaphore.NewWeighted(int64(len(queries)))
join.TryAcquire(int64(len(queries))) // Always succeeds because there are no waiters.
var needWait bool
for i, qt := range queries {
q := AsAny(qt) // This will also cache the result of q.Key() for us.
if q == nil {
return nil, fmt.Errorf(
"protocompile/incremental: nil query at index %[1]d while resolving from %[2]T/%[2]v",
i, caller.task.underlying(),
)
}
anyQueries[i] = q
dep := caller.exec.getOrCreateTask(q)
deps[i] = dep
// Update dependency graph.
callerTask := caller.task
if callerTask == nil {
continue // Root.
}
callerTask.deps.Store(dep, struct{}{})
dep.callers.Store(callerTask, struct{}{})
}
// Schedule all but the first query to run asynchronously.
for i, dep := range slices.Backward(deps) {
q := anyQueries[i]
// Run the zeroth query synchronously, inheriting this task's semaphore hold.
runNow := i == 0
async := dep.start(caller, q, runNow, func(r *result) {
if r != nil {
if r.Value != nil {
// This type assertion will always succeed, unless the user has
// distinct queries with the same key, which is a sufficiently
// unrecoverable condition that a panic is acceptable.
results[i].Value = r.Value.(T) //nolint:errcheck
}
results[i].Fatal = r.Fatal
results[i].Changed = r.runID == caller.runID
}
join.Release(1)
})
needWait = needWait || async
}
if needWait {
// Release our current hold on the global semaphore, since we're about to
// go to sleep. This avoids potential resource starvation for deeply-nested
// queries on low parallelism settings.
caller.release()
if join.Acquire(caller.ctx, int64(len(queries))) != nil {
return nil, context.Cause(caller.ctx)
}
// Reacquire from the global semaphore before returning, so
// execution of the calling task may resume.
if !caller.acquire() {
return nil, context.Cause(caller.ctx)
}
}
return results, context.Cause(caller.ctx)
}
// checkDone returns an error if this task is completed. This is to avoid shenanigans with
// tasks that escape their scope.
func (t *Task) checkDone() {
if closed(t.result.done) {
panic("protocompile/incremental: use of Task after the associated Query.Execute call returned")
}
}
// task is book-keeping information for a memoized Task in an Executor.
type task struct {
// The query that executed this task.
query *AnyQuery
// Direct dependencies. Tasks that this task depends on.
// Written on setup in Resolve. May be concurrent on invalid cyclic structures.
// TODO: See the comment on Executor.tasks.
deps sync.Map // map[*task]struct{}
// Inverse of deps. Contains all tasks that directly depend on this task.
// Written by multiple tasks concurrently.
// TODO: See the comment on Executor.tasks.
callers sync.Map // [*task]struct{}
// If this task has not been started yet, this is nil.
// Otherwise, if it is complete, result.done will be closed.
//
// In other words, if result is non-nil and result.done is not closed, this
// task is pending.
result atomic.Pointer[result]
report report.Report
}
// Result is the Result of executing a query on an [Executor], either via
// [Run] or [Resolve].
type Result[T any] struct {
Value T // Value is unspecified if Fatal is non-nil.
Fatal error
// Set if this result has possibly changed since the last time [Run] call in
// which this query was computed.
//
// This has important semantics wrt to calls to [Run]. If *any* call to
// [Resolve] downstream of a particular call to [Run] returns a true value
// for Changed for a particular query, all such calls to [Resolve] will.
// This ensures that the value of Changed is deterministic regardless of
// the order in which queries are actually scheduled.
//
// This flag can be used to implement partial caching of a query. If a query
// calculates the result of merging several queries, it can use its own
// cached result (provided by the caller of [Run] in some way) and the value
// of [Changed] to only perform a partial mutation instead of a complete
// merge of the queries.
Changed bool
}
// result is a Result[any] with a completion channel appended to it.
type result struct {
Result[any]
// This is the sequence ID of the Run call that caused this result to be
// computed. If it is equal to the ID of the current Run, it was computed
// during the current call. Otherwise, it is cached from a previous Run.
//
// Proof of correctness. As long as any Runs are ongoing, it is not possible
// for queries to be evicted, so once a query is calculated, its runID is
// fixed. Suppose two Runs race the same query. One of them will win as the
// leader, and the other will wait until it's done. The leader will mark it
// with its run ID, so the leader sees Changed and the loser sees !Changed.
// Any other queries from the same or other Runs racing this query will see
// the same result.
//
// Note that runID itself does not require synchronization, because loads of
// runID are synchronized-after the done channel being closed.
runID uint64
done chan struct{}
}
// start executes a query in the context of some task and records the result by
// calling done.
//
// If sync is false, the computation will occur asynchronously. Returns whether
// the computation is in fact executing asynchronously as a result.
func (t *task) start(caller *Task, q *AnyQuery, sync bool, done func(*result)) (async bool) {
// Common case for cached values; no need to spawn a separate goroutine.
r := t.result.Load()
if r != nil && closed(r.done) {
caller.log("cache hit", "%[1]T/%[1]v", q.Underlying())
done(r)
return false
}
if sync {
done(t.run(caller, q, false))
return false
}
// Complete the rest of the computation asynchronously.
go func() {
done(t.run(caller, q, true))
}()
return true
}
// checkCycle checks for a potential cycle. This is only possible if output is
// pending; if it isn't, it can't be in our history path.
func (t *task) checkCycle(caller *Task, q *AnyQuery) error {
deps := slicesx.NewQueue[*task](1)
deps.PushFront(t)
parent := make(map[*task]*task)
hasCycle := false
for node, ok := deps.PopFront(); ok; node, ok = deps.PopFront() {
if node == caller.task {
hasCycle = true
break
}
node.deps.Range(func(depAny any, _ any) bool {
dep := depAny.(*task) //nolint:errcheck
if _, ok := parent[dep]; !ok {
parent[dep] = node
deps.PushBack(dep)
}
return true
})
}
if !hasCycle {
return nil
}
// Reconstruct the cycle path from t.task back to target.
var cycle []*AnyQuery
cycle = append(cycle, caller.task.query)
for current := parent[caller.task]; current != nil && current != t; current = parent[current] {
cycle = append(cycle, current.query)
}
cycle = append(cycle, t.query)
// Reverse to get the correct dependency order (target -> ... -> t.task).
slices.Reverse(cycle)
// Add q at the end to complete the cycle (target -> ... -> t.task -> targetQuery).
// We use q instead of t.query because it has the import request info.
cycle = append(cycle, q)
return &ErrCycle{Cycle: cycle}
}
// run actually executes the query passed to start. It is called on its own
// goroutine.
func (t *task) run(caller *Task, q *AnyQuery, async bool) (output *result) {
output = t.result.Load()
if output != nil {
if closed(output.done) {
return output
}
return t.waitUntilDone(caller, output, q, async)
}
// Try to become the leader (the task responsible for computing the result).
output = &result{done: make(chan struct{})}
if !t.result.CompareAndSwap(nil, output) {
// We failed to become the executor, so we're gonna go to sleep
// until it's done.
output := t.result.Load()
if output == nil {
return nil // Leader panicked but we did see a result.
}
return t.waitUntilDone(caller, output, q, async)
}
callee := &Task{
ctx: caller.ctx,
cancel: caller.cancel,
exec: caller.exec,
runID: caller.runID,
task: t,
result: output,
onRootGoroutine: caller.onRootGoroutine && !async,
}
defer func() {
if caller.aborted() == nil {
if panicked := recover(); panicked != nil {
caller.log("panic", "%[1]T/%[1]v, %[2]v", q.Underlying(), panicked)
t.result.CompareAndSwap(output, nil)
output = nil
caller.cancel(&ErrPanic{
Query: q,
Panic: panicked,
Backtrace: string(debug.Stack()),
})
}
} else {
// If this task is pending and we're the leader, do not allow it to
// stick around. This will cause future calls to the same failed
// query to hit the cache.
t.result.CompareAndSwap(output, nil)
output = nil
if !callee.onRootGoroutine {
// For Gs spawned by the executor, we just kill them here without
// panicking, so we don't blow up the whole process. The root G for
// this Run call will panic when it exits Resolve.
_ = recover()
runtime.Goexit()
}
}
if output != nil && !closed(output.done) {
callee.log("done", "%[1]T/%[1]v", q.Underlying())
close(output.done)
}
}()
if async {
// If synchronous, this is executing under the hold of the caller query.
if !callee.acquire() {
return nil
}
defer callee.release()
} else {
// Steal our caller's semaphore hold.
callee.transferFrom(caller)
defer caller.transferFrom(callee)
}
callee.log("executing", "%[1]T/%[1]v", q.Underlying())
output.Value, output.Fatal = t.query.Execute(callee)
output.runID = callee.runID
callee.log("returning", "%[1]T/%[1]v", q.Underlying())
return output
}
// waitUntilDone waits for this task to be completed by another goroutine.
func (t *task) waitUntilDone(caller *Task, output *result, q *AnyQuery, async bool) *result {
if err := t.checkCycle(caller, q); err != nil {
output.Fatal = err
return output
}
// If this task is being executed synchronously with its caller, we need to
// drop our semaphore hold, otherwise we will deadlock: this caller will
// be waiting for the leader of this task to complete, but that one
// may be waiting on a semaphore hold, which it will not acquire due to
// tasks waiting for it to complete holding the semaphore in this function.
//
// If the task is being executed asynchronously, this function is not
// called while the semaphore is being held, which avoids the above
// deadlock scenario.
if !async {
caller.release()
}
select {
case <-output.done:
case <-caller.ctx.Done():
}
if !async && !caller.acquire() {
return nil
}
// Reload the result pointer. This is needed if the leader panics,
// because the result will be set to nil.
return t.result.Load()
}
// underlying returns the tasks query underlying key.
func (t *task) underlying() any {
if t != nil {
return t.query.Underlying()
}
return nil
}
// closed checks if ch is closed. This may return false negatives, in that it
// may return false for a channel which is closed immediately after this
// function returns.
func closed[T any](ch <-chan T) bool {
select {
case _, ok := <-ch:
return !ok
default:
return false
}
}
// 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 astx
import (
"fmt"
"slices"
"google.golang.org/protobuf/proto"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
compilerpb "github.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1"
)
// ToProtoOptions contains options for the [File.ToProto] function.
type ToProtoOptions struct {
// If set, no spans will be serialized.
//
// This operation only destroys non-semantic information.
OmitSpans bool
// If set, the contents of the file the AST was parsed from will not
// be serialized.
OmitFile bool
}
// ToProto converts this AST into a Protobuf representation, which may be
// serialized.
//
// Note that package ast does not support deserialization from this proto;
// instead, you will need to re-parse the text file included in the message.
// This is because the AST is much richer than what is stored in this message;
// the message only provides enough information for further semantic analysis
// and diagnostic generation, but not for pretty-printing.
//
// Panics if the AST contains a cycle (e.g. a message that contains itself as
// a nested message). Parsed ASTs will never contain cycles, but users may
// modify them into a cyclic state.
func ToProto(f *ast.File, options ToProtoOptions) proto.Message {
return (&protoEncoder{ToProtoOptions: options}).file(f) // See codec.go
}
// protoEncoder is the state needed for converting an AST node into a Protobuf message.
type protoEncoder struct {
ToProtoOptions
stack []source.Spanner
stackMap map[source.Spanner]struct{}
}
// checkCycle panics if v is visited cyclically.
//
// Should be called like this, so that on function exit the entry is popped:
//
// defer c.checkCycle(v)()
func (c *protoEncoder) checkCycle(v source.Spanner) func() {
// By default, we just perform a linear search, because inserting into
// a map is extremely slow. However, if the stack gets tall enough, we
// switch to using the map to avoid going quadratic.
if len(c.stack) > 32 {
c.stackMap = make(map[source.Spanner]struct{})
for _, v := range c.stack {
c.stackMap[v] = struct{}{}
}
c.stack = nil
}
var cycle bool
if c.stackMap != nil {
_, cycle = c.stackMap[v]
c.stackMap[v] = struct{}{}
} else {
cycle = slices.Contains(c.stack, v)
c.stack = append(c.stack, v)
}
if cycle {
panic(fmt.Sprintf("protocompile/ast: called File.ToProto on a cyclic AST %v", v.Span()))
}
return func() {
if c.stackMap != nil {
delete(c.stackMap, v)
} else {
c.stack = c.stack[len(c.stack)-1:]
}
}
}
func (c *protoEncoder) file(file *ast.File) *compilerpb.File {
proto := &compilerpb.File{
Decls: slices.Collect(seq.Map(file.Decls(), c.decl)),
}
if !c.OmitFile {
proto.File = &compilerpb.Report_File{
Path: file.Stream().Path(),
Text: []byte(file.Stream().Text()),
}
}
return proto
}
func (c *protoEncoder) span(s source.Spanner) *compilerpb.Span {
if c.OmitSpans || s == nil {
return nil
}
span := s.Span()
if span.IsZero() {
return nil
}
return &compilerpb.Span{
Start: uint32(span.Start),
End: uint32(span.End),
}
}
// commas is a non-generic subinterface of Commas[T].
type commas interface {
Len() int
Comma(int) token.Token
}
func (c *protoEncoder) commas(cs commas) []*compilerpb.Span {
if c.OmitSpans {
return nil
}
spans := make([]*compilerpb.Span, cs.Len())
for i := range spans {
spans[i] = c.span(cs.Comma(i))
}
return spans
}
func (c *protoEncoder) path(path ast.Path) *compilerpb.Path {
if path.IsZero() {
return nil
}
defer c.checkCycle(path)()
proto := &compilerpb.Path{
Span: c.span(path),
}
for pc := range path.Components {
component := new(compilerpb.Path_Component)
switch pc.Separator().Text() {
case ".":
component.Separator = compilerpb.Path_Component_SEPARATOR_DOT
case "/":
component.Separator = compilerpb.Path_Component_SEPARATOR_SLASH
}
component.SeparatorSpan = c.span(pc.Separator())
if extn := pc.AsExtension(); !extn.IsZero() {
extn := pc.AsExtension()
component.Component = &compilerpb.Path_Component_Extension{Extension: c.path(extn)}
component.ComponentSpan = c.span(extn)
} else if ident := pc.AsIdent(); !ident.IsZero() {
component.Component = &compilerpb.Path_Component_Ident{Ident: ident.Name()}
component.ComponentSpan = c.span(ident)
}
proto.Components = append(proto.Components, component)
}
return proto
}
func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl {
if decl.IsZero() {
return nil
}
defer c.checkCycle(decl)()
switch k := decl.Kind(); k {
case ast.DeclKindEmpty:
decl := decl.AsEmpty()
return &compilerpb.Decl{Decl: &compilerpb.Decl_Empty_{Empty: &compilerpb.Decl_Empty{
Span: c.span(decl),
}}}
case ast.DeclKindSyntax:
decl := decl.AsSyntax()
var kind compilerpb.Decl_Syntax_Kind
switch {
case decl.IsSyntax():
kind = compilerpb.Decl_Syntax_KIND_SYNTAX
case decl.IsEdition():
kind = compilerpb.Decl_Syntax_KIND_EDITION
}
return &compilerpb.Decl{Decl: &compilerpb.Decl_Syntax_{Syntax: &compilerpb.Decl_Syntax{
Kind: kind,
Value: c.expr(decl.Value()),
Options: c.options(decl.Options()),
Span: c.span(decl),
KeywordSpan: c.span(decl.KeywordToken()),
EqualsSpan: c.span(decl.Equals()),
SemicolonSpan: c.span(decl.Semicolon()),
}}}
case ast.DeclKindPackage:
decl := decl.AsPackage()
return &compilerpb.Decl{Decl: &compilerpb.Decl_Package_{Package: &compilerpb.Decl_Package{
Path: c.path(decl.Path()),
Options: c.options(decl.Options()),
Span: c.span(decl),
KeywordSpan: c.span(decl.KeywordToken()),
SemicolonSpan: c.span(decl.Semicolon()),
}}}
case ast.DeclKindImport:
decl := decl.AsImport()
var mods []compilerpb.Decl_Import_Modifier
var modSpans []*compilerpb.Span
for mod := range seq.Values(decl.ModifierTokens()) {
switch mod.Keyword() {
case keyword.Public:
mods = append(mods, compilerpb.Decl_Import_MODIFIER_PUBLIC)
case keyword.Weak:
mods = append(mods, compilerpb.Decl_Import_MODIFIER_WEAK)
default: // Add support for keyword.Option whenever it gets added to ast.proto.
mods = append(mods, compilerpb.Decl_Import_MODIFIER_UNSPECIFIED)
}
modSpans = append(modSpans, c.span(mod))
}
return &compilerpb.Decl{Decl: &compilerpb.Decl_Import_{Import: &compilerpb.Decl_Import{
Modifier: mods,
ImportPath: c.expr(decl.ImportPath()),
Options: c.options(decl.Options()),
Span: c.span(decl),
KeywordSpan: c.span(decl.KeywordToken()),
ModifierSpan: modSpans,
ImportPathSpan: c.span(decl.ImportPath()),
SemicolonSpan: c.span(decl.Semicolon()),
}}}
case ast.DeclKindBody:
decl := decl.AsBody()
return &compilerpb.Decl{Decl: &compilerpb.Decl_Body_{Body: &compilerpb.Decl_Body{
Span: c.span(decl),
Decls: slices.Collect(seq.Map(decl.Decls(), c.decl)),
}}}
case ast.DeclKindRange:
decl := decl.AsRange()
var kind compilerpb.Decl_Range_Kind
if decl.IsExtensions() {
kind = compilerpb.Decl_Range_KIND_EXTENSIONS
} else if decl.IsReserved() {
kind = compilerpb.Decl_Range_KIND_RESERVED
}
return &compilerpb.Decl{Decl: &compilerpb.Decl_Range_{Range: &compilerpb.Decl_Range{
Kind: kind,
Options: c.options(decl.Options()),
Span: c.span(decl),
KeywordSpan: c.span(decl.KeywordToken()),
SemicolonSpan: c.span(decl.Semicolon()),
Ranges: slices.Collect(seq.Map(decl.Ranges(), c.expr)),
}}}
case ast.DeclKindDef:
decl := decl.AsDef()
var kind compilerpb.Def_Kind
switch decl.Classify() {
case ast.DefKindMessage:
kind = compilerpb.Def_KIND_MESSAGE
case ast.DefKindEnum:
kind = compilerpb.Def_KIND_ENUM
case ast.DefKindService:
kind = compilerpb.Def_KIND_SERVICE
case ast.DefKindExtend:
kind = compilerpb.Def_KIND_EXTEND
case ast.DefKindField:
kind = compilerpb.Def_KIND_FIELD
case ast.DefKindEnumValue:
kind = compilerpb.Def_KIND_ENUM_VALUE
case ast.DefKindOneof:
kind = compilerpb.Def_KIND_ONEOF
case ast.DefKindGroup:
kind = compilerpb.Def_KIND_GROUP
case ast.DefKindMethod:
kind = compilerpb.Def_KIND_METHOD
case ast.DefKindOption:
kind = compilerpb.Def_KIND_OPTION
}
proto := &compilerpb.Def{
Kind: kind,
Name: c.path(decl.Name()),
Value: c.expr(decl.Value()),
Options: c.options(decl.Options()),
Span: c.span(decl),
KeywordSpan: c.span(decl.KeywordToken()),
EqualsSpan: c.span(decl.Equals()),
SemicolonSpan: c.span(decl.Semicolon()),
}
if kind == compilerpb.Def_KIND_FIELD ||
kind == compilerpb.Def_KIND_GROUP ||
kind == compilerpb.Def_KIND_UNSPECIFIED {
proto.Type = c.type_(decl.Type())
}
if signature := decl.Signature(); !signature.IsZero() {
proto.Signature = &compilerpb.Def_Signature{
Span: c.span(signature),
InputSpan: c.span(signature.Inputs()),
ReturnsSpan: c.span(signature.Returns()),
OutputSpan: c.span(signature.Outputs()),
Inputs: slices.Collect(seq.Map(signature.Inputs(), c.type_)),
Outputs: slices.Collect(seq.Map(signature.Outputs(), c.type_)),
}
}
if body := decl.Body(); !body.IsZero() {
proto.Body = &compilerpb.Decl_Body{
Span: c.span(decl.Body()),
Decls: slices.Collect(seq.Map(body.Decls(), c.decl)),
}
}
return &compilerpb.Decl{Decl: &compilerpb.Decl_Def{Def: proto}}
default:
panic(fmt.Sprintf("protocompile/ast: unknown DeclKind: %d", k))
}
}
func (c *protoEncoder) options(options ast.CompactOptions) *compilerpb.Options {
if options.IsZero() {
return nil
}
defer c.checkCycle(options)()
return &compilerpb.Options{
Span: c.span(options),
Entries: slices.Collect(seq.Map(options.Entries(), func(o ast.Option) *compilerpb.Options_Entry {
return &compilerpb.Options_Entry{
Path: c.path(o.Path),
Value: c.expr(o.Value),
EqualsSpan: c.span(o.Equals),
}
})),
}
}
func (c *protoEncoder) expr(expr ast.ExprAny) *compilerpb.Expr {
if expr.IsZero() {
return nil
}
defer c.checkCycle(expr)()
switch k := expr.Kind(); k {
case ast.ExprKindLiteral:
expr := expr.AsLiteral()
proto := &compilerpb.Expr_Literal{
Span: c.span(expr),
}
switch expr.Kind() {
case token.Number:
if v, exact := expr.Token.AsNumber().Int(); exact {
proto.Value = &compilerpb.Expr_Literal_IntValue{IntValue: v}
} else {
v, _ := expr.Token.AsNumber().Float()
proto.Value = &compilerpb.Expr_Literal_FloatValue{FloatValue: v}
}
case token.String:
proto.Value = &compilerpb.Expr_Literal_StringValue{StringValue: expr.AsString().Text()}
default:
panic(fmt.Sprintf("protocompile/ast: ExprLiteral contains neither string nor int: %v", expr.Token))
}
return &compilerpb.Expr{Expr: &compilerpb.Expr_Literal_{Literal: proto}}
case ast.ExprKindPath:
expr := expr.AsPath()
return &compilerpb.Expr{Expr: &compilerpb.Expr_Path{Path: c.path(expr.Path)}}
case ast.ExprKindPrefixed:
expr := expr.AsPrefixed()
var prefix compilerpb.Expr_Prefixed_Prefix
if expr.Prefix() == keyword.Sub {
prefix = compilerpb.Expr_Prefixed_PREFIX_MINUS
}
return &compilerpb.Expr{Expr: &compilerpb.Expr_Prefixed_{Prefixed: &compilerpb.Expr_Prefixed{
Prefix: prefix,
Expr: c.expr(expr.Expr()),
Span: c.span(expr),
PrefixSpan: c.span(expr.PrefixToken()),
}}}
case ast.ExprKindRange:
expr := expr.AsRange()
start, end := expr.Bounds()
return &compilerpb.Expr{Expr: &compilerpb.Expr_Range_{Range: &compilerpb.Expr_Range{
Start: c.expr(start),
End: c.expr(end),
Span: c.span(expr),
ToSpan: c.span(expr.Keyword()),
}}}
case ast.ExprKindArray:
expr := expr.AsArray()
a, b := expr.Brackets().StartEnd()
return &compilerpb.Expr{Expr: &compilerpb.Expr_Array_{Array: &compilerpb.Expr_Array{
Span: c.span(expr),
OpenSpan: c.span(a.LeafSpan()),
CloseSpan: c.span(b.LeafSpan()),
CommaSpans: c.commas(expr.Elements()),
Elements: slices.Collect(seq.Map(expr.Elements(), c.expr)),
}}}
case ast.ExprKindDict:
expr := expr.AsDict()
a, b := expr.Braces().StartEnd()
return &compilerpb.Expr{Expr: &compilerpb.Expr_Dict_{Dict: &compilerpb.Expr_Dict{
Span: c.span(expr),
OpenSpan: c.span(a.LeafSpan()),
CloseSpan: c.span(b.LeafSpan()),
CommaSpans: c.commas(expr.Elements()),
Entries: slices.Collect(seq.Map(expr.Elements(), c.exprField)),
}}}
case ast.ExprKindField:
expr := expr.AsField()
return &compilerpb.Expr{Expr: &compilerpb.Expr_Field_{Field: c.exprField(expr)}}
default:
panic(fmt.Sprintf("protocompile/ast: unknown ExprKind: %d", k))
}
}
func (c *protoEncoder) exprField(expr ast.ExprField) *compilerpb.Expr_Field {
if expr.IsZero() {
return nil
}
return &compilerpb.Expr_Field{
Key: c.expr(expr.Key()),
Value: c.expr(expr.Value()),
Span: c.span(expr),
ColonSpan: c.span(expr.Colon()),
}
}
//nolint:revive // "method type_ should be type" is incorrect because type is a keyword.
func (c *protoEncoder) type_(ty ast.TypeAny) *compilerpb.Type {
if ty.IsZero() {
return nil
}
defer c.checkCycle(ty)()
switch k := ty.Kind(); k {
case ast.TypeKindPath:
ty := ty.AsPath()
return &compilerpb.Type{Type: &compilerpb.Type_Path{Path: c.path(ty.Path)}}
case ast.TypeKindPrefixed:
ty := ty.AsPrefixed()
var prefix compilerpb.Type_Prefixed_Prefix
switch ty.Prefix() {
case keyword.Optional:
prefix = compilerpb.Type_Prefixed_PREFIX_OPTIONAL
case keyword.Repeated:
prefix = compilerpb.Type_Prefixed_PREFIX_REPEATED
case keyword.Required:
prefix = compilerpb.Type_Prefixed_PREFIX_REQUIRED
case keyword.Stream:
prefix = compilerpb.Type_Prefixed_PREFIX_STREAM
}
return &compilerpb.Type{Type: &compilerpb.Type_Prefixed_{Prefixed: &compilerpb.Type_Prefixed{
Prefix: prefix,
Type: c.type_(ty.Type()),
Span: c.span(ty),
PrefixSpan: c.span(ty.PrefixToken()),
}}}
case ast.TypeKindGeneric:
ty := ty.AsGeneric()
a, b := ty.Args().Brackets().StartEnd()
return &compilerpb.Type{Type: &compilerpb.Type_Generic_{Generic: &compilerpb.Type_Generic{
Path: c.path(ty.Path()),
Span: c.span(ty),
OpenSpan: c.span(a.LeafSpan()),
CloseSpan: c.span(b.LeafSpan()),
CommaSpans: c.commas(ty.Args()),
Args: slices.Collect(seq.Map(ty.Args(), c.type_)),
}}}
default:
panic(fmt.Sprintf("protocompile/ast: unknown TypeKind: %d", k))
}
}
// 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 astx
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// NewPath creates a new parser-generated path.
//
// This function should not be used outside of the parser, so it is implemented
// using unsafe to avoid needing to export it.
func NewPath(file *ast.File, start, end token.Token) ast.Path {
// fakePath has the same GC shape as ast.Path; there is a test for this in
// path_test.go
return unsafex.Bitcast[ast.Path](fakePath{
with: id.WrapContext(file),
raw: struct{ Start, End token.ID }{start.ID(), end.ID()},
})
}
type fakePath struct {
with id.HasContext[*ast.File]
raw struct{ Start, End token.ID }
}
// 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 cycle contains internal helpers for dealing with dependency cycles.
package cycle
import (
"fmt"
"strings"
)
// ErrCycle is an error due to cyclic dependencies.
type Error[T any] struct {
// The offending cycle. The first and last entries will be equal.
Cycle []T
}
// Error implements [error].
func (e *Error[T]) Error() string {
var buf strings.Builder
buf.WriteString("cycle detected: ")
for i, q := range e.Cycle {
if i != 0 {
buf.WriteString(" -> ")
}
fmt.Fprintf(&buf, "%#v", q)
}
return buf.String()
}
// 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"
"github.com/bufbuild/protocompile/experimental/source"
)
// TooOld diagnoses an edition that is too old for the feature used.
type TooOld struct {
What any
Where source.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 source.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
}
// 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:]
}
// 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 errtoken
import (
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// Unmatched diagnoses a delimiter for which we found one half of a matched
// delimiter but not the other.
type Unmatched struct {
Span source.Span // The offending delimiter.
Keyword keyword.Keyword
// If present, this indicates that we did match with another brace delimiter, but it
// was of the wrong kind
Mismatch source.Span
// If present, this is a brace delimiter we think we *should* have matched.
ShouldMatch source.Span
}
// Diagnose implements [report.Diagnose].
func (e Unmatched) Diagnose(d *report.Diagnostic) {
d.Apply(report.Message("encountered unmatched `%s` delimiter", e.Span.Text()))
left, right, _ := e.Keyword.Brackets()
if e.Keyword == left {
d.Apply(report.Snippetf(e.Span, "expected a closing `%s`", right))
if !e.Mismatch.IsZero() {
d.Apply(report.Snippetf(e.Mismatch, "closed by this instead"))
}
if !e.ShouldMatch.IsZero() {
d.Apply(report.Snippetf(e.ShouldMatch, "help: perhaps it was meant to match this?"))
}
} else {
d.Apply(report.Snippetf(e.Span, "expected a closing `%s`", left))
}
}
// 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 errtoken
import (
"strings"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
// errInvalidNumber diagnoses a numeric literal with invalid syntax.
type InvalidNumber struct {
Token token.Token // The offending number token.
}
// Diagnose implements [report.Diagnose].
func (e InvalidNumber) Diagnose(d *report.Diagnostic) {
// Check for an extra decimal point in the mantissa.
mant := e.Token.AsNumber().Mantissa()
first := strings.Index(mant.Text(), ".")
if first != -1 {
second := strings.Index(mant.Text()[first+1:], ".")
if second != -1 {
second += first + 1
d.Apply(
report.Message("extra decimal point in %s", taxa.Classify(e.Token)),
report.Snippet(mant.Range(second, second)),
report.Snippetf(mant.Range(first, first), "first one is here"),
)
return
}
}
// Ditto for the exponent.
exp := e.Token.AsNumber().Exponent()
first = strings.Index(exp.Text(), ".")
if first != -1 {
if first < exp.Len()-1 {
first++
}
d.Apply(
report.Message("non-integer exponent in %s", taxa.Classify(e.Token)),
report.Snippetf(exp.Range(first, exp.Len()), "fractional part given here"),
)
return
}
// Check for bad digits.
if e.badDigit(d, e.Token.AsNumber().Mantissa()) {
return
}
if e.badDigit(d, e.Token.AsNumber().Exponent()) {
return
}
// Fallback for when we don't have anything useful to say.
d.Apply(
report.Message("unexpected characters in %s", taxa.Classify(e.Token)),
report.Snippet(e.Token),
)
}
func (e InvalidNumber) badDigit(d *report.Diagnostic, digits source.Span) bool {
if digits.IsZero() {
return false
}
base := e.Token.AsNumber().Base()
for i, r := range digits.Text() {
if strings.ContainsRune("_-+.", r) {
continue
}
if _, ok := unicodex.Digit(r, base); ok {
continue
}
d.Apply(
report.Message("invalid digit in %s %s", e.baseName(), taxa.Classify(e.Token)),
report.Snippetf(digits.Range(i, i), "expected %s", e.digits()),
report.Snippetf(e.Token.AsNumber().Prefix(), "implies %s", e.baseName()),
)
if e.Token.AsNumber().IsLegacyOctal() {
d.Apply(report.Helpf("a leading `0` digit causes the whole literal to be interpreted as octal"))
}
return true
}
return false
}
func (e InvalidNumber) baseName() string {
switch e.Token.AsNumber().Base() {
case 2:
return "binary"
case 8:
return "octal"
case 10:
return "decimal"
case 16:
return "hexadecimal"
default:
return ""
}
}
func (e InvalidNumber) digits() string {
switch e.Token.AsNumber().Base() {
case 2:
return "`0` or `1`"
case 8:
return "`0` to `7`"
case 10:
return "`0` to `9`"
case 16:
return "`0` to `9`, or `a` to `f`"
default:
return ""
}
}
// 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 errtoken contains standard diagnostics involving tokens, usually emitted
// by the lexer.
package errtoken
import (
"fmt"
"strconv"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
)
// ImpureString diagnoses a string literal that probably should not contain
// escapes or concatenation.
type ImpureString struct {
Token token.Token
Where taxa.Place
}
// Diagnose implements [report.Diagnose].
func (e ImpureString) Diagnose(d *report.Diagnostic) {
text := e.Token.AsString().Text()
quote := e.Token.Text()[0]
d.Apply(
report.Message("non-canonical string literal %s", e.Where.String()),
report.Snippet(e.Token),
report.SuggestEdits(e.Token, "replace it with a canonical string", report.Edit{
Start: 0, End: e.Token.Span().Len(),
Replace: fmt.Sprintf("%c%v%c", quote, text, quote),
}),
)
if !e.Token.IsLeaf() {
d.Apply(
report.Notef(
"Protobuf implicitly concatenates adjacent %ss, like C or Python; this can lead to surprising behavior",
taxa.String,
),
)
}
}
// InvalidEscape diagnoses an invalid escape sequence within a string
// literal.
type InvalidEscape struct {
Span source.Span // The span of the offending escape within a literal.
}
// Diagnose implements [report.Diagnose].
func (e InvalidEscape) Diagnose(d *report.Diagnostic) {
d.Apply(report.Message("invalid escape sequence"))
text := e.Span.Text()
if len(text) < 2 {
d.Apply(report.Snippet(e.Span))
}
switch c := text[1]; c {
case 'x', 'X':
if len(text) < 3 {
d.Apply(report.Snippetf(e.Span, "`\\%c` must be followed by at least one hex digit", c))
return
}
return
case 'u', 'U':
expected := 4
if c == 'U' {
expected = 8
}
if len(text[2:]) != expected {
d.Apply(report.Snippetf(e.Span, "`\\%c` must be followed by exactly %d hex digits", c, expected))
return
}
value, _ := strconv.ParseUint(text[2:], 16, 32)
if !utf8.ValidRune(rune(value)) {
d.Apply(report.Snippetf(e.Span, "must be in the range U+0000 to U+10FFFF, except U+DC00 to U+DFFF"))
return
}
return
}
d.Apply(report.Snippet(e.Span))
}
// 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 errtoken
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// Unexpected is a low-level parser error for when we hit a token we don't
// know how to handle.
type Unexpected struct {
// The unexpected thing (may be a token or AST node).
What source.Spanner
// The context we're in. Should be format-able with %v.
Where taxa.Place
// Useful when where is an "after" position: if non-nil, this will be
// highlighted as "previous where.Object is here"
Prev source.Spanner
// What we wanted vs. what we got. Got can be used to customize what gets
// shown, but if it's not set, we call describe(what) to get a user-visible
// description.
Want taxa.Set
// If set and want is empty, the snippet will repeat the "unexpected foo"
// text under the snippet.
RepeatUnexpected bool
Got any
// If nonempty, inserting this text will be suggested at the given offset.
Insert string
InsertAt int
InsertJustify just.Kind
Stream *token.Stream
}
// UnexpectedEOF is a helper for constructing EOF diagnostics that need to
// provide *no* suggestions. This is used in places where any suggestion we
// could provide would be nonsensical.
func UnexpectedEOF(c *token.Cursor, where taxa.Place) Unexpected {
tok, span := c.Clone().SeekToEnd()
if tok.IsZero() {
return Unexpected{
What: span,
Where: where,
Got: taxa.EOF,
}
}
return Unexpected{What: tok, Where: where}
}
func (e Unexpected) Diagnose(d *report.Diagnostic) {
got := e.Got
if got == nil {
got = taxa.Classify(e.What)
if got == taxa.Unknown {
got = "tokens"
}
}
var message string
if e.Where.Subject() == taxa.Unknown {
message = fmt.Sprintf("unexpected %v", got)
} else {
message = fmt.Sprintf("unexpected %v %v", got, e.Where)
}
what := e.What.Span()
snippet := report.Snippet(what)
if e.Want.Len() > 0 {
snippet = report.Snippetf(what, "expected %v", e.Want.Join("or"))
} else if e.RepeatUnexpected {
snippet = report.Snippetf(what, "%v", message)
}
d.Apply(
report.Message("%v", message),
snippet,
report.Snippetf(e.Prev, "previous %v is here", e.Where.Subject()),
)
if tok, ok := e.What.(token.Token); ok {
d.Apply(
report.Debugf("token: %v, kind: %#v, keyword: %#v", tok.ID(), tok.Kind(), tok.Keyword()),
)
}
if e.Insert != "" {
want, _ := iterx.First(e.Want.All())
d.Apply(just.Justify(
e.Stream,
what,
fmt.Sprintf("consider inserting a %v", want),
just.Edit{
Edit: report.Edit{Replace: e.Insert},
Kind: e.InsertJustify,
},
))
}
}
// 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 just adds a "justification" helper for diagnostics.
//
// This package is currently internal because the API is a bit too messy to
// expose in report.
package just
import (
"slices"
"unicode"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
)
// Kind is a kind of justification implemented by this package.
//
// See [Justify] for details.
type Kind int
const (
None Kind = iota
Between
Left
Right
)
// Edit is a [report.Edit] with attached justification information, which
// can be passed to [Justify].
type Edit struct {
report.Edit
Kind Kind
}
// Justify generates suggested edits using justification information.
//
// "Justification" is a token-aware operation that ensures that each suggested
// edit is either:
//
// 1. Is [Between] spaces: surrounded on both sides by at least once space.
// 2. Has no whitespace to its [Left] or its [Right].
//
// See the comments on doJustify* for details on the different cases this
// function handles.
func Justify(stream *token.Stream, span source.Span, message string, edits ...Edit) report.DiagnosticOption {
for i := range edits {
switch edits[i].Kind {
case Between:
between(span, &edits[i].Edit)
case Left:
left(stream, span, &edits[i].Edit)
case Right:
right(stream, span, &edits[i].Edit)
}
}
return report.SuggestEditsWithWidening(span, message,
slices.Collect(slicesx.Map(edits, func(e Edit) report.Edit { return e.Edit }))...)
}
// between performs "between" justification.
//
// In well-formatted Protobuf, an equals sign should be surrounded by spaces on
// both sides. Thus, if the user wrote [option: 5], we want to suggest
// [option = 5]. justifyBetween handles this case by inserting an extra space
// into the replacement string, so that it goes from "=" to " =". We need to
// not blindly convert it into " = ", because that would suggest [option = 5],
// which looks ugly.
//
// It also handles the case [option/*foo*/: 5] by *not* being token aware: it
// will suggest [option/*foo*/ = 5].
//
// We *also* need to handle cases like [foo 5], where we want to insert an
// sign that somehow got deleted. The suggestion will probably be placed right
// after foo, so naively it will become [foo= 5], and after justification,
// [foo = 5]. To avoid this, we have a special case where we move the insertion
// point one space over to avoid needing to insert an extra space, producing
// [foo = 5].
//
// Of course, all of these operations are performed symmetrically.
func between(span source.Span, e *report.Edit) {
text := span.File.Text()
// Helpers which returns the number of bytes of the space before or
// after the given offset. This byte width is used to shift the
// replaced region when there are extra spaces around it.
spaceAfter := func(idx int) int {
r, ok := stringsx.Rune(text, idx+span.Start)
if !ok || !unicode.IsSpace(r) {
return 0
}
return utf8.RuneLen(r)
}
spaceBefore := func(idx int) int {
r, ok := stringsx.PrevRune(text, idx+span.Start)
if !ok || !unicode.IsSpace(r) {
return 0
}
return utf8.RuneLen(r)
}
// If possible, shift the offset such that it is surrounded by
// whitespace. However, this is not always possible, in which case we
// must add whitespace to text.
prev := spaceBefore(e.Start)
next := spaceAfter(e.End)
switch {
case prev > 0 && next > 0:
// Nothing to do here.
case prev > 0:
if !e.IsDeletion() && spaceBefore(e.Start-prev) > 0 {
// Case for inserting = into [foo 5].
e.Start -= prev
e.End -= prev
} else {
// Case for replacing : -> = in [foo :5].
e.Replace += " "
}
case next > 0:
if !e.IsDeletion() && spaceAfter(e.End+next) > 0 {
// Mirror-image case for inserting = into [foo 5].
e.Start += next
e.End += next
} else {
// Case for replacing : -> = in [foo: 5].
e.Replace = " " + e.Replace
}
default:
// Case for replacing : -> = in [foo:5].
e.Replace = " " + e.Replace + " "
}
}
// left performs left justification.
//
// This will ensure that the suggestion is as far to the left as possible before
// any other token.
//
// For example, consider the following fragment.
//
// int32 x
// int32 y;
//
// We want to suggest a semicolon after x. However, the parser won't give up
// parsing followers of x until it hits int32 on the second line, by which time
// it's very hard to figure out, from the parser state, where the semicolon
// should go. So, we suggest inserting it immediately before the second int32,
// but with left justification: that will cause the suggestion to move until
// just after x on the first line.
//
// This must use token information to work correctly. Consider now
//
// int32 x // comment
// int32 y;
//
// If we simply chased spaces backwards, we would wind up with the following
// bad suggestion:
//
// int32 x // comment;
// int32 y;
//
// To avoid this, we instead rewind past any skippable tokens, which is why
// we use a stream here.
//
// This is used in some other palces, such as when converting {x = y} into
// {x: y}. In this case, because we're performing a deletion, we *consume*
// the extra space, instead of merely moving the insertion point. This case
// can result in comments getting deleted; avoiding this is probably not
// worth it. E.g. `{x/*f*/ = y}` becomes `{x: y}`, because the deleted region
// is expanded from "=" into "/*f*/ =".
func left(stream *token.Stream, span source.Span, e *report.Edit) {
wasDelete := e.IsDeletion()
// Get the token at the start of the span.
start, _ := stream.Around(e.Start + span.Start)
if start.IsZero() {
// Start of the file, so we can't rewind beyond this.
return
}
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
}
}
// right is the mirror image of doJustifyLeft.
func right(stream *token.Stream, span source.Span, e *report.Edit) {
wasDelete := e.IsDeletion()
// Get the token at the end of the span.
_, end := stream.Around(e.End + span.Start)
if end.IsZero() {
// End of the file, so we can't fast-forward beyond this.
return
}
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
}
}
// 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 lexer
import (
"math"
"math/big"
"strings"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
)
// MaxFileSize is the maximum file size the lexer supports.
const MaxFileSize int = math.MaxInt32 // 2GB
// OnKeyword is an action to take in response to a [Lexer] encountering a
// [keyword.Keyword].
type OnKeyword int
const (
// If the keyword is punctuation, reject it; if it's a reserved word, treat
// it as an identifier.
DiscardKeyword OnKeyword = iota
// Accept the keyword as a [token.Keyword].
HardKeyword
// Accept the keyword as a [token.Ident]. The keyword must be a reserved
// word, otherwise behaves like HardKeyword.
SoftKeyword
// Accept the keyword, and treat it as an open brace. It must be one of
// the open brace keywords.
BracketKeyword
// Treat the keyword as starting a line comment through to the next newline.
LineComment
// Treat the keyword as starting a block comment. It must be one of the
// open brace keywords.
BlockComment
)
// Lexer is the general-purpose lexer exposed by this file.
type Lexer struct {
// How to handle a known keyword when encountered by the lexer.
OnKeyword func(keyword.Keyword) OnKeyword
// Used for validating prefixes and suffixes of strings and numbers.
IsAffix func(affix string, kind token.Kind, suffix bool) bool
// EmitNewline indicates to the lexer that whitespace containing newlines
// should be emitted as keywords and specifies the conditions for doing so.
// This allows for using newlines as synthetic line endings.
//
// EmitNewline is called for each newline appearing in the input text, with
// non-skippable, non-newline tokens before and after it. If the function
// returns true, that newline is treated as a keyword; otherwise, it is
// treated as a space.
EmitNewline func(before, after token.Token) bool
// If true, a dot immediately followed by a digit is taken to begin a
// digit.
NumberCanStartWithDot bool
// If true, decimal numbers starting with 0 are treated as octal instead.
OldStyleOctal bool
// If true, diagnostics are emitted for non-ASCII identifiers.
RequireASCIIIdent bool
EscapeExtended bool // Escapes \a, \b, \f, and \v.
EscapeAsk bool // Escape \?.
EscapeOctal bool // Octal escapes other than \0
EscapePartialX bool // Partial \xN escapes.
EscapeUppercaseX bool // The unusual \XNN escape.
EscapeOldStyleUnicode bool // Old-style Unicode escapes \uXXXX and \UXXXXXXXX.
}
// Lex runs lexical analysis on file and returns a new token stream as a result.
func (l *Lexer) Lex(file *source.File, r *report.Report) *token.Stream {
stream := &token.Stream{File: file}
loop(&lexer{Lexer: l, Stream: stream, Report: r})
return stream
}
// lexer is the actual lexer book-keeping used in this package.
type lexer struct {
*Lexer
*token.Stream
*report.Report
cursor, count int
braces []token.ID
scratch []byte
scratchFloat *big.Float
// Used for determining longest runs of unrecognized tokens.
badBytes int
}
// push pushes a new token onto the stream the lexer is building.
func (l *lexer) push(length int, kind token.Kind) token.Token {
return l.keyword(length, kind, keyword.Unknown)
}
// keyword pushes a new keyword token onto the stream the lexer is building.
func (l *lexer) keyword(length int, kind token.Kind, kw keyword.Keyword) token.Token {
if l.badBytes > 0 {
l.count++
tok := l.Stream.Push(l.badBytes, token.Unrecognized)
l.badBytes = 0
l.Errorf("unrecognized token").Apply(
report.Snippet(tok),
)
}
l.count++
return l.Stream.PushKeyword(length, kind, kw)
}
// rest returns the remaining unlexed text.
func (l *lexer) rest() string {
return l.Text()[l.cursor:]
}
// done returns whether or not we're done lexing runes.
func (l *lexer) done() bool {
return l.rest() == ""
}
// peek peeks the next character.
//
// Returns -1 if l.done().
func (l *lexer) peek() rune {
r, ok := stringsx.Rune(l.rest(), 0)
if !ok {
return -1
}
return r
}
// pop consumes the next character.
//
// Returns -1 if l.done().
func (l *lexer) pop() rune {
r := l.peek()
if r != -1 {
l.cursor += utf8.RuneLen(r)
return r
}
return -1
}
// takeWhile consumes the characters while they match the given function.
// Returns consumed characters.
func (l *lexer) takeWhile(f func(rune) bool) string {
start := l.cursor
for !l.done() {
r := l.peek()
if r == -1 || !f(r) {
break
}
_ = l.pop()
}
return l.Text()[start:l.cursor]
}
// seekInclusive seek until the given needle is found; returns the prefix inclusive that
// needle, and updates the cursor to point after it.
func (l *lexer) seekInclusive(needle string) (string, bool) {
if idx := strings.Index(l.rest(), needle); idx != -1 {
prefix := l.rest()[:idx+len(needle)]
l.cursor += idx + len(needle)
return prefix, true
}
return "", false
}
// seekEOF seeks the cursor to the end of the file and returns the remaining text.
func (l *lexer) seekEOF() string {
rest := l.rest()
l.cursor += len(rest)
return rest
}
func (l *lexer) spanFrom(start int) source.Span {
return l.Span(start, l.cursor)
}
// mustProgress returns a progress checker for this lexer.
func (l *lexer) mustProgress() mustProgress {
return mustProgress{l, -1}
}
// 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 lexer
import (
"slices"
"strings"
"unicode"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"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/stringsx"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
// loop is the main loop of the lexer.
func loop(l *lexer) {
defer l.CatchICE(false, func(d *report.Diagnostic) {
d.Apply(
report.Snippetf(l.Span(l.cursor, l.cursor), "cursor is here"),
report.Notef("cursor: %d, count: %d", l.cursor, l.count),
)
})
if !lexPrelude(l) {
return
}
// This is the main loop of the lexer. Each iteration will examine the next
// rune in the source file to determine what action to take.
mp := l.mustProgress()
for !l.done() {
mp.check()
if unicode.In(l.peek(), unicode.Pattern_White_Space) {
// Whitespace. Consume as much whitespace as possible and mint a
// whitespace token.
whitespace := l.takeWhile(func(r rune) bool {
return unicode.In(r, unicode.Pattern_White_Space)
})
// Chop the consumed whitespace into lines and emit the newlines as
// keywords. At the end, we will convert newlines that we want to
// eliminate into spaces.
for {
var space string
var newline bool
space, whitespace, newline = strings.Cut(whitespace, "\n")
if space != "" {
l.push(len(space), token.Space)
}
if !newline {
break
}
if l.EmitNewline == nil {
// No need to actually emit a keyword in this case.
l.push(1, token.Space)
} else {
l.keyword(1, token.Keyword, keyword.Newline)
}
}
}
// Find the next valid keyword.
var what OnKeyword
var kw keyword.Keyword
for k := range keyword.Prefixes(l.rest()) {
n := l.OnKeyword(k)
if n != DiscardKeyword {
kw = k
what = n
}
}
switch what {
case SoftKeyword, HardKeyword, BracketKeyword:
word := kw.String()
if l.NumberCanStartWithDot && kw == keyword.Dot {
next, _ := stringsx.Rune(l.rest(), len(word))
if unicode.IsDigit(next) {
break
}
}
kind := token.Keyword
if kw.IsReservedWord() {
if what == SoftKeyword {
kind = token.Ident
}
// If this is a reserved word, the rune after it must not be
// an XID continue.
next, _ := stringsx.Rune(l.rest(), len(word))
if unicodex.IsXIDContinue(next) {
break
}
}
l.cursor += len(word)
tok := l.keyword(len(word), kind, kw)
if what == BracketKeyword {
l.braces = append(l.braces, tok.ID())
}
continue
case LineComment:
word := kw.String()
l.cursor += len(word)
var text string
if comment, ok := l.seekInclusive("\n"); ok {
text = comment
} else {
text = l.seekEOF()
}
var newline bool
text, newline = strings.CutSuffix(text, "\n")
l.keyword(len(word)+len(text), token.Comment, kw)
if newline {
if l.EmitNewline == nil {
// No need to actually emit a keyword in this case.
l.push(1, token.Space)
} else {
l.keyword(1, token.Keyword, keyword.Newline)
}
}
continue
case BlockComment:
word := kw.String()
l.cursor += len(word)
// Block comment. Seek to the next "*/". Protobuf comments
// unfortunately do not nest, and allowing them to nest can't
// be done in a backwards-compatible manner. We acknowledge that
// this behavior is user-hostile.
//
// If we encounter no "*/", seek EOF and emit a diagnostic. Trying
// to lex a partial comment is hopeless.
_, end, fused := kw.Brackets()
if kw == end {
// The user definitely thought nested comments were allowed. :/
tok := l.push(len(end.String()), token.Unrecognized)
l.Error(errtoken.Unmatched{Span: tok.Span(), Keyword: kw}).Apply(
report.Notef("nested `%s` comments are not supported", fused),
)
continue
}
var text string
if comment, ok := l.seekInclusive(end.String()); ok {
text = comment
} else {
// Create a span for the /*, that's what we're gonna highlight.
l.Error(errtoken.Unmatched{
Span: l.spanFrom(l.cursor - len(word)),
Keyword: kw,
})
text = l.seekEOF()
}
l.keyword(len(word)+len(text), token.Comment, fused)
continue
}
r := l.pop()
switch {
case r == '"', r == '\'':
l.cursor-- // Back up to behind the quote before resuming.
lexString(l, "")
case l.NumberCanStartWithDot && r == '.', unicode.IsDigit(r):
// Back up behind the rune we just popped.
l.cursor -= utf8.RuneLen(r)
lexNumber(l)
case unicodex.IsXIDStart(r):
// Back up behind the rune we just popped.
l.cursor -= utf8.RuneLen(r)
rawIdent := l.takeWhile(unicodex.IsXIDContinue)
// Eject any trailing unprintable characters.
id := strings.TrimRightFunc(rawIdent, func(r rune) bool {
return !unicode.IsPrint(r)
})
if id == "" {
// This "identifier" appears to consist entirely of unprintable
// characters (e.g. combining marks).
tok := l.push(len(rawIdent), token.Unrecognized)
l.Errorf("unrecognized token").Apply(
report.Snippet(tok),
report.Debugf("%v, %v, %q", tok.ID(), tok.Span(), tok.Text()),
)
continue
}
// Figure out if we should be doing a prefixed string instead.
next := l.peek()
if next == '"' || next == '\'' &&
// Check to see if we like this prefix.
l.IsAffix != nil && l.IsAffix(rawIdent, token.String, false) {
l.cursor -= len(rawIdent)
lexString(l, rawIdent)
continue
}
l.cursor -= len(rawIdent) - len(id)
tok := l.push(len(id), token.Ident)
// Legalize non-ASCII runes.
if l.RequireASCIIIdent && !unicodex.IsASCIIIdent(tok.Text()) {
l.Errorf("non-ASCII identifiers are not allowed").Apply(
report.Snippet(tok),
)
}
default:
l.badBytes += utf8.RuneLen(r)
}
}
// Fuse brace pairs. We do this at the very end because it's easier to apply
// lookahead heuristics here.
fuseBraces(l)
// Perform implicit string concatenation.
fuseStrings(l)
// Eliminate any newline tokens that we don't actually need to have exist.
newlines(l, token.Zero)
}
// lexPrelude performs various file-prelude checks, such as size and encoding
// verification. Returns whether lexing should proceed.
func lexPrelude(l *lexer) bool {
if l.Text() == "" {
return true
}
// Check that the file isn't too big. We give up immediately if that's
// the case.
if len(l.Text()) > MaxFileSize {
l.Errorf("files larger than 2GB (%d bytes) are not supported", MaxFileSize).Apply(
report.InFile(l.Path()),
)
return false
}
// Heuristically check for a UTF-16-encoded file. There are two good
// heuristics:
// 1. Presence of a UTF-16 BOM, which is either FE FF or FF FE, depending on
// endianness.
// 2. Exactly one of the first two bytes is a NUL. Valid Protobuf cannot
// contain a NUL in the first two bytes, so this is probably a UTF-16-encoded
// ASCII rune.
bom16 := strings.HasPrefix(l.Text(), "\xfe\xff") || strings.HasPrefix(l.Text(), "\xff\xfe")
ascii16 := len(l.Text()) >= 2 && (l.Text()[0] == 0 || l.Text()[1] == 0)
if bom16 || ascii16 {
l.Errorf("input appears to be encoded with UTF-16").Apply(
report.InFile(l.Path()),
report.Notef("Protobuf files must be UTF-8 encoded"),
)
return false
}
// Check that the text of the file is actually UTF-8.
var idx int
var count int
for i, r := range stringsx.Runes(l.Text()) {
if r != -1 {
continue
}
if count == 0 {
idx = i
}
count++
}
frac := float64(count) / float64(len(l.Text()))
switch {
case frac == 0:
break
case frac < 0.2:
// This diagnostic is for cases where this file appears to be corrupt.
// We pick 20% non-UTF-8 as the threshold to show this error.
l.Errorf("input appears to be encoded with UTF-8, but found invalid byte").Apply(
report.Snippet(l.Span(idx, idx+1)),
report.Notef("non-UTF-8 byte occurs at offset %d (%#x)", idx, idx),
report.Notef("Protobuf files must be UTF-8 encoded"),
)
return false
default:
l.Errorf("input appears to be a binary file").Apply(
report.InFile(l.Path()),
report.Notef("non-UTF-8 byte occurs at offset %d (%#x)", idx, idx),
report.Notef("Protobuf files must be UTF-8 encoded"),
)
return false
}
if l.peek() == '\uFEFF' {
l.pop() // Peel off a leading UTF-8 BOM.
l.push(3, token.Unrecognized)
}
return true
}
// fuseBraces performs brace matching and token fusion, based on the contents of
// l.braces.
func fuseBraces(l *lexer) {
var opens []token.ID
for i := 0; i < len(l.braces); i++ {
// At most four tokens are considered for fusion in one loop iteration,
// named t0 through t3. The first token we extract is the third in this
// sequence and thus is named t2.
t2 := id.Wrap(l.Stream, l.braces[i])
open, _, _ := t2.Keyword().Brackets()
if t2.Keyword() == open {
opens = append(opens, t2.ID())
continue
}
// If no opens are present, this is an orphaned close brace.
if len(opens) == 0 {
l.Error(errtoken.Unmatched{Span: t2.Span(), Keyword: t2.Keyword()})
continue
}
t1 := id.Wrap(l.Stream, opens[len(opens)-1])
if t1.Keyword() == open {
// Common case: the braces match.
token.Fuse(t1, t2)
opens = opens[:len(opens)-1]
continue
}
// Check to see how similar this situation is to something like
// the "irreducible" braces {[}]. This catches common cases of unpaired
// braces.
var t0, t3 token.Token
if len(opens) > 1 {
t0 = id.Wrap(l.Stream, opens[len(opens)-2])
}
// Don't seek for the next unpaired closer; that results in quadratic
// behavior. Instead, we just look at i+1.
if i+1 < len(l.braces) {
t3 = id.Wrap(l.Stream, l.braces[i+1])
}
nextOpen, _, _ := t3.Keyword().Brackets()
leftMatch := t0.Keyword() == open
rightMatch := t3.Keyword() != nextOpen && t1.Keyword() == nextOpen
switch {
case leftMatch && rightMatch:
l.Error(errtoken.Unmatched{
Span: t1.Span(),
Keyword: t1.Keyword(),
Mismatch: t2.Span(),
ShouldMatch: t3.Span(),
})
token.Fuse(t0, t2)
// We do not fuse t1 to t3, since that would result in partially
// overlapping nested token trees, which violates an invariant of
// the token stream data structure.
opens = opens[:len(opens)-2]
i++
case leftMatch:
l.Error(errtoken.Unmatched{
Span: t1.Span(),
Keyword: t1.Keyword(),
})
token.Fuse(t0, t2) // t1 does not get fused in this case.
opens = opens[:len(opens)-2]
case rightMatch:
l.Error(errtoken.Unmatched{
Span: t1.Span(),
Keyword: t1.Keyword(),
Mismatch: t2.Span(),
ShouldMatch: t3.Span(),
})
token.Fuse(t1, t3) // t2 does not get fused in this case.
opens = opens[:len(opens)-1]
i++
default:
l.Error(errtoken.Unmatched{
Span: t2.Span(),
Keyword: t2.Keyword(),
})
// No fusion happens here, we treat t2 as being orphaned.
}
}
// Legalize against unclosed delimiters.
for _, open := range opens {
open := id.Wrap(l.Stream, open)
l.Error(errtoken.Unmatched{Span: open.Span(), Keyword: open.Keyword()})
}
// In backwards order, generate empty tokens to fuse with
// the unclosed delimiters.
for _, open := range slices.Backward(opens) {
empty := l.push(0, token.Unrecognized)
token.Fuse(id.Wrap(l.Stream, open), empty)
}
}
// fuseStrings fuses adjacent string literals into their concatenations. This
// implements implicit concatenation by juxtaposition.
func fuseStrings(l *lexer) {
concat := func(start, end token.Token) {
if start.IsZero() || start == end {
return
}
var escapes []tokenmeta.Escape
var buf strings.Builder
for i := start.ID(); i <= end.ID(); i++ {
tok := id.Wrap(l.Stream, i)
if s := tok.AsString(); !s.IsZero() {
buf.WriteString(s.Text())
if s.Raw() != nil {
escapes = append(escapes, s.Raw().Escapes...)
}
}
}
meta := token.MutateMeta[tokenmeta.String](start)
meta.Text = buf.String()
meta.Concatenated = true
meta.Escapes = escapes
token.Fuse(start, end)
}
var start, end token.Token
for tok := range l.All() {
switch tok.Kind() {
case token.Space, token.Comment:
case token.String:
if start.IsZero() {
start = tok
} else {
overall := start.AsString().Prefix()
prefix := tok.AsString().Prefix()
if !prefix.IsZero() && overall.Text() != prefix.Text() {
l.Errorf("implicitly-concatenated string has incompatible prefix").Apply(
report.Snippet(prefix),
report.Snippetf(overall, "must match this prefix"),
)
}
}
end = tok
default:
concat(start, end)
start = token.Zero
end = token.Zero
}
}
concat(start, end)
}
// newlines suppresses newline keyword tokens according to l.EmitNewline.
func newlines(l *lexer, tree token.Token) {
if l.EmitNewline == nil {
return
}
var prev, next token.Token
var cursor *token.Cursor
if tree.IsZero() {
cursor = l.Stream.Cursor()
} else {
cursor = tree.Children()
prev, next = tree.StartEnd()
}
end := next
needNext := true
for !cursor.Done() {
tok := cursor.Next()
if tok.Keyword() != keyword.Newline {
prev = tok
next = end
needNext = true
continue
}
// Fast forward to the next non-newline token.
if needNext {
cursor := cursor.Clone()
for !cursor.Done() {
if tok := cursor.Next(); tok.Keyword() != keyword.Newline {
next = tok
break
}
}
needNext = false
}
if !l.EmitNewline(prev, next) {
// Overwrite the type of this token.
tok.SetKind(token.Space)
}
}
}
// 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 lexer
import (
"math"
"math/big"
"math/bits"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
var log2Table = func() (logs [16]float64) {
for i := range logs {
logs[i] = math.Log2(float64(i + 1))
}
return logs
}()
type parseIntResult struct {
small uint64
big *big.Float
hasThousands bool
}
// parseInt parses an integer into a uint64 or, on overflow, into a big.Int.
//
// This function ignores any thousands separator underscores in digits.
func parseInt(digits string, base byte) (result parseIntResult, ok bool) {
var bigBase, bigDigit *big.Float
for _, r := range digits {
if r == '_' {
result.hasThousands = true
continue
}
digit, ok := unicodex.Digit(r, base)
if !ok {
return result, false
}
if result.big == nil {
// Perform arithmetic while checking for overflow.
extra, shift := bits.Mul64(result.small, uint64(base))
sum, carry := bits.Add64(shift, uint64(digit), 0)
if extra == 0 && carry == 0 {
result.small = sum
continue
}
// We overflowed, so we need to spill into a big.Float.
result.big = new(big.Float)
result.big.SetUint64(result.small)
bigBase = new(big.Float).SetUint64(uint64(base)) // Memoize converting the base.
bigDigit = new(big.Float)
}
result.big.Mul(result.big, bigBase)
result.big.Add(result.big, bigDigit.SetUint64(uint64(digit)))
}
return result, true
}
// 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 lexer
import (
"fmt"
"math"
"math/big"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
var (
decFloat = fpRegexp("0-9", "eEpP")
hexFloat = fpRegexp("0-9a-fA-F", "pP")
// e and E are conspicuously missing here; this is so that 01e1 is treated
// as a decimal float.
treatAsOctal = regexp.MustCompile("^[0-9a-dfA-DF_]+$")
)
// lexNumber lexes a number starting at the current cursor.
func lexNumber(l *lexer) token.Token {
tok := lexRawNumber(l)
digits := tok.Text()
// Select the correct base we're going to be parsing.
var (
prefix string
base byte
legacyOctal bool // Whether this is a C-style 0777 octal.
)
if len(digits) >= 2 {
prefix = digits[:2]
}
switch prefix {
case "0b", "0B":
digits = digits[2:]
base = 2
case "0o", "0O":
digits = digits[2:]
base = 8
case "0x", "0X":
digits = digits[2:]
base = 16
default:
if l.OldStyleOctal &&
len(digits) >= 2 && digits[0] == '0' && // Note: `0` is not octal.
treatAsOctal.MatchString(digits) { // Float-likes are not octal.
prefix = digits[:1]
base = 8
legacyOctal = true
break
}
prefix = ""
base = 10
}
if base != 10 {
token.MutateMeta[tokenmeta.Number](tok).Base = base
}
isFloat := taxa.IsFloatText(digits)
expBase := 1
expIdx := -1
if isFloat {
if expIdx = strings.IndexAny(digits, "pP"); expIdx != -1 {
expBase = 2
} else if expIdx = strings.IndexAny(digits, "eE"); expIdx != -1 {
expBase = 10
}
}
if expBase != 1 {
token.MutateMeta[tokenmeta.Number](tok).ExpBase = byte(expBase)
}
// Peel a suffix off of digits consisting of characters not in the
// desired base.
haystack := digits
suffixBase := base
if expIdx != -1 {
suffixBase = 10
haystack = haystack[expIdx+1:]
}
suffixIdx := strings.IndexFunc(haystack, func(r rune) bool {
if strings.ContainsRune("_.+-", r) {
return false
}
_, ok := unicodex.Digit(r, suffixBase)
return !ok
})
var suffix string
if suffixIdx != -1 {
suffix = haystack[suffixIdx:]
// Check to see if we like this suffix.
if l.IsAffix != nil && l.IsAffix(suffix, token.Number, true) {
digits = digits[:len(digits)-len(suffix)]
} else {
suffix = ""
}
}
if prefix != "" {
token.MutateMeta[tokenmeta.Number](tok).Prefix = uint32(len(prefix))
}
if suffix != "" {
token.MutateMeta[tokenmeta.Number](tok).Suffix = uint32(len(suffix))
}
if expIdx != -1 {
// Example: 123e456suffix, want len("e456").
// len(digits) = 13
// expIdx = 3
// suffix = 6
//
// -> 13 - 6 - 3 = 13 - 9 = 4
offset := len(digits) - expIdx - len(suffix)
token.MutateMeta[tokenmeta.Number](tok).Exp = uint32(offset)
}
result, ok := parseInt(digits, base)
switch {
case !ok:
if l.scratchFloat == nil {
l.scratchFloat = new(big.Float)
}
v := l.scratchFloat
meta := token.MutateMeta[tokenmeta.Number](tok)
// Convert legacyOctal values that are *not* pure integers into decimal
// floats.
if legacyOctal && !treatAsOctal.MatchString(digits) {
base = 10
meta.Base = 10
meta.Prefix = 0
}
meta.IsFloat = strings.ContainsAny(digits, ".-") // Positive exponents are not necessarily floats.
meta.ThousandsSep = strings.Contains(digits, "_")
// Wrapper over big.Float.Parse that ensures we never round. big.Float
// does not have a parse mode that uses maximum precision for that
// input, so we infer the correct precision from the size of the
// mantissa and exponent.
parse := func(v *big.Float, digits string) (*big.Float, error) {
n := tok.AsNumber()
mant := n.Mantissa().Text()
exp := n.Exponent().Text()
// Convert digits into binary digits. Note that digits in base b
// is log_b(n). Note that, per the log base change formula:
//
// log_2(n) = log_b(n)/log_b(2)
// log_b(2) = log_2(2)/log_2(b) = 1/log_2(b)
//
// Thus, we want to multiply by log_2(b), which we precompute in
// a table.
prec := float64(len(mant)) * log2Table[base-1]
// If there is an exponent, add it to the precision.
if exp != "" {
// Convert to the right base and add it to prec.
exp, _ := strconv.ParseInt(exp, 0, 64)
prec += math.Abs(float64(exp)) * log2Table[expBase-1]
}
v.SetPrec(uint(math.Ceil(prec)))
_, _, err := v.Parse(digits, 0)
return v, err
}
var err error
switch base {
case 10:
match := decFloat.FindStringSubmatch(digits)
if match == nil {
goto fail
}
if expBase != 2 {
v, err = parse(v, digits)
break
}
v, err = parse(v, match[1])
exp, err := strconv.ParseInt(match[3], 10, 64)
if err != nil {
exp = math.MaxInt
}
exp += int64(v.MantExp(nil))
v.SetMantExp(v, int(exp))
case 16:
if !hexFloat.MatchString(digits) {
goto fail
}
l.scratch = l.scratch[:0]
l.scratch = append(l.scratch, "0x"...)
l.scratch = append(l.scratch, digits...)
digits := unsafex.StringAlias(l.scratch)
v, err = parse(v, digits)
default:
goto fail
}
if err != nil {
goto fail
}
// We want this to overflow to Infinity as needed, which ParseFloat
// will do for us. Otherwise it will ties-to-even as the
// protobuf.com spec requires.
//
// ParseFloat itself says it "returns the nearest floating-point
// number rounded using IEEE754 unbiased rounding", which is just a
// weird, non-standard way to say "ties-to-even".
if meta.IsFloat {
f64, acc := v.Float64()
if acc != big.Exact {
meta.Big = v
l.scratchFloat = nil
} else {
meta.Word = math.Float64bits(f64)
l.scratchFloat.SetUint64(0)
}
} else {
u64, acc := v.Uint64()
if acc != big.Exact {
meta.Big = v
l.scratchFloat = nil
} else {
meta.Word = u64
l.scratchFloat.SetUint64(0)
}
}
return tok
case result.big != nil:
token.MutateMeta[tokenmeta.Number](tok).Big = result.big
case base == 10 && !result.hasThousands:
// We explicitly do not call SetValue for the most common case of base
// 10 integers, because that is handled for us on-demand in AsInt. This
// is a memory consumption optimization.
default:
token.MutateMeta[tokenmeta.Number](tok).Word = result.small
}
if result.hasThousands {
token.MutateMeta[tokenmeta.Number](tok).ThousandsSep = true
}
return tok
fail:
l.Error(errtoken.InvalidNumber{Token: tok})
token.MutateMeta[tokenmeta.Number](tok).SyntaxError = true
return tok
}
// lexRawNumber lexes a raw number per the rules at
// https://protobuf.com/docs/language-spec#numeric-literals
func lexRawNumber(l *lexer) token.Token {
start := l.cursor
for !l.done() {
r := l.peek()
//nolint:gocritic // This trips a noisy "use a switch" lint that makes
// this code less readable.
if r == 'e' || r == 'E' {
l.pop()
r = l.peek()
if r == '+' || r == '-' {
l.pop()
}
} else if r == '.' || unicode.IsDigit(r) || unicode.IsLetter(r) ||
// We consume _ because 0_n is not valid in any context, so we
// can offer _ digit separators as an extension.
r == '_' {
l.pop()
} else {
break
}
}
// Create the token, even if this is an invalid number. This will help
// the parser pick up bad numbers into number literals.
digits := l.Text()[start:l.cursor]
return l.push(len(digits), token.Number)
}
// fpRegexp constructs a regexp for a float with the given digits and exponent
// characters.
func fpRegexp(digits string, exp string) *regexp.Regexp {
block := func(digits string) string {
// This ensures that underscores only appear between digits: either
// the subpattern consisting of just digits matches, or the subpattern
// containing underscores, but bookended by digits, matches.
return fmt.Sprintf(`[%[1]s]+|[%[1]s][%[1]s_]+[%[1]s]`, digits)
}
pat := fmt.Sprintf(
`^((?:%[1]s)?(?:\.(?:%[1]s)?)?)(?:([%[2]s])([+-]?%[3]s))?$`,
block(digits), exp, block("0-9"),
)
return regexp.MustCompile(pat)
}
// 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 lexer
// mustProgress is a helper for ensuring that the lexer makes progress
// in each loop iteration. This is intended for turning infinite loops into
// panics.
type mustProgress struct {
l *lexer
prev int
}
// check panics if the lexer has not advanced since the last call.
func (mp *mustProgress) check() {
if mp.prev == mp.l.cursor {
// NOTE: no need to annotate this panic; it will get wrapped in the
// call to HandleICE for us.
panic("lexer failed to make progress; this is a bug in protocompile")
}
mp.prev = mp.l.cursor
}
// 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 lexer
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
// lexString lexes a string starting at the current cursor.
//
// The cursor position should be just before the string's first quote character.
func lexString(l *lexer, sigil string) {
start := l.cursor
l.cursor += len(sigil)
// Check for a triple quote.
quote := l.rest()[:1]
if len(l.rest()) >= 3 && l.rest()[1:2] == quote && l.rest()[2:3] == quote {
quote = l.rest()[:3]
}
l.cursor += len(quote)
var (
buf strings.Builder
terminated bool
escapes []tokenmeta.Escape
)
for !l.done() {
if strings.HasPrefix(l.rest(), quote) {
l.cursor += len(quote)
terminated = true
break
}
cursor := l.cursor
sc := lexStringContent(l)
if !sc.escape.IsZero() || escapes != nil {
if escapes == nil {
// If we saw our first escape, spill the string into the buffer
// up to just before the escape.
buf.WriteString(l.Text()[start+1 : cursor])
}
escape := tokenmeta.Escape{
Start: uint32(sc.escape.Start),
End: uint32(sc.escape.End),
}
if sc.isRawByte {
escape.Byte = byte(sc.rune)
buf.WriteByte(byte(sc.rune))
} else {
escape.Rune = sc.rune
buf.WriteRune(sc.rune)
}
escapes = append(escapes, escape)
}
}
tok := l.push(l.cursor-start, token.String)
if escapes != nil {
meta := token.MutateMeta[tokenmeta.String](tok)
meta.Text = buf.String()
meta.Escapes = escapes
}
if sigil != "" {
token.MutateMeta[tokenmeta.String](tok).Prefix = uint32(len(sigil))
}
if len(quote) > 1 {
token.MutateMeta[tokenmeta.String](tok).Quote = uint32(len(quote))
}
if !terminated {
var note report.DiagnosticOption
if len(tok.Text()) == 1 {
note = report.Notef("this string consists of a single orphaned quote")
} else if strings.HasSuffix(tok.Text(), quote) && len(quote) == 1 {
note = report.SuggestEdits(
tok,
"this string appears to end in an escaped quote",
report.Edit{
Start: tok.Span().Len() - 2, End: tok.Span().Len(),
Replace: fmt.Sprintf(`\\%s%s`, quote, quote),
},
)
}
l.Errorf("unterminated string literal").Apply(
report.Snippetf(tok, "expected to be terminated by `%s`", quote),
note,
)
}
}
type stringContent struct {
rune rune
isRawByte bool
escape source.Span
}
// lexStringContent lexes a single logical rune's worth of content for a quoted
// string.
func lexStringContent(l *lexer) (sc stringContent) {
start := l.cursor
r := l.pop()
switch {
case r == 0:
esc := l.spanFrom(l.cursor - utf8.RuneLen(r))
l.Errorf("unescaped NUL bytes are not permitted in string literals").Apply(
report.Snippet(esc),
report.SuggestEdits(esc, "replace it with `\\0` or `\\x00`", report.Edit{
Start: 0,
End: 1,
Replace: "\\0",
}),
)
case r == '\n':
// TODO: This diagnostic is simply user-hostile. We should remove it.
// Not having this is valuable for strings that contain e.g. CEL
// expressions, and there is no technical reason that Protobuf forbids
// it. (A historical note: C forbids this because Ken's original
// C preprocessor, written in PDP11 assembly, was incapable of dealing
// with multi-line tokens because Ken didn't originally bother.
// Many programming languages have since thoughtlessly copied this
// choice, including Protobuf, whose lexical morphology is almost
// exactly C's).
nl := l.spanFrom(l.cursor - utf8.RuneLen(r))
l.Errorf("unescaped newlines are not permitted in string literals").Apply(
report.Snippet(nl),
report.Helpf("consider splitting this into adjacent string literals; Protobuf will automatically concatenate them"),
)
case unicodex.NonPrint(r):
// Warn if the user has a non-printable character in their string that isn't
// ASCII whitespace.
var escape string
switch {
case r < 0x80:
escape = fmt.Sprintf(`\x%02x`, r)
case r < 0x10000:
escape = fmt.Sprintf(`\u%04x`, r)
default:
escape = fmt.Sprintf(`\U%08x`, r)
}
esc := l.spanFrom(l.cursor - utf8.RuneLen(r))
l.Warnf("non-printable character in string literal").Apply(
report.Snippet(esc),
report.SuggestEdits(esc, "consider escaping it", report.Edit{
Start: 0,
End: len(esc.Text()),
Replace: escape,
}),
)
}
if r != '\\' {
// We intentionally do not legalize against literal \0 and \n. The above warning
// covers \0 and legalizing against \n is user-hostile. This is valuable for
// e.g. strings that contain CEL code.
//
// In other words, this limitation helps no one, so we ignore it.
return stringContent{rune: r}
}
r = l.pop()
escSwitch:
switch r {
// These are all the simple escapes.
case 'n':
sc.rune = '\n'
sc.escape = l.spanFrom(start)
return sc
case 'r':
sc.rune = '\r'
sc.escape = l.spanFrom(start)
return sc
case 't':
sc.rune = '\t'
sc.escape = l.spanFrom(start)
return sc
case '\\', '\'', '"':
sc.escape = l.spanFrom(start)
sc.rune = r
return sc
case 'a':
if !l.EscapeExtended {
break
}
sc.rune = '\a' // U+0007
sc.escape = l.spanFrom(start)
return sc
case 'b':
if !l.EscapeExtended {
break
}
sc.rune = '\b' // U+0008
sc.escape = l.spanFrom(start)
return sc
case 'f':
if !l.EscapeExtended {
break
}
sc.rune = '\f' // U+000C
sc.escape = l.spanFrom(start)
return sc
case 'v':
if !l.EscapeExtended {
break
}
sc.rune = '\v' // U+000B
sc.escape = l.spanFrom(start)
return sc
case '?':
if !l.EscapeAsk {
break
}
sc.rune = '?'
sc.escape = l.spanFrom(start)
return sc
// Octal escape. Need to eat the next two runes if they're octal.
case '0', '1', '2', '3', '4', '5', '6', '7':
if !l.EscapeOctal {
if r == '0' {
sc.rune = 0
sc.escape = l.spanFrom(start)
return sc
}
break
}
sc.isRawByte = true
sc.rune = r - '0'
for i := 0; i < 2 && !l.done(); i++ {
// Check before consuming the rune. If we see e.g.
// an 8, we don't want to consume it.
r = l.peek()
if r < '0' || r > '7' {
break
}
_ = l.pop()
sc.rune *= 8
sc.rune += r - '0'
}
sc.escape = l.spanFrom(start)
return sc
// Hex escapes. And yes, the 'X' is no mistake: Protobuf is one of the
// only language that supports \XNN as an alias for \xNN, something not
// even C offers! https://en.cppreference.com/w/c/language/escape
case 'x', 'X', 'u', 'U':
var digits, consumed int
switch r {
case 'X':
if !l.EscapeUppercaseX {
break escSwitch
}
fallthrough
case 'x':
digits = 2
sc.isRawByte = true
case 'u':
if !l.EscapeOldStyleUnicode {
break escSwitch
}
digits = 4
case 'U':
if !l.EscapeOldStyleUnicode {
break escSwitch
}
digits = 8
}
for i := 0; i < digits && !l.done(); i++ {
digit, ok := unicodex.Digit(l.peek(), 16)
if !ok {
break
}
sc.rune *= 16
sc.rune += rune(digit)
l.pop()
consumed++
}
sc.escape = l.spanFrom(start)
if consumed == 0 {
l.Error(errtoken.InvalidEscape{Span: sc.escape})
} else if !l.EscapePartialX || !sc.isRawByte {
// \u and \U must have exact numbers of digits.
if consumed != digits || !utf8.ValidRune(sc.rune) {
l.Error(errtoken.InvalidEscape{Span: sc.escape})
}
}
return sc
}
sc.escape = l.spanFrom(start)
l.Error(errtoken.InvalidEscape{Span: sc.escape})
return sc
}
// 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 taxa
import (
"strings"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// IsFloat checks whether or not tok is intended to be a floating-point literal.
func IsFloat(tok token.Token) bool {
return tok.AsNumber().IsFloat()
}
// IsFloatText checks whether or not the given number text is intended to be
// a floating-point literal.
func IsFloatText(digits string) bool {
needle := ".EePp"
if strings.HasPrefix(digits, "0x") || strings.HasPrefix(digits, "0X") {
needle = ".Pp"
}
return strings.ContainsAny(digits, needle)
}
// Classify attempts to classify node for use in a diagnostic.
func Classify(node source.Spanner) Noun {
// This is a giant type switch on every AST and token type in the compiler.
switch node := node.(type) {
case token.Token:
switch node.Kind() {
case token.Space:
return Whitespace
case token.Comment:
return Comment
case token.Ident:
if kw := node.Keyword(); kw != keyword.Unknown {
return Noun(kw)
}
return Ident
case token.String:
return String
case token.Number:
if node.AsNumber().IsFloat() {
return Float
}
return Int
case token.Keyword:
return Noun(node.Keyword())
default:
return Unrecognized
}
case *ast.File:
return TopLevel
case ast.Path:
if first, ok := iterx.OnlyOne(node.Components); ok && first.Separator().IsZero() {
if id := first.AsIdent(); !id.IsZero() {
return Classify(id)
}
if !first.AsExtension().IsZero() {
return ExtensionName
}
}
if node.Absolute() {
return FullyQualifiedName
}
return QualifiedName
case ast.DeclAny:
switch node.Kind() {
case ast.DeclKindEmpty:
return Classify(node.AsEmpty())
case ast.DeclKindSyntax:
return Classify(node.AsSyntax())
case ast.DeclKindPackage:
return Classify(node.AsPackage())
case ast.DeclKindImport:
return Classify(node.AsImport())
case ast.DeclKindRange:
return Classify(node.AsRange())
case ast.DeclKindBody:
return Classify(node.AsBody())
case ast.DeclKindDef:
return Classify(node.AsDef())
default:
return Decl
}
case ast.DeclEmpty:
return Empty
case ast.DeclSyntax:
if node.IsEdition() {
return Edition
}
return Syntax
case ast.DeclPackage:
return Package
case ast.DeclImport:
return Import
case ast.DeclRange:
if node.IsExtensions() {
return Extensions
}
return Reserved
case ast.DeclBody:
return Body
case ast.DeclDef:
switch node.Classify() {
case ast.DefKindMessage:
return Classify(node.AsMessage())
case ast.DefKindEnum:
return Classify(node.AsEnum())
case ast.DefKindService:
return Classify(node.AsService())
case ast.DefKindExtend:
return Classify(node.AsExtend())
case ast.DefKindOption:
return Classify(node.AsOption())
case ast.DefKindField:
return Classify(node.AsField())
case ast.DefKindEnumValue:
return Classify(node.AsEnumValue())
case ast.DefKindMethod:
return Classify(node.AsMethod())
case ast.DefKindOneof:
return Classify(node.AsOneof())
case ast.DefKindGroup:
return Classify(node.AsGroup())
default:
return Def
}
case ast.DefMessage:
return Message
case ast.DefEnum:
return Enum
case ast.DefService:
return Service
case ast.DefExtend:
return Extend
case ast.DefOption:
var first ast.PathComponent
node.Path.Components(func(pc ast.PathComponent) bool {
first = pc
return false
})
if !first.AsExtension().IsZero() {
return CustomOption
}
return Option
case ast.DefField:
return Field
case ast.DefGroup:
return Group
case ast.DefEnumValue:
return EnumValue
case ast.DefMethod:
return Method
case ast.DefOneof:
return Oneof
case ast.ExprAny:
switch node.Kind() {
case ast.ExprKindLiteral:
return Classify(node.AsLiteral())
case ast.ExprKindPrefixed:
return Classify(node.AsPrefixed())
case ast.ExprKindPath:
return Classify(node.AsPath())
case ast.ExprKindRange:
return Classify(node.AsRange())
case ast.ExprKindArray:
return Classify(node.AsArray())
case ast.ExprKindDict:
return Classify(node.AsDict())
case ast.ExprKindField:
return Classify(node.AsField())
default:
return Expr
}
case ast.ExprLiteral:
return Classify(node.Token)
case ast.ExprPrefixed:
// This ensures that e.g. -1 is described as a number rather than as
// an "expression".
return Classify(node.Expr())
case ast.ExprPath:
return Classify(node.Path)
case ast.ExprRange:
return Range
case ast.ExprArray:
return Array
case ast.ExprDict:
return Dict
case ast.ExprField:
return DictField
case ast.TypeAny:
switch node.Kind() {
case ast.TypeKindPath:
return Classify(node.AsPath())
default:
return Type
}
case ast.TypePath:
return TypePath
case ast.TypePrefixed, ast.TypeGeneric:
return Type
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
}
// 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 noun.yaml. DO NOT EDIT.
package taxa
import (
"fmt"
"iter"
)
// Noun is a syntactic or semantic element within the grammar that can be
// referred to within a diagnostic.
//
// All [keyword.Keyword] values can be safely cast to [taxa.Noun].
type Noun int
const (
Unrecognized Noun = keywordCount + iota + 1
TopLevel
EOF
SyntaxMode
EditionMode
Decl
Empty
Syntax
Edition
Package
Import
Extensions
Reserved
Body
Def
Message
Enum
Service
Extend
Oneof
Group
Option
CustomOption
FieldSelector
PseudoOption
Field
Extension
EnumValue
Method
CompactOptions
MethodIns
MethodOuts
Signature
FieldTag
FieldNumber
MessageSetNumber
FieldName
OptionValue
QualifiedName
FullyQualifiedName
ExtensionName
TypeURL
Expr
Range
Array
Dict
DictField
Type
TypePath
TypeParams
TypePrefix
MessageType
MessageSet
EnumType
ScalarType
EntryType
MapKey
MapValue
Whitespace
Comment
Ident
String
Float
Int
Number
ReturnsParens
nounCount int = iota
)
// String implements [fmt.Stringer].
func (v Noun) String() string {
if int(v) < 0 || int(v) > len(_table_Noun_String) {
return fmt.Sprintf("Noun(%v)", int(v))
}
return _table_Noun_String[v]
}
// GoString implements [fmt.GoStringer].
func (v Noun) GoString() string {
if int(v) < 0 || int(v) > len(_table_Noun_GoString) {
return fmt.Sprintf("taxa.Noun(%v)", int(v))
}
return _table_Noun_GoString[v]
}
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",
Edition: "`edition` declaration",
Package: "`package` declaration",
Import: "import",
Extensions: "extension range",
Reserved: "reserved range",
Body: "definition body",
Def: "definition",
Message: "message definition",
Enum: "enum definition",
Service: "service definition",
Extend: "message extension block",
Oneof: "oneof definition",
Group: "group definition",
Option: "option setting",
CustomOption: "custom option setting",
FieldSelector: "field selector",
PseudoOption: "pseudo-option",
Field: "message field",
Extension: "message extension",
EnumValue: "enum value",
Method: "service method",
CompactOptions: "compact options",
MethodIns: "method parameter list",
MethodOuts: "method return type",
Signature: "method signature",
FieldTag: "message field tag",
FieldNumber: "field number",
MessageSetNumber: "message set extension number",
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",
Dict: "message expression",
DictField: "message field value",
Type: "type",
TypePath: "type name",
TypeParams: "type parameters",
TypePrefix: "type modifier",
MessageType: "message type",
MessageSet: "message set type",
EnumType: "enum type",
ScalarType: "scalar type",
EntryType: "map entry type",
MapKey: "map key type",
MapValue: "map value type",
Whitespace: "whitespace",
Comment: "comment",
Ident: "identifier",
String: "string literal",
Float: "floating-point literal",
Int: "integer literal",
Number: "number literal",
ReturnsParens: "`returns (...)`",
}
var _table_Noun_GoString = [...]string{
Unrecognized: "taxa.Unrecognized",
TopLevel: "taxa.TopLevel",
EOF: "taxa.EOF",
SyntaxMode: "taxa.SyntaxMode",
EditionMode: "taxa.EditionMode",
Decl: "taxa.Decl",
Empty: "taxa.Empty",
Syntax: "taxa.Syntax",
Edition: "taxa.Edition",
Package: "taxa.Package",
Import: "taxa.Import",
Extensions: "taxa.Extensions",
Reserved: "taxa.Reserved",
Body: "taxa.Body",
Def: "taxa.Def",
Message: "taxa.Message",
Enum: "taxa.Enum",
Service: "taxa.Service",
Extend: "taxa.Extend",
Oneof: "taxa.Oneof",
Group: "taxa.Group",
Option: "taxa.Option",
CustomOption: "taxa.CustomOption",
FieldSelector: "taxa.FieldSelector",
PseudoOption: "taxa.PseudoOption",
Field: "taxa.Field",
Extension: "taxa.Extension",
EnumValue: "taxa.EnumValue",
Method: "taxa.Method",
CompactOptions: "taxa.CompactOptions",
MethodIns: "taxa.MethodIns",
MethodOuts: "taxa.MethodOuts",
Signature: "taxa.Signature",
FieldTag: "taxa.FieldTag",
FieldNumber: "taxa.FieldNumber",
MessageSetNumber: "taxa.MessageSetNumber",
FieldName: "taxa.FieldName",
OptionValue: "taxa.OptionValue",
QualifiedName: "taxa.QualifiedName",
FullyQualifiedName: "taxa.FullyQualifiedName",
ExtensionName: "taxa.ExtensionName",
TypeURL: "taxa.TypeURL",
Expr: "taxa.Expr",
Range: "taxa.Range",
Array: "taxa.Array",
Dict: "taxa.Dict",
DictField: "taxa.DictField",
Type: "taxa.Type",
TypePath: "taxa.TypePath",
TypeParams: "taxa.TypeParams",
TypePrefix: "taxa.TypePrefix",
MessageType: "taxa.MessageType",
MessageSet: "taxa.MessageSet",
EnumType: "taxa.EnumType",
ScalarType: "taxa.ScalarType",
EntryType: "taxa.EntryType",
MapKey: "taxa.MapKey",
MapValue: "taxa.MapValue",
Whitespace: "taxa.Whitespace",
Comment: "taxa.Comment",
Ident: "taxa.Ident",
String: "taxa.String",
Float: "taxa.Float",
Int: "taxa.Int",
Number: "taxa.Number",
ReturnsParens: "taxa.ReturnsParens",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 taxa
import (
"fmt"
"iter"
"math/bits"
"slices"
"strings"
)
const setWords = (taxaCount + 1 + 63) / 64
// Set is a set of [Noun] values, implicitly ordered by the [Noun] values'
// intrinsic order.
//
// A zero Set is empty and ready to use.
type Set struct {
bits [setWords]uint64
}
// NewSet returns a new [Set] with the given values set.
//
// Panics if any value is not one of the constants in this package.
func NewSet(subjects ...Noun) Set {
return Set{}.With(subjects...)
}
// Len returns the number of values in the set.
func (s Set) Len() int {
var n int
for _, v := range s.bits {
n += bits.OnesCount64(v)
}
return n
}
// Has checks whether w is present in this set.
func (s Set) Has(w Noun) bool {
if w >= Noun(taxaCount) {
return false
}
has := s.bits[int(w)/64] & (uint64(1) << (int(w) % 64))
return has != 0
}
// With returns a new Set with the given values inserted.
//
// Panics if any value is not one of the constants in this package.
func (s Set) With(subjects ...Noun) Set {
for _, v := range subjects {
if v >= Noun(taxaCount) {
panic(fmt.Sprintf("internal/what: inserted invalid value %d", v))
}
s.bits[int(v)/64] |= uint64(1) << (int(v) % 64)
}
return s
}
// Keywords returns the subset of s consisting only of the keywords.
func (s Set) Keywords() Set {
for i, bits := range s.NonKeywords().bits {
s.bits[i] &^= bits
}
return s
}
// NonKeywords returns the subset of s consisting only of the non-keywords.
func (s Set) NonKeywords() Set {
for bits := keywordCount; bits > 0; bits -= 64 {
mask := uint64(1<<bits) - 1
s.bits[(keywordCount-bits)/64] &^= mask
}
return s
}
// String implements [fmt.Stringer].
func (s Set) String() string {
buf := new(strings.Builder)
buf.WriteString("taxa.Set{")
first := true
for bit := range s.setBits() {
if !first {
buf.WriteString(", ")
}
first = false
buf.WriteString(Noun(bit).GoString())
}
buf.WriteString("}")
return buf.String()
}
// String implements [fmt.GoStringer].
func (s Set) GoString() string {
return s.String()
}
// All returns an iterator over the elements in the set.
func (s Set) All() iter.Seq[Noun] {
return func(yield func(Noun) bool) {
// First loop over non-keywords, then the keywords.
for bit := range s.NonKeywords().setBits() {
if !yield(Noun(bit)) {
return
}
}
for bit := range s.Keywords().setBits() {
if !yield(Noun(bit)) {
return
}
}
}
}
func (s Set) setBits() iter.Seq[int] {
return func(yield func(int) bool) {
for i, word := range s.bits {
next := i * 64
for word != 0 {
if word&1 == 1 && !yield(next) {
return
}
word >>= 1
next++
}
}
}
}
// Join returns a comma-delimited string containing the names of the elements of
// this set, using the given conjunction as the final separator, and taking
// care to include an Oxford comma only when necessary.
//
// For example, NewSet(Message, Enum, Service).Join("and") will produce the
// string "message, enum, and service".
//
// If the set is empty, returns the empty string.
func (s Set) Join(conj string) string {
elems := slices.Collect(s.All())
var out strings.Builder
switch len(elems) {
case 0:
case 1:
fmt.Fprintf(&out, "%v", elems[0])
case 2:
fmt.Fprintf(&out, "%v %s %v", elems[0], conj, elems[1])
default:
for _, v := range elems[:len(elems)-1] {
fmt.Fprintf(&out, "%v, ", v)
}
fmt.Fprintf(&out, "%s %v", conj, elems[len(elems)-1])
}
return out.String()
}
// 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 taxa (plural of taxon, an element of a taxonomy) provides support for
// classifying Protobuf syntax productions for use in the parser and in
// diagnostics.
//
// The Subject enum is also used in the parser stack as a simple way to inform
// recursive descent calls of what their caller is, since the What enum
// represents "everything" the parser stack pushes around.
package taxa
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
//go:generate go run github.com/bufbuild/protocompile/internal/enum noun.yaml
const (
keywordCount = 135 // Verified by a unit test.
taxaCount = nounCount + keywordCount
)
const Unknown Noun = 0
// In is a shorthand for the "in" preposition.
func (n Noun) In() Place {
return Place{n, "in"}
}
// After is a shorthand for the "after" preposition.
func (n Noun) After() Place {
return Place{n, "after"}
}
// Without is a shorthand for the "without" preposition.
func (n Noun) Without() Place {
return Place{n, "without"}
}
// On is a shorthand for the "on" preposition.
func (n Noun) On() Place {
return Place{n, "on"}
}
// AsSet returns a singleton set containing this What.
func (n Noun) AsSet() Set {
return NewSet(n)
}
// IsKeyword returns whether this is a wrapped [keyword.Keyword] value.
func (n Noun) IsKeyword() bool {
return n > 0 && n < keywordCount
}
// Place is a location within the grammar that can be referred to within a
// diagnostic.
//
// It corresponds to a prepositional phrase in English, so it is actually
// somewhat more general than a place, and more accurately describes a general
// state of being.
type Place struct {
subject Noun
preposition string
}
// Subject returns this place's subject.
func (p Place) Subject() Noun {
return p.subject
}
// String implements [fmt.Stringer].
func (p Place) String() string {
return p.preposition + " " + p.subject.String()
}
// GoString implements [fmt.GoStringer].
//
// This exists to get pretty output out of the assert package.
func (p Place) GoString() string {
return fmt.Sprintf("{%#v, %#v}", p.subject, p.preposition)
}
func init() {
// Fill out the string tables for Noun with their keyword values.
_table_Noun_String[Unknown] = "<unknown>"
_table_Noun_GoString[Unknown] = "taxa.Unknown"
for kw := range keyword.All() {
if kw == keyword.Unknown {
continue
}
name := kw.String()
if kw == keyword.Newline {
name = "\\n" // Make sure the newline token is escaped.
}
_table_Noun_String[kw] = "`" + name + "`"
_table_Noun_GoString[kw] = kw.GoString()
}
}
// 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 meta defines internal metadata types shared between the token package
// and the lexer.
package tokenmeta
import (
"math/big"
)
// Meta is a type defined in this package.
type Meta interface{ meta() }
type Number struct {
// Inlined storage for small int/float values.
Word uint64
// big.Float can represent any uint64 or float64 (except NaN), and any
// *big.Int, too.
Big *big.Float
// Length of a prefix or suffix on this integer.
// The prefix is the base prefix; the suffix is any identifier
// characters that follow the last digit.
Prefix, Suffix uint32
Exp uint32 // Length of the exponent measured from the e.
IsFloat bool
ThousandsSep bool
Base, ExpBase byte
SyntaxError bool // Whether parsing a concrete value failed.
}
type String struct {
// Post-processed string contents.
Text string
// Lengths of the sigil and quotes for this string
Prefix, Quote uint32
// Whether concatenation took place.
Concatenated bool
// Spans at which escapes occur.
Escapes []Escape
}
type Escape struct {
Start, End uint32
Rune rune
Byte byte
}
func (Number) meta() {}
func (String) meta() {}
// 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 ir
import (
"fmt"
"reflect"
"strings"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/intern"
)
// builtinIDs contains [intern.ID]s for symbols with special meaning in the
// language.
// builtins contains those symbols that are built into the language, and which the compiler cannot
// handle not being present. This field is only present in the Context
// for descriptor.proto.
//
// This is resolved using reflection in [resolveLangSymbols]. The names of the
// fields of this type must match those in builtinIDs that names its symbol.
type builtins struct {
FileOptions Member
MessageOptions Member
FieldOptions Member
OneofOptions Member
RangeOptions Member
EnumOptions Member
EnumValueOptions Member
ServiceOptions Member
MethodOptions Member
JavaUTF8 Member
JavaMultipleFiles Member
OptimizeFor Member
MapEntry Member
Packed Member
OptionTargets Member
CType, JSType Member
Lazy, UnverifiedLazy Member
AllowAlias Member
MessageSet Member
JSONName Member
ExtnDecls Member
ExtnVerification Member
ExtnDeclNumber Member
ExtnDeclName Member
ExtnDeclType Member
ExtnDeclReserved Member
ExtnDeclRepeated Member
FileDeprecated Member
MessageDeprecated Member
FieldDeprecated Member
EnumDeprecated Member
EnumValueDeprecated Member
ServiceDeprecated Member
MethodDeprecated Member
EditionDefaults, EditionDefaultsKey, EditionDefaultsValue Member
EditionSupport Member
EditionSupportIntroduced Member
EditionSupportDeprecated Member
EditionSupportWarning Member
EditionSupportRemoved Member
FeatureSet Type
FeaturePresence Member
FeatureEnumType Member
FeaturePacked Member
FeatureUTF8 Member
FeatureGroup Member
FeatureEnum Member
FeatureJSON Member
FeatureVisibility Member `builtin:"optional"`
FeatureNamingStyle Member `builtin:"optional"`
FileFeatures Member
MessageFeatures Member
FieldFeatures Member
OneofFeatures Member
RangeFeatures Member
EnumFeatures Member
EnumValueFeatures Member
ServiceFeatures Member
MethodFeatures Member
}
// builtinIDs is all of the interning IDs of names in [builtins], plus some
// others. This lives inside of [Session] and is constructed once.
type builtinIDs struct {
DescriptorFile intern.ID `intern:"google/protobuf/descriptor.proto"`
AnyPath intern.ID `intern:"google.protobuf.Any"`
FileOptions intern.ID `intern:"google.protobuf.FileDescriptorProto.options"`
MessageOptions intern.ID `intern:"google.protobuf.DescriptorProto.options"`
FieldOptions intern.ID `intern:"google.protobuf.FieldDescriptorProto.options"`
OneofOptions intern.ID `intern:"google.protobuf.OneofDescriptorProto.options"`
RangeOptions intern.ID `intern:"google.protobuf.DescriptorProto.ExtensionRange.options"`
EnumOptions intern.ID `intern:"google.protobuf.EnumDescriptorProto.options"`
EnumValueOptions intern.ID `intern:"google.protobuf.EnumValueDescriptorProto.options"`
ServiceOptions intern.ID `intern:"google.protobuf.ServiceDescriptorProto.options"`
MethodOptions intern.ID `intern:"google.protobuf.MethodDescriptorProto.options"`
JavaUTF8 intern.ID `intern:"google.protobuf.FileOptions.java_string_check_utf8"`
JavaMultipleFiles intern.ID `intern:"google.protobuf.FileOptions.java_multiple_files"`
OptimizeFor intern.ID `intern:"google.protobuf.FileOptions.optimize_for"`
MapEntry intern.ID `intern:"google.protobuf.MessageOptions.map_entry"`
MessageSet intern.ID `intern:"google.protobuf.MessageOptions.message_set_wire_format"`
Packed intern.ID `intern:"google.protobuf.FieldOptions.packed"`
OptionTargets intern.ID `intern:"google.protobuf.FieldOptions.targets"`
CType intern.ID `intern:"google.protobuf.FieldOptions.ctype"`
JSType intern.ID `intern:"google.protobuf.FieldOptions.jstype"`
Lazy intern.ID `intern:"google.protobuf.FieldOptions.lazy"`
UnverifiedLazy intern.ID `intern:"google.protobuf.FieldOptions.unverified_lazy"`
AllowAlias intern.ID `intern:"google.protobuf.EnumOptions.allow_alias"`
JSONName intern.ID `intern:"google.protobuf.FieldDescriptorProto.json_name"`
ExtnDecls intern.ID `intern:"google.protobuf.ExtensionRangeOptions.declaration"`
ExtnVerification intern.ID `intern:"google.protobuf.ExtensionRangeOptions.verification"`
ExtnDeclNumber intern.ID `intern:"google.protobuf.ExtensionRangeOptions.Declaration.number"`
ExtnDeclName intern.ID `intern:"google.protobuf.ExtensionRangeOptions.Declaration.full_name"`
ExtnDeclType intern.ID `intern:"google.protobuf.ExtensionRangeOptions.Declaration.type"`
ExtnDeclReserved intern.ID `intern:"google.protobuf.ExtensionRangeOptions.Declaration.reserved"`
ExtnDeclRepeated intern.ID `intern:"google.protobuf.ExtensionRangeOptions.Declaration.repeated"`
FileUninterpreted intern.ID `intern:"google.protobuf.FileOptions.uninterpreted_option"`
MessageUninterpreted intern.ID `intern:"google.protobuf.MessageOptions.uninterpreted_option"`
FieldUninterpreted intern.ID `intern:"google.protobuf.FieldOptions.uninterpreted_option"`
OneofUninterpreted intern.ID `intern:"google.protobuf.OneofOptions.uninterpreted_option"`
RangeUninterpreted intern.ID `intern:"google.protobuf.ExtensionRangeOptions.uninterpreted_option"`
EnumUninterpreted intern.ID `intern:"google.protobuf.EnumOptions.uninterpreted_option"`
EnumValueUninterpreted intern.ID `intern:"google.protobuf.EnumValueOptions.uninterpreted_option"`
ServiceUninterpreted intern.ID `intern:"google.protobuf.ServiceOptions.uninterpreted_option"`
MethodUninterpreted intern.ID `intern:"google.protobuf.MethodOptions.uninterpreted_option"`
FileDeprecated intern.ID `intern:"google.protobuf.FileOptions.deprecated"`
MessageDeprecated intern.ID `intern:"google.protobuf.MessageOptions.deprecated"`
FieldDeprecated intern.ID `intern:"google.protobuf.FieldOptions.deprecated"`
EnumDeprecated intern.ID `intern:"google.protobuf.EnumOptions.deprecated"`
EnumValueDeprecated intern.ID `intern:"google.protobuf.EnumValueOptions.deprecated"`
ServiceDeprecated intern.ID `intern:"google.protobuf.ServiceOptions.deprecated"`
MethodDeprecated intern.ID `intern:"google.protobuf.MethodOptions.deprecated"`
EditionDefaults intern.ID `intern:"google.protobuf.FieldOptions.edition_defaults"`
EditionDefaultsKey intern.ID `intern:"google.protobuf.FieldOptions.EditionDefault.edition"`
EditionDefaultsValue intern.ID `intern:"google.protobuf.FieldOptions.EditionDefault.value"`
EditionSupport intern.ID `intern:"google.protobuf.FieldOptions.feature_support"`
EditionSupportIntroduced intern.ID `intern:"google.protobuf.FieldOptions.FeatureSupport.edition_introduced"`
EditionSupportDeprecated intern.ID `intern:"google.protobuf.FieldOptions.FeatureSupport.edition_deprecated"`
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"`
FeatureVisibility intern.ID `intern:"google.protobuf.FeatureSet.default_symbol_visibility"`
FeatureNamingStyle intern.ID `intern:"google.protobuf.FeatureSet.enforce_naming_style"`
FileFeatures intern.ID `intern:"google.protobuf.FileOptions.features"`
MessageFeatures intern.ID `intern:"google.protobuf.MessageOptions.features"`
FieldFeatures intern.ID `intern:"google.protobuf.FieldOptions.features"`
OneofFeatures intern.ID `intern:"google.protobuf.OneofOptions.features"`
RangeFeatures intern.ID `intern:"google.protobuf.ExtensionRangeOptions.features"`
EnumFeatures intern.ID `intern:"google.protobuf.EnumOptions.features"`
EnumValueFeatures intern.ID `intern:"google.protobuf.EnumValueOptions.features"`
ServiceFeatures intern.ID `intern:"google.protobuf.ServiceOptions.features"`
MethodFeatures intern.ID `intern:"google.protobuf.MethodOptions.features"`
}
func resolveBuiltins(file *File) {
if !file.IsDescriptorProto() {
return
}
// If adding a new kind of symbol to resolve, add it to this map.
kinds := map[reflect.Type]struct {
kind SymbolKind
wrap func(arena.Untyped, reflect.Value)
}{
reflect.TypeFor[Member](): {
kind: SymbolKindField,
wrap: makeBuiltinWrapper[Member](file),
},
reflect.TypeFor[Type](): {
kind: SymbolKindMessage,
wrap: makeBuiltinWrapper[Type](file),
},
}
file.dpBuiltins = new(builtins)
v := reflect.ValueOf(file.dpBuiltins).Elem()
ids := reflect.ValueOf(file.session.builtins)
for i := range v.NumField() {
field := v.Field(i)
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",
kind.kind.noun(), file.session.intern.Value(id), sym.Kind(),
))
}
kind.wrap(sym.Raw().data, field)
}
}
// makeBuiltinWrapper helps construct reflection shims for resolveBuiltins.
func makeBuiltinWrapper[T ~id.Node[T, *File, Raw], Raw any](
file *File,
) func(arena.Untyped, reflect.Value) {
return func(p arena.Untyped, out reflect.Value) {
x := id.Wrap(file, id.ID[T](p))
out.Set(reflect.ValueOf(x))
}
}
// 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 ir
import (
"cmp"
"slices"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
)
// FeatureSet represents the Editions-mediated features of a particular
// declaration.
type FeatureSet id.Node[FeatureSet, *File, *rawFeatureSet]
// Feature is a feature setting retrieved from a [FeatureSet].
type Feature struct {
withContext
raw rawFeature
}
// FeatureInfo represents information about a message field being used as a
// feature. This corresponds to the edition_defaults and feature_support options
// on a field.
type FeatureInfo struct {
withContext
raw *rawFeatureInfo
}
type rawFeatureSet struct {
features map[featureKey]rawFeature
parent id.ID[FeatureSet]
options id.ID[Value]
}
type rawFeature struct {
// Can't be a ref because it might not be imported by this file at all.
value Value
isCustom, isInherited, isDefault bool
}
type rawFeatureInfo struct {
defaults []featureDefault // Sorted by edition.
introduced, deprecated, removed syntax.Syntax
deprecationWarning string
}
type featureKey struct {
extension, field *rawMember
}
type featureDefault struct {
edition syntax.Syntax
value id.ID[Value]
}
// Parent returns the feature set of the parent scope for this feature.
//
// Returns zero if this is the feature set for the file.
func (fs FeatureSet) Parent() FeatureSet {
if fs.IsZero() {
return FeatureSet{}
}
return id.Wrap(fs.Context(), fs.Raw().parent)
}
// Options returns the value of the google.protobuf.FeatureSet message that
// this FeatureSet is built from.
func (fs FeatureSet) Options() MessageValue {
if fs.IsZero() {
return MessageValue{}
}
return id.Wrap(fs.Context(), fs.Raw().options).AsMessage()
}
// Lookup looks up a feature with the given google.protobuf.FeatureSet member.
func (fs FeatureSet) Lookup(field Member) Feature {
return fs.LookupCustom(Member{}, field)
}
// LookupCustom looks up a custom feature in the given extension's field.
func (fs FeatureSet) LookupCustom(extension, field Member) Feature {
if fs.IsZero() {
return Feature{}
}
// First, check if this value is cached.
key := featureKey{extension.Raw(), field.Raw()}
if f, ok := fs.Raw().features[key]; ok {
return Feature{id.WrapContext(fs.Context()), f}
}
raw := rawFeature{isCustom: !extension.IsZero()}
// Check to see if it's set in the options message.
options := fs.Options()
if !options.IsZero() && !extension.IsZero() {
// If the extension is not set, this will zero out options, so we'll
// just go to the next one.
options = options.Field(extension).AsMessage()
}
if !options.IsZero() {
raw.value = options.Field(field)
}
if raw.value.IsZero() {
if parent := fs.Parent(); !parent.IsZero() {
// If parent is non-nil, recurse.
raw = fs.Parent().LookupCustom(extension, field).raw
raw.isInherited = true
} else {
// Otherwise, we need to look for the edition default.
raw.value = field.FeatureInfo().Default(fs.Context().Syntax())
raw.isInherited = true
raw.isDefault = true
}
}
if raw.value.IsZero() {
return Feature{}
}
if fs.Raw().features == nil {
fs.Raw().features = make(map[featureKey]rawFeature)
}
fs.Raw().features[key] = raw
return Feature{id.WrapContext(fs.Context()), raw}
}
// Field returns the field corresponding to this feature value.
func (f Feature) Field() Member {
return f.Value().Field()
}
// IsCustom returns whether this is a custom feature.
func (f Feature) IsCustom() bool {
return !f.IsZero() && f.raw.isCustom
}
// IsInherited returns whether this feature value was inherited from its parent.
func (f Feature) IsInherited() bool {
return !f.IsZero() && f.raw.isInherited
}
// IsExplicit returns whether this feature was set explicitly.
func (f Feature) IsExplicit() bool {
return !f.IsZero() && !f.raw.isInherited
}
// IsDefault returns whether this feature was inherited from edition defaults.
// An explicit setting to the default will return false for this method.
func (f Feature) IsDefault() bool {
return !f.IsZero() && f.raw.isDefault
}
// Type returns the type of this feature. May be zero if there is no specified
// default value for this feature in the current edition.
func (f Feature) Type() Type {
return f.Field().Element()
}
// Value returns the value of this feature. May be zero if there is no specified
// value for this feature, given the current edition.
func (f Feature) Value() Value {
return f.raw.value
}
// Default returns the default value for this feature.
func (f FeatureInfo) Default(edition syntax.Syntax) Value {
if f.IsZero() {
return Value{}
}
idx, ok := slices.BinarySearchFunc(f.raw.defaults, edition, func(a featureDefault, b syntax.Syntax) int {
return cmp.Compare(a.edition, b)
})
if !ok && idx > 0 {
idx-- // We're looking for the greatest lower bound.
}
return id.Wrap(f.Context(), f.raw.defaults[idx].value)
}
// Introduced returns which edition this feature is first allowed in.
func (f FeatureInfo) Introduced() syntax.Syntax {
if f.IsZero() {
return syntax.Unknown
}
return f.raw.introduced
}
// IsIntroduced returns whether this feature has been introduced yet.
func (f FeatureInfo) IsIntroduced(in syntax.Syntax) bool {
return f.Introduced() <= in
}
// Deprecated returns whether this feature has been deprecated, and in which
// edition.
func (f FeatureInfo) Deprecated() syntax.Syntax {
if f.IsZero() {
return syntax.Unknown
}
return f.raw.deprecated
}
// IsDeprecated returns whether this feature has been deprecated yet.
func (f FeatureInfo) IsDeprecated(in syntax.Syntax) bool {
return f.Deprecated() != syntax.Unknown && f.Deprecated() <= in
}
// Removed returns whether this feature has been removed, and in which
// edition.
func (f FeatureInfo) Removed() syntax.Syntax {
if f.IsZero() {
return syntax.Unknown
}
return f.raw.removed
}
// IsRemoved returns whether this feature has been removed yet.
func (f FeatureInfo) IsRemoved(in syntax.Syntax) bool {
return f.Removed() != syntax.Unknown && f.Removed() <= in
}
// DeprecationWarning returns the literal text of the deprecation warning for
// this feature, if it has been deprecated.
func (f FeatureInfo) DeprecationWarning() string {
if f.IsZero() {
return ""
}
return f.raw.deprecationWarning
}
// 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 ir
import (
"iter"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
"github.com/bufbuild/protocompile/internal/intern"
"github.com/bufbuild/protocompile/internal/toposort"
)
// File is an IR file, which provides access to the top-level declarations of
// a Protobuf *File.
//
//nolint:govet // For some reason, this lint mangles the field order on this struct. >:(
type File struct {
_ unsafex.NoCopy
session *Session
ast *ast.File
// The path for this file. This need not be what ast.Span() reports, because
// it has been passed through filepath.Clean() and filepath.ToSlash() first,
// to normalize it.
path intern.ID
syntax syntax.Syntax
pkg intern.ID
imports imports
types []id.ID[Type]
topLevelTypesEnd int // Index of the last top-level type in types.
extns []id.ID[Member]
topLevelExtnsEnd int // Index of the last top-level extension in extns.
extends []id.ID[Extend]
topLevelExtendsEnd int // Index of last top-level extension in extends.
options id.ID[Value]
services []id.ID[Service]
features id.ID[FeatureSet]
// Table of all symbols transitively imported by this file. This is all
// local symbols plus the imported tables of all direct imports. Importing
// everything and checking visibility later allows us to diagnose
// missing import errors.
// This file's symbol tables. Each file has two symbol tables: its imported
// symbols and its exported symbols.
//
// The exported symbols are formed from the file's local symbols, and the
// exported symbols of each transitive public import.
//
// The imported symbols are the exported symbols plus the exported symbols
// of each direct import.
exported, imported symtab
dpBuiltins *builtins // Only non-nil for descriptor.proto.
arenas struct {
types arena.Arena[rawType]
members arena.Arena[rawMember]
ranges arena.Arena[rawReservedRange]
extendees arena.Arena[rawExtend]
oneofs arena.Arena[rawOneof]
services arena.Arena[rawService]
methods arena.Arena[rawMethod]
values arena.Arena[rawValue]
messages arena.Arena[rawMessageValue]
arrays arena.Arena[[]rawValueBits]
features arena.Arena[rawFeatureSet]
symbols arena.Arena[rawSymbol]
}
}
type withContext = id.HasContext[*File]
// builtins returns the builtin descriptor.proto names.
func (f *File) builtins() *builtins {
if f.dpBuiltins != nil {
return f.dpBuiltins
}
return f.imports.DescriptorProto().dpBuiltins
}
// AST returns the AST this file was parsed from.
func (f *File) AST() *ast.File {
if f == nil {
return nil
}
return f.ast
}
// Syntax returns the syntax pragma that applies to this file.
func (f *File) Syntax() syntax.Syntax {
if f == nil {
return syntax.Unknown
}
return f.syntax
}
// Path returns the canonical path for this file.
//
// This need not be the same as [File.AST]().Span().Path().
func (f *File) Path() string {
if f == nil {
return ""
}
if f == primitiveCtx {
return "<predeclared>"
}
c := f
return c.session.intern.Value(c.path)
}
// InternedPath returns the intern ID for the value of [File.Path].
func (f *File) InternedPath() intern.ID {
if f == nil {
return 0
}
return f.path
}
// IsDescriptorProto returns whether this is the special file
// google/protobuf/descriptor.proto, which is given special treatment in
// the language.
func (f *File) IsDescriptorProto() bool {
if f == nil {
return false
}
return f.InternedPath() == f.session.builtins.DescriptorFile
}
// Package returns the package name for this file.
//
// The name will not include a leading dot. It will be empty for the empty
// package.
func (f *File) Package() FullName {
if f == nil {
return ""
}
c := f
if f == primitiveCtx {
return ""
}
return FullName(c.session.intern.Value(c.pkg))
}
// InternedPackage returns the intern ID for the value of [File.Package].
func (f *File) InternedPackage() intern.ID {
if f == nil {
return 0
}
return f.pkg
}
// Imports returns an indexer over the imports declared in this file.
func (f *File) Imports() seq.Indexer[Import] {
var imp imports
if f != nil {
imp = f.imports
}
return imp.Directs()
}
// TransitiveImports returns an indexer over the transitive imports for this
// file.
//
// This function does not report whether those imports are weak or not.
func (f *File) TransitiveImports() seq.Indexer[Import] {
var imp imports
if f != nil {
imp = f.imports
}
return imp.Transitive()
}
// ImportFor returns import metadata for a given file, if this file imports it.
func (f *File) ImportFor(that *File) Import {
idx, ok := f.imports.byPath[that.InternedPath()]
if !ok {
return Import{}
}
return f.TransitiveImports().At(int(idx))
}
// Types returns the top level types of this file.
func (f *File) Types() seq.Indexer[Type] {
var types []id.ID[Type]
if f != nil {
types = f.types[:f.topLevelTypesEnd]
}
return seq.NewFixedSlice(
types,
func(_ int, p id.ID[Type]) Type {
return id.Wrap(f, p)
},
)
}
// AllTypes returns all types defined in this file.
func (f *File) AllTypes() seq.Indexer[Type] {
var types []id.ID[Type]
if f != nil {
types = f.types
}
return seq.NewFixedSlice(
types,
func(_ int, p id.ID[Type]) Type {
return id.Wrap(f, p)
},
)
}
// Extensions returns the top level extensions defined in this file (i.e.,
// the contents of any top-level `extends` blocks).
func (f *File) Extensions() seq.Indexer[Member] {
var slice []id.ID[Member]
if f != nil {
slice = f.extns[:f.topLevelExtnsEnd]
}
return seq.NewFixedSlice(
slice,
func(_ int, p id.ID[Member]) Member {
return id.Wrap(f, p)
},
)
}
// AllExtensions returns all extensions defined in this file.
func (f *File) AllExtensions() seq.Indexer[Member] {
var extns []id.ID[Member]
if f != nil {
extns = f.extns
}
return seq.NewFixedSlice(
extns,
func(_ int, p id.ID[Member]) Member {
return id.Wrap(f, p)
},
)
}
// Extends returns the top level extend blocks in this file.
func (f *File) Extends() seq.Indexer[Extend] {
var slice []id.ID[Extend]
if f != nil {
slice = f.extends[:f.topLevelExtendsEnd]
}
return seq.NewFixedSlice(
slice,
func(_ int, p id.ID[Extend]) Extend {
return id.Wrap(f, p)
},
)
}
// AllExtends returns all extend blocks in this file.
func (f *File) AllExtends() seq.Indexer[Extend] {
var extends []id.ID[Extend]
if f != nil {
extends = f.extends
}
return seq.NewFixedSlice(
extends,
func(_ int, p id.ID[Extend]) Extend {
return id.Wrap(f, p)
},
)
}
// AllMembers returns all fields defined in this file, including extensions
// and enum values.
func (f *File) AllMembers() iter.Seq[Member] {
var raw iter.Seq[*rawMember]
if f != nil {
raw = f.arenas.members.Values()
}
i := 0
return iterx.Map(raw, func(raw *rawMember) Member {
i++
return id.WrapRaw(f, id.ID[Member](i), raw)
})
}
// Services returns all services defined in this file.
func (f *File) Services() seq.Indexer[Service] {
var services []id.ID[Service]
if f != nil {
services = f.services
}
return seq.NewFixedSlice(
services,
func(_ int, p id.ID[Service]) Service {
return id.Wrap(f, p)
},
)
}
// Options returns the top level options applied to this file.
func (f *File) Options() MessageValue {
var options id.ID[Value]
if f != nil {
options = f.options
}
return id.Wrap(f, options).AsMessage()
}
// FeatureSet returns the Editions features associated with this file.
func (f *File) FeatureSet() FeatureSet {
if f == nil {
return FeatureSet{}
}
return id.Wrap(f, f.features)
}
// Deprecated returns whether this file is deprecated, by returning the
// relevant option value for setting deprecation.
func (f *File) Deprecated() Value {
if f == nil {
return Value{}
}
builtins := f.builtins()
d := f.Options().Field(builtins.FileDeprecated)
if b, _ := d.AsBool(); b {
return d
}
return Value{}
}
// Symbols returns this file's symbol table.
//
// The symbol table includes both symbols defined in this file, and symbols
// imported by the file. The symbols are returned in an arbitrary but fixed
// order.
func (f *File) Symbols() seq.Indexer[Symbol] {
var symbols []Ref[Symbol]
if f != nil {
symbols = f.imported
}
return seq.NewFixedSlice(
symbols,
func(_ int, r Ref[Symbol]) Symbol {
return GetRef(f, r)
},
)
}
// FindSymbol finds a symbol among [File.Symbols] with the given fully-qualified
// name.
func (f *File) FindSymbol(fqn FullName) Symbol {
return GetRef(f,
f.imported.lookupBytes(f,
unsafex.BytesAlias[[]byte](string(fqn))))
}
// TopoSort sorts a graph of [File]s according to their dependency graph,
// in topological order. Files with no dependencies are yielded first.
func TopoSort(files []*File) iter.Seq[*File] {
// NOTE: This cannot panic because Files, by construction, do not contain
// graph cycles.
return toposort.Sort(
files,
func(f *File) *File { return f },
func(f *File) iter.Seq[*File] {
return seq.Map(
f.imports.Directs(),
func(i Import) *File { return i.File },
)
},
)
}
func (f *File) FromID(id uint64, want any) any {
switch want.(type) {
case **rawType:
return f.arenas.types.Deref(arena.Pointer[rawType](id))
case **rawMember:
return f.arenas.members.Deref(arena.Pointer[rawMember](id))
case **rawReservedRange:
return f.arenas.ranges.Deref(arena.Pointer[rawReservedRange](id))
case **rawExtend:
return f.arenas.extendees.Deref(arena.Pointer[rawExtend](id))
case **rawOneof:
return f.arenas.oneofs.Deref(arena.Pointer[rawOneof](id))
case **rawService:
return f.arenas.services.Deref(arena.Pointer[rawService](id))
case **rawMethod:
return f.arenas.methods.Deref(arena.Pointer[rawMethod](id))
case **rawValue:
return f.arenas.values.Deref(arena.Pointer[rawValue](id))
case **rawMessageValue:
return f.arenas.messages.Deref(arena.Pointer[rawMessageValue](id))
case **rawFeatureSet:
return f.arenas.features.Deref(arena.Pointer[rawFeatureSet](id))
case **rawSymbol:
return f.arenas.symbols.Deref(arena.Pointer[rawSymbol](id))
default:
return f.AST().FromID(id, want)
}
}
// 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 ir
import (
"slices"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/intern"
)
// Import is an import in a [File].
type Import struct {
*File // The file that is imported.
// The kind of import this is.
Public, Weak, Option bool
Direct bool // Whether this is a direct or transitive import.
Visible bool // Whether this import's symbols are visible in the current file.
Used bool // Whether this import has been marked as used.
Decl ast.DeclImport // The import declaration.
}
// imports is a data structure for compactly classifying the transitive imports
// of a Protobuf file.
//
// When building the importable symbol table, we include the symbols from each
// direct import, as well as direct import's transitive public imports, NOT
// the *current* file's transitive public imports. The transitive public
// of a file are those which have a path from the importing file via public
// imports only.
//
// For example, where -> is a normal import and => a public import, a => b => c
// has that c's transitive public imports are {a, b}, but a => b -> d has d with
// no transitive public imports. However, both c and d's importable symbol
// tables will include all symbols from a and b, because b is a direct import,
// and a is a transitive public import of a direct import.
type imports struct {
// All transitively-imported files and their AST definition. This slice is divided
// into the following segments:
//
// 1. Public imports.
// 2. Weak imports.
// 3. Regular imports.
// 4. Transitive public imports.
// 5. Transitive imports.
//
// The fields after this one specify where each of these segments ends.
//
// The last element of this slice is always descriptor.proto, even if it
// exists elsewhere as an ordinary import.
files []imported
// Maps the path of each file to its index in files. This is used for
// mapping from one [Context]'s file IDs to another's.
byPath intern.Map[uint32]
// Map of path of each imported file to a direct import which causes it to
// be imported. This is used for marking which imports are used.
causes intern.Map[uint32]
// NOTE: public imports always come first. This ensures that when
// recursively determining public imports, we consider public imports'
// recursive imports first. Consider the following sequence of files:
//
// // a.proto
// message A {}
//
// // b.proto
// import public "a.proto"
//
// // c.proto
// import "d.proto"
//
// // d.proto
// import public "b.proto"
// import "c.proto"
//
// // e.proto
// import "d.proto"
//
// message B { A foo = 1; }
//
// Because b imports a publicly, we need a to wind up as a transitive
// public import so that when we search the transitive public imports of d
// for symbols, we pick up "a.proto".
//
// There is a test in ir_imports_test.go that validates this behavior. So
// much pain for a little-used feature...
publicEnd, importEnd, transPublicEnd uint32
}
// imported wraps an imported [File] and the import statement declaration [ast.DeclImport].
type imported struct {
file *File
decl ast.DeclImport
weak, option bool
visible, used bool
}
// AddDirect appends a direct import to this imports table.
func (i *imports) AddDirect(imp Import) {
if imp.Public {
i.Insert(imp, int(i.publicEnd), true)
i.publicEnd++
} else {
i.Insert(imp, int(i.importEnd), true)
}
i.importEnd++
i.transPublicEnd++
}
// Recurse updates the import table to incorporate the transitive imports of
// each import.
//
// Must only be called once, after all direct imports are added.
func (i *imports) Recurse(dedup intern.Map[ast.DeclImport]) {
for k, file := range seq.All(i.Directs()) {
for imp := range seq.Values(file.TransitiveImports()) {
if !mapsx.AddZero(dedup, imp.InternedPath()) {
// If imp is public, but file is already present, we need to
// treat this import as non-option, because this overrides it.
if imp.Public {
i.files[k].option = false
}
continue
}
// Transitive imports are public to us if and only if they are
// imported through a public import.
if file.Public && imp.Public {
i.Insert(imp, int(i.transPublicEnd), true)
i.transPublicEnd++
continue
}
// Public imports of direct imports are visible in the current file.
i.Insert(imp, -1, imp.Public)
}
}
// Now, build the path and causes maps.
i.byPath = make(intern.Map[uint32])
i.causes = make(intern.Map[uint32])
for n, imp := range i.files {
i.byPath[imp.file.InternedPath()] = uint32(n)
}
for k, file := range seq.All(i.Directs()) {
// Direct imports take precedence over transitive imports.
i.causes[file.InternedPath()] = uint32(k)
for imp := range seq.Values(file.TransitiveImports()) {
mapsx.Add(i.causes, imp.InternedPath(), uint32(k))
}
}
}
// Insert inserts a new import at the given position.
//
// If pos is < 0, appends at the end.
func (i *imports) Insert(imp Import, pos int, visible bool) {
if pos < 0 {
pos = len(i.files)
}
i.files = slices.Insert(i.files, pos, imported{
file: imp.File,
decl: imp.Decl,
weak: imp.Weak,
option: imp.Option,
visible: visible,
})
}
// MarkUsed records a file as used, which affects the values of [Import].Used.
func (i *imports) MarkUsed(file *File) {
idx, ok := i.causes[file.InternedPath()]
if ok {
i.files[idx].used = true
}
}
// DescriptorProto returns the file for descriptor.proto.
func (i *imports) DescriptorProto() *File {
if i == nil {
return nil
}
imported, _ := slicesx.Last(i.files)
return imported.file
}
// Directs returns an indexer over the Directs imports.
func (i *imports) Directs() seq.Indexer[Import] {
var slice []imported
if i != nil {
slice = i.files[:i.importEnd]
}
return seq.NewFixedSlice(
slice,
func(j int, imported imported) Import {
n := uint32(j)
public := n < i.publicEnd
return Import{
File: imported.file,
Public: public,
Weak: imported.weak,
Option: imported.option,
Direct: true,
Visible: true,
Decl: imported.decl,
// Public imports are implicitly always used.
Used: imported.used || public,
}
},
)
}
// Transitive returns an indexer over the Transitive imports.
//
// This function does not report whether those imports are weak, option, or used.
func (i *imports) Transitive() seq.Indexer[Import] {
var slice []imported
if i != nil {
slice = i.files[:max(0, len(i.files)-1)] // Exclude the implicit descriptor.proto
}
return seq.NewFixedSlice(
slice,
func(j int, imported imported) Import {
n := uint32(j)
return Import{
File: imported.file,
Public: n < i.publicEnd ||
(n >= i.importEnd && n < i.transPublicEnd),
Direct: n < i.importEnd,
Visible: imported.visible,
Decl: imported.decl,
}
},
)
}
// 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 ir
import (
"iter"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/ir/presence"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/intern"
)
//go:generate go run github.com/bufbuild/protocompile/internal/enum option_target.yaml
// Member is a Protobuf message field, enum value, or extension field.
//
// A member has three types associated with it. The English language struggles
// to give these succinct names, so we review them here.
//
// 1. Its _element_, i.e. the type it contains. This is the type that a member
// is declared to be _of_. Not present for enum values.
//
// 2. Its _parent_, i.e., the type it is syntactically defined within.
// Extensions appear syntactically within their parent.
//
// 3. Its _container_, i.e., the type which it is part of for the purposes of
// serialization. Extensions are fields of their container, but are declared
// within their parent.
type Member id.Node[Member, *File, *rawMember]
type rawMember struct {
featureInfo *rawFeatureInfo
elem Ref[Type]
number int32
extendee id.ID[Extend]
fqn intern.ID
name intern.ID
syntheticOneofName intern.ID
def id.ID[ast.DeclDef]
parent id.ID[Type]
features id.ID[FeatureSet]
options id.ID[Value]
oneof int32
optionTargets uint32
jsonName intern.ID
isGroup bool
numberOk bool
}
// IsMessageField returns whether this is a non-extension message field.
func (m Member) IsMessageField() bool {
return !m.IsZero() && !m.Raw().elem.IsZero() && m.Raw().extendee.IsZero()
}
// IsExtension returns whether this is a extension message field.
func (m Member) IsExtension() bool {
return !m.IsZero() && !m.Raw().elem.IsZero() && !m.Raw().extendee.IsZero()
}
// IsEnumValue returns whether this is an enum value.
func (m Member) IsEnumValue() bool {
return !m.IsZero() && m.Raw().elem.IsZero()
}
// IsGroup returns whether this is a group-encoded field.
func (m Member) IsGroup() bool {
return !m.IsZero() && m.Raw().isGroup
}
// IsSynthetic returns whether or not this is a synthetic field, such as the
// fields of a map entry.
func (m Member) IsSynthetic() bool {
return !m.IsZero() && m.AST().IsZero()
}
// IsSingular returns whether this is a singular field; this includes oneof
// members.
func (m Member) IsSingular() bool {
return m.Presence() != presence.Unknown && m.Presence() != presence.Repeated
}
// IsRepeated returns whether this is a repeated field; this includes map
// fields.
func (m Member) IsRepeated() bool {
return m.Presence() == presence.Repeated
}
// IsMap returns whether this is a map field.
func (m Member) IsMap() bool {
return !m.IsZero() && m == m.Element().MapField()
}
// IsPacked returns whether this is a packed message field.
func (m Member) IsPacked() bool {
if !m.IsRepeated() {
return false
}
builtins := m.Context().builtins()
option := m.Options().Field(builtins.Packed)
if packed, ok := option.AsBool(); ok {
return packed
}
feature := m.FeatureSet().Lookup(builtins.FeaturePacked).Value()
value, _ := feature.AsInt()
return value == 1 // google.protobuf.FeatureSet.PACKED
}
// IsUnicode returns whether this is a string-typed message field that must
// contain UTF-8 bytes.
func (m Member) IsUnicode() bool {
if m.Element().Predeclared() != predeclared.String {
return false
}
builtins := m.Context().builtins()
utf8Feature, _ := m.FeatureSet().Lookup(builtins.FeatureUTF8).Value().AsInt()
return utf8Feature == 2 // FeatureSet.VERIFY
}
// AsTagRange wraps this member in a TagRange.
func (m Member) AsTagRange() TagRange {
if m.IsZero() {
return TagRange{}
}
return TagRange{
id.WrapContext(m.Context()),
rawTagRange{
isMember: true,
ptr: arena.Untyped(m.Context().arenas.members.Compress(m.Raw())),
},
}
}
// AST returns the declaration for this member, if known.
func (m Member) AST() ast.DeclDef {
if m.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(m.Context().AST(), m.Raw().def)
}
// TypeAST returns the type AST node for this member, if known.
func (m Member) TypeAST() ast.TypeAny {
decl := m.AST()
if !decl.IsZero() {
if m.IsGroup() {
return ast.TypePath{Path: decl.Name()}.AsAny()
}
return decl.Type()
}
ty := m.Container()
if !ty.MapField().IsZero() {
k, v := ty.AST().Type().RemovePrefixes().AsGeneric().AsMap()
switch m.Number() {
case 1:
return k
case 2:
return v
}
}
return ast.TypeAny{}
}
// Name returns this member's name.
func (m Member) Name() string {
if m.IsZero() {
return ""
}
return m.Context().session.intern.Value(m.Raw().name)
}
// FullName returns this member's fully-qualified name.
func (m Member) FullName() FullName {
if m.IsZero() {
return ""
}
return FullName(m.Context().session.intern.Value(m.Raw().fqn))
}
// JSONName returns this member's JSON name, either the default-generated one
// or the one set via the json_name pseudo-option.
func (m Member) JSONName() string {
if m.IsZero() {
return ""
}
return m.Context().session.intern.Value(m.Raw().jsonName)
}
// Scope returns the scope in which this member is defined.
func (m Member) Scope() FullName {
if m.IsZero() {
return ""
}
return FullName(m.Context().session.intern.Value(m.InternedScope()))
}
// InternedName returns the intern ID for [Member.FullName]().Name().
func (m Member) InternedName() intern.ID {
if m.IsZero() {
return 0
}
return m.Raw().name
}
// InternedFullName returns the intern ID for [Member.FullName].
func (m Member) InternedFullName() intern.ID {
if m.IsZero() {
return 0
}
return m.Raw().fqn
}
// InternedScope returns the intern ID for [Member.Scope].
func (m Member) InternedScope() intern.ID {
if m.IsZero() {
return 0
}
if parent := m.Parent(); !parent.IsZero() {
return parent.InternedFullName()
}
return m.Context().InternedPackage()
}
// InternedJSONName returns the intern ID for [Member.JSONName].
func (m Member) InternedJSONName() intern.ID {
if m.IsZero() {
return 0
}
return m.Raw().jsonName
}
// Number returns the number for this member after expression evaluation.
//
// Defaults to zero if the number is not specified.
func (m Member) Number() int32 {
if m.IsZero() {
return 0
}
return m.Raw().number
}
// Presence returns this member's presence kind.
//
// Returns [presence.Unknown] for enum values.
func (m Member) Presence() presence.Kind {
if m.IsZero() {
return presence.Unknown
}
if m.Raw().oneof >= 0 {
if m.Parent().IsEnum() {
return presence.Unknown
}
return presence.Shared
}
return presence.Kind(-m.Raw().oneof)
}
// Parent returns the type this member is syntactically located in. This is the
// type it is declared *in*, but which it is not necessarily part of.
//
// May be zero for extensions declared at the top level.
func (m Member) Parent() Type {
if m.IsZero() {
return Type{}
}
return id.Wrap(m.Context(), m.Raw().parent)
}
// Element returns the this member's element type. This is the type it is
// declared to be *of*, such as in the phrase "a string field's type is string".
//
// This does not include the member's presence: for example, a repeated int32
// member will report the type as being the int32 primitive, not an int32 array.
//
// This is zero for enum values.
func (m Member) Element() Type {
if m.IsZero() {
return Type{}
}
return GetRef(m.Context(), m.Raw().elem)
}
// Container returns the type which contains this member: this is either
// [Member.Parent], or the extendee if this is an extension. This is the
// type it is declared to be *part of*.
func (m Member) Container() Type {
if m.IsZero() {
return Type{}
}
extends := id.Wrap(m.Context(), m.Raw().extendee)
if extends.IsZero() {
return m.Parent()
}
return extends.Extendee()
}
// Extend returns the extend block this member is declared in, if any.
func (m Member) Extend() Extend {
if m.IsZero() || m.Raw().extendee.IsZero() {
return Extend{}
}
return id.Wrap(m.Context(), m.Raw().extendee)
}
// Oneof returns the oneof that this member is a member of.
//
// Returns the zero value if this member does not have [presence.Shared].
func (m Member) Oneof() Oneof {
if m.Presence() != presence.Shared {
return Oneof{}
}
return m.Parent().Oneofs().At(int(m.Raw().oneof))
}
// Options returns the options applied to this member.
func (m Member) Options() MessageValue {
if m.IsZero() {
return MessageValue{}
}
return id.Wrap(m.Context(), m.Raw().options).AsMessage()
}
// PseudoOptions returns this member's pseudo options.
func (m Member) PseudoOptions() PseudoFields {
return m.Options().pseudoFields()
}
// FeatureSet returns the Editions features associated with this member.
func (m Member) FeatureSet() FeatureSet {
if m.IsZero() {
return FeatureSet{}
}
return id.Wrap(m.Context(), m.Raw().features)
}
// FeatureInfo returns feature definition information relating to this field
// (for when using this field as a feature).
//
// Returns a zero value if this information does not exist.
func (m Member) FeatureInfo() FeatureInfo {
if m.IsZero() || m.Raw().featureInfo == nil {
return FeatureInfo{}
}
return FeatureInfo{
id.WrapContext(m.Context()),
m.Raw().featureInfo,
}
}
// Deprecated returns whether this member is deprecated, by returning the
// relevant option value for setting deprecation.
func (m Member) Deprecated() Value {
if m.IsZero() {
return Value{}
}
builtins := m.Context().builtins()
field := builtins.FieldDeprecated
if m.IsEnumValue() {
field = builtins.EnumValueDeprecated
}
d := m.Options().Field(field)
if b, _ := d.AsBool(); b {
return d
}
return Value{}
}
// SyntheticOneofName returns the name of the corresponding synthetic oneof for this
// member, if there should be one.
//
// For proto3 sources, a oneof is synthesized to track explicit optional presence of a
// field. For details on generating the synthesized name, see the docs for [syntheticNames]
// and/or refer to https://protobuf.com/docs/descriptors#synthetic-oneofs.
func (m Member) SyntheticOneofName() string {
if m.IsZero() {
return ""
}
return m.Context().session.intern.Value(m.Raw().syntheticOneofName)
}
// CanTarget returns whether this message field can be set as an option for the
// given option target type.
//
// This is mediated by the option FieldOptions.targets, which controls whether
// this field can be set (transitively) on the options of a given entity type.
// This is useful for options which re-use the same message type for different
// option types, such as FeatureSet.
func (m Member) CanTarget(target OptionTarget) bool {
if m.IsZero() {
return false
}
return m.Raw().optionTargets == 0 ||
(m.Raw().optionTargets>>uint(target))&1 != 0 // Check if the target-th bit is set.
}
// Targets returns an iterator over the valid option targets for this member.
func (m Member) Targets() iter.Seq[OptionTarget] {
return func(yield func(OptionTarget) bool) {
if m.IsZero() {
return
}
if m.Raw().optionTargets == 0 {
OptionTargets()(yield)
return
}
bits := m.Raw().optionTargets
for t := range OptionTargets() {
if bits == 0 {
return
}
mask := uint32(1) << t
if bits&mask != 0 && !yield(t) {
return
}
bits &^= mask
}
}
}
// noun returns a [taxa.Noun] for diagnostics.
func (m Member) noun() taxa.Noun {
switch {
case m.IsEnumValue():
return taxa.EnumValue
case m.IsExtension():
return taxa.Extension
default:
return taxa.Field
}
}
// toRef returns a ref to this member relative to the given context.
func (m Member) toRef(f *File) Ref[Member] {
return Ref[Member]{id: m.ID()}.ChangeContext(m.Context(), f)
}
// Extend represents an extend block associated with some extension field.
type Extend id.Node[Extend, *File, *rawExtend]
// rawExtend represents an extends block.
//
// Rather than each field carrying a reference to its extends block's AST, we
// have a level of indirection to amortize symbol lookups.
type rawExtend struct {
def id.ID[ast.DeclDef]
ty Ref[Type]
parent id.ID[Type]
members []id.ID[Member]
}
// AST returns the declaration for this extend block, if known.
func (e Extend) AST() ast.DeclDef {
if e.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(e.Context().AST(), e.Raw().def)
}
// Scope returns the scope that symbol lookups in this block should be performed
// against.
func (e Extend) Scope() FullName {
if e.IsZero() {
return ""
}
return FullName(e.Context().session.intern.Value(e.InternedScope()))
}
// InternedScope returns the intern ID for [Extend.Scope].
func (e Extend) InternedScope() intern.ID {
if e.IsZero() {
return 0
}
if ty := e.Parent(); !ty.IsZero() {
return ty.InternedFullName()
}
return e.Context().InternedPackage()
}
// Extendee returns the extendee type of this extend block.
func (e Extend) Extendee() Type {
if e.IsZero() {
return Type{}
}
return GetRef(e.Context(), e.Raw().ty)
}
// Parent returns the type this extend block is declared in.
func (e Extend) Parent() Type {
if e.IsZero() {
return Type{}
}
return id.Wrap(e.Context(), e.Raw().parent)
}
// Extensions returns the extensions declared in this block.
func (e Extend) Extensions() seq.Indexer[Member] {
var members []id.ID[Member]
if !e.IsZero() {
members = e.Raw().members
}
return seq.NewFixedSlice(members, func(_ int, p id.ID[Member]) Member {
return id.Wrap(e.Context(), p)
})
}
// Oneof represents a oneof within a message definition.
type Oneof id.Node[Oneof, *File, *rawOneof]
type rawOneof struct {
def id.ID[ast.DeclDef]
fqn, name intern.ID
index uint32
container id.ID[Type]
members []id.ID[Member]
options id.ID[Value]
features id.ID[FeatureSet]
}
// AST returns the declaration for this oneof, if known.
func (o Oneof) AST() ast.DeclDef {
if o.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(o.Context().AST(), o.Raw().def)
}
// Name returns this oneof's declared name.
func (o Oneof) Name() string {
if o.IsZero() {
return ""
}
return o.Context().session.intern.Value(o.Raw().name)
}
// FullName returns this oneof's fully-qualified name.
func (o Oneof) FullName() FullName {
if o.IsZero() {
return ""
}
return FullName(o.Context().session.intern.Value(o.Raw().fqn))
}
// InternedName returns the intern ID for [Oneof.FullName]().Name().
func (o Oneof) InternedName() intern.ID {
if o.IsZero() {
return 0
}
return o.Raw().name
}
// InternedFullName returns the intern ID for [Oneof.FullName].
func (o Oneof) InternedFullName() intern.ID {
if o.IsZero() {
return 0
}
return o.Raw().fqn
}
// Container returns the message type which contains it.
func (o Oneof) Container() Type {
if o.IsZero() {
return Type{}
}
return id.Wrap(o.Context(), o.Raw().container)
}
// Index returns this oneof's index in its containing message.
func (o Oneof) Index() int {
if o.IsZero() {
return 0
}
return int(o.Raw().index)
}
// Members returns this oneof's member fields.
func (o Oneof) Members() seq.Indexer[Member] {
var members []id.ID[Member]
if !o.IsZero() {
members = o.Raw().members
}
return seq.NewFixedSlice(members, func(_ int, p id.ID[Member]) Member {
return id.Wrap(o.Context(), p)
})
}
// Parent returns the type that this oneof is declared within,.
func (o Oneof) Parent() Type {
if o.IsZero() {
return Type{}
}
// Empty oneofs are not permitted, so this will always succeed.
return o.Members().At(0).Parent()
}
// Options returns the options applied to this oneof.
func (o Oneof) Options() MessageValue {
if o.IsZero() {
return MessageValue{}
}
return id.Wrap(o.Context(), o.Raw().options).AsMessage()
}
// FeatureSet returns the Editions features associated with this oneof.
func (o Oneof) FeatureSet() FeatureSet {
if o.IsZero() {
return FeatureSet{}
}
return id.Wrap(o.Context(), o.Raw().features)
}
// ReservedRange is a range of reserved field or enum numbers,
// either from a reserved or extensions declaration.
type ReservedRange id.Node[ReservedRange, *File, *rawReservedRange]
type rawReservedRange struct {
value id.Dyn[ast.ExprAny, ast.ExprKind]
decl id.ID[ast.DeclRange]
first int32
last int32
options id.ID[Value]
features id.ID[FeatureSet]
forExtensions bool
rangeOk bool
}
// AST returns the expression that this range was evaluated from, if known.
func (r ReservedRange) AST() ast.ExprAny {
if r.IsZero() {
return ast.ExprAny{}
}
return id.WrapDyn(r.Context().AST(), r.Raw().value)
}
// DeclAST returns the declaration this range came from. Multiple ranges may
// have the same declaration.
func (r ReservedRange) DeclAST() ast.DeclRange {
if r.IsZero() {
return ast.DeclRange{}
}
return id.Wrap(r.Context().AST(), r.Raw().decl)
}
// Range returns the start and end of the range.
func (r ReservedRange) Range() (start, end int32) {
if r.IsZero() {
return 0, 0
}
return r.Raw().first, r.Raw().last
}
// ForExtensions returns whether this is an extension range.
func (r ReservedRange) ForExtensions() bool {
return !r.IsZero() && r.Raw().forExtensions
}
// AsTagRange wraps this range in a TagRange.
func (r ReservedRange) AsTagRange() TagRange {
if r.IsZero() {
return TagRange{}
}
return TagRange{
id.WrapContext(r.Context()),
rawTagRange{
isMember: true,
ptr: arena.Untyped(r.ID()),
},
}
}
// Options returns the options applied to this range.
//
// Reserved ranges cannot carry options; only extension ranges do.
func (r ReservedRange) Options() MessageValue {
if r.IsZero() {
return MessageValue{}
}
return id.Wrap(r.Context(), r.Raw().options).AsMessage()
}
// FeatureSet returns the Editions features associated with this file.
func (r ReservedRange) FeatureSet() FeatureSet {
if r.IsZero() {
return FeatureSet{}
}
return id.Wrap(r.Context(), r.Raw().features)
}
// ReservedName is a name for a field or enum value that has been reserved for
// future use.
type ReservedName struct {
withContext
raw *rawReservedName
}
type rawReservedName struct {
ast ast.ExprAny
name intern.ID
decl id.ID[ast.DeclRange]
}
// AST returns the expression that this name was evaluated from, if known.
func (r ReservedName) AST() ast.ExprAny {
if r.IsZero() {
return ast.ExprAny{}
}
return r.raw.ast
}
// DeclAST returns the declaration this name came from. Multiple names may
// have the same declaration.
func (r ReservedName) DeclAST() ast.DeclRange {
if r.IsZero() {
return ast.DeclRange{}
}
return id.Wrap(r.Context().AST(), r.raw.decl)
}
// Name returns the name (i.e., an identifier) that was reserved.
func (r ReservedName) Name() string {
if r.IsZero() {
return ""
}
return r.Context().session.intern.Value(r.raw.name)
}
// InternedName returns the intern ID for [ReservedName.Name].
func (r ReservedName) InternedName() intern.ID {
if r.IsZero() {
return 0
}
return r.raw.name
}
// 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 ir
import (
"iter"
"slices"
"strings"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// FullName is a fully-qualified Protobuf name, which is a dot-separated list of
// identifiers, with an optional dot prefix.
//
// This is a helper type for common operations on such names. This is essentially
// protoreflect.FullName, without depending on protoreflect. Unlike protoreflect,
// we do not provide validation methods.
type FullName string
// Absolute returns whether this is an absolute name, i.e., has a leading dot.
func (n FullName) Absolute() bool {
return n != "" && n[0] == '.'
}
// IsIdent returns whether this name is a single identifier.
func (n FullName) IsIdent() bool {
return !strings.Contains(string(n), ".")
}
// ToAbsolute returns this name with a leading dot.
func (n FullName) ToAbsolute() FullName {
if n.Absolute() {
return n
}
return "." + n
}
// ToRelative returns this name without a leading dot.
func (n FullName) ToRelative() FullName {
return FullName(strings.TrimPrefix(string(n), "."))
}
// First returns the first component of this name.
func (n FullName) First() string {
n = n.ToRelative()
name, _, _ := strings.Cut(string(n), ".")
return name
}
// Components returns an iterator over the components of this name.
//
// If there are adjacent dots, e.g. foo..bar, this will yield an empty string
// within the name.
func (n FullName) Components() iter.Seq[string] {
return func(yield func(string) bool) {
n = n.ToRelative()
for {
name, rest, more := strings.Cut(string(n), ".")
if !yield(name) || !more {
return
}
n = FullName(rest)
}
}
}
// Name returns the last component of this name.
func (n FullName) Name() string {
_, name, _ := stringsx.CutLast(string(n), ".")
return name
}
// Parent returns the name of the parent entity for this name.
//
// If the name only has one component, returns the zero value. In particular,
// the parent of ".foo" is "".
func (n FullName) Parent() FullName {
parent, _, _ := stringsx.CutLast(string(n), ".")
return FullName(parent)
}
// Append returns a name with the given component(s) appended.
//
// If this is an empty name, the resulting name will not be absolute.
func (n FullName) Append(names ...string) FullName {
if len(names) == 0 {
return n
}
return FullName(unsafex.StringAlias(n.appendToBytes(nil, names...)))
}
// appendToBytes is like [FullName.Append], but it appends to the given slice.
func (n FullName) appendToBytes(b []byte, names ...string) []byte {
if len(names) == 0 {
return append(b, n...)
}
m := len(n) + len(names) - 1
if n != "" {
m++
}
for _, name := range names {
m += len(name)
}
b = slices.Grow(b, m)
b = append(b, n...)
for _, name := range names {
if len(b) > 0 {
b = append(b, '.')
}
b = append(b, name...)
}
return b
}
// 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 ir
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/id"
)
// Ref is a reference in a Protobuf file: an [id.ID] along with information for
// retrieving which file that ID is for, relative to the referencing file's
// context.
//
// The context needed for resolving a ref is called its "base context", which
// the user is expected to keep track of.
type Ref[T any] struct {
// The file this ref is defined in. If zero, it refers to the current file.
// If -1, it refers to a predeclared type. Otherwise, it refers to an
// import (with its index offset by 1).
file int32
id id.ID[T]
}
// IsZero returns whether this is the zero ID.
func (r Ref[T]) IsZero() bool {
return r.id == 0
}
// Get vets the value that a reference refers to.
func GetRef[T ~id.Node[T, *File, Raw], Raw any](base *File, r Ref[T]) T {
return id.Wrap(r.Context(base), r.id)
}
// Context returns the context for this reference relative to a base context.
func (r Ref[T]) Context(base *File) *File {
switch r.file {
case 0:
return base
case -1:
return primitiveCtx
default:
return base.imports.files[r.file-1].file
}
}
// ChangeContext changes the implicit context for this ref to be with respect to
// the new one given.
func (r Ref[T]) ChangeContext(base, next *File) Ref[T] {
if base == next {
return r
}
ctx := r.Context(base)
if ctx == primitiveCtx {
r.file = -1
return r
}
// Figure out where file sits in next.
idx, ok := next.imports.byPath[ctx.InternedPath()]
if !ok {
panic(fmt.Sprintf("protocompile/ir: could not change contexts %q -> %q", base.Path(), next.Path()))
}
r.file = int32(idx) + 1
return r
}
// 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 ir
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/intern"
)
// Service is a Protobuf service definition.
type Service id.Node[Service, *File, *rawService]
// Method is a Protobuf service method.
type Method id.Node[Method, *File, *rawMethod]
type rawService struct {
def id.ID[ast.DeclDef]
fqn, name intern.ID
methods []id.ID[Method]
options id.ID[Value]
features id.ID[FeatureSet]
}
type rawMethod struct {
def id.ID[ast.DeclDef]
fqn, name intern.ID
service id.ID[Service]
input, output Ref[Type]
options id.ID[Value]
features id.ID[FeatureSet]
inputStream, outputStream bool
}
// AST returns the declaration for this service, if known.
func (s Service) AST() ast.DeclDef {
if s.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(s.Context().AST(), s.Raw().def)
}
// Name returns this service's declared name, i.e. the last component of its
// full name.
func (s Service) Name() string {
return s.FullName().Name()
}
// FullName returns this service's fully-qualified name.
func (s Service) FullName() FullName {
if s.IsZero() {
return ""
}
return FullName(s.Context().session.intern.Value(s.Raw().fqn))
}
// InternedName returns the intern ID for [Service.FullName]().Name().
func (s Service) InternedName() intern.ID {
if s.IsZero() {
return 0
}
return s.Raw().name
}
// InternedFullName returns the intern ID for [Service.FullName].
func (s Service) InternedFullName() intern.ID {
if s.IsZero() {
return 0
}
return s.Raw().fqn
}
// Options returns the options applied to this service.
func (s Service) Options() MessageValue {
if s.IsZero() {
return MessageValue{}
}
return id.Wrap(s.Context(), s.Raw().options).AsMessage()
}
// FeatureSet returns the Editions features associated with this service.
func (s Service) FeatureSet() FeatureSet {
if s.IsZero() {
return FeatureSet{}
}
return id.Wrap(s.Context(), s.Raw().features)
}
// Deprecated returns whether this service is deprecated, by returning the
// relevant option value for setting deprecation.
func (s Service) Deprecated() Value {
if s.IsZero() {
return Value{}
}
builtins := s.Context().builtins()
d := s.Options().Field(builtins.ServiceDeprecated)
if b, _ := d.AsBool(); b {
return d
}
return Value{}
}
// Methods returns the methods of this service.
func (s Service) Methods() seq.Indexer[Method] {
var methods []id.ID[Method]
if !s.IsZero() {
methods = s.Raw().methods
}
return seq.NewFixedSlice(
methods,
func(_ int, p id.ID[Method]) Method {
return id.Wrap(s.Context(), p)
},
)
}
// AST returns the declaration for this method, if known.
func (m Method) AST() ast.DeclDef {
if m.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(m.Context().AST(), m.Raw().def)
}
// Name returns this method's declared name, i.e. the last component of its
// full name.
func (m Method) Name() string {
return m.FullName().Name()
}
// FullName returns this method's fully-qualified name.
func (m Method) FullName() FullName {
if m.IsZero() {
return ""
}
return FullName(m.Context().session.intern.Value(m.Raw().fqn))
}
// InternedName returns the intern ID for [Method.FullName]().Name().
func (m Method) InternedName() intern.ID {
if m.IsZero() {
return 0
}
return m.Raw().name
}
// InternedFullName returns the intern ID for [Method.FullName].
func (m Method) InternedFullName() intern.ID {
if m.IsZero() {
return 0
}
return m.Raw().fqn
}
// Options returns the options applied to this method.
func (m Method) Options() MessageValue {
if m.IsZero() {
return MessageValue{}
}
return id.Wrap(m.Context(), m.Raw().options).AsMessage()
}
// FeatureSet returns the Editions features associated with this method.
func (m Method) FeatureSet() FeatureSet {
if m.IsZero() {
return FeatureSet{}
}
return id.Wrap(m.Context(), m.Raw().features)
}
// Deprecated returns whether this service is deprecated, by returning the
// relevant option value for setting deprecation.
func (m Method) Deprecated() Value {
if m.IsZero() {
return Value{}
}
builtins := m.Context().builtins()
d := m.Options().Field(builtins.MethodDeprecated)
if b, _ := d.AsBool(); b {
return d
}
return Value{}
}
// Service returns the service this method is part of.
func (m Method) Service() Service {
if m.IsZero() {
return Service{}
}
return id.Wrap(m.Context(), m.Raw().service)
}
// Input returns the input type for this method, and whether it is a streaming
// input.
func (m Method) Input() (ty Type, stream bool) {
if m.IsZero() {
return Type{}, false
}
return GetRef(m.Context(), m.Raw().input), m.Raw().inputStream
}
// Output returns the output type for this method, and whether it is a streaming
// output.
func (m Method) Output() (ty Type, stream bool) {
if m.IsZero() {
return Type{}, false
}
return GetRef(m.Context(), m.Raw().output), m.Raw().outputStream
}
// 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 ir
import (
"cmp"
"iter"
"slices"
"sync"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/intern"
)
//go:generate go run github.com/bufbuild/protocompile/internal/enum symbol_kind.yaml
// Symbol is an entry in a [File]'s symbol table.
//
// [Symbol.Context] returns the context for the file which imported this
// symbol. To map this to the context in which the symbol was defined, use
// [Symbol.InDefFile].
type Symbol id.Node[Symbol, *File, *rawSymbol]
type rawSymbol struct {
kind SymbolKind
fqn intern.ID
data arena.Untyped
}
// FullName returns this symbol's fully-qualified name.
func (s Symbol) FullName() FullName {
if s.IsZero() {
return ""
}
if s.Kind() == SymbolKindScalar {
return s.AsType().FullName()
}
return FullName(s.Context().session.intern.Value(s.Raw().fqn))
}
// InternedFullName returns the intern ID for [Symbol.FullName].
func (s Symbol) InternedFullName() intern.ID {
if s.IsZero() {
return 0
}
return s.Raw().fqn
}
// Kind returns which kind of symbol this is.
func (s Symbol) Kind() SymbolKind {
if s.IsZero() {
return SymbolKindInvalid
}
return s.Raw().kind
}
// AsType returns the type this symbol refers to, if it is one.
func (s Symbol) AsType() Type {
if !s.Kind().IsType() {
return Type{}
}
return id.Wrap(s.Context(), id.ID[Type](s.Raw().data))
}
// AsMember returns the member this symbol refers to, if it is one.
func (s Symbol) AsMember() Member {
if !s.Kind().IsMember() {
return Member{}
}
return id.Wrap(s.Context(), id.ID[Member](s.Raw().data))
}
// AsOneof returns the oneof this symbol refers to, if it is one.
func (s Symbol) AsOneof() Oneof {
if s.Kind() != SymbolKindOneof {
return Oneof{}
}
return id.Wrap(s.Context(), id.ID[Oneof](s.Raw().data))
}
// AsService returns the service this symbol refers to, if it is one.
func (s Symbol) AsService() Service {
if s.Kind() != SymbolKindService {
return Service{}
}
return id.Wrap(s.Context(), id.ID[Service](s.Raw().data))
}
// AsMethod returns the method this symbol refers to, if it is one.
func (s Symbol) AsMethod() Method {
if s.Kind() != SymbolKindMethod {
return Method{}
}
return id.Wrap(s.Context(), id.ID[Method](s.Raw().data))
}
// FeatureSet returns the features associated with this symbol.
func (s Symbol) FeatureSet() FeatureSet {
switch s.Kind() {
case SymbolKindMessage, SymbolKindEnum:
return s.AsType().FeatureSet()
case SymbolKindField, SymbolKindEnumValue, SymbolKindExtension:
return s.AsMember().FeatureSet()
case SymbolKindOneof:
return s.AsOneof().FeatureSet()
case SymbolKindService:
return s.AsService().FeatureSet()
case SymbolKindMethod:
return s.AsMethod().FeatureSet()
default:
return FeatureSet{}
}
}
// Deprecated returns whether this symbol is deprecated, but returning the
// relevant option value for setting deprecation.
//
// Note that although files can be marked as deprecated, packages cannot,
// so package symbols never show up as deprecated.
func (s Symbol) Deprecated() Value {
switch s.Kind() {
case SymbolKindMessage, SymbolKindEnum:
return s.AsType().Deprecated()
case SymbolKindField, SymbolKindExtension, SymbolKindEnumValue:
return s.AsType().Deprecated()
case SymbolKindService:
return s.AsService().Deprecated()
case SymbolKindMethod:
return s.AsMethod().Deprecated()
default:
return Value{}
}
}
// Visible returns whether or not this symbol is visible according to Protobuf's
// import semantics, within the given file.
//
// If allowOptions is true, symbols that were pulled in via import option are
// accepted.
func (s Symbol) Visible(in *File, allowOptions bool) bool {
if s.Context() == in || s.Context() == primitiveCtx || s.Kind() == SymbolKindPackage {
// Packages don't get visibility checks.
return true
}
idx, imported := in.imports.byPath[s.Context().InternedPath()]
if !imported {
return false
}
imp := in.imports.files[idx]
if !imp.visible || !(allowOptions || !imp.option) {
return false
}
if ty := s.AsType(); !ty.IsZero() {
exported, _ := ty.IsExported()
return exported
}
return imp.visible
}
// Definition returns a span for the definition site of this symbol;
// specifically, this is (typically) just an identifier.
func (s Symbol) Definition() source.Span {
switch s.Kind() {
case SymbolKindPackage:
return s.Context().AST().Package().Span()
case SymbolKindMessage, SymbolKindEnum:
ty := s.AsType()
if mf := ty.MapField(); !mf.IsZero() {
return mf.TypeAST().Span()
}
return ty.AST().Name().Span()
case SymbolKindField, SymbolKindEnumValue, SymbolKindExtension:
return s.AsMember().AST().Name().Span()
case SymbolKindOneof:
return s.AsOneof().AST().Name().Span()
case SymbolKindService:
return s.AsService().AST().Name().Span()
case SymbolKindMethod:
return s.AsMethod().AST().Name().Span()
}
return source.Span{}
}
// Import returns the import declaration that brought this symbol into scope
// in the given file.
//
// Returns zero if s is defined in the current file or if s is not imported
// by in.
func (s Symbol) Import(in *File) Import {
if s.Context() == in || s.Context() == primitiveCtx {
return Import{}
}
idx, imported := in.imports.byPath[s.Context().InternedPath()]
if !imported {
return Import{}
}
return in.imports.Transitive().At(int(idx))
}
// noun returns a [taxa.Noun] for diagnostic use.
func (s Symbol) noun() taxa.Noun {
return s.Kind().noun()
}
// noun returns a [taxa.Noun] for diagnostic use.
func (k SymbolKind) noun() taxa.Noun {
return symbolNouns[k]
}
var symbolNouns = [...]taxa.Noun{
SymbolKindPackage: taxa.Package,
SymbolKindScalar: taxa.ScalarType,
SymbolKindMessage: taxa.MessageType,
SymbolKindEnum: taxa.EnumType,
SymbolKindField: taxa.Field,
SymbolKindEnumValue: taxa.EnumValue,
SymbolKindExtension: taxa.Extension,
SymbolKindOneof: taxa.Oneof,
SymbolKindService: taxa.Service,
SymbolKindMethod: taxa.Method,
}
// IsType returns whether this is a type's symbol kind.
func (k SymbolKind) IsType() bool {
switch k {
case SymbolKindMessage, SymbolKindEnum, SymbolKindScalar:
return true
default:
return false
}
}
// IsMember returns whether this is a field's symbol kind. This includes
// enum values, which the ir package treats as fields of enum types.
func (k SymbolKind) IsMember() bool {
switch k {
case SymbolKindField, SymbolKindExtension, SymbolKindEnumValue:
return true
default:
return false
}
}
// IsMessageField returns whether this is a field's symbol kind.
func (k SymbolKind) IsMessageField() bool {
switch k {
case SymbolKindField, SymbolKindExtension:
return true
default:
return false
}
}
// IsScope returns whether this is a symbol that defines a scope, for the
// purposes of name lookup.
func (k SymbolKind) IsScope() bool {
switch k {
case SymbolKindPackage, SymbolKindMessage:
return true
default:
return false
}
}
// OptionTarget returns the OptionTarget type for a symbol of this kind.
//
// Returns [OptionTargetInvalid] if there is no corresponding target for this
// type of symbol.
func (k SymbolKind) OptionTarget() OptionTarget {
return optionTargets[k]
}
var optionTargets = [...]OptionTarget{
SymbolKindMessage: OptionTargetMessage,
SymbolKindEnum: OptionTargetEnum,
SymbolKindField: OptionTargetField,
SymbolKindEnumValue: OptionTargetEnumValue,
SymbolKindExtension: OptionTargetField,
SymbolKindOneof: OptionTargetOneof,
SymbolKindService: OptionTargetService,
SymbolKindMethod: OptionTargetMethod,
}
// symtab is a symbol table: a mapping of the fully qualified names of symbols
// to the entities they refer to.
//
// The elements of a symtab are sorted by the [intern.ID] of their FQN, allowing
// for O(n) merging of symbol tables.
type symtab []Ref[Symbol]
var resolveScratch = sync.Pool{
New: func() any { return new([]byte) },
}
// symtabMerge merges the given symbol tables in the given context.
func symtabMerge(file *File, tables iter.Seq[symtab], fileForTable func(int) *File) symtab {
return slicesx.MergeKeySeq(
tables,
func(which int, elem Ref[Symbol]) intern.ID {
f := fileForTable(which)
return GetRef(f, elem).InternedFullName()
},
func(which int, elem Ref[Symbol]) Ref[Symbol] {
// We need top map the file number from src to the current one.
src := fileForTable(which)
if src != file {
theirs := GetRef(src, elem)
ours := file.imports.byPath[theirs.Context().InternedPath()]
elem.file = int32(ours + 1)
}
return elem
},
)
}
// sort sorts this symbol table according to the value of each intern
// ID.
func (s symtab) sort(file *File) {
slices.SortFunc(s, func(a, b Ref[Symbol]) int {
symA := GetRef(file, a)
symB := GetRef(file, b)
return cmp.Compare(symA.InternedFullName(), symB.InternedFullName())
})
}
// lookupBytes looks up a symbol with the given fully-qualified name.
func (s symtab) lookup(file *File, fqn intern.ID) Ref[Symbol] {
idx, ok := slicesx.BinarySearchKey(s, fqn, func(r Ref[Symbol]) intern.ID {
return GetRef(file, r).InternedFullName()
})
if !ok {
return Ref[Symbol]{}
}
return s[idx]
}
// lookupBytes looks up a symbol with the given fully-qualified name.
func (s symtab) lookupBytes(file *File, fqn []byte) Ref[Symbol] {
id, ok := file.session.intern.QueryBytes(fqn)
if !ok {
return Ref[Symbol]{}
}
idx, ok := slicesx.BinarySearchKey(s, id, func(r Ref[Symbol]) intern.ID {
return GetRef(file, r).InternedFullName()
})
if !ok {
return Ref[Symbol]{}
}
return s[idx]
}
// resolve attempts to resolve the relative path name within the given scope
// (which should itself be a possibly-empty relative path).
//
// Returns zero if the symbol is not found. If the symbol is not found due to
// Protobuf's weird double-lookup semantics around nested identifiers, this
// function will try to find the name as if this language bug did not exist, and
// will report the name it had expected to find.
//
// If skipIfNot is nil, the symbol's kind will not be checked to determine if
// we should continue climbing scopes.
//
// If candidates is not nil, debugging remarks will be appended to the
// diagnostic.
func (s symtab) resolve(
file *File,
scope, name FullName,
skipIfNot func(SymbolKind) bool,
remarks *report.Diagnostic,
) (found Ref[Symbol], expected FullName) {
// This function implements the name resolution algorithm specified at
// https://protobuf.com/docs/language-spec#reference-resolution.
// Symbol resolution is not quite as simple as trying p + name for all
// ancestors of scope. Consider the following files:
//
// // a.proto
// package foo.bar;
// message M {}
//
// // b.proto
// package foo;
// import "a.proto";
// message M {}
//
// // c.proto
// package foo.bar.baz;
// import "b.proto";
// message N {
// M m = 1;
// }
//
// The candidates, in order, are:
// - foo.bar.baz.M; does not exist.
// - foo.bar.M; not visible.
// - foo.M; correct answer.
// - M; not tried.
//
// If we do not keep going after encountering symbols that are not visible
// to us, we will reject valid code.
// A similar situation happens here:
//
// package foo;
// message M {
// message N {}
// message P {
// enum X { N = 1; }
// N n = 1;
// }
// }
//
// If we look up N, the candidates are foo.M.P.N, foo.M.N, foo.N, and N.
// We will find foo.M.P.N, which is not a message or enum type, so we must
// skip it to find the correct name, foo.M.N. This is what the accept
// predicate is for.
// Finally, consider the following situation, which involves partial
// names.
//
// package foo;
// message M {
// message N {}
// message M {
// M.N n = 1;
// }
// }
//
// The candidates are foo.M.M.N, foo.M.N, M.N. However, protoc rejects this,
// because it actually searches for M first, and then appends the rest of
// the path and searches for that, in two phases.
//
// It is not clear why protoc does this, but it does mean we need to be
// careful in how we resolve partial names.
scopeSearch := !name.IsIdent()
first := name.First()
// This needs to be a mutable byte slice, because in the loop below, we
// delete intermediate chunks of it, e.g. a.b.c.d -> a.b.d -> a.d -> d.
//
// To avoid the cost of allocating a tiny slice every time we come through
// here, we us a sync.Pool. This also means we don't have to constantly
// zero memory that we're going to immediately overwrite.
buf := resolveScratch.Get().(*[]byte) //nolint:errcheck
candidate := (*buf)[:0]
defer func() {
// Re-using the buf pointer here allows us to avoid needing to
// re-allocate a *[]byte to stick back into the pool.
*buf = candidate
resolveScratch.Put(buf)
}()
candidate = scope.appendToBytes(candidate, first)
// Adapt skipIfNot to account for scopeSearch and to be ok to call if nil.
accept := func(kind SymbolKind) bool {
if scopeSearch {
return kind.IsScope()
}
return skipIfNot == nil || skipIfNot(kind)
}
again:
for {
r := s.lookupBytes(file, candidate)
remarks.Apply(report.Debugf("candidate: `%s`", candidate))
if !r.IsZero() {
found = r
sym := GetRef(file, r)
if sym.Visible(file, true) && accept(sym.Kind()) {
// If the symbol is not visible, keep looking; we may find
// another match that is actually visible.
break
}
}
if scope == "" {
// Out of places to look. This is probably a fail.
break
}
oldLen := len(scope)
scope = scope.Parent()
if scope == "" {
oldLen++
}
// Delete in-place to avoid spamming allocations for each candidate.
candidate = slices.Delete(candidate, len(scope), oldLen)
}
if scopeSearch {
// Now search for the full name inside of the scope we found.
candidate = append(candidate, name[len(first):]...)
found = s.lookupBytes(file, candidate)
if found.IsZero() {
// Try again, this time using the full candidate name. This happens
// expressly for the purpose of diagnostics.
scopeSearch = false
// Need to clear the found scope, since otherwise we might get a weird
// error message where we say that we found the parent scope.
found = Ref[Symbol]{}
expected = FullName(candidate)
goto again
}
}
foundFile := found.Context(file)
if foundFile != file {
file.imports.MarkUsed(foundFile)
}
return found, expected
}
// 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 ir
import (
"fmt"
"iter"
"math"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/intern"
"github.com/bufbuild/protocompile/internal/interval"
)
// TagRange is a range of tag numbers in a [Type].
//
// This can represent either a [Member] or a [ReservedRange].
type TagRange struct {
withContext
raw rawTagRange
}
// AsMember returns the [Member] this range points to, or zero if it isn't a
// member.
func (r TagRange) AsMember() Member {
if r.IsZero() || !r.raw.isMember {
return Member{}
}
return id.Wrap(r.Context(), id.ID[Member](r.raw.ptr))
}
// AsReserved returns the [ReservedRange] this range points to, or zero if it
// isn't a member.
func (r TagRange) AsReserved() ReservedRange {
if r.IsZero() || r.raw.isMember {
return ReservedRange{}
}
return id.Wrap(r.Context(), id.ID[ReservedRange](r.raw.ptr))
}
// Type is a Protobuf message field type.
type Type id.Node[Type, *File, *rawType]
type rawType struct {
nested []id.ID[Type]
members []id.ID[Member]
memberByName func() intern.Map[id.ID[Member]]
ranges []id.ID[ReservedRange]
rangesByNumber interval.Intersect[int32, rawTagRange]
reservedNames []rawReservedName
oneofs []id.ID[Oneof]
extends []id.ID[Extend]
def id.ID[ast.DeclDef]
options id.ID[Value]
fqn, name intern.ID // 0 for predeclared types.
parent id.ID[Type]
extnsStart uint32
rangesExtnStart uint32
mapEntryOf id.ID[Member]
features id.ID[FeatureSet]
isEnum, isMessageSet bool
allowsAlias bool
missingRanges bool // See lower_numbers.go.
visibility token.ID
}
type rawTagRange struct {
isMember bool
ptr arena.Untyped
}
// primitiveCtx represents a special file that defines all of the primitive
// types.
var primitiveCtx = func() *File {
ctx := new(File)
nextPtr := 1
for n := range predeclared.All() {
if n == predeclared.Unknown || !n.IsScalar() {
// Skip allocating a pointer for the very first value. This ensures
// that the arena.Pointer value of the Type corresponding to a
// predeclared name corresponds to is the same as the name's integer
// value.
continue
}
for nextPtr != int(n) {
_ = ctx.arenas.types.NewCompressed(rawType{})
_ = ctx.arenas.symbols.NewCompressed(rawSymbol{})
nextPtr++
}
ptr := ctx.arenas.types.NewCompressed(rawType{})
ctx.arenas.symbols.NewCompressed(rawSymbol{
kind: SymbolKindScalar,
data: ptr.Untyped(),
})
nextPtr++
if int(ptr) != int(n) {
panic(fmt.Sprintf("IR initialization error: %d != %d; this is a bug in protocompile", ptr, n))
}
ctx.types = append(ctx.types, id.ID[Type](ptr))
}
return ctx
}()
// PredeclaredType returns the type corresponding to a predeclared name.
//
// Returns the zero value if !n.IsScalar().
func PredeclaredType(n predeclared.Name) Type {
if !n.IsScalar() {
return Type{}
}
return id.Wrap(primitiveCtx, id.ID[Type](n))
}
// AST returns the declaration for this type, if known.
//
// This need not be an [ast.DefMessage] or [ast.DefEnum]; it may be something
// else in the case of e.g. a map field's entry type.
func (t Type) AST() ast.DeclDef {
if t.IsZero() {
return ast.DeclDef{}
}
return id.Wrap(t.Context().AST(), t.Raw().def)
}
// IsPredeclared returns whether this is a predeclared type.
func (t Type) IsPredeclared() bool {
return t.Context() == primitiveCtx
}
// IsMessage returns whether this is a message type.
func (t Type) IsMessage() bool {
return !t.IsZero() && !t.IsPredeclared() && !t.Raw().isEnum
}
// IsMessageSet returns whether this is a message type using the message set
// encoding.
func (t Type) IsMessageSet() bool {
return !t.IsZero() && t.Raw().isMessageSet
}
// IsMapEntry returns whether this is a map type's entry.
func (t Type) IsMapEntry() bool {
return !t.MapField().IsZero()
}
// IsEnum returns whether this is an enum type.
func (t Type) IsEnum() bool {
// All of the predeclared types have isEnum set to false, so we don't
// need to check for them here.
return !t.IsZero() && t.Raw().isEnum
}
func (t Type) IsClosedEnum() bool {
if !t.IsEnum() {
return false
}
builtins := t.Context().builtins()
n, _ := t.FeatureSet().Lookup(builtins.FeatureEnum).Value().AsInt()
return n == 2 // FeatureSet.CLOSED
}
// IsPackable returns whether this type can be the element of a packed repeated
// field.
func (t Type) IsPackable() bool {
return t.IsEnum() || t.Predeclared().IsPackable()
}
// AllowsAlias returns whether this is an enum type with the allow_alias
// option set.
func (t Type) AllowsAlias() bool {
return !t.IsZero() && t.Raw().allowsAlias
}
// IsAny returns whether this is the type google.protobuf.Any, which gets special
// treatment in the language.
func (t Type) IsAny() bool {
return !t.IsZero() && t.InternedFullName() == t.Context().session.builtins.AnyPath
}
// IsExported returns whether this type is exported for the purposes of
// visibility in other files.
//
// Returns whether this was set explicitly via the export or local keywords.
func (t Type) IsExported() (exported, explicit bool) {
if t.IsZero() {
return false, false
}
// This is explicitly set via keyword.
if !t.Raw().visibility.IsZero() {
tok := id.Wrap(t.AST().Context().Stream(), t.Raw().visibility)
return tok.Keyword() == keyword.Export, true
}
// Look up the feature.
if key := t.Context().builtins().FeatureVisibility; !key.IsZero() {
feature := t.FeatureSet().Lookup(key)
switch v, _ := feature.Value().AsInt(); v {
case 0, 1: // DEFAULT_SYMBOL_VISIBILITY_UNKNOWN, EXPORT_ALL
return true, false
case 2: // EXPORT_TOP_LEVEL
return t.Parent().IsZero(), false
case 3, 4: // LOCAL_ALL, STRICT
return false, false
}
}
// If descriptor.proto is too old to have this feature, assume this
// type is exported.
return true, false
}
// Predeclared returns the predeclared type that this Type corresponds to, if any.
//
// Returns either [predeclared.Unknown] or a value such that [predeclared.Name.IsScalar]
// returns true. For example, this will *not* return [predeclared.Map] for map
// fields.
func (t Type) Predeclared() predeclared.Name {
if !t.IsPredeclared() {
return predeclared.Unknown
}
return predeclared.Name(
// NOTE: The code that allocates all the primitive types in the
// primitive context ensures that the pointer value equals the
// predeclared.Name value.
t.Context().arenas.types.Compress(t.Raw()),
)
}
// Name returns this type's declared name, i.e. the last component of its
// full name.
func (t Type) Name() string {
return t.FullName().Name()
}
// FullName returns this type's fully-qualified name.
//
// If t is zero, returns "". Otherwise, the returned name will be absolute
// unless this is a primitive type.
func (t Type) FullName() FullName {
if t.IsZero() {
return ""
}
if p := t.Predeclared(); p != predeclared.Unknown {
return FullName(p.String())
}
return FullName(t.Context().session.intern.Value(t.Raw().fqn))
}
// Scope returns the scope in which this type is defined.
func (t Type) Scope() FullName {
if t.IsZero() {
return ""
}
return FullName(t.Context().session.intern.Value(t.InternedScope()))
}
// InternedName returns the intern ID for [Type.FullName]().Name()
//
// Predeclared types do not have an interned name.
func (t Type) InternedName() intern.ID {
if t.IsZero() {
return 0
}
return t.Raw().name
}
// InternedName returns the intern ID for [Type.FullName]
//
// Predeclared types do not have an interned name.
func (t Type) InternedFullName() intern.ID {
if t.IsZero() {
return 0
}
return t.Raw().fqn
}
// InternedScope returns the intern ID for [Type.Scope]
//
// Predeclared types do not have an interned name.
func (t Type) InternedScope() intern.ID {
if t.IsZero() {
return 0
}
if parent := t.Parent(); !parent.IsZero() {
return parent.InternedFullName()
}
return t.Context().InternedPackage()
}
// Parent returns the type that this type is declared inside of, if it isn't
// at the top level.
func (t Type) Parent() Type {
if t.IsZero() {
return Type{}
}
return id.Wrap(t.Context(), t.Raw().parent)
}
// Nested returns those types which are nested within this one.
//
// Only message types have nested types.
func (t Type) Nested() seq.Indexer[Type] {
var slice []id.ID[Type]
if !t.IsZero() {
slice = t.Raw().nested
}
return seq.NewFixedSlice(
slice,
func(_ int, p id.ID[Type]) Type {
return id.Wrap(t.Context(), p)
},
)
}
// MapField returns the map field that generated this type, if any.
func (t Type) MapField() Member {
if t.IsZero() {
return Member{}
}
return id.Wrap(t.Context(), t.Raw().mapEntryOf)
}
// EntryFields returns the key and value fields for this map entry type.
func (t Type) EntryFields() (key, value Member) {
if !t.IsMapEntry() {
return Member{}, Member{}
}
return id.Wrap(t.Context(), t.Raw().members[0]), id.Wrap(t.Context(), t.Raw().members[1])
}
// Members returns the members of this type.
//
// Predeclared types have no members; message and enum types do.
func (t Type) Members() seq.Indexer[Member] {
var slice []id.ID[Member]
if !t.IsZero() {
slice = t.Raw().members[:t.Raw().extnsStart]
}
return seq.NewFixedSlice(
slice,
func(_ int, p id.ID[Member]) Member {
return id.Wrap(t.Context(), p)
},
)
}
// MemberByName looks up a member with the given name.
//
// Returns a zero member if there is no such member.
func (t Type) MemberByName(name string) Member {
if t.IsZero() {
return Member{}
}
id, ok := t.Context().session.intern.Query(name)
if !ok {
return Member{}
}
return t.MemberByInternedName(id)
}
// MemberByInternedName is like [Type.MemberByName], but takes an interned string.
func (t Type) MemberByInternedName(name intern.ID) Member {
if t.IsZero() {
return Member{}
}
return id.Wrap(t.Context(), t.Raw().memberByName()[name])
}
// Ranges returns an iterator over [TagRange]s that contain number.
func (t Type) Ranges(number int32) iter.Seq[TagRange] {
return func(yield func(TagRange) bool) {
if t.IsZero() {
return
}
entry := t.Raw().rangesByNumber.Get(number)
for _, raw := range entry.Value {
if !yield(TagRange{id.WrapContext(t.Context()), raw}) {
return
}
}
}
}
// MemberByNumber looks up a member with the given number.
//
// Returns a zero member if there is no such member.
func (t Type) MemberByNumber(number int32) Member {
if t.IsZero() {
return Member{}
}
_, member := iterx.Find(t.Ranges(number), func(r TagRange) bool {
return !r.AsMember().IsZero()
})
return member.AsMember()
}
// membersByNameFunc creates the MemberByName map. This is used to keep
// construction of this map lazy.
func (t Type) makeMembersByName() intern.Map[id.ID[Member]] {
table := make(intern.Map[id.ID[Member]], t.Members().Len())
for _, p := range t.Raw().members[:t.Raw().extnsStart] {
field := id.Wrap(t.Context(), p)
table[field.InternedName()] = p
}
return table
}
// Extensions returns any extensions nested within this type.
func (t Type) Extensions() seq.Indexer[Member] {
var slice []id.ID[Member]
if !t.IsZero() {
slice = t.Raw().members[t.Raw().extnsStart:]
}
return seq.NewFixedSlice(
slice,
func(_ int, p id.ID[Member]) Member {
return id.Wrap(t.Context(), p)
},
)
}
// AllRanges returns all reserved/extension ranges declared in this type.
//
// This does not include reserved field names; see [Type.ReservedNames].
func (t Type) AllRanges() seq.Indexer[ReservedRange] {
var slice []id.ID[ReservedRange]
if !t.IsZero() {
slice = t.Raw().ranges
}
return seq.NewFixedSlice(slice, func(_ int, p id.ID[ReservedRange]) ReservedRange {
return id.Wrap(t.Context(), p)
})
}
// ReservedRanges returns the reserved ranges declared in this type.
//
// This does not include reserved field names; see [Type.ReservedNames].
func (t Type) ReservedRanges() seq.Indexer[ReservedRange] {
var slice []id.ID[ReservedRange]
if !t.IsZero() {
slice = t.Raw().ranges[:t.Raw().rangesExtnStart]
}
return seq.NewFixedSlice(slice, func(_ int, p id.ID[ReservedRange]) ReservedRange {
return id.Wrap(t.Context(), p)
})
}
// ExtensionRanges returns the extension ranges declared in this type.
func (t Type) ExtensionRanges() seq.Indexer[ReservedRange] {
var slice []id.ID[ReservedRange]
if !t.IsZero() {
slice = t.Raw().ranges[t.Raw().rangesExtnStart:]
}
return seq.NewFixedSlice(slice, func(_ int, p id.ID[ReservedRange]) ReservedRange {
return id.Wrap(t.Context(), p)
})
}
// ReservedNames returns the reserved named declared in this type.
func (t Type) ReservedNames() seq.Indexer[ReservedName] {
var slice []rawReservedName
if !t.IsZero() {
slice = t.Raw().reservedNames
}
return seq.NewFixedSlice(
slice,
func(i int, _ rawReservedName) ReservedName {
return ReservedName{id.WrapContext(t.Context()), &t.Raw().reservedNames[i]}
},
)
}
// AbsoluteRange returns the smallest and largest number a member of this type
// can have.
//
// This range is inclusive.
func (t Type) AbsoluteRange() (start, end int32) {
if t.IsZero() {
return 0, 0
}
switch {
case t.IsEnum():
return math.MinInt32, math.MaxInt32
case t.IsMessageSet():
return 1, messageSetNumberMax
default:
return 1, fieldNumberMax
}
}
// OccupiedRanges returns ranges of member numbers currently in use in this
// type. The pairs of numbers are inclusive ranges.
func (t Type) OccupiedRanges() iter.Seq2[[2]int32, seq.Indexer[TagRange]] {
return func(yield func([2]int32, seq.Indexer[TagRange]) bool) {
if t.IsZero() {
return
}
for e := range t.Raw().rangesByNumber.Contiguous(true) {
ranges := seq.NewFixedSlice(e.Value, func(_ int, v rawTagRange) TagRange {
return TagRange{id.WrapContext(t.Context()), v}
})
if !yield([2]int32{e.Start, e.End}, ranges) {
return
}
}
}
}
// Oneofs returns the options applied to this type.
func (t Type) Oneofs() seq.Indexer[Oneof] {
var oneofs []id.ID[Oneof]
if !t.IsZero() {
oneofs = t.Raw().oneofs
}
return seq.NewFixedSlice(
oneofs,
func(_ int, p id.ID[Oneof]) Oneof {
return id.Wrap(t.Context(), p)
},
)
}
// Extends returns the options applied to this type.
func (t Type) Extends() seq.Indexer[Extend] {
var extends []id.ID[Extend]
if !t.IsZero() {
extends = t.Raw().extends
}
return seq.NewFixedSlice(
extends,
func(_ int, p id.ID[Extend]) Extend {
return id.Wrap(t.Context(), p)
},
)
}
// Options returns the options applied to this type.
func (t Type) Options() MessageValue {
if t.IsZero() {
return MessageValue{}
}
return id.Wrap(t.Context(), t.Raw().options).AsMessage()
}
// FeatureSet returns the Editions features associated with this type.
func (t Type) FeatureSet() FeatureSet {
if t.IsZero() {
return FeatureSet{}
}
return id.Wrap(t.Context(), t.Raw().features)
}
// Deprecated returns whether this type is deprecated, by returning the
// relevant option value for setting deprecation.
func (t Type) Deprecated() Value {
if t.IsZero() || t.IsPredeclared() {
return Value{}
}
builtins := t.Context().builtins()
field := builtins.MessageDeprecated
if t.IsEnum() {
field = builtins.EnumDeprecated
}
d := t.Options().Field(field)
if b, _ := d.AsBool(); b {
return d
}
return Value{}
}
// noun returns a [taxa.Noun] for diagnostics.
func (t Type) noun() taxa.Noun {
switch {
case t.IsPredeclared():
return taxa.ScalarType
case t.IsEnum():
return taxa.EnumType
case t.IsMapEntry():
return taxa.EntryType
default:
return taxa.MessageType
}
}
// toRef returns a ref to this type relative to the given context.
func (t Type) toRef(file *File) Ref[Type] {
return Ref[Type]{id: t.ID()}.ChangeContext(t.Context(), file)
}
// 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 ir
import (
"cmp"
"fmt"
"iter"
"math"
"slices"
"google.golang.org/protobuf/encoding/protowire"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/intern"
)
// Value is an evaluated expression, corresponding to an option in a Protobuf
// file.
type Value id.Node[Value, *File, *rawValue]
// rawValue is a [rawValueBits] with field information attached to it.
type rawValue struct {
// Expressions that contributes to this value.
//
// The representation of this field is quite complicated, to deal with
// potentially complicated source ASTs. The worst case is as follows.
// Consider:
//
// option foo = { a: [1, 2, 3], a: [4, 5] }; // (*)
//
// Here, two ast.FieldExprs contribute to the value of a, but there are
// five subexpressions for the elements of a. We would like to be able to
// report both those FieldExprs, *and* report an expression for each value
// therein.
//
// However, there is another potentially subtle case we *do not* have to
// deal with (for a singular message field a):
//
// option foo = { a { b: 1 }, a { c: 2 } };
//
// This is an error, because foo.a has already been set when we process
// the second value. If a is repeated, each of these produces a separate
// element.
//
// Because case (*) is rare, we adopt a compression strategy here. exprs
// refers to all contributing expressions for the value. If any array
// expressions occurred, elemIndices will be non-nil, and will be a prefix
// sum of the number of values that each expr in exprs contributes. This is
// binary-searched by Element.AST to find the AST nodes of each element.
//
// Specifically, elemIndices[i] will be the number of elements that every
// expression, up to an including exprs[i], contributes. This layout is
// chosen because it significantly simplifies construction and searching of
// this slice.
exprs []id.Dyn[ast.ExprAny, ast.ExprKind]
elemIndices []uint32
// The AST nodes for the path of the option (compact or otherwise) that
// specify this value. This is intended for diagnostics.
//
// For example, the node
//
// option a.b.c = 9;
//
// results in a field a: {b: {c: 9}}, which is four rawValues deep.
// Each of these will have the same optionPath, for a.b.c.
//
// There will be one such value for each contributing expression, to deal
// with the repeated field case
//
// option f = 1; option f = 2;
optionPaths []ast.PathID
// The field that this value sets. This is where type information comes
// from.
//
// NOTE: we steal the high bit of the pointer to indicate whether or not
// bits refers to a slice. If the pointer part is negative, bits is a
// repeated field with multiple elements.
field Ref[Member]
bits rawValueBits
// The message which contains this value.
container id.ID[MessageValue]
}
// rawValueBits is used to represent the actual value for all types, according to
// the following encoding:
// 1. All numeric types, including bool and enums. This holds the bits.
// 2. String and bytes. This holds an intern.ID.
// 3. Messages. This holds an id.ID[Message].
// 4. Repeated fields with two or more entries. This holds an
// arena.Pointer[[]rawValueBits], where each value is interpreted as a
// non-array with this value's type.
// This exploits the fact that arrays cannot contain other arrays.
// Note that within the IR, map fields do not exist, and are represented as
// the repeated message fields that they will ultimately become.
type rawValueBits uint64
// OptionSpan returns a representative span for the option that set this value.
//
// The Spanner will be an [ast.ExprField], if it is set in an [ast.ExprDict].
func (v Value) OptionSpan() source.Spanner {
if v.IsZero() || len(v.Raw().exprs) == 0 {
return nil
}
c := v.Context().AST()
expr := id.WrapDyn(c, v.Raw().exprs[0])
if field := expr.AsField(); !field.IsZero() {
return field
}
return source.Join(ast.ExprPath{Path: v.Raw().optionPaths[0].In(c)}, expr)
}
// OptionSpans returns an indexer over spans for the option that set this value.
//
// The Spanner will be an [ast.ExprField], if it is set in an [ast.ExprDict].
func (v Value) OptionSpans() seq.Indexer[source.Spanner] {
var slice []id.Dyn[ast.ExprAny, ast.ExprKind]
if !v.IsZero() {
slice = v.Raw().exprs
}
return seq.NewFixedSlice(slice, func(i int, p id.Dyn[ast.ExprAny, ast.ExprKind]) source.Spanner {
c := v.Context().AST()
expr := id.WrapDyn(c, p)
if field := expr.AsField(); !field.IsZero() {
return field
}
return source.Join(ast.ExprPath{Path: v.Raw().optionPaths[i].In(c)}, expr)
})
}
// ValueAST returns a representative expression that evaluated to this value.
//
// For complicated options (such as repeated fields), there may be more than
// one contributing expression; this will just return *one* of them.
func (v Value) ValueAST() ast.ExprAny {
if v.IsZero() || len(v.Raw().exprs) == 0 {
return ast.ExprAny{}
}
c := v.Context().AST()
expr := id.WrapDyn(c, v.Raw().exprs[0])
if field := expr.AsField(); !field.IsZero() {
return field.Value()
}
return expr
}
// ValueASTs returns all expressions that contributed to evaluating this value.
//
// There may be more than one such expression, for repeated fields set more
// than once.
func (v Value) ValueASTs() seq.Indexer[ast.ExprAny] {
var slice []id.Dyn[ast.ExprAny, ast.ExprKind]
if !v.IsZero() {
slice = v.Raw().exprs
}
return seq.NewFixedSlice(slice, func(_ int, p id.Dyn[ast.ExprAny, ast.ExprKind]) ast.ExprAny {
c := v.Context().AST()
expr := id.WrapDyn(c, p)
if field := expr.AsField(); !field.IsZero() {
return field.Value()
}
return expr
})
}
// KeyAST returns a representative AST node for the message key that evaluated
// from this value.
func (v Value) KeyAST() ast.ExprAny {
if v.IsZero() || len(v.Raw().exprs) == 0 {
return ast.ExprAny{}
}
c := v.Context().AST()
expr := id.WrapDyn(c, v.Raw().exprs[0])
if field := expr.AsField(); !field.IsZero() {
return field.Key()
}
return ast.ExprPath{Path: v.Raw().optionPaths[0].In(c)}.AsAny()
}
// KeyASTs returns the AST nodes for each key associated with a value in
// [Value.ValueASTs].
//
// This will either be the key value from an [ast.FieldExpr] (which need not be
// an [ast.PathExpr], in the case of an extension) or the [ast.PathExpr]
// associated with the left-hand-side of an option setting.
func (v Value) KeyASTs() seq.Indexer[ast.ExprAny] {
var slice []id.Dyn[ast.ExprAny, ast.ExprKind]
if !v.IsZero() {
slice = v.Raw().exprs
}
return seq.NewFixedSlice(slice, func(n int, p id.Dyn[ast.ExprAny, ast.ExprKind]) ast.ExprAny {
c := v.Context().AST()
expr := id.WrapDyn(c, p)
if field := expr.AsField(); !field.IsZero() {
return field.Key()
}
return ast.ExprPath{Path: v.Raw().optionPaths[n].In(c)}.AsAny()
})
}
// OptionPaths returns the AST nodes for option paths that set this node.
//
// There will be one path per value returned from [Value.ValueASTs]. Generally,
// you'll want to use [Value.KeyASTs] instead.
func (v Value) OptionPaths() seq.Indexer[ast.Path] {
var slice []ast.PathID
if !v.IsZero() {
slice = v.Raw().optionPaths
}
return seq.NewFixedSlice(slice, func(_ int, e ast.PathID) ast.Path {
c := v.Context().AST()
return e.In(c)
})
}
// Field returns the field this value sets, which includes the value's type
// information.
//
// NOTE: [Member.Element] returns google.protobuf.Any, the concrete type of the
// values in [Value.Elements] may be distinct from it.
func (v Value) Field() Member {
if v.IsZero() {
return Member{}
}
field := v.Raw().field
if int32(field.id) < 0 {
field.id ^= -1
}
return GetRef(v.Context(), field)
}
// Container returns the message value which contains this value, assuming it
// is not a top-level value.
//
// This function is analogous to [Member.Container], which returns the type
// that contains a member; in particular, for extensions, it returns an
// extendee.
func (v Value) Container() MessageValue {
if v.IsZero() {
return MessageValue{}
}
return id.Wrap(v.Context(), v.Raw().container)
}
// Elements returns an indexer over the elements within this value.
//
// If the value is not an array, it contains the singular element within;
// otherwise, it returns the elements of the array.
//
// The indexer will be nonempty except for the zero Value. That is to say, unset
// fields of [MessageValue]s are not represented as a distinct "empty" Value.
func (v Value) Elements() seq.Indexer[Element] {
return seq.NewFixedSlice(v.getElements(), func(n int, bits rawValueBits) Element {
return Element{
withContext: id.WrapContext(v.Context()),
index: n,
value: v,
bits: bits,
}
})
}
// Outlined to promote inlining of Elements().
func (v Value) getElements() []rawValueBits {
var slice []rawValueBits
switch {
case v.IsZero():
break
case int32(v.Raw().field.id) < 0:
slice = *v.slice()
default:
slice = slicesx.One(&v.Raw().bits)
}
return slice
}
// IsZeroValue is a shortcut for [Element.IsZeroValue].
func (v Value) IsZeroValue() bool {
if v.IsZero() {
return false
}
return v.Elements().At(0).IsZeroValue()
}
// AsBool is a shortcut for [Element.AsBool], if this value is singular.
func (v Value) AsBool() (value, ok bool) {
if v.IsZero() || v.Field().IsRepeated() {
return false, false
}
return v.Elements().At(0).AsBool()
}
// AsUInt is a shortcut for [Element.AsUInt], if this value is singular.
func (v Value) AsUInt() (uint64, bool) {
if v.IsZero() || v.Field().IsRepeated() {
return 0, false
}
return v.Elements().At(0).AsUInt()
}
// AsInt is a shortcut for [Element.AsInt], if this value is singular.
func (v Value) AsInt() (int64, bool) {
if v.IsZero() || v.Field().IsRepeated() {
return 0, false
}
return v.Elements().At(0).AsInt()
}
// AsEnum is a shortcut for [Element.AsEnum], if this value is singular.
func (v Value) AsEnum() Member {
if v.IsZero() || v.Field().IsRepeated() {
return Member{}
}
return v.Elements().At(0).AsEnum()
}
// AsFloat is a shortcut for [Element.AsFloat], if this value is singular.
func (v Value) AsFloat() (float64, bool) {
if v.IsZero() || v.Field().IsRepeated() {
return 0, false
}
return v.Elements().At(0).AsFloat()
}
// AsString is a shortcut for [Element.AsString], if this value is singular.
func (v Value) AsString() (string, bool) {
if v.IsZero() || v.Field().IsRepeated() {
return "", false
}
return v.Elements().At(0).AsString()
}
// AsMessage is a shortcut for [Element.AsMessage], if this value is singular.
func (v Value) AsMessage() MessageValue {
if v.IsZero() {
return MessageValue{}
}
m := v.Elements().At(0).AsMessage()
if m.TypeURL() != "" {
// If this is the concrete version of an Any message, it is effectively
// singular: even if the reported field is a repeated g.p.Any, we treat
// Any as having a singular "concrete" field that contains the actual
// value (see [MessageValue.Concrete]).
return m
}
if v.Field().IsRepeated() {
return MessageValue{}
}
return m
}
// slice returns the underlying slice for this value.
//
// If this value isn't already in slice form, this puts it into it.
func (v Value) slice() *[]rawValueBits {
if int32(v.Raw().field.id) < 0 {
return v.Context().arenas.arrays.Deref(arena.Pointer[[]rawValueBits](v.Raw().bits))
}
slice := v.Context().arenas.arrays.New([]rawValueBits{v.Raw().bits})
v.Raw().bits = rawValueBits(v.Context().arenas.arrays.Compress(slice))
v.Raw().field.id ^= -1
return slice
}
// Marshal converts this value into a wire format record and appends it to buf.
//
// If r is not nil, it will be used to record diagnostics generated during the
// marshal operation.
func (v Value) Marshal(buf []byte, r *report.Report) []byte {
var ranges [][2]int
buf, _ = v.marshal(buf, r, &ranges)
return deleteRanges(buf, ranges)
}
// marshal is the recursive part of [Value.Marshal].
//
// See marshalFramed for the meanings of ranges and the int return value.
func (v Value) marshal(buf []byte, r *report.Report, ranges *[][2]int) ([]byte, int) {
if r != nil {
defer r.AnnotateICE(report.Snippetf(v.ValueAST(), "while marshalling this value"))
}
scalar := v.Field().Element().Predeclared()
if v.Field().IsRepeated() && v.Elements().Len() > 1 {
// Packed fields.
switch {
case scalar.IsVarint(), v.Field().Element().IsEnum():
var bytes int
for v := range seq.Values(v.Elements()) {
bits := uint64(v.bits)
if scalar.IsZigZag() {
bits = protowire.EncodeZigZag(int64(bits))
}
bytes += protowire.SizeVarint(bits)
}
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.BytesType)
buf = protowire.AppendVarint(buf, uint64(bytes))
for v := range seq.Values(v.Elements()) {
bits := uint64(v.bits)
if scalar.IsZigZag() {
bits = protowire.EncodeZigZag(int64(bits))
}
buf = protowire.AppendVarint(buf, bits)
}
return buf, 0
case scalar.IsFixed():
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.BytesType)
buf = protowire.AppendVarint(buf, uint64(v.Elements().Len()*scalar.Bits()/8))
for v := range seq.Values(v.Elements()) {
bits := uint64(v.bits)
switch {
case scalar == predeclared.Float32:
f64, _ := v.AsFloat()
f32 := math.Float32bits(float32(f64))
buf = protowire.AppendFixed32(buf, f32)
case scalar.Bits() == 32:
buf = protowire.AppendFixed32(buf, uint32(bits))
default:
buf = protowire.AppendFixed64(buf, bits)
}
}
return buf, 0
}
}
var n int
for v := range seq.Values(v.Elements()) {
switch {
case scalar.IsVarint(), v.Field().Element().IsEnum():
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.VarintType)
bits := uint64(v.bits)
if scalar.IsZigZag() {
bits = protowire.EncodeZigZag(int64(bits))
}
buf = protowire.AppendVarint(buf, bits)
case scalar == predeclared.Float32:
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.Fixed32Type)
f64, _ := v.AsFloat()
f32 := math.Float32bits(float32(f64))
buf = protowire.AppendFixed32(buf, f32)
case scalar.IsFixed() && scalar.Bits() == 32:
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.Fixed32Type)
buf = protowire.AppendFixed32(buf, uint32(v.bits))
case scalar.IsFixed():
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.Fixed64Type)
buf = protowire.AppendFixed64(buf, uint64(v.bits))
case scalar.IsString():
s, _ := v.AsString()
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.BytesType)
buf = protowire.AppendVarint(buf, uint64(len(s)))
buf = append(buf, s...)
default: // Message type.
m := v.AsMessage()
var k int
var group bool // TODO: v.Field().IsGroup()
if group {
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.StartGroupType)
buf, k = m.marshal(buf, r, ranges)
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.EndGroupType)
} else {
buf = protowire.AppendTag(buf, protowire.Number(v.Field().Number()), protowire.BytesType)
buf, k = marshalFramed(buf, r, ranges, func(buf []byte) ([]byte, int) {
return m.marshal(buf, r, ranges)
})
}
n += k
}
}
return buf, n
}
func (v Value) suggestEdit(path, expr string, format string, args ...any) report.DiagnosticOption {
key := v.KeyAST()
value := v.ValueASTs().At(0)
joined := source.Join(key, value)
return report.SuggestEdits(
joined,
fmt.Sprintf(format, args...),
report.Edit{
Start: 0, End: key.Span().Len(),
Replace: path,
},
report.Edit{
Start: value.Span().Start - joined.Start,
End: value.Span().End - joined.Start,
Replace: expr,
},
)
}
// Element is an element within a [Value].
//
// This exists because array values contain multiple non-array elements; this
// type provides uniform access to such elements. See [Value.Elements].
type Element struct {
withContext
index int
value Value
bits rawValueBits
}
// AST returns the expression this value was evaluated from.
func (e Element) AST() ast.ExprAny {
if e.IsZero() || e.value.Raw().exprs == nil {
return ast.ExprAny{}
}
idx := e.ValueNodeIndex()
c := e.Context().AST()
expr := id.WrapDyn(c, e.value.Raw().exprs[idx])
if field := expr.AsField(); !field.IsZero() {
expr = field.Value()
}
if array := expr.AsArray(); !array.IsZero() && e.value.Raw().elemIndices != nil {
// We need to index into the array expression. The index is going to be
// offset by the number of expressions before this one, which we
// can get via elemIndices.
n := int(e.value.Raw().elemIndices[idx]) - e.index - 1
expr = array.Elements().At(n)
}
return expr
}
// ValueNodeIndex returns the index into [Value.ValueASTs] for this element's
// contributing expression. This can be used to obtain other ASTs related to
// this element, e.g.
//
// key := e.Value().MessageKeys().At(e.ValueNodeIndex())
//
// If the element is empty, this returns -1.
func (e Element) ValueNodeIndex() int {
if e.IsZero() {
return -1
}
// We do O(log n) work here, because this function doesn't get called except
// for diagnostics.
idx := e.index
if e.value.Raw().elemIndices != nil {
// Figure out which expression contributes the value for e. We're looking
// for the least upper bound.
//
// For example, if we have expressions [1, 2], [3, 4, 5], elemIndices
// will be [2, 5], and we have that BinarySearch returns
//
// 0 -> 0, false
// 1 -> 0, false
// 2 -> 0, true
// 3 -> 1, false
// 4 -> 1, false
var exact bool
idx, exact = slices.BinarySearch(e.value.Raw().elemIndices, uint32(e.index))
if exact {
idx++
}
}
return idx
}
// Value is the [Value] this element came from.
func (e Element) Value() Value {
return e.value
}
// Field returns the field this value sets, which includes the value's type
// information.
func (e Element) Field() Member {
return e.Value().Field()
}
// Type returns the type of this element.
//
// Note that this may be distinct from [Member.Element]. In the case that this is
// a google.protobuf.Any-typed field, this function will return the concrete
// type if known, rather than Any.
func (e Element) Type() Type {
if msg := e.AsMessage(); !msg.IsZero() {
// This will always be the concrete type, except in the case of
// something naughty like my_any: { type_url: "...", value: "..." };
// in that case this will be Any.
return msg.Type()
}
return e.Field().Element()
}
// IsZeroValue returns whether this element contains the zero value for its type.
//
// Always returns false for repeated or message-typed fields.
func (e Element) IsZeroValue() bool {
if e.IsZero() || e.Field().IsRepeated() || e.Field().Element().IsMessage() {
return false
}
return e.value.Raw().bits == 0
}
// AsBool returns the bool value of this element.
//
// Returns ok == false if this is not a bool.
func (e Element) AsBool() (value, ok bool) {
if e.Type().Predeclared() != predeclared.Bool {
return false, false
}
return e.bits != 0, true
}
// AsUInt returns the value of this element as an unsigned integer.
//
// Returns false if this is not an unsigned integer.
func (e Element) AsUInt() (uint64, bool) {
if !e.Type().Predeclared().IsUnsigned() {
return 0, false
}
return uint64(e.bits), true
}
// AsInt returns the value of this element as a signed integer.
//
// Returns false if this is not a signed integer (enums are included as signed
// integers).
func (e Element) AsInt() (int64, bool) {
if !e.Type().Predeclared().IsSigned() && !e.Type().IsEnum() {
return 0, false
}
return int64(e.bits), true
}
// AsEnum returns the value of this element as a known enum value.
//
// Returns zero if this is not an enum or if the enum value is out of range.
func (e Element) AsEnum() Member {
ty := e.Type()
if !ty.IsEnum() {
return Member{}
}
return ty.MemberByNumber(int32(e.bits))
}
// AsFloat returns the value of this element as a floating-point number.
//
// Returns false if this is not a float.
func (e Element) AsFloat() (float64, bool) {
if !e.Type().Predeclared().IsFloat() {
return 0, false
}
return math.Float64frombits(uint64(e.bits)), true
}
// AsString returns the value of this element as a string.
//
// Returns false if this is not a string.
func (e Element) AsString() (string, bool) {
if !e.Type().Predeclared().IsString() {
return "", false
}
return e.Context().session.intern.Value(intern.ID(e.bits)), true
}
// AsMessage returns the value of this element as a message literal.
//
// Returns the zero value if this is not a message.
func (e Element) AsMessage() MessageValue {
// Avoid infinite recursion: Type() calls AsMessage().
if !e.Field().Element().IsMessage() {
return MessageValue{}
}
return id.Wrap(e.Context(), id.ID[MessageValue](e.bits))
}
// MessageValue is a message literal, represented as a list of ordered
// key-value pairs.
type MessageValue id.Node[MessageValue, *File, *rawMessageValue]
type rawMessageValue struct {
byName intern.Map[uint32]
entries []id.ID[Value]
ty Ref[Type]
self id.ID[Value]
url intern.ID
concrete id.ID[MessageValue]
pseudo struct {
defaultValue id.ID[Value]
jsonName id.ID[Value]
}
}
// PseudoFields contains pseudo options, which are special option-like syntax
// for fields which are not real options. They can be accessed via
// [Message.PseudoFields].
type PseudoFields struct {
Default Value
JSONName Value
}
// AsValue returns the [Value] corresponding to this message.
//
// This value can be used to retrieve the associated [Member] and from it the
// message's declared [Type].
func (v MessageValue) AsValue() Value {
if v.IsZero() {
return Value{}
}
return id.Wrap(v.Context(), v.Raw().self)
}
// Type returns this value's message type.
//
// If v was returned from [MessageValue.Concrete], its type need not be the
// same as v.AsValue()'s (although it can be, in the case of pathological
// Any-within-an-Any messages).
func (v MessageValue) Type() Type {
if v.IsZero() {
return Type{}
}
return GetRef(v.Context(), v.Raw().ty)
}
// TypeURL returns this value's type URL, if it is the concrete value of an
// Any.
func (v MessageValue) TypeURL() string {
if v.IsZero() {
return ""
}
return v.Context().session.intern.Value(v.Raw().url)
}
// Concrete returns the concrete version of this value if it is an Any.
//
// If it isn't an Any, or a .Raw()" Any (one not specified with the special type
// URL syntax), this returns v.
func (v MessageValue) Concrete() MessageValue {
if v.IsZero() || v.Raw().concrete.IsZero() {
return v
}
return id.Wrap(v.Context(), v.Raw().concrete)
}
// Field returns the field corresponding with the given member, if it is set.
func (v MessageValue) Field(field Member) Value {
if field.Container() != v.Type() {
return Value{}
}
name := field.InternedFullName()
if o := field.Oneof(); !o.IsZero() {
name = o.InternedFullName()
}
idx, ok := v.Raw().byName[name]
if !ok {
return Value{}
}
return id.Wrap(v.Context(), v.Raw().entries[idx])
}
// Fields yields the fields within this message literal, in insertion order.
func (v MessageValue) Fields() iter.Seq[Value] {
return func(yield func(Value) bool) {
if v.IsZero() {
return
}
for _, p := range v.Raw().entries {
v := id.Wrap(v.Context(), p)
if !v.IsZero() && !yield(v) {
return
}
}
}
}
// pseudoFields returns pseudofields set on this message.
//
// This feature is used for tracking special options that do not correspond to
// real fields in an options message. They are not part of the message value
// and are not returned by Fields().
func (v MessageValue) pseudoFields() PseudoFields {
if v.IsZero() {
return PseudoFields{}
}
return PseudoFields{
Default: id.Wrap(v.Context(), v.Raw().pseudo.defaultValue),
JSONName: id.Wrap(v.Context(), v.Raw().pseudo.jsonName),
}
}
// Marshal serializes this message as wire format and appends it to buf.
//
// If r is not nil, it will be used to record diagnostics generated during the
// marshal operation.
func (v MessageValue) Marshal(buf []byte, r *report.Report) []byte {
var ranges [][2]int
buf, _ = v.marshal(buf, r, &ranges)
return deleteRanges(buf, ranges)
}
// marshal is the recursive part of [MessageValue.Marshal].
//
// See marshalFramed for the meanings of ranges and the int return value.
func (v MessageValue) marshal(buf []byte, r *report.Report, ranges *[][2]int) ([]byte, int) {
if v.IsZero() {
return buf, 0
}
if m := v.Concrete(); m != v { // Manual handling for Any.
url := m.TypeURL()
buf = protowire.AppendTag(buf, 1, protowire.BytesType)
buf = protowire.AppendVarint(buf, uint64(len(url)))
buf = append(buf, url...)
buf = protowire.AppendTag(buf, 2, protowire.BytesType)
return marshalFramed(buf, r, ranges, func(buf []byte) ([]byte, int) {
return m.marshal(buf, r, ranges)
})
}
var n int
for v := range v.Fields() {
var k int
buf, k = v.marshal(buf, r, ranges)
n += k
}
return buf, n
}
// marshalFramed marshals arbitrary data, as appended by body, with a leading
// length prefix.
//
// The body function must return the number of bytes that it marked as "extra",
// by appending them to ranges. This allows the length prefix to be correct
// after accounting for deletions in deleteRanges. This allows us to marshal
// minimal length prefixes without quadratic time copying buffers around.
func marshalFramed(buf []byte, _ *report.Report, ranges *[][2]int, body func([]byte) ([]byte, int)) ([]byte, int) {
// To avoid being accidentally quadratic, we encode every message
// length with five bytes.
mark := len(buf)
buf = append(buf, make([]byte, 5)...)
var n int
buf, n = body(buf)
bytes := uint64(len(buf) - (mark + 5) - n)
if bytes > math.MaxUint32 {
// This is not reachable today, because input files may be
// no larger than 4GB. However, that may change at some point,
// so keeping an ICE around is better than potentially getting
// corrupt output later.
//
// Later, this should probably become a diagnostic.
panic("protocompile/ir: marshalling options value overflowed length prefixes")
}
varint := protowire.AppendVarint(buf[mark:mark], bytes)
if k := len(varint); k < 5 {
*ranges = append(*ranges, [2]int{mark + k, mark + 5})
}
return buf, n + 5 - len(varint)
}
// deleteRanges deletes the given ranges from a byte array.
func deleteRanges(buf []byte, ranges [][2]int) []byte {
if len(ranges) == 0 {
return buf
}
slices.SortFunc(ranges, func(a, b [2]int) int {
return cmp.Compare(a[0], b[0])
})
offset := 0
for i, r1 := range ranges[:len(ranges)-1] {
r2 := ranges[i+1]
// Need to delete the interval between r1[0] and r1[1]. We do this
// by copying r1[1]..r2[0] to r1[0]..
copy(buf[r1[0]-offset:], buf[r1[1]:r2[0]])
offset += r1[1] - r1[0]
}
// Need to delete the last interval. To do this, we do what we did above,
// but where r2[0] is the end limit.
r1 := ranges[len(ranges)-1]
copy(buf[r1[0]-offset:], buf[r1[1]:])
offset += r1[1] - r1[0]
return buf[:len(buf)-offset]
}
// slot is returned by [MessageValue.insert]. It is a helper for making sure
// that the backreference for Value.Container is populated correctly.
type slot struct {
msg MessageValue
slot *id.ID[Value]
}
func (s slot) IsZero() bool {
return s.slot.IsZero()
}
func (s slot) Value() Value {
return id.Wrap(s.msg.Context(), *s.slot)
}
func (s slot) Insert(v Value) {
if !v.Container().IsZero() {
panic("protocompile/ir: slot.Insert with non-top-level value")
}
v.Raw().container = s.msg.ID()
*s.slot = v.ID()
}
// slot adds a new field to this message value, returning a pointer to the
// corresponding entry in the entries array, which can be initialized as-needed.
//
// A conflict occurs if there is already a field with the same number or part of
// the same oneof in this value. To determine whether to diagnose as a duplicate
// field or duplicate oneof, simply compare the field number of entry to that
// of the duplicate. If they are different, they share a oneof.
//
// When a conflict occurs, the existing rawValue pointer will be returned,
// whereas if the value is being inserted for the first time, the returned arena
// pointer will be nil and can be initialized by the caller.
func (v MessageValue) slot(field Member) slot {
id := field.InternedFullName()
if o := field.Oneof(); !o.IsZero() {
id = o.InternedFullName()
}
n := len(v.Raw().entries)
if actual, ok := mapsx.Add(v.Raw().byName, id, uint32(n)); !ok {
return slot{v, &v.Raw().entries[actual]}
}
v.Raw().entries = append(v.Raw().entries, 0)
return slot{v, slicesx.LastPointer(v.Raw().entries)}
}
// scalar is a type that can be converted into a [rawValueBits].
type scalar interface {
bool |
int32 | uint32 | int64 | uint64 |
float32 | float64 |
intern.ID | string
}
// newZeroScalar constructs a new scalar value.
func newZeroScalar(file *File, field Ref[Member]) Value {
return id.Wrap(file, id.ID[Value](file.arenas.values.NewCompressed(rawValue{
field: field,
})))
}
// appendRaw appends a scalar value to the given array value.
func appendRaw(array Value, bits rawValueBits) {
slice := array.slice()
*slice = append(*slice, bits)
}
// newScalar appends a new message value to the given array value, and returns it.
//
// If anyType is not zero, it will be used as the type of the inner message
// value. This is used for Any-typed fields. Otherwise, the type of field is
// used instead.
func appendMessage(array Value) MessageValue {
v := id.ID[MessageValue](array.Context().arenas.messages.NewCompressed(rawMessageValue{
self: array.ID(),
ty: array.Elements().At(0).AsMessage().Raw().ty,
byName: make(intern.Map[uint32]),
}))
slice := array.slice()
*slice = append(*slice, rawValueBits(v))
return id.Wrap(array.Context(), v)
}
// newMessage constructs a new message value.
//
// If anyType is not zero, it will be used as the type of the inner message
// value. This is used for Any-typed fields. Otherwise, the type of field is
// used instead.
func newMessage(file *File, field Ref[Member]) MessageValue {
member := GetRef(file, field)
raw := id.ID[MessageValue](file.arenas.messages.NewCompressed(rawMessageValue{
ty: member.Raw().elem.ChangeContext(member.Context(), file),
byName: make(intern.Map[uint32]),
}))
msg := id.Wrap(file, raw)
msg.Raw().self = id.ID[Value](file.arenas.values.NewCompressed(rawValue{
field: field,
bits: rawValueBits(raw),
}))
return msg
}
// newConcrete constructs a new value to be the concrete representation of
// v with the given type.
func newConcrete(m MessageValue, ty Type, url string) MessageValue {
if !m.Raw().concrete.IsZero() {
panic("protocompile/ir: set a concrete type more than once")
}
if !m.Type().IsAny() {
panic("protocompile/ir: set concrete type on non-Any")
}
field := m.AsValue().Raw().field
if int32(field.id) < 0 {
field.id ^= -1
}
msg := newMessage(m.Context(), field)
msg.Raw().ty = ty.toRef(m.Context())
msg.Raw().url = m.Context().session.intern.Intern(url)
m.Raw().concrete = msg.ID()
return msg
}
// newScalarBits converts a scalar into.Raw() bits for storing in a [Value].
func newScalarBits[T scalar](file *File, v T) rawValueBits {
switch v := any(v).(type) {
case bool:
if v {
return 1
}
return 0
case int32:
return rawValueBits(v)
case uint32:
return rawValueBits(v)
case int64:
return rawValueBits(v)
case uint64:
return rawValueBits(v)
case float32:
// All float values are stored as binary64. All binary32 floats can be
// losslessly encoded as binary64, so this conversion does not result
// in precision loss.
return rawValueBits(math.Float64bits(float64(v)))
case float64:
return rawValueBits(math.Float64bits(v))
case intern.ID:
return rawValueBits(v)
case string:
return rawValueBits(file.session.intern.Intern(v))
default:
panic("unreachable")
}
}
// 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 ir
import (
"sync"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/internal/intern"
)
// Session is shared global configuration and state for all IR values that are
// being used together.
//
// It is used to track shared book-keeping.
//
// A zero [Session] is ready to use.
type Session struct {
intern intern.Table
once sync.Once
builtins builtinIDs
}
// Lower lowers an AST into an IR module.
//
// The ir package does not provide a mechanism for resolving imports; instead,
// they must be provided as an argument to this function.
func (s *Session) Lower(source *ast.File, errs *report.Report, importer Importer) (file *File, ok bool) {
s.init()
prior := len(errs.Diagnostics)
file = &File{session: s, ast: source}
file.path = file.session.intern.Intern(CanonicalizeFilePath(source.Path()))
errs.SaveOptions(func() {
errs.SuppressWarnings = errs.SuppressWarnings || file.IsDescriptorProto()
lower(file, errs, importer)
})
ok = true
for _, d := range errs.Diagnostics[prior:] {
if d.Level() >= report.Error {
ok = false
break
}
}
return file, ok
}
func (s *Session) init() {
s.once.Do(func() { s.intern.Preload(&s.builtins) })
}
func lower(file *File, r *report.Report, importer Importer) {
defer r.CatchICE(false, func(d *report.Diagnostic) {
d.Apply(report.Notef("while lowering %q", file.Path()))
})
// First, build the Type graph for this file.
(&walker{File: file, Report: r}).walk()
// Now, resolve all the imports.
buildImports(file, r, importer)
generateMapEntries(file, r)
// Next, we can build various symbol tables in preparation for name
// resolution.
buildLocalSymbols(file)
mergeImportedSymbolTables(file, r)
// Perform "early" name resolution, i.e. field names and extension types.
resolveNames(file, r)
resolveEarlyOptions(file)
// Perform constant evaluation.
evaluateFieldNumbers(file, r)
// Check for number overlaps now that we have numbers loaded.
buildFieldNumberRanges(file, r)
// Perform "late" name resolution, that is, options.
resolveOptions(file, r)
// Figure out what the option targets of everything is, and validate that
// those are respected. This requires options to be resolved, and must be
// done in two separate steps.
populateOptionTargets(file, r)
validateOptionTargets(file, r)
// Build feature info for validating features after they are constructed.
// Then validate all feature settings throughout the file.
buildAllFeatureInfo(file, r)
validateAllFeatures(file, r)
populateJSONNames(file, r)
// Validate all the little constraint details that didn't get caught above.
diagnoseUnusedImports(file, r)
validateConstraints(file, r)
checkDeprecated(file, r)
}
// 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 ir
import (
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/report/tags"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
)
// checkDeprecated checks for deprecation warnings in the given file.
func checkDeprecated(file *File, r *report.Report) {
for imp := range seq.Values(file.Imports()) {
if d := imp.Deprecated(); !d.IsZero() {
r.Warn(errDeprecated{
ref: imp.Decl.ImportPath(),
name: imp.Path(),
cause: d.OptionSpan(),
})
}
}
checkDeprecatedOptions(file.Options(), r)
for ty := range seq.Values(file.AllTypes()) {
checkDeprecatedOptions(ty.Options(), r)
for o := range seq.Values(ty.Oneofs()) {
checkDeprecatedOptions(o.Options(), r)
}
}
for m := range file.AllMembers() {
checkDeprecatedOptions(m.Options(), r)
ty := m.Element()
// We do not emit deprecation warnings for references to a type
// defined in the same file, because this is a relatively common case.
if m.Context() != ty.Context() {
if d := ty.Deprecated(); !d.IsZero() {
r.Warn(errDeprecated{
ref: m.TypeAST().RemovePrefixes(),
name: string(ty.FullName()),
cause: d.OptionSpan(),
})
}
}
}
for s := range seq.Values(file.Services()) {
checkDeprecatedOptions(s.Options(), r)
for m := range seq.Values(s.Methods()) {
checkDeprecatedOptions(m.Options(), r)
in, _ := m.Input()
if m.Context() != in.Context() {
if d := in.Deprecated(); !d.IsZero() {
r.Warn(errDeprecated{
ref: m.AST().Signature().Inputs().At(0).RemovePrefixes(),
name: string(in.FullName()),
cause: d.OptionSpan(),
})
}
}
out, _ := m.Input()
if m.Context() != out.Context() {
if d := out.Deprecated(); !d.IsZero() {
r.Warn(errDeprecated{
ref: m.AST().Signature().Outputs().At(0).RemovePrefixes(),
name: string(out.FullName()),
cause: d.OptionSpan(),
})
}
}
}
}
}
func checkDeprecatedOptions(value MessageValue, r *report.Report) {
for field := range value.Fields() {
if d := field.Field().Deprecated(); !d.IsZero() {
for key := range seq.Values(field.KeyASTs()) {
r.Warn(errDeprecated{
ref: key,
name: string(field.Field().FullName()),
cause: d.OptionSpan(),
})
}
}
for elem := range seq.Values(field.Elements()) {
if enum := elem.AsEnum(); !enum.IsZero() {
if d := enum.Deprecated(); !d.IsZero() {
r.Warn(errDeprecated{
ref: elem.AST(),
name: string(enum.FullName()),
cause: d.OptionSpan(),
})
}
} else if msg := elem.AsMessage(); !msg.IsZero() {
checkDeprecatedOptions(msg, r)
}
}
}
}
// errDeprecated diagnoses a deprecation.
type errDeprecated struct {
ref, cause source.Spanner
name string
}
func (e errDeprecated) Diagnose(d *report.Diagnostic) {
d.Apply(
report.Message("`%s` is deprecated", e.name),
report.Snippet(e.ref),
report.Snippetf(e.cause, "deprecated here"),
report.Tag(tags.Deprecated),
)
}
// 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 ir
import (
"fmt"
"math"
"math/big"
"strconv"
"strings"
"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/ir/presence"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"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"
)
const (
fieldNumberBits = 29
fieldNumberMax = 1<<fieldNumberBits - 1
firstReserved = 19000
lastReserved = 19999
messageSetNumberBits = 31
messageSetNumberMax = 1<<messageSetNumberBits - 2 // Int32Max is not valid!
enumNumberBits = 32
enumNumberMax = math.MaxInt32
// These are the NaN bits used by virtually every language ever: quiet,
// positive, and with an all-zeros payload. `protoc`, by nature of being
// written in C++, picks up this bitpattern automatically.
//
// Go's math.NaN() does not specify NaN it returns, but it isn't this one.
// Originally, their NaN was 0x7ff0000000000001, which is floatBits(inf)+1,
// which is a valid NaN. However, it's a signaling NaN, which causes all
// kinds of unintended mayhem. They eventually fixed it to be a quiet NaN
// by setting the quiet bit, but left the payload as-is. This was,
// apparently, not a breaking change. We depend on the exact bit pattern,
// since that winds up in our tests, so depending on math.NaN() opens us up
// to Go randomly breaking us if they decide to fix their NaN constant
// again.
//
// The bitpattern probably doesn't matter for our users, but being explicit
// protects us from Go being sloppy with floating-point, which has
// historically been an issue, as noted above.
nanBits = 0x7ff8000000000000
)
// evaluator is the context needed to evaluate an expression.
type evaluator struct {
*File
*report.Report
scope FullName
}
//nolint:govet // vet complains about 8 wasted padding bytes.
type evalArgs struct {
expr ast.ExprAny // The expression to evaluate.
// The location to write the parsed expression into. If zero, a new value
// will be allocated.
target Value
field Member
optionPath ast.Path
rawField Ref[Member]
isConcreteAny bool
isArrayElement bool
// A span for whatever caused the above field to be selected.
annotation source.Spanner
textFormat bool // Whether we're inside of a message literal.
allowMax bool // Whether the max keyword is to be honored.
memberNumber memberNumber // Specifies which member number type we're resolving.
}
// memberNumber is used to tag evalArgs with one of the special types associated
// with a member number.
type memberNumber byte
const (
enumNumber memberNumber = iota + 1 // int32
fieldNumber // uint29
messageSetNumber // uint31-ish, 0x7fff_ffff is not allowed.
)
// Type returns the type that evaluation is targeting.
func (ea evalArgs) Type() Type {
if ea.isConcreteAny {
msg := ea.target.Elements().At(0).AsMessage()
return msg.Type()
}
return ea.field.Element()
}
// mismatch constructs a type mismatch error.
func (ea evalArgs) mismatch(got any) errTypeCheck {
var want any
if ty := ea.Type(); !ty.IsZero() {
want = ty
} else {
switch ea.memberNumber {
case enumNumber:
want = PredeclaredType(predeclared.Int32)
case fieldNumber:
want = taxa.FieldNumber
case messageSetNumber:
want = taxa.MessageSetNumber
}
}
return errTypeCheck{
want: want,
got: got,
expr: ea.expr,
annotation: ea.annotation,
}
}
// eval evaluates an expression into a value.
//
// Returns a zero value if evaluation produced "no value", such as due to
// a type checking failure or an empty array.
func (e *evaluator) eval(args evalArgs) Value {
defer e.AnnotateICE(report.Snippetf(args.expr, "while evaluating this"))
if arr := args.expr.AsArray(); !arr.IsZero() && arr.Elements().Len() == 0 {
// We don't create a value for empty arrays, but we still need to
// perform type checking.
if args.field.Presence() != presence.Repeated {
e.Error(args.mismatch(taxa.Array))
}
return Value{}
}
first := args.target.IsZero()
if first && args.rawField.IsZero() {
args.rawField = args.field.toRef(e.File)
} else if !first {
args.rawField = args.target.Raw().field
}
switch args.expr.Kind() {
case ast.ExprKindArray:
if args.field.IsSingular() {
e.Error(args.mismatch(taxa.Array))
}
expr := args.expr.AsArray()
for elem := range seq.Values(expr.Elements()) {
copied := args // Copy.
copied.expr = elem
copied.isArrayElement = true
v := e.eval(copied)
if args.target.IsZero() {
// Make sure to pick up a freshly allocated value, if this
// was the first iteration.
args.target = v
}
}
case ast.ExprKindDict:
args.target = e.evalMessage(args, args.expr.AsDict())
default:
bits, ok := e.evalBits(args)
if !ok {
return Value{}
}
if first {
args.target = newZeroScalar(e.File, args.rawField)
args.target.Raw().bits = bits
} else {
appendRaw(args.target, bits)
}
}
if !args.target.IsZero() {
raw := args.target.Raw()
isArray := args.expr.Kind() == ast.ExprKindArray
// Only populate elemIndices if we run into an array expression.
if raw.elemIndices == nil && isArray {
for i := range len(raw.exprs) {
// If this is the first array we're seeing, each expression
// contributes exactly one element.
raw.elemIndices = append(raw.elemIndices, uint32(i+1))
}
}
if !args.isArrayElement {
raw.exprs = append(raw.exprs, args.expr.ID())
raw.optionPaths = append(raw.optionPaths, args.optionPath.ID())
if raw.elemIndices != nil || isArray {
var n uint32
if raw.elemIndices != nil {
n = raw.elemIndices[len(raw.elemIndices)-1]
}
if isArray {
n += uint32(args.expr.AsArray().Elements().Len())
} else {
n++
}
raw.elemIndices = append(raw.elemIndices, n)
}
}
}
return args.target
}
// evalBits evaluates an expression, returning raw bits for the computed value.
//
// [evaluator.eval] is preferred; this is a separate function for the benefit
// of field number evaluation.
func (e *evaluator) evalBits(args evalArgs) (rawValueBits, bool) {
switch args.expr.Kind() {
case ast.ExprKindInvalid, ast.ExprKindError:
return 0, false
case ast.ExprKindLiteral:
return e.evalLiteral(args, args.expr.AsLiteral(), ast.ExprPrefixed{})
case ast.ExprKindPath:
return e.evalPath(args, args.expr.AsPath().Path, ast.ExprPrefixed{})
case ast.ExprKindPrefixed:
expr := args.expr.AsPrefixed()
inner := expr.Expr()
switch expr.Prefix() {
case keyword.Sub:
// Special handling to ensure that negative literals work correctly.
if !inner.AsLiteral().IsZero() {
return e.evalLiteral(args, inner.AsLiteral(), expr)
}
// Special cases for "signed identifiers".
if inner.Kind() == ast.ExprKindPath {
return e.evalPath(args, inner.AsPath().Path, expr)
}
// All other expressions cannot have a leading -.
err := args.mismatch(taxa.Classify(inner))
err.want = taxa.Number
e.Error(err)
return 0, false
default:
panic("unreachable")
}
case ast.ExprKindArray:
e.Error(args.mismatch(taxa.Array))
case ast.ExprKindDict:
e.Error(args.mismatch(taxa.Dict))
case ast.ExprKindRange:
if args.memberNumber == 0 {
break // Legalized in the parser.
}
e.Error(args.mismatch(taxa.Range))
case ast.ExprKindField:
break // Legalized in the parser.
default:
panic("unexpected ast.ExprKind")
}
return 0, false
}
// evalKey evaluates a key in a message literal.
func (e *evaluator) evalKey(args evalArgs, expr ast.ExprField) Member {
// There are a number of potentially incorrect ways of specifying
// a field here, which we want to diagnose.
//
// 1. A field might be named by number. In this case, we rely on
// field numbers having already been evaluated and we try to
// the field up by number. This seems hard to make work for
// extensions.
//
// 2. A partially qualified path to a field or extension. We try
// to do symbol resolution in the current scope.
//
// 3. The above in [] when it shouldn't be.
//
// 4. The above as a string literal.
//
// Everything else is unrecoverable.
ty := args.Type()
mapFieldHelp := func(d *report.Diagnostic) {
if !ty.MapField().IsZero() {
d.Apply(
// TODO: Generate a suggestion. It would be nice to tell the
// user to replace `k: v` with `{ key: k, value: v }`. Doing so
// for general expressions is unfortunately quite tricky, in
// particular because {k1: v1, k2: v2} needs to turn into
// [{key: k1, value: v1}, {key: k2, value: v2}].
report.Helpf(
"the text format lacks syntax for map-typed fields; instead, the syntax "+
"is the same as for a repeated message whose fields are named `key` and `value`",
),
report.Helpf(
"for example, `map_field { key: ..., value: ... }`",
),
)
}
}
cannotResolveKey := func() {
d := e.Errorf("cannot resolve %s name for `%s`", taxa.Field, ty.FullName()).Apply(
report.Snippetf(expr, "field referenced here"),
report.Snippetf(args.annotation, "expected `%s` field due to this", ty.Name()),
)
mapFieldHelp(d)
}
var member Member
var path string
var hasBrackets, isPath, isNumber, isString bool
key := expr.Key()
again:
switch key.Kind() {
case ast.ExprKindPath:
path = key.AsPath().Canonicalized()
if strings.Contains(path, "/") {
if ty.IsAny() {
// Any type names for an actual any are diagnosed elsewhere.
return Member{}
}
// This appears to be an Any type name.
d := e.Errorf("unexpected %s", taxa.TypeURL).Apply(
report.Snippet(expr.Key()),
report.Snippetf(args.annotation, "expected this to be `google.protobuf.Any`"),
report.Notef("%s may only appear in a `google.protobuf.Any`-typed %s", taxa.Dict),
)
mapFieldHelp(d)
return Member{}
}
isPath = true
case ast.ExprKindArray:
array := key.AsArray()
if hasBrackets || array.Elements().Len() != 1 {
// Diagnosed in the parser.
return Member{}
}
hasBrackets = true
key = array.Elements().At(0)
goto again
case ast.ExprKindLiteral:
lit := key.AsLiteral()
if lit.Kind() == token.Number {
n, exact := lit.AsNumber().Int()
if exact && n < math.MaxInt32 {
member = ty.MemberByNumber(int32(n))
if !member.IsZero() {
isNumber = true
goto validate
}
}
cannotResolveKey()
return Member{}
}
path = lit.AsString().Text()
isString = true
default:
cannotResolveKey()
return Member{}
}
// Try checking if this is just a member of the message directly.
if !hasBrackets {
member = ty.MemberByName(path)
}
if member.IsZero() {
if isPath && !hasBrackets && strings.HasPrefix(path, "(") {
// This was already diagnosed in legalize_option.go.
//
// TODO: we should try to do better here and use the contents of
// the () as the symbol lookup target.
return Member{}
}
// Otherwise kick off full symbol resolution.
sym := symbolRef{
File: e.File,
Report: nil, // Suppress diagnostics.
scope: e.scope,
name: FullName(path),
span: expr.Key(),
}.resolve()
if sym.IsZero() {
// This catches cases where a user names a non-extension field with
// [], but name lookup does not find it.
if member = ty.MemberByName(path); !member.IsZero() {
goto validate
}
cannotResolveKey()
return Member{}
} else if !sym.Kind().IsMessageField() {
d := e.Error(errTypeCheck{
want: fmt.Sprintf("`%s` field", ty.FullName()),
got: sym,
expr: expr.Key(),
annotation: args.annotation,
})
mapFieldHelp(d)
return Member{}
}
// NOTE: Absolute paths in this position are diagnosed in the parser.
member = sym.AsMember()
}
validate:
// Validate that the member is actually of the correct type.
if member.Container() != ty {
d := e.Error(errTypeCheck{
want: fmt.Sprintf("`%s` field", ty.FullName()),
got: fmt.Sprintf("`%s` field", member.Container().FullName()),
expr: expr.Key(),
annotation: args.annotation,
})
mapFieldHelp(d)
return Member{}
}
// Validate that the key was spelled correctly: if it is a field,
// it is a single identifier with the name of that field, and has no
// brackets; if it is an extension, it is the FQN and it has brackets.
wrongPath := member.IsMessageField() && path != member.Name()
misspelled := !isPath || hasBrackets != member.IsExtension() || wrongPath
if misspelled {
replace := member.Name()
if member.IsExtension() {
replace = fmt.Sprintf("[%s]", member.FullName())
}
d := e.Errorf("%s `%s` referenced incorrectly", member.noun(), member.FullName()).Apply(
report.Snippetf(expr.Key(), "referenced here"),
report.SuggestEdits(expr.Key(), fmt.Sprintf("reference it as `%s`", replace), report.Edit{
Start: 0, End: expr.Key().Span().Len(),
Replace: replace,
}),
)
if hasBrackets && !member.IsExtension() {
d.Apply(report.Notef("`[...]` must only be used when referencing extensions or concrete `Any` types"))
}
if !hasBrackets && member.IsExtension() {
d.Apply(report.Notef("extension names must be surrounded by `[...]`"))
}
if wrongPath {
d.Apply(report.Notef("field names must be a single identifier"))
}
if !hasBrackets {
if isNumber {
d.Apply(report.Notef("due to a parser quirk, `.protoc` rejects numbers here, even though textproto does not"))
}
if isString {
d.Apply(report.Notef("due to a parser quirk, `.protoc` rejects quoted strings here, even though textproto does not"))
}
}
mapFieldHelp(d)
}
return member
}
func (e *evaluator) evalMessage(args evalArgs, expr ast.ExprDict) Value {
if !args.Type().IsMessage() {
e.Error(args.mismatch(taxa.Dict))
return Value{}
}
var message MessageValue
switch {
case args.isConcreteAny:
message = args.target.Elements().At(0).AsMessage()
case args.target.IsZero():
message = newMessage(e.File, args.rawField)
args.target = message.AsValue()
default:
message = appendMessage(args.target)
}
if args.Type().IsAny() {
// Check if this is a valid concrete Any. There should be exactly
// one [host/path] key in the dictionary. If there is *at least one*,
// we choose the first one, and diagnose all other keys as invalid.
var url string
var urlExpr ast.ExprField
var key ast.ExprAny
urlSearch:
for expr := range seq.Values(expr.Elements()) {
key = expr.Key()
var hasBrackets bool
again:
switch key.Kind() {
case ast.ExprKindPath:
path := key.AsPath().Canonicalized()
if strings.Contains(path, "/") {
url = path
urlExpr = expr
break urlSearch
}
case ast.ExprKindArray:
array := key.AsArray()
if hasBrackets || array.Elements().Len() != 1 {
// Diagnosed in the parser.
continue
}
hasBrackets = true
key = array.Elements().At(0)
goto again
}
}
if url != "" {
// First, scold all the other fields.
first := true
for expr := range seq.Values(expr.Elements()) {
if expr == urlExpr {
continue
}
d := e.Errorf("unexpected field in `Any` expression").Apply(
report.Snippet(expr.Key()),
report.Notef("the %s must be the only field", taxa.TypeURL),
)
if first {
first = false
d.Apply(report.Snippetf(urlExpr.Key(), "expected this to be the only field"))
}
}
splitURL := func(path ast.Path) (before, after ast.Path) {
// Figure out what part of the key expression actually contains
// the domain. Look for the last component whose separator is a /.
pc, _ := iterx.Last(iterx.Filter(path.Components, func(pc ast.PathComponent) bool {
return pc.Separator().Text() == "/"
}))
hostSpan := path.Span()
hostSpan.End = pc.Span().Start
before, after = pc.SplitBefore()
return before, after.ToRelative()
}
// Next, resolve the type name. protoc only allows one /, but
// we allow multiple and simply diagnose the domain.
host, path, _ := stringsx.CutLast(url, "/")
hostPath, typePath := splitURL(key.AsPath().Path)
const anyDomainNote = "The domain must be one of `type.googleapis.com` or `type.googleprod.com`. " +
"This is a quirk of textformat; the compiler does not actually make any network requests."
switch host {
case "type.googleapis.com", "type.googleprod.com":
break
case "":
e.Errorf("missing domain in %s", taxa.TypeURL).Apply(
report.Snippet(urlExpr.Key()),
report.Notef(anyDomainNote),
)
default:
e.Errorf("unsupported domain `%s` in %s", host, taxa.TypeURL).Apply(
report.Snippet(hostPath),
report.Notef(anyDomainNote),
)
}
// Now try to resolve a concrete type. We do it exactly like
// we would for a field type, but *not* including scalar types.
ty := symbolRef{
File: e.File,
Report: e.Report,
scope: e.scope,
name: FullName(path),
span: typePath,
skipIfNot: SymbolKind.IsType,
accept: func(sk SymbolKind) bool { return sk == SymbolKindMessage },
want: taxa.MessageType,
allowScalars: false,
suggestImport: true,
}.resolve().AsType()
if ty.IsZero() {
// Diagnosed for us by resolve().
return Value{}
}
// Check that the URL contains the full name of the type.
if path != string(ty.FullName()) {
_, typePath := splitURL(key.AsPath().Path)
e.Errorf("partly-qualified name in %s", taxa.TypeURL).Apply(
report.Snippetf(typePath, "type referenced here"),
report.SuggestEdits(typePath, fmt.Sprintf("replace with %s", taxa.FullyQualifiedName), report.Edit{
Start: 0, End: typePath.Span().Len(),
Replace: string(ty.FullName()),
}),
report.Notef("%s require %ss", taxa.TypeURL, taxa.FullyQualifiedName),
)
}
// Apply the Any type and recurse.
abstract := args.target
args.target = newConcrete(message, ty, url).AsValue()
args.expr = urlExpr.Value()
args.annotation = urlExpr.Key()
args.isConcreteAny = true
_ = e.eval(args)
return abstract // Want to return the outer any here!
}
}
for expr := range seq.Values(expr.Elements()) {
field := e.evalKey(args, expr)
if field.IsZero() {
continue
}
copied := args
copied.textFormat = true
copied.isConcreteAny = false
copied.expr = expr.Value()
copied.annotation = field.TypeAST()
copied.field = field
copied.rawField = Ref[Member]{}
var exprCount int
slot := message.slot(field)
if slot.IsZero() {
copied.target = Value{}
} else {
value := slot.Value()
switch {
case field.IsRepeated():
copied.target = value
exprCount = len(value.Raw().exprs)
case value.Field() != field:
// A different member of a oneof was set.
e.Error(errSetMultipleTimes{
member: field.Oneof(),
first: value.KeyAST(),
second: expr.Key(),
})
copied.target = Value{}
case field.Element().IsMessage():
copied.target = value
exprCount = len(value.Raw().exprs)
default:
e.Error(errSetMultipleTimes{
member: field,
first: value.KeyAST(),
second: expr.Key(),
})
copied.target = Value{}
}
}
v := e.eval(copied)
if !v.IsZero() {
// Overwrite the most recently-added expression with the FieldExpr
// so that key lookup works correctly.
for i := range len(v.Raw().exprs) - exprCount {
v.Raw().exprs[exprCount+i] = expr.AsAny().ID()
}
if slot.IsZero() {
// Make sure to pick up a freshly allocated value, if this
// was the first iteration.
slot.Insert(v)
}
}
}
return message.AsValue()
}
// evalLiteral evaluates a literal expression.
func (e *evaluator) evalLiteral(args evalArgs, expr ast.ExprLiteral, neg ast.ExprPrefixed) (rawValueBits, bool) {
scalar := args.Type().Predeclared()
if args.Type().IsEnum() {
scalar = predeclared.Int32
}
switch expr.Kind() {
case token.Number:
lit := expr.AsNumber()
// Handle floats first, since all number formats can be used as floats.
if scalar.IsFloat() {
n, _ := lit.Float()
// If the number contains no decimal point, check that it has no
// 0x prefix. Hex literals are not permitted for float-typed
// values, but we don't know that until here, much later than
// all the other base checks in the compiler.
text := expr.Text()
if !taxa.IsFloatText(text) && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) {
e.Errorf("unsupported base for %s", taxa.Float).Apply(
report.SuggestEdits(expr, "use a decimal literal instead", report.Edit{
Start: 0, End: len(text),
Replace: strconv.FormatFloat(n, 'g', 40, 64),
}),
report.Notef("Protobuf does not support hexadecimal %s", taxa.Float),
)
}
if !neg.IsZero() {
n = -n
}
if scalar == predeclared.Float32 {
// This will, among other things, snap n to Infinity or zero
// if it is in-range for float64 but not float32.
n = float64(float32(n))
}
// Emit a diagnostic if the value is snapped to infinity.
// TODO: Should we emit a diagnostic when rounding produces
// the value 0.0 but expr.Text() contains non-zero digits?
if math.IsInf(n, 0) {
d := e.Warnf("%s rounds to infinity", taxa.Float).Apply(
report.Snippetf(expr, "this value is beyond the dynamic range of `%s`", scalar),
report.SuggestEdits(expr, "replace with `inf`", report.Edit{
Start: 0, End: len(text),
Replace: "inf", // The sign is not part of the expression.
}),
)
// If possible, show the power-of-10 exponent of the value.
f := new(big.Float)
if _, _, err := f.Parse(expr.Text(), 0); err == nil {
maxExp := 308
if scalar == predeclared.Float32 {
maxExp = 38
}
exp2 := f.MantExp(nil) // ~ log2 f
exp10 := int(float64(exp2) / math.Log2(10)) // log10 f = log2 f / log2 10
d.Apply(report.Notef(
"this value is of order 1e%d; `%s` can only represent around 1e%d",
exp10, scalar, maxExp))
}
}
// 32-bit floats are stored as 64-bit floats; this conversion is
// lossless.
return rawValueBits(math.Float64bits(n)), true
}
if n, exact := lit.Int(); exact && !lit.IsFloat() {
switch args.memberNumber {
case enumNumber:
return e.checkIntBounds(args, true, enumNumberBits, !neg.IsZero(), n)
case fieldNumber:
return e.checkIntBounds(args, false, fieldNumberBits, !neg.IsZero(), n)
case messageSetNumber:
return e.checkIntBounds(args, false, messageSetNumberBits, !neg.IsZero(), n)
}
if !scalar.IsNumber() {
e.Error(args.mismatch(taxa.Int))
return 0, false
}
return e.checkIntBounds(args, scalar.IsSigned(), scalar.Bits(), !neg.IsZero(), n)
}
if !lit.IsFloat() {
n := lit.Value()
switch args.memberNumber {
case enumNumber:
return e.checkIntBounds(args, true, enumNumberBits, !neg.IsZero(), n)
case fieldNumber:
return e.checkIntBounds(args, false, fieldNumberBits, !neg.IsZero(), n)
case messageSetNumber:
return e.checkIntBounds(args, false, messageSetNumberBits, !neg.IsZero(), n)
}
if !scalar.IsNumber() {
e.Error(args.mismatch(taxa.Int))
return 0, false
}
return e.checkIntBounds(args, scalar.IsSigned(), scalar.Bits(), !neg.IsZero(), n)
}
e.Error(args.mismatch(taxa.Float))
return 0, false
case token.String:
if scalar != predeclared.String && scalar != predeclared.Bytes {
e.Error(args.mismatch(PredeclaredType(predeclared.String)))
return 0, false
}
if !neg.IsZero() {
e.Error(errTypeCheck{
want: "number",
got: args.Type(),
expr: expr,
annotation: neg.PrefixToken(),
})
}
data := expr.AsString().Text()
return newScalarBits(e.File, data), true
}
return 0, false
}
// checkIntBounds checks that an integer is within the bounds of a possibly
// signed value with the given number of bits. Failure results in a saturated
// result.
//
// If neg is set, this means that the expression had a - out in front of it.
//
// If bits == fieldNumberBits, the field number bounds check is used instead, which disallows
// 0 and values in the implementation-reserved range.
func (e *evaluator) checkIntBounds(args evalArgs, signed bool, bits int, neg bool, got any) (rawValueBits, bool) {
err := func() {
e.Error(errLiteralRange{
errTypeCheck: args.mismatch(nil),
got: got,
signed: signed,
bits: bits,
})
}
var tooLarge bool
var v uint64
switch n := got.(type) {
case uint64:
v = n
case *big.Float:
// We assume that a big.Float is always larger than a uint64.
tooLarge = true
default:
panic("unreachable")
}
if signed {
hi := (int64(1) << (bits - 1)) - 1
lo := ^hi // Ensure that lo is sign-extended to 64 bits.
if neg {
v = -v
}
v := int64(v)
// If bits == 64, we may be in a situation where - overflows. For
// example, if the input value is uint64(math.MaxInt32+1), then -
// is the identity (this is the only value other than 0 that its
// own two's complement).
//
// To detect this, we have to check that the sign of v is consistent
// with neg. If -v > 0 or v < 0, overflow has occurred.
if (neg && tooLarge) || (neg && v > 0) || v < lo {
err()
return rawValueBits(lo), false
}
if (!neg && tooLarge) || (!neg && v < 0) || v > hi {
err()
return rawValueBits(hi), false
}
} else {
if neg {
err()
return 0, false
}
hi := (uint64(1) << bits) - 1
if bits == messageSetNumberBits {
hi = messageSetNumberMax
}
if tooLarge || v > hi {
err()
return rawValueBits(hi), false
}
}
if bits == fieldNumberBits {
if v == 0 {
err()
return 0, false
}
// Check that this is not one of the special reserved numbers.
if v >= firstReserved && v <= lastReserved {
err()
return rawValueBits(v), false
}
}
return rawValueBits(v), true
}
// evalPath evaluates a path expression.
func (e *evaluator) evalPath(args evalArgs, expr ast.Path, neg ast.ExprPrefixed) (rawValueBits, bool) {
if ty := args.Type(); ty.IsEnum() {
// We can just plumb the text of the expression directly here, since
// if it's anything that isn't an identifier, this lookup will fail.
//
// TODO: This depends on field numbers being resolved before options,
// but some options need to be resolved first.
value := ty.MemberByName(expr.Span().Text())
if !value.IsZero() {
v := value.Number()
if !neg.IsZero() {
v = -v
e.Error(errTypeCheck{
want: "number",
got: ty,
expr: expr,
annotation: neg.PrefixToken(),
}).Apply(report.SuggestEdits(neg, "replace it with a literal value", report.Edit{
Start: 0, End: neg.Span().Len(),
Replace: fmt.Sprint(v), //nolint:perfsprint // False positive.
}))
}
return newScalarBits(e.File, v), true
}
// Allow fall-through, which proceeds to eventually hit full symbol
// resolution at the bottom.
}
scalar := args.Type().Predeclared()
// If we see a name that matches one of the predeclared names, resolve
// to it, just like it would for type lookup.
//
// TODO: When implementing message literals, we need to make sure to accept
// all of the non-standard forms that are allowed only inside of them.
switch name := expr.AsPredeclared(); name {
case predeclared.Max:
ok := args.allowMax
if ok {
switch args.memberNumber {
case enumNumber:
return enumNumberMax, ok
case fieldNumber:
return fieldNumberMax, ok
case messageSetNumber:
return messageSetNumberMax, ok
}
} else {
e.Errorf("`max` outside of range end").Apply(
report.Snippet(expr),
report.Notef(
"the special `max` expression can only be used at the end of a range"),
)
return 0, false
}
if !neg.IsZero() {
e.Errorf("negated `max`").Apply(
report.Snippet(neg),
report.Notef("the special `max` expression may not be negated"),
)
}
if !scalar.IsNumber() {
e.Error(args.mismatch(taxa.Noun(keyword.Max)))
return 0, false
}
if scalar.IsFloat() {
v := math.Inf(0)
if !neg.IsZero() {
v = -v
}
return newScalarBits(e.File, v), ok
}
n := uint64(1) << scalar.Bits()
if scalar.IsSigned() {
n >>= 1
}
n--
if !neg.IsZero() {
n = -n
}
return rawValueBits(n), ok
case predeclared.True, predeclared.False:
if scalar != predeclared.Bool {
e.Error(args.mismatch(PredeclaredType(predeclared.Bool)))
return 0, false
}
if !neg.IsZero() {
e.Error(errTypeCheck{
want: "number",
got: PredeclaredType(predeclared.Bool),
expr: expr,
annotation: neg.PrefixToken(),
})
}
switch name {
case predeclared.False:
return 0, true
case predeclared.True:
return 1, true
}
case predeclared.Inf, predeclared.NAN:
if !scalar.IsFloat() {
e.Error(args.mismatch(taxa.Float))
return 0, false
}
var v float64
switch name {
case predeclared.Inf:
v = math.Inf(0)
case predeclared.NAN:
v = math.Float64frombits(nanBits)
}
if !neg.IsZero() {
v = -v
}
return newScalarBits(e.File, v), true
}
// Match the "non standard" symbols for true, false, inf, and nan. Make
// sure to warn when users do it in text mode, and error when outside of
// it.
text := expr.Span().Text()
switch scalar {
case predeclared.Bool:
if slicesx.Among(text, "False", "f", "True", "t") {
value := slicesx.Among(text, "True", "t")
var d *report.Diagnostic
if args.textFormat {
d = e.Warnf("non-canonical `bool` literal")
} else {
d = e.Errorf("non-canonical `bool` literal outside of %s", taxa.Dict)
}
d.Apply(
report.Snippet(expr),
report.SuggestEdits(expr, fmt.Sprintf("replace with `%v`", value), report.Edit{
Start: 0, End: len(text),
Replace: strconv.FormatBool(value),
}),
report.Notef("within %ss only, `%s` is permitted as a `bool`, but should be avoided", taxa.Dict, text),
)
if !neg.IsZero() {
e.Error(errTypeCheck{
want: "number",
got: PredeclaredType(predeclared.Bool),
expr: expr,
annotation: neg.PrefixToken(),
})
}
if value {
return 1, args.textFormat
}
return 0, args.textFormat
}
case predeclared.Float32, predeclared.Float64:
var v float64
var canonical string
switch {
case strings.EqualFold(text, "inf"), strings.EqualFold(text, "infinity"):
canonical = "inf"
v = math.Inf(0)
case strings.EqualFold(text, "nan"):
canonical = "nan"
v = math.Float64frombits(nanBits)
}
if !neg.IsZero() {
v = -v
}
var d *report.Diagnostic
if args.textFormat {
d = e.Warnf("non-canonical %s", taxa.Float)
} else {
d = e.Errorf("non-canonical %s outside of %s", taxa.Float, taxa.Dict)
}
d.Apply(
report.Snippet(expr),
report.SuggestEdits(expr, fmt.Sprintf("replace with `%v`", canonical), report.Edit{
Start: 0, End: len(text),
Replace: canonical,
}),
report.Notef("within %ss only, some %ss are case-insensitive", taxa.Dict, taxa.Float),
)
return newScalarBits(e.File, v), args.textFormat
}
// Perform symbol lookup in the current scope. This isn't what protoc
// does, but it allows us to produce better diagnostics.
sym := symbolRef{
File: e.File,
Report: e.Report,
scope: e.scope,
name: FullName(expr.Canonicalized()),
span: expr,
allowScalars: true,
}.resolve()
if ty := sym.AsType(); !ty.IsZero() {
e.Error(args.mismatch(fmt.Sprintf("type reference `%s`", ty.FullName())))
} else if ev := sym.AsMember(); ev.IsEnumValue() {
if ev.Container() == args.Type() {
e.Errorf("qualified enum value reference").Apply(
report.Snippet(expr),
report.SuggestEdits(expr, "replace it with the value's name", report.Edit{
Start: 0, End: expr.Span().Len(),
Replace: ev.Name(),
}),
report.Notef("Protobuf requires single identifiers when referencing to the names of enum values"),
)
return newScalarBits(e.File, ev.Number()), false
}
e.Error(args.mismatch(ev.Container()))
} else if !sym.IsZero() {
e.Error(args.mismatch(sym))
}
return 0, false
}
// errTypeCheck is a type-checking failure.
type errTypeCheck struct {
want, got any
expr source.Spanner
annotation source.Spanner
wantRepeated, gotRepeated bool
}
// Diagnose implements [report.Diagnose].
func (e errTypeCheck) Diagnose(d *report.Diagnostic) {
strings := func(v any, repeated bool) (name, what string) {
type symbol interface {
FullName() FullName
noun() taxa.Noun
}
if sym, ok := v.(symbol); ok {
r := ""
if repeated {
r = "repeated "
}
name = fmt.Sprintf("`%s%s`", r, sym.FullName())
return name, sym.noun().String() + " " + name
}
name = fmt.Sprint(v)
return name, name
}
wantName, wantWhat := strings(e.want, e.wantRepeated)
gotName, gotWhat := strings(e.got, e.gotRepeated)
d.Apply(
report.Message("mismatched types"),
report.Snippetf(e.expr, "expected %s, found %s", wantName, gotName),
report.Notef("expected: %s\n found: %s", wantWhat, gotWhat),
)
if e.annotation != nil {
d.Apply(report.Snippetf(e.annotation, "expected due to this"))
}
}
// errTypeConstraint is like errTypeCheck, but intended for dealing with a case
// where a type does not satisfy a constraint, e.g., expecting a message type.
type errTypeConstraint struct {
want any
got Type
decl ast.TypeAny
}
// Diagnose implements [report.Diagnose].
func (e errTypeConstraint) Diagnose(d *report.Diagnostic) {
d.Apply(
report.Message("expected %s, found %s `%s`", e.want, e.got.noun(), e.got.FullName()),
report.Snippet(e.decl.RemovePrefixes()),
)
}
// errLiteralRange is like [errTypeCheck], but is specifically about integer
// ranges.
type errLiteralRange struct {
errTypeCheck
got any
signed bool
bits int
}
func (e errLiteralRange) Diagnose(d *report.Diagnostic) {
name := e.want
if sym, ok := e.want.(interface{ FullName() FullName }); ok {
name = "`" + string(sym.FullName()) + "`"
}
var lo, hi uint64
var sign string
if e.signed {
sign = "-"
lo = uint64(1) << (e.bits - 1)
hi = lo - 1
} else {
hi = (uint64(1) << e.bits) - 1
if e.bits == messageSetNumberBits {
hi = messageSetNumberMax
}
}
var base int
var prefix string
text := e.expr.Span().Text()
text = text[strings.IndexAny(text, "0123456789xXoObB"):]
switch {
case strings.HasPrefix(text, "0x"), strings.HasPrefix(text, "0X"):
base = 16
prefix = text[:2]
case text != "0" && strings.HasPrefix(text, "0"):
base = 8
prefix = "0"
case strings.HasPrefix(text, "0o"), strings.HasPrefix(text, "0O"):
base = 8
prefix = text[:2]
case strings.HasPrefix(text, "0b"), strings.HasPrefix(text, "0B"):
base = 2
prefix = text[:2]
default:
base = 10
}
itoa := func(v uint64) string {
return prefix + strconv.FormatUint(v, base)
}
if e.bits == fieldNumberBits {
d.Apply(
report.Message("%s out of range", taxa.FieldNumber),
report.Snippet(e.expr),
report.Notef("the range for %ss is `%v to %v`,\n"+
"minus `%v to %v`, which is reserved for internal use",
taxa.FieldNumber,
itoa(1),
itoa(hi),
itoa(uint64(firstReserved)),
itoa(uint64(lastReserved))),
)
} else {
d.Apply(
report.Message("literal out of range for %s", name),
report.Snippet(e.expr),
report.Notef("the range for %s is `%v%v to %v`", name, sign,
itoa(lo), itoa(hi)),
)
}
if e.annotation != nil {
d.Apply(report.Snippetf(e.annotation, "expected due to this"))
}
}
// 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 ir
import (
"cmp"
"slices"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/erredition"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
func buildAllFeatureInfo(file *File, r *report.Report) {
for m := range file.AllMembers() {
if !m.IsEnumValue() {
buildFeatureInfo(m, r)
}
}
}
// buildFeatureInfo builds feature information for a feature field.
//
// A feature field is any field which sets either of the editions_defaults or
// feature_support fields.
func buildFeatureInfo(field Member, r *report.Report) {
builtins := field.Context().builtins()
defaults := field.Options().Field(builtins.EditionDefaults)
support := field.Options().Field(builtins.EditionSupport).AsMessage()
if defaults.IsZero() && support.IsZero() {
return
}
mistake := report.Notef("this is likely a mistake, but it is not rejected by protoc")
info := new(rawFeatureInfo)
if defaults.IsZero() {
r.Warnf("expected feature field to set `%s`", builtins.EditionDefaults.Name()).Apply(
report.Snippet(field.AST().Options()), mistake,
)
} else {
for def := range seq.Values(defaults.Elements()) {
def := def.AsMessage()
value := def.Field(builtins.EditionDefaultsKey)
key, _ := value.AsInt()
edition := syntax.Syntax(key)
if value.IsZero() {
r.Warnf("missing `%s.%s`",
builtins.EditionDefaultsKey.Container().Name(),
builtins.EditionDefaultsKey.Name(),
).Apply(
report.Snippet(def.AsValue().ValueAST()),
mistake,
)
} else if !edition.IsConstraint() {
r.Warnf("unexpected `%s` in `%s.%s`",
syntax.EditionLegacy.DescriptorName(),
builtins.EditionDefaultsKey.Container().Name(),
builtins.EditionDefaultsKey.Name(),
).Apply(
report.Snippet(value.ValueAST()),
mistake,
report.Helpf("this should be a released edition or `%s`",
syntax.EditionLegacy.DescriptorName()),
)
}
value = def.Field(builtins.EditionDefaultsValue)
// We can't use eval() here because we would need to run the whole
// parser on the contents of the quoted string.
var bits rawValueBits
if value.IsZero() {
r.Warnf("missing value for `%s.%s`",
builtins.EditionDefaultsKey.Container().Name(),
builtins.EditionDefaultsKey.Name(),
).Apply(
report.Snippet(def.AsValue().ValueAST()), mistake,
)
} else {
text, _ := value.AsString()
switch {
case field.Element().IsEnum():
ev := field.Element().MemberByName(text)
if ev.IsZero() {
r.Warnf("expected quoted enum value in `%s.%s`",
builtins.EditionDefaultsKey.Container().Name(),
builtins.EditionDefaultsKey.Name(),
).Apply(
report.Snippet(value.ValueAST()),
report.Snippetf(field.TypeAST(), "expected due to this"),
report.Helpf("`value` must be the name of a value in `%s`", field.Element().FullName()),
mistake,
)
}
bits = rawValueBits(ev.Number())
case field.Element().Predeclared() == predeclared.Bool:
switch text {
case "false":
bits = 0
case "true":
bits = 1
default:
r.Warnf("expected quoted bool in `%s.%s`",
builtins.EditionDefaultsValue.Container().Name(),
builtins.EditionDefaultsValue.Name(),
).Apply(
report.Snippet(value.ValueAST()),
report.Snippetf(field.TypeAST(), "expected due to this"),
report.Helpf("`value` must one of \"true\" or \"false\""),
mistake,
)
}
default:
r.Warn(errTypeConstraint{
want: "`bool` or enum type",
got: field.Element(),
decl: field.TypeAST(),
}).Apply(
report.Snippetf(defaults.KeyAST(), "expected because this makes `%s` into a feature", field.Name()),
report.Helpf("features should have `bool` or enum type"),
mistake,
)
continue
}
}
// Cook up a value corresponding to the thing we just evaluated.
var copied rawValue
if !value.IsZero() {
copied = *value.Raw()
}
copied.field = field.toRef(field.Context())
copied.bits = bits
raw := field.Context().arenas.values.NewCompressed(copied)
// Push this information onto the edition defaults list.
info.defaults = append(info.defaults, featureDefault{
edition: edition,
value: id.ID[Value](raw),
})
}
}
// Sort the defaults by their editions.
slices.SortStableFunc(info.defaults, func(a, b featureDefault) int {
return cmp.Compare(a.edition, b.edition)
})
if len(info.defaults) > 0 && !slicesx.Among(info.defaults[0].edition, syntax.EditionLegacy, syntax.Proto2) {
r.Warnf("`%s` does not cover all editions", builtins.EditionDefaults.Name()).Apply(
report.Snippet(defaults.ValueAST()),
report.Helpf(
"`%s` must specify a default for `%s` or `%s` to cover all editions",
builtins.EditionDefaults.Name(),
syntax.Proto2.DescriptorName(),
syntax.EditionLegacy.DescriptorName(),
),
mistake,
)
}
// Insert a default value so FeatureSet.Lookup always returns *something*.
info.defaults = slices.Insert(info.defaults, 0, featureDefault{
edition: syntax.Unknown,
value: id.ID[Value](field.Context().arenas.values.NewCompressed(rawValue{
field: field.toRef(field.Context()),
})),
})
if support.IsZero() {
r.Warnf("expected feature field to set `%s`", builtins.EditionSupport.Name()).Apply(
report.Snippet(field.AST().Options()), mistake,
)
} else {
value := support.Field(builtins.EditionSupportIntroduced)
n, _ := value.AsInt()
info.introduced = syntax.Syntax(n)
if value.IsZero() {
r.Warnf("expected `%s.%s` to be set",
builtins.EditionSupportIntroduced.Container().Name(),
builtins.EditionSupportIntroduced.Name(),
).Apply(
report.Snippet(support.AsValue().ValueAST()),
mistake,
)
} else if info.introduced == syntax.Unknown {
r.Warnf("unexpected `%s` in `%s`",
info.introduced.DescriptorName(),
builtins.EditionSupportIntroduced.Name(),
).Apply(
report.Snippet(value.ValueAST()),
mistake,
)
}
value = support.Field(builtins.EditionSupportDeprecated)
n, _ = value.AsInt()
info.deprecated = syntax.Syntax(n)
if !value.IsZero() && info.deprecated == syntax.Unknown {
r.Warnf("unexpected `%s` in `%s`",
info.deprecated.DescriptorName(),
builtins.EditionSupportDeprecated.Name(),
).Apply(
report.Snippet(value.ValueAST()),
mistake,
)
}
value = support.Field(builtins.EditionSupportRemoved)
n, _ = value.AsInt()
info.removed = syntax.Syntax(n)
if !value.IsZero() && info.removed == syntax.Unknown {
r.Warnf("unexpected `%s` in `%s`",
info.removed.DescriptorName(),
builtins.EditionSupportRemoved.Name(),
).Apply(
report.Snippet(value.ValueAST()),
mistake,
)
}
value = support.Field(builtins.EditionSupportWarning)
info.deprecationWarning, _ = value.AsString()
}
field.Raw().featureInfo = info
}
func validateAllFeatures(file *File, r *report.Report) {
builtins := file.builtins()
features := file.Options().Field(builtins.FileFeatures)
validateFeatures(features.AsMessage(), r)
file.features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
}))
for ty := range seq.Values(file.AllTypes()) {
if !ty.MapField().IsZero() {
// Map entries never have features.
continue
}
parent := file.features
if !ty.Parent().IsZero() {
parent = ty.Parent().Raw().features
}
option := builtins.MessageFeatures
if ty.IsEnum() {
option = builtins.EnumFeatures
}
features := ty.Options().Field(option)
validateFeatures(features.AsMessage(), r)
ty.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: parent,
}))
for member := range seq.Values(ty.Members()) {
option := builtins.FieldFeatures
if member.IsEnumValue() {
option = builtins.EnumFeatures
}
features := member.Options().Field(option)
validateFeatures(features.AsMessage(), r)
member.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: ty.Raw().features,
}))
}
for oneof := range seq.Values(ty.Oneofs()) {
features := oneof.Options().Field(builtins.OneofFeatures)
validateFeatures(features.AsMessage(), r)
oneof.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: ty.Raw().features,
}))
}
for extns := range seq.Values(ty.ExtensionRanges()) {
features := extns.Options().Field(builtins.RangeFeatures)
validateFeatures(features.AsMessage(), r)
extns.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: ty.Raw().features,
}))
}
}
for field := range seq.Values(file.AllExtensions()) {
parent := file.features
if !field.Parent().IsZero() {
parent = field.Parent().Raw().features
}
features := field.Options().Field(builtins.FieldFeatures)
validateFeatures(features.AsMessage(), r)
field.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: parent,
}))
}
for service := range seq.Values(file.Services()) {
features := service.Options().Field(builtins.ServiceFeatures)
validateFeatures(features.AsMessage(), r)
service.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: file.features,
}))
for method := range seq.Values(service.Methods()) {
features := method.Options().Field(builtins.MethodFeatures)
validateFeatures(features.AsMessage(), r)
method.Raw().features = id.ID[FeatureSet](file.arenas.features.NewCompressed(rawFeatureSet{
options: features.ID(),
parent: service.Raw().features,
}))
}
}
}
// validateFeatures validates that the given features are compatible with the
// current edition.
func validateFeatures(features MessageValue, r *report.Report) {
if features.IsZero() {
return
}
defer r.AnnotateICE(report.Snippetf(
features.AsValue().ValueAST(),
"while validating this features message",
))
builtins := features.Context().builtins()
edition := features.Context().Syntax()
for feature := range features.Fields() {
if msg := feature.AsMessage(); !msg.IsZero() {
validateFeatures(msg, r)
continue
}
info := feature.Field().FeatureInfo()
if info.IsZero() {
r.Warnf("non-feature field set within `%s`", features.AsValue().Field().Name()).Apply(
report.Snippet(feature.ValueAST()),
report.Helpf("a feature field is a field which sets the `%s` and `%s` options",
builtins.EditionDefaults.Name(),
builtins.EditionSupport.Name(),
),
)
continue
}
// We check these in reverse order, because the user might have set
// introduced == deprecated == removed, and protoc doesn't enforce
// any relationship between these.
switch {
case info.IsRemoved(edition), info.IsDeprecated(edition):
r.SoftError(info.IsRemoved(edition), erredition.TooNew{
Current: features.Context().Syntax(),
Decl: features.Context().AST().Syntax(),
Removed: info.Removed(),
Deprecated: info.Deprecated(),
DeprecatedReason: info.DeprecationWarning(),
What: feature.Field().Name(),
Where: feature.KeyAST(),
})
case !info.IsIntroduced(edition):
r.Error(erredition.TooOld{
Current: features.Context().Syntax(),
Decl: features.Context().AST().Syntax(),
Intro: info.Introduced(),
What: feature.Field().Name(),
Where: feature.KeyAST(),
})
}
}
}
// 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 ir
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strconv"
"strings"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/cycle"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/intern"
)
const DescriptorProtoPath = "google/protobuf/descriptor.proto"
// Importer is a callback to resolve the imports of an [ast.File] being
// lowered.
//
// If a cycle is encountered, should return an *[incremental.ErrCycle],
// starting from decl and ending when the currently lowered file is imported.
//
// [Session.Lower] may not call this function on all imports; only those for
// which it needs the caller to resolve a [File] for it.
//
// This function will also be called with [DescriptorProtoPath] if it isn't
// transitively imported by the lowered file, with an index value of -1.
// Returning an error or a zero file will trigger an ICE.
type Importer func(n int, path string, decl ast.DeclImport) (*File, error)
// ErrCycle is returned by an [Importer] when encountering an import cycle.
type ErrCycle = cycle.Error[ast.DeclImport]
// buildImports builds the transitive imports table.
func buildImports(file *File, r *report.Report, importer Importer) {
dedup := make(intern.Map[ast.DeclImport], iterx.Count(file.AST().Imports()))
for i, imp := range iterx.Enumerate(file.AST().Imports()) {
lit := imp.ImportPath().AsLiteral().AsString()
if lit.IsZero() {
continue // Already legalized in parser.legalizeImport()
}
path := canonicalizeImportPath(lit.Text(), r, imp)
if path == "" {
continue
}
imported, err := importer(i, path, imp)
var cycle *ErrCycle
switch {
case err == nil:
case errors.As(err, &cycle):
diagnoseCycle(r, cycle)
continue
case errors.Is(err, fs.ErrNotExist):
r.Errorf("imported file does not exist").Apply(
report.Snippetf(imp, "imported here"),
)
continue
default:
r.Errorf("could not open imported file: %v", err).Apply(
report.Snippetf(imp, "imported here"),
)
continue
}
if prev, ok := dedup.AddID(imported.InternedPath(), imp); !ok {
d := r.Errorf("file imported multiple times").Apply(
report.Snippet(imp),
report.Snippetf(prev, "first imported here"),
)
if prev.ImportPath().AsLiteral().Text() != imp.ImportPath().AsLiteral().Text() {
d.Apply(report.Helpf("both paths are equivalent to %q", path))
}
continue
}
file.imports.AddDirect(Import{
File: imported,
Public: imp.IsPublic(),
Weak: imp.IsWeak(),
Option: imp.IsOption(),
Decl: imp,
})
}
// Having found all of the imports that are not cyclic, we now need to pull
// in all of *their* transitive imports.
file.imports.Recurse(dedup)
// Check if descriptor.proto was transitively imported. If not, import it.
if idx, ok := file.imports.byPath[file.session.builtins.DescriptorFile]; ok {
// Copy it to the end so that it's easy to find.
file.imports.files = append(file.imports.files, file.imports.files[idx])
return
}
// If this is descriptor.proto itself, use it. This step is necessary to
// avoid cycles.
if file.IsDescriptorProto() {
file.imports.Insert(Import{File: file}, -1, false)
file.imports.byPath[file.session.builtins.DescriptorFile] = uint32(len(file.imports.files) - 1)
file.imports.causes[file.session.builtins.DescriptorFile] = uint32(len(file.imports.files) - 1)
return
}
// Otherwise, try to look it up.
dproto, err := importer(-1, DescriptorProtoPath, ast.DeclImport{})
if err != nil {
panic(fmt.Errorf("could not import %q: %w", DescriptorProtoPath, err))
}
if dproto == nil {
panic(fmt.Errorf("importing %q produced an invalid file", DescriptorProtoPath))
}
file.imports.Insert(Import{File: dproto, Decl: ast.DeclImport{}}, -1, false)
file.imports.byPath[file.session.builtins.DescriptorFile] = uint32(len(file.imports.files) - 1)
file.imports.causes[file.session.builtins.DescriptorFile] = uint32(len(file.imports.files) - 1)
}
// diagnoseCycle generates a diagnostic for an import cycle, showing each
// import contributing to the cycle in turn.
func diagnoseCycle(r *report.Report, cycle *ErrCycle) {
path := cycle.Cycle[0].ImportPath().AsLiteral().AsString().Text()
err := r.Errorf("detected cyclic import while importing %q", path)
for i, imp := range cycle.Cycle {
var message string
if path := imp.ImportPath().AsLiteral().AsString(); !path.IsZero() {
switch i {
case 0:
message = "imported here"
case len(cycle.Cycle) - 1:
message = fmt.Sprintf("...which imports %q, completing the cycle", path.Text())
default:
message = fmt.Sprintf("...which imports %q...", path.Text())
}
}
err.Apply(
report.PageBreak,
report.Snippetf(imp, "%v", message),
)
}
}
// canonicalizeImportPath canonicalizes the path of an import declaration.
//
// This will generate diagnostics for invalid paths. Returns "" for paths that
// cannot be made canonical.
//
// If r is nil, no diagnostics are emitted. This behavior exists to avoid
// duplicating code with [CanonicalizeFilePath].
func canonicalizeImportPath(path string, r *report.Report, decl ast.DeclImport) string {
if path == "" {
if r != nil {
r.Errorf("import path cannot be empty").Apply(
report.Snippet(decl.ImportPath()),
)
}
return ""
}
orig := path
// Not filepath.ToSlash, since this conversion is file-system independent.
path = strings.ReplaceAll(path, `\`, `/`)
hasBackslash := orig != path
if r != nil && hasBackslash {
r.Errorf("import path cannot use `\\` as a path separator").Apply(
report.Snippetf(decl.ImportPath(), "this path begins with a `%c`", path[0]),
report.SuggestEdits(decl.ImportPath(), "use `/` as the separator instead", report.Edit{
Start: 0, End: decl.ImportPath().Span().Len(),
Replace: strconv.Quote(path),
}),
report.Notef("this restriction also applies when compiling on a non-Windows system"),
)
}
path = filepath.ToSlash(filepath.Clean(path))
isClean := !hasBackslash && orig == path
if r != nil && !isClean {
r.Errorf("import path must not contain `.`, `..`, or repeated separators").Apply(
report.Snippetf(decl.ImportPath(), "imported here"),
report.SuggestEdits(decl.ImportPath(), "canonicalize this path", report.Edit{
Start: 0, End: decl.ImportPath().Span().Len(),
Replace: strconv.Quote(path),
}),
)
}
if r != nil && isClean && strings.HasPrefix(path, "../") {
r.Errorf("import path must not refer to parent directory").Apply(
report.Snippetf(decl.ImportPath(), "imported here"),
)
return "" // Refuse to escape to a parent directory.
}
if r != nil {
isLetter := func(b byte) bool {
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
}
if len(path) >= 2 && isLetter(path[0]) && path[1] == ':' {
// TODO: error on windows?
r.Warnf("import path appears to begin with the Windows drive prefix `%s`", path[:2]).Apply(
report.Snippet(decl.ImportPath()),
report.Notef("this is not an error, because `protoc` accepts it, but may result in unexpected behavior on Windows"),
)
}
}
if r != nil && strings.HasPrefix(path, "/") {
r.Errorf("import path must be relative").Apply(
report.Snippetf(decl.ImportPath(), "this path begins with a `%c`", path[0]),
)
return ""
}
return path
}
// CanonicalizeFilePath puts a file path into canonical form.
//
// This function is exported so that all code depending on this module can make
// sure paths are consistently canonicalized.
func CanonicalizeFilePath(path string) string {
return canonicalizeImportPath(path, nil, ast.DeclImport{})
}
// 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 ir
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/cases"
"github.com/bufbuild/protocompile/internal/intern"
)
func populateJSONNames(file *File, r *report.Report) {
builtins := file.builtins()
names := intern.Map[Member]{}
for ty := range seq.Values(file.AllTypes()) {
clear(names)
jsonFormat, _ := ty.FeatureSet().Lookup(builtins.FeatureJSON).Value().AsInt()
strict := jsonFormat == 1
// First, populate the default names, and check for collisions among
// them.
for field := range seq.Values(ty.Members()) {
var name string
if ty.IsEnum() {
name = internal.TrimPrefix(field.Name(), ty.Name())
name = cases.Enum.Convert(name)
} else {
name = internal.JSONName(field.Name())
}
field.Raw().jsonName = file.session.intern.Intern(name)
prev, ok := names.AddID(field.Raw().jsonName, field)
if prev.Number() == field.Number() {
// This handles the case where enum numbers coincide in an
// allow_alias enum. In all other cases where numbers coincide,
// this has been diagnosed elsewhere already.
continue
}
if !ok {
r.SoftError(strict, errJSONConflict{
first: prev, second: field,
})
}
}
if ty.IsEnum() {
// Don't bother iterating again, since enums cannot have custom
// JSON names.
continue
}
clear(names)
// Now do custom names. These are always an error if they conflict.
for field := range seq.Values(ty.Members()) {
option := field.PseudoOptions().JSONName
name, custom := option.AsString()
if custom {
field.Raw().jsonName = file.session.intern.Intern(name)
}
prev, ok := names.AddID(field.Raw().jsonName, field)
if !ok && (custom || !prev.PseudoOptions().JSONName.IsZero()) {
r.Error(errJSONConflict{
first: prev, second: field,
involvesCustomName: true,
})
}
}
}
for extn := range seq.Values(file.AllExtensions()) {
want := internal.JSONName(extn.Name())
option := extn.PseudoOptions().JSONName
got, custom := option.AsString()
name := want
if custom {
name = got
}
extn.Raw().jsonName = file.session.intern.Intern(name)
if custom {
d := r.SoftErrorf(want != got, "%s cannot specify `json_name`", taxa.Extension).Apply(
report.Snippet(option.OptionSpan()),
report.Notef("JSON format for extensions always uses the extension's fully-qualified name"),
)
if want == got {
d.Apply(report.Helpf("protoc erroneously accepts `json_name` on an extension " +
"if it happens to match the default JSON name exactly"))
}
}
}
}
type errJSONConflict struct {
first, second Member
involvesCustomName bool
}
func (e errJSONConflict) Diagnose(d *report.Diagnostic) {
eitherIsCustom := !e.first.PseudoOptions().JSONName.IsZero() ||
!e.second.PseudoOptions().JSONName.IsZero()
if !e.involvesCustomName && eitherIsCustom {
d.Apply(report.Message("%ss have the same (default) JSON name", e.first.noun()))
} else {
d.Apply(report.Message("%ss have the same JSON name", e.first.noun()))
}
snippet := func(m Member) report.DiagnosticOption {
option := m.PseudoOptions().JSONName
if _, custom := option.AsString(); custom {
if e.involvesCustomName {
return report.Snippetf(option.ValueAST(), "`%s` specifies custom name here", m.Name())
}
return report.Snippetf(m.AST().Name(), "this implies (default) JSON name `%s`", m.JSONName())
}
if m == e.second {
return report.Snippetf(m.AST().Name(), "this also implies that name")
}
return report.Snippetf(m.AST().Name(), "this implies JSON name `%s`", m.JSONName())
}
d.Apply(snippet(e.second), snippet(e.first))
if !e.involvesCustomName {
_, firstCustom := e.first.PseudoOptions().JSONName.AsString()
_, secondCustom := e.second.PseudoOptions().JSONName.AsString()
var what string
switch {
case firstCustom && secondCustom:
what = "both fields set"
case firstCustom:
what = fmt.Sprintf("`%s` sets", e.first.Name())
case secondCustom:
what = fmt.Sprintf("`%s` sets", e.second.Name())
default:
return
}
d.Apply(report.Helpf("even though %s `json_name`, their default "+
"JSON names must not conflict, because `google.protobuf.FieldMask`'s "+
"JSON syntax erroneously does not account for custom JSON names", what))
}
}
// 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 ir
import (
"slices"
"sync"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/ir/presence"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
pcinternal "github.com/bufbuild/protocompile/internal"
)
// generateMapEntries generates map entry types for all map-typed fields.
func generateMapEntries(file *File, r *report.Report) {
lowerField := func(field Member) {
// optional, repeated etc. on map types is already legalized in
// the parser.
decl := field.AST().Type().RemovePrefixes().AsGeneric()
if decl.IsZero() {
return
}
key, _ := decl.AsMap()
if key.IsZero() {
return // Legalized in the parser.
}
parent := field.Parent()
base := parent.FullName()
if base == "" {
base = file.Package()
}
name := pcinternal.MapEntry(field.Name())
fqn := base.Append(name)
// Set option map_entry = true;
builtins := file.builtins()
messageOptions := builtins.MessageOptions.toRef(file)
mapEntry := builtins.MapEntry.toRef(file)
options := newMessage(file, builtins.MessageOptions.toRef(file))
options.slot(GetRef(file, mapEntry)).Insert(id.Wrap(
file,
id.ID[Value](file.arenas.values.NewCompressed(rawValue{
field: mapEntry,
bits: 1,
})),
))
// Construct the type itself.
ty := id.Wrap(file, id.ID[Type](file.arenas.types.NewCompressed(rawType{
def: field.AST().ID(),
name: file.session.intern.Intern(name),
fqn: file.session.intern.Intern(string(fqn)),
parent: parent.ID(),
options: id.ID[Value](file.arenas.values.NewCompressed(rawValue{
field: messageOptions,
bits: rawValueBits(file.arenas.messages.Compress(options.Raw())),
})),
mapEntryOf: field.ID(),
})))
ty.Raw().memberByName = sync.OnceValue(ty.makeMembersByName)
if parent.IsZero() {
file.types = slices.Insert(file.types, file.topLevelTypesEnd, ty.ID())
file.topLevelTypesEnd++
} else {
file.types = append(file.types, ty.ID())
parent.Raw().nested = append(parent.Raw().nested, ty.ID())
}
// Construct the fields and attach them to ty.
makeField := func(name string, number int32) {
fqn := fqn.Append(name)
p := id.ID[Member](file.arenas.members.NewCompressed(rawMember{
name: file.session.intern.Intern(name),
fqn: file.session.intern.Intern(string(fqn)),
parent: ty.ID(),
number: number,
oneof: -int32(presence.Explicit),
}))
ty.Raw().members = slices.Insert(ty.Raw().members, int(ty.Raw().extnsStart), p)
ty.Raw().extnsStart++
}
makeField("key", 1)
makeField("value", 2)
// Update the field to be a repeated field of the given type.
field.Raw().elem = ty.toRef(file)
field.Raw().oneof = -int32(presence.Repeated)
}
for parent := range seq.Values(file.AllTypes()) {
if !parent.IsMessage() {
continue
}
for field := range seq.Values(parent.Members()) {
lowerField(field)
}
}
for extn := range seq.Values(file.AllExtensions()) {
k, _ := extn.AST().Type().RemovePrefixes().AsGeneric().AsMap()
if k.IsZero() {
continue
}
r.Errorf("unsupported map-typed extension").Apply(
report.Snippetf(extn.AST().Type(), "declared here"),
report.Helpf("extensions cannot be map-typed; instead, "+
"define a message type with a map-typed field"),
)
lowerField(extn)
}
}
// 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 ir
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// evaluateFieldNumbers evaluates all non-extension field numbers: that is,
// the numbers in reserved ranges and in non-extension field and enum value
// declarations.
func evaluateFieldNumbers(file *File, r *report.Report) {
for ty := range seq.Values(file.AllTypes()) {
if !ty.MapField().IsZero() {
// Map entry types come with numbers pre-calculated.
continue
}
scope := ty.FullName()
var kind memberNumber
switch {
case ty.IsEnum():
kind = enumNumber
case ty.IsMessageSet():
kind = messageSetNumber
default:
kind = fieldNumber
}
for member := range seq.Values(ty.Members()) {
member.Raw().number, member.Raw().numberOk = evaluateMemberNumber(
file, scope, member.AST().Value(), kind, false, r)
}
for tags := range seq.Values(ty.AllRanges()) {
switch tags.AST().Kind() {
case ast.ExprKindRange:
a, b := tags.AST().AsRange().Bounds()
start, startOk := evaluateMemberNumber(file, scope, a, kind, false, r)
end, endOk := evaluateMemberNumber(file, scope, b, kind, true, r)
if !startOk || !endOk {
continue
}
if start > end {
what := taxa.Reserved
if tags.ForExtensions() {
what = taxa.Extensions
}
r.Errorf("empty %s %v %v", what).Apply(
report.Snippet(tags.AST()),
report.Notef("range syntax requires that start <= end"),
)
continue
}
if start == end {
r.Warnf("singleton range can be simplified").Apply(
report.Snippet(tags.AST()),
report.SuggestEdits(tags.AST(), "replace with a single number", report.Edit{
Start: 0, End: tags.AST().Span().Len(),
Replace: a.Span().Text(),
}),
)
}
tags.Raw().first = start
tags.Raw().last = end
tags.Raw().rangeOk = startOk && endOk
default:
n, ok := evaluateMemberNumber(file, scope, tags.AST(), kind, false, r)
if !ok {
continue
}
tags.Raw().first = n
tags.Raw().last = n
tags.Raw().rangeOk = ok
}
}
}
for extn := range seq.Values(file.AllExtensions()) {
var kind memberNumber
switch {
case extn.Container().IsMessageSet():
kind = messageSetNumber
default:
kind = fieldNumber
}
scope := extn.Context().Package()
if ty := extn.Parent(); !ty.IsZero() {
scope = ty.FullName()
}
extn.Raw().number, extn.Raw().numberOk = evaluateMemberNumber(
file, scope, extn.AST().Value(), kind, false, r)
}
}
func evaluateMemberNumber(file *File, scope FullName, number ast.ExprAny, kind memberNumber, allowMax bool, r *report.Report) (int32, bool) {
if number.IsZero() {
return 0, false // Diagnosed for us elsewhere.
}
e := &evaluator{
File: file,
Report: r,
scope: scope,
}
// Don't bother allocating a whole Value for this.
v, ok := e.evalBits(evalArgs{
expr: number,
memberNumber: kind,
allowMax: allowMax,
})
return int32(v), ok
}
// buildFieldNumberRanges builds the field number range table for all types.
//
// This also checks for and diagnoses overlaps.
func buildFieldNumberRanges(file *File, r *report.Report) {
// overlapLimit sets the maximum number of overlapping ranges we tolerate
// before we stop processing overlapping ranges.
//
// Inserting to an [interval.Intersect] is actually O(k log n), where k is
// the number of intersecting intervals. No valid Protobuf file can trigger
// this behavior, but pathological files can, such as the fragment
//
// reserved 1, 1 to 2, 1 to 3, 1 to 4, ...
//
// which forces k = n. This can be mitigated by not processing more ranges
// after we see N overlaps This ensures k = O(N). For example, in the
// example above, we would only have ranges up to 1 to N before stopping,
// and so we get at worst performance O(N^2 log n) = O(log n), because N
// is a constant. Note processing members still proceeds, and is still
// O(n log n) work (because k <= 1 in this case).
//
// This is that N. In this case, we set missingRanges so that extension
// range checks are skipped, to avoid generating incorrect diagnostics (the
// file will already be rejected in this case, so not providing precise
// diagnostics in is fine).
const overlapLimit = 50
// First, dump all of the ranges into the intersection set.
for ty := range seq.Values(file.AllTypes()) {
var totalOverlaps int
for tagRange := range iterx.Chain(
seq.Values(ty.ReservedRanges()),
seq.Values(ty.ExtensionRanges()),
) {
lo, hi := tagRange.Range()
if !tagRange.Raw().rangeOk {
continue // Diagnosed already.
}
disjoint := ty.Raw().rangesByNumber.Insert(lo, hi, rawTagRange{
ptr: arena.Untyped(file.arenas.ranges.Compress(tagRange.Raw())),
})
// Avoid quadratic behavior. See overlapLimit's comment above.
if !disjoint {
totalOverlaps++
if totalOverlaps > overlapLimit {
ty.Raw().missingRanges = true
break
}
}
}
// Members last, so that if an intersection contains only members, the
// first value is a member.
for member := range seq.Values(ty.Members()) {
n := member.Number()
if !member.Raw().numberOk {
continue // Diagnosed already.
}
ty.Raw().rangesByNumber.Insert(n, n, rawTagRange{
isMember: true,
ptr: arena.Untyped(file.arenas.members.Compress(member.Raw())),
})
}
// Now, iterate over every entry and diagnose the ones that have more
// than one value.
for entry := range ty.Raw().rangesByNumber.Entries() {
if len(entry.Value) < 2 {
continue
}
first := TagRange{id.WrapContext(ty.Context()), entry.Value[0]}
if ty.AllowsAlias() && first.raw.isMember {
// If all of the members of the intersections are members,
// we don't diagnose.
continue
}
for _, tags := range entry.Value[1:] {
tags := TagRange{id.WrapContext(ty.Context()), tags}
if a, b := first.AsMember(), tags.AsMember(); ty.AllowsAlias() && !a.IsZero() && !b.IsZero() {
continue
}
r.Error(errOverlap{
ty: ty,
first: first,
second: tags,
})
}
}
}
// Check that every extension has a corresponding extension range.
extensions:
for extn := range seq.Values(file.AllExtensions()) {
n := extn.Number()
if n == 0 {
continue // Diagnosed already.
}
ty := extn.Container()
if ty.IsZero() || !ty.IsMessage() {
continue
}
var first TagRange
for tags := range ty.Ranges(n) {
if first.IsZero() {
first = tags
}
if tags.AsReserved().ForExtensions() {
continue extensions
}
}
var d *report.Diagnostic
if !first.IsZero() {
d = r.Error(errOverlap{
ty: ty,
first: first,
second: extn.AsTagRange(),
})
} else {
// Don't diagnose if we're missing some ranges, because we might
// produce false positives. This can only happen for types that have
// already generated diagnostics, so it's ok to skip diagnosing.
if ty.Raw().missingRanges {
continue
}
d = r.Errorf("extension with unreserved number `%v`", n).Apply(
report.Snippet(extn.AST().Value()),
)
}
d.Apply(report.Helpf(
"the parent message `%s` must have reserved this number with an %s, e.g. `extensions %v;`",
ty.FullName(), taxa.Extensions, extn.Number(),
))
}
// NOTE: Can't do extension number overlap checking yet, because we need
// a global view of all files to do that.
}
type errOverlap struct {
ty Type
first, second TagRange
}
func (e errOverlap) Diagnose(d *report.Diagnostic) {
what := taxa.FieldNumber
if e.ty.IsEnum() {
what = taxa.EnumValue
}
again:
if second := e.second.AsMember(); !second.IsZero() {
if first := e.first.AsMember(); !first.IsZero() {
d.Apply(
report.Message("%v `%v` used more than once", what, second.Number()),
report.Snippetf(second.AST().Value(), "used here"),
report.Snippetf(first.AST().Value(), "previously used here"),
)
} else {
first := e.first.AsReserved()
d.Apply(
report.Message("use of reserved %v `%v`", what, second.Number()),
report.Snippetf(second.AST().Value(), "used here"),
report.Snippetf(first.AST(), "%v reserved here", what),
)
}
} else {
if first := e.first.AsMember(); !first.IsZero() {
e.second, e.first = e.first, e.second
goto again
}
second := e.second.AsReserved()
first := e.first.AsReserved()
lo1, hil := first.Range()
lo2, hi2 := second.Range()
d.Apply(
report.Message("overlapping %v ranges", what),
report.Snippetf(second.AST(), "this range"),
report.Snippetf(first.AST(), "overlaps with this one"),
)
lo1 = max(lo1, lo2)
hil = min(hil, hi2)
if lo1 == hil {
d.Apply(report.Helpf("they overlap at `%v`", lo1))
} else {
d.Apply(report.Helpf("they overlap in the range `%v to %v`", lo1, hil))
}
// TODO: Generate a suggestion to split the range, if both ranges are
// of the same type.
}
}
// 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 ir
import (
"fmt"
"iter"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/intern"
)
// resolveEarlyOptions resolves options whose values must be discovered very
// early during compilation. This does not create option values, nor does it
// generate diagnostics; it simply records this information to special fields
// in [Type].
func resolveEarlyOptions(file *File) {
builtins := &file.session.builtins
for ty := range seq.Values(file.AllTypes()) {
for decl := range seq.Values(ty.AST().Body().Decls()) {
def := decl.AsDef()
if def.IsZero() || def.Classify() != ast.DefKindOption {
continue
}
option := def.AsOption().Option
// If this option's path has more than one component, skip.
first, ok := iterx.OnlyOne(option.Path.Components)
if !ok || !first.Separator().IsZero() {
continue
}
// Resolve the name of this option.
var name intern.ID
if ident := first.AsIdent(); !ident.IsZero() {
switch ident.Text() {
case "message_set_wire_format":
name = builtins.MessageSet
case "allow_alias":
name = builtins.AllowAlias
}
} else if extn := first.AsExtension(); !extn.IsZero() {
sym, _ := file.imported.resolve(
file,
ty.Scope(),
FullName(extn.Canonicalized()),
nil,
nil,
)
name = GetRef(file, sym).AsMember().InternedFullName()
}
// Get the value of this option. We only care about a value of
// "true" for both options.
value := option.Value.AsPath().AsKeyword() == keyword.True
switch name {
case builtins.MessageSet:
ty.Raw().isMessageSet = ty.IsMessage() && value
case builtins.AllowAlias:
ty.Raw().allowsAlias = ty.IsEnum() && value
}
}
}
}
// resolveOptions resolves all of the options in a file.
func resolveOptions(file *File, r *report.Report) {
builtins := file.builtins()
bodyOptions := func(decls seq.Inserter[ast.DeclAny]) iter.Seq[ast.Option] {
return iterx.FilterMap(seq.Values(decls), func(d ast.DeclAny) (ast.Option, bool) {
def := d.AsDef()
if def.IsZero() || def.Classify() != ast.DefKindOption {
return ast.Option{}, false
}
return def.AsOption().Option, true
})
}
for def := range bodyOptions(file.AST().Decls()) {
optionRef{
File: file,
Report: r,
scope: file.Package(),
def: def,
field: builtins.FileOptions,
raw: &file.options,
}.resolve()
}
// Reusable space for duplicating options values between extension ranges.
extnOpts := make(map[ast.DeclRange]id.ID[Value])
for ty := range seq.Values(file.AllTypes()) {
if !ty.MapField().IsZero() {
// Map entries already come with options pre-calculated.
continue
}
for def := range bodyOptions(ty.AST().Body().Decls()) {
options := builtins.MessageOptions
if ty.IsEnum() {
options = builtins.EnumOptions
}
optionRef{
File: file,
Report: r,
scope: ty.Scope(),
def: def,
field: options,
raw: &ty.Raw().options,
}.resolve()
}
for field := range seq.Values(ty.Members()) {
for def := range seq.Values(field.AST().Options().Entries()) {
options := builtins.FieldOptions
if ty.IsEnum() {
options = builtins.EnumValueOptions
}
optionRef{
File: file,
Report: r,
scope: field.Scope(),
def: def,
field: options,
raw: &field.Raw().options,
target: field,
}.resolve()
}
}
for oneof := range seq.Values(ty.Oneofs()) {
for def := range bodyOptions(oneof.AST().Body().Decls()) {
optionRef{
File: file,
Report: r,
scope: ty.Scope(),
def: def,
field: builtins.OneofOptions,
raw: &oneof.Raw().options,
}.resolve()
}
}
clear(extnOpts)
for extns := range seq.Values(ty.ExtensionRanges()) {
decl := extns.DeclAST()
if p := extnOpts[decl]; !p.IsZero() {
extns.Raw().options = p
continue
}
for def := range seq.Values(extns.DeclAST().Options().Entries()) {
optionRef{
File: file,
Report: r,
scope: ty.Scope(),
def: def,
field: builtins.RangeOptions,
raw: &extns.Raw().options,
}.resolve()
}
extnOpts[decl] = extns.Raw().options
}
}
for field := range seq.Values(file.AllExtensions()) {
for def := range seq.Values(field.AST().Options().Entries()) {
optionRef{
File: file,
Report: r,
scope: field.Scope(),
def: def,
field: builtins.FieldOptions,
raw: &field.Raw().options,
target: field,
}.resolve()
}
}
for service := range seq.Values(file.Services()) {
for def := range bodyOptions(service.AST().Body().Decls()) {
optionRef{
File: file,
Report: r,
scope: service.FullName(),
def: def,
field: builtins.ServiceOptions,
raw: &service.Raw().options,
}.resolve()
}
for method := range seq.Values(service.Methods()) {
for def := range bodyOptions(method.AST().Body().Decls()) {
optionRef{
File: file,
Report: r,
scope: service.FullName(),
def: def,
field: builtins.MethodOptions,
raw: &method.Raw().options,
}.resolve()
}
}
}
}
// populateOptionTargets builds option target sets for each field in a file.
func populateOptionTargets(file *File, _ *report.Report) {
targets := file.builtins().OptionTargets
populate := func(m Member) {
for target := range seq.Values(m.Options().Field(targets).Elements()) {
n, _ := target.AsInt()
target := OptionTarget(n)
if target == OptionTargetInvalid || target >= optionTargetMax {
continue
}
m.Raw().optionTargets |= 1 << target
}
}
for ty := range seq.Values(file.AllTypes()) {
if !ty.IsMessage() {
continue
}
for field := range seq.Values(ty.Members()) {
populate(field)
}
}
for extn := range seq.Values(file.AllExtensions()) {
populate(extn)
}
}
func validateOptionTargets(f *File, r *report.Report) {
validateOptionTargetsInValue(f.Options(), source.Span{}, OptionTargetFile, r)
for ty := range seq.Values(f.AllTypes()) {
tyTarget, memberTarget := OptionTargetMessage, OptionTargetField
if ty.IsEnum() {
tyTarget, memberTarget = OptionTargetEnum, OptionTargetEnumValue
}
validateOptionTargetsInValue(ty.Options(), ty.AST().Name().Span(), tyTarget, r)
for member := range seq.Values(ty.Members()) {
validateOptionTargetsInValue(member.Options(), member.AST().Name().Span(), memberTarget, r)
}
for oneof := range seq.Values(ty.Oneofs()) {
validateOptionTargetsInValue(oneof.Options(), oneof.AST().Name().Span(), OptionTargetOneof, r)
}
}
for extn := range seq.Values(f.AllExtensions()) {
validateOptionTargetsInValue(extn.Options(), extn.AST().Name().Span(), OptionTargetField, r)
}
}
func validateOptionTargetsInValue(m MessageValue, decl source.Span, target OptionTarget, r *report.Report) {
if m.IsZero() {
return
}
if c := m.Concrete(); c != m {
validateOptionTargetsInValue(c, decl, target, r)
}
for value := range m.Fields() {
field := value.Field()
if !field.CanTarget(target) {
var nouns taxa.Set
var targets int
for target := range field.Targets() {
switch target {
case OptionTargetFile:
nouns = nouns.With(taxa.TopLevel)
case OptionTargetRange:
nouns = nouns.With(taxa.Extensions)
case OptionTargetMessage:
nouns = nouns.With(taxa.Message)
case OptionTargetEnum:
nouns = nouns.With(taxa.Enum)
case OptionTargetField:
nouns = nouns.With(taxa.Field)
case OptionTargetEnumValue:
nouns = nouns.With(taxa.EnumValue)
case OptionTargetOneof:
nouns = nouns.With(taxa.Oneof)
case OptionTargetService:
nouns = nouns.With(taxa.Service)
case OptionTargetMethod:
nouns = nouns.With(taxa.Method)
}
targets++
}
// Pull out the place where this option was set so we can show it to
// the user.
constraints := field.Options().Field(m.Context().builtins().OptionTargets)
key := value.KeyAST()
span := key.Span()
if path := key.AsPath(); !path.IsZero() {
// Pull out the last component.
// TODO: write a function on Path that does this cheaply.
last, _ := iterx.Last(path.Components)
span = last.Name().Span()
}
d := r.Errorf("unsupported option target for `%s`", field.Name()).Apply(
report.Snippetf(span, "option set here"),
report.Snippetf(decl, "applied to this"),
report.Snippetf(constraints.ValueAST(), "targets constrained here"),
)
if targets == 1 {
d.Apply(report.Helpf(
"`%s` is constrained to %ss",
field.FullName(),
nouns.Join("or")))
} else {
d.Apply(report.Helpf(
"`%s` is constrained to one of %s",
field.FullName(),
nouns.Join("or")))
}
continue // Don't recurse and generate a mess of diagnostics.
}
validateOptionTargetsInValue(value.AsMessage(), decl, target, r)
}
}
// optionRef is all of the information necessary to resolve an option reference.
type optionRef struct {
*File
*report.Report
scope FullName
def ast.Option
field Member
raw *id.ID[Value]
// A member being annotated. This is used for pseudo-option resolution.
target Member
}
// resolve performs symbol resolution.
func (r optionRef) resolve() {
ids := &r.session.builtins
root := r.field.Element()
if r.raw.IsZero() {
*r.raw = newMessage(r.File, r.field.toRef(r.File)).AsValue().ID()
}
current := id.Wrap(r.File, *r.raw)
field := current.Field()
var path ast.Path
var raw slot
for pc := range r.def.Path.Components {
// If this is the first iteration, use the *Options value as the current
// message.
message := field.Element()
if message.IsZero() {
message = root
}
// Calculate the corresponding member for this path component, which may
// be either a simple path or an extension name.
prev := field
pseudo := pc.IsFirst() &&
r.field.InternedFullName() == ids.FieldOptions &&
pc.AsIdent().Keyword().IsPseudoOption()
if pseudo {
// Check if this is a pseudo-option.
m := current.AsMessage()
switch pc.AsIdent().Keyword() {
case keyword.Default:
field = r.target
raw = slot{m, &m.Raw().pseudo.defaultValue}
case keyword.JsonName:
field = r.builtins().JSONName
raw = slot{m, ¤t.AsMessage().Raw().pseudo.jsonName}
}
} else if extn := pc.AsExtension(); !extn.IsZero() {
sym := symbolRef{
File: r.File,
Report: r.Report,
span: extn,
scope: r.scope,
name: FullName(extn.Canonicalized()),
accept: SymbolKind.IsMessageField,
want: taxa.Extension,
allowScalars: false,
allowOption: true,
suggestImport: true,
}.resolve()
if !sym.Kind().IsMessageField() {
// Already diagnosed by resolve().
return
}
field = sym.AsMember()
if field.Container() != message {
d := r.Errorf("expected `%s` extension, found %s in `%s`",
message.FullName(), field.noun(), field.Container().FullName(),
).Apply(
report.Snippetf(pc, "because of this %s", taxa.FieldSelector),
report.Snippetf(field.AST().Name(), "`%s` defined here", field.FullName()),
)
if field.IsExtension() {
d.Apply(report.Snippetf(field.Extend().AST(), "... within this %s", taxa.Extend))
} else {
d.Apply(report.Snippetf(field.Container().AST(), "... within this %s", taxa.Message))
}
return
}
if !field.IsExtension() {
// Protoc accepts this! The horror!
r.Warnf("redundant %s syntax", taxa.CustomOption).Apply(
report.Snippetf(pc, "this field is not a %s", taxa.Extension),
report.Snippetf(field.AST().Name(), "field declared inside of `%s` here", field.Parent().FullName()),
report.Helpf("%s syntax should only be used with %ss", taxa.CustomOption, taxa.Extension),
report.SuggestEdits(pc.Name(), "replace `(...)` with a field name", report.Edit{
Start: 0, End: pc.Name().Span().Len(),
Replace: field.Name(),
}),
)
}
} else if ident := pc.AsIdent(); !ident.IsZero() {
field = message.MemberByName(ident.Text())
if field.IsZero() {
d := r.Errorf("cannot find %s `%s` in `%s`", taxa.Field, ident.Text(), message.FullName()).Apply(
report.Snippetf(pc, "because of this %s", taxa.FieldSelector),
)
if !pc.IsFirst() {
d.Apply(report.Snippetf(prev.AST().Type(), "`%s` specified here", message.FullName()))
}
return
}
}
if pc.IsFirst() {
switch field.InternedFullName() {
case ids.MapEntry:
r.Errorf("`map_entry` cannot be set explicitly").Apply(
report.Snippet(pc),
report.Helpf("`map_entry` is set automatically for synthetic map "+
"entry types, and cannot be set with an %s", taxa.Option),
)
case ids.FileUninterpreted,
ids.MessageUninterpreted, ids.FieldUninterpreted, ids.OneofUninterpreted, ids.RangeUninterpreted,
ids.EnumUninterpreted, ids.EnumValueUninterpreted,
ids.MethodUninterpreted, ids.ServiceUninterpreted:
r.Errorf("`uninterpreted_option` cannot be set explicitly").Apply(
report.Snippet(pc),
report.Helpf("`uninterpreted_option` is an implementation detail of protoc"),
)
case ids.FileFeatures,
ids.MessageFeatures, ids.FieldFeatures, ids.OneofFeatures,
ids.EnumFeatures, ids.EnumValueFeatures:
if syn := r.Syntax(); !syn.IsEdition() {
r.Errorf("`features` cannot be set in %s", syn.Name()).Apply(
report.Snippet(pc),
report.Snippetf(r.AST().Syntax().Value(), "syntax specified here"),
)
}
}
}
path, _ = pc.SplitAfter()
// Check to see if this value has already been set in the parent message.
// We have already validated current as a singular message by this point.
parent := current.AsMessage()
// Check if this field is already set. The only cases where this is
// allowed is if:
//
// 1. The current field is repeated and this is the last component.
// 2. The current field is of message type and this is not the last
// component.
if !pseudo {
raw = parent.slot(field)
}
if !raw.IsZero() {
value := raw.Value()
switch {
case field.IsRepeated():
break // Handled below.
case value.Field() != field:
// A different member of a oneof was set.
r.Error(errSetMultipleTimes{
member: field.Oneof(),
first: value.OptionPaths().At(0),
second: path,
root: pc.IsFirst(),
})
return
case prev.Element().IsMessage():
if !pc.IsLast() {
current = value
continue
}
fallthrough
default:
r.Error(errSetMultipleTimes{
member: field,
first: value.OptionPaths().At(0),
second: path,
root: pc.IsFirst(),
})
return
}
}
if pc.IsLast() {
break
}
// Handle a non-final component in an option path. That must be
// a singular message value, which the successive elements of the
// path index into as field names.
message = field.Element()
// This diagnoses that people do not write option a.b.c where b is
// a repeated field.
if field.IsRepeated() {
r.Error(errOptionMustBeMessage{
selector: pc.Next().Name(),
prev: pc.Name(),
got: "repeated",
gotName: message.FullName(),
spec: field.TypeAST(),
})
return
}
// This diagnoses that people do not write option a.b.c where b is
// not a message field.
if !message.IsZero() && !message.IsMessage() {
r.Error(errOptionMustBeMessage{
selector: pc.Next().Name(),
prev: pc.Name(),
got: message.noun(),
gotName: message.FullName(),
spec: field.TypeAST().RemovePrefixes(),
})
return
}
value := newMessage(r.File, field.toRef(r.File)).AsValue()
value.Raw().optionPaths = append(value.Raw().optionPaths, path.ID())
value.Raw().exprs = append(value.Raw().exprs, ast.ExprPath{Path: path}.AsAny().ID())
raw.Insert(value)
current = value
}
// Now, evaluate the expression and assign it to the field we found.
evaluator := evaluator{
File: r.File,
Report: r.Report,
scope: r.scope,
}
args := evalArgs{
expr: r.def.Value,
field: field,
annotation: field.AST().Type(),
optionPath: path,
}
if !raw.IsZero() {
args.target = raw.Value()
}
v := evaluator.eval(args)
if raw.IsZero() && !v.IsZero() {
raw.Insert(v)
}
}
type errSetMultipleTimes struct {
member any
first, second source.Spanner
root bool
}
func (e errSetMultipleTimes) Diagnose(d *report.Diagnostic) {
var what any
var name FullName
var note string
var def source.Spanner
switch member := e.member.(type) {
case Member:
if !member.IsExtension() && e.root {
// For non-custom options, use the short name and call it
// an "option".
name = FullName(member.Name())
what = "option"
} else {
name = member.FullName()
what = member.noun()
}
note = "a non-`repeated` option may be set at most once"
def = member.AST().Name()
case Oneof:
name = member.FullName()
what = "oneof"
note = "at most one member of a oneof may be set by an option"
def = member.AST().Name()
default:
panic("unreachable")
}
d.Apply(
report.Message("%v `%v` set multiple times", what, name),
report.Snippetf(e.second, "... also set here"),
report.Snippetf(e.first, "first set here..."),
report.Snippetf(def, "not a repeated field"),
report.Notef(note),
)
}
type errOptionMustBeMessage struct {
selector, prev, spec source.Spanner
got, gotName any
}
func (e errOptionMustBeMessage) Diagnose(d *report.Diagnostic) {
got := e.got
if e.gotName != nil {
got = fmt.Sprintf("%v `%v`", got, e.gotName)
}
d.Apply(
report.Message("expected singular message, found %s", got),
report.Snippetf(e.selector, "%s requires singular message", taxa.FieldSelector),
report.Snippetf(e.prev, "found %s", got),
)
if e.spec != nil {
d.Apply(report.Snippetf(e.spec, "type specified here"))
}
}
// 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 ir
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/ir/presence"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/report/tags"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// resolveNames resolves all of the names that need resolving in a file.
func resolveNames(file *File, r *report.Report) {
resolveBuiltins(file)
for ty := range seq.Values(file.AllTypes()) {
if ty.IsMessage() {
var names syntheticNames
for field := range seq.Values(ty.Members()) {
resolveFieldType(field, r)
// For proto3 sources, we need to resolve the synthetic oneof names for fields with
// explicit optional presence. See the docs for [Member.SyntheticOneofName] for details.
if file.syntax == syntax.Proto3 && field.Presence() == presence.Explicit {
if !field.Oneof().IsZero() {
continue
}
field.Raw().syntheticOneofName = file.session.intern.Intern(
names.generate(field.Name(), field.Parent()),
)
}
}
}
}
for extend := range seq.Values(file.AllExtends()) {
resolveExtendeeType(extend, r)
}
for field := range seq.Values(file.AllExtensions()) {
resolveFieldType(field, r)
}
for service := range seq.Values(file.Services()) {
for method := range seq.Values(service.Methods()) {
resolveMethodTypes(method, r)
}
}
}
// resolveFieldType fully resolves the type of a field (extension or otherwise).
func resolveFieldType(field Member, r *report.Report) {
ty := field.TypeAST()
var path ast.Path
kind := presence.Explicit
switch ty.Kind() {
case ast.TypeKindPath:
if field.Context().Syntax() == syntax.Proto3 {
kind = presence.Implicit
}
// NOTE: Editions features are resolved elsewhere, so we default to
// explicit presence here.
path = ty.AsPath().Path
case ast.TypeKindPrefixed:
switch ty.AsPrefixed().Prefix() {
case keyword.Optional:
kind = presence.Explicit
case keyword.Required:
kind = presence.Required
case keyword.Repeated:
kind = presence.Repeated
}
// Unwrap as many prefixed fields as necessary to get to the bottom
// of this.
ty = ty.RemovePrefixes()
if p := ty.AsPath().Path; !p.IsZero() {
path = p
break
}
fallthrough
case ast.TypeKindGeneric:
// Resolved elsewhere.
return
}
if path.IsZero() {
// Enum value; this is legalized elsewhere.
return
}
if field.Raw().oneof < 0 {
field.Raw().oneof = -int32(kind)
}
sym := symbolRef{
File: field.Context(),
Report: r,
span: path,
scope: field.Scope(),
name: FullName(path.Canonicalized()),
skipIfNot: SymbolKind.IsType,
accept: SymbolKind.IsType,
want: taxa.Type,
allowScalars: true,
suggestImport: true,
}.resolve()
if sym.Kind().IsType() {
ty := sym.AsType()
field.Raw().elem = ty.toRef(field.Context())
if mf := sym.AsType().MapField(); !mf.IsZero() {
r.Errorf("use of synthetic map entry type").Apply(
report.Snippetf(path, "referenced here"),
report.Snippetf(mf.TypeAST(), "synthesized by this type"),
report.Helpf("despite having a user-visible symbol, map entry "+
"types cannot be used as field types"),
)
}
if !field.Container().MapField().IsZero() && field.Number() == 1 {
// Legalize that the key type must be comparable.
ty := sym.AsType()
if !ty.Predeclared().IsMapKey() {
d := r.Error(errTypeConstraint{
want: "map key type",
got: sym.AsType(),
decl: field.TypeAST(),
}).Apply(
report.Helpf("valid map key types are integer types, `string`, and `bool`"),
)
if ty.IsEnum() {
d.Apply(report.Helpf(
"counterintuitively, user-defined enum types " +
"cannot be used as keys"))
}
}
}
}
}
func resolveExtendeeType(extend Extend, r *report.Report) {
path := extend.AST().Name()
sym := symbolRef{
File: extend.Context(),
Report: r,
span: path,
scope: extend.Scope(),
name: FullName(path.Canonicalized()),
accept: func(k SymbolKind) bool { return k == SymbolKindMessage },
want: taxa.MessageType,
allowScalars: true,
suggestImport: true,
}.resolve()
if sym.Kind().IsType() {
extend.Raw().ty = sym.AsType().toRef(extend.Context())
}
}
func resolveMethodTypes(m Method, r *report.Report) {
resolve := func(ty ast.TypeAny) (out Ref[Type], stream bool) {
var path ast.Path
for path.IsZero() {
switch ty.Kind() {
case ast.TypeKindPath:
path = ty.AsPath().Path
case ast.TypeKindPrefixed:
prefixed := ty.AsPrefixed()
if prefixed.Prefix() == keyword.Stream {
stream = true
}
ty = prefixed.Type()
default:
// This is already diagnosed in the parser for us.
return out, stream
}
}
sym := symbolRef{
File: m.Context(),
Report: r,
span: path,
scope: m.Service().FullName(),
name: FullName(path.Canonicalized()),
accept: func(k SymbolKind) bool { return k == SymbolKindMessage },
want: taxa.MessageType,
allowScalars: true,
suggestImport: true,
}.resolve()
if sym.Kind().IsType() {
out = sym.AsType().toRef(m.Context())
}
return out, stream
}
signature := m.AST().Signature()
if signature.Inputs().Len() > 0 {
m.Raw().input, m.Raw().inputStream = resolve(m.AST().Signature().Inputs().At(0))
}
if signature.Outputs().Len() > 0 {
m.Raw().output, m.Raw().outputStream = resolve(m.AST().Signature().Outputs().At(0))
}
}
// symbolRef is all of the information necessary to resolve a symbol reference.
type symbolRef struct {
*File
*report.Report
scope, name FullName
span source.Spanner
skipIfNot, accept func(SymbolKind) bool
want taxa.Noun
// If true, the names of scalars will be resolved as potential symbols.
allowScalars bool
// If true, diagnostics will not suggest adding an import.
suggestImport bool
// Allow pulling in symbols via import option.
allowOption bool
}
// resolve performs symbol resolution.
func (r symbolRef) resolve() Symbol {
var (
found Ref[Symbol]
expected FullName
)
var fullResolve bool
switch {
case r.name.Absolute():
if id, ok := r.session.intern.Query(string(r.name.ToRelative())); ok {
found = r.imported.lookup(r.File, id)
}
case r.allowScalars:
// TODO: if symbol resolution would provide a different answer for
// looking up this primitive, we should consider diagnosing it. We don't
// currently because:
//
// 1. Diagnosing every use would be extremely noisy.
//
// 2. Diagnosing only the first might be a false positive, which would
// make this warning user-hostile.
prim := predeclared.Lookup(string(r.name))
if prim.IsScalar() {
sym := GetRef(r.File, Ref[Symbol]{
file: -1,
id: id.ID[Symbol](prim),
})
r.diagnoseLookup(sym, expected)
return sym
}
fallthrough
default:
fullResolve = true
found, expected = r.imported.resolve(r.File, r.scope, r.name, r.skipIfNot, nil)
}
sym := GetRef(r.File, found)
if r.Report != nil {
d := r.diagnoseLookup(sym, expected)
if fullResolve && d != nil {
// Resolve a second time to add debugging information to the diagnostic.
r.imported.resolve(r.File, r.scope, r.name, r.skipIfNot, d)
}
}
return sym
}
// diagnoseLookup generates diagnostics for a possibly-failed symbol resolution
// operation.
func (r symbolRef) diagnoseLookup(sym Symbol, expectedName FullName) *report.Diagnostic {
if sym.IsZero() {
return r.Errorf("cannot find `%s` in this scope", r.name).Apply(
report.Tag(tags.UnknownSymbol),
report.Snippetf(r.span, "not found in this scope"),
report.Helpf("the full name of this scope is `%s`", r.scope),
)
}
if k := sym.Kind(); r.accept != nil && !r.accept(k) {
return r.Errorf("expected %s, found %s `%s`", r.want, k.noun(), sym.FullName()).Apply(
report.Snippetf(r.span, "expected %s", r.want),
report.Snippetf(sym.Definition(), "defined here"),
)
}
switch {
case expectedName != "":
// Complain if we found the "wrong" type.
return r.Errorf("cannot find `%s` in this scope", r.name).Apply(
report.Tag(tags.UnknownSymbol),
report.Snippetf(r.span, "not found in this scope"),
report.Snippetf(sym.Definition(),
"found possibly related symbol `%s`", sym.FullName()),
report.Notef(
"Protobuf's name lookup rules expected a symbol `%s`, "+
"rather than the one we found",
expectedName),
)
case !sym.Visible(r.File, r.allowOption):
if !r.allowOption && sym.Visible(r.File, true) {
decl := sym.Import(r.File).Decl
var option token.Token
for m := range seq.Values(decl.ModifierTokens()) {
if m.Keyword() == keyword.Option {
option = m
}
}
span := source.Join(decl.KeywordToken(), option)
// This symbol is only visible in option position.
return r.Errorf("`%s` is only imported for use in options", r.name).Apply(
report.Snippetf(r.span, "requires non-`option` import"),
report.Snippetf(decl, "imported as `option` here"),
report.SuggestEdits(span, "delete `option`", report.Edit{
Start: 0, End: span.Len(),
Replace: "import",
}),
)
}
// Check to see if the corresponding import is visible. If it is, that
// means that this is an unexported type.
if imp := sym.Import(r.File); imp.Visible {
if ty := sym.AsType(); !ty.IsZero() {
d := r.Errorf("found unexported %s `%s`", ty.noun(), ty.FullName()).Apply(
report.Snippetf(r.span, "unexported type"),
)
// First, see if local was set explicitly.
var local token.Token
for prefix := range ty.AST().Type().Prefixes() {
if prefix.Prefix() == keyword.Local {
local = prefix.PrefixToken()
break
}
}
if !local.IsZero() {
d.Apply(report.Snippetf(local, "marked as local here"))
} else {
var span source.Span
// Otherwise, see if this was set due to a feature.
if key := ty.Context().builtins().FeatureVisibility; !key.IsZero() {
feature := ty.FeatureSet().Lookup(key)
if !feature.IsDefault() {
span = feature.Value().ValueAST().Span()
} else {
span = ty.Context().AST().Syntax().Value().Span()
}
}
d.Apply(report.Snippetf(span, "this implies `local`"))
}
return d
}
}
// Complain that we need to import a symbol.
d := r.Errorf("cannot find `%s` in this scope", r.name).Apply(
report.Snippetf(r.span, "not visible in this scope"),
report.Snippetf(sym.Definition(), "found in unimported file"),
)
if !r.suggestImport {
return d
}
// Find the last import statement and stick the suggestion after it.
decls := sym.Context().AST().Decls()
_, _, imp := iterx.Find2(seq.Backward(decls), func(_ int, d ast.DeclAny) bool {
return d.Kind() == ast.DeclKindImport
})
var offset int
if !imp.IsZero() {
offset = imp.Span().End
}
replacement := fmt.Sprintf("\nimport %q;", sym.Context().Path())
if offset == 0 {
replacement = replacement[1:] + "\n"
}
d.Apply(report.SuggestEdits(
imp.Span().File.Span(offset, offset),
fmt.Sprintf("bring `%s` into scope", r.name),
report.Edit{Replace: replacement},
))
return d
}
return nil
}
// 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 ir
import (
"fmt"
"slices"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/intern"
)
// buildLocalSymbols allocates new symbols for each definition in this file,
// and places them in the local symbol table.
func buildLocalSymbols(file *File) {
sym := file.arenas.symbols.NewCompressed(rawSymbol{
kind: SymbolKindPackage,
fqn: file.InternedPackage(),
})
file.exported = append(file.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
for ty := range seq.Values(file.AllTypes()) {
newTypeSymbol(ty)
for f := range seq.Values(ty.Members()) {
newFieldSymbol(f)
}
for f := range seq.Values(ty.Extensions()) {
newFieldSymbol(f)
}
for o := range seq.Values(ty.Oneofs()) {
newOneofSymbol(o)
}
}
for f := range seq.Values(file.Extensions()) {
newFieldSymbol(f)
}
for s := range seq.Values(file.Services()) {
newServiceSymbol(s)
for m := range seq.Values(s.Methods()) {
newMethodSymbol(m)
}
}
file.exported.sort(file)
}
func newTypeSymbol(ty Type) {
c := ty.Context()
kind := SymbolKindMessage
if ty.IsEnum() {
kind = SymbolKindEnum
}
sym := c.arenas.symbols.NewCompressed(rawSymbol{
kind: kind,
fqn: ty.InternedFullName(),
data: arena.Untyped(c.arenas.types.Compress(ty.Raw())),
})
c.exported = append(c.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
}
func newFieldSymbol(f Member) {
c := f.Context()
kind := SymbolKindField
if !f.Extend().IsZero() {
kind = SymbolKindExtension
} else if f.AST().Classify() == ast.DefKindEnumValue {
kind = SymbolKindEnumValue
}
sym := c.arenas.symbols.NewCompressed(rawSymbol{
kind: kind,
fqn: f.InternedFullName(),
data: arena.Untyped(c.arenas.members.Compress(f.Raw())),
})
c.exported = append(c.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
}
func newOneofSymbol(o Oneof) {
c := o.Context()
sym := c.arenas.symbols.NewCompressed(rawSymbol{
kind: SymbolKindOneof,
fqn: o.InternedFullName(),
data: arena.Untyped(c.arenas.oneofs.Compress(o.Raw())),
})
c.exported = append(c.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
}
func newServiceSymbol(s Service) {
c := s.Context()
sym := c.arenas.symbols.NewCompressed(rawSymbol{
kind: SymbolKindService,
fqn: s.InternedFullName(),
data: arena.Untyped(c.arenas.services.Compress(s.Raw())),
})
c.exported = append(c.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
}
func newMethodSymbol(m Method) {
c := m.Context()
sym := c.arenas.symbols.NewCompressed(rawSymbol{
kind: SymbolKindMethod,
fqn: m.InternedFullName(),
data: arena.Untyped(c.arenas.methods.Compress(m.Raw())),
})
c.exported = append(c.exported, Ref[Symbol]{id: id.ID[Symbol](sym)})
}
// mergeImportedSymbolTables builds a symbol table of every imported symbol.
//
// It also enhances the exported symbol table with the exported symbols of each
// public import.
func mergeImportedSymbolTables(file *File, r *report.Report) {
imports := file.Imports()
var havePublic bool
for sym := range seq.Values(imports) {
if sym.Public {
havePublic = true
break
}
}
// Form the exported symbol table from the public imports. Not necessary
// if there are no public imports.
if havePublic {
file.exported = symtabMerge(
file,
iterx.Chain(
iterx.Of(file.exported),
seq.Map(imports, func(i Import) symtab {
if !i.Public {
// Return an empty symbol table so that the table to
// context mapping can still be an array index.
return symtab{}
}
return i.exported
}),
),
func(i int) *File {
if i == 0 {
return file
}
return file.imports.files[i-1].file
},
)
}
// Form the imported symbol table from the exports list by adding all of
// the non-public imports.
file.imported = symtabMerge(
file,
iterx.Chain(
iterx.Of(file.exported),
seq.Map(imports, func(i Import) symtab {
if i.Public {
// Already processed in the loop above.
return symtab{}
}
return i.exported
}),
),
func(i int) *File {
if i == 0 {
return file
}
return file.imports.files[i-1].file
},
)
dedupSymbols(file, &file.exported, nil)
dedupSymbols(file, &file.imported, r)
}
// dedupSymbols diagnoses duplicate symbols in a sorted symbol table, and
// deletes the duplicates.
//
// Which duplicate is chosen for deletion is deterministic: ties are broken
// according to file names and span starts, in that order. This avoids
// non-determinism around how intern IDs are assigned to names.
func dedupSymbols(file *File, symbols *symtab, r *report.Report) {
*symbols = slicesx.DedupKey(
*symbols,
func(r Ref[Symbol]) intern.ID { return GetRef(file, r).InternedFullName() },
func(refs []Ref[Symbol]) Ref[Symbol] {
if len(refs) == 1 {
return refs[0]
}
slices.SortFunc(refs, cmpx.Map(
func(r Ref[Symbol]) Symbol { return GetRef(file, r) },
cmpx.Key(Symbol.Kind), // Packages sort first, reserved names sort last.
cmpx.Key(func(s Symbol) string {
// NOTE: we do not choose a winner based on the path's intern
// ID, because that is non-deterministic!
return s.Context().Path()
}),
// Break ties with whichever came first in the file.
cmpx.Key(func(s Symbol) int { return s.Definition().Start }),
))
types := mapsx.CollectSet(iterx.FilterMap(slices.Values(refs), func(r Ref[Symbol]) (ast.DeclDef, bool) {
s := GetRef(file, r)
ty := s.AsType()
return ty.AST(), !ty.IsZero()
}))
isFirst := true
refs = slices.DeleteFunc(refs, func(r Ref[Symbol]) bool {
s := GetRef(file, r)
if !isFirst && !s.AsMember().Container().MapField().IsZero() {
// Ignore all symbols that are map entry fields, because those
// can only be duplicated when two map entry messages' names
// collide, so diagnosing them just creates a mess.
return true
}
if !isFirst && s.AsMember().IsGroup() && mapsx.Contains(types, s.AsMember().AST()) {
// If a group field collides with its own message type, remove it;
// groups with names that might collide with their fields are already
// diagnosed in the parser.
return true
}
if !isFirst && s.Kind() == SymbolKindPackage {
// Ignore all refs that are packages except for the first one. This
// is because a package can be defined in multiple files.
return true
}
isFirst = false
return false
})
// Deduplicate references to the same element.
refs = slicesx.Dedup(refs)
if len(refs) > 1 && r != nil {
r.Error(errDuplicates{file, refs})
}
return refs[0]
},
)
}
// errDuplicates diagnoses duplicate symbols.
type errDuplicates struct {
*File
refs []Ref[Symbol]
}
func (e errDuplicates) symbol(n int) Symbol {
return GetRef(e.File, e.refs[n])
}
func (e errDuplicates) Diagnose(d *report.Diagnostic) {
var havePkg bool
for i := range e.refs {
if e.symbol(i).Kind() == SymbolKindPackage {
havePkg = true
break
}
}
first, second := e.symbol(0), e.symbol(1)
name := first.FullName()
if !havePkg {
name = FullName(name.Name())
}
var inParent string
if !havePkg && name.Parent() != "" {
inParent = fmt.Sprintf(" in `%s`", name.Parent())
}
// TODO: In the diagnostic construction code below, we can wind up
// saying nonsense like "a enum". Currently, we chose the article
// based on whether the noun starts with an e, but this is really
// icky.
article := func(n taxa.Noun) string {
if n.String()[0] == 'e' {
return "an"
}
return "a"
}
noun := first.Kind().noun()
d.Apply(
report.Message("`%s` declared multiple times%s", name, inParent),
report.Snippetf(first.Definition(),
"first here, as %s %s",
article(noun), noun),
)
if next := second.Kind().noun(); next != noun {
d.Apply(report.Snippetf(second.Definition(),
"...also declared here, now as %s %s", article(next), next))
noun = next
} else {
d.Apply(report.Snippetf(second.Definition(),
"...also declared here"))
}
spans := make(map[source.Span]struct{})
for i := range e.refs[2:] {
s := e.symbol(i + 2)
next := s.Kind().noun()
span := s.Definition()
if !mapsx.AddZero(spans, span) && i > 1 {
// Avoid overlapping spans.
continue
}
if noun != next {
d.Apply(report.Snippetf(span,
"...and then here as a %s %s", article(next), next))
noun = next
} else {
d.Apply(report.Snippetf(span, "...and here"))
}
}
// If at least one duplicated symbol is non-visible, explain
// that symbol names are global!
for i := range e.refs {
s := e.symbol(i)
if s.Visible(e.File, true) {
continue
}
d.Apply(report.Helpf(
"symbol names must be unique across all transitive imports; "+
"for example, %q declares `%s` but is not directly imported",
s.Context().Path(),
first.FullName(),
))
break
}
// If at least one of them was an enum value, we note the weird language
// bug with enum scoping.
for i := range e.refs {
s := e.symbol(i)
v := s.AsMember()
if !v.Container().IsEnum() {
continue
}
enum := v.Container()
// Avoid unreasonably-nested names where reasonable.
parentName := enum.FullName().Parent()
if parent := enum.Parent(); !parent.IsZero() {
parentName = FullName(parent.Name())
}
d.Apply(report.Helpf(
"the fully-qualified names of enum values do not include the name of the enum; "+
"`%[3]s` defined inside of enum `%[1]s.%[2]s` has the name `%[1]s.%[3]s`, "+
"not `%[1]s.%[2]s.%[3]s`",
parentName,
enum.Name(),
v.Name(),
))
}
for i := range e.refs {
ty := e.symbol(i).AsType()
if mf := ty.MapField(); !mf.IsZero() {
d.Apply(
report.Snippetf(mf.AST().Name(), "implies `repeated %s`", ty.Name()),
report.Helpf(
"map-typed fields implicitly declare a nested message type: "+
"field `%s` produces a map entry type `%s`",
mf.Name(), ty.Name(),
),
)
break
}
}
}
// 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 ir
import (
"fmt"
"path"
"regexp"
"slices"
"strings"
"unicode"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/erredition"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/ir/presence"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/report/tags"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
var asciiIdent = regexp.MustCompile(`^[a-zA-Z_][0-9a-zA-Z_]*$`)
// diagnoseUnusedImports generates diagnostics for each unused import.
func diagnoseUnusedImports(f *File, r *report.Report) {
for imp := range seq.Values(f.Imports()) {
if imp.Used {
continue
}
r.Warnf("unused import %s", imp.Decl.ImportPath().AsLiteral().Text()).Apply(
report.Snippet(imp.Decl.ImportPath()),
report.SuggestEdits(imp.Decl, "delete it", report.Edit{
Start: 0, End: imp.Decl.Span().Len(),
}),
report.Helpf("no symbols from this file are referenced"),
report.Tag(tags.UnusedImport),
)
}
}
// validateConstraints validates miscellaneous constraints that depend on the
// whole IR being constructed properly.
func validateConstraints(f *File, r *report.Report) {
validateFileOptions(f, r)
validateNamingStyle(f, r)
for ty := range seq.Values(f.AllTypes()) {
validateReservedNames(ty, r)
validateVisibility(ty, r)
switch {
case ty.IsEnum():
validateEnum(ty, r)
case ty.IsMessageSet():
validateMessageSet(ty, r)
validateExtensionDeclarations(ty, r)
case ty.IsMessage():
for oneof := range seq.Values(ty.Oneofs()) {
validateOneof(oneof, r)
}
validateExtensionDeclarations(ty, r)
}
for rr := range seq.Values(ty.ExtensionRanges()) {
validateExtensionRange(rr, r)
}
}
for m := range f.AllMembers() {
// https://protobuf.com/docs/language-spec#field-option-validation
validatePacked(m, r)
validateCType(m, r)
validateLazy(m, r)
validateJSType(m, r)
validateDefault(m, r)
validatePresence(m, r)
validateUTF8(m, r)
validateMessageEncoding(m, r)
// NOTE: extensions already cannot be map fields, so we don't need to
// validate them.
if m.IsExtension() && !m.IsMap() {
extendee := m.Container()
if extendee.IsMessageSet() {
validateMessageSetExtension(m, r)
}
validateDeclaredExtension(m, r)
}
}
i := 0
for p := range f.arenas.messages.Values() {
i++
m := id.WrapRaw(f, id.ID[MessageValue](i), p)
for v := range m.Fields() {
// This is a simple way of picking up all of the option values
// without tripping over custom defaults, which we explicitly should
// *not* validate.
validateUTF8Values(v, r)
}
}
for e := range seq.Values(f.AllExtends()) {
validateExtend(e, r)
}
}
func validateEnum(ty Type, r *report.Report) {
builtins := ty.Context().builtins()
if ty.Members().Len() == 0 {
r.Errorf("%s must define at least one value", taxa.EnumType).Apply(
report.Snippet(ty.AST()),
)
return
}
// Check if allow_alias is actually used. This does not happen in
// lower_numbers.go because we want to be able to include the allow_alias
// option span in the diagnostic.
if ty.AllowsAlias() {
// Check to see if there are at least two enum values with the same
// number.
var hasAlias bool
numbers := make(map[int32]struct{})
for member := range seq.Values(ty.Members()) {
if !mapsx.AddZero(numbers, member.Number()) {
hasAlias = true
break
}
}
if !hasAlias {
option := ty.Options().Field(builtins.AllowAlias)
r.Errorf("`%s` requires at least one aliasing %s", option.Field().Name(), taxa.EnumValue).Apply(
report.Snippet(option.OptionSpan()),
)
}
}
first := ty.Members().At(0)
if first.Number() != 0 && !ty.IsClosedEnum() {
// Figure out why this enum is open.
feature := ty.FeatureSet().Lookup(builtins.FeatureEnum)
why := feature.Value().ValueAST().Span()
if feature.IsDefault() {
why = ty.Context().AST().Syntax().Value().Span()
}
r.Errorf("first value of open enum must be zero").Apply(
report.Snippet(first.AST().Value()),
report.PageBreak,
report.Snippetf(why, "this makes `%s` an open enum", ty.FullName()),
report.Helpf("open enums must define a zero value, and it must be the first one"),
)
}
}
func validateFileOptions(f *File, r *report.Report) {
builtins := f.builtins()
// https://protobuf.com/docs/language-spec#option-validation
javaUTF8 := f.Options().Field(builtins.JavaUTF8)
if !javaUTF8.IsZero() && f.Syntax().IsEdition() {
want := "DEFAULT"
if b, _ := javaUTF8.AsBool(); b {
want = "VERIFY"
}
r.Errorf("cannot set `%s` in %s", javaUTF8.Field().Name(), taxa.EditionMode).Apply(
report.Snippet(javaUTF8.KeyAST()),
javaUTF8.suggestEdit("features.(pb.java).utf8_validation", want, "replace with `features.(pb.java).utf8_validation`"),
)
}
javaMultipleFiles := f.Options().Field(builtins.JavaMultipleFiles)
if !javaMultipleFiles.IsZero() && f.Syntax() >= syntax.Edition2024 {
want := "YES"
if b, _ := javaMultipleFiles.AsBool(); !b {
want = "NO"
}
r.Error(erredition.TooNew{
Current: f.Syntax(),
Decl: f.AST().Syntax(),
Deprecated: syntax.Edition2023,
Removed: syntax.Edition2024,
RemovedReason: "`java_multiple_files` has been replaced with `features.(pb.java).nest_in_file_class`",
What: javaMultipleFiles.Field().Name(),
Where: javaMultipleFiles.KeyAST(),
}).Apply(javaMultipleFiles.suggestEdit("features.(pb.java).nest_in_file_class", want, "replace with `features.(pb.java).nest_in_file_class`"))
}
optimize := f.Options().Field(builtins.OptimizeFor)
if v, _ := optimize.AsInt(); v != 3 { // google.protobuf.FileOptions.LITE_RUNTIME
for imp := range seq.Values(f.Imports()) {
impOptimize := imp.Options().Field(builtins.OptimizeFor)
if v, _ := impOptimize.AsInt(); v == 3 { // google.protobuf.FileOptions.LITE_RUNTIME
r.Errorf("`LITE_RUNTIME` file imported in non-`LITE_RUNTIME` file").Apply(
report.Snippet(imp.Decl.ImportPath()),
report.Snippetf(optimize.ValueAST(), "optimization level set here"),
report.Snippetf(impOptimize.ValueAST(), "`%s` set as `LITE_RUNTIME` here", path.Base(imp.Path())),
report.Helpf("files using `LITE_RUNTIME` compile to types that use `MessageLite` or "+
"equivalent in some runtimes, which ordinary message types cannot depend on"),
)
}
}
}
defaultPresence := f.FeatureSet().Lookup(builtins.FeaturePresence).Value()
if v, _ := defaultPresence.AsInt(); v == 3 { // google.protobuf.FeatureSet.LEGACY_REQUIRED
r.Errorf("cannot set `LEGACY_REQUIRED` at the file level").Apply(
report.Snippet(defaultPresence.ValueAST()),
)
}
}
func validateReservedNames(ty Type, r *report.Report) {
for name := range seq.Values(ty.ReservedNames()) {
member := ty.MemberByInternedName(name.InternedName())
if member.IsZero() {
continue
}
r.Errorf("use of reserved %s name", member.noun()).Apply(
report.Snippet(member.AST().Name()),
report.Snippetf(name.AST(), "`%s` reserved here", member.Name()),
)
}
}
func validateOneof(oneof Oneof, r *report.Report) {
if oneof.Members().Len() == 0 {
r.Errorf("oneof must define at least one member").Apply(
report.Snippet(oneof.AST()),
)
}
}
func validateExtensionRange(rr ReservedRange, r *report.Report) {
if rr.Context().Syntax() != syntax.Proto3 {
return
}
r.Errorf("%s in \"proto3\"", taxa.Extensions).Apply(
report.Snippet(rr.AST()),
report.PageBreak,
report.Snippetf(rr.Context().AST().Syntax().Value(), "\"proto3\" specified here"),
report.Helpf("extension numbers cannot be reserved in \"proto3\""),
)
}
func validateExtend(extend Extend, r *report.Report) {
if extend.Extensions().Len() == 0 {
r.Errorf("%s must declare at least one %s", taxa.Extend, taxa.Extension).Apply(
report.Snippet(extend.AST()),
)
}
if extend.Context().Syntax() != syntax.Proto3 {
return
}
builtins := extend.Context().builtins()
if slicesx.Among(extend.Extendee(),
builtins.FileOptions.Element(),
builtins.MessageOptions.Element(),
builtins.FieldOptions.Element(),
builtins.RangeOptions.Element(),
builtins.OneofOptions.Element(),
builtins.EnumOptions.Element(),
builtins.EnumValueOptions.Element(),
builtins.ServiceOptions.Element(),
builtins.MethodOptions.Element(),
) {
return
}
r.Error(errTypeConstraint{
want: "built-in options message",
got: extend.Extendee(),
decl: extend.AST().Type(),
}).Apply(
report.PageBreak,
report.Snippetf(extend.Context().AST().Syntax().Value(), "\"proto3\" specified here"),
report.Helpf("extendees in \"proto3\" files are restricted to an `google.protobuf.*Options` message types", taxa.Extend),
)
}
func validateMessageSet(ty Type, r *report.Report) {
f := ty.Context()
builtins := ty.Context().builtins()
if f.Syntax() == syntax.Proto3 {
r.Errorf("%s are not supported", taxa.MessageSet).Apply(
report.Snippetf(ty.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(ty.AST().Stem()),
report.PageBreak,
report.Snippetf(f.AST().Syntax().Value(), "\"proto3\" specified here"),
report.Helpf("%ss cannot be defined in \"proto3\" only", taxa.MessageSet),
report.Helpf("%ss are not implemented correctly in most Protobuf implementations", taxa.MessageSet),
)
return
}
ok := true
for member := range seq.Values(ty.Members()) {
ok = false
r.Errorf("field declared in %s `%s`", taxa.MessageSet, ty.FullName()).Apply(
report.Snippet(member.AST()),
report.PageBreak,
report.Snippet(ty.AST().Stem()),
report.Snippetf(ty.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Helpf("message set types may only declare extension ranges"),
)
}
for oneof := range seq.Values(ty.Oneofs()) {
ok = false
r.Errorf("field declared in %s `%s`", taxa.MessageSet, ty.FullName()).Apply(
report.Snippet(oneof.AST()),
report.PageBreak,
report.Snippetf(ty.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(ty.AST().Stem()),
report.Helpf("message set types may only declare extension ranges"),
)
}
if ty.ExtensionRanges().Len() == 0 {
ok = false
r.Errorf("%s `%s` declares no %ss", taxa.MessageSet, ty.FullName(), taxa.Extensions).Apply(
report.Snippetf(ty.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(ty.AST().Stem()),
)
}
if ok {
r.Warnf("%ss are deprecated", taxa.MessageSet).Apply(
report.Snippetf(ty.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(ty.AST().Stem()),
report.Helpf("%ss are not implemented correctly in most Protobuf implementations", taxa.MessageSet),
)
}
}
func validateMessageSetExtension(extn Member, r *report.Report) {
builtins := extn.Context().builtins()
extendee := extn.Container()
if extn.IsRepeated() {
_, repeated := iterx.Find(extn.AST().Type().Prefixes(), func(ty ast.TypePrefixed) bool {
return ty.Prefix() == keyword.Repeated
})
r.Errorf("repeated message set extension").Apply(
report.Snippet(repeated.PrefixToken()),
report.PageBreak,
report.Snippetf(extendee.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(extendee.AST().Stem()),
report.Helpf("message set extensions must be singular message fields"),
)
}
if !extn.Element().IsMessage() {
r.Errorf("non-message message set extension").Apply(
report.Snippet(extn.AST().Type().RemovePrefixes()),
report.PageBreak,
report.Snippetf(extendee.Options().Field(builtins.MessageSet).KeyAST(), "declared as message set here"),
report.Snippet(extendee.AST().Stem()),
report.Helpf("message set extensions must be singular message fields"),
)
}
}
func validateExtensionDeclarations(ty Type, r *report.Report) {
builtins := ty.Context().builtins()
// First, walk through all of the extension ranges to get their associated
// option objects.
options := make(map[MessageValue][]ReservedRange)
for r := range seq.Values(ty.ExtensionRanges()) {
if r.Options().IsZero() {
continue
}
mapsx.Append(options, r.Options(), r)
}
// Now, walk through each grouping of extensions and match up their
// declarations.
for options, ranges := range options {
rangeSpan := func() source.Span {
return source.JoinSeq(iterx.Map(slices.Values(ranges), func(r ReservedRange) source.Span {
return r.AST().Span()
}))
}
decls := options.Field(builtins.ExtnDecls)
verification := options.Field(builtins.ExtnVerification)
if v, ok := verification.AsInt(); ok && (v == 1) != decls.IsZero() {
if decls.IsZero() {
r.Errorf("extension range requires declarations, but does not define any").Apply(
report.Snippetf(verification.ValueAST(), "required by this option"),
report.Snippet(rangeSpan()),
)
} else {
r.Errorf("unverified extension range defines declarations").Apply(
report.Snippetf(decls.OptionSpan(), "defined here"),
report.Snippetf(verification.ValueAST(), "required by this option"),
)
}
}
if decls.IsZero() {
continue
}
if len(ranges) > 1 {
// An extension range with declarations and multiple ranges
// is not allowed.
r.Errorf("multi-range `extensions` with extension declarations").Apply(
report.Snippetf(decls.KeyAST(), "declaration defined here"),
report.Snippetf(rangeSpan(), "multiple ranges declared here"),
report.Helpf("this is rejected by protoc due to a quirk in its internal representation of extension ranges"),
)
}
var haveMissingField bool
numbers := make(map[int32]struct{})
for elem := range seq.Values(decls.Elements()) {
decl := elem.AsMessage()
number := decl.Field(builtins.ExtnDeclNumber)
if n, ok := number.AsInt(); ok {
// Find the range that contains n.
var found bool
for _, r := range ranges {
start, end := r.Range()
if int64(start) <= n && n <= int64(end) {
found = true
numbers[int32(n)] = struct{}{}
break
}
}
if !found {
r.Errorf("out-of-range `%s` in extension declaration", number.Field().Name()).Apply(
report.Snippet(number.ValueAST()),
report.Snippetf(rangeSpan(), "%v must be among one of these ranges", n),
)
}
} else {
r.Errorf("extension declaration must specify `%s`", builtins.ExtnDeclNumber.Name()).Apply(
report.Snippet(elem.AST()),
)
haveMissingField = true
}
validatePath := func(v Value, want any) bool {
// First, check this is a valid name in the first place.
s, _ := v.AsString()
name := FullName(s)
for component := range name.Components() {
if !asciiIdent.MatchString(component) {
d := r.Errorf("expected %s in `%s.%s`", want,
v.Field().Container().Name(), v.Field().Name(),
).Apply(
report.Snippet(v.ValueAST()),
)
if strings.ContainsFunc(component, unicode.IsSpace) {
d.Apply(report.Helpf("the name may not contain whitespace"))
}
return false
}
}
if !name.Absolute() {
d := r.Errorf("relative name in `%s.%s`",
v.Field().Container().Name(), v.Field().Name(),
).Apply(
report.Snippet(v.ValueAST()),
)
if lit := v.ValueAST().AsLiteral(); !lit.IsZero() {
str := lit.AsString()
start := lit.Span().Start
offset := str.RawContent().Start - start
d.Apply(report.SuggestEdits(v.ValueAST(), "add a leading `.`", report.Edit{
Start: offset, End: offset,
Replace: ".",
}))
}
}
return true
}
// NOTE: name deduplication needs to wait until global linking,
// similar to extension number deduplication.
name := decl.Field(builtins.ExtnDeclName)
if !name.IsZero() {
validatePath(name, "fully-qualified name")
} else if !haveMissingField {
r.Errorf("extension declaration must specify `%s`", builtins.ExtnDeclName.Name()).Apply(
report.Snippet(elem.AST()),
)
haveMissingField = true
}
tyName := decl.Field(builtins.ExtnDeclType)
if !tyName.IsZero() {
v, _ := tyName.AsString()
if predeclared.Lookup(v) == predeclared.Unknown {
ok := validatePath(tyName, "predeclared type or fully-qualified name")
if ok {
// Check to see whether this is a legit type.
sym := ty.Context().FindSymbol(FullName(v).ToRelative())
if !sym.IsZero() && !sym.Kind().IsType() {
r.Warnf("expected type, got %s `%s`", sym.noun(), sym.FullName()).Apply(
report.Snippet(tyName.ValueAST()),
report.PageBreak,
report.Snippetf(sym.Definition(), "`%s` declared here", sym.FullName()),
report.Helpf("`%s.%s` must name a (possibly unimported) type", tyName.Field().Container().Name(), tyName.Field().Name()),
)
}
}
}
} else if !haveMissingField {
r.Errorf("extension declaration must specify `%s`", builtins.ExtnDeclType.Name()).Apply(
report.Snippet(elem.AST()),
)
haveMissingField = true
}
}
// Generate warnings for each range that is missing at least one value.
missingDecls:
for _, rr := range ranges {
start, end := rr.Range()
// The complexity of this loop is only O(decls), so `1 to max` will
// not need to loop two billion times.
for i := start; i <= end; i++ {
if !mapsx.Contains(numbers, i) {
r.Warnf("missing declaration for extension number `%v`", i).Apply(
report.Snippetf(rr.AST(), "required by this range"),
report.Notef("this is likely a mistake, but it is not rejected by protoc"),
)
break missingDecls // Only diagnose the first problematic range.
}
}
}
}
}
func validateDeclaredExtension(m Member, r *report.Report) {
builtins := m.Context().builtins()
// First, figure out whether this is a declared extension.
extendee := m.Container()
var decl MessageValue
var elem Element
declSearch:
for r := range extendee.Ranges(m.Number()) {
decls := r.AsReserved().Options().Field(builtins.ExtnDecls)
for v := range seq.Values(decls.Elements()) {
msg := v.AsMessage()
number := msg.Field(builtins.ExtnDeclNumber)
if n, ok := number.AsInt(); ok && n == int64(m.Number()) {
elem = v
decl = msg
break declSearch
}
}
}
if decl.IsZero() {
return // Not a declared extension.
}
reserved := decl.Field(builtins.ExtnDeclReserved)
if v, _ := reserved.AsBool(); v {
r.Errorf("use of reserved extension number").Apply(
report.Snippet(m.AST().Value()),
report.PageBreak,
report.Snippetf(elem.AST(), "extension declared here"),
report.Snippetf(reserved.ValueAST(), "... and reserved here"),
)
}
name := decl.Field(builtins.ExtnDeclName)
if v, ok := name.AsString(); ok && m.FullName() != FullName(v).ToRelative() {
r.Errorf("unexpected %s name", taxa.Extension).Apply(
report.Snippetf(m.AST().Name(), "expected `%s`", v),
report.PageBreak,
report.Snippetf(name.ValueAST(), "expected name declared here"),
)
}
tyName := decl.Field(builtins.ExtnDeclType)
repeated := decl.Field(builtins.ExtnDeclRepeated)
wantRepeated, _ := repeated.AsBool()
if v, ok := tyName.AsString(); ok {
ty := PredeclaredType(predeclared.Lookup(v))
var sym Symbol
if ty.IsZero() {
sym = m.Context().FindSymbol(FullName(v).ToRelative())
ty = sym.AsType()
}
if m.Element() != ty || wantRepeated != m.IsRepeated() {
want := any(sym)
if sym.IsZero() {
if !ty.IsZero() {
want = ty
} else {
want = fmt.Sprintf("unknown type `%s`", FullName(v).ToRelative())
}
}
d := r.Error(errTypeCheck{
want: want, got: m.Element(),
wantRepeated: wantRepeated,
gotRepeated: m.IsRepeated(),
expr: m.TypeAST(),
annotation: tyName.ValueAST(),
})
if wantRepeated {
d.Apply(report.Snippetf(repeated.OptionSpan(), "`repeated` required here"))
}
if !sym.IsZero() && ty.IsZero() {
d.Apply(report.Notef("`%s` is not a type; this indicates a bug in the extension declaration", sym.FullName()))
}
}
}
}
func validatePresence(m Member, r *report.Report) {
if m.IsEnumValue() {
return
}
builtins := m.Context().builtins()
feature := m.FeatureSet().Lookup(builtins.FeaturePresence)
if !feature.IsExplicit() {
return
}
switch {
case !m.IsSingular():
what := "repeated"
if m.IsMap() {
what = "map"
}
r.Errorf("expected singular field, found %s field", what).Apply(
report.Snippet(m.TypeAST()),
report.Snippetf(
feature.Value().KeyAST(),
"`%s` set here", feature.Field().Name(),
),
report.Helpf("`%s` can only be set on singular fields", feature.Field().Name()),
)
case m.Presence() == presence.Shared:
r.Errorf("expected singular field, found oneof member").Apply(
report.Snippet(m.AST()),
report.Snippetf(m.Oneof().AST(), "defined in this oneof"),
report.Snippetf(
feature.Value().KeyAST(),
"`%s` set here", feature.Field().Name(),
),
report.Helpf("`%s` cannot be set on oneof members", feature.Field().Name()),
report.Helpf("all oneof members have explicit presence"),
)
case m.IsExtension():
r.Errorf("expected singular field, found extension").Apply(
report.Snippet(m.AST()),
report.Snippetf(
feature.Value().KeyAST(),
"`%s` set here", feature.Field().Name(),
),
report.Helpf("`%s` cannot be set on extensions", feature.Field().Name()),
report.Helpf("all singular extensions have explicit presence"),
)
}
switch v, _ := feature.Value().AsInt(); v {
case 1: // EXPLICIT
case 2: // IMPLICIT
if m.Element().IsMessage() {
r.Error(errTypeConstraint{
want: taxa.MessageType,
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippet(m.TypeAST()),
report.Snippetf(
feature.Value().ValueAST(),
"implicit presence set here",
),
report.Helpf("all message-typed fields explicit presence"),
)
}
case 3: // LEGACY_REQUIRED
r.Warnf("required fields are deprecated").Apply(
report.Snippet(feature.Value().ValueAST()),
report.Helpf(
"do not attempt to change this to `EXPLICIT` if the field is "+
"already in-use; doing so is a wire protocol break"),
)
}
}
// validatePacked validates constraints on the packed option and feature.
func validatePacked(m Member, r *report.Report) {
builtins := m.Context().builtins()
validate := func(span source.Span) {
switch {
case m.IsSingular() || m.IsMap():
r.Errorf("expected repeated field, found singular field").Apply(
report.Snippet(m.TypeAST()),
report.Snippetf(span, "packed encoding set here"),
report.Helpf("packed encoding can only be set on repeated fields of integer, float, `bool`, or enum type"),
)
case !m.Element().IsPackable():
r.Error(errTypeConstraint{
want: "packable type",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(span, "packed encoding set here"),
report.Helpf("packed encoding can only be set on repeated fields of integer, float, `bool`, or enum type"),
)
}
}
option := m.Options().Field(builtins.Packed)
if !option.IsZero() {
if m.Context().Syntax().IsEdition() {
packed, _ := option.AsBool()
want := "PACKED"
if !packed {
want = "EXPANDED"
}
r.Error(erredition.TooNew{
Current: m.Context().Syntax(),
Decl: m.Context().AST().Syntax(),
Removed: syntax.Edition2023,
What: option.Field().Name(),
Where: option.KeyAST(),
}).Apply(option.suggestEdit(
builtins.FeaturePacked.Name(), want,
"replace with `%s`", builtins.FeaturePacked.Name(),
))
} else if v, _ := option.AsBool(); v {
// Don't validate [packed = false], protoc accepts that.
validate(option.ValueAST().Span())
}
}
feature := m.FeatureSet().Lookup(builtins.FeaturePacked)
if feature.IsExplicit() {
validate(feature.Value().KeyAST().Span())
}
}
func validateLazy(m Member, r *report.Report) {
builtins := m.Context().builtins()
validate := func(key Member) {
lazy := m.Options().Field(key)
if lazy.IsZero() {
return
}
set, _ := lazy.AsBool()
if !m.Element().IsMessage() {
r.SoftError(set, errTypeConstraint{
want: "message type",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(lazy.KeyAST(), "`%s` set here", lazy.Field().Name()),
report.Helpf("`%s` can only be set on message-typed fields", lazy.Field().Name()),
)
}
if m.IsGroup() {
r.SoftErrorf(set, "expected length-prefixed field").Apply(
report.Snippet(m.AST()),
report.Snippetf(m.AST().KeywordToken(), "groups are not length-prefixed"),
report.Snippetf(lazy.KeyAST(), "`%s` set here", lazy.Field().Name()),
report.Helpf("`%s` only makes sense for length-prefixed messages", lazy.Field().Name()),
)
}
group := m.FeatureSet().Lookup(builtins.FeatureGroup)
groupValue, _ := group.Value().AsInt()
if groupValue == 2 { // FeatureSet.DELIMITED
d := r.SoftErrorf(set, "expected length-prefixed field").Apply(
report.Snippet(m.AST()),
report.Snippetf(lazy.KeyAST(), "`%s` set here", lazy.Field().Name()),
report.Helpf("`%s` only makes sense for length-prefixed messages", lazy.Field().Name()),
)
if group.IsInherited() {
d.Apply(report.PageBreak)
}
d.Apply(report.Snippetf(group.Value().ValueAST(), "set to use delimited encoding here"))
}
}
validate(builtins.Lazy)
validate(builtins.UnverifiedLazy)
}
func validateJSType(m Member, r *report.Report) {
builtins := m.Context().builtins()
option := m.Options().Field(builtins.JSType)
if option.IsZero() {
return
}
ty := m.Element().Predeclared()
if !ty.IsInt() || ty.Bits() != 64 {
r.Error(errTypeConstraint{
want: "64-bit integer type",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(option.KeyAST(), "`%s` set here", option.Field().Name()),
report.Helpf("`%s` is specifically for controlling the formatting of large integer types, "+
"which lose precision when JavaScript converts them into 64-bit IEEE 754 floats", option.Field().Name()),
)
}
}
func validateCType(m Member, r *report.Report) {
builtins := m.Context().builtins()
f := m.Context()
ctype := m.Options().Field(builtins.CType)
if ctype.IsZero() {
return
}
ctypeValue, _ := ctype.AsInt()
var want string
switch ctypeValue {
case 0: // FieldOptions.STRING
want = "STRING"
case 1: // FieldOptions.CORD
want = "CORD"
case 2: // FieldOptions.STRING_PIECE
want = "VIEW"
}
is2023 := f.Syntax() == syntax.Edition2023
switch {
case f.Syntax() > syntax.Edition2023:
r.Error(erredition.TooNew{
Current: m.Context().Syntax(),
Decl: m.Context().AST().Syntax(),
Deprecated: syntax.Edition2023,
Removed: syntax.Edition2024,
What: ctype.Field().Name(),
Where: ctype.KeyAST(),
}).Apply(ctype.suggestEdit(
"features.(pb.cpp).string_type", want,
"replace with `features.(pb.cpp).string_type`",
))
case !m.Element().Predeclared().IsString():
d := r.SoftError(is2023, errTypeConstraint{
want: "`string` or `bytes`",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(ctype.KeyAST(), "`%s` set here", ctype.Field().Name()),
)
if !is2023 {
d.Apply(report.Helpf("this becomes a hard error in %s", syntax.Edition2023.Name()))
}
case m.IsExtension() && ctypeValue == 1: // google.protobuf.FieldOptions.CORD
d := r.SoftErrorf(is2023, "cannot use `CORD` on an extension field").Apply(
report.Snippet(m.AST()),
report.Snippetf(ctype.ValueAST(), "`CORD` set here"),
)
if !is2023 {
d.Apply(report.Helpf("this becomes a hard error in %s", syntax.Edition2023.Name()))
}
case is2023:
r.Warn(erredition.TooNew{
Current: m.Context().Syntax(),
Decl: m.Context().AST().Syntax(),
Deprecated: syntax.Edition2023,
Removed: syntax.Edition2024,
What: ctype.Field().Name(),
Where: ctype.KeyAST(),
}).Apply(ctype.suggestEdit(
"features.(pb.cpp).string_type", want,
"replace with `features.(pb.cpp).string_type`",
))
}
}
func validateUTF8(m Member, r *report.Report) {
builtins := m.Context().builtins()
feature := m.FeatureSet().Lookup(builtins.FeatureUTF8)
if !feature.IsExplicit() {
return
}
if m.Element().Predeclared() == predeclared.String {
return
}
if k, v := m.Element().EntryFields(); k.Element().Predeclared() == predeclared.String ||
v.Element().Predeclared() == predeclared.String {
return
}
r.Error(errTypeConstraint{
want: "`string`",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(
feature.Value().KeyAST(),
"`%s` set here", feature.Field().Name(),
),
report.Helpf(
"`%s` can only be set on `string` typed fields, "+
"or map fields whose key or value is `string`",
feature.Field().Name(),
),
)
}
func validateMessageEncoding(m Member, r *report.Report) {
builtins := m.Context().builtins()
feature := m.FeatureSet().Lookup(builtins.FeatureGroup)
if !feature.IsExplicit() {
return
}
if m.Element().IsMessage() && !m.IsMap() {
return
}
d := r.Error(errTypeConstraint{
want: taxa.MessageType,
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(
feature.Value().KeyAST(),
"`%s` set here", feature.Field().Name(),
),
report.Helpf(
"`%s` can only be set on message-typed fields", feature.Field().Name(),
),
)
if m.IsMap() {
d.Apply(report.Helpf(
"even though map fields count as repeated message-typed fields, "+
"`%s` cannot be set on them",
feature.Field().Name(),
))
}
}
func validateDefault(m Member, r *report.Report) {
option := m.PseudoOptions().Default
if option.IsZero() {
return
}
if file := m.Context(); file.Syntax() == syntax.Proto3 {
r.Errorf("custom default in \"proto3\"").Apply(
report.Snippet(option.OptionSpan()),
report.PageBreak,
report.Snippetf(file.AST().Syntax().Value(), "\"proto3\" specified here"),
report.Helpf("custom defaults cannot be defined in \"proto3\" only"),
)
}
if m.IsRepeated() || m.Element().IsMessage() {
r.Error(errTypeConstraint{
want: "singular scalar- or enum-typed field",
got: m.Element(),
decl: m.TypeAST(),
}).Apply(
report.Snippetf(option.KeyAST(), "custom default specified here"),
report.Helpf("custom defaults are only for non-repeated fields that have a non-message type"),
)
}
if m.IsUnicode() {
if s, _ := option.AsString(); !utf8.ValidString(s) {
r.Warn(&errNotUTF8{value: option.Elements().At(0)}).Apply(
report.Helpf("protoc erroneously accepts non-UTF-8 defaults for UTF-8 fields; for all other options, UTF-8 validation failure causes protoc to crash"),
)
}
}
// Warn if the zero value is used, because it's redundant.
if option.IsZeroValue() {
r.Warnf("redundant custom default").Apply(
report.Snippetf(option.ValueAST(), "this is the zero value for `%s`", m.Element().FullName()),
report.Helpf("fields without a custom default will default to the zero value, making this option redundant"),
)
}
}
// validateUTF8Values validates that strings in a value are actually UTF-8.
func validateUTF8Values(v Value, r *report.Report) {
for elem := range seq.Values(v.Elements()) {
if v.Field().IsUnicode() {
if s, _ := elem.AsString(); !utf8.ValidString(s) {
r.Error(&errNotUTF8{value: elem})
}
}
}
}
func validateVisibility(ty Type, r *report.Report) {
key := ty.Context().builtins().FeatureVisibility
if key.IsZero() {
return
}
feature := ty.FeatureSet().Lookup(key)
value, _ := feature.Value().AsInt()
strict := value == 4 // STRICT
var impliedExport bool
switch value {
case 0, 1: // DEFAULT_SYMBOL_VISIBILITY_UNKNOWN, EXPORT_ALL
impliedExport = true
case 2: // EXPORT_TOP_LEVEL
impliedExport = ty.Parent().IsZero()
case 3, 4: // LOCAL_ALL, STRICT
impliedExport = false
}
var why source.Span
if feature.IsDefault() {
why = ty.Context().AST().Syntax().Value().Span()
} else {
why = feature.Value().ValueAST().Span()
}
vis := id.Wrap(ty.AST().Context().Stream(), ty.Raw().visibility)
export := vis.Keyword() == keyword.Export
if !ty.Raw().visibility.IsZero() && export == impliedExport {
r.Warnf("redundant visibility modifier").Apply(
report.Snippetf(vis, "specified here"),
report.PageBreak,
report.Snippetf(why, "this implies it"),
)
}
if !strict || !export { // STRICT
return
}
// STRICT requires that we check two things:
//
// 1. Nested types are not explicitly exported.
// 2. Unless they are nested within a message that reserves all of its
// field numbers.
parent := ty.Parent()
if ty.Parent().IsZero() {
return
}
start, end := parent.AbsoluteRange()
// Find any gaps in the reserved ranges.
gap := start
ranges := slices.Collect(seq.Values(parent.ReservedRanges()))
if len(ranges) > 0 {
slices.SortFunc(ranges, cmpx.Join(
cmpx.Key(func(r ReservedRange) int32 {
start, _ := r.Range()
return start
}),
cmpx.Key(func(r ReservedRange) int32 {
start, end := r.Range()
return end - start
}),
))
// Skip all ranges whose end is less than start.
for len(ranges) > 0 {
_, end := ranges[0].Range()
if end >= start {
break
}
ranges = ranges[1:]
}
for _, rr := range ranges {
a, b := rr.Range()
if gap < a {
// We're done, gap is not reserved.
break
}
gap = b + 1
}
}
if end <= gap {
// If there are multiple reserved ranges, protoc rejects this, because it
// doesn't do the same sophisticated interval sorting we do.
switch {
case parent.ReservedRanges().Len() != 1:
d := r.Errorf("expected exactly one reserved range").Apply(
report.Snippetf(vis, "nested type exported here"),
report.Snippetf(parent.AST(), "... within this type"),
)
ranges := parent.ReservedRanges()
if ranges.Len() > 0 {
d.Apply(
report.Snippetf(ranges.At(0).AST(), "one here"),
report.Snippetf(ranges.At(1).AST(), "another here"),
)
}
//nolint:dupword
d.Apply(
report.PageBreak,
report.Snippetf(why, "`STRICT` specified here"),
report.Helpf("in strict mode, nesting an exported type within another type "+
"requires that that type declare `reserved 1 to max;`, even if all of its field "+
"numbers are `reserved`"),
report.Helpf("protoc erroneously rejects this, despite being equivalent"),
)
case ty.IsMessage():
r.Errorf("nested message type marked as exported").Apply(
report.Snippetf(vis, "nested type exported here"),
report.Snippetf(parent.AST(), "... within this type"),
report.PageBreak,
report.Snippetf(why, "`STRICT` specified here"),
report.Helpf("in strict mode, nested message types cannot be marked as "+
"exported, even if all the field numbers of its parent are reserved"),
)
}
return
}
// If this is true, the protoc check is bugged and we emit a warning...
bugged := parent.ReservedRanges().Len() == 1
//nolint:dupword
d := r.SoftErrorf(!bugged, "%s `%s` does not reserve all field numbers", parent.noun(), parent.FullName()).Apply(
report.Snippetf(vis, "nested type exported here"),
report.Snippetf(parent.AST(), "... within this type"),
report.PageBreak,
report.Snippetf(why, "`STRICT` specified here"),
report.Helpf("in strict mode, nesting an exported type within another type "+
`requires that that type reserve every field number (the "C++ namespace exception"), `+
"but this type does not reserve the field number %d", gap),
)
if bugged {
d.Apply(report.Helpf("protoc erroneously accepts this code due to a bug: it only " +
"checks that there is exactly one reserved range"))
}
}
func validateNamingStyle(f *File, r *report.Report) {
key := f.builtins().FeatureNamingStyle
if key.IsZero() {
return // Feature doesn't exist (pre-2024)
}
// Helper to check if STYLE2024 is enabled at a given scope.
isStyle2024 := func(featureSet FeatureSet) bool {
feature := featureSet.Lookup(key)
value, _ := feature.Value().AsInt()
return value == 1 // STYLE2024
}
// Validate package name (file-level scope).
if isStyle2024(f.FeatureSet()) {
pkg := f.Package()
if pkg != "" && !isValidPackageName(string(pkg)) {
r.Errorf("package name should be lower_snake_case").Apply(
report.Snippetf(f.AST().Package().Path(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires package names to be lower_snake_case or dot.delimited.lower_snake_case"),
)
}
}
// Validate all services in the file.
for svc := range seq.Values(f.Services()) {
name := svc.Name()
// PascalCase required for services.
if isStyle2024(svc.FeatureSet()) && !isPascalCase(name) {
r.Errorf("service name should be PascalCase").Apply(
report.Snippetf(svc.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires service names to be PascalCase (e.g., MyService)"),
)
}
// Validate RPC method names.
for method := range seq.Values(svc.Methods()) {
if method.IsZero() || !isStyle2024(method.FeatureSet()) {
continue
}
methodName := method.Name()
if !isPascalCase(methodName) {
r.Errorf("RPC method name should be PascalCase").Apply(
report.Snippetf(method.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires RPC method names to be PascalCase (e.g., GetMessage)"),
)
}
}
}
// Validate naming conventions based on type.
for ty := range seq.Values(f.AllTypes()) {
name := ty.Name()
switch {
case ty.IsMessage():
// PascalCase required for messages.
if isStyle2024(ty.FeatureSet()) && !isPascalCase(name) {
r.Errorf("%s name should be PascalCase", ty.noun()).Apply(
report.Snippetf(ty.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires message names to be PascalCase (e.g., MyMessage)"),
)
}
// Validate field names (check at field level).
for field := range seq.Values(ty.Members()) {
if field.IsZero() || field.IsGroup() || !isStyle2024(field.FeatureSet()) {
continue
}
fieldName := field.Name()
if !isSnakeCase(fieldName) {
r.Errorf("field name should be snake_case").Apply(
report.Snippetf(field.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires field names to be snake_case (e.g., my_field)"),
)
}
}
// Validate oneof names (check at oneof level).
for oneof := range seq.Values(ty.Oneofs()) {
if oneof.IsZero() || !isStyle2024(oneof.FeatureSet()) {
continue
}
oneofName := oneof.Name()
if !isSnakeCase(oneofName) {
r.Errorf("oneof name should be snake_case").Apply(
report.Snippetf(oneof.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires oneof names to be snake_case (e.g., my_choice)"),
)
}
}
case ty.IsEnum():
// PascalCase required for enums.
if isStyle2024(ty.FeatureSet()) && !isPascalCase(name) {
r.Errorf("%s name should be PascalCase", ty.noun()).Apply(
report.Snippetf(ty.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires enum names to be PascalCase (e.g., MyEnum)"),
)
}
// Validate enum value names (check at value level).
for value := range seq.Values(ty.Members()) {
if value.IsZero() || !isStyle2024(value.FeatureSet()) {
continue
}
valueName := value.Name()
if !isScreamingSnakeCase(valueName) {
r.Errorf("enum value name should be SCREAMING_SNAKE_CASE").Apply(
report.Snippetf(value.AST().Name(), "this name violates STYLE2024"),
report.Helpf("STYLE2024 requires enum value names to be SCREAMING_SNAKE_CASE (e.g., MY_VALUE)"),
)
}
}
}
}
}
// isPascalCase checks if a name is in PascalCase format.
func isPascalCase(name string) bool {
if len(name) == 0 {
return false
}
// Must start with uppercase letter.
if !unicode.IsUpper(rune(name[0])) {
return false
}
// Should not contain underscores.
if strings.Contains(name, "_") {
return false
}
// All characters should be letters or digits.
for _, r := range name {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return false
}
}
return true
}
// isSnakeCase checks if a name is in snake_case format.
func isSnakeCase(name string) bool {
if len(name) == 0 {
return false
}
// Must start with lowercase letter.
if !unicode.IsLower(rune(name[0])) {
return false
}
// Should not have leading underscore.
if strings.HasPrefix(name, "_") {
return false
}
// Should only contain lowercase letters, digits, and underscores.
for i, r := range name {
if !unicode.IsLower(r) && !unicode.IsDigit(r) && r != '_' {
return false
}
// Underscore must be followed by a letter (not a digit or another underscore).
if r == '_' && i+1 < len(name) {
next := rune(name[i+1])
if !unicode.IsLower(next) {
return false
}
}
}
// Should not have consecutive underscores or end with underscore.
if strings.Contains(name, "__") || strings.HasSuffix(name, "_") {
return false
}
return true
}
// isScreamingSnakeCase checks if a name is in SCREAMING_SNAKE_CASE format.
func isScreamingSnakeCase(name string) bool {
if len(name) == 0 {
return false
}
// Should only contain uppercase letters, digits, and underscores.
for i, r := range name {
if !unicode.IsUpper(r) && !unicode.IsDigit(r) && r != '_' {
return false
}
// Underscore must be followed by a letter (not a digit or another underscore).
if r == '_' && i+1 < len(name) {
next := rune(name[i+1])
if !unicode.IsUpper(next) {
return false
}
}
}
// Should not have consecutive underscores or start/end with underscore.
if strings.Contains(name, "__") || strings.HasPrefix(name, "_") || strings.HasSuffix(name, "_") {
return false
}
return true
}
// isValidPackageName checks if a package name is in lower_snake_case or dot.delimited.lower_snake_case format.
func isValidPackageName(name string) bool {
if len(name) == 0 {
return false
}
// Split on dots and validate each component.
for part := range strings.SplitSeq(name, ".") {
if len(part) == 0 {
return false
}
// Each part should be lower_snake_case.
if !isSnakeCase(part) {
return false
}
}
return true
}
// errNotUTF8 diagnoses a non-UTF8 value.
type errNotUTF8 struct {
value Element
}
func (e *errNotUTF8) Diagnose(d *report.Diagnostic) {
d.Apply(report.Message("non-UTF-8 string literal"))
if lit := e.value.AST().AsLiteral().AsString(); !lit.IsZero() {
// Figure out the byte offset and the invalid byte. Because this will
// necessarily have come from a \xNN escape, we should look for it.
text := lit.Text()
offset := 0
var invalid byte
for text != "" {
r, n := utf8.DecodeRuneInString(text[offset:])
if r == utf8.RuneError {
invalid = text[offset]
break
}
offset += n
}
// Now, find the invalid escape...
var esc token.Escape
for escape := range seq.Values(lit.Escapes()) {
if escape.Byte == invalid {
esc = escape
break
}
}
d.Apply(report.Snippetf(esc.Span, "non-UTF-8 byte"))
} else {
// String came from non-literal.
d.Apply(report.Snippet(e.value.AST()))
}
d.Apply(
report.Snippetf(e.value.Field().AST(), "this field requires a UTF-8 string"),
)
// Figure out where the relevant feature was set.
builtins := e.value.Context().builtins()
feature := e.value.Field().FeatureSet().Lookup(builtins.FeatureUTF8)
if !feature.IsDefault() {
if feature.IsInherited() {
d.Apply(report.PageBreak)
}
d.Apply(report.Snippetf(feature.Value().ValueAST(), "UTF-8 required here"))
} else {
d.Apply(
report.PageBreak,
report.Snippetf(e.value.Context().AST().Syntax().Value(), "UTF-8 required here"),
)
}
}
// 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 ir
import (
"math"
"slices"
"strings"
"sync"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/id"
"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"
)
// walker is the state struct for the AST-walking logic.
type walker struct {
*File
*report.Report
pkg FullName
}
// walk is the first step in constructing an IR module: converting an AST file
// into a type-and-field graph, and recording the AST source for each structure.
//
// This operation performs no name resolution.
func (w *walker) walk() {
if pkg := w.AST().Package(); !pkg.IsZero() {
w.File.pkg = w.session.intern.Intern(pkg.Path().Canonicalized())
}
w.pkg = w.Package()
w.syntax = syntax.Proto2
if syn := w.AST().Syntax(); !syn.IsZero() {
text := syn.Value().Span().Text()
if unquoted := syn.Value().AsLiteral().AsString(); !unquoted.IsZero() {
text = unquoted.Text()
}
// NOTE: This matches fallback behavior in parser/legalize_file.go.
w.syntax = syntax.Lookup(text)
if w.syntax == syntax.Unknown {
if syn.IsEdition() {
// If they wrote edition = "garbage" they probably want *an*
// edition, so we pick the oldest one.
w.syntax = syntax.Edition2023
} else {
w.syntax = syntax.Proto2
}
}
}
for decl := range seq.Values(w.AST().Decls()) {
w.recurse(decl, nil)
}
}
type extend struct {
parent Type
extendee id.ID[Extend]
}
type oneof struct {
parent Type
Oneof
}
func extractParentType(parent any) Type {
switch parent := parent.(type) {
case Type:
return parent
case extend:
return parent.parent
case oneof:
return parent.parent
default:
return Type{}
}
}
// recurse recursively walks the AST to build the out all of the types in a file.
func (w *walker) recurse(decl ast.DeclAny, parent any) {
switch decl.Kind() {
case ast.DeclKindBody:
for decl := range seq.Values(decl.AsBody().Decls()) {
w.recurse(decl, parent)
}
case ast.DeclKindRange:
// Handled in NewType.
case ast.DeclKindDef:
def := decl.AsDef()
if def.IsCorrupt() {
return
}
switch kind := def.Classify(); kind {
case ast.DefKindMessage, ast.DefKindEnum, ast.DefKindGroup:
ty := w.newType(def, parent)
if kind == ast.DefKindGroup {
w.newField(def, parent, true)
}
w.recurse(def.Body().AsAny(), ty)
case ast.DefKindField, ast.DefKindEnumValue:
w.newField(def, parent, false)
case ast.DefKindOneof:
parent := extractParentType(parent)
if parent.IsZero() {
return // Already diagnosed elsewhere.
}
oneofDef := def.AsOneof()
w.recurse(def.Body().AsAny(), oneof{
parent: extractParentType(parent),
Oneof: w.newOneof(oneofDef, parent),
})
case ast.DefKindExtend:
w.recurse(def.Body().AsAny(), extend{
parent: extractParentType(parent),
extendee: w.newExtendee(def.AsExtend(), parent).ID(),
})
case ast.DefKindService:
service := w.newService(def, parent)
if service.IsZero() {
break
}
w.recurse(def.Body().AsAny(), service)
case ast.DefKindMethod:
w.newMethod(def, parent)
case ast.DefKindOption:
// Options are lowered elsewhere.
}
}
}
func (w *walker) newType(def ast.DeclDef, parent any) Type {
parentTy := extractParentType(parent)
name := def.Name().AsIdent().Name()
fqn := w.fullname(parentTy, name)
var visibility token.ID
for prefix := range def.Type().Prefixes() {
switch prefix.Prefix() {
case keyword.Local, keyword.Export:
visibility = prefix.PrefixToken().ID()
default:
continue
}
break
}
ty := id.Wrap(w.File, id.ID[Type](w.arenas.types.NewCompressed(rawType{
def: def.ID(),
name: w.session.intern.Intern(name),
fqn: w.session.intern.Intern(fqn),
parent: parentTy.ID(),
isEnum: def.Keyword() == keyword.Enum,
visibility: visibility,
})))
ty.Raw().memberByName = sync.OnceValue(ty.makeMembersByName)
for decl := range seq.Values(def.Body().Decls()) {
rangeDecl := decl.AsRange()
if rangeDecl.IsZero() {
continue
}
for v := range seq.Values(rangeDecl.Ranges()) {
if !v.AsPath().AsIdent().IsZero() || v.AsLiteral().Kind() == token.String {
var name string
if id := v.AsPath().AsIdent(); !id.IsZero() {
name = id.Text()
} else {
name = v.AsLiteral().AsString().Text()
}
ty.Raw().reservedNames = append(ty.Raw().reservedNames, rawReservedName{
ast: v,
name: ty.Context().session.intern.Intern(name),
decl: rangeDecl.ID(),
})
continue
}
raw := id.ID[ReservedRange](w.arenas.ranges.NewCompressed(rawReservedRange{
decl: rangeDecl.ID(),
value: v.ID(),
forExtensions: rangeDecl.IsExtensions(),
}))
if rangeDecl.IsReserved() {
ty.Raw().ranges = slices.Insert(ty.Raw().ranges, int(ty.Raw().rangesExtnStart), raw)
ty.Raw().rangesExtnStart++
} else {
ty.Raw().ranges = append(ty.Raw().ranges, raw)
}
}
}
if !parentTy.IsZero() {
parentTy.Raw().nested = append(parentTy.Raw().nested, ty.ID())
w.File.types = append(w.File.types, ty.ID())
} else {
w.File.types = slices.Insert(w.File.types, w.File.topLevelTypesEnd, ty.ID())
w.File.topLevelTypesEnd++
}
return ty
}
//nolint:unparam // Complains about the return value for some reason.
func (w *walker) newField(def ast.DeclDef, parent any, group bool) Member {
parentTy := extractParentType(parent)
name := def.Name().AsIdent().Name()
if group {
name = strings.ToLower(name)
}
fqn := w.fullname(parentTy, name)
member := id.Wrap(w.File, id.ID[Member](w.arenas.members.NewCompressed(rawMember{
def: def.ID(),
name: w.session.intern.Intern(name),
fqn: w.session.intern.Intern(fqn),
parent: parentTy.ID(),
oneof: math.MinInt32,
isGroup: group,
})))
switch parent := parent.(type) {
case oneof:
member.Raw().oneof = int32(parent.Index())
parent.Raw().members = append(parent.Raw().members, member.ID())
case extend:
member.Raw().extendee = parent.extendee
block := id.Wrap(w.File, parent.extendee)
block.Raw().members = append(block.Raw().members, member.ID())
}
if !parentTy.IsZero() {
if _, ok := parent.(extend); ok {
parentTy.Raw().members = append(parentTy.Raw().members, member.ID())
w.File.extns = append(w.File.extns, member.ID())
} else {
parentTy.Raw().members = slices.Insert(parentTy.Raw().members, int(parentTy.Raw().extnsStart), member.ID())
parentTy.Raw().extnsStart++
}
} else if _, ok := parent.(extend); ok {
w.File.extns = slices.Insert(w.File.extns, w.File.topLevelExtnsEnd, member.ID())
w.File.topLevelExtnsEnd++
}
return member
}
func (w *walker) newOneof(def ast.DefOneof, parent any) Oneof {
parentTy := extractParentType(parent)
name := def.Name.Name()
fqn := w.fullname(parentTy, name)
if parentTy.IsZero() {
return Oneof{}
}
oneof := id.Wrap(w.File, id.ID[Oneof](w.arenas.oneofs.NewCompressed(rawOneof{
def: def.Decl.ID(),
name: w.session.intern.Intern(name),
fqn: w.session.intern.Intern(fqn),
index: uint32(len(parentTy.Raw().oneofs)),
container: parentTy.ID(),
})))
parentTy.Raw().oneofs = append(parentTy.Raw().oneofs, oneof.ID())
return oneof
}
func (w *walker) newExtendee(def ast.DefExtend, parent any) Extend {
c := w.File
parentTy := extractParentType(parent)
extend := id.Wrap(w.File, id.ID[Extend](w.arenas.extendees.NewCompressed(rawExtend{
def: def.Decl.ID(),
parent: parentTy.ID(),
})))
if !parentTy.IsZero() {
parentTy.Raw().extends = append(parentTy.Raw().extends, extend.ID())
c.extends = append(c.extends, extend.ID())
} else {
c.extends = slices.Insert(c.extends, c.topLevelExtendsEnd, extend.ID())
c.topLevelExtendsEnd++
}
return extend
}
func (w *walker) newService(def ast.DeclDef, parent any) Service {
if parent != nil {
return Service{}
}
name := def.Name().AsIdent().Name()
fqn := w.pkg.Append(name)
service := id.Wrap(w.File, id.ID[Service](w.arenas.services.NewCompressed(rawService{
def: def.ID(),
name: w.session.intern.Intern(name),
fqn: w.session.intern.Intern(string(fqn)),
})))
w.File.services = append(w.File.services, service.ID())
return service
}
func (w *walker) newMethod(def ast.DeclDef, parent any) Method {
service, ok := parent.(Service)
if !ok {
return Method{}
}
name := def.Name().AsIdent().Name()
fqn := service.FullName().Append(name)
method := id.Wrap(w.File, id.ID[Method](w.arenas.methods.NewCompressed(rawMethod{
def: def.ID(),
name: w.session.intern.Intern(name),
fqn: w.session.intern.Intern(string(fqn)),
service: service.ID(),
})))
service.Raw().methods = append(service.Raw().methods, method.ID())
return method
}
func (w *walker) fullname(parentTy Type, name string) string {
parentName := w.pkg
if !parentTy.IsZero() {
parentName = parentTy.FullName()
if parentTy.IsEnum() {
// Protobuf dumps the names of enum values in the enum's parent, so
// we need to drop a path component.
parentName = parentName.Parent()
}
}
return string(parentName.Append(name))
}
// 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 option_target.yaml. DO NOT EDIT.
package ir
import (
"fmt"
"iter"
)
// OptionTarget is target for an Editions feature, corresponding to
// google.protobuf.FieldOptions.OptionTargetType. The values of this enum match
// those in descriptor.proto one-to-one.
type OptionTarget int32
const (
OptionTargetInvalid OptionTarget = iota
OptionTargetFile
OptionTargetRange
OptionTargetMessage
OptionTargetField
OptionTargetOneof
OptionTargetEnum
OptionTargetEnumValue
OptionTargetService
OptionTargetMethod
optionTargetMax
)
// String implements [fmt.Stringer].
func (v OptionTarget) String() string {
if int(v) < 0 || int(v) > len(_table_OptionTarget_String) {
return fmt.Sprintf("OptionTarget(%v)", int(v))
}
return _table_OptionTarget_String[v]
}
// GoString implements [fmt.GoStringer].
func (v OptionTarget) GoString() string {
if int(v) < 0 || int(v) > len(_table_OptionTarget_GoString) {
return fmt.Sprintf("ir.OptionTarget(%v)", int(v))
}
return _table_OptionTarget_GoString[v]
}
// OptionTargets returns an iterator over all of the possible valid targets,
// which excludes [OptionTargetInvalid].
func OptionTargets() iter.Seq[OptionTarget] {
return func(yield func(OptionTarget) bool) {
for i := 1; i < 10; i++ {
if !yield(OptionTarget(i)) {
return
}
}
}
}
var _table_OptionTarget_String = [...]string{
OptionTargetInvalid: "OptionTargetInvalid",
OptionTargetFile: "OptionTargetFile",
OptionTargetRange: "OptionTargetRange",
OptionTargetMessage: "OptionTargetMessage",
OptionTargetField: "OptionTargetField",
OptionTargetOneof: "OptionTargetOneof",
OptionTargetEnum: "OptionTargetEnum",
OptionTargetEnumValue: "OptionTargetEnumValue",
OptionTargetService: "OptionTargetService",
OptionTargetMethod: "OptionTargetMethod",
}
var _table_OptionTarget_GoString = [...]string{
OptionTargetInvalid: "ir.OptionTargetInvalid",
OptionTargetFile: "ir.OptionTargetFile",
OptionTargetRange: "ir.OptionTargetRange",
OptionTargetMessage: "ir.OptionTargetMessage",
OptionTargetField: "ir.OptionTargetField",
OptionTargetOneof: "ir.OptionTargetOneof",
OptionTargetEnum: "ir.OptionTargetEnum",
OptionTargetEnumValue: "ir.OptionTargetEnumValue",
OptionTargetService: "ir.OptionTargetService",
OptionTargetMethod: "ir.OptionTargetMethod",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 kind.yaml. DO NOT EDIT.
package presence
import (
"fmt"
"iter"
)
// Presence represents how a field is present in a message. This generalizes
// cardinality (viz. optional or repeated).
type Kind byte
const (
Unknown Kind = iota
// The field is singular and presence is a distinct state; corresponds to
// fields marked as "optional", for example.
Explicit
// The field is singular and presence is equivalent to the field having its
// zero value. This corresponds to a non-message field not marked "optional"
// in proto3, for example.
Implicit
// The field is not optional; it is always serialized, and an error is raised
// if it is missing when deserializing.
Required
// The field is repeated: it can occur multiple times, and semantically
// represents a list of values.
Repeated
// The field is part of a oneof: it is singular, and its presence is shared
// with the other fields in the oneof (at most one of them can be present
// at a time).
//
// Notably, oneof members cannot be repeated.
Shared
)
// String implements [fmt.Stringer].
func (v Kind) String() string {
if int(v) < 0 || int(v) > len(_table_Kind_String) {
return fmt.Sprintf("Kind(%v)", int(v))
}
return _table_Kind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v Kind) GoString() string {
if int(v) < 0 || int(v) > len(_table_Kind_GoString) {
return fmt.Sprintf("presence.Kind(%v)", int(v))
}
return _table_Kind_GoString[v]
}
var _table_Kind_String = [...]string{
Unknown: "Unknown",
Explicit: "Explicit",
Implicit: "Implicit",
Required: "Required",
Repeated: "Repeated",
Shared: "Shared",
}
var _table_Kind_GoString = [...]string{
Unknown: "presence.Unknown",
Explicit: "presence.Explicit",
Implicit: "presence.Implicit",
Required: "presence.Required",
Repeated: "presence.Repeated",
Shared: "presence.Shared",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 symbol_kind.yaml. DO NOT EDIT.
package ir
import (
"fmt"
"iter"
)
// SymbolKind is a kind of symbol, i.e. an IR entity that defines a
// fully-qualified name.
type SymbolKind int8
const (
SymbolKindInvalid SymbolKind = iota
SymbolKindPackage
SymbolKindScalar
SymbolKindMessage
SymbolKindEnum
SymbolKindField
SymbolKindEnumValue
SymbolKindExtension
SymbolKindOneof
SymbolKindService
SymbolKindMethod
)
// String implements [fmt.Stringer].
func (v SymbolKind) String() string {
if int(v) < 0 || int(v) > len(_table_SymbolKind_String) {
return fmt.Sprintf("SymbolKind(%v)", int(v))
}
return _table_SymbolKind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v SymbolKind) GoString() string {
if int(v) < 0 || int(v) > len(_table_SymbolKind_GoString) {
return fmt.Sprintf("ir.SymbolKind(%v)", int(v))
}
return _table_SymbolKind_GoString[v]
}
var _table_SymbolKind_String = [...]string{
SymbolKindInvalid: "SymbolKindInvalid",
SymbolKindPackage: "SymbolKindPackage",
SymbolKindScalar: "SymbolKindScalar",
SymbolKindMessage: "SymbolKindMessage",
SymbolKindEnum: "SymbolKindEnum",
SymbolKindField: "SymbolKindField",
SymbolKindEnumValue: "SymbolKindEnumValue",
SymbolKindExtension: "SymbolKindExtension",
SymbolKindOneof: "SymbolKindOneof",
SymbolKindService: "SymbolKindService",
SymbolKindMethod: "SymbolKindMethod",
}
var _table_SymbolKind_GoString = [...]string{
SymbolKindInvalid: "ir.SymbolKindInvalid",
SymbolKindPackage: "ir.SymbolKindPackage",
SymbolKindScalar: "ir.SymbolKindScalar",
SymbolKindMessage: "ir.SymbolKindMessage",
SymbolKindEnum: "ir.SymbolKindEnum",
SymbolKindField: "ir.SymbolKindField",
SymbolKindEnumValue: "ir.SymbolKindEnumValue",
SymbolKindExtension: "ir.SymbolKindExtension",
SymbolKindOneof: "ir.SymbolKindOneof",
SymbolKindService: "ir.SymbolKindService",
SymbolKindMethod: "ir.SymbolKindMethod",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 ir
import (
"iter"
"strings"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/intern"
)
// Set of all names that are defined in scope of some message; used for
// generating synthetic names.
type syntheticNames intern.Set
// generate generates a new synthetic name, according to the rules for synthetic
// oneofs.
//
// Specifically, given a message, we can construct a table of the declared
// single-identifier name of each declaration in the message. This includes the
// names of enum values within a nested enum, due to a C++-friendly language
// bug.
//
// Then, we canonicalize candidate by prepending an underscore to it if it
// doesn't already have one, and then prepending X until we get a name that
// isn't in use yet.
//
// For example, the candidate "foo" will have the sequence of potential
// synthetic names: "_foo" -> "X_foo" -> "XX_foo" -> ...; notably, "_foo" also
// has the same sequence of synthetic names.
func (sn *syntheticNames) generate(candidate string, message Type) string {
if *sn == nil {
// The elements within a message that contribute names scoped to that
// message are:
//
// 1. Fields.
// 2. Extensions (so, more fields).
// 3. Oneofs.
// 4. Nested types, including enums, synthetic map entry types and
// synthetic group types.
// 5. Nested enums' values, due to a language bug.
*sn = mapsx.CollectSet(iterx.Chain(
seq.Map(message.Members(), Member.InternedName),
seq.Map(message.Extensions(), Member.InternedName),
seq.Map(message.Oneofs(), Oneof.InternedName),
iterx.FlatMap(seq.Values(message.Nested()), func(ty Type) iter.Seq[intern.ID] {
if !ty.IsEnum() {
return iterx.Of(ty.InternedName())
}
return iterx.Chain(
iterx.Of(ty.InternedName()),
// We need to include the enum values' names.
seq.Map(ty.Members(), Member.InternedName),
)
}),
))
}
return sn.generateIn(candidate, &message.Context().session.intern)
}
// generateIn is the part of [SyntheticNames.generate] that actually constructs
// the string.
//
// it is outlined so that it can be tested separately.
func (sn *syntheticNames) generateIn(candidate string, table *intern.Table) string {
// The _ prefix is unconditional, but only if candidate does not already
// start with one.
if !strings.HasPrefix(candidate, "_") {
candidate = "_" + candidate
}
// Each time we fail, we add an X to the prefix.
for !intern.Set(*sn).Add(table, candidate) {
candidate = "X" + candidate
}
return candidate
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// errMoreThanOne is used to diagnose the occurrence of some construct more
// than one time, when it is expected to occur at most once.
type errMoreThanOne struct {
first, second source.Spanner
what taxa.Noun
}
func (e errMoreThanOne) Diagnose(d *report.Diagnostic) {
what := e.what
if what == taxa.Unknown {
what = taxa.Classify(e.first)
}
d.Apply(
report.Message("encountered more than one %v", what),
report.Snippetf(e.second, "help: consider removing this"),
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 {
source.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 source.Spanner
validParents taxa.Set
}
func (e errBadNest) Diagnose(d *report.Diagnostic) {
what := taxa.Classify(e.child)
if e.parent.what == taxa.TopLevel {
d.Apply(
report.Message("unexpected %s at %s", what, e.parent.what),
report.Snippetf(e.child, "this %s cannot be declared here", what),
)
} else {
d.Apply(
report.Message("unexpected %s within %s", what, e.parent.what),
report.Snippetf(e.child, "this %s...", what),
report.Snippetf(e.parent, "...cannot be declared within this %s", e.parent.what),
)
}
if e.validParents.Len() == 1 {
v, _ := iterx.First(e.validParents.All())
if v == taxa.TopLevel {
// This case is just to avoid printing "within a top-level scope",
// which looks wrong.
d.Apply(report.Helpf("this %s can only appear at %s", what, v))
} else {
d.Apply(report.Helpf("this %s can only appear within a %s", what, v))
}
} else {
d.Apply(report.Helpf(
"this %s can only appear within one of %s",
what, e.validParents.Join("or"),
))
}
}
// errUnexpectedMod diagnoses a modifier placed in the wrong position.
type errUnexpectedMod struct {
mod token.Token
where taxa.Place
syntax syntax.Syntax
noDelete bool
}
func (e errUnexpectedMod) Diagnose(d *report.Diagnostic) {
d.Apply(
report.Message("unexpected `%s` modifier %s", e.mod.Keyword(), e.where),
report.Snippet(e.mod),
)
if !e.noDelete {
d.Apply(
just.Justify(e.mod.Context(), e.mod.Span(), "delete it", just.Edit{
Edit: report.Edit{Start: 0, End: e.mod.Span().Len()},
Kind: just.Right,
}))
}
switch k := e.mod.Keyword(); {
case k.IsFieldTypeModifier():
d.Apply(report.Helpf("`%s` only applies to a %s", k, taxa.Field))
case k.IsTypeModifier():
d.Apply(report.Helpf("`%s` only applies to a type definition", k))
case k.IsImportModifier():
d.Apply(report.Helpf("`%s` only applies to an %s", k, taxa.Import))
case k.IsMethodTypeModifier():
d.Apply(report.Helpf("`%s` only applies to an input or output of a %s", k, taxa.Method))
}
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"slices"
"unicode"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"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"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
// legalizeDecl legalizes a declaration.
//
// The parent definition is used for determining if a declaration nesting is
// permitted.
func legalizeDecl(p *parser, parent classified, decl ast.DeclAny) {
switch decl.Kind() {
case ast.DeclKindSyntax:
legalizeSyntax(p, parent, -1, nil, decl.AsSyntax())
case ast.DeclKindPackage:
legalizePackage(p, parent, -1, nil, decl.AsPackage())
case ast.DeclKindImport:
legalizeImport(p, parent, decl.AsImport())
case ast.DeclKindRange:
legalizeRange(p, parent, decl.AsRange())
case ast.DeclKindBody:
body := decl.AsBody()
braces := body.Braces().Span()
p.Errorf("unexpected definition body in %v", parent.what).Apply(
report.Snippet(decl),
report.SuggestEdits(
braces,
"remove these braces",
report.Edit{Start: 0, End: 1},
report.Edit{Start: braces.Len() - 1, End: braces.Len()},
),
)
for decl := range seq.Values(body.Decls()) {
// Treat bodies as being immediately inlined, hence we pass
// parent here and not body as the parent.
legalizeDecl(p, parent, decl)
}
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)
for decl := range seq.Values(body.Decls()) {
legalizeDecl(p, what, decl)
}
}
}
// legalizeDecl legalizes an extension or reserved range.
func legalizeRange(p *parser, parent classified, decl ast.DeclRange) {
in := taxa.Extensions
validParents := taxa.Message.AsSet()
if decl.IsReserved() {
in = taxa.Reserved
validParents = validParents.With(taxa.Enum)
}
if !validParents.Has(parent.what) {
p.Error(errBadNest{parent: parent, child: decl, validParents: validParents})
return
}
if options := decl.Options(); !options.IsZero() {
if in == taxa.Reserved {
p.Error(errHasOptions{decl})
} else {
legalizeCompactOptions(p, options)
}
}
want := taxa.NewSet(taxa.Int, taxa.Range)
if in == taxa.Reserved {
if p.syntax.IsEdition() {
want = want.With(taxa.Ident)
} else {
want = want.With(taxa.String)
}
}
var names, tags []ast.ExprAny
for expr := range seq.Values(decl.Ranges()) {
switch expr.Kind() {
case ast.ExprKindPath:
if in != taxa.Reserved {
break
}
names = append(names, expr)
if p.syntax.IsEdition() {
break
}
p.Errorf("cannot use %vs in %v in %v", taxa.Ident, in, taxa.SyntaxMode).Apply(
report.Snippet(expr),
report.Snippetf(p.syntaxNode, "%v is specified here", taxa.SyntaxMode),
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 str := lit.AsString(); !str.IsZero() {
name := str.Text()
if in == taxa.Extensions {
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: want,
})
break
}
names = append(names, expr)
if p.syntax.IsEdition() {
err := p.Errorf("cannot use %vs in %v in %v", taxa.String, in, taxa.EditionMode).Apply(
report.Snippet(expr),
report.Snippetf(p.syntaxNode, "%v is specified here", taxa.EditionMode),
)
// Only suggest unquoting if it's already an identifier.
if unicodex.IsASCIIIdent(name) {
err.Apply(report.SuggestEdits(
lit, "replace this with an identifier",
report.Edit{
Start: 0, End: lit.Span().Len(),
Replace: name,
},
))
}
break
}
if !unicodex.IsASCIIIdent(name) {
field := taxa.Field
if parent.what == taxa.Enum {
field = taxa.EnumValue
}
p.Errorf("reserved %v name is not a valid identifier", field).Apply(
report.Snippet(expr),
)
break
}
if !str.IsPure() {
p.Warn(errtoken.ImpureString{Token: lit.Token, Where: in.In()})
}
break
}
fallthrough
case ast.ExprKindPrefixed, ast.ExprKindRange:
tags = append(tags, expr)
default:
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: want,
})
}
}
if len(names) > 0 && len(tags) > 0 {
parentWhat := "field"
if parent.what == taxa.Enum {
parentWhat = "value"
}
// We want to diagnose whichever element is least common in the range.
least := names
most := tags
leastWhat := "name"
mostWhat := "tag"
if len(names) > len(tags) ||
// When tied, use whichever comes last lexicographically.
(len(names) == len(tags) && names[0].Span().Start < tags[0].Span().Start) {
least, most = most, least
leastWhat, mostWhat = mostWhat, leastWhat
}
err := p.Errorf("cannot mix tags and names in %s", taxa.Reserved).Apply(
report.Snippetf(least[0], "this %s %s must go in its own %s", parentWhat, leastWhat, taxa.Reserved),
report.Snippetf(most[0], "but expected a %s %s because of this", parentWhat, mostWhat),
)
span := decl.Span()
var edits []report.Edit
for _, expr := range least {
// Delete leading whitespace and trailing whitespace (and a comma, too).
toDelete := expr.Span().GrowLeft(unicode.IsSpace).GrowRight(unicode.IsSpace)
if r, _ := stringsx.Rune(toDelete.After(), 0); r == ',' {
toDelete.End++
}
edits = append(edits, report.Edit{
Start: toDelete.Start - span.Start,
End: toDelete.End - span.Start,
})
}
// If we're moving the last element out of the range, we need to obliterate
// the trailing comma.
comma := slicesx.LastPointer(most).Span()
if comma.End < slicesx.LastPointer(least).Span().End {
comma.Start = comma.End
comma = comma.GrowRight(unicode.IsSpace)
if r, _ := stringsx.Rune(comma.After(), 0); r == ',' {
comma.End++
edits = append(edits, report.Edit{
Start: comma.Start - span.Start,
End: comma.End - span.Start,
})
}
}
edits = append(edits, report.Edit{
Start: span.Len(), End: span.Len(),
Replace: fmt.Sprintf("\n%sreserved %s;", span.Indentation(), iterx.Join(
iterx.Map(slices.Values(least), func(e ast.ExprAny) string { return e.Span().Text() }),
", ",
)),
})
err.Apply(report.SuggestEdits(
span,
fmt.Sprintf("split the %s", taxa.Reserved),
edits...,
))
}
}
// 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 (
"unicode"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/internal/erredition"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// Map of a def kind to the valid parents it can have.
//
// We use taxa.Set here because it already exists and is pretty cheap.
var validDefParents = [...]taxa.Set{
ast.DefKindMessage: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group),
ast.DefKindEnum: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group),
ast.DefKindService: taxa.NewSet(taxa.TopLevel),
ast.DefKindExtend: taxa.NewSet(taxa.TopLevel, taxa.Message, taxa.Group),
ast.DefKindField: taxa.NewSet(taxa.Message, taxa.Group, taxa.Extend, taxa.Oneof),
ast.DefKindOneof: taxa.NewSet(taxa.Message, taxa.Group),
ast.DefKindGroup: taxa.NewSet(taxa.Message, taxa.Group, taxa.Extend),
ast.DefKindEnumValue: taxa.NewSet(taxa.Enum),
ast.DefKindMethod: taxa.NewSet(taxa.Service),
ast.DefKindOption: taxa.NewSet(
taxa.TopLevel, taxa.Message, taxa.Enum, taxa.Service,
taxa.Oneof, taxa.Group, taxa.Method,
),
}
// legalizeDef legalizes a definition.
//
// It will mark the definition as corrupt if it encounters any particularly
// egregious problems.
func legalizeDef(p *parser, parent classified, def ast.DeclDef) {
kind := def.Classify()
if !validDefParents[kind].Has(parent.what) {
p.Error(errBadNest{parent: parent, child: def, validParents: validDefParents[kind]})
}
switch kind {
case ast.DefKindMessage, ast.DefKindEnum, ast.DefKindService, ast.DefKindOneof, ast.DefKindExtend:
legalizeTypeDefLike(p, taxa.Classify(def), def)
case ast.DefKindField, ast.DefKindEnumValue, ast.DefKindGroup:
legalizeFieldLike(p, taxa.Classify(def), def, parent)
case ast.DefKindOption:
legalizeOption(p, def)
case ast.DefKindMethod:
legalizeMethod(p, def)
}
}
// legalizeTypeDefLike legalizes something that resembles a type definition:
// namely, messages, enums, oneofs, services, and extension blocks.
func legalizeTypeDefLike(p *parser, what taxa.Noun, def ast.DeclDef) {
switch {
case def.Name().IsZero():
def.MarkCorrupt()
kw := taxa.Noun(def.Keyword())
p.Errorf("missing name %v", kw.After()).Apply(
report.Snippet(def),
)
case what == taxa.Extend:
legalizePath(p, what.In(), def.Name(), pathOptions{AllowAbsolute: true})
case def.Name().AsIdent().IsZero():
def.MarkCorrupt()
kw := taxa.Noun(def.Keyword())
err := errtoken.Unexpected{
What: def.Name(),
Where: kw.After(),
Want: taxa.Ident.AsSet(),
}
// Look for a separator, and use that instead. We can't "just" pick out
// the first separator, because def.Name might be a one-component
// extension path, e.g. (a.b.c).
def.Name().Components(func(pc ast.PathComponent) bool {
if pc.Separator().IsZero() {
return true
}
err = errtoken.Unexpected{
What: pc.Separator(),
Where: taxa.Ident.In(),
RepeatUnexpected: true,
}
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.
)
}
for mod := range def.Prefixes() {
isType := what == taxa.Message || what == taxa.Enum
if isType && mod.Prefix().IsTypeModifier() {
if p.syntax < syntax.Edition2024 {
p.Error(erredition.TooOld{
Current: p.syntax,
Decl: p.syntaxNode,
Intro: syntax.Edition2024,
What: mod.Prefix(),
Where: mod.PrefixToken(),
})
}
continue
}
suggestExport := isType && mod.Prefix() == keyword.Public
d := p.Error(errUnexpectedMod{
mod: mod.PrefixToken(),
where: what.On(),
syntax: p.syntax,
noDelete: suggestExport,
})
if suggestExport {
d.Apply(report.SuggestEdits(mod, "replace with `export`", report.Edit{
Start: 0, End: mod.Span().Len(),
Replace: "export",
}))
}
}
hasValue := !def.Equals().IsZero() || !def.Value().IsZero()
if hasValue {
p.Error(errtoken.Unexpected{
What: source.Join(def.Equals(), def.Value()),
Where: what.In(),
Got: taxa.Classify(def.Value()),
})
}
if sig := def.Signature(); !sig.IsZero() {
p.Error(errHasSignature{def})
}
if def.Body().IsZero() {
// NOTE: There is currently no way to trip this diagnostic, because
// a message with no body is interpreted as a field.
p.Errorf("missing body for %v", what).Apply(
report.Snippet(def),
)
}
if options := def.Options(); !options.IsZero() {
p.Error(errHasOptions{def})
}
}
// legalizeFieldLike legalizes something that resembles a field definition:
// namely, fields, groups, and enum values.
func legalizeFieldLike(p *parser, what taxa.Noun, def ast.DeclDef, parent classified) {
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(errtoken.Unexpected{
What: def.Name(),
Where: what.In(),
Want: taxa.Ident.AsSet(),
})
}
tag := taxa.FieldTag
if def.Classify() == ast.DefKindEnumValue {
tag = taxa.EnumValue
}
if def.Value().IsZero() {
p.Errorf("missing %v in declaration", tag).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.
)
} else {
legalizeValue(p, def.Span(), ast.ExprAny{}, def.Value(), tag.In())
}
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),
)
}
name := def.Name().AsIdent().Text()
var capitalized bool
for _, r := range name {
capitalized = unicode.IsUpper(r)
break
}
if !capitalized {
p.Errorf("group names must start with an uppercase letter").Apply(
report.Snippet(def.Name()),
)
}
if p.syntax == syntax.Proto2 {
p.Warnf("group syntax is deprecated").Apply(
report.Snippet(def.Type().RemovePrefixes()),
report.Notef("group syntax is not available in proto3 or editions"),
)
} else {
p.Errorf("group syntax is not supported").Apply(
report.Snippet(def.Type().RemovePrefixes()),
report.Notef("group syntax is only available in proto2"),
)
}
case taxa.Field, taxa.EnumValue:
if body := def.Body(); !body.IsZero() {
p.Error(errtoken.Unexpected{
What: body,
Where: what.In(),
})
}
}
if options := def.Options(); !options.IsZero() {
legalizeCompactOptions(p, options)
}
if what == taxa.Field || what == taxa.Group {
var oneof ast.DeclDef
if parent.what == taxa.Oneof {
oneof, _ = parent.Spanner.(ast.DeclDef)
}
legalizeFieldType(p, what, def.Type(), true, ast.TypePrefixed{}, oneof)
}
}
// legalizeOption legalizes an option definition (see legalize_option.go).
func legalizeOption(p *parser, def ast.DeclDef) {
if sig := def.Signature(); !sig.IsZero() {
p.Error(errHasSignature{def})
}
if body := def.Body(); !body.IsZero() {
p.Error(errtoken.Unexpected{
What: body,
Where: taxa.Option.In(),
})
}
if options := def.Options(); !options.IsZero() {
p.Error(errHasOptions{def})
}
legalizeOptionEntry(p, def.AsOption().Option, def.Span())
}
// legalizeMethod legalizes a service method.
func legalizeMethod(p *parser, def ast.DeclDef) {
if def.Name().IsZero() {
def.MarkCorrupt()
p.Errorf("missing name %v", taxa.Method.In()).Apply(
report.Snippet(def),
)
} else if def.Name().AsIdent().IsZero() {
def.MarkCorrupt()
p.Error(errtoken.Unexpected{
What: def.Name(),
Where: taxa.Method.In(),
Want: taxa.Ident.AsSet(),
})
}
hasValue := !def.Equals().IsZero() || !def.Value().IsZero()
if hasValue {
p.Error(errtoken.Unexpected{
What: source.Join(def.Equals(), def.Value()),
Where: taxa.Method.In(),
Got: taxa.Classify(def.Value()),
})
}
sig := def.Signature()
if sig.IsZero() {
def.MarkCorrupt()
p.Errorf("missing %v in %v", taxa.Signature, taxa.Method).Apply(
report.Snippet(def),
)
} else {
// There are cases where part of the signature is present, but the
// span for one or the other half is zero because there were no brackets
// or type.
if sig.Inputs().Span().IsZero() {
def.MarkCorrupt()
p.Errorf("missing %v in %v", taxa.MethodIns, taxa.Method).Apply(
report.Snippetf(def.Name(), "expected %s after this", taxa.Noun(keyword.Parens)),
)
} else {
legalizeMethodParams(p, sig.Inputs(), taxa.MethodIns)
}
if sig.Outputs().Span().IsZero() {
def.MarkCorrupt()
var after source.Spanner
var expected taxa.Noun
switch {
case !sig.Returns().IsZero():
after = sig.Returns()
expected = taxa.Noun(keyword.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 %s after this", expected),
)
} else {
legalizeMethodParams(p, sig.Outputs(), taxa.MethodOuts)
}
}
for mod := range def.Prefixes() {
p.Error(errUnexpectedMod{
mod: mod.PrefixToken(),
where: taxa.Method.On(),
syntax: p.syntax,
})
}
// Methods are unique in that they can end in either a ; or a {}.
// The parser already checks for defs to end with either one of these,
// so we don't need to do anything here.
if options := def.Options(); !options.IsZero() {
p.Error(errHasOptions{def}).Apply(
report.Notef(
"service method options are applied using `option`; declarations " +
"in the `{...}` following the method definition",
),
// TODO: Generate a suggestion for this.
)
}
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"regexp"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/internal/erredition"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"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) {
// Legalize the first syntax node as soon as possible. This is because many
// grammar-level things depend on having figured out the file's syntax
// setting.
for i, decl := range seq.All(file.Decls()) {
if syn := decl.AsSyntax(); !syn.IsZero() {
file := classified{file, taxa.TopLevel}
legalizeSyntax(p, file, i, &p.syntaxNode, decl.AsSyntax())
}
}
if p.syntax == syntax.Unknown {
p.syntax = syntax.Proto2
if p.syntaxNode.IsZero() { // Don't complain if we found a bad syntax node.
p.Warnf("missing %s", taxa.Syntax).Apply(
report.InFile(p.File().Stream().Path()),
report.Notef("this defaults to \"proto2\"; not specifying this "+
"explicitly is discouraged"),
// TODO: suggestion.
)
}
}
var pkg ast.DeclPackage
for i, decl := range seq.All(file.Decls()) {
file := classified{file, taxa.TopLevel}
switch decl.Kind() {
case ast.DeclKindSyntax:
continue // Already did this one in the loop above.
case ast.DeclKindPackage:
legalizePackage(p, file, i, &pkg, decl.AsPackage())
case ast.DeclKindImport:
legalizeImport(p, file, decl.AsImport())
default:
legalizeDecl(p, file, decl)
}
}
if pkg.IsZero() {
p.Warnf("missing %s", taxa.Package).Apply(
report.InFile(p.File().Stream().Path()),
report.Notef(
"not explicitly specifying a package places the file in the "+
"unnamed package; using it strongly is discouraged"),
)
}
}
// legalizeSyntax legalizes a DeclSyntax.
//
// idx is the index of this declaration within its parent; first is a pointer to
// a slot where we can store the first DeclSyntax seen, so we can legalize
// against duplicates.
func legalizeSyntax(p *parser, parent classified, idx int, first *ast.DeclSyntax, decl ast.DeclSyntax) {
in := taxa.Syntax
if decl.IsEdition() {
in = taxa.Edition
}
if parent.what != taxa.TopLevel || first == nil {
p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()})
return
}
file := parent.Spanner.(*ast.File) //nolint:errcheck // Implied by == taxa.TopLevel.
switch {
case !first.IsZero():
p.Errorf("unexpected %s", in).Apply(
report.Snippet(decl),
report.Snippetf(*first, "previous declaration is here"),
report.SuggestEdits(
decl,
"remove this",
report.Edit{Start: 0, End: decl.Span().Len()},
),
report.Notef("a file may contain at most one `syntax` or `edition` declaration"),
)
return
case idx > 0:
p.Errorf("unexpected %s", in).Apply(
report.Snippet(decl),
report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"),
// TODO: Add a suggestion to move this up.
report.Notef("a %s must be the first declaration in a file", in),
)
*first = decl
return
default:
*first = decl
}
if !decl.Options().IsZero() {
p.Error(errHasOptions{decl})
}
expr := decl.Value()
var name string
switch expr.Kind() {
case ast.ExprKindLiteral:
if text := expr.AsLiteral().AsString(); !text.IsZero() {
name = text.Text()
break
}
fallthrough
case ast.ExprKindPath:
name = expr.Span().Text()
case ast.ExprKindInvalid:
return
default:
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: taxa.String.AsSet(),
})
return
}
value := syntax.Lookup(name)
lit := expr.AsLiteral()
switch {
case !value.IsValid():
values := iterx.FilterMap(syntax.All(), func(s syntax.Syntax) (string, bool) {
if s.IsEdition() != (in == taxa.Edition) || !s.IsSupported() {
return "", false
}
return fmt.Sprintf("%q", s), true
})
// NOTE: This matches fallback behavior in ir/lower_walk.go.
fallback := `"proto2"`
if decl.IsEdition() {
fallback = "Edition 2023"
}
p.Errorf("unrecognized %s value", in).Apply(
report.Snippet(expr),
report.Notef("treating the file as %s instead", fallback),
report.Helpf("permitted values: %s", iterx.Join(values, ", ")),
)
case !value.IsSupported():
p.Errorf("sorry, Edition %s is not fully implemented", value).Apply(
report.Snippet(expr),
report.Helpf("Edition %s will be implemented in a future release", value),
)
}
if value.IsValid() {
if value.IsEdition() && in == taxa.Syntax {
p.Errorf("editions must use the `edition` keyword").Apply(
report.Snippet(decl.KeywordToken()),
report.SuggestEdits(decl.KeywordToken(), "replace with `edition`", report.Edit{
Start: 0, End: decl.KeywordToken().Span().Len(),
Replace: "edition",
}),
)
}
if !value.IsEdition() && in == taxa.Edition {
lit := expr.Span().Text()
p.Errorf("%s use the `syntax` keyword", lit).Apply(
report.Snippet(decl.KeywordToken()),
report.SuggestEdits(decl.KeywordToken(), "replace with `syntax`", report.Edit{
Start: 0, End: decl.KeywordToken().Span().Len(),
Replace: "syntax",
}),
report.Helpf("%s is technically an edition, but cannot use `edition`", lit),
)
}
if 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: `"`},
),
)
} else if str := lit.AsString(); !str.IsZero() && !str.IsPure() {
p.Warn(errtoken.ImpureString{Token: lit.Token, Where: in.In()})
}
}
if p.syntax == syntax.Unknown {
p.syntax = value
}
}
// legalizePackage legalizes a DeclPackage.
//
// idx is the index of this declaration within its parent; first is a pointer to
// a slot where we can store the first DeclPackage seen, so we can legalize
// against duplicates.
func legalizePackage(p *parser, parent classified, idx int, first *ast.DeclPackage, decl ast.DeclPackage) {
if parent.what != taxa.TopLevel || first == nil {
p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()})
return
}
file := parent.Spanner.(*ast.File) //nolint:errcheck // Implied by == taxa.TopLevel.
switch {
case !first.IsZero():
p.Errorf("unexpected %s", taxa.Package).Apply(
report.Snippet(decl),
report.Snippetf(*first, "previous declaration is here"),
report.SuggestEdits(
decl,
"remove this",
report.Edit{Start: 0, End: decl.Span().Len()},
),
report.Notef("a file must contain exactly one %s", taxa.Package),
)
return
case idx > 0:
if idx > 1 || file.Decls().At(0).Kind() != ast.DeclKindSyntax {
p.Warnf("the %s should be placed at the top of the file", taxa.Package).Apply(
report.Snippet(decl),
report.Snippetf(file.Decls().At(idx-1), "previous declaration is here"),
// TODO: Add a suggestion to move this up.
report.Helpf(
"a file's %s should immediately follow the `syntax` or `edition` declaration",
taxa.Package,
),
)
return
}
fallthrough
default:
*first = decl
}
if !decl.Options().IsZero() {
p.Error(errHasOptions{decl})
}
if decl.Path().IsZero() {
p.Errorf("missing path in %s", taxa.Package).Apply(
report.Snippet(decl),
report.Helpf(
"to place a file in the unnamed package, omit the %s; however, "+
"using the unnamed package is discouraged",
taxa.Package,
),
)
}
legalizePath(p, taxa.Package.In(), decl.Path(), pathOptions{
MaxBytes: 512,
MaxComponents: 101,
})
}
// legalizeImport legalizes a DeclImport.
func legalizeImport(p *parser, parent classified, decl ast.DeclImport) {
if parent.what != taxa.TopLevel {
p.Error(errBadNest{parent: parent, child: decl, validParents: taxa.TopLevel.AsSet()})
return
}
if !decl.Options().IsZero() {
p.Error(errHasOptions{decl})
}
in := taxa.Classify(decl)
expr := decl.ImportPath()
switch expr.Kind() {
case ast.ExprKindLiteral:
if lit := expr.AsLiteral().AsString(); !lit.IsZero() {
if !lit.IsPure() {
// Only warn for cases where the import is alphanumeric.
if isOrdinaryFilePath.MatchString(lit.Text()) {
p.Warn(errtoken.ImpureString{Token: lit.Token(), Where: in.In()})
}
}
break
}
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: taxa.String.AsSet(),
})
return
case ast.ExprKindPath:
p.Error(errtoken.Unexpected{
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.Helpf("Protobuf does not support importing symbols by name, instead, " +
"try importing a file, e.g. `import \"google/protobuf/descriptor.proto\";`"),
)
return
case ast.ExprKindInvalid:
if decl.Semicolon().IsZero() {
// If there is a missing semicolon, this is some other kind of syntax error
// so we should avoid diagnosing it twice.
return
}
p.Errorf("missing import path in %s", in).Apply(
report.Snippet(decl),
)
return
default:
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: taxa.String.AsSet(),
})
return
}
var isOption bool
for i, mod := range seq.All(decl.ModifierTokens()) {
if i > 0 {
p.Errorf("unexpected `%s` modifier in %s", mod.Text(), in).Apply(
report.Snippet(mod),
report.Snippetf(source.Join(
decl.KeywordToken(),
decl.ModifierTokens().At(0),
), "already modified here"),
)
continue
}
switch k := mod.Keyword(); k {
case keyword.Public:
case keyword.Weak:
p.SoftError(p.syntax >= syntax.Edition2024, erredition.TooNew{
Current: p.syntax,
Decl: p.syntaxNode,
Deprecated: syntax.Proto2,
DeprecatedReason: "`import weak` is not implemented correctly in most Protobuf implementations",
Removed: syntax.Edition2024,
RemovedReason: "`import weak` has been replaced with `import option`",
What: "import weak",
Where: mod,
})
case keyword.Option:
p.importOptionNode = decl
isOption = true
if p.syntax < syntax.Edition2024 {
p.Error(erredition.TooOld{
Current: p.syntax,
Decl: p.syntaxNode,
Intro: syntax.Edition2024,
What: "import option",
Where: mod,
})
}
default:
d := p.Error(errUnexpectedMod{
mod: mod,
where: taxa.Import.In(),
syntax: p.syntax,
noDelete: k == keyword.Export || k == keyword.Optional,
})
switch k {
case keyword.Export:
d.Apply(report.SuggestEdits(mod, "replace with `public`", report.Edit{
Start: 0, End: mod.Span().Len(),
Replace: "public",
}))
case keyword.Optional:
d.Apply(report.SuggestEdits(mod, "replace with `option`", report.Edit{
Start: 0, End: mod.Span().Len(),
Replace: "option",
}))
}
}
}
if !isOption && !p.importOptionNode.IsZero() {
p.Errorf("%s after `import option`", taxa.Import).Apply(
report.Snippet(decl),
report.Snippetf(p.importOptionNode, "previous `import option` here"),
report.Helpf("`import option`s must be the last imports in a file"),
)
}
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"strings"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"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"
)
// legalizeCompactOptions legalizes a [...] of options.
//
// All this really does is check that opt is non-empty and then forwards each
// entry to [legalizeOptionEntry].
func legalizeCompactOptions(p *parser, opts ast.CompactOptions) {
entries := opts.Entries()
if entries.Len() == 0 {
p.Errorf("%s cannot be empty", taxa.CompactOptions).Apply(
report.Snippetf(opts, "help: remove this"),
)
return
}
for opt := range seq.Values(entries) {
legalizeOptionEntry(p, opt, opt.Span())
}
}
// legalizeCompactOptions is the common path for legalizing options, either
// from an option def or from compact options.
//
// We can't perform type-checking yet, so all we can really do here
// is check that the path is ok for an option. Legalizing the value cannot
// happen until type-checking in IR construction.
func legalizeOptionEntry(p *parser, opt ast.Option, decl source.Span) {
if opt.Path.IsZero() {
p.Errorf("missing %v path", taxa.Option).Apply(
report.Snippet(decl),
)
// Don't bother legalizing if the value is zero. That can only happen
// when the user writes just option;, which will produce two very
// similar diagnostics.
return
}
legalizePath(p, taxa.Option.In(), opt.Path, pathOptions{
AllowExts: true,
})
if opt.Value.IsZero() {
p.Errorf("missing %v", taxa.OptionValue).Apply(
report.Snippet(decl),
)
} else {
legalizeValue(p, decl, ast.ExprAny{}, opt.Value, taxa.OptionValue.In())
}
}
// legalizeValue conservatively legalizes a def's value.
func legalizeValue(p *parser, decl source.Span, parent ast.ExprAny, value ast.ExprAny, where taxa.Place) {
// 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:
legalizeLiteral(p, value.AsLiteral())
case ast.ExprKindPath:
// Qualified paths are allowed, since we want to diagnose them once we
// have symbol lookup information so that we can suggest a proper
// reference.
case ast.ExprKindPrefixed:
// - is only allowed before certain identifiers, but which ones is
// quite tricky to determine. This needs to happen during constant
// evaluation, so repeating that logic here is somewhat redundant.
case ast.ExprKindArray:
array := value.AsArray().Elements()
switch {
case parent.IsZero() && where.Subject() == taxa.OptionValue:
err := p.Error(errtoken.Unexpected{
What: value,
Where: where,
}).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:
// TODO: generate a suggestion for this.
// 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:
for e := range seq.Values(array) {
legalizeValue(p, decl, value, e, where)
}
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().Keyword() == keyword.Angles {
var err *report.Diagnostic
if parent.IsZero() {
err = p.Errorf("cannot use `<...>` for %s here", taxa.Dict)
} else {
err = p.Warnf("using `<...>` for %s is not recommended", taxa.Dict)
}
err.Apply(
report.Snippet(value),
report.SuggestEdits(
dict, "use `{...}` instead",
report.Edit{Start: 0, End: 1, Replace: "{"},
report.Edit{Start: dict.Span().Len() - 1, End: dict.Span().Len(), Replace: "}"},
),
report.Notef("`<...>` are only permitted for sub-messages within a %s, but as top-level option values", taxa.Dict),
report.Helpf("`<...>` %ss are an obscure feature and not recommended", taxa.Dict),
)
}
for kv := range seq.Values(dict.Elements()) {
want := taxa.NewSet(taxa.FieldName, taxa.ExtensionName, taxa.TypeURL)
switch kv.Key().Kind() {
case ast.ExprKindLiteral:
legalizeLiteral(p, kv.Key().AsLiteral())
case ast.ExprKindPath:
path := kv.Key().AsPath()
first, _ := iterx.First(path.Components)
if !first.AsExtension().IsZero() {
// TODO: move this into ir/lower_eval.go
p.Errorf("cannot name extension field using `(...)` in %s", taxa.Dict).Apply(
report.Snippetf(path, "expected this to be wrapped in `[...]` instead"),
report.SuggestEdits(
path, "replace the `(...)` with `[...]`",
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() {
if !elem.AsLiteral().IsZero() {
// Allow literals in this position, since we can diagnose
// them better later.
break
}
p.Error(errtoken.Unexpected{
What: kv.Key(),
Where: taxa.DictField.In(),
Want: want,
})
break
}
slashIdx, _ := iterx.Find(path.Components, func(pc ast.PathComponent) bool {
return pc.Separator().Keyword() == keyword.Div
})
if slashIdx != -1 {
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(errtoken.Unexpected{
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...
for e := range seq.Values(kv.Value().AsArray().Elements()) {
if e.Kind() == ast.ExprKindDict {
continue
}
p.Error(errtoken.Unexpected{
What: e,
Where: taxa.Array.In(),
Want: taxa.Dict.AsSet(),
}).Apply(
report.Snippetf(kv.Key(), "because this %s is missing a `:`", taxa.DictField),
report.Notef(
"the `:` can be omitted in a %s, but only if the value is a %s or a %s of them",
taxa.DictField, taxa.Dict, taxa.Array),
)
break // Only diagnose the first one.
}
}
legalizeValue(p, decl, kv.AsAny(), kv.Value(), where)
}
default:
p.Error(errtoken.Unexpected{What: value, Where: where})
}
}
// legalizeLiteral conservatively legalizes a literal.
func legalizeLiteral(p *parser, value ast.ExprLiteral) {
switch value.Kind() {
case token.Number:
n := value.AsNumber()
if !n.IsValid() {
return
}
what := taxa.Int
if n.IsFloat() {
what = taxa.Float
}
base := n.Base()
var validBase bool
switch base {
case 2:
validBase = false
case 8:
validBase = n.Prefix().Text() == "0"
case 10:
validBase = true
case 16:
validBase = what == taxa.Int
}
// Diagnose against number literals we currently accept but which are not
// part of Protobuf.
if !validBase {
d := p.Errorf("unsupported base for %s", what)
if what == taxa.Int {
switch base {
case 2:
v, _ := n.Value().Int(nil)
d.Apply(
report.SuggestEdits(value, "use a hexadecimal literal instead", report.Edit{
Start: 0,
End: len(value.Text()),
Replace: fmt.Sprintf("%#.0x%s", v, n.Suffix().Text()),
}),
report.Notef("Protobuf does not support binary literals"),
)
return
case 8:
d.Apply(
report.SuggestEdits(value, "remove the `o`", report.Edit{Start: 1, End: 2}),
report.Notef("octal literals are prefixed with `0`, not `0o`"),
)
return
}
}
var name string
switch base {
case 2:
name = "binary"
case 8:
name = "octal"
case 16:
name = "hexadecimal"
}
d.Apply(
report.Snippet(value),
report.Notef("Protobuf does not support %s %ss", name, what),
)
return
}
if suffix := n.Suffix(); suffix.Text() != "" {
p.Errorf("unrecognized suffix for %s", what).Apply(
report.SuggestEdits(suffix, "delete it", report.Edit{
Start: 0,
End: len(suffix.Text()),
}),
)
return
}
if n.HasSeparators() {
p.Errorf("%s contains underscores", what).Apply(
report.SuggestEdits(value, "remove these underscores", report.Edit{
Start: 0,
End: len(value.Text()),
Replace: strings.ReplaceAll(value.Text(), "_", ""),
}),
report.Notef("Protobuf does not support Go/Java/Rust-style thousands separators"),
)
return
}
case token.String:
s := value.AsString()
if sigil := s.Prefix(); sigil.Text() != "" {
p.Errorf("unrecognized prefix for %s", taxa.String).Apply(
report.SuggestEdits(sigil, "delete it", report.Edit{
Start: 0,
End: len(sigil.Text()),
}),
)
}
// NOTE: we do not need to legalize triple-quoted strings:
// """a""" is just "" "a" "" without whitespace, which have equivalent
// contents.
}
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// pathOptions is configuration for [legalizePath].
type pathOptions struct {
// If set, the path must be relative.
AllowAbsolute bool
// If set, the path may contain precisely one `/` separator.
AllowSlash bool
// If set, the path may contain extension components.
AllowExts bool
// If nonzero, the maximum number of bytes in the path.
MaxBytes int
// If nonzero, the maximum number of components in the path.
MaxComponents int
}
// legalizePath legalizes a path to satisfy the configuration in opts.
func legalizePath(p *parser, where taxa.Place, path ast.Path, opts pathOptions) (ok bool) {
ok = true
var bytes, components int
var slash token.Token
for i, pc := range iterx.Enumerate(path.Components) {
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().Text() == "." {
p.Errorf("unexpected absolute path %s", where).Apply(
report.Snippetf(path, "expected a path without a leading `%s`", pc.Separator().Text()),
report.SuggestEdits(path, "remove the leading `.`", report.Edit{Start: 0, End: 1}),
)
ok = false
continue
}
if pc.Separator().Keyword() == keyword.Div {
if !opts.AllowSlash {
p.Errorf("unexpected `/` in path %s", where).Apply(
report.Snippetf(pc.Separator(), "help: replace this with a `.`"),
)
ok = false
continue
} else if !slash.IsZero() {
p.Errorf("type URL can only contain a single `/`").Apply(
report.Snippet(pc.Separator()),
report.Snippetf(slash, "first one is here"),
)
ok = false
continue
}
slash = pc.Separator()
}
if ext := pc.AsExtension(); !ext.IsZero() {
if opts.AllowExts {
ok = legalizePath(p, where, ext, pathOptions{
AllowAbsolute: true,
AllowExts: false,
})
if !ok {
continue
}
} 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
continue
}
}
}
if ok {
if opts.MaxBytes > 0 && bytes > opts.MaxBytes {
p.Errorf("path %s is too large", where).Apply(
report.Snippet(path),
report.Notef("Protobuf imposes a limit of %v bytes here", opts.MaxBytes),
)
} else if opts.MaxComponents > 0 && components > opts.MaxComponents {
p.Errorf("path %s is too large", where).Apply(
report.Snippet(path),
report.Notef("Protobuf imposes a limit of %v components here", opts.MaxComponents),
)
}
}
return ok
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// legalizeMethodParams legalizes part of the signature of a method.
func legalizeMethodParams(p *parser, list ast.TypeList, what taxa.Noun) {
if list.Len() != 1 {
p.Errorf("expected exactly one type in %s, got %d", what, list.Len()).Apply(
report.Snippet(list),
)
return
}
ty := list.At(0)
switch ty.Kind() {
case ast.TypeKindPath:
legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true})
case ast.TypeKindPrefixed:
prefixed := ty.AsPrefixed()
var mod ast.TypePrefixed
for {
switch {
case !prefixed.Prefix().IsMethodTypeModifier():
p.Error(errUnexpectedMod{
mod: prefixed.PrefixToken(),
where: taxa.Signature.In(),
syntax: p.syntax,
})
case !mod.IsZero():
p.Error(errMoreThanOne{
first: mod.PrefixToken(),
second: prefixed.PrefixToken(),
what: taxa.Noun(keyword.Stream),
})
default:
mod = prefixed
}
switch prefixed.Type().Kind() {
case ast.TypeKindPath:
legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true})
return
case ast.TypeKindPrefixed:
prefixed = prefixed.Type().AsPrefixed()
continue
}
break
}
ty = prefixed.Type()
fallthrough
default:
p.Error(errtoken.Unexpected{
What: ty,
Where: what.In(),
Want: taxa.NewSet(taxa.MessageType),
})
}
}
// legalizeFieldType legalizes the type of a message field.
func legalizeFieldType(p *parser, what taxa.Noun, ty ast.TypeAny, topLevel bool, mod ast.TypePrefixed, oneof ast.DeclDef) {
expected := taxa.TypePath.AsSet()
if oneof.IsZero() {
switch p.syntax {
case syntax.Proto2:
expected = taxa.NewSet(
taxa.Noun(keyword.Required), taxa.Noun(keyword.Optional), taxa.Noun(keyword.Repeated))
case syntax.Proto3:
expected = taxa.NewSet(
taxa.TypePath, taxa.Noun(keyword.Optional), taxa.Noun(keyword.Repeated))
default:
expected = taxa.NewSet(
taxa.TypePath, taxa.Noun(keyword.Repeated))
}
}
switch ty.Kind() {
case ast.TypeKindPath:
if topLevel && p.syntax == syntax.Proto2 && oneof.IsZero() {
p.Error(errtoken.Unexpected{
What: ty,
Want: expected,
}).Apply(
report.SuggestEdits(ty, "use the `optional` modifier", report.Edit{
Replace: "optional ",
}),
report.Notef("modifiers are required in %s", syntax.Proto2),
)
}
legalizePath(p, what.In(), ty.AsPath().Path, pathOptions{AllowAbsolute: true})
case ast.TypeKindPrefixed:
ty := ty.AsPrefixed()
if !mod.IsZero() {
p.Errorf("multiple modifiers on %v type", taxa.Field).Apply(
report.Snippet(ty.PrefixToken()),
report.Snippetf(mod.PrefixToken(), "previous one is here"),
just.Justify(p.File().Stream(), ty.PrefixToken().Span(), "delete it", just.Edit{
Edit: report.Edit{Start: 0, End: ty.PrefixToken().Span().Len()},
Kind: just.Right,
}),
)
} else {
if mod.IsZero() {
mod = ty
}
if !oneof.IsZero() {
d := p.Error(errtoken.Unexpected{
What: ty.PrefixToken(),
Want: expected,
}).Apply(
report.Snippetf(oneof, "within this %s", taxa.Oneof),
just.Justify(p.File().Stream(), ty.PrefixToken().Span(), "delete it", just.Edit{
Edit: report.Edit{Start: 0, End: ty.PrefixToken().Span().Len()},
Kind: just.Right,
}),
report.Notef("fields defined as part of a %s may not have modifiers applied to them", taxa.Oneof),
)
if ty.Prefix() == keyword.Repeated {
d.Apply(report.Helpf(
"to emulate a repeated field in a %s, define a local message type with a single repeated field",
taxa.Oneof))
}
} else {
switch k := ty.Prefix(); k {
case keyword.Required:
switch p.syntax {
case syntax.Proto2:
// TODO: This appears verbatim in lower_validate. Move this check
// into IR lowering?
p.Warnf("required fields are deprecated").Apply(
report.Snippet(ty.PrefixToken()),
report.Helpf(
"do not attempt to change this to `optional` if the field is "+
"already in-use; doing so is a wire protocol break"),
)
default:
p.Error(errtoken.Unexpected{
What: ty.PrefixToken(),
Want: expected,
}).Apply(
just.Justify(p.File().Stream(), ty.PrefixToken().Span(), "delete it", just.Edit{
Edit: report.Edit{Start: 0, End: ty.PrefixToken().Span().Len()},
Kind: just.Right,
}),
report.Helpf("required fields are only permitted in %s; even then, their use is strongly discouraged",
syntax.Proto2),
)
}
case keyword.Optional:
if p.syntax.IsEdition() {
p.Error(errtoken.Unexpected{
What: ty.PrefixToken(),
Want: expected,
}).Apply(
just.Justify(p.File().Stream(), ty.PrefixToken().Span(), "delete it", just.Edit{
Edit: report.Edit{Start: 0, End: ty.PrefixToken().Span().Len()},
Kind: just.Right,
}),
report.Helpf(
"in %s, the presence behavior of a singular field "+
"is controlled with `[feature.field_presence = ...]`, with "+
"the default being equivalent to a %s `optional` field",
taxa.EditionMode, syntax.Proto2),
report.Helpf("see <https://protobuf.com/docs/language-spec#field-presence>"),
)
}
case keyword.Repeated:
break
default:
d := p.Error(errUnexpectedMod{
mod: ty.PrefixToken(),
where: what.On(),
syntax: p.syntax,
noDelete: k == keyword.Option,
})
if k == keyword.Option {
d.Apply(report.SuggestEdits(ty.PrefixToken(), "replace with `optional`", report.Edit{
Start: 0, End: ty.PrefixToken().Span().Len(),
Replace: "optional",
}))
}
}
}
}
inner := ty.Type()
switch inner.Kind() {
case ast.TypeKindPath, ast.TypeKindPrefixed:
legalizeFieldType(p, what, inner, false, mod, oneof)
default:
p.Error(errtoken.Unexpected{
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 !oneof.IsZero():
p.Errorf("map fields are not allowed inside of a %s", taxa.Oneof).Apply(
report.Snippet(ty),
report.Helpf(
"to emulate a map field in a %s, fine a local message type with a single map field",
taxa.Oneof),
)
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()
switch k.Kind() {
case ast.TypeKindPath:
legalizeFieldType(p, what, k, false, ast.TypePrefixed{}, oneof)
case ast.TypeKindPrefixed:
p.Error(errtoken.Unexpected{
What: k.AsPrefixed().PrefixToken(),
Where: taxa.MapKey.In(),
})
default:
p.Error(errtoken.Unexpected{
What: k,
Where: taxa.MapKey.In(),
Want: taxa.TypePath.AsSet(),
})
}
switch v.Kind() {
case ast.TypeKindPath:
legalizeFieldType(p, what, v, false, ast.TypePrefixed{}, oneof)
case ast.TypeKindPrefixed:
p.Error(errtoken.Unexpected{
What: v.AsPrefixed().PrefixToken(),
Where: taxa.MapValue.In(),
})
default:
p.Error(errtoken.Unexpected{
What: v,
Where: taxa.MapValue.In(),
Want: taxa.TypePath.AsSet(),
})
}
}
}
}
// 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/lexer"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// lex is a combined lexer for Protobuf and CEL.
var lex = lexer.Lexer{
OnKeyword: func(k keyword.Keyword) lexer.OnKeyword {
switch k {
case keyword.Comment:
return lexer.LineComment
case keyword.LComment, keyword.RComment:
return lexer.BlockComment
case keyword.LParen, keyword.LBracket, keyword.LBrace,
keyword.RParen, keyword.RBracket, keyword.RBrace:
return lexer.BracketKeyword
default:
if k.IsProtobuf() || k.IsCEL() {
return lexer.SoftKeyword
}
return lexer.DiscardKeyword
}
},
IsAffix: func(affix string, kind token.Kind, suffix bool) bool {
switch kind {
case token.Number:
return suffix && slicesx.Among(affix, "u", "U")
case token.String:
return !suffix && slicesx.Among(affix, "r", "b", "rb")
default:
return false
}
},
NumberCanStartWithDot: true,
OldStyleOctal: true,
RequireASCIIIdent: true,
EscapeExtended: true,
EscapeAsk: true,
EscapeOctal: true,
EscapePartialX: true,
EscapeUppercaseX: true,
EscapeOldStyleUnicode: true,
}
// Parse lexes and parses the Protobuf file tracked by ctx.
//
// Diagnostics generated by this process are written to errs. Returns whether
// parsing succeeded without errors.
//
// Parse will freeze the stream in ctx when it is done.
func Parse(path string, source *source.File, r *report.Report) (file *ast.File, ok bool) {
prior := len(r.Diagnostics)
r.SaveOptions(func() {
if path == "google/protobuf/descriptor.proto" {
// descriptor.proto contains required fields, which we warn against.
// However, that would cause literally every project ever to have
// warnings, and in general, any warnings we add should not ding
// the worst WKT file of them all.
r.SuppressWarnings = true
}
file = ast.New(path, lex.Lex(source, r))
parse(file, r)
defer file.Stream().Freeze()
})
ok = true
for _, d := range r.Diagnostics[prior:] {
if d.Level() >= report.Error {
ok = false
break
}
}
return file, ok
}
// parse implements the core parser loop.
func parse(file *ast.File, errs *report.Report) {
p := &parser{
Nodes: file.Nodes(),
Report: errs,
}
defer p.CatchICE(false, nil)
c := file.Stream().Cursor()
var mark token.CursorMark
for !c.Done() {
ensureProgress(c, &mark)
node := parseDecl(p, c, taxa.TopLevel)
if !node.IsZero() {
seq.Append(file.Decls(), node)
}
}
p.parseComplete = true
legalizeFile(p, file)
}
// ensureProgress is used to make sure that the parser makes progress on each
// loop iteration. See mustProgress in lex_state.go for the lexer equivalent.
func ensureProgress(c *token.Cursor, m *token.CursorMark) {
next := c.Mark()
if *m == next {
panic("protocompile/parser: parser failed to make progress; this is a bug in protocompile")
}
*m = next
}
// 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 (
"slices"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
type exprComma struct {
expr ast.ExprAny
comma token.Token
}
func (e exprComma) Span() source.Span {
return e.expr.Span()
}
// parseDecl parses any Protobuf declaration.
//
// This function will always advance cursor if it is not empty.
func parseDecl(p *parser, c *token.Cursor, in taxa.Noun) ast.DeclAny {
first := c.Peek()
if first.IsZero() {
return ast.DeclAny{}
}
var unexpected []token.Token
for !c.Done() && !canStartDecl(first) {
unexpected = append(unexpected, c.Next())
first = c.Peek()
}
switch len(unexpected) {
case 0:
case 1:
p.Error(errtoken.Unexpected{
What: unexpected[0],
Where: in.In(),
Want: startsDecl,
})
case 2:
p.Error(errtoken.Unexpected{
What: source.JoinSeq(slices.Values(unexpected)),
Where: in.In(),
Want: startsDecl,
Got: "tokens",
})
}
if first.Keyword() == keyword.Semi {
c.Next()
// This is an empty decl.
return p.NewDeclEmpty(first).AsAny()
}
// This is a bare declaration body.
if canStartBody(first) {
return parseBody(p, c.Next(), in).AsAny()
}
// We need to parse a path here. At this point, we need to generate a
// diagnostic if there is anything else in our way before hitting parsePath.
if !canStartPath(first) {
return ast.DeclAny{}
}
// Parse a type followed by a path. This is the "most general" prefix of almost all
// possible productions in a decl. If the type is a TypePath which happens to be
// a keyword, we try to parse the appropriate thing (with one token of lookahead),
// and otherwise parse a field.
mark := c.Mark()
ty, path := parseTypeAndPath(p, c, in.In())
// Extract a putative leading keyword from this. Note that a field's type,
// if relative, cannot start with any of the following identifiers:
//
// message enum oneof reserved
// extensions extend option
// optional required repeated
//
// This is used here to disambiguated between a generic DeclDef and one of
// the other decl nodes.
var kw token.Token
if path := ty.RemovePrefixes().AsPath(); !path.IsZero() {
kw = path.AsIdent()
}
// Check for the various special cases.
next := c.Peek()
switch kw.Keyword() {
case keyword.Syntax, keyword.Edition:
// Syntax and edition are parsed only at the top level. Otherwise, they
// start a def.
if in != taxa.TopLevel {
break
}
args := ast.DeclSyntaxArgs{
Keyword: kw,
}
in := taxa.Syntax
if kw.Keyword() == keyword.Edition {
in = taxa.Edition
}
if c.Done() {
// If we see an EOF at this point, suggestions from the next
// few stanzas will be garbage.
p.Error(errtoken.UnexpectedEOF(c, in.In()))
} else {
eq, err := parseEquals(p, c, in)
args.Equals = eq
if err != nil {
p.Error(err)
}
// Regardless of if we see an = sign, try to parse an expression if we
// can.
if !args.Equals.IsZero() || canStartExpr(c.Peek()) {
args.Value = parseExpr(p, c, in.In())
}
args.Options = tryParseOptions(p, c, in)
args.Semicolon, err = parseSemi(p, c, in)
// Only diagnose a missing semicolon if we successfully parsed some
// kind of partially-valid expression. Otherwise, we might diagnose
// the same extraneous/missing ; twice.
//
// For example, consider `syntax = ;`. WHen we enter parseExpr, it
// will complain about the unexpected ;.
//
// TODO: Add something like ExprError and check if args.Value
// contains one.
if err != nil && !args.Value.IsZero() {
p.Error(err)
}
}
return p.NewDeclSyntax(args).AsAny()
case keyword.Package:
// Package is only parsed only at the top level. Otherwise, it starts
// a def.
//
// TODO: This is not ideal. What we should do instead is to parse a
// package unconditionally, and if this is not the top level AND
// the path is an identifier, rewind and reinterpret this as a field,
// much like we do with ranges in some cases.
if in != taxa.TopLevel {
break
}
in := taxa.Package
args := ast.DeclPackageArgs{
Keyword: kw,
Path: path,
}
if c.Done() && path.IsZero() {
// If we see an EOF at this point, suggestions from the next
// few stanzas will be garbage.
p.Error(errtoken.UnexpectedEOF(c, in.In()))
} else {
args.Options = tryParseOptions(p, c, in)
semi, err := parseSemi(p, c, in)
args.Semicolon = semi
if err != nil {
p.Error(err)
}
}
return p.NewDeclPackage(args).AsAny()
case keyword.Import:
// We parse imports inside of any body. However, outside of the top
// level, we interpret import foo as a field. import foo.bar is still
// an import, because we want to diagnose what is clearly an attempt to
// import by path rather than by file.
//
// 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() && next.Kind() != token.String) {
break
}
// This is definitely a field.
if next.Keyword() == keyword.Assign {
break
}
args := ast.DeclImportArgs{
Keyword: kw,
}
in := taxa.Import
for path.AsIdent().Keyword().IsModifier() {
args.Modifiers = append(args.Modifiers, path.AsIdent())
path = ast.Path{}
if canStartPath(c.Peek()) {
path = parsePath(p, c)
}
}
if !path.IsZero() {
// This will catch someone writing `import foo.bar;` when we legalize.
args.ImportPath = ast.ExprPath{Path: path}.AsAny()
}
if args.ImportPath.IsZero() && canStartExpr(next) {
args.ImportPath = parseExpr(p, c, in.In())
}
args.Options = tryParseOptions(p, c, in)
if args.ImportPath.IsZero() && c.Done() {
// If we see an EOF at this point, suggestions from the next
// few stanzas will be garbage.
p.Error(errtoken.UnexpectedEOF(c, in.In()))
} else {
semi, err := parseSemi(p, c, in)
args.Semicolon = semi
if err != nil {
p.Error(err)
}
}
return p.NewDeclImport(args).AsAny()
case keyword.Reserved, keyword.Extensions:
if next.Keyword() == keyword.Assign {
// If whatever follows the path is an =, we're going to assume this
// is trying to be a field.
break
}
// Otherwise, rewind the cursor to before we parsed a type, and
// parse a range instead. Rewinding is necessary because otherwise we get
// into an annoying situation where if we have e.g. reserved foo to bar;
// we have already consumed reserved foo, but we want to push foo
// through the expression machinery to get foo to bar as a single
// expression.
c.Rewind(mark)
return parseRange(p, c).AsAny()
}
def := &defParser{
parser: p,
c: c,
kw: kw,
in: in,
args: ast.DeclDefArgs{Type: ty, Name: path},
}
return def.parse().AsAny()
}
// parseBody parses a ({}-delimited) body of declarations.
func parseBody(p *parser, braces token.Token, in taxa.Noun) ast.DeclBody {
body := p.NewDeclBody(braces)
// Drain the contents of the body into it. Remember,
// parseDecl must always make progress if there is more to
// parse.
c := braces.Children()
for !c.Done() {
if next := parseDecl(p, c, in); !next.IsZero() {
seq.Append(body.Decls(), next)
}
}
return body
}
// parseRange parses a reserved/extensions range.
func parseRange(p *parser, c *token.Cursor) ast.DeclRange {
// Consume the keyword token.
kw := c.Next()
in := taxa.Extensions
if kw.Keyword() == keyword.Reserved {
in = taxa.Reserved
}
var (
// badExpr keeps track of whether we exited the loop due to a parse
// error or because we hit ; or [ or EOF.
badExpr bool
exprs []exprComma
)
// Note that this means that we do not parse `reserved [1, 2, 3];`
// "correctly": that is, as a reserved range whose first expression is an
// array. Instead, we parse it as an invalid compact options.
//
// TODO: This could be mitigated with backtracking: if the compact options
// is empty, or if the first comma occurs without seeing an =, we can choose
// to parse this as an array, instead.
if !canStartOptions(c.Peek()) {
var last token.Token
d := delimited[ast.ExprAny]{
p: p, c: c,
what: taxa.Expr,
in: in,
required: true,
exhaust: false,
parse: func(c *token.Cursor) (ast.ExprAny, bool) {
last = c.Peek()
expr := parseExpr(p, c, in.In())
badExpr = expr.IsZero()
return expr, !expr.IsZero()
},
start: canStartExpr,
stop: func(t token.Token) bool {
if slicesx.Among(t.Keyword(), keyword.Semi, keyword.Brackets) {
return true
}
// After the first element, stop if we see an identifier
// coming up. This is for a case like this:
//
// reserved 1, 2
// message Foo {}
//
// If we don't do this, message will be interpreted as an
// expression.
if !last.IsZero() && t.Kind() == token.Ident {
// However, this will cause
//
// reserved foo, bar baz;
//
// to treat baz as a new declaration, rather than assume a
// missing comma. Distinguishing this case is tricky: the
// cheapest option is to check whether a newline exists between
// this token and the last position passed to parse.
//
// This case will not be hit for valid syntax, so it's ok
// to do 2*O(log n) line lookups.
prev := last.Span().EndLoc()
next := t.Span().StartLoc()
return prev.Line != next.Line
}
return false
},
}
for expr, comma := range d.iter {
exprs = append(exprs, exprComma{expr, comma})
}
}
options := tryParseOptions(p, c, in)
// Parse a semicolon, if possible.
semi, err := parseSemi(p, c, in)
if err != nil && (!options.IsZero() || !badExpr) {
p.Error(err)
}
r := p.NewDeclRange(ast.DeclRangeArgs{
Keyword: kw,
Options: options,
Semicolon: semi,
})
for _, e := range exprs {
r.Ranges().AppendComma(e.expr, e.comma)
}
return r
}
// 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(),
what: taxa.Type,
in: in,
required: true,
exhaust: true,
parse: func(c *token.Cursor) (ast.TypeAny, bool) {
ty := parseType(p, c, in.In())
return ty, !ty.IsZero()
},
start: canStartPath,
}.appendTo(types)
}
func tryParseOptions(p *parser, c *token.Cursor, in taxa.Noun) ast.CompactOptions {
if !canStartOptions(c.Peek()) {
return ast.CompactOptions{}
}
return parseOptions(p, c.Next(), in)
}
// parseOptions parses a ([]-delimited) compact options list.
func parseOptions(p *parser, brackets token.Token, _ taxa.Noun) ast.CompactOptions {
options := p.NewCompactOptions(brackets)
delimited[ast.Option]{
p: p,
c: brackets.Children(),
what: taxa.Option,
in: taxa.CompactOptions,
required: true,
exhaust: true,
parse: func(c *token.Cursor) (ast.Option, bool) {
path := parsePath(p, c)
if path.IsZero() {
return ast.Option{}, false
}
eq := c.Peek()
switch eq.Text() {
case ":": // Allow colons, which is usually a mistake.
p.Errorf("unexpected `:` in compact option").Apply(
report.Snippet(eq),
just.Justify(p.File().Stream(), eq.Span(), "replace this with an `=`", just.Edit{
Edit: report.Edit{Start: 0, End: 1, Replace: "="},
Kind: just.Between,
}),
report.Notef("top-level `option` assignment uses `=`, not `:`"),
)
fallthrough
case "=":
c.Next()
default:
p.Error(errtoken.Unexpected{
What: eq,
Want: taxa.Noun(keyword.Assign).AsSet(),
Where: taxa.CompactOptions.In(),
})
eq = token.Zero
}
option := ast.Option{
Path: path,
Equals: eq,
Value: parseExpr(p, c, taxa.CompactOptions.In()),
}
return option, !option.Value.IsZero()
},
start: canStartPath,
}.appendTo(options.Entries())
return options
}
// 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/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
type defParser struct {
*parser
c *token.Cursor
kw token.Token
in taxa.Noun
args ast.DeclDefArgs
inputs, outputs token.Token
braces token.Token
outputTy ast.TypeAny // Used only for diagnostics.
}
type defFollower interface {
// what returns the noun for this follower.
what(*defParser) taxa.Noun
// canStart returns whether this follower can be parsed next.
canStart(*defParser) bool
// parse parses this follower and returns its span; returns nil on failure.
parse(*defParser) source.Span
// prev returns the span of the first value parsed for this follower, or nil
// if it has not been parsed yet.
prev(*defParser) source.Span
}
var defFollowers = []defFollower{
defInputs{}, defOutputs{},
defValue{}, defOptions{},
defBody{},
}
// parse parses a generic definition.
func (p *defParser) parse() ast.DeclDef {
// Try to parse the various "followers". We try to parse as many as
// possible: if we have `foo = 5 = 6`, we want to parse the second = 6,
// diagnose it, and throw it away.
var mark token.CursorMark
var skipSemi bool
lastFollower := -1
for !p.c.Done() {
ensureProgress(p.c, &mark)
idx := -1
for i := range defFollowers {
if defFollowers[i].canStart(p) {
idx = i
break
}
}
if idx < 0 {
break
}
next := defFollowers[idx].parse(p)
if next.IsZero() {
continue
}
switch {
case idx < lastFollower:
// TODO: if we have already seen a follower at idx, we should
// suggest removing this follower. Otherwise, we should suggest
// moving this follower before the previous one.
f := defFollowers[lastFollower]
p.Error(errtoken.Unexpected{
What: next,
Where: f.what(p).After(),
Prev: f.prev(p),
Got: defFollowers[idx].what(p),
})
case idx == lastFollower:
f := defFollowers[lastFollower]
p.Error(errMoreThanOne{
first: f.prev(p),
second: next,
what: f.what(p),
})
default:
lastFollower = idx
}
if lastFollower == len(defFollowers)-1 {
// Once we parse a body, we're done.
skipSemi = true
break
}
}
// If we didn't see any braces, this def needs to be ended by a semicolon.
if !skipSemi {
semi, err := parseSemi(p.parser, p.c, taxa.Def)
p.args.Semicolon = semi
if err != nil {
p.Error(err)
}
}
if p.in == taxa.Enum {
// Convert something that looks like an enum value into one.
if tyPath := p.args.Type.AsPath(); !tyPath.IsZero() && p.args.Name.IsZero() &&
// The reason for this is because if the user writes `message {}`, we want
// to *not* turn it into an enum value with a body.
//
// TODO: Add a case for making sure that `rpc(foo)` is properly
// diagnosed as an anonymous method.
p.braces.IsZero() {
p.args.Name = tyPath.Path
p.args.Type = ast.TypeAny{}
}
}
def := p.NewDeclDef(p.args)
if !p.inputs.IsZero() {
parseTypeList(p.parser, p.inputs, def.WithSignature().Inputs(), taxa.MethodIns)
}
if !p.outputs.IsZero() {
parseTypeList(p.parser, p.outputs, def.WithSignature().Outputs(), taxa.MethodOuts)
} else if !p.outputTy.IsZero() {
span := p.outputTy.Span()
p.Errorf("missing `(...)` around method return type").Apply(
report.Snippet(span),
report.SuggestEdits(
span,
"insert (...) around the return type",
report.Edit{Start: 0, End: 0, Replace: "("},
report.Edit{Start: span.Len(), End: span.Len(), Replace: ")"},
),
)
seq.Append(def.WithSignature().Outputs(), p.outputTy)
}
if !p.braces.IsZero() {
var in taxa.Noun
switch p.kw.Text() {
case "message":
in = taxa.Message
case "enum":
in = taxa.Enum
case "service":
in = taxa.Service
case "extend":
in = taxa.Extend
case "group":
in = taxa.Field
case "oneof":
in = taxa.Oneof
case "rpc":
in = taxa.Method
default:
in = taxa.Def
}
def.SetBody(parseBody(p.parser, p.braces, in))
}
return def
}
type defInputs struct{}
func (defInputs) what(*defParser) taxa.Noun { return taxa.MethodIns }
func (defInputs) canStart(p *defParser) bool { return p.c.Peek().Keyword() == keyword.Parens }
func (defInputs) parse(p *defParser) source.Span {
next := p.c.Next()
if next.IsLeaf() {
return source.Span{} // Diagnosed by the lexer.
}
if p.inputs.IsZero() {
p.inputs = next
}
return next.Span()
}
func (defInputs) prev(p *defParser) source.Span { return p.inputs.Span() }
type defOutputs struct{}
func (defOutputs) what(*defParser) taxa.Noun { return taxa.MethodOuts }
func (defOutputs) canStart(p *defParser) bool { return p.c.Peek().Keyword() == keyword.Returns }
func (defOutputs) parse(p *defParser) source.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 := punctParser{
parser: p.parser, c: p.c,
want: keyword.Parens,
where: taxa.Noun(keyword.Returns).After(),
}.parse()
if list.IsZero() && canStartPath(p.c.Peek()) {
// Suppose the user writes `returns my.Response`. This is
// invalid but reasonable so we want to diagnose it. To do this,
// we parse a single type w/o parens and diagnose it later.
ty = parseType(p.parser, p.c, taxa.Noun(keyword.Returns).After())
} else if err != nil {
p.Error(err)
return source.Span{}
}
if p.outputs.IsZero() && p.outputTy.IsZero() {
if !list.IsZero() {
p.outputs = list
} else {
p.outputTy = ty
}
}
if !list.IsZero() {
return source.Join(returns, list)
}
return source.Join(returns, ty)
}
func (defOutputs) prev(p *defParser) source.Span {
if !p.outputTy.IsZero() {
return source.Join(p.args.Returns, p.outputTy)
}
return source.Join(p.args.Returns, p.outputs)
}
type defValue struct{}
func (defValue) what(p *defParser) taxa.Noun {
switch {
case p.kw.Text() == "option":
return taxa.OptionValue
case p.args.Type.IsZero():
return taxa.EnumValue
default:
return taxa.FieldTag
}
}
func (defValue) canStart(p *defParser) bool {
next := p.c.Peek()
// This will slurp up a value *not* prefixed with an =, too, but
// that case needs to be diagnosed. This allows us to diagnose
// e.g.
//
// optional int32 x 5; // Missing =.
//
// However, if we've already seen {}, [], or another value, we break
// instead, since this suggests we're peeking the next def.
switch {
case next.Keyword() == keyword.Assign:
return true
case canStartPath(next):
// 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):
// Exclude the two followers after this one.
return false
case canStartExpr(next):
// Don't try to parse an expression if we've already parsed
// an expression, options, or another expression.
return p.args.Value.IsZero() && p.args.Options.IsZero() && p.braces.IsZero()
default:
return false
}
}
func (defValue) parse(p *defParser) source.Span {
eq, err := punctParser{
parser: p.parser, c: p.c,
want: keyword.Assign,
where: taxa.Def.In(),
insert: just.Between,
}.parse()
if err != nil {
p.Error(err)
}
expr := parseExpr(p.parser, p.c, taxa.Def.In())
if expr.IsZero() {
return source.Span{} // parseExpr already generated diagnostics.
}
if p.args.Value.IsZero() {
p.args.Equals = eq
p.args.Value = expr
}
return source.Join(eq, expr)
}
func (defValue) prev(p *defParser) source.Span {
if p.args.Value.IsZero() {
return source.Span{}
}
return source.Join(p.args.Equals, p.args.Value)
}
type defOptions struct{}
func (defOptions) what(*defParser) taxa.Noun { return taxa.CompactOptions }
func (defOptions) canStart(p *defParser) bool { return canStartOptions(p.c.Peek()) }
func (defOptions) parse(p *defParser) source.Span {
next := p.c.Next()
if next.IsLeaf() {
return source.Span{} // Diagnosed by the lexer.
}
if p.args.Options.IsZero() {
p.args.Options = parseOptions(p.parser, next, taxa.Def)
}
return next.Span()
}
func (defOptions) prev(p *defParser) source.Span { return p.args.Options.Span() }
type defBody struct{}
func (defBody) what(*defParser) taxa.Noun { return taxa.Body }
func (defBody) canStart(p *defParser) bool { return canStartBody(p.c.Peek()) }
func (defBody) parse(p *defParser) source.Span {
next := p.c.Next()
if next.IsLeaf() {
return source.Span{} // Diagnosed by the lexer.
}
if p.braces.IsZero() {
p.braces = next
}
return next.Span()
}
func (defBody) prev(p *defParser) source.Span { return p.braces.Span() }
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"slices"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// delimited is a mechanism for parsing a punctuation-delimited list.
type delimited[T source.Spanner] struct {
p *parser
c *token.Cursor
// What are we parsing, and within what context? This is used for
// generating diagnostics.
what, in taxa.Noun
// Permitted delimiters. If empty, assumed to be keyword.Comma.
delims []keyword.Keyword
// Whether a delimiter must be present, rather than merely optional.
required bool
// Whether iteration should expect to exhaust c.
exhaust bool
// Whether trailing delimiters are permitted.
trailing bool
// A function for parsing elements as they come.
//
// This function is expected to exhaust
parse func(*token.Cursor) (T, bool)
// Used for skipping tokens until we can begin parsing.
//
// start is called until we see a token that returns true for it. However,
// if stop is not nil and it returns true for that token, parsing stops.
start, stop func(token.Token) bool
}
func (d delimited[T]) appendTo(commas ast.Commas[T]) {
for v, d := range d.iter {
commas.AppendComma(v, d)
}
}
func (d delimited[T]) iter(yield func(value T, delim token.Token) bool) {
// NOTE: We do not use errUnexpected here, because we want to insert the
// terms "leading", "extra", and "trailing" where appropriate, and because
// we don't want to have to deal with asking the caller to provide Nouns
// for each delimiter.
if len(d.delims) == 0 {
d.delims = []keyword.Keyword{keyword.Comma}
}
var delim token.Token
var latest int // The index of the most recently seen delimiter.
next := d.c.Peek()
if idx := slices.Index(d.delims, next.Keyword()); idx >= 0 {
_ = d.c.Next()
latest = idx
d.p.Error(errtoken.Unexpected{
What: next,
Where: d.in.In(),
Want: d.what.AsSet(),
Got: fmt.Sprintf("leading `%s`", next.Text()),
}).Apply(report.SuggestEdits(
next.Span(),
fmt.Sprintf("delete this `%s`", next.Text()),
report.Edit{Start: 0, End: len(next.Text())},
))
}
var needDelim bool
var mark token.CursorMark
for !d.c.Done() {
ensureProgress(d.c, &mark)
// Set if we should not diagnose a missing comma, because there was
// garbage in front of the call to parse().
var badPrefix bool
if !d.start(d.c.Peek()) {
if d.stop != nil && d.stop(d.c.Peek()) {
break
}
first := d.c.Next()
var last token.Token
for !d.c.Done() && !d.start(d.c.Peek()) {
if d.stop != nil && d.stop(d.c.Peek()) {
break
}
last = d.c.Next()
}
want := d.what.AsSet()
if needDelim && delim.IsZero() {
want = d.delimNouns()
}
what := source.Spanner(first)
if !last.IsZero() {
what = source.Join(first, last)
}
badPrefix = true
d.p.Error(errtoken.Unexpected{
What: what,
Where: d.in.In(),
Want: want,
})
}
v, ok := d.parse(d.c)
if !ok {
break
}
if !badPrefix && needDelim && delim.IsZero() {
d.p.Error(errtoken.Unexpected{
What: v,
Where: d.in.In(),
Want: d.delimNouns(),
}).Apply(
report.Snippetf(v.Span().Rune(0), "note: assuming a missing `%s` here", d.delims[latest]),
just.Justify(
d.p.File().Stream(),
v.Span(),
fmt.Sprintf("add a `%s` here", d.delims[latest]),
just.Edit{
Edit: report.Edit{Replace: d.delims[latest].String()},
Kind: just.Left,
},
),
)
}
needDelim = d.required
// Pop as many delimiters as we can.
delim = token.Zero
for {
which := slices.Index(d.delims, d.c.Peek().Keyword())
if which < 0 {
break
}
latest = which
next := d.c.Next()
if delim.IsZero() {
delim = next
continue
}
// Diagnose all extra delimiters after the first.
d.p.Error(errtoken.Unexpected{
What: next,
Where: d.in.In(),
Want: d.what.AsSet(),
Got: fmt.Sprintf("extra `%s`", next.Text()),
}).Apply(
report.Snippetf(delim, "first delimiter is here"),
report.SuggestEdits(
next.Span(),
fmt.Sprintf("delete this `%s`", next.Text()),
report.Edit{Start: 0, End: len(next.Text())},
),
)
}
if !yield(v, delim) {
break
}
// In non-exhaust mode, if we miss a required comma, bail if we have
// reached a stop token, or if we don't have a stop predicate.
// Otherwise, go again to parse another thing.
if delim.IsZero() && d.required && !d.exhaust {
if d.stop == nil || d.stop(d.c.Peek()) {
break
}
}
}
switch {
case d.exhaust && !d.c.Done():
d.p.Error(errtoken.Unexpected{
What: source.JoinSeq(d.c.Rest()),
Where: d.in.In(),
Want: d.what.AsSet(),
Got: "tokens",
})
case !d.trailing && !delim.IsZero():
d.p.Error(errtoken.Unexpected{
What: delim,
Where: d.in.In(),
Got: fmt.Sprintf("trailing `%s`", delim.Text()),
}).Apply(report.SuggestEdits(
delim.Span(),
fmt.Sprintf("delete this `%s`", delim.Text()),
report.Edit{Start: 0, End: len(delim.Text())},
))
}
}
func (d delimited[T]) delimNouns() taxa.Set {
var set taxa.Set
for _, delim := range d.delims {
set = set.With(taxa.Noun(delim))
}
return set
}
// 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/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"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/slicesx"
)
// parseExpr attempts to parse a full expression.
//
// May return nil if parsing completely fails.
// TODO: return something like ast.ExprError instead.
func parseExpr(p *parser, c *token.Cursor, where taxa.Place) ast.ExprAny {
return parseExprInfix(p, c, where, ast.ExprAny{}, 0)
}
// parseExprInfix parses an infix expression.
//
// prec is the precedence; higher values mean tighter binding. This function calls itself
// with higher (or equal) precedence values.
func parseExprInfix(p *parser, c *token.Cursor, where taxa.Place, lhs ast.ExprAny, prec int) ast.ExprAny {
if lhs.IsZero() {
lhs = parseExprPrefix(p, c, where)
if lhs.IsZero() || c.Done() {
return lhs
}
}
next := peekTokenExpr(p, c)
switch prec {
case 0:
if where.Subject() == taxa.Array || where.Subject() == taxa.Dict {
switch next.Keyword() {
case keyword.Assign: // Allow equals signs, which are usually a mistake.
p.Errorf("unexpected `=` in expression").Apply(
report.Snippet(next),
just.Justify(p.File().Stream(), next.Span(), "replace this with an `:`", just.Edit{
Edit: report.Edit{Start: 0, End: 1, Replace: ":"},
Kind: just.Left,
}),
report.Notef("a %s use `=`, not `:`, for setting fields", taxa.Dict),
)
fallthrough
case keyword.Colon:
return p.NewExprField(ast.ExprFieldArgs{
Key: lhs,
Colon: c.Next(),
Value: parseExprInfix(p, c, where, ast.ExprAny{}, prec+1),
}).AsAny()
case keyword.Braces, keyword.Lt, keyword.Brackets:
// This is for colon-less, array or dict-valued fields.
if next.IsLeaf() {
break
}
// The previous expression cannot also be a key-value pair, since
// this messes with parsing of dicts, which are not comma-separated.
//
// In other words, consider the following, inside of an expression
// context:
//
// foo: bar { ... }
//
// We want to diagnose the { as unexpected here, and it is better
// for that to be done by whatever is calling parseExpr since it
// will have more context.
//
// We also do not allow this inside of arrays, because we want
// [a {}] to parse as [a, {}] not [a: {}].
if lhs.Kind() == ast.ExprKindField || where.Subject() == taxa.Array {
break
}
return p.NewExprField(ast.ExprFieldArgs{
Key: lhs,
// Why not call parseExprSolo? Suppose the following
// (invalid) production:
//
// foo { ... } to { ... }
//
// Calling parseExprInfix will cause this to be parsed
// as a range expression, which will be diagnosed when
// we legalize.
Value: parseExprInfix(p, c, where, ast.ExprAny{}, prec+1),
}).AsAny()
}
}
return parseExprInfix(p, c, where, lhs, prec+1)
case 1:
//nolint:gocritic // This is a switch for consistency with the rest of the file.
switch next.Keyword() {
case keyword.To:
return p.NewExprRange(ast.ExprRangeArgs{
Start: lhs,
To: c.Next(),
End: parseExprInfix(p, c, taxa.Noun(keyword.To).After(), ast.ExprAny{}, prec),
}).AsAny()
}
return parseExprInfix(p, c, where, lhs, prec+1)
default:
return lhs
}
}
// parseExprPrefix parses a prefix expression.
//
// This is separate from "solo" expressions because if we every gain suffix-type
// expressions, such as f(), we need to parse -f() as -(f()), not (-f)().
func parseExprPrefix(p *parser, c *token.Cursor, where taxa.Place) ast.ExprAny {
next := peekTokenExpr(p, c)
switch {
case next.IsZero():
return ast.ExprAny{}
case next.Keyword() == keyword.Sub:
c.Next()
inner := parseExprPrefix(p, c, taxa.Noun(keyword.Sub).After())
return p.NewExprPrefixed(ast.ExprPrefixedArgs{
Prefix: next,
Expr: inner,
}).AsAny()
default:
return parseExprSolo(p, c, where)
}
}
// parseExprSolo attempts to parse a "solo" expression, which is an expression that
// does not contain any operators.
//
// May return nil if parsing completely fails.
func parseExprSolo(p *parser, c *token.Cursor, where taxa.Place) ast.ExprAny {
next := peekTokenExpr(p, c)
switch {
case next.IsZero():
return ast.ExprAny{}
case next.Kind() == token.String, next.Kind() == token.Number:
return ast.ExprLiteral{File: p.File(), Token: c.Next()}.AsAny()
case canStartPath(next):
return ast.ExprPath{Path: parsePath(p, c)}.AsAny()
case slicesx.Among(next.Keyword(), keyword.Braces, keyword.Lt, keyword.Brackets):
body := c.Next()
in := taxa.Dict
if body.Keyword() == keyword.Brackets {
in = taxa.Array
}
// Due to wanting to not have <...> be a token tree by default in the
// lexer, we need to perform rather complicated parsing here to handle
// <a: b> syntax messages. (ugh)
angles := body.Keyword() == keyword.Lt
children := c
if !angles {
children = body.Children()
}
elems := delimited[ast.ExprAny]{
p: p,
c: children,
what: taxa.DictField,
in: in,
delims: []keyword.Keyword{keyword.Comma, keyword.Semi},
required: false,
exhaust: !angles,
trailing: true,
parse: func(c *token.Cursor) (ast.ExprAny, bool) {
expr := parseExpr(p, c, in.In())
return expr, !expr.IsZero()
},
start: canStartExpr,
stop: func(t token.Token) bool {
return angles && t.Keyword() == keyword.Gt
},
}
if in == taxa.Array {
elems.what = taxa.Expr
elems.delims = []keyword.Keyword{keyword.Comma}
elems.required = true
elems.trailing = false
array := p.NewExprArray(body)
elems.appendTo(array.Elements())
return array.AsAny()
}
dict := p.NewExprDict(body)
for expr, comma := range elems.iter {
field := expr.AsField()
if field.IsZero() {
p.Error(errtoken.Unexpected{
What: expr,
Where: in.In(),
Want: taxa.DictField.AsSet(),
})
field = p.NewExprField(ast.ExprFieldArgs{Value: expr})
}
dict.Elements().AppendComma(field, comma)
}
// If this is a pair of angle brackets, we need to fuse them.
if angles {
if c.Peek().Keyword() == keyword.Gt {
token.Fuse(body, c.Next())
}
}
return dict.AsAny()
default:
p.Error(errtoken.Unexpected{
What: next,
Where: where,
Want: taxa.Expr.AsSet(),
})
return ast.ExprAny{}
}
}
// peekTokenExpr peeks a token and generates an expression-specific diagnostic
// if the cursor is exhausted.
func peekTokenExpr(p *parser, c *token.Cursor) token.Token {
next := c.Peek()
if next.IsZero() {
token, span := c.SeekToEnd()
err := errtoken.Unexpected{
What: span,
Where: taxa.Expr.In(),
Want: taxa.Expr.AsSet(),
Got: taxa.EOF,
}
if !token.IsZero() {
err.Got = taxa.Classify(token)
}
p.Error(err)
}
return next
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/internal/astx"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// parsePath parses the longest path at cursor. Returns a nil path if
// the next token is neither an identifier, a dot, or a ().
//
// If an invalid token occurs after a dot, returns the longest path up until that dot.
// The cursor is then placed after the dot.
//
// This function assumes that we have decided to definitely parse a path, and
// will emit diagnostics to that effect. As such, the current token position on cursor
// should not be nil.
func parsePath(p *parser, c *token.Cursor) ast.Path {
start := c.Peek()
if !canStartPath(start) {
p.Error(errtoken.Unexpected{What: start, Want: startsPath})
return ast.Path{}
}
var prevSeparator token.Token
if slicesx.Among(start.Keyword(), keyword.Dot, keyword.Div) {
prevSeparator = c.Next()
}
var done bool
end := start
for !done && !c.Done() {
next := c.Peek()
first := start == next
switch {
case slicesx.Among(next.Keyword(), keyword.Dot, keyword.Div):
if !prevSeparator.IsZero() {
// This is a double dot, so something like foo..bar, ..foo, or
// foo.. We diagnose it and move on -- Path.Components is robust
// against double dots.
// We consume additional separators here so that we can diagnose
// them all in one shot.
for {
prevSeparator = c.Next()
next := c.Peek()
if !slicesx.Among(next.Text(), ".", "/") {
break
}
}
tokens := source.Join(next, prevSeparator)
p.Error(errtoken.Unexpected{
What: tokens,
Where: taxa.Classify(next).After(),
Want: taxa.NewSet(taxa.Ident, taxa.Noun(keyword.Parens)),
Got: "tokens",
})
} else {
prevSeparator = c.Next()
}
case next.Kind() == token.Ident:
if !first && prevSeparator.IsZero() {
// This means we found something like `foo bar`, which means we
// should stop consuming components.
done = true
continue
}
end = next
prevSeparator = token.Zero
c.Next()
case next.Keyword() == keyword.Parens:
if !first && prevSeparator.IsZero() {
// This means we found something like `foo(bar)`, which means we
// should stop consuming components.
done = true
continue
}
// Recurse into this token and check it, too, contains a path. We throw
// the result away once we're done, because we don't need to store it;
// a Path simply stores its start and end tokens and knows how to
// recurse into extensions. We also need to check there are no
// extraneous tokens.
contents := next.Children()
parsePath(p, contents)
if tok := contents.Peek(); !tok.IsZero() {
p.Error(errtoken.Unexpected{
What: start,
Where: taxa.ExtensionName.After(),
})
}
end = next
prevSeparator = token.Zero
c.Next()
default:
if prevSeparator.IsZero() {
// This means we found something like `foo =`, which means we
// should stop consuming components.
done = true
continue
}
// This means we found something like foo.1 or bar."xyz" or bar.[...].
// TODO: Do smarter recovery here. Generally speaking it's likely we should *not*
// consume this token.
p.Error(errtoken.Unexpected{
What: next,
Where: taxa.QualifiedName.After(),
Want: taxa.NewSet(taxa.Ident, taxa.Noun(keyword.Parens)),
}).Apply(report.SuggestEdits(
prevSeparator,
fmt.Sprintf("delete the extra `%s`", prevSeparator.Text()),
report.Edit{Start: 0, End: 1},
))
end = prevSeparator // Include the trailing separator.
done = true
}
}
// NOTE: We do not need to legalize against a single-dot path; that
// is already done for us by the if nextDot checks.
return astx.NewPath(p.File(), start, end)
}
// 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/internal/taxa"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
var (
startsPath = taxa.NewSet(taxa.Ident, taxa.Noun(keyword.Parens), taxa.Noun(keyword.Dot))
startsDecl = startsPath.With(taxa.Noun(keyword.Braces), taxa.Noun(keyword.Semi))
)
func canStartDecl(tok token.Token) bool {
return canStartPath(tok) ||
slicesx.Among(tok.Keyword(), keyword.Semi, keyword.Braces)
}
// canStartPath returns whether or not tok can start a path.
func canStartPath(tok token.Token) bool {
return tok.Kind() == token.Ident ||
slicesx.Among(tok.Keyword(), keyword.Dot, keyword.Div, keyword.Parens)
}
// canStartExpr returns whether or not tok can start an expression.
func canStartExpr(tok token.Token) bool {
return canStartPath(tok) ||
tok.Kind() == token.Number || tok.Kind() == token.String ||
slicesx.Among(tok.Keyword(), keyword.Sub, keyword.Braces, keyword.Brackets, keyword.Lt)
}
func canStartOptions(tok token.Token) bool {
return tok.Keyword() == keyword.Brackets
}
func canStartBody(tok token.Token) bool {
return tok.Keyword() == keyword.Braces
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/syntax"
"github.com/bufbuild/protocompile/experimental/internal/errtoken"
"github.com/bufbuild/protocompile/experimental/internal/just"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// lexer is a Protobuf parser.
type parser struct {
*ast.Nodes
*report.Report
syntaxNode ast.DeclSyntax
importOptionNode ast.DeclImport
syntax syntax.Syntax
parseComplete bool
}
// classified is a spanner that has been classified by taxa.
type classified struct {
source.Spanner
what taxa.Noun
}
type punctParser struct {
*parser
c *token.Cursor
want keyword.Keyword
where taxa.Place
insert just.Kind
}
// parse attempts to unconditionally parse some punctuation.
//
// If the wrong token is encountered, it DOES NOT consume the token, returning a nil
// token instead. Returns a diagnostic on failure.
func (p punctParser) parse() (token.Token, report.Diagnose) {
start := p.c.PeekSkippable().Span()
start = start.File.Span(start.Start, start.Start)
next := p.c.Peek()
if next.Keyword() == p.want {
return p.c.Next(), nil
}
wanted := taxa.Noun(p.want).AsSet()
err := errtoken.Unexpected{
What: next,
Where: p.where,
Want: wanted,
}
if next.IsZero() {
end, span := p.c.SeekToEnd()
err.What = span
err.Got = taxa.EOF
if _, c, _ := end.Keyword().Brackets(); c != keyword.Unknown {
// Special case for closing braces.
err.Got = "`" + c.String() + "`"
} else if !end.IsZero() {
err.Got = taxa.Classify(end)
}
}
if p.insert != 0 {
err.Stream = p.File().Stream()
err.Insert = p.want.String()
err.InsertAt = err.What.Span().Start
err.InsertJustify = p.insert
}
return token.Zero, err
}
// parseEquals parses an equals sign.
//
// This is a shorthand for a very common version of punctParser.
func parseEquals(p *parser, c *token.Cursor, in taxa.Noun) (token.Token, report.Diagnose) {
return punctParser{
parser: p, c: c,
want: keyword.Assign,
where: in.In(),
insert: just.Between,
}.parse()
}
// parseSemi parses a semicolon.
//
// This is a shorthand for a very common version of punctParser.
func parseSemi(p *parser, c *token.Cursor, after taxa.Noun) (token.Token, report.Diagnose) {
return punctParser{
parser: p, c: c,
want: keyword.Semi,
where: after.After(),
insert: just.Left,
}.parse()
}
// 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/astx"
"github.com/bufbuild/protocompile/experimental/internal/taxa"
"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"
)
// parseType attempts to parse a type, optionally followed by a non-absolute
// path (depending on what pathAfter says).
//
// May return nil if parsing completely fails.
func parseType(p *parser, c *token.Cursor, where taxa.Place) ast.TypeAny {
ty, _ := parseTypeImpl(p, c, where, false)
return ty
}
// parseType attempts to parse a type, optionally followed by a non-absolute
// path (depending on what pathAfter says).
//
// This function is called in many situations that seem a bit weird to be
// parsing a type in, such as at the top level. This is because of an essential
// ambiguity in Protobuf's grammar (or rather, the version of it that we parse):
// message Foo can start either a field (message Foo;) or a message (message Foo
// {}). Thus, in such contexts we always parse a type-and-path, and based on
// what comes next, reinterpret the type as potentially being a keyword.
//
// This function assumes that we have decided to definitely parse a type, and
// will emit diagnostics to that effect. As such, the current token position on
// c should not be nil.
//
// May return nil if parsing completely fails.
// TODO: return something like ast.TypeError instead.
func parseTypeAndPath(p *parser, c *token.Cursor, where taxa.Place) (ast.TypeAny, ast.Path) {
return parseTypeImpl(p, c, where, true)
}
func parseTypeImpl(p *parser, c *token.Cursor, where taxa.Place, pathAfter bool) (ast.TypeAny, ast.Path) {
var isList, isInMethod bool
switch where.Subject() {
case taxa.MethodIns, taxa.MethodOuts,
taxa.Noun(keyword.Returns): // Used when parsing the invalid `returns foo.Bar` production.
isInMethod = true
fallthrough
case taxa.TypeParams:
isList = true
}
// First, parse a path, possibly preceded by a sequence of modifiers.
//
// To do this, we repeatedly parse paths, and each time we get a path that
// starts with an identifier, we interpret it as a modifier. For example,
// repeated.foo needs to be interpreted as repeated .foo.
var (
mods []token.Token
tyPath ast.Path
)
for !c.Done() && tyPath.IsZero() {
next := c.Peek()
if !canStartPath(next) {
break
}
tyPath = parsePath(p, c)
if tyPath.Absolute() {
break // Absolute paths cannot start with a modifier, so we are done.
}
first, _ := iterx.First(tyPath.Components)
ident := first.AsIdent()
if ident.IsZero() {
break // If this starts with an extension, we're done.
}
// Here is a nasty case. Suppose the user has written within message
// scope something like
//
// package .foo.bar = 5;
//
// This is a syntax error but only because the name of the field is a
// non-trivial path. However, we would like for this to be diagnosed as
// a package declaration. Thus, if this looks like a bad package, we
// break it up so that the type is "package" and the path is ".foo.bar",
// so that the DeclPackage code path can diagnose it.
//
// We require that this have no modifiers and that it not be followed by
// a path, so that the following productions are *not* treated as weird
// packages:
//
// optional package.foo.bar;
// package.foo.bar baz;
//
// Note that this does not apply inside of type lists. This is because
// type lists *only* contain types, and not productions started by
// keywords.
//
// This case applies to the keywords:
// - package
// - extend
// - option
if !isList && len(mods) == 0 &&
slicesx.Among(ident.Keyword(), keyword.Package, keyword.Extend, keyword.Option) &&
!canStartPath(c.Peek()) {
kw, path := tyPath.Split(1)
if !path.IsZero() {
return ast.TypePath{Path: kw}.AsAny(), path
}
}
// Check if ident is a modifier, and if so, peel it off.
//
// We need to be careful to only peel off `stream` inside of a method
// type. If the entire path is a single identifier, we always peel it
// off, since code that follows handles turning it back into a path
// based on what comes after it.
var isMod bool
_, rest := tyPath.Split(1)
switch k := ident.Keyword(); {
case k.IsFieldTypeModifier():
// NOTE: We do not need to look at isInMethod here, because it
// implies isList (sort of: in the case of writing something
// like `returns optional.Foo`, this will be parsed as
// `returns (optional .Foo)`. However, this production is already
// invalid, because of the missing parentheses, so we don't need to
// legalize it.
isMod = !isList || rest.IsZero()
case k.IsTypeModifier(), k.IsImportModifier():
// Do not pick these up if rest is non-zero, because that means
// we're in a case like export.Foo x = 1;. The only case where
// export/local should be picked up is if the next token is not
// .Foo or similar.
isMod = rest.IsZero()
case k.IsMethodTypeModifier():
isMod = isInMethod || rest.IsZero()
}
if isMod {
mods = append(mods, ident)
tyPath = rest
}
}
if tyPath.IsZero() {
if len(mods) == 0 {
return ast.TypeAny{}, ast.Path{}
}
// Pop the last mod and make that into the type path. This makes
// `optional optional` work as a type.
last := mods[len(mods)-1]
tyPath = astx.NewPath(p.File(), last, last)
mods = mods[:len(mods)-1]
}
ty := ast.TypePath{Path: tyPath}.AsAny()
// Next, look for some angle brackets. We need to do this before draining
// mods, because angle brackets bind more tightly than modifiers.
if angles := c.Peek(); angles.Keyword() == keyword.Lt {
c.Next() // Consume the angle brackets.
generic := p.NewTypeGeneric(ast.TypeGenericArgs{
Path: tyPath,
AngleBrackets: angles,
})
delimited[ast.TypeAny]{
p: p,
c: c,
what: taxa.Type,
in: taxa.TypeParams,
required: true,
exhaust: false,
parse: func(c *token.Cursor) (ast.TypeAny, bool) {
ty := parseType(p, c, taxa.TypeParams.In())
return ty, !ty.IsZero()
},
start: canStartPath,
stop: func(t token.Token) bool {
kw := t.Keyword()
return kw == keyword.Gt ||
kw == keyword.Assign // Heuristic for stopping reasonably early in the case of map<K, V m = 1;
},
}.appendTo(generic.Args())
// Need to fuse the angle brackets, because the lexer won't do it.
if c.Peek().Keyword() == keyword.Gt {
token.Fuse(angles, c.Next())
}
ty = generic.AsAny()
}
// Now, check for a path that follows all this. If there isn't a path, and
// ty is (still) a TypePath, and there is still at least one modifier, we
// interpret the last modifier as the type and the current path type as the
// path after the type.
var path ast.Path
if pathAfter {
next := c.Peek()
if canStartPath(next) {
path = parsePath(p, c)
} else if !isList && ty.Kind() == ast.TypeKindPath && len(mods) > 0 {
path = tyPath
// Pop the last mod and make that into the type. This makes
// `optional optional = 1` work as a proto3 field.
last := mods[len(mods)-1]
tyPath = astx.NewPath(p.File(), last, last)
mods = mods[:len(mods)-1]
ty = ast.TypePath{Path: tyPath}.AsAny()
}
}
// Finally, apply any remaining modifiers (in reverse order) to ty.
for i := len(mods) - 1; i >= 0; i-- {
ty = p.NewTypePrefixed(ast.TypePrefixedArgs{
Prefix: mods[i],
Type: ty,
}).AsAny()
}
return ty, path
}
// 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 report
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// Level represents the severity of a diagnostic message.
type Level int8
const (
// Internal compiler error. Indicates a panic within the compiler.
ICE Level = 1 + iota
// Red. Indicates a semantic constraint violation.
Error
// Yellow. Indicates something that probably should not be ignored.
Warning
// Cyan. This is the diagnostics version of "info".
Remark
noteLevel // Used internally within the diagnostic renderer.
)
// Diagnostic is a type of error that can be rendered as a rich diagnostic.
//
// Not all Diagnostics are "errors", even though Diagnostic does embed error;
// some represent warnings, or perhaps debugging remarks.
//
// To construct a diagnostic, create one using a function like [Report.Error].
// Then, call [Diagnostic.Apply] to apply options to it. You should at minimum
// apply [Message] and either [InFile] or at least one [Snippetf].
type Diagnostic struct {
tag, message string
level Level
// sortOrder is used to force diagnostics to sort before or after each other
// in groups. See [Report.Sort].
sortOrder int
// The file this diagnostic occurs in, if it has no associated Annotations. This
// is used for errors like "file too big" that cannot be given a snippet.
inFile string
// A list of annotated source code spans in the diagnostic.
snippets []snippet
notes, help, debug []string
}
// Edit is an edit to suggest on a snippet.
//
// See [SuggestEdits].
type Edit struct {
// The start and end offsets of the edit, relative the span of the snippet
// this edit is applied to (so, Start == 0 means the edit starts at the
// start of the span).
//
// An insertion without deletion is modeled by Start == End.
Start, End int
// Text to replace the content between Start and End with.
//
// A pure deletion is modeled by Replace == "".
Replace string
}
// IsDeletion returns whether this edit involves deleting part of the source
// text.
func (e Edit) IsDeletion() bool {
return e.Start < e.End
}
// IsInsertion returns whether this edit involves inserting new text.
func (e Edit) IsInsertion() bool {
return e.Replace != ""
}
// DiagnosticOption is an option that can be applied to a [Diagnostic].
//
// IsZero values passed to [Diagnostic.Apply] are ignored.
type DiagnosticOption interface {
apply(*Diagnostic)
}
// Primary returns this diagnostic's primary span, if it has one.
//
// If it doesn't have one, it returns the zero span.
func (d *Diagnostic) Primary() source.Span {
for _, annotation := range d.snippets {
if annotation.primary {
return annotation.Span
}
}
return source.Span{}
}
// Level returns this diagnostic's level.
func (d *Diagnostic) Level() Level {
return d.level
}
// Message returns this diagnostic's message, set using [Message].
func (d *Diagnostic) Message() string {
return d.message
}
// Tag returns this diagnostic's tag, set using [Tag].
func (d *Diagnostic) Tag() string {
return d.tag
}
// File returns the path of the file this diagnostic is associated with.
//
// It returns the value set by [InFile] if present, otherwise it returns
// the path from the primary span. Returns empty string if neither is available.
func (d *Diagnostic) File() string {
if d.inFile != "" {
return d.inFile
}
span := d.Primary()
if span.File != nil {
return span.File.Path()
}
return ""
}
// Notes returns this diagnostic's notes, set using [Notef].
func (d *Diagnostic) Notes() []string {
return d.notes
}
// Help returns this diagnostic's suggestions, set using [Helpf].
func (d *Diagnostic) Help() []string {
return d.help
}
// Debug returns this diagnostic's debugging information, set using [Debugf].
func (d *Diagnostic) Debug() []string {
return d.debug
}
// Is checks whether this diagnostic has a particular tag.
func (d *Diagnostic) Is(tag string) bool {
return d.tag == tag
}
// Apply applies the given options to this diagnostic.
//
// Nil values are ignored; does nothing if d is nil.
func (d *Diagnostic) Apply(options ...DiagnosticOption) *Diagnostic {
if d != nil {
for _, option := range options {
if option != nil {
option.apply(d)
}
}
}
return d
}
// Tag returns a DiagnosticOption that sets a diagnostic's tag.
//
// Tags are machine-readable identifiers for diagnostics. Tags should be
// lowercase identifiers separated by dashes, e.g. my-error-tag. If a package
// generates diagnostics with tags, it should expose those tags as constants.
func Tag(t string) DiagnosticOption {
return tag(t)
}
// Message returns a DiagnosticOption that sets the main diagnostic message.
func Message(format string, args ...any) DiagnosticOption {
return message{format, args}
}
// InFile returns a DiagnosticOption that causes a diagnostic without a primary
// span to mention the given file.
func InFile(path string) DiagnosticOption {
return inFile(path)
}
// Snippet is like [Snippetf], but it attaches no message to the snippet.
//
// The first annotation added is the "primary" annotation, and will be rendered
// differently from the others.
//
// If at is nil or returns the zero span, the returned DiagnosticOption is a no-op.
func Snippet(at source.Spanner) DiagnosticOption {
return Snippetf(at, "")
}
// Snippetf returns a DiagnosticOption that adds a new snippet to a diagnostic.
//
// Any additional arguments to this function are passed to [fmt.Sprintf] to
// produce a message to go with the span.
//
// The first annotation added is the "primary" annotation, and will be rendered
// differently from the others.
//
// If at is nil or returns the zero span, the returned DiagnosticOption is a no-op.
func Snippetf(at source.Spanner, format string, args ...any) DiagnosticOption {
return snippet{
Span: source.GetSpan(at),
message: fmt.Sprintf(format, args...),
}
}
// SuggestEdits is like [Snippet], but generates a snippet that contains
// machine-applicable suggestions.
//
// A snippet with suggestions will be displayed separately from other snippets.
// The message associated with the snippet will be prefixed with "help:" when
// rendered.
func SuggestEdits(at source.Spanner, message string, edits ...Edit) DiagnosticOption {
span := source.GetSpan(at)
text := span.Text()
for _, edit := range edits {
// Force a bounds check here to make it easier to debug, instead of
// panicking in the renderer (or emitting an invalid report proto).
_ = text[edit.Start:edit.End]
}
return snippet{
Span: span,
message: message,
edits: edits,
}
}
// SuggestEditsWithWidening is like [SuggestEdits], but it allows edits' starts and
// ends to not conform to the given span exactly (e.g., the end points are
// negative or greater than the length of the span).
//
// This will widen the span for the suggestion to fit the edits.
func SuggestEditsWithWidening(at source.Spanner, message string, edits ...Edit) DiagnosticOption {
span := source.GetSpan(at)
start := span.Start
span = source.JoinSeq(slicesx.Map(edits, func(e Edit) source.Span {
return span.File.Span(e.Start+start, e.End+start)
}))
delta := start - span.Start
for i := range edits {
edits[i].Start += delta
edits[i].End += delta
}
return SuggestEdits(span, message, edits...)
}
// Notef returns a DiagnosticOption that provides the user with context about the
// diagnostic, after the annotations.
func Notef(format string, args ...any) DiagnosticOption {
return note{format, args}
}
// Helpf returns a DiagnosticOption that provides the user with a helpful prose
// suggestion for resolving the diagnostic.
func Helpf(format string, args ...any) DiagnosticOption {
return help{format, args}
}
// Debugf returns a DiagnosticOption appends debugging information to a diagnostic that
// is not intended to be shown to normal users.
func Debugf(format string, args ...any) DiagnosticOption {
return debug{format, args}
}
// PageBreak is a DiagnosticOption that inserts a "page break", separating
// diagnostic snippets before and after it into separate windows.
var PageBreak pageBreak
// snippet is an annotated source code snippet within a [Diagnostic].
//
// Snippets will render as annotated source code spans that show the context
// around the annotated region. More literally, this is e.g. a red squiggly
// line under some code.
type snippet struct {
// The span for this annotation.
source.Span
// A message to show under this snippet.
//
// May be empty, in which case it will simply render as the red/yellow/etc
// squiggly line with no note attached to it. This is useful for cases where
// the overall error message already explains what the problem is and there
// is no additional context that would be useful to add to the error.
message string
// Whether this is a "primary" snippet, which is used for deciding whether or not
// to mark the snippet with the same color as the overall diagnostic.
primary bool
// Whether this snippet ends in a page break, i.e., it should not be
// rendered together with following snippets, even if they're in the same
// file.
pageBreak bool
// Edits to include in this snippet. This causes this snippet to be rendered
// in its own window when it is non-empty, and no underline will appear for
// the overall span of the snippet. The message will still be used, as the
// title of the window.
edits []Edit
}
func (a snippet) apply(d *Diagnostic) {
if a.Span.IsZero() {
return
}
a.primary = len(d.snippets) == 0
d.snippets = append(d.snippets, a)
}
type lazySprintf struct {
format string
args []any
}
func (ls lazySprintf) String() string {
return fmt.Sprintf(ls.format, ls.args...)
}
type tag string
type inFile string
type message lazySprintf
type note lazySprintf
type help lazySprintf
type debug lazySprintf
type pageBreak struct{}
func (t tag) apply(d *Diagnostic) {
if d.tag != "" {
panic("protocompile/report: set diagnostic tag more than once")
}
d.tag = string(t)
}
func (f inFile) apply(d *Diagnostic) {
if d.inFile != "" {
panic("protocompile/report: set diagnostic path more than once")
}
d.inFile = string(f)
}
func (m message) apply(d *Diagnostic) {
if d.message != "" {
panic("protocompile/report: set diagnostic message more than once")
}
d.message = lazySprintf(m).String()
}
func (n note) apply(d *Diagnostic) { d.notes = append(d.notes, lazySprintf(n).String()) }
func (n help) apply(d *Diagnostic) { d.help = append(d.help, lazySprintf(n).String()) }
func (n debug) apply(d *Diagnostic) { d.debug = append(d.debug, lazySprintf(n).String()) }
func (pageBreak) apply(d *Diagnostic) {
if len(d.snippets) == 0 {
return
}
d.snippets[len(d.snippets)-1].pageBreak = true
}
// 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 report
import (
"slices"
"sort"
"strings"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
const (
hunkUnchanged = ' '
hunkAdd = '+'
hunkDelete = '-'
)
// hunk is a render-able piece of a diff.
type hunk struct {
kind rune
content string
}
func (h hunk) color(ss *styleSheet) string {
switch h.kind {
case hunkAdd:
return ss.nAdd
case hunkDelete:
return ss.nDelete
default:
return ss.reset
}
}
func (h hunk) bold(ss *styleSheet) string {
switch h.kind {
case hunkAdd:
return ss.bAdd
case hunkDelete:
return ss.bDelete
default:
return ss.reset
}
}
// hunkDiff computes edit hunks for a diff.
func hunkDiff(span source.Span, edits []Edit) (source.Span, []hunk) {
out := make([]hunk, 0, len(edits)*3+1)
var prev int
span, edits = offsetsForDiffing(span, edits)
src := span.Text()
for _, edit := range edits {
out = append(out,
hunk{hunkUnchanged, src[prev:edit.Start]},
hunk{hunkDelete, src[edit.Start:edit.End]},
hunk{hunkAdd, edit.Replace},
)
prev = edit.End
}
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 source.Span, edits []Edit) (source.Span, []hunk) {
// Sort the edits such that they are ordered by starting offset.
span, edits = offsetsForDiffing(span, edits)
src := span.Text()
sort.Slice(edits, func(i, j int) bool {
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.
parts := slicesx.SplitAfterFunc(edits, func(i int, edit Edit) bool {
next, ok := slicesx.Get(edits, i+1)
return ok && edit.End < next.Start && // Go treats str[x:y] for x > y as an error.
strings.Contains(src[edit.End:next.Start], "\n")
})
var out []hunk //nolint:prealloc // False positive.
var prevHunk int
for edits := range parts {
if len(edits) == 0 {
continue
}
// First, figure out the start and end of the modified region.
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.
start, end = adjustLineOffsets(src, start, end)
original := src[start:end]
// Now, apply the edits to original to produce the modified result.
var buf strings.Builder
prev := 0
for _, edit := range edits {
buf.WriteString(original[prev:max(prev, edit.Start-start)])
buf.WriteString(edit.Replace)
prev = edit.End - start
}
buf.WriteString(original[prev:])
unchanged := src[prevHunk:start]
deleted := src[start:end]
added := buf.String()
if stripped, ok := strings.CutPrefix(added, deleted); ok &&
strings.HasPrefix(stripped, "\n") {
// It is possible for deleted to be a line suffix of added; outputting
// a diff like this doesn't look good, so we should fix it up here.
unchanged = src[prevHunk:end]
deleted = ""
added = strings.TrimPrefix(stripped, "\n")
}
trim := func(s string) string {
s = strings.TrimPrefix(s, "\n")
s = strings.TrimSuffix(s, "\n")
return s
}
// Dump the result into the output.
out = append(out,
hunk{hunkUnchanged, trim(unchanged)},
hunk{hunkDelete, deleted},
hunk{hunkAdd, added},
)
prevHunk = end
}
return span, append(out, hunk{hunkUnchanged, src[prevHunk:]})
}
// offsetsForDiffing pre-calculates information needed for diffing:
// the line-snapped span, and edits which are adjusted to conform to that
// span.
func offsetsForDiffing(span source.Span, edits []Edit) (source.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.Span(start, end), edits
}
// 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.
//nolint:errcheck // There is a very noisy, unhelpful lint around WriteString().
package report
import (
"bytes"
"fmt"
"io"
"math"
"math/bits"
"slices"
"strconv"
"strings"
"unicode"
_ "unsafe" // For go:linkname.
"google.golang.org/protobuf/encoding/protojson"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/source/length"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
"github.com/bufbuild/protocompile/internal/interval"
)
// Renderer configures a diagnostic rendering operation.
type Renderer struct {
// If set, uses a compact one-line format for each diagnostic.
Compact bool
// If set, rendering results are enriched with ANSI color escapes.
Colorize bool
// Upgrades all warnings to errors.
WarningsAreErrors bool
// If set, remark diagnostics will be printed.
ShowRemarks bool
// If set, rendering a diagnostic will show the debug footer.
ShowDebug bool
}
// renderer contains shared state for a rendering operation, allowing e.g.
// allocations to be re-used and simplifying function signatures.
type renderer struct {
Renderer
writer
ss styleSheet
// The width, in columns, of the line number margin in the diagnostic
// currently being rendered.
margin int
}
// Render renders a diagnostic report.
//
// In addition to returning the rendering result, returns whether the report
// contains any errors.
//
// On the other hand, the actual error-typed return is an error when writing to
// the writer.
func (r Renderer) Render(report *Report, out io.Writer) (errorCount, warningCount int, err error) {
state := &renderer{
Renderer: r,
writer: writer{out: out},
ss: newStyleSheet(r),
}
return state.render(report)
}
// RenderString is a helper for calling [Renderer.Render] with a [strings.Builder].
func (r Renderer) RenderString(report *Report) (text string, errorCount, warningCount int) {
var buf strings.Builder
e, w, _ := r.Render(report, &buf)
return buf.String(), e, w
}
func (r *renderer) render(report *Report) (errorCount, warningCount int, err error) {
for _, diagnostic := range report.Diagnostics {
if !r.ShowRemarks && diagnostic.level == Remark {
continue
}
r.diagnostic(report, diagnostic)
if err := r.Flush(); err != nil {
return errorCount, warningCount, err
}
switch {
case diagnostic.level <= Error:
errorCount++
case diagnostic.level <= Warning:
if r.WarningsAreErrors {
errorCount++
} else {
warningCount++
}
}
}
if r.Compact {
return errorCount, warningCount, err
}
switch {
case errorCount > 0 && warningCount > 0:
fmt.Fprintf(r, "%sencountered %d error%v and %d warning%v\n%s",
r.ss.bError,
errorCount, plural(errorCount), warningCount, plural(warningCount),
r.ss.reset,
)
case errorCount > 0:
fmt.Fprintf(r, "%sencountered %d error%v\n%s",
r.ss.bError,
errorCount, plural(errorCount), r.ss.reset,
)
case warningCount > 0:
fmt.Fprintf(r, "%sencountered %d warning%v\n%s",
r.ss.bWarning,
warningCount, plural(warningCount), r.ss.reset,
)
}
return errorCount, warningCount, r.Flush()
}
// diagnostic renders a single diagnostic to a string.
func (r *renderer) diagnostic(report *Report, d Diagnostic) {
if report.Tracing > 0 {
// If we're debugging diagnostic traces, and we panic, show where this
// particular diagnostic was generated. This is useful for debugging
// renderer bugs.
defer func() {
if panicked := recover(); panicked != nil {
proto := report.ToProto()
json, _ := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(proto)
stack := strings.Join(d.debug[:min(report.Tracing, len(d.debug))], "\n")
panic(fmt.Sprintf("protocompile/report: panic in renderer: %v\ndiagnosed at:\n%sreport: %s", panicked, stack, json))
}
}()
}
var level string
switch d.level {
case ICE:
level = "internal compiler error"
case Error:
level = "error"
case Warning:
if r.WarningsAreErrors {
level = "error"
} else {
level = "warning"
}
case Remark:
level = "remark"
}
// For the simple style, we imitate the Go compiler.
if r.Compact {
r.WriteString(r.ss.ColorForLevel(d.level))
primary := d.Primary()
switch {
case primary.File != nil:
start := primary.StartLoc()
fmt.Fprintf(r, "%s: %s:%d:%d: %s",
level, primary.Path(),
start.Line, start.Column,
d.message,
)
case d.inFile != "":
fmt.Fprintf(r, "%s: %s: %s",
level, d.inFile, d.message,
)
default:
fmt.Fprintf(r, "%s: %s", level, d.message)
}
r.WriteString(r.ss.reset)
r.WriteString("\n")
return
}
// 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, ": ")
r.WriteWrapped(d.message, unicodex.MaxMessageWidth)
locations := make([][2]source.Location, len(d.snippets))
for i, snip := range d.snippets {
locations[i][0] = fileLocation(snip.File, snip.Start, length.TermWidth, false)
if strings.HasSuffix(snip.Text(), "\n") {
// If the snippet ends in a newline, don't include the newline in the
// printed span.
locations[i][1] = fileLocation(snip.File, snip.End-1, length.TermWidth, false)
locations[i][1].Column++
} else {
locations[i][1] = fileLocation(snip.File, snip.End, length.TermWidth, false)
}
}
// Figure out how wide the line bar needs to be. This is given by
// the width of the largest line value among the snippets.
var greatestLine int
for _, loc := range locations {
greatestLine = max(greatestLine, loc[1].Line)
}
r.margin = max(2, len(strconv.Itoa(greatestLine))) // Easier than messing with math.Log10()
// Render all the diagnostic windows.
parts := slicesx.PartitionFunc(d.snippets, func(a, b snippet) bool {
if len(a.edits) > 0 || len(b.edits) > 0 {
// Suggestions are always rendered in their own windows, so they
// always split.
return true
}
// If a has a page break, we split.
if a.pageBreak {
return true
}
// Otherwise, split if the paths are distinct.
return a.Path() != b.Path()
})
var needsTrailingBreak bool
for i, snippets := range parts {
if needsTrailingBreak {
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
}
if i == 0 || d.snippets[i-1].pageBreak || d.snippets[i-1].Path() != d.snippets[i].Path() {
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
primary := snippets[0]
start := locations[i][0]
sep := ":::"
if i == 0 {
sep = "-->"
}
fmt.Fprintf(r, "%s %s:%d:%d\n", sep, primary.Path(), start.Line, start.Column)
} else if len(snippets[0].edits) > 0 {
r.WriteString("\n")
}
if len(snippets[0].edits) > 0 {
r.suggestion(snippets[0])
continue
}
// Add a blank line after the file. This gives the diagnostic window some
// visual breathing room.
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
window := buildWindow(d.level, locations[i:i+len(snippets)], snippets)
needsTrailingBreak = r.window(window)
}
// Render a remedial file name for spanless errors.
if len(d.snippets) == 0 && d.inFile != "" {
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin - 1)
fmt.Fprintf(r, "--> %s", d.inFile)
}
if needsTrailingBreak && !(d.notes == nil && d.help == nil && (!r.ShowDebug || d.debug == nil)) {
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
}
type footer struct {
color, label, text string
}
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} }),
)
var haveFooter bool
for f := range footers {
haveFooter = true
isDebug := f.label == "debug"
if isDebug && !r.ShowDebug {
continue
}
r.WriteString("\n")
r.WriteSpaces(r.margin)
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, unicodex.MaxMessageWidth)
}
}
if !haveFooter && bytes.Equal(bytes.TrimSpace(r.buf), []byte("|")) {
r.buf = r.buf[:0]
r.WriteString(r.ss.reset)
r.WriteString("\n")
return
}
r.WriteString("\n")
r.WriteString(r.ss.reset)
r.WriteString("\n")
}
const maxMultilinesPerWindow = 8
// window is an intermediate structure for rendering an annotated code snippet
// consisting of multiple spans in the same file.
type window struct {
file *source.File
// The line number at which the text starts in the overall source file.
start int
// The byte offset range this window's text occupies in the containing
// source File.
offsets [2]int
// A list of all underline elements in this window.
underlines []underline
multilines []multiline
}
// buildWindow builds a diagnostic window for the given snippets, which must all have
// the same file.
//
// This is separate from [window.Render] because it performs certain layout
// decisions that cannot happen in the middle of actually rendering the source
// code (well, they could, but the resulting code would be far more complicated).
func buildWindow(level Level, locations [][2]source.Location, snippets []snippet) *window {
w := new(window)
w.file = snippets[0].File
// Calculate the range of the file we will be printing. This is given
// by every line that has a piece of diagnostic in it. To find this, we
// calculate the join of all of the spans in the window, and find the
// nearest \n runes in the text.
w.start = locations[0][0].Line
w.offsets[0] = snippets[0].Start
for i := range snippets {
w.start = min(w.start, locations[i][0].Line)
w.offsets[0] = min(w.offsets[0], locations[i][0].Offset)
w.offsets[1] = max(w.offsets[1], locations[i][1].Offset)
}
w.offsets[0], w.offsets[1] = adjustLineOffsets(w.file.Text(), w.offsets[0], w.offsets[1])
// Now, convert each span into an underline or multiline.
for i, snippet := range snippets {
isMulti := locations[i][0].Line != locations[i][1].Line
if isMulti && len(w.multilines) < maxMultilinesPerWindow {
w.multilines = append(w.multilines, multiline{
start: locations[i][0].Line,
end: locations[i][1].Line,
startWidth: locations[i][0].Column,
endWidth: locations[i][1].Column,
level: noteLevel, message: snippet.message,
})
ml := &w.multilines[len(w.multilines)-1]
if ml.startWidth == ml.endWidth {
ml.endWidth++
}
// Calculate whether this snippet starts on the first non-space rune of
// the line.
if snippet.Start != 0 {
firstLineStart := strings.LastIndexByte(w.file.Text()[:snippet.Start], '\n')
if !strings.ContainsFunc(
w.file.Text()[firstLineStart+1:snippet.Start],
func(r rune) bool { return !unicode.IsSpace(r) },
) {
ml.startWidth = 0
}
}
if snippet.primary {
ml.level = level
}
continue
}
w.underlines = append(w.underlines, underline{
line: locations[i][0].Line,
start: locations[i][0].Column,
end: locations[i][1].Column,
level: noteLevel,
message: snippet.message,
subline: -1,
})
ul := &w.underlines[len(w.underlines)-1]
if snippet.primary {
ul.level = level
}
if ul.start == ul.end {
ul.end++
}
if isMulti {
// This is an "overflow multiline" for diagnostics with too
// many multilines. In this case, we want to end the underline at
// the end of the first line.
lineEnd := strings.Index(w.file.Text()[snippet.Start:], "\n")
if lineEnd == -1 {
lineEnd = len(w.file.Text())
} else {
lineEnd += snippet.Start
}
uw := unicodex.Width{Column: ul.start, EscapeNonPrint: true}
uw.WriteString(w.file.Text()[snippet.Start:lineEnd])
ul.end = uw.Column
}
// Make sure no empty underlines exist.
if ul.Len() == 0 {
ul.start++
}
}
slices.SortFunc(w.underlines, cmpx.Join(
cmpx.Key(func(u underline) int { return u.line }),
cmpx.Key(func(u underline) int { return u.start }),
cmpx.Key(func(u underline) Level { return u.level }),
))
slices.SortFunc(w.multilines, cmpx.Join(
cmpx.Key(func(m multiline) int { return m.start }),
cmpx.Key(func(m multiline) int { return -m.end }), // Descending order.
))
return w
}
func (r *renderer) window(w *window) (needsTrailingBreak bool) {
// lineInfo is layout information for a single line of this window. There
// is one lineInfo for each line of w.file.Text we intend to render, as
// given by w.offsets.
type lineInfo struct {
// This is the multilines whose pipes intersect with this line.
sidebar []*multiline
// This is a set of strings to render verbatim under the actual source
// code line. This makes it possible to lay out all of the complex
// underlines ahead of time instead of interleaved with rendering the
// source code lines.
underlines []string
// This is whether this line should be printed in the window. This is
// used to avoid emitting e.g. lines between the start and end of a
// 100-line multi.
shouldEmit bool
}
lines := strings.Split(w.file.Text()[w.offsets[0]:w.offsets[1]], "\n")
// Populate ancillary info for each line.
info := make([]lineInfo, len(lines))
nesting := new(interval.Nesting[int, *underline])
// First, lay out the multilines, and compute how wide the sidebar is.
for i := range w.multilines {
multi := &w.multilines[i]
// Find the smallest unused index by every line in the range.
//
// We want to assign to each multiline a "sidebar index", which is which
// column its connecting pipes | are placed on. For each multiline, we
// want to allocate the leftmost index such that it does not conflict
// with any previously allocated sidebar pipes that are in the same
// range as this multiline. We cannot simply take the max of their
// indices, because it might happen that this multiline only intersects
// with multis on lines that only use indices 0 and 2. This can happen
// if the multi on index 2 intersects a *different* range that already
// has two other multis in it.
//
// We achieve this by looking at all already-laid-out multis in this
// multi's range and using a bitset to detect the least unused index.
// Note that we artificially limit the number of rendered multis to
// 8 in the code that builds the window itself.
var multilineBitset uint
for i := multi.start; i <= multi.end; i++ {
for col, ml := range info[i-w.start].sidebar {
if ml != nil {
multilineBitset |= 1 << col
}
}
}
idx := bits.TrailingZeros(^multilineBitset)
// Apply the index to every element of sidebar.
for i := multi.start; i <= multi.end; i++ {
line := &info[i-w.start].sidebar
for len(*line) < idx+1 {
*line = append(*line, nil)
}
(*line)[idx] = multi
}
// Mark the start and end as must-emit.
info[multi.start-w.start].shouldEmit = true
info[multi.end-w.start].shouldEmit = true
}
var sidebarLen int
for _, info := range info {
sidebarLen = max(sidebarLen, len(info.sidebar))
}
// Next, we can render the underline parts. This aggregates all underlines
// for the same line into rendered chunks
parts := slicesx.PartitionKey(w.underlines, func(u underline) int { return u.line })
for _, part := range parts {
cur := &info[part[0].line-w.start]
cur.shouldEmit = true
// Arrange for a "sidebar prefix" for this line. This is determined by any sidebars that are
// active on this line, even if they end on it.
sidebar := r.sidebar(sidebarLen, -1, -1, cur.sidebar)
// Lay out the physical underlines. We use a cubic (oops) algorithm
// to pack the underlines in such a way that none of them overlap.
// To do so, we loop over the underlines several times until all of them
// are laid out.
//
// This algorithm is O(n^3) where n is the number of underlines in a
// line, meaning that n can't really get out of hand for normal
// inputs.
slices.SortStableFunc(part, cmpx.Key(func(ul underline) int { return -ul.Len() }))
nesting.Clear()
for _, ul := range slicesx.Pointers(part) {
nesting.Insert(ul.start, ul.end-1, ul)
}
var sublines [][]*underline
for set := range nesting.Sets() {
var subline []*underline
for entry := range set {
subline = append(subline, entry.Value)
}
slices.SortStableFunc(subline, cmpx.Key(func(ul *underline) int { return ul.start }))
sublines = append(sublines, subline)
}
// Convert the underlines into strings. Collect the rightmost underlines
// in a slice.
rightmost := slicesx.Transform(sublines, func(subline []*underline) *underline {
var rightmost *underline
for _, ul := range subline {
if rightmost == nil {
rightmost = ul
continue
}
if ul.end > rightmost.end {
rightmost = ul
}
}
return rightmost
})
sublineLens := slicesx.Transform(rightmost, func(ul *underline) int {
return ul.end
})
// Clear rightmost messages that will result in wrapping.
for i, ul := range rightmost {
if ul == nil {
continue
}
startCol := 4 +
int(math.Log10(float64(w.start+len(lines)))) + // Approximation.
len(sidebar) + ul.end
uw := unicodex.Width{Column: startCol}
uw.WriteString(ul.message)
if uw.Column > unicodex.MaxMessageWidth {
// Move rightmost into the normal underlines, because it causes wrapping.
rightmost[i] = nil
}
}
// *Now*, lay out the underlines, so that we can stick pipes on them.
// We will also need to add color later.
sublineBufs := make([][]byte, len(sublines))
for i, sub := range sublines {
buf := bytes.Repeat([]byte(" "), sublineLens[i])
for _, ul := range sub {
b := byte('^')
if ul.level == noteLevel {
b = '-'
}
slicesx.Fill(buf[ul.start-1:ul.end-1], b)
}
sublineBufs[i] = buf
}
// Reserve space for the above, which still need further processing.
cur.underlines = make([]string, len(sublines))
// Now, do all the other messages. We're going to split them such that
// no two messages overlap, using an interval nesting collection.
//
// For each message, we also need to draw pipes (|) above each one to
// connect it to its underline.
//
// This is slightly complicated, because there are two layers: the
// pipes, and whatever message goes on the pipes.
var rest []*underline
for _, ul := range slicesx.Pointers(part) {
if slices.Contains(rightmost, ul) || ul.message == "" {
continue
}
rest = append(rest, ul)
}
slices.SortStableFunc(rest, cmpx.Key(func(ul *underline) int {
return len(ul.message)
}))
nesting.Clear()
for _, ul := range rest {
start := ul.start - 1
if ul.Len() > 3 {
start++ // Move the pipe a little forward for long underlines.
}
nesting.Insert(start, start+len(ul.message)-1, ul)
}
var messages [][]*underline
for set := range nesting.Sets() {
var s []*underline
for entry := range set {
s = append(s, entry.Value)
}
slices.SortStableFunc(s, cmpx.Key(func(ul *underline) int {
return ul.start
}))
messages = append(messages, s)
}
slices.SortStableFunc(messages, cmpx.Key(func(uls []*underline) int {
return uls[0].start
}))
var buf1, buf2 []byte
for i, line := range messages {
// Clear the temp buffers.
buf1 = buf1[:0]
buf2 = buf2[:0]
// First, lay out the pipes for this and all following lines.
var nonColorLen int
for _, line := range messages[i:] {
for _, ul := range line {
col := ul.start - 1
if ul.Len() > 3 {
col++ // Move the pipe a little forward for long underlines.
}
for nonColorLen < col {
buf1 = append(buf1, ' ')
nonColorLen++
}
if nonColorLen == col {
// Two pipes may appear on the same column!
// This is why this is in a conditional. To record the
// level, rather than actually using a pipe, we use
// the value of level here.
buf1 = append(buf1, ^byte(ul.level))
nonColorLen++
if i == 0 {
// Apply this pipe to all of the sublines.
for _, sub := range slicesx.Pointers(sublineBufs) {
for len(*sub) < col+1 {
*sub = append(*sub, ' ')
}
if (*sub)[col] == ' ' {
(*sub)[col] = ^byte(ul.level)
}
}
}
}
}
}
// Insert the colors for the pipes.
buf2 = append(buf2, buf1...)
for i := 0; i < len(buf1); i++ {
b := buf1[i]
if b&0x80 == 0 {
continue
}
color := r.ss.BoldForLevel(Level(^b))
buf1[i] = '|'
buf1 = slices.Insert(buf1, i, []byte(color)...)
i += len(color)
}
// Splat in the one with all the pipes in it as-is. Having two rows like
// this ensures that each message has one pipe directly above it.
cur.underlines = append(cur.underlines, strings.TrimRight(sidebar+string(buf1), " "))
// Then, splat in the messages for this line.
offset := 0
for _, ul := range line {
start := ul.start - 1 + offset
if ul.Len() > 3 {
start++ // Move the pipe a little forward for long underlines.
}
for len(buf2) < start+len(ul.message)+1 {
buf2 = append(buf2, ' ')
}
color := r.ss.BoldForLevel(ul.level)
copy(buf2[start:], ul.message)
buf2 = slices.Insert(buf2, start, []byte(color)...)
offset += len(color)
}
// Insert the colors for the pipes, again...
for i := 0; i < len(buf2); i++ {
b := buf2[i]
if b&0x80 == 0 {
continue
}
color := r.ss.BoldForLevel(Level(^b))
buf2[i] = '|'
buf2 = slices.Insert(buf2, i, []byte(color)...)
i += len(color)
}
cur.underlines = append(cur.underlines, strings.TrimRight(sidebar+string(buf2), " "))
}
// Finally, finalize the underlines by coloring them!
for i, sub := range sublines {
buf := sublineBufs[i]
ul := rightmost[i]
// Count the number of pipes that come before the end of the
// underlines.
pipes := 0
for i, b := range buf {
if ul != nil && ul.end <= i {
break
}
if b&0x80 != 0 {
pipes++
}
}
offset := 0
// First, just the underlines.
for _, ul := range sub {
color := r.ss.BoldForLevel(ul.level)
buf = slices.Insert(buf, ul.start-1+offset, []byte(color)...)
offset += len(color)
}
// Now, color the pipes.
for i := 0; i < len(buf); i++ {
b := buf[i]
if b&0x80 == 0 {
continue
}
color := r.ss.BoldForLevel(Level(^b))
buf[i] = '|'
buf = slices.Insert(buf, i, []byte(color)...)
i += len(color)
if pipes > 0 {
offset += len(color)
}
pipes--
}
// Append the message for this subline, if any.
if ul != nil && ul.message != "" {
color := r.ss.BoldForLevel(ul.level)
end := ul.end + offset + len(color) + len(ul.message)
for len(buf) < end {
buf = append(buf, ' ')
}
copy(buf[ul.end+offset:], color)
copy(buf[ul.end+offset+len(color):], ul.message)
}
cur.underlines[i] = sidebar + string(buf)
}
}
//nolint:dupword
// Now that we've laid out the underlines, we can add the starts and ends of all
// of the multilines, which go after the underlines.
//
// The result is that a multiline will look like this:
//
// code
// ____^
// | code code code
// \______________^ message
var line strings.Builder
for lineIdx := range info {
cur := &info[lineIdx]
prevStart := -1
for mlIdx, ml := range cur.sidebar {
if ml == nil {
continue
}
line.Reset()
var isStart bool
switch w.start + lineIdx {
case ml.start:
if ml.startWidth == 0 {
continue
}
isStart = true
fallthrough
case ml.end:
// We need to be flush with the sidebar here, so we trim the trailing space.
sidebar := []byte(strings.TrimRight(r.sidebar(0, -1, prevStart, cur.sidebar[:mlIdx+1]), " "))
// We also need to erase the bars of any multis that are before this multi
// and start/end on the same line.
if !isStart {
for mlIdx, otherML := range cur.sidebar[:mlIdx+1] {
if otherML != nil && otherML.end == ml.end {
// All the color escapes have the same byte length, so we can use the length of
// any of them to measure how far we need to adjust the offset to get to the
// pipe. We need to account for one escape per multiline, and also need to skip
// past the color escape on the pipe we want to erase.
codeLen := len(r.ss.bAccent)
idx := mlIdx*(2+codeLen) + codeLen
if idx < len(sidebar) {
sidebar[idx] = ' '
}
}
}
}
// Delete the last pipe and replace it with a slash or space, depending.
// on orientation.
line.Write(sidebar[:len(sidebar)-1])
if isStart {
line.WriteByte(' ')
} else {
line.WriteByte('\\')
}
// Pad out to the gutter of the code block.
remaining := sidebarLen - (mlIdx + 1)
padByRune(&line, remaining*2, '_')
// Pad to right before we need to insert a ^ or -
if isStart {
padByRune(&line, ml.startWidth-1, '_')
} else {
padByRune(&line, ml.endWidth-1, '_')
}
if ml.level == noteLevel {
line.WriteByte('-')
} else {
line.WriteByte('^')
}
// TODO: If the source code has extremely long lines, this will cause
// the message to wind up wrapped crazy far. It may be worth doing
// wrapping ourselves in some cases (beyond a threshold of, say, 120
// columns). It is unlikely users will hit this problem with "realistic"
// inputs, though, and e.g. rustc and clang do not bother to handle this
// case nicely.
if !isStart && ml.message != "" {
line.WriteByte(' ')
line.WriteString(ml.message)
}
cur.underlines = append(cur.underlines, line.String())
}
if isStart {
prevStart = mlIdx
} else {
prevStart = -1
}
}
}
// Make sure to emit any lines adjacent to another line we want to emit, so long as that
// line contains printable characters.
//
// We copy a set of all the lines we plan to emit before this transformation;
// otherwise, doing it in-place will cause every nonempty line after a must-emit line
// to be shown, which we don't want.
mustEmit := make(map[int]bool)
for i := range info {
if info[i].shouldEmit {
mustEmit[i] = true
}
}
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], printable) != -1 {
score++
}
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] && 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 {
cur := &info[i]
lineno := i + w.start
if !cur.shouldEmit {
continue
}
// If the last multi of the previous line starts on that line, make its
// pipe here a slash so that it connects properly.
slashAt := -1
if i > 0 {
prevSidebar := info[i-1].sidebar
if len(prevSidebar) > 0 &&
prevSidebar[len(prevSidebar)-1].start == lineno-1 &&
prevSidebar[len(prevSidebar)-1].startWidth > 0 {
slashAt = len(prevSidebar) - 1
}
}
sidebar := r.sidebar(sidebarLen, lineno, slashAt, cur.sidebar)
if i > 0 && !info[i-1].shouldEmit {
// Generate a visual break if this is right after a real line.
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin - 2)
r.WriteString("... ")
// Generate a sidebar as before but this time we want to look at the
// last line that was actually emitted.
slashAt := -1
prevSidebar := info[lastEmit-w.start].sidebar
if len(prevSidebar) > 0 &&
prevSidebar[len(prevSidebar)-1].start == lastEmit &&
prevSidebar[len(prevSidebar)-1].startWidth > 0 {
slashAt = len(prevSidebar) - 1
}
r.WriteString(r.sidebar(sidebarLen, lastEmit+1, slashAt, info[lastEmit-w.start].sidebar))
}
// Ok, we are definitely printing this line out.
//
// Note that sidebar already includes a trailing ss.reset for us.
fmt.Fprintf(r, "\n%s%*d | %s%s", r.ss.nAccent, r.margin, lineno, sidebar, r.ss.reset)
lastEmit = lineno
// Re-use the logic from width calculation to correctly format a line for
// showing in a terminal.
uw := &unicodex.Width{EscapeNonPrint: true, Out: &r.writer}
uw.WriteString(line)
needsTrailingBreak = true
// If this happens to be an annotated line, this is when it gets annotated.
for _, line := range cur.underlines {
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
r.WriteString(line)
// Gross hack to pick up whether a trailing break is necessary; we
// only add one if the underline contains text.
needsTrailingBreak = strings.ContainsFunc(line, unicode.IsLetter)
}
}
return needsTrailingBreak
}
type underline struct {
line int
start, end int
level Level
message string
subline int
}
func (u underline) Len() int {
return u.end - u.start
}
type multiline struct {
start, end int
startWidth, endWidth int
level Level
message string
}
func (r *renderer) sidebar(bars, lineno, slashAt int, multis []*multiline) string {
var sidebar strings.Builder
for i, ml := range multis {
if ml == nil {
sidebar.WriteString(" ")
continue
}
sidebar.WriteString(r.ss.BoldForLevel(ml.level))
switch {
case slashAt == i:
sidebar.WriteByte('/')
case lineno != ml.start:
sidebar.WriteByte('|')
case ml.startWidth == 0:
sidebar.WriteByte('/')
default:
sidebar.WriteByte(' ')
}
sidebar.WriteByte(' ')
}
for sidebar.Len() < bars*2 {
sidebar.WriteByte(' ')
}
return sidebar.String()
}
// suggestion renders a single suggestion window.
func (r *renderer) suggestion(snip snippet) {
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString("help: ")
r.WriteWrapped(snip.message, unicodex.MaxMessageWidth)
// Add a blank line after the file. This gives the diagnostic window some
// visual breathing room.
r.WriteString("\n")
r.WriteSpaces(r.margin)
r.WriteString(r.ss.nAccent)
r.WriteString(" | ")
// When the suggestion spans multiple lines, we don't bother doing a by-the-rune
// diff, because the result can be hard for users to understand how to apply
// to their code. Also, if the suggestion contains deletions, use
multiline := slices.ContainsFunc(snip.edits, func(e Edit) bool {
// Prefer multiline suggestions in the case of deletions.
return e.IsDeletion() || strings.Contains(e.Replace, "\n")
}) ||
strings.Contains(snip.Span.Text(), "\n")
if multiline {
span, hunks := unifiedDiff(snip.Span, snip.edits)
startLine := span.StartLoc().Line
aLine := startLine
bLine := startLine
for i, hunk := range hunks {
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 == '+' {
lineno = bLine
}
// Draw the line as we would for an ordinary window, but prefix
// each line with a the hunk's kind and color.
fmt.Fprintf(r, "\n%s%*d | %s%c%s ",
r.ss.nAccent, r.margin, lineno,
hunk.bold(&r.ss), hunk.kind, hunk.color(&r.ss),
)
uw := &unicodex.Width{EscapeNonPrint: true, Out: &r.writer}
uw.WriteString(line)
switch hunk.kind {
case hunkUnchanged:
aLine++
bLine++
case hunkDelete:
aLine++
case hunkAdd:
bLine++
}
}
}
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
return
}
span, hunks := hunkDiff(snip.Span, snip.edits)
fmt.Fprintf(r, "\n%s%*d | ", r.ss.nAccent, r.margin, span.StartLoc().Line)
uw := &unicodex.Width{EscapeNonPrint: true, Out: &r.writer}
for _, hunk := range hunks {
if hunk.content == "" {
continue
}
r.WriteString(hunk.color(&r.ss))
// Re-use the logic from width calculation to correctly format a line for
// showing in a terminal.
uw.WriteString(hunk.content)
}
// Draw underlines for each modified segment, using + and - as the
// underline characters.
r.WriteString("\n")
r.WriteString(r.ss.nAccent)
r.WriteSpaces(r.margin)
r.WriteString(" | ")
uw.Column = 0
uw.Out = nil
for _, hunk := range hunks {
if hunk.content == "" {
continue
}
prev := uw.Column
uw.WriteString(hunk.content)
r.WriteString(hunk.bold(&r.ss))
for range uw.Column - prev {
r.WriteString(string(hunk.kind))
}
}
}
func adjustLineOffsets(text string, start, end int) (int, int) {
// Find the newlines before and after the given ranges, respectively.
// This snaps the range to start immediately after a newline (or SOF) and
// end immediately before a newline (or EOF).
start = strings.LastIndexByte(text[:start], '\n') + 1 // +1 gives the byte *after* the newline.
if offset := strings.IndexByte(text[end:], '\n'); offset != -1 {
end += offset
} else {
end = len(text)
}
return start, end
}
func padByRune(out *strings.Builder, spaces int, r rune) {
for range spaces {
out.WriteRune(r)
}
}
// This is an unexported method of source.File which we need in this file. It's
// simpler to just linkname it than to expose allowNonPrint, which should really
// not be a public API.
//
// The linkname-ing actually happens in source.File.
//
//go:linkname fileLocation
func fileLocation(f *source.File, offset int, units length.Unit, allowNonPrint bool) source.Location
// 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 report
import (
"errors"
"fmt"
"runtime"
runtimedebug "runtime/debug"
"slices"
"strings"
"google.golang.org/protobuf/proto"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
compilerpb "github.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1"
)
// Report is a collection of diagnostics.
//
// Report is not thread-safe (in the sense that distinct goroutines should not
// all write to Report at the same time). Instead, the recommendation is to create
// multiple reports and then merge them, using [Report.Sort] to canonicalize the result.
type Report struct {
Options
// The actual diagnostics on this report. Generally, you'll want to use one of
// the helpers like [Report.Error] instead of appending directly.
Diagnostics []Diagnostic
}
// Options for how a report should be constructed.
type Options struct {
// The stage to apply to any new diagnostics created with this report.
//
// Diagnostics with the same stage will sort together. See [Report.Sort].
Stage int
// When greater than zero, this will capture debugging information at the
// site of each call to Error() etc. This will make diagnostic construction
// orders of magnitude slower; it is intended to help tool writers to debug
// their diagnostics.
//
// Higher values mean more debugging information. What debugging information
// is actually provided is subject to change.
Tracing int
// If set, [Report.Sort] will not discard duplicate diagnostics, as defined
// in that function's contract.
KeepDuplicates bool
// If set, all diagnostics of severity at most Warning (i.e., >= Warning
// as integers) are suppressed.
SuppressWarnings bool
}
// Diagnose is a type that can be rendered as a diagnostic.
type Diagnose interface {
Diagnose(*Diagnostic)
}
// Error pushes an error diagnostic onto this report.
func (r *Report) Error(err Diagnose) *Diagnostic {
d := r.push(1, Error)
err.Diagnose(d)
return d
}
// SoftError pushes a diagnostic with the onto this report, making it a warning
// if hard is false.
func (r *Report) SoftError(hard bool, err Diagnose) *Diagnostic {
level := Warning
if hard {
level = Error
}
d := r.push(1, level)
err.Diagnose(d)
return d
}
// Warn pushes a warning diagnostic onto this report.
func (r *Report) Warn(err Diagnose) *Diagnostic {
d := r.push(1, Warning)
err.Diagnose(d)
return d
}
// Remark pushes a remark diagnostic onto this report.
func (r *Report) Remark(err Diagnose) *Diagnostic {
d := r.push(1, Remark)
err.Diagnose(d)
return d
}
// Level pushes a diagnostic with the given level onto this report.
func (r *Report) Level(level Level, err Diagnose) *Diagnostic {
d := r.push(1, level)
err.Diagnose(d)
return d
}
// Fatalf creates an ad-hoc [ICE] diagnostic with the given message; analogous to
// [fmt.Errorf].
func (r *Report) Fatalf(format string, args ...any) *Diagnostic {
return r.push(1, ICE).Apply(Message(format, args...))
}
// Errorf creates an ad-hoc error diagnostic with the given message; analogous to
// [fmt.Errorf].
func (r *Report) Errorf(format string, args ...any) *Diagnostic {
return r.push(1, Error).Apply(Message(format, args...))
}
// SoftError pushes an ad-hoc soft error diagnostic with the given message; analogous to
// [fmt.Errorf].
func (r *Report) SoftErrorf(hard bool, format string, args ...any) *Diagnostic {
level := Warning
if hard {
level = Error
}
return r.push(1, level).Apply(Message(format, args...))
}
// Warnf creates an ad-hoc warning diagnostic with the given message; analogous to
// [fmt.Errorf].
func (r *Report) Warnf(format string, args ...any) *Diagnostic {
return r.push(1, Warning).Apply(Message(format, args...))
}
// Remarkf creates an ad-hoc remark diagnostic with the given message; analogous to
// [fmt.Errorf].
func (r *Report) Remarkf(format string, args ...any) *Diagnostic {
return r.push(1, Remark).Apply(Message(format, args...))
}
// Levelf creates an ad-hoc diagnostic with the given level and message; analogous to
// [fmt.Errorf].
func (r *Report) Levelf(level Level, format string, args ...any) *Diagnostic {
return r.push(1, level).Apply(Message(format, args...))
}
// SaveOptions calls the given function and, upon its completion, restores
// r.Options to the value it had before it was called.
func (r *Report) SaveOptions(body func()) {
prev := r.Options
body()
r.Options = prev
}
// CatchICE will recover a panic (an internal compiler error, or ICE) and log it
// as an error diagnostic. This function should be called in a defer statement.
//
// When constructing the diagnostic, diagnose is called, to provide an
// opportunity to annotate further.
//
// If resume is true, resumes the recovered panic.
func (r *Report) CatchICE(resume bool, diagnose func(*Diagnostic)) {
panicked := recover()
if panicked == nil {
return
}
// Instead of using the built-in tracing function, which causes the stack
// trace to be hidden by default, use debug.Stack and convert it into notes
// so that it is always visible.
tracing := r.Tracing
r.Tracing = 0 // Temporarily disable built-in tracing.
diagnostic := r.push(1, ICE).Apply(
Message("unexpected panic; this is a bug"),
Notef("%v", panicked),
)
r.Tracing = tracing
if diagnose != nil {
diagnose(diagnostic)
}
var ice *icePanic
if err, _ := panicked.(error); errors.As(err, &ice) {
diagnostic.Apply(ice.options...)
}
// Append a stack trace but only after any user-provided diagnostic
// information.
stack := strings.Split(strings.TrimSpace(string(runtimedebug.Stack())), "\n")
// Remove the goroutine number and the first two frames (debug.Stack and
// Report.CatchICE).
stack = stack[5:]
diagnostic.debug = append(diagnostic.debug, "", "stack trace:")
diagnostic.debug = append(diagnostic.debug, stack...)
if resume {
panic(panicked)
}
}
type icePanic struct {
error
options []DiagnosticOption
}
func (e *icePanic) Unwrap() error { return e.error }
// AnnotatePanic will recover a panic and annotate it such that when [CatchICE]
// recovers it, it can extract this information and display it in the
// diagnostic.
func (r *Report) AnnotateICE(options ...DiagnosticOption) {
panicked := recover()
if panicked == nil {
return
}
err, _ := panicked.(error)
if err == nil {
err = fmt.Errorf("%v", err)
}
var ice *icePanic
if errors.As(err, &ice) {
ice.options = append(ice.options, options...)
} else {
ice = &icePanic{err, options}
}
panic(ice)
}
// Canonicalize sorts this report's diagnostics according to an specific
// ordering criteria. Diagnostics are sorted by, in order:
//
// 1. File name of primary span.
// 2. SortOrder value.
// 3. Start offset of primary snippet.
// 4. End offset of primary snippet.
// 5. Diagnostic tag.
// 6. Textual content of error message.
//
// Where diagnostics have no primary span, the file is treated as empty and the
// offsets are treated as zero.
//
// These criteria ensure that diagnostics for the same file go together,
// diagnostics for the same sort order (lex, parse, etc) go together, and they
// are otherwise ordered by where they occur in the file.
//
// Canonicalize will deduplicate diagnostics whose primary span and (nonempty)
// diagnostic tags are equal, selecting the diagnostic that sorts as greatest
// as the canonical value. This allows later diagnostics to replace earlier
// diagnostics, so long as they cooperate by using the same tag. Deduplication
// can be suppressed using [Options].KeepDuplicates.
func (r *Report) Canonicalize() {
slices.SortFunc(r.Diagnostics, cmpx.Join(
cmpx.Key(func(d Diagnostic) string { return d.Primary().Path() }),
cmpx.Key(func(d Diagnostic) int { return d.sortOrder }),
cmpx.Key(func(d Diagnostic) int { return d.Primary().Start }),
cmpx.Key(func(d Diagnostic) int { return d.Primary().End }),
cmpx.Key(func(d Diagnostic) string { return d.tag }),
cmpx.Key(func(d Diagnostic) string { return d.message }),
))
if r.KeepDuplicates {
return
}
type key struct {
span source.Span
tag string
}
var cur key
slices.Backward(r.Diagnostics)(func(i int, d Diagnostic) bool {
if d.tag == "" {
return true
}
key := key{d.Primary().Span(), d.tag}
if cur.tag != "" && cur == key {
r.Diagnostics[i].level = -1 // Use this to mark which diagnostics to delete.
} else {
cur = key
}
return true
})
r.Diagnostics = slices.DeleteFunc(r.Diagnostics, func(d Diagnostic) bool { return d.level == -1 })
}
// ToProto converts this report into a Protobuf message for serialization.
//
// This operation is lossy: only the Diagnostics slice is serialized. It also discards
// concrete types of Diagnostic.Err, replacing them with opaque [errors.New] values
// on deserialization.
//
// It will also deduplicate [File2] values based on their paths, paying no attention to
// their contents.
func (r *Report) ToProto() proto.Message {
proto := new(compilerpb.Report)
fileToIndex := map[string]uint32{}
for _, d := range r.Diagnostics {
dProto := &compilerpb.Diagnostic{
Message: d.message,
Tag: d.tag,
Level: compilerpb.Diagnostic_Level(d.level),
InFile: d.inFile,
Notes: d.notes,
Help: d.help,
Debug: d.debug,
}
for _, snip := range d.snippets {
file, ok := fileToIndex[snip.Path()]
if !ok {
file = uint32(len(proto.Files))
fileToIndex[snip.Path()] = file
proto.Files = append(proto.Files, &compilerpb.Report_File{
Path: snip.Path(),
Text: []byte(snip.Text()),
})
}
snippet := &compilerpb.Diagnostic_Annotation{
File: file,
Start: uint32(snip.Start),
End: uint32(snip.End),
Message: snip.message,
Primary: snip.primary,
PageBreak: snip.pageBreak,
}
for _, edit := range snip.edits {
snippet.Edits = append(snippet.Edits, &compilerpb.Diagnostic_Edit{
Start: uint32(edit.Start),
End: uint32(edit.End),
Replace: edit.Replace,
})
}
dProto.Annotations = append(dProto.Annotations, snippet)
}
proto.Diagnostics = append(proto.Diagnostics, dProto)
}
return proto
}
// AppendFromProto appends diagnostics from a Protobuf message to this report.
//
// deserialize will be called with an empty message that should be deserialized
// onto, which this function will then convert into [Diagnostic]s to populate the
// report with.
func (r *Report) AppendFromProto(deserialize func(proto.Message) error) error {
proto := new(compilerpb.Report)
if err := deserialize(proto); err != nil {
return err
}
files := make([]*source.File, len(proto.Files))
for i, fProto := range proto.Files {
files[i] = source.NewFile(fProto.Path, string(fProto.Text))
}
for i, dProto := range proto.Diagnostics {
if dProto.Message == "" {
return fmt.Errorf("protocompile/report: missing message for diagnostic[%d]", i)
}
level := Level(dProto.Level)
switch level {
case Error, Warning, Remark:
default:
return fmt.Errorf("protocompile/report: invalid value for Diagnostic.level: %d", int(level))
}
d := Diagnostic{
tag: dProto.Tag,
message: dProto.Message,
level: level,
inFile: dProto.InFile,
notes: dProto.Notes,
help: dProto.Help,
debug: dProto.Debug,
}
var havePrimary bool
for j, snip := range dProto.Annotations {
if int(snip.File) >= len(proto.Files) {
return fmt.Errorf(
"protocompile/report: invalid file index for diagnostic[%d].annotation[%d]: %d",
i, j, snip.File,
)
}
file := files[snip.File]
if int(snip.Start) >= len(file.Text()) ||
int(snip.End) > len(file.Text()) ||
snip.Start > snip.End {
return fmt.Errorf(
"protocompile/report: out-of-bounds span for diagnostic[%d].annotation[%d]: [%d:%d]",
i, j, snip.Start, snip.End,
)
}
snippet := snippet{
Span: source.Span{
File: file,
Start: int(snip.Start),
End: int(snip.End),
},
message: snip.Message,
primary: snip.Primary,
pageBreak: snip.PageBreak,
}
for _, edit := range snip.Edits {
snippet.edits = append(snippet.edits, Edit{
Start: int(edit.Start),
End: int(edit.End),
Replace: edit.Replace,
})
}
d.snippets = append(d.snippets, snippet)
havePrimary = havePrimary || snip.Primary
}
if !havePrimary && len(d.snippets) > 0 {
d.snippets[0].primary = true
}
r.Diagnostics = append(r.Diagnostics, d)
}
return nil
}
// push is the core "make me a diagnostic" function.
//
//nolint:unparam // For skip, see the comment below.
func (r *Report) push(skip int, level Level) *Diagnostic {
// The linter does not like that skip is statically a constant.
// We provide it as an argument for documentation purposes, so
// that callers of this function within this package can specify
// can specify how deeply-nested they are, even if they all have
// the same level of nesting right now.
if level >= Warning && r.SuppressWarnings {
return &Diagnostic{}
}
r.Diagnostics = append(r.Diagnostics, Diagnostic{
level: level,
sortOrder: r.Stage,
})
d := &(r.Diagnostics)[len(r.Diagnostics)-1]
// If debugging is on, capture a stack trace.
if r.Tracing > 0 {
// Unwind the stack to find program counter information.
pc := make([]uintptr, 64)
pc = pc[:runtime.Callers(skip+2, pc)]
// Fill trace with the result.
var (
zero runtime.Frame
buf strings.Builder
)
frames := runtime.CallersFrames(pc)
for range r.Tracing {
frame, more := frames.Next()
if frame == zero || !more {
break
}
fmt.Fprintf(&buf, "at %s\n %s:%d\n", frame.Function, frame.File, frame.Line)
}
d.Apply(Debugf("%s", buf.String()))
}
return d
}
// 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 report
// styleSheet is the colors used for pretty-rendering diagnostics.
type styleSheet struct {
r Renderer
reset string
// Normal colors.
nError, nWarning, nRemark, nAccent, nAdd, nDelete string
// Bold colors.
bError, bWarning, bRemark, bAccent, bAdd, bDelete string
}
func newStyleSheet(r Renderer) styleSheet {
if !r.Colorize {
return styleSheet{r: r}
}
return styleSheet{
r: r,
reset: "\033[0m",
// Red.
nError: "\033[0;31m",
bError: "\033[1;31m",
// Yellow.
nWarning: "\033[0;33m",
bWarning: "\033[1;33m",
// Cyan.
nRemark: "\033[0;36m",
bRemark: "\033[1;36m",
// Blue. Used for "accents" such as non-primary span underlines, line
// numbers, and other rendering details to clearly separate them from
// the source code (which appears in white).
nAccent: "\033[0;34m",
bAccent: "\033[1;34m",
// Green.
nAdd: "\033[0;32m",
bAdd: "\033[1;32m",
// Red.
nDelete: "\033[0;31m",
bDelete: "\033[1;31m",
}
}
// ColorForLevel returns the escape sequence for the non-bold color to use for
// the given level.
func (c styleSheet) ColorForLevel(l Level) string {
switch l {
case Error, ICE:
return c.nError
case Warning:
if c.r.WarningsAreErrors {
return c.nError
}
return c.nWarning
case Remark:
return c.nRemark
case noteLevel:
return c.nAccent
default:
return ""
}
}
// BoldForLevel returns the escape sequence for the bold color to use for
// the given level.
func (c styleSheet) BoldForLevel(l Level) string {
switch l {
case Error, ICE:
return c.bError
case Warning:
if c.r.WarningsAreErrors {
return c.nError
}
return c.bWarning
case Remark:
return c.bRemark
case noteLevel:
return c.bAccent
default:
return ""
}
}
// 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 report
import (
"bytes"
"io"
"regexp"
"slices"
"unicode"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// writer implements low-level writing helpers, including a custom buffering
// routine to avoid printing trailing whitespace to the output.
type writer struct {
out io.Writer
buf []byte // Never contains a '\n' byte.
err error
}
// Write implements [io.Writer].
func (w *writer) Write(data []byte) (int, error) {
_, _ = w.WriteString(unsafex.StringAlias(data))
return len(data), nil
}
func (w *writer) WriteSpaces(n int) {
w.buf = slices.Grow(w.buf, n)
const spaces = " "
for n > len(spaces) {
w.buf = append(w.buf, spaces...)
n -= len(spaces)
}
w.buf = append(w.buf, spaces[:n]...)
}
func (w *writer) WriteString(data string) (int, error) {
// Break the input along newlines; each time we're about to append a
// newline, discard all trailing whitespace that isn't a newline.
for i, line := range iterx.Enumerate(stringsx.Lines(data)) {
if i > 0 {
w.flush(true)
}
w.buf = append(w.buf, line...)
}
return len(data), nil
}
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++
}
uw := &unicodex.Width{EscapeNonPrint: true}
for i, line := range iterx.Enumerate(uw.WordWrap(data, width-margin)) {
if i > 0 {
_, _ = w.WriteString("\n")
w.WriteSpaces(margin)
}
_, _ = w.WriteString(line)
}
}
// Flush flushes the buffer to the writer's output.
func (w *writer) Flush() error {
defer func() { w.err = nil }()
return w.flush(false)
}
// flush is like [writer.Flush], but instead retains the error to be returned
// out of Flush later. This allows e.g. WriteString to call flush() without
// needing to return an error and complicating the rendering code.
//
// If withNewline is set, appends a newline to the data being written.
func (w *writer) flush(withNewline bool) error {
if w.err != nil {
return w.err
}
orig := w.buf
w.buf = bytes.TrimRightFunc(w.buf, unicode.IsSpace)
if withNewline {
w.buf = append(w.buf, '\n')
}
// NOTE: The contract for Write requires that it return len(buf) when
// the error is nil. This means that the length return only matters if
// we hit an error condition, which we treat as fatal anyways.
_, w.err = w.out.Write(w.buf)
if withNewline {
w.buf = w.buf[:0]
return w.err
}
// Delete everything up until the first space; we don't know if the caller
// intends to append more to the current line or not.
//
// Avoid slices.Delete because that includes an unnecessary bounds check and
// a call to clear().
//
// gocritic has a noisy warning about writing a = append(b, ...).
w.buf = append(orig[:0], orig[len(w.buf):]...) //nolint:gocritic
return w.err
}
// plural is a helper for printing out plurals of numbers.
type plural int
// String implements [fmt.Stringer].
func (p plural) String() string {
if p == 1 {
return ""
}
return "s"
}
// 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 seq
// Func implements [Indexer][T] using an access function as underlying storage.
//
// It uses a function that gets the value at the given index. Attempting to
// Set a value will panic.
type Func[T any] struct {
Count int
Get func(int) T
}
// NewFunc constructs a new [Func].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewFunc[T any](count int, get func(int) T) Func[T] {
return Func[T]{count, get}
}
// Len implements [Indexer].
func (s Func[T]) Len() int {
return s.Count
}
// At implements [Indexer].
func (s Func[T]) At(idx int) T {
// Panicking bounds check. This does not allocate.
_ = make([]struct{}, s.Count)[idx]
return s.Get(idx)
}
// SetAt implements [Setter] by panicking.
func (s Func[T]) SetAt(int, T) {
panic("seq: called Func[...].SetAt")
}
// 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 seq provides an interface for sequence-like types that can be indexed
// and inserted into.
//
// Protocompile avoids storing slices of its public types as a means of achieving
// memory efficiency. However, this means that it cannot return those slices,
// and thus must use proxy types that implement the interfaces in this package.
package seq
import (
"iter"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// Indexer is a type that can be indexed like a slice.
type Indexer[T any] interface {
// Len returns the length of this sequence.
Len() int
// At returns the element at the given index.
//
// Should panic if idx < 0 or idx => Len().
At(idx int) T
}
// Setter is an [Indexer] that can be mutated by modifying already present
// values.
type Setter[T any] interface {
Indexer[T]
// SetAt sets the value of the element at the given index.
//
// Should panic if idx < 0 or idx => Len().
SetAt(idx int, value T)
}
// Inserter is an [Indexer] that can be mutated by inserting or deleting values.
type Inserter[T any] interface {
Setter[T]
// Insert inserts an element at the given index.
//
// Should panic if idx < 0 or idx > Len().
Insert(idx int, value T)
// Delete deletes the element at the given index.
//
// Should panic if idx < 0 or idx => Len().
Delete(idx int)
}
// All returns an iterator over the elements in seq, like [slices.All].
func All[T any](seq Indexer[T]) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
n := seq.Len()
for i := range n {
if !yield(i, seq.At(i)) {
return
}
}
}
}
// Backward returns an iterator over the elements in seq in reverse,
// like [slices.Backward].
func Backward[T any](seq Indexer[T]) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i := seq.Len() - 1; i >= 0; i-- {
if !yield(i, seq.At(i)) {
return
}
}
}
}
// Values returns an iterator over the elements in seq, like [slices.Values].
func Values[T any](seq Indexer[T]) iter.Seq[T] {
return func(yield func(T) bool) {
n := seq.Len()
for i := range n {
if !yield(seq.At(i)) {
return
}
}
}
}
// Map is like [slicesx.Map].
func Map[T, U any](seq Indexer[T], f func(T) U) iter.Seq[U] {
return iterx.Map(Values(seq), f)
}
// Append appends values to an Inserter.
func Append[T any](seq Inserter[T], values ...T) {
for _, v := range values {
seq.Insert(seq.Len(), v)
}
}
// ToSlice copies an [Indexer] into a slice.
func ToSlice[T any](seq Indexer[T]) []T {
out := make([]T, seq.Len())
for i := range out {
out[i] = seq.At(i)
}
return out
}
// 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 seq
import (
"slices"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// TODO: Would this optimize better if Wrap/Unwrap was a single type parameter
// constrained by interface { Wrap(E) T; Unwrap(T) E }? Indexer values are
// ephemera, so the size of this struct is not crucial, but it would save on
// having to allocate two [runtime.funcval]s when returning an Indexer.
// Slice implements [Indexer][T] using an ordinary slice as the backing storage,
// and using the given functions to perform the conversion to and from the
// underlying raw values.
//
// The first argument of Wrap/Unwrap given is the index the value has/will have
// in the slice.
type Slice[T, E any] struct {
Slice []E
Wrap func(int, E) T
Unwrap func(int, T) E
}
// NewSlice constructs a new [Slice].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewSlice[T, E any](
slice []E,
wrap func(int, E) T,
unwrap func(int, T) E,
) Slice[T, E] {
return Slice[T, E]{slice, wrap, unwrap}
}
// NewFixedSlice constructs a new [Slice] whose Set method panics. This
// function is intended for cases where the [Slice] will immediately be turned
// into an [Indexer].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewFixedSlice[T, E any](
slice []E,
wrap func(int, E) T,
) Slice[T, E] {
return Slice[T, E]{slice, wrap, nil}
}
// Len implements [Indexer].
func (s Slice[T, _]) Len() int {
return len(s.Slice)
}
// At implements [Indexer].
func (s Slice[T, _]) At(idx int) T {
return s.Wrap(idx, s.Slice[idx])
}
// SetAt implements [Setter].
func (s Slice[T, _]) SetAt(idx int, value T) {
s.Slice[idx] = s.Unwrap(idx, value)
}
// SliceInserter is like [Slice], but also implements [Inserter][T].
type SliceInserter[T, E any] struct {
Slice *[]E
Wrap func(int, E) T
Unwrap func(int, T) E
}
var empty []uint64 // Maximally aligned.
// EmptySliceInserter returns a [SliceInserter] that is always empty and whose
// insertion operations panic.
func EmptySliceInserter[T, E any]() SliceInserter[T, E] {
return NewSliceInserter(
unsafex.Bitcast[*[]E](&empty),
func(_ int, _ E) T {
var z T
return z
},
nil,
)
}
// NewSliceInserter constructs a new [SliceInserter].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewSliceInserter[T, E any](
slice *[]E,
wrap func(int, E) T,
unwrap func(int, T) E,
) SliceInserter[T, E] {
return SliceInserter[T, E]{slice, wrap, unwrap}
}
// Len implements [Indexer].
func (s SliceInserter[T, _]) Len() int {
if s.Slice == nil {
return 0
}
return len(*s.Slice)
}
// At implements [Indexer].
func (s SliceInserter[T, _]) At(idx int) T {
return s.Wrap(idx, (*s.Slice)[idx])
}
// SetAt implements [Setter].
func (s SliceInserter[T, _]) SetAt(idx int, value T) {
(*s.Slice)[idx] = s.Unwrap(idx, value)
}
// Insert implements [Inserter].
func (s SliceInserter[T, _]) Insert(idx int, value T) {
*s.Slice = slices.Insert(*s.Slice, idx, s.Unwrap(idx, value))
}
// Delete implements [Inserter].
func (s SliceInserter[T, _]) Delete(idx int) {
*s.Slice = slices.Delete(*s.Slice, idx, idx+1)
}
// Slice2 is like Slice, but it uses a pair of slices instead of one.
//
// This is useful for cases where the raw data is represented in
// a struct-of-arrays format for memory efficiency.
type Slice2[T, E1, E2 any] struct {
// All functions on Slice2 assume that these two slices are
// always of equal length.
Slice1 []E1
Slice2 []E2
Wrap func(int, E1, E2) T
Unwrap func(int, T) (E1, E2)
}
// NewSlice2 constructs a new [Slice2].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewSlice2[T, E1, E2 any](
slice1 []E1,
slice2 []E2,
wrap func(int, E1, E2) T,
unwrap func(int, T) (E1, E2),
) Slice2[T, E1, E2] {
return Slice2[T, E1, E2]{slice1, slice2, wrap, unwrap}
}
// Len implements [Indexer].
func (s Slice2[T, _, _]) Len() int {
return len(s.Slice1)
}
// At implements [Indexer].
func (s Slice2[T, _, _]) At(idx int) T {
return s.Wrap(idx, s.Slice1[idx], s.Slice2[idx])
}
// SetAt implements [Setter].
func (s Slice2[T, _, _]) SetAt(idx int, value T) {
s.Slice1[idx], s.Slice2[idx] = s.Unwrap(idx, value)
}
// SliceInserter2 is like Slice2, but also implements [Inserter][T].
type SliceInserter2[T, E1, E2 any] struct {
// All functions on SliceInserter2 assume that these two slices are
// always of equal length.
Slice1 *[]E1
Slice2 *[]E2
Wrap func(int, E1, E2) T
Unwrap func(int, T) (E1, E2)
}
// NewSliceInserter2 constructs a new [SliceInserter2].
//
// This method exists because Go currently will not infer type parameters of a
// type.
func NewSliceInserter2[T, E1, E2 any](
slice1 *[]E1,
slice2 *[]E2,
wrap func(int, E1, E2) T,
unwrap func(int, T) (E1, E2),
) SliceInserter2[T, E1, E2] {
return SliceInserter2[T, E1, E2]{slice1, slice2, wrap, unwrap}
}
// Len implements [Indexer].
func (s SliceInserter2[T, _, _]) Len() int {
if s.Slice1 == nil {
return 0
}
return len(*s.Slice1)
}
// At implements [Indexer].
func (s SliceInserter2[T, _, _]) At(idx int) T {
return s.Wrap(idx, (*s.Slice1)[idx], (*s.Slice2)[idx])
}
// SetAt implements [Setter].
func (s SliceInserter2[T, _, _]) SetAt(idx int, value T) {
(*s.Slice1)[idx], (*s.Slice2)[idx] = s.Unwrap(idx, value)
}
// Insert implements [Inserter].
func (s SliceInserter2[T, _, _]) Insert(idx int, value T) {
r1, r2 := s.Unwrap(idx, value)
*s.Slice1 = slices.Insert(*s.Slice1, idx, r1)
*s.Slice2 = slices.Insert(*s.Slice2, idx, r2)
}
// Delete implements [Inserter].
func (s SliceInserter2[T, _, _]) Delete(idx int) {
*s.Slice1 = slices.Delete(*s.Slice1, idx, idx+1)
*s.Slice2 = slices.Delete(*s.Slice2, idx, idx+1)
}
// 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 source
import (
"slices"
"strings"
"sync"
"unicode"
"unicode/utf16"
_ "unsafe" // For go:linkname.
"github.com/bufbuild/protocompile/experimental/source/length"
"github.com/bufbuild/protocompile/internal/ext/unicodex"
)
// File is a source code file involved in a diagnostic.
//
// It contains additional book-keeping information for resolving span locations.
// Files are immutable once created.
//
// A nil *File behaves like an empty file with the path name "".
type File struct {
path, text string
once sync.Once
// A prefix sum of the line lengths of text. Given a byte offset, it is possible
// to recover which line that offset is on by performing a binary search on this
// list.
//
// Alternatively, this slice can be interpreted as the index after each \n in the
// original file.
lineIndex []int
}
// NewFile constructs a new source file.
func NewFile(path, text string) *File {
return &File{path: path, text: text}
}
// Path returns this file's filesystem path.
//
// It doesn't need to be a real path, but it will be used to deduplicate spans
// according to their file.
func (f *File) Path() string {
if f == nil {
return ""
}
return f.path
}
// Text returns this file's textual contents.
func (f *File) Text() string {
if f == nil {
return ""
}
return f.text
}
// LineByOffset searches this index to find the line number for the line
// containing this byte offset.
//
// This operation is O(log n).
func (f *File) LineByOffset(offset int) (number int) {
lines := f.lines()
// Find the smallest index in c.lines such that lines[line] <= offset.
line, exact := slices.BinarySearch(lines, offset)
if !exact {
line--
}
return line
}
// Location searches this index to build full Location information for the given
// byte offset.
//
// This operation is O(log n).
func (f *File) Location(offset int, units length.Unit) Location {
if f == nil || offset == 0 {
return Location{Offset: 0, Line: 1, Column: 1}
}
return location(f, offset, units, true)
}
// InverseLocation inverts the operation in [File.Location].
//
// line and column should be 1-indexed, and units should be the units used to
// measure the column width. If units is [TermWidth], this function panics,
// because inverting a [TermWidth] location is not supported.
func (f *File) InverseLocation(line, column int, units length.Unit) Location {
if f == nil || (line == 1 && column == 1) {
return Location{Offset: 0, Line: 1, Column: 1}
}
return Location{
Line: line, Column: column,
Offset: inverseLocation(f, line, column, units),
}
}
// Indentation calculates the indentation some offset.
//
// Indentation is defined as the substring between the last newline in
// before the offset and the first non-Pattern_White_Space after that newline.
func (f *File) Indentation(offset int) string {
nl := strings.LastIndexByte(f.Text()[:offset], '\n') + 1
margin := strings.IndexFunc(f.Text()[nl:], func(r rune) bool {
return !unicode.In(r, unicode.Pattern_White_Space)
})
return f.Text()[nl : nl+margin]
}
// Span is a shorthand for creating a new Span.
func (f *File) Span(start, end int) Span {
if f == nil {
return Span{}
}
return Span{f, start, end}
}
// Line returns the given line, including its trailing newline.
//
// line is expected to be 1-indexed.
func (f *File) Line(line int) string {
start, end := f.LineOffsets(line)
return f.text[start:end]
}
// LineOffsets returns the offsets for the given line, including its trailing
// newline.
//
// line is expected to be 1-indexed.
func (f *File) LineOffsets(line int) (start, end int) {
lines := f.lines()
if len(lines) == line {
return lines[line-1], len(f.Text())
}
return lines[line-1], lines[line]
}
// EOF returns a Span pointing to the end-of-file.
func (f *File) EOF() Span {
if f == nil {
return Span{}
}
// Find the last non-space rune; we moor the span immediately after it.
eof := strings.LastIndexFunc(f.Text(), func(r rune) bool {
return !unicode.In(r, unicode.Pattern_White_Space)
})
if eof == -1 {
eof = 0 // The whole file is whitespace.
}
return f.Span(eof+1, eof+1)
}
//go:linkname location github.com/bufbuild/protocompile/experimental/report.fileLocation
func location(f *File, offset int, units length.Unit, allowNonPrint bool) Location {
lines := f.lines()
// Find the smallest index in c.lines such that lines[line] <= offset.
line, exact := slices.BinarySearch(lines, offset)
if !exact {
line--
}
chunk := f.Text()[lines[line]:offset]
var column int
switch units {
case length.Runes:
for range chunk {
column++
}
case length.Bytes:
column = len(chunk)
case length.UTF16:
for _, r := range chunk {
column += utf16.RuneLen(r)
}
case length.TermWidth:
w := &unicodex.Width{
EscapeNonPrint: !allowNonPrint,
}
_, _ = w.WriteString(chunk)
column = w.Column
}
return Location{
Offset: offset,
Line: line + 1,
Column: column + 1,
}
}
func inverseLocation(f *File, line, column int, units length.Unit) int {
// Find the start the given line.
start, end := f.LineOffsets(line)
chunk := f.text[start:end]
var offset int
switch units {
case length.Runes:
for offset = range chunk {
column--
if column <= 0 {
break
}
}
offset += column
case length.Bytes:
offset = column - 1
case length.UTF16:
var r rune
for offset, r = range chunk {
column -= utf16.RuneLen(r)
if column <= 0 {
break
}
}
if column > 0 {
offset += column
}
case length.TermWidth:
panic("protocompile/source: passed TermWidth to File.InvertLocation")
}
return start + offset
}
func (f *File) lines() []int {
if f == nil {
return nil
}
// Compute the prefix sum on-demand.
f.once.Do(func() {
var next int
// We add 1 to the return value of IndexByte because we want to work
// with the index immediately *after* the newline byte.
text := f.Text()
for {
newline := strings.IndexByte(text, '\n') + 1
if newline == 0 {
break
}
text = text[newline:]
f.lineIndex = append(f.lineIndex, next)
next += newline
}
f.lineIndex = append(f.lineIndex, next)
})
return f.lineIndex
}
// 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 unit.yaml. DO NOT EDIT.
package length
import (
"fmt"
"iter"
)
// Unit represents units of measurement for the length of a string.
//
// The most commonly used [Unit] in protocompile is [TermWidth], which
// approximates columns in a terminal emulator. This takes into account the
// Unicode width of runes, and tabstops. The rune A is one column wide, the rune
// 貓 is two columns wide, and the multi-rune emoji presentation sequence 🐈⬛ is
// also two columns wide.
//
// Other units of length can be used to cope with the needs of other rendering
// contexts, such as the Language Server Protocol.
type Unit int
const (
Bytes Unit = iota // The length in UTF-8 code units (bytes).
UTF16 // The length in UTF-16 code units (uint16s).
Runes // The length in UTF-32 code units (runes).
TermWidth // The length in approximate terminal columns.
)
// String implements [fmt.Stringer].
func (v Unit) String() string {
if int(v) < 0 || int(v) > len(_table_Unit_String) {
return fmt.Sprintf("Unit(%v)", int(v))
}
return _table_Unit_String[v]
}
// GoString implements [fmt.GoStringer].
func (v Unit) GoString() string {
if int(v) < 0 || int(v) > len(_table_Unit_GoString) {
return fmt.Sprintf("length.Unit(%v)", int(v))
}
return _table_Unit_GoString[v]
}
// Units returns an iterator over all of the [Unit]s
func Units() iter.Seq[Unit] {
return func(yield func(Unit) bool) {
for i := 0; i < 4; i++ {
if !yield(Unit(i)) {
return
}
}
}
}
var _table_Unit_String = [...]string{
Bytes: "Bytes",
UTF16: "UTF16",
Runes: "Runes",
TermWidth: "TermWidth",
}
var _table_Unit_GoString = [...]string{
Bytes: "length.Bytes",
UTF16: "length.UTF16",
Runes: "length.Runes",
TermWidth: "length.TermWidth",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 source
import (
"errors"
"io"
"io/fs"
"strings"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
)
// Opener is a mechanism for opening files.
//
// Opener implementations are assumed by Protocompile to be comparable. It is
// sufficient to always ensure that the implementation uses a pointer receiver.
type Opener interface {
// Open opens a file, potentially returning an error.
//
// Note that the path of the returned file need not he path; this path should
// *only* be used for diagnostics.
//
// A return value of [fs.ErrNotExist] is given special treatment by some
// Opener adapters, such as the [Openers] type.
Open(path string) (*File, error)
}
// Map implements [Opener] via lookup of a built-in map. This map is not
// directly accessible, to help avoid mistaken uses that cause different *Map
// pointer values (for the same built-in map value) to wind up in different
// queries, which breaks query caching.
//
// Missing entries result in [fs.ErrNotExist].
type Map cmpx.MapWrapper[string, *File]
// NewMap creates a new [Map] wrapping the given map.
//
// If passed nil, this will update the map to be an empty non-nil map.
func NewMap(m map[string]*File) Map {
if m == nil {
m = make(map[string]*File)
}
return Map(cmpx.NewMapWrapper(m))
}
// Get returns the map this [Map] wraps. This can be used to modify the map.
//
// Never returns nil.
func (m Map) Get() map[string]*File {
return cmpx.MapWrapper[string, *File](m).Get()
}
// Add adds a new file to this map.
func (m Map) Add(path, text string) {
m.Get()[path] = NewFile(path, text)
}
// Open implements [Opener].
func (m Map) Open(path string) (*File, error) {
file, ok := m.Get()[path]
if !ok {
return nil, fs.ErrNotExist
}
return file, nil
}
// FS wraps an [fs.FS] to give it an [Opener] interface.
type FS struct {
fs.FS
// If not nil, paths are passed to this function before being forwarded
// to fs.
PathMapper func(string) string
}
// Open implements [Opener].
func (fs *FS) Open(path string) (*File, error) {
if fs.PathMapper != nil {
path = fs.PathMapper(path)
}
file, err := fs.FS.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var buf strings.Builder
_, err = io.Copy(&buf, file)
if err != nil {
return nil, err
}
return NewFile(path, buf.String()), nil
}
// Openers wraps a sequence of [Opener]s.
//
// When calling Open, it calls each Opener in sequence until one does not return
// [fs.ErrNotExist].
type Openers []Opener
// Open implements [Opener].
func (o *Openers) Open(path string) (*File, error) {
for _, opener := range *o {
file, err := opener.Open(path)
if errors.Is(err, fs.ErrNotExist) {
continue
}
return file, err
}
return nil, fs.ErrNotExist
}
// 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 source
import (
"fmt"
"iter"
"math"
"slices"
"unicode/utf8"
"github.com/bufbuild/protocompile/experimental/source/length"
)
// Spanner is any type with a [Span].
type Spanner interface {
// Should return the zero [Span] to indicate that it does not contribute
// span information.
Span() Span
}
// Span is a location within a [File].
type Span struct {
// The file this span refers to. The file must be indexed, since we plan to
// convert Start/End into editor coordinates.
*File
// The start and end byte offsets for this span.
Start, End int
}
// Location is a user-displayable location within a source code file.
type Location struct {
// The byte offset for this location.
Offset int
// The line and column for this location, 1-indexed.
//
// The units of measurement for column depend on the [length.Unit] used when
// constructing it.
//
// Because these are 1-indexed, a zero Line can be used as a sentinel.
Line, Column int
}
// IsZero returns whether or not this is the zero span.
func (s Span) IsZero() bool {
return s.File == nil
}
// Text returns the text corresponding to this span.
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 {
return s.File.Indentation(s.Start)
}
// Before returns all text before this span.
func (s Span) Before() string {
return s.File.Text()[:s.Start]
}
// After 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
}
// GrowRight returns a new span which contains the largest prefix of [Span.After]
// which match p.
func (s Span) GrowRight(p func(r rune) bool) Span {
for {
r, sz := utf8.DecodeRuneInString(s.After())
if r == utf8.RuneError || !p(r) {
break
}
s.End += sz
}
return s
}
// Len returns the length of this span, in bytes.
func (s Span) Len() int {
return s.End - s.Start
}
// StartLoc returns the start location for this span.
func (s Span) StartLoc() Location {
return s.Location(s.Start, length.TermWidth)
}
// EndLoc returns the end location for this span.
func (s Span) EndLoc() Location {
return s.Location(s.End, length.TermWidth)
}
// Span implements [Spanner].
func (s Span) Span() Span {
return s
}
// Range slices this span along the given byte indices.
//
// Unlike slicing into a string, out-of-bounds indices are snapped to the
// boundaries of the string, and negative indices are taken from the back of
// the span. For example, s.RuneRange(-2, -1) is the final rune of the span
// (or an empty span, if s is empty).
func (s Span) Range(i, j int) Span {
i = idxToByteOffset(s.Text(), i)
j = idxToByteOffset(s.Text(), j)
if i > j {
i, j = j, i
}
return s.File.Span(i+s.Start, j+s.Start)
}
// RuneRange slices this span along the given rune indices.
//
// For example, s.RuneRange(0, 2) returns at most the first two runes of the
// span.
//
// Unlike slicing into a string, out-of-bounds indices are snapped to the
// boundaries of the string, and negative indices are taken from the back of
// the span. For example, s.RuneRange(-2, -1) is the final rune of the span
// (or an empty span, if s is empty).
func (s Span) RuneRange(i, j int) Span {
i = runeIdxToByteOffset(s.Text(), i)
j = runeIdxToByteOffset(s.Text(), j)
if i > j {
i, j = j, i
}
return s.File.Span(i+s.Start, j+s.Start)
}
// Rune is a shorthand for RuneRange(i, i+1) or RuneRange(i-1, i), depending
// on the sign of i.
func (s Span) Rune(i int) Span {
if i < 0 {
return s.RuneRange(i-1, i)
}
return s.RuneRange(i, i+1)
}
// String implements [string.Stringer].
func (s Span) String() string {
start := s.StartLoc()
return fmt.Sprintf("%q:%d:%d[%d:%d]", s.Path(), start.Line, start.Column, s.Start, s.End)
}
// Join joins a collection of spans, returning the smallest span that
// contains all of them.
//
// IsZero spans among spans are ignored. If every span in spans is zero, returns
// the zero span.
//
// If there are at least two distinct files among the non-zero spans,
// this function panics.
func Join(spans ...Spanner) Span {
return JoinSeq[Spanner](slices.Values(spans))
}
// JoinSeq is like [Join], but takes a sequence of any spannable type.
func JoinSeq[S Spanner](seq iter.Seq[S]) Span {
joined := Span{Start: math.MaxInt}
for spanner := range seq {
span := GetSpan(spanner)
if span.IsZero() {
continue
}
if joined.IsZero() {
joined.File = span.File
} else if joined.File != span.File {
panic(fmt.Sprintf(
"protocompile/source: passed spans with distinct files to JoinSpans(): %q != %q",
joined.File.Path(),
span.File.Path(),
))
}
joined.Start = min(joined.Start, span.Start)
joined.End = max(joined.End, span.End)
}
if joined.File == nil {
return Span{}
}
return joined
}
// GetSpan extracts a span from a Spanner, but returns the zero span when
// s is zero, which would otherwise panic.
func GetSpan(s Spanner) Span {
if s == nil {
return Span{}
}
return s.Span()
}
// Between is a helper function that returns a [Span] for the space between spans a and b,
// inclusive. If a and b do not have the same [File] or if the spans overlap, then this
// returns a zero span.
func Between(a, b Span) Span {
if a.File != b.File || b.Start < a.End {
return Span{}
}
return Span{
File: a.File,
Start: a.Start,
End: b.End,
}
}
// idxToByteOffset converts a byte index into s into a byte offset.
//
// If i is negative, this produces the index of the -ith byte from the end of
// the string.
//
// If i > len(s) or i < -len(s), returns len(s) or 0, respectively; i is always
// valid to index into s with.
func idxToByteOffset(s string, i int) int {
switch {
case i > len(s):
return len(s)
case i < -len(s):
return 0
case i < 0:
return len(s) + i
default:
return i
}
}
// runeIdxToByteOffset converts a rune index into s into a byte offset.
//
// If i is negative, this produces the index of the -ith rune from the end of
// the string.
//
// If i > len(s) or i < -len(s), returns len(s) or 0, respectively; i is always
// valid to index into s with.
func runeIdxToByteOffset(s string, i int) int {
for i < 0 {
i++
if i == 0 || s == "" {
return len(s)
}
_, j := utf8.DecodeLastRuneInString(s)
s = s[:len(s)-j]
}
for j := range s {
if i == 0 {
return j
}
i--
}
return len(s)
}
// 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 source
import "github.com/bufbuild/protocompile/wellknownimports"
var wktFS = FS{FS: wellknownimports.FS()}
// WKTs returns an [Opener] that yields in-memory Protobuf well-known type sources.
func WKTs() Opener {
// All openers returned by this function compare equal.
return wkts{}
}
type wkts struct{}
func (wkts) Open(path string) (*File, error) {
file, err := wktFS.Open(path)
if err != nil {
return nil, err
}
file.path = "<built-in>/" + path
return file, nil
}
// 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 token
import (
"fmt"
"iter"
"strings"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// Cursor is an iterator-like construct for looping over a token tree.
// Unlike a plain range func, it supports peeking.
type Cursor struct {
context *Stream
// This is used if this is a cursor over the children of a synthetic token.
// If stream is nil, we know we're in the natural case.
stream []ID
// This is the index into either Context().Stream().nats or the stream.
idx int
// This is used to know if we moved forwards or backwards when calculating
// the offset jump on a change of directions.
isBackwards bool
}
// CursorMark is the return value of [Cursor.Mark], which marks a position on
// a Cursor for rewinding to.
type CursorMark struct {
// This contains exactly the values needed to rewind the cursor.
owner *Cursor
idx int
isBackwards bool
}
// NewCursorAt returns a new cursor at the given token.
//
// Panics if the token is zero or synthetic.
func NewCursorAt(tok Token) *Cursor {
if tok.IsZero() {
panic(fmt.Sprintf("protocompile/token: passed zero token to NewCursorAt: %v", tok))
}
if tok.IsSynthetic() {
panic(fmt.Sprintf("protocompile/token: passed synthetic token to NewCursorAt: %v", tok))
}
return &Cursor{
context: tok.Context(),
idx: naturalIndex(tok.ID()), // Convert to 0-based index.
isBackwards: tok.nat().IsClose(), // Set the direction to calculate the offset.
}
}
// NewSliceCursor returns a new cursor over a slice of token IDs in the given
// context.
func NewSliceCursor(stream *Stream, slice []ID) *Cursor {
return &Cursor{
context: stream,
stream: slice,
}
}
// Context returns this Cursor's context.
func (c *Cursor) Context() *Stream {
return c.context
}
// Done returns whether or not there are still tokens left to yield.
func (c *Cursor) Done() bool {
return c.Peek().IsZero()
}
// IsSynthetic returns whether this is a cursor over synthetic tokens.
func (c *Cursor) IsSynthetic() bool {
return c.stream != nil
}
// Clone returns a copy of this cursor, which allows performing operations on
// it without mutating the original cursor.
func (c *Cursor) Clone() *Cursor {
clone := *c
return &clone
}
// Mark makes a mark on this cursor to indicate a place that can be rewound
// to.
func (c *Cursor) Mark() CursorMark {
return CursorMark{
owner: c,
idx: c.idx,
isBackwards: c.isBackwards,
}
}
// Rewind moves this cursor back to the position described by Rewind.
//
// Panics if mark was not created using this cursor's Mark method.
func (c *Cursor) Rewind(mark CursorMark) {
if c != mark.owner {
panic("protocompile/token: rewound cursor using the wrong cursor's mark")
}
c.idx = mark.idx
c.isBackwards = mark.isBackwards
}
// PeekSkippable returns the current token in the sequence, if there is one.
// This may return a skippable token.
//
// Returns the zero token if this cursor is at the end of the stream.
func (c *Cursor) PeekSkippable() Token {
if c == nil {
return Zero
}
if c.IsSynthetic() {
tokenID, ok := slicesx.Get(c.stream, c.idx)
if !ok {
return Zero
}
return id.Wrap(c.Context(), tokenID)
}
stream := c.Context()
impl, ok := slicesx.Get(stream.nats, c.idx)
if !ok || (!c.isBackwards && impl.IsClose()) {
return Zero // Reached the end.
}
return id.Wrap(c.Context(), ID(c.idx+1))
}
// PeekPrevSkippable returns the token before the current token in the sequence, if there is one.
// This may return a skippable token.
//
// Returns the zero token if this cursor is at the beginning of the stream.
func (c *Cursor) PeekPrevSkippable() Token {
if c == nil {
return Zero
}
if c.IsSynthetic() {
tokenID, ok := slicesx.Get(c.stream, c.idx-1)
if !ok {
return Zero
}
return id.Wrap(c.Context(), tokenID)
}
stream := c.Context()
idx := c.idx - 1
if c.isBackwards {
impl, ok := slicesx.Get(stream.nats, c.idx)
if ok && impl.IsClose() {
idx += impl.Offset()
}
}
impl, ok := slicesx.Get(stream.nats, idx)
if !ok || impl.IsOpen() {
return Zero // Reached the start.
}
return id.Wrap(c.Context(), ID(idx+1))
}
// NextSkippable returns the next skippable token in the sequence, and advances the cursor.
func (c *Cursor) NextSkippable() Token {
tok := c.PeekSkippable()
if tok.IsZero() {
return tok
}
c.isBackwards = false
if c.IsSynthetic() {
c.idx++
} else {
impl := tok.nat()
if impl.IsOpen() {
c.idx += impl.Offset()
}
c.idx++
}
return tok
}
// PrevSkippable returns the previous skippable token in the sequence, and decrements the cursor.
func (c *Cursor) PrevSkippable() Token {
tok := c.PeekPrevSkippable()
if tok.IsZero() {
return tok
}
c.isBackwards = true
if c.IsSynthetic() {
c.idx--
} else {
c.idx = naturalIndex(tok.ID())
}
return tok
}
// Peek returns the next token in the sequence, if there is one.
// This automatically skips past skippable tokens.
//
// Returns the zero token if this cursor is at the end of the stream.
func (c *Cursor) Peek() Token {
if c == nil {
return Zero
}
cursor := *c
return cursor.Next()
}
// PeekPrev returns the previous token in the sequence, if there is one.
// This automatically skips past skippable tokens.
//
// Returns the zero token if this cursor is at the start of the stream.
func (c *Cursor) PeekPrev() Token {
if c == nil {
return Zero
}
cursor := *c
return cursor.Prev()
}
// Next returns the next token in the sequence, and advances the cursor.
func (c *Cursor) Next() Token {
for {
next := c.NextSkippable()
if next.IsZero() || !next.Kind().IsSkippable() {
return next
}
}
}
// Prev returns the previous token in the sequence, and decrements the cursor.
func (c *Cursor) Prev() Token {
for {
prev := c.PrevSkippable()
if prev.IsZero() || !prev.Kind().IsSkippable() {
return prev
}
}
}
// Rest returns an iterator over the remaining tokens in the cursor.
//
// Note that breaking out of a loop over this iterator, and starting
// a new loop, will resume at the iteration that was broken at. E.g., if
// we break out of a loop over c.Iter at token tok, and start a new range
// over c.Iter, the first yielded token will be tok.
func (c *Cursor) Rest() iter.Seq[Token] {
return func(yield func(Token) bool) {
for {
tok := c.Peek()
if tok.IsZero() || !yield(tok) {
break
}
_ = c.Next()
}
}
}
// RestSkippable is like [Cursor.Rest]. but it yields skippable tokens, too.
//
// Note that breaking out of a loop over this iterator, and starting
// a new loop, will resume at the iteration that was broken at. E.g., if
// we break out of a loop over c.Iter at token tok, and start a new range
// over c.Iter, the first yielded token will be tok.
func (c *Cursor) RestSkippable() iter.Seq[Token] {
return func(yield func(Token) bool) {
for {
tok := c.PeekSkippable()
if tok.IsZero() || !yield(tok) {
break
}
_ = c.NextSkippable()
}
}
}
// SeekToEnd returns a span for whatever comes immediately after the end of this
// cursor (be that a token or the EOF), and advances the cursor to the end.
// If it is a token, this will return that token, too.
//
// Returns [Zero] for a synthetic cursor.
func (c *Cursor) SeekToEnd() (Token, source.Span) {
if c == nil || c.stream != nil {
return Zero, source.Span{}
}
// Seek to the end.
end := c.NextSkippable()
for !end.IsZero() {
end = c.NextSkippable()
}
stream := c.Context()
if c.idx >= len(stream.nats) {
// This is the case where this cursor is a Stream.Cursor(). Thus, the
// just-after span should be the EOF.
return Zero, stream.EOF()
}
// Otherwise, return end.
tok := id.Wrap(c.Context(), ID(c.idx+1))
return tok, stream.Span(tok.offsets())
}
// NewLinesBetween counts the number of \n characters between the end of [token.Token] a
// and the start of b, up to the limit.
//
// The final rune of a is included in this count, since comments may end in a \n rune.
func (c *Cursor) NewLinesBetween(a, b Token, limit int) int {
end := a.LeafSpan().End
if end != 0 {
// Account for the final rune of a
end--
}
start := b.LeafSpan().Start
between := c.Context().Text()[end:start]
var total int
for total < limit {
var found bool
_, between, found = strings.Cut(between, "\n")
if !found {
break
}
total++
}
return total
}
// 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 keyword.yaml. DO NOT EDIT.
package keyword
import (
"fmt"
"iter"
)
// Keywords are special "grammar particles" recognized by the compiler.
//
// These include both reserved words, including all identifiers with special
// treatment [in Protobuf], as well as all valid punctuation.
//
// [in Protobuf]: https://protobuf.com/docs/language-spec#identifiers-and-keywords
type Keyword byte
const (
Unknown Keyword = iota // Zero value, not a real keyword.
Syntax // syntax
Edition // edition
Import // import
Weak // weak
Public // public
Package // package
Message // message
Enum // enum
Service // service
Extend // extend
Option // option
Group // group
Oneof // oneof
Extensions // extensions
Reserved // reserved
RPC // rpc
Returns // returns
To // to
Optional // optional
Repeated // repeated
Required // required
Stream // stream
Export // export
Local // local
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
Inf // inf
NaN // nan
True // true
False // false
Null // null
Map // map
Max // max
Return // return
Break // break
Continue // continue
Yield // yield
Defer // defer
Try // try
Catch // catch
If // if
Unless // unless
Else // else
Loop // loop
While // while
Do // do
For // for
In // in
Switch // switch
Match // match
Case // case
As // as
Func // func
Const // const
Let // let
Var // var
Type // type
Extern // extern
And // and
Or // or
Not // not
Default // default
JsonName // json_name
Semi // ;
Comma // ,
Dot // .
Colon // :
Newline // \n
At // @
Hash // #
Dollar // $
Twiddle // ~
Add // +
Sub // -
Mul // *
Div // /
Rem // %
Amp // &
Pipe // |
Xor // ^
Shl // <<
Shr // >>
Bang // !
Bangs // !!
Ask // ?
Asks // ??
Amps // &&
Pipes // ||
Assign // =
AssignNew // :=
AssignAdd // +=
AssignSub // -=
AssignMul // *=
AssignDiv // /=
AssignRem // %=
AssignAmp // &=
AssignPipe // |=
AssignXor // ^=
AssignShl // <<=
AssignShr // >>=
Range // ..
RangeEq // ..=
LParen // (
RParen // )
LBracket // [
RBracket // ]
LBrace // {
RBrace // }
Lt // <
Gt // >
Le // <=
Ge // >=
Eq // ==
Ne // !=
Comment // //
LComment // /*
RComment // */
Parens // (...) (fused)
Brackets // [...] (fused)
Braces // {...} (fused)
Angles // <...> (fused)
BlockComment // /* ... */ (fused)
)
// String implements [fmt.Stringer].
func (v Keyword) String() string {
if int(v) < 0 || int(v) > len(_table_Keyword_String) {
return fmt.Sprintf("Keyword(%v)", int(v))
}
return _table_Keyword_String[v]
}
// GoString implements [fmt.GoStringer].
func (v Keyword) GoString() string {
if int(v) < 0 || int(v) > len(_table_Keyword_GoString) {
return fmt.Sprintf("keyword.Keyword(%v)", int(v))
}
return _table_Keyword_GoString[v]
}
// Lookup looks up a keyword by name.
//
// If name does not name a keyword, returns [Unknown].
func Lookup(s string) Keyword {
return _table_Keyword_Lookup[s]
}
// All returns an iterator over all distinct [Keyword] values.
func All() iter.Seq[Keyword] {
return func(yield func(Keyword) bool) {
for i := 1; i < 136; i++ {
if !yield(Keyword(i)) {
return
}
}
}
}
var _table_Keyword_String = [...]string{
Unknown: "unknown",
Syntax: "syntax",
Edition: "edition",
Import: "import",
Weak: "weak",
Public: "public",
Package: "package",
Message: "message",
Enum: "enum",
Service: "service",
Extend: "extend",
Option: "option",
Group: "group",
Oneof: "oneof",
Extensions: "extensions",
Reserved: "reserved",
RPC: "rpc",
Returns: "returns",
To: "to",
Optional: "optional",
Repeated: "repeated",
Required: "required",
Stream: "stream",
Export: "export",
Local: "local",
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",
Inf: "inf",
NaN: "nan",
True: "true",
False: "false",
Null: "null",
Map: "map",
Max: "max",
Return: "return",
Break: "break",
Continue: "continue",
Yield: "yield",
Defer: "defer",
Try: "try",
Catch: "catch",
If: "if",
Unless: "unless",
Else: "else",
Loop: "loop",
While: "while",
Do: "do",
For: "for",
In: "in",
Switch: "switch",
Match: "match",
Case: "case",
As: "as",
Func: "func",
Const: "const",
Let: "let",
Var: "var",
Type: "type",
Extern: "extern",
And: "and",
Or: "or",
Not: "not",
Default: "default",
JsonName: "json_name",
Semi: ";",
Comma: ",",
Dot: ".",
Colon: ":",
Newline: "\n",
At: "@",
Hash: "#",
Dollar: "$",
Twiddle: "~",
Add: "+",
Sub: "-",
Mul: "*",
Div: "/",
Rem: "%",
Amp: "&",
Pipe: "|",
Xor: "^",
Shl: "<<",
Shr: ">>",
Bang: "!",
Bangs: "!!",
Ask: "?",
Asks: "??",
Amps: "&&",
Pipes: "||",
Assign: "=",
AssignNew: ":=",
AssignAdd: "+=",
AssignSub: "-=",
AssignMul: "*=",
AssignDiv: "/=",
AssignRem: "%=",
AssignAmp: "&=",
AssignPipe: "|=",
AssignXor: "^=",
AssignShl: "<<=",
AssignShr: ">>=",
Range: "..",
RangeEq: "..=",
LParen: "(",
RParen: ")",
LBracket: "[",
RBracket: "]",
LBrace: "{",
RBrace: "}",
Lt: "<",
Gt: ">",
Le: "<=",
Ge: ">=",
Eq: "==",
Ne: "!=",
Comment: "//",
LComment: "/*",
RComment: "*/",
Parens: "(...)",
Brackets: "[...]",
Braces: "{...}",
Angles: "<...>",
BlockComment: "/* ... */",
}
var _table_Keyword_GoString = [...]string{
Unknown: "keyword.Unknown",
Syntax: "keyword.Syntax",
Edition: "keyword.Edition",
Import: "keyword.Import",
Weak: "keyword.Weak",
Public: "keyword.Public",
Package: "keyword.Package",
Message: "keyword.Message",
Enum: "keyword.Enum",
Service: "keyword.Service",
Extend: "keyword.Extend",
Option: "keyword.Option",
Group: "keyword.Group",
Oneof: "keyword.Oneof",
Extensions: "keyword.Extensions",
Reserved: "keyword.Reserved",
RPC: "keyword.RPC",
Returns: "keyword.Returns",
To: "keyword.To",
Optional: "keyword.Optional",
Repeated: "keyword.Repeated",
Required: "keyword.Required",
Stream: "keyword.Stream",
Export: "keyword.Export",
Local: "keyword.Local",
Int32: "keyword.Int32",
Int64: "keyword.Int64",
Uint32: "keyword.Uint32",
Uint64: "keyword.Uint64",
Sint32: "keyword.Sint32",
Sint64: "keyword.Sint64",
Fixed32: "keyword.Fixed32",
Fixed64: "keyword.Fixed64",
Sfixed32: "keyword.Sfixed32",
Sfixed64: "keyword.Sfixed64",
Float: "keyword.Float",
Double: "keyword.Double",
Bool: "keyword.Bool",
String: "keyword.String",
Bytes: "keyword.Bytes",
Inf: "keyword.Inf",
NaN: "keyword.NaN",
True: "keyword.True",
False: "keyword.False",
Null: "keyword.Null",
Map: "keyword.Map",
Max: "keyword.Max",
Return: "keyword.Return",
Break: "keyword.Break",
Continue: "keyword.Continue",
Yield: "keyword.Yield",
Defer: "keyword.Defer",
Try: "keyword.Try",
Catch: "keyword.Catch",
If: "keyword.If",
Unless: "keyword.Unless",
Else: "keyword.Else",
Loop: "keyword.Loop",
While: "keyword.While",
Do: "keyword.Do",
For: "keyword.For",
In: "keyword.In",
Switch: "keyword.Switch",
Match: "keyword.Match",
Case: "keyword.Case",
As: "keyword.As",
Func: "keyword.Func",
Const: "keyword.Const",
Let: "keyword.Let",
Var: "keyword.Var",
Type: "keyword.Type",
Extern: "keyword.Extern",
And: "keyword.And",
Or: "keyword.Or",
Not: "keyword.Not",
Default: "keyword.Default",
JsonName: "keyword.JsonName",
Semi: "keyword.Semi",
Comma: "keyword.Comma",
Dot: "keyword.Dot",
Colon: "keyword.Colon",
Newline: "keyword.Newline",
At: "keyword.At",
Hash: "keyword.Hash",
Dollar: "keyword.Dollar",
Twiddle: "keyword.Twiddle",
Add: "keyword.Add",
Sub: "keyword.Sub",
Mul: "keyword.Mul",
Div: "keyword.Div",
Rem: "keyword.Rem",
Amp: "keyword.Amp",
Pipe: "keyword.Pipe",
Xor: "keyword.Xor",
Shl: "keyword.Shl",
Shr: "keyword.Shr",
Bang: "keyword.Bang",
Bangs: "keyword.Bangs",
Ask: "keyword.Ask",
Asks: "keyword.Asks",
Amps: "keyword.Amps",
Pipes: "keyword.Pipes",
Assign: "keyword.Assign",
AssignNew: "keyword.AssignNew",
AssignAdd: "keyword.AssignAdd",
AssignSub: "keyword.AssignSub",
AssignMul: "keyword.AssignMul",
AssignDiv: "keyword.AssignDiv",
AssignRem: "keyword.AssignRem",
AssignAmp: "keyword.AssignAmp",
AssignPipe: "keyword.AssignPipe",
AssignXor: "keyword.AssignXor",
AssignShl: "keyword.AssignShl",
AssignShr: "keyword.AssignShr",
Range: "keyword.Range",
RangeEq: "keyword.RangeEq",
LParen: "keyword.LParen",
RParen: "keyword.RParen",
LBracket: "keyword.LBracket",
RBracket: "keyword.RBracket",
LBrace: "keyword.LBrace",
RBrace: "keyword.RBrace",
Lt: "keyword.Lt",
Gt: "keyword.Gt",
Le: "keyword.Le",
Ge: "keyword.Ge",
Eq: "keyword.Eq",
Ne: "keyword.Ne",
Comment: "keyword.Comment",
LComment: "keyword.LComment",
RComment: "keyword.RComment",
Parens: "keyword.Parens",
Brackets: "keyword.Brackets",
Braces: "keyword.Braces",
Angles: "keyword.Angles",
BlockComment: "keyword.BlockComment",
}
var _table_Keyword_Lookup = map[string]Keyword{
"unknown": Unknown,
"syntax": Syntax,
"edition": Edition,
"import": Import,
"weak": Weak,
"public": Public,
"package": Package,
"message": Message,
"enum": Enum,
"service": Service,
"extend": Extend,
"option": Option,
"group": Group,
"oneof": Oneof,
"extensions": Extensions,
"reserved": Reserved,
"rpc": RPC,
"returns": Returns,
"to": To,
"optional": Optional,
"repeated": Repeated,
"required": Required,
"stream": Stream,
"export": Export,
"local": Local,
"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,
"inf": Inf,
"nan": NaN,
"true": True,
"false": False,
"null": Null,
"map": Map,
"max": Max,
"return": Return,
"break": Break,
"continue": Continue,
"yield": Yield,
"defer": Defer,
"try": Try,
"catch": Catch,
"if": If,
"unless": Unless,
"else": Else,
"loop": Loop,
"while": While,
"do": Do,
"for": For,
"in": In,
"switch": Switch,
"match": Match,
"case": Case,
"as": As,
"func": Func,
"const": Const,
"let": Let,
"var": Var,
"type": Type,
"extern": Extern,
"and": And,
"or": Or,
"not": Not,
"default": Default,
"json_name": JsonName,
";": Semi,
",": Comma,
".": Dot,
":": Colon,
"\n": Newline,
"@": At,
"#": Hash,
"$": Dollar,
"~": Twiddle,
"+": Add,
"-": Sub,
"*": Mul,
"/": Div,
"%": Rem,
"&": Amp,
"|": Pipe,
"^": Xor,
"<<": Shl,
">>": Shr,
"!": Bang,
"!!": Bangs,
"?": Ask,
"??": Asks,
"&&": Amps,
"||": Pipes,
"=": Assign,
":=": AssignNew,
"+=": AssignAdd,
"-=": AssignSub,
"*=": AssignMul,
"/=": AssignDiv,
"%=": AssignRem,
"&=": AssignAmp,
"|=": AssignPipe,
"^=": AssignXor,
"<<=": AssignShl,
">>=": AssignShr,
"..": Range,
"..=": RangeEq,
"(": LParen,
")": RParen,
"[": LBracket,
"]": RBracket,
"{": LBrace,
"}": RBrace,
"<": Lt,
">": Gt,
"<=": Le,
">=": Ge,
"==": Eq,
"!=": Ne,
"//": Comment,
"/*": LComment,
"*/": RComment,
}
var _ iter.Seq[int] // Mark iter as used.
// 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 keyword
import (
"iter"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/trie"
)
var kwTrie = func() *trie.Trie[Keyword] {
trie := new(trie.Trie[Keyword])
for kw := range All() {
if !kw.IsBrackets() {
trie.Insert(kw.String(), kw)
}
}
return trie
}()
// Prefix returns the longest prefix of text which matches any of the keywords
// that can be returned by [Lookup].
func Prefix(text string) Keyword {
_, kw := kwTrie.Get(text)
return kw
}
// Prefix returns an iterator over the keywords that can be returned by [Lookup]
// which are prefixes of text, in ascending order of length.
func Prefixes(text string) iter.Seq[Keyword] {
return iterx.Right(kwTrie.Prefixes(text))
}
// Brackets returns the open and close brackets if k is a bracket keyword.
func (k Keyword) Brackets() (left, right, joined Keyword) {
if int(k) >= len(braces) {
return Unknown, Unknown, Unknown
}
return braces[k][0], braces[k][1], braces[k][2]
}
// IsValid returns whether this is a valid keyword value (not including
// [Unknown]).
func (k Keyword) IsValid() bool {
return k.properties()&valid != 0
}
// IsProtobuf returns whether this keyword is used in Protobuf.
func (k Keyword) IsProtobuf() bool {
return k.properties()&protobuf != 0
}
// IsCEL returns whether this keyword is used in CEL.
func (k Keyword) IsCEL() bool {
return k.properties()&cel != 0
}
// IsPunctuation returns whether this keyword is punctuation (i.e., not a word).
func (k Keyword) IsPunctuation() bool {
return k.properties()&punct != 0
}
// IsReservedWord returns whether this keyword is a known reserved word (i.e.,
// not punctuation).
func (k Keyword) IsReservedWord() bool {
return k.properties()&word != 0
}
// IsBrackets returns whether this is "paired brackets" keyword.
func (k Keyword) IsBrackets() bool {
return k.properties()&brackets != 0
}
// IsModifier returns whether this keyword is any kind of modifier.
func (k Keyword) IsModifier() bool {
return k.IsMethodTypeModifier() ||
k.IsTypeModifier() ||
k.IsImportModifier() ||
k.IsMethodTypeModifier()
}
// IsFieldTypeModifier returns whether this is a modifier for a field type
// in Protobuf.
func (k Keyword) IsFieldTypeModifier() bool {
return k.properties()&modField != 0
}
// IsTypeModifier returns whether this is a modifier for a type declaration
// in Protobuf.
func (k Keyword) IsTypeModifier() bool {
return k.properties()&modType != 0
}
// IsImportModifier returns whether this is a modifier for an import declaration
// in Protobuf.
func (k Keyword) IsImportModifier() bool {
return k.properties()&modImport != 0
}
// IsMethodTypeModifier returns whether this is a modifier for a method
// declaration in Protobuf.
func (k Keyword) IsMethodTypeModifier() bool {
return k.properties()&modMethodType != 0
}
// IsPseudoOption returns whether this is a Protobuf pseudo-option name
// ([default = "..."] and [json_name = "..."]).
func (k Keyword) IsPseudoOption() bool {
return k.properties()&pseudoOption != 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 keyword
type property uint16
const (
valid property = 1 << iota
punct
word
brackets
protobuf
cel
modField
modType
modImport
modMethodType
pseudoOption
)
func (k Keyword) properties() property {
if int(k) < len(properties) {
return properties[k]
}
return 0
}
// properties is a table of keyword properties, stored as bitsets.
var properties = [...]property{
Syntax: valid | word | protobuf,
Edition: valid | word | protobuf,
Import: valid | word | protobuf | cel,
Weak: valid | word | protobuf | modImport,
Public: valid | word | protobuf | modImport,
Package: valid | word | protobuf | cel,
Message: valid | word | protobuf,
Enum: valid | word | protobuf,
Service: valid | word | protobuf,
Extend: valid | word | protobuf,
Option: valid | word | protobuf | modImport,
Group: valid | word | protobuf,
Oneof: valid | word | protobuf,
Extensions: valid | word | protobuf,
Reserved: valid | word | protobuf,
RPC: valid | word | protobuf,
Returns: valid | word | protobuf,
To: valid | word | protobuf,
Repeated: valid | word | protobuf | modField,
Optional: valid | word | protobuf | modField,
Required: valid | word | protobuf | modField,
Stream: valid | word | protobuf | modMethodType,
Export: valid | word | protobuf | modType,
Local: valid | word | protobuf | modType,
Int32: valid | word | protobuf,
Int64: valid | word | protobuf,
Uint32: valid | word | protobuf,
Uint64: valid | word | protobuf,
Sint32: valid | word | protobuf,
Sint64: valid | word | protobuf,
Fixed32: valid | word | protobuf,
Fixed64: valid | word | protobuf,
Sfixed32: valid | word | protobuf,
Sfixed64: valid | word | protobuf,
Float: valid | word | protobuf,
Double: valid | word | protobuf,
Bool: valid | word | protobuf,
String: valid | word | protobuf,
Bytes: valid | word | protobuf,
Inf: valid | word | protobuf,
NaN: valid | word | protobuf,
True: valid | word | protobuf | cel,
False: valid | word | protobuf | cel,
Null: valid | word | protobuf | cel,
Map: valid | word | protobuf,
Max: valid | word | protobuf,
Return: valid | word,
Break: valid | word,
Continue: valid | word,
Yield: valid | word,
Defer: valid | word,
Try: valid | word,
Catch: valid | word,
If: valid | word,
Unless: valid | word,
Else: valid | word,
Loop: valid | word,
While: valid | word,
Do: valid | word,
For: valid | word,
In: valid | word | cel,
Switch: valid | word,
Match: valid | word,
Case: valid | word,
As: valid | word,
Func: valid | word,
Const: valid | word,
Let: valid | word,
Var: valid | word,
Type: valid | word,
Extern: valid | word,
And: valid | word,
Or: valid | word,
Not: valid | word,
Default: valid | word | protobuf | pseudoOption,
JsonName: valid | word | protobuf | pseudoOption,
Semi: valid | punct | protobuf,
Comma: valid | punct | protobuf | cel,
Dot: valid | punct | protobuf | cel,
Colon: valid | punct | protobuf | cel,
Newline: valid | punct,
At: valid | punct | protobuf,
Hash: valid | punct | protobuf,
Dollar: valid | punct | protobuf,
Twiddle: valid | punct | protobuf,
Add: valid | punct | cel,
Sub: valid | punct | cel,
Mul: valid | punct | cel,
Div: valid | punct | protobuf | cel,
Rem: valid | punct | cel,
Amp: valid | punct,
Pipe: valid | punct,
Xor: valid | punct,
Shl: valid | punct,
Shr: valid | punct,
Bang: valid | punct | cel,
Bangs: valid | punct,
Ask: valid | punct | cel,
Asks: valid | punct,
Amps: valid | punct | cel,
Pipes: valid | punct | cel,
Assign: valid | punct | protobuf,
AssignNew: valid | punct,
AssignAdd: valid | punct,
AssignSub: valid | punct,
AssignMul: valid | punct,
AssignDiv: valid | punct,
AssignRem: valid | punct,
AssignAmp: valid | punct,
AssignPipe: valid | punct,
AssignXor: valid | punct,
AssignShl: valid | punct,
AssignShr: valid | punct,
Range: valid | punct,
RangeEq: valid | punct,
LParen: valid | punct | protobuf | cel,
RParen: valid | punct | protobuf | cel,
LBracket: valid | punct | protobuf | cel,
RBracket: valid | punct | protobuf | cel,
LBrace: valid | punct | protobuf | cel,
RBrace: valid | punct | protobuf | cel,
Comment: valid | punct | protobuf | cel,
LComment: valid | punct | protobuf | cel,
RComment: valid | punct | protobuf | cel,
Lt: valid | punct | protobuf | cel,
Gt: valid | punct | protobuf | cel,
Le: valid | punct,
Ge: valid | punct,
Eq: valid | punct,
Ne: valid | punct,
Parens: valid | punct | brackets | protobuf | cel,
Brackets: valid | punct | brackets | protobuf | cel,
Braces: valid | punct | brackets | protobuf | cel,
Angles: valid | punct | brackets | protobuf,
BlockComment: valid | punct | brackets | protobuf | cel,
}
var braces = [...][3]Keyword{
LParen: {LParen, RParen, Parens},
RParen: {LParen, RParen, Parens},
Parens: {LParen, RParen, Parens},
LBracket: {LBracket, RBracket, Brackets},
RBracket: {LBracket, RBracket, Brackets},
Brackets: {LBracket, RBracket, Brackets},
LBrace: {LBrace, RBrace, Braces},
RBrace: {LBrace, RBrace, Braces},
Braces: {LBrace, RBrace, Braces},
Lt: {Lt, Gt, Angles},
Gt: {Lt, Gt, Angles},
Angles: {Lt, Gt, Angles},
LComment: {LComment, RComment, BlockComment},
RComment: {LComment, RComment, BlockComment},
BlockComment: {LComment, RComment, BlockComment},
}
// 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 kind.yaml. DO NOT EDIT.
package token
import (
"fmt"
"iter"
)
// Kind identifies what kind of token a particular [Token] is.
type Kind byte
const (
Unrecognized Kind = iota // Unrecognized garbage in the input file.
Space // Non-comment contiguous whitespace.
Comment // A single comment.
Ident // An identifier, which may be a soft keyword.
String // A string token. May be a non-leaf for non-contiguous quoted strings.
Number // A run of digits that is some kind of number.
Keyword // A hard keyword. May be a non-leaf for delimiters like {}.
)
// String implements [fmt.Stringer].
func (v Kind) String() string {
if int(v) < 0 || int(v) > len(_table_Kind_String) {
return fmt.Sprintf("Kind(%v)", int(v))
}
return _table_Kind_String[v]
}
// GoString implements [fmt.GoStringer].
func (v Kind) GoString() string {
if int(v) < 0 || int(v) > len(_table_Kind_GoString) {
return fmt.Sprintf("token.Kind(%v)", int(v))
}
return _table_Kind_GoString[v]
}
var _table_Kind_String = [...]string{
Unrecognized: "Unrecognized",
Space: "Space",
Comment: "Comment",
Ident: "Ident",
String: "String",
Number: "Number",
Keyword: "Keyword",
}
var _table_Kind_GoString = [...]string{
Unrecognized: "token.Unrecognized",
Space: "token.Space",
Comment: "token.Comment",
Ident: "token.Ident",
String: "token.String",
Number: "token.Number",
Keyword: "token.Keyword",
}
var _ iter.Seq[int] // Mark iter as used.
// 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 token
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
)
// GetMeta returns the metadata value associated with this token. This function
// cannot be called outside of protocompile.
//
// Note: this function wants to be a method of [Token], but cannot because it
// is generic.
func GetMeta[M tokenmeta.Meta](token Token) *M {
stream := token.Context()
if meta, ok := stream.meta[token.ID()].(*M); ok {
return meta
}
return nil
}
// MutateMeta is like [GetMeta], but it first initializes the meta value.
//
// Panics if the given token is zero, or if the token is natural and the stream
// is frozen.
//
// Note: this function wants to be a method of [Token], but cannot because it
// is generic.
func MutateMeta[M tokenmeta.Meta](token Token) *M {
if token.IsZero() {
panic(fmt.Sprintf("protocompile/token: passed zero token to MutateMeta: %s", token))
}
stream := token.Context()
if token.nat() != nil && stream.frozen {
panic("protocompile/token: attempted to mutate frozen stream")
}
if stream.meta == nil {
stream.meta = make(map[ID]any)
}
meta, _ := stream.meta[token.ID()].(*M)
if meta == nil {
meta = new(M)
stream.meta[token.ID()] = meta
}
return meta
}
// ClearMeta clears the associated literal value of a token.
//
// Panics if the given token is zero, or if the token is natural and the stream
// is frozen.
//
// Note: this function wants to be a method of [Token], but cannot because it
// is generic.
func ClearMeta[M tokenmeta.Meta](token Token) {
if token.IsZero() {
panic(fmt.Sprintf("protocompile/token: passed zero token to ClearMeta: %s", token))
}
stream := token.Context()
if token.nat() != nil && stream.frozen {
panic("protocompile/token: attempted to mutate frozen stream")
}
meta, _ := stream.meta[token.ID()].(*M)
if meta != nil {
delete(stream.meta, token.ID())
}
}
// 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 token
import (
"math"
"math/big"
"strconv"
"strings"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"github.com/bufbuild/protocompile/experimental/source"
)
// NumberToken provides access to detailed information about a [Number].
type NumberToken id.Node[NumberToken, *Stream, *tokenmeta.Number]
// Token returns the wrapped token value.
func (n NumberToken) Token() Token {
return id.Wrap(n.Context(), ID(n.ID()))
}
// Base returns this number's base.
func (n NumberToken) Base() byte {
if n.Raw() == nil {
return 10
}
base := n.Raw().Base
if base == 0 {
return 10
}
return base
}
// IsLegacyOctal returns whether this is a C-style octal literal, such as 0777,
// as opposed to a modern octal literal like 0o777.
func (n NumberToken) IsLegacyOctal() bool {
if n.Base() != 8 {
return false
}
text := n.Token().Text()
return !strings.HasPrefix(text, "0o") && !strings.HasPrefix(text, "0O")
}
// ExpBase returns this number's exponent base, if this number has an exponent;
// returns 1 if it has no exponent.
func (n NumberToken) ExpBase() int {
if n.Raw() == nil {
return 1
}
return max(1, int(n.Raw().ExpBase))
}
// Prefix returns this number's base prefix (e.g. 0x).
func (n NumberToken) Prefix() source.Span {
if n.Raw() == nil || n.Raw().Prefix == 0 {
return source.Span{}
}
span := n.Token().Span()
span.End = span.Start + int(n.Raw().Prefix)
return span
}
// Suffix returns an arbitrary suffix attached to this number (the suffix will
// have no whitespace before the end of the digits).
func (n NumberToken) Suffix() source.Span {
if n.Raw() == nil || n.Raw().Prefix == 0 {
return source.Span{}
}
span := n.Token().Span()
span.Start = span.End - int(n.Raw().Suffix)
return span
}
// Mantissa returns the mantissa digits for this literal, i.e., everything
// between the prefix and the (possibly empty) exponent.
//
// For example, for 0x123.456p-789, this will be 123.456.
func (n NumberToken) Mantissa() source.Span {
span := n.Token().Span()
if n.Raw() == nil {
return span
}
start := int(n.Raw().Prefix)
end := span.Len() - int(n.Raw().Suffix) - int(n.Raw().Exp)
return span.Range(start, end)
}
// Exponent returns the exponent digits for this literal, i.e., everything
// after the exponent letter. Returns the zero span if there is no exponent.
//
// For example, for 0x123.456p-789, this will be -789.
func (n NumberToken) Exponent() source.Span {
if n.Raw() == nil || n.Raw().Exp == 0 {
return source.Span{}
}
span := n.Token().Span()
end := span.Len() - int(n.Raw().Suffix)
start := end - int(n.Raw().Exp) + 1 // Skip the exponent letter.
return span.Range(start, end)
}
// IsFloat returns whether this token can only be used as a float literal (even
// if it has integer value).
func (n NumberToken) IsFloat() bool {
return n.Raw() != nil && n.Raw().IsFloat
}
// HasSeparators returns whether this token contains thousands separator
// runes.
func (n NumberToken) HasSeparators() bool {
return n.Raw() != nil && n.Raw().ThousandsSep
}
// IsValid returns whether this token was able to parse properly at all.
func (n NumberToken) IsValid() bool {
return n.Raw() == nil || !n.Raw().SyntaxError
}
// Int converts this value into a 64-bit unsigned integer.
//
// Returns whether the conversion was exact.
func (n NumberToken) Int() (v uint64, exact bool) {
if n.Raw() == nil {
// This is a decimal integer, so we just parse on the fly.
v, err := strconv.ParseUint(n.Token().Text(), 10, 64)
return v, err == nil
}
switch {
case n.Raw().Big != nil:
v, acc := n.Raw().Big.Uint64()
return v, acc == big.Exact && n.Raw().Big.IsInt()
case n.Raw().IsFloat:
f := math.Float64frombits(n.Raw().Word)
n := uint64(f)
return n, f == float64(n)
default:
return n.Raw().Word, true
}
}
// Float converts this value into a 64-bit float.
//
// Returns whether the conversion was exact.
func (n NumberToken) Float() (v float64, exact bool) {
if n.Raw() == nil {
// This is a decimal integer, so we just parse on the fly.
v, err := strconv.ParseUint(n.Token().Text(), 10, 64)
return float64(v), err == nil && uint64(float64(v)) == v
}
switch {
case n.Raw().Big != nil:
v, acc := n.Raw().Big.Float64()
return v, acc == big.Exact
case n.Raw().IsFloat:
f := math.Float64frombits(n.Raw().Word)
return f, true
default:
v := n.Raw().Word
return float64(v), uint64(float64(v)) == v
}
}
// Value returns the underlying arbitrary-precision numeric value.
func (n NumberToken) Value() *big.Float {
if n.Raw() == nil {
// This is a decimal integer, so we just parse on the fly.
v, _ := strconv.ParseUint(n.Token().Text(), 10, 64)
return new(big.Float).SetUint64(v)
}
switch {
case n.Raw().Big != nil:
return n.Raw().Big
case n.Raw().IsFloat:
f := math.Float64frombits(n.Raw().Word)
return new(big.Float).SetFloat64(f)
default:
v := n.Raw().Word
return new(big.Float).SetUint64(v)
}
}
// 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 token
import (
"fmt"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// Implementation notes:
//
// Let n := int(id). If n is zero, it is the nil token. If n is positive, it is
// a natural token, whose index is n - 1. If it is negative, it is a
// synthetic token, whose index is ^n.
// ID is the raw ID of a [Token] separated from its [Context].
//
// The zero value is reserved as a nil representation. All other values are
// opaque.
type ID = id.ID[Token]
// naturalIndex returns the index of this token in the natural stream.
func naturalIndex(t ID) int {
if t.IsZero() {
panic("protocompile/token: called naturalIndex on zero token")
}
if t < 0 {
panic("protocompile/token: called naturalIndex on synthetic token")
}
// Need to subtract off one, because the zeroth
// ID is used as a "missing" sentinel.
return int(t) - 1
}
// syntheticIndex returns the index of this token in the synthetic stream.
func syntheticIndex(t ID) int {
if t.IsZero() {
panic("protocompile/token: called syntheticIndex on zero token")
}
if t > 0 {
panic("protocompile/token: called syntheticIndex on natural token")
}
// Need to invert the bits, because synthetic tokens are
// stored as negative numbers.
return ^int(t)
}
// Constants for extracting the parts of tokenImpl.kindAndOffset.
const (
kindMask = 0b00_0_111
isTreeMask = 0b00_1_000
treeKwMask = 0b11_0_000
offsetShift = 6
keywordShift = 4
)
// nat is the data of a token stored in a [Context].
type nat struct {
// We store the end of the token, and the start is implicitly
// given by the end of the previous token. We use the end, rather
// than the start, it makes adding tokens one by one to the stream
// easier, because once the token is pushed, its start and end are
// set correctly, and don't depend on the next token being pushed.
end uint32
// This contains compressed metadata about a token in the following format.
// (Bit ranges [a:b] are exclusive like Go slice syntax.)
//
// 1. Bits [0:3] is the Kind.
// 2. Bit [3] is whether this is a non-leaf.
// a. If it is a non-leaf, bits [4:6] determine the Keyword value if
// Kind is Punct, and bits [6:32] are a signed offset to the matching
// open/close.
// b. If it is a leaf, bits [4:12] are a Keyword value.
//
// TODO: One potential optimization for the tree representation is to use
// fewer bits for kind, since in practice, it is only ever Punct or String.
// We do not currently make this optimization because it seems that the
// current 32 million maximum size for separating two tokens is probably
// sufficient, for now.
metadata int32
}
// Kind extracts the token's kind, which is stored.
func (t nat) Kind() Kind {
return Kind(t.metadata & kindMask)
}
// WithKind returns a copy with the given kind.
func (t nat) WithKind(k Kind) nat {
t.metadata &^= kindMask
t.metadata |= int32(k)
return t
}
// WithKeyword returns a copy with the given keyword.
func (t nat) WithKeyword(kw keyword.Keyword) nat {
if !t.IsLeaf() {
return t
}
t.metadata &^= 0xff << keywordShift
t.metadata |= int32(kw) << keywordShift
return t
}
// Offset returns the offset from this token to its matching open/close, if any.
func (t nat) Offset() int {
if t.metadata&isTreeMask == 0 {
return 0
}
return int(t.metadata >> offsetShift)
}
// Keyword returns the keyword for this token, if it is an identifier.
func (t nat) Keyword() keyword.Keyword {
if !slicesx.Among(t.Kind(), Ident, Keyword, Comment) {
return keyword.Unknown
}
if t.IsLeaf() {
return keyword.Keyword(t.metadata >> keywordShift)
}
if t.Kind() != Keyword {
return keyword.Unknown
}
return keyword.Parens + keyword.Keyword((t.metadata&treeKwMask)>>keywordShift)
}
// IsLeaf checks whether this is a leaf token.
func (t nat) IsLeaf() bool {
return t.Offset() == 0
}
// IsOpen checks whether this is a open token with a matching closer.
func (t nat) IsOpen() bool {
return t.Offset() > 0
}
// IsClose checks whether this is a closer token with a matching opener.
func (t nat) IsClose() bool {
return t.Offset() < 0
}
func (t nat) GoString() string {
type nat struct {
End int
Kind Kind
IsLeaf bool
Keyword keyword.Keyword
Offset int
}
return fmt.Sprintf("%#v", nat{int(t.end), t.Kind(), t.IsLeaf(), t.Keyword(), t.Offset()})
}
// Fuse marks a pair of tokens as their respective open and close.
//
// If open or close are synthetic or not currently a leaf, this function panics.
//
//nolint:predeclared,revive // For close.
func fuseImpl(diff int32, open, close *nat) {
if diff <= 0 {
panic("protocompile/token: called Fuse() with out-of-order")
}
compressKw := func(kw keyword.Keyword) int32 {
_, _, fused := kw.Brackets()
v := int32(fused - keyword.Parens)
if v >= 0 && v < 4 {
return v << keywordShift
}
return 0
}
open.metadata = diff<<offsetShift | compressKw(open.Keyword()) | isTreeMask | int32(open.Kind())
close.metadata = -diff<<offsetShift | compressKw(close.Keyword()) | isTreeMask | int32(close.Kind())
}
// synth is the data of a synth token stored in a [Context].
type synth struct {
text string
kind Kind
// Non-zero if this token has a matching other end. Whether this is
// the opener or the closer is determined by whether children is
// nil: it is nil for the closer.
otherEnd ID
children []ID
}
// Keyword returns the keyword for this token, if it is an identifier.
func (t synth) Keyword() keyword.Keyword {
if !slicesx.Among(t.kind, Ident, Keyword, Comment) {
return keyword.Unknown
}
kw := keyword.Lookup(t.text)
if !t.IsLeaf() {
_, _, kw = kw.Brackets()
}
return kw
}
// IsLeaf checks whether this is a leaf token.
func (t synth) IsLeaf() bool {
return t.otherEnd == 0
}
// IsOpen checks whether this is a open token with a matching closer.
func (t synth) IsOpen() bool {
return !t.IsLeaf() && t.children != nil
}
// IsClose checks whether this is a closer token with a matching opener.
func (t synth) IsClose() bool {
return !t.IsLeaf() && t.children == nil
}
// 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 token
import (
"cmp"
"fmt"
"iter"
"math"
"slices"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token/keyword"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// Stream is a token stream.
//
// Internally, Stream uses a compressed representation for storing tokens, and
// is not precisely a [][Token]. In particular, it supports the creation of
// "synthetic" tokens, described in detail in this package's documentation.
//
// Streams may be "frozen", meaning that whatever lexing operation it was
// meant for is complete, and new tokens cannot be pushed to it. This is used
// by the Protocompile lexer to prevent re-use of a stream for multiple files.
type Stream struct {
_ unsafex.NoCopy
// The file this stream is over.
*source.File
// Storage for tokens.
nats []nat
synths []synth
// This contains materialized literals for some tokens.
//
// Not all literal tokens will have an entry here; only those that have
// uncommon representations, such as hex literals, floats, and strings with
// escapes/implicit concatenation.
//
// This means the lexer can deal with the complex literal parsing logic on
// our behalf in general, but common cases are re-parsed on-demand.
// Specifically, the most common literals (decimal integers and simple
// quoted strings) do not generate entries in this map and thus do not
// contribute at-rest memory usage.
meta map[ID]any
// If true, no further mutations (except for synthetic tokens) are
// permitted.
frozen bool
}
var _ id.Context = (*Stream)(nil)
// FromID implements [id.Context].
func (s *Stream) FromID(id uint64, want any) any {
switch want.(type) {
case *rawToken:
return rawToken{}
case **tokenmeta.String:
meta, _ := s.meta[ID(id)].(*tokenmeta.String)
return meta
case **tokenmeta.Number:
meta, _ := s.meta[ID(id)].(*tokenmeta.Number)
return meta
default:
panic(fmt.Sprintf("called FromID with unknown type %T", want))
}
}
// All returns an iterator over all tokens in this stream. First the natural
// tokens in order of creation, and then the synthetic tokens in the same.
func (s *Stream) All() iter.Seq[Token] {
return func(yield func(Token) bool) {
for i := range s.nats {
if !yield(id.Wrap(s, ID(i+1))) {
return
}
}
for i := range s.synths {
if !yield(id.Wrap(s, ID(^i))) {
return
}
}
}
}
// Around returns the tokens around the given offset. It has the following
// potential return values:
//
// 1. offset == 0, returns [Zero], first token.
// 2. offset == len(File.Text()), returns last token, [Zero].
// 3. offset is the end of a token. Returns the tokens ending and starting
// at offset, respectively.
// 4. offset is inside of a token tok. Returns tok, tok.
func (s *Stream) Around(offset int) (Token, Token) {
if offset == 0 {
return Zero, id.Wrap(s, ID(1))
}
if offset == len(s.File.Text()) {
return id.Wrap(s, ID(len(s.nats))), Zero
}
idx, exact := slices.BinarySearchFunc(s.nats, offset, func(n nat, offset int) int {
return cmp.Compare(int(n.end), offset)
})
if exact {
// We landed between two tokens. idx+1 is the ID of the token that ends
// at offset.
return id.Wrap(s, ID(idx+1)), id.Wrap(s, ID(idx+2))
}
// We landed in the middle of a token, specifically idx+1.
return id.Wrap(s, ID(idx+1)), id.Wrap(s, ID(idx+1))
}
// Cursor returns a cursor over the natural token stream.
func (s *Stream) Cursor() *Cursor {
return &Cursor{context: s}
}
// AssertEmpty asserts that no natural tokens have been created in this stream
// yet. It panics if they already have.
func (s *Stream) AssertEmpty() {
if len(s.nats) > 0 {
panic("protocompile/token: expected an empty token stream for " + s.Path())
}
}
// Freeze marks this stream as frozen. This means that all mutation operations
// except for creation of synthetic tokens will panic.
//
// Freezing cannot be checked for or undone; callers must assume any token
// stream they did not create has already been frozen.
func (s *Stream) Freeze() {
if s != nil {
s.frozen = true
}
}
// Push mints the next token referring to a piece of the input source.
//
// Panics if this stream is frozen.
func (s *Stream) Push(length int, kind Kind) Token {
return s.PushKeyword(length, kind, keyword.Unknown)
}
// Push mints the next token referring to a piece of the input source, marking
// it with the given keyword.
//
// Panics if this stream is frozen.
func (s *Stream) PushKeyword(length int, kind Kind, kw keyword.Keyword) Token {
if s.frozen {
panic("protocompile/token: attempted to mutate frozen stream")
}
if length < 0 || length > math.MaxInt32 {
panic(fmt.Sprintf("protocompile/token: Push() called with invalid length: %d", length))
}
var prevEnd int
if len(s.nats) != 0 {
prevEnd = int(s.nats[len(s.nats)-1].end)
}
end := prevEnd + length
if end > len(s.Text()) {
panic(fmt.Sprintf("protocompile/token: Push() overflowed backing text: %d > %d", end, len(s.Text())))
}
s.nats = append(s.nats, nat{
end: uint32(prevEnd + length),
metadata: (int32(kind) & kindMask) | (int32(kw) << keywordShift),
})
return id.Wrap(s, ID(len(s.nats)))
}
// NewIdent mints a new synthetic identifier token with the given name.
func (s *Stream) NewIdent(name string) Token {
return s.newSynth(synth{
text: name,
kind: Ident,
})
}
// NewPunct mints a new synthetic punctuation token with the given text.
func (s *Stream) NewPunct(text string) Token {
return s.newSynth(synth{
text: text,
kind: Keyword,
})
}
// NewString mints a new synthetic string containing the given text.
func (s *Stream) NewString(text string) Token {
return s.newSynth(synth{
text: text,
kind: String,
})
}
// NewFused mints a new synthetic open/close pair using the given tokens.
//
// Panics if either open or close is natural or non-leaf.
func (s *Stream) NewFused(openTok, closeTok Token, children ...Token) {
if !openTok.IsSynthetic() || !closeTok.IsSynthetic() {
panic("protocompile/token: called NewOpenClose() with natural delimiters")
}
if !openTok.IsLeaf() || !closeTok.IsLeaf() {
panic("protocompile/token: called PushCloseToken() with non-leaf as a delimiter token")
}
synth := openTok.synth()
synth.otherEnd = closeTok.ID()
synth.children = make([]ID, len(children))
for i, t := range children {
synth.children[i] = t.ID()
}
closeTok.synth().otherEnd = openTok.ID()
}
func (s *Stream) newSynth(tok synth) Token {
raw := ID(^len(s.synths))
s.synths = append(s.synths, tok)
return id.Wrap(s, raw)
}
// 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 token
import (
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/internal/tokenmeta"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
)
// StringToken provides access to detailed information about a [String].
type StringToken id.Node[StringToken, *Stream, *tokenmeta.String]
// Escape is an escape inside of a [StringToken]. See [StringToken.Escapes].
type Escape struct {
source.Span
// If Rune is zero, this escape represents a raw byte rather than a
// Unicode character.
Rune rune
Byte byte
}
// Token returns the wrapped token value.
func (s StringToken) Token() Token {
return id.Wrap(s.Context(), ID(s.ID()))
}
// Text returns the post-processed contents of this string.
func (s StringToken) Text() string {
if s.Raw() != nil && s.Raw().Text != "" {
return s.Raw().Text
}
return s.RawContent().Text()
}
// HasEscapes returns whether the string had escapes which were processed.
func (s StringToken) HasEscapes() bool {
return s.Raw() != nil && s.Raw().Escapes != nil
}
// Escapes returns the escapes that contribute to the value of this string.
func (s StringToken) Escapes() seq.Indexer[Escape] {
var spans []tokenmeta.Escape
if s.Raw() != nil {
spans = s.Raw().Escapes
}
return seq.NewFixedSlice(spans, func(_ int, esc tokenmeta.Escape) Escape {
return Escape{
Span: s.Token().Context().Span(int(esc.Start), int(esc.End)),
Rune: esc.Rune,
Byte: esc.Byte,
}
})
}
// IsConcatenated returns whether the string was built from
// implicitly-concatenated strings.
func (s StringToken) IsConcatenated() bool {
return s.Raw() != nil && s.Raw().Concatenated
}
// IsPure returns whether the string required post-processing (escaping or
// concatenation) after lexing.
func (s StringToken) IsPure() bool {
return s.Raw() == nil || !(s.Raw().Escapes != nil || s.Raw().Concatenated)
}
// Prefix returns an arbitrary prefix attached to this string (the prefix will
// have no whitespace before the open quote).
func (s StringToken) Prefix() source.Span {
if s.Raw() == nil {
return source.Span{}
}
span := s.Token().LeafSpan()
span.End = span.Start + int(s.Raw().Prefix)
return span
}
// Quotes returns the opening and closing delimiters for this string literal,
// not including the sigil.
//
//nolint:revive,predeclared
func (s StringToken) Quotes() (open, close source.Span) {
if s.IsZero() {
return source.Span{}, source.Span{}
}
open = s.Token().LeafSpan()
close = open
if s.Raw() == nil {
if open.Len() < 2 {
// Deal with the really degenerate case of a single quote.
close.Start = close.End
return open, close
}
// Assume that the quotes are a single byte wide if we don't have any
// metadata.
open.End = open.Start + 1
close.Start = close.End - 1
return open, close
}
open.Start += int(s.Raw().Prefix)
close.Start += int(s.Raw().Prefix)
quote := int(max(1, s.Raw().Quote)) // 1 byte quotes if not set explicitly.
// Unterminated?
switch {
case open.Len() < quote:
close.Start = close.End
case open.Len() < 2*quote:
open.End = open.Start + quote
close.Start = open.End
default:
open.End = open.Start + quote
close.Start = close.End - quote
}
return open, close
}
// RawContent returns the unprocessed contents of the string.
func (s StringToken) RawContent() source.Span {
open, close := s.Quotes() //nolint:revive,predeclared
open.Start = open.End
open.End = close.Start
return open
}
// 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 token
import (
"fmt"
"math/big"
"strings"
"unicode"
"github.com/bufbuild/protocompile/experimental/id"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/token/keyword"
)
// Set to true to enable verbose debug printing of tokens.
const debug = true
// IsSkippable returns whether this is a token that should be examined during
// syntactic analysis.
func (t Kind) IsSkippable() bool {
// Note: kind.go is a generated file.
return t == Space || t == Comment || t == Unrecognized
}
// Zero is the zero [Token].
var Zero Token
// Value is a constraint that represents a literal scalar value in source.
//
// This union does not include bool because they are lexed as
// identifiers and then later converted to boolean values based on their
// context (since "true" and "false" are also valid identifiers for named
// types).
type Value interface {
uint64 | float64 | *big.Int | string
}
// Token is a lexical element of a Protobuf file.
//
// Protocompile's token stream is actually a tree of tokens. Some tokens, called
// non-leaf tokens, contain a selection of tokens "within" them. For example, the
// two matched braces of a message body are a single token, and all of the tokens
// between the braces are contained inside it. This moves certain complexity into
// the lexer in a way that allows us to handle matching delimiters generically.
//
// The zero value of Token is the so-called "zero token", which is used to denote the
// absence of a token.
type Token id.Node[Token, *Stream, rawToken]
type rawToken struct{}
// IsLeaf returns whether this is a non-zero leaf token.
func (t Token) IsLeaf() bool {
if t.IsZero() {
return false
}
if impl := t.nat(); impl != nil {
return impl.IsLeaf()
}
return t.synth().IsLeaf()
}
// IsSynthetic returns whether this is a non-zero synthetic token (i.e., a token that didn't
// come from a parsing operation.)
func (t Token) IsSynthetic() bool {
return t.ID() < 0
}
// Kind returns what kind of token this is.
//
// Returns [Unrecognized] if this token is zero.
func (t Token) Kind() Kind {
if t.IsZero() {
return Unrecognized
}
if impl := t.nat(); impl != nil {
return impl.Kind()
}
return t.synth().kind
}
// Keyword returns a [keyword.Keyword] indicating that this token has special
// meaning in the grammar.
//
// Both a [Keyword] and an [Ident] may produce keyword values; the former
// are called hard keywords; the latter soft keywords. Soft keywords are meant
// to be useable as identifiers by a parser unless they happen to be in the
// right place for their keyword value to matter; hard keywords are rejected
// when used as identifiers.
func (t Token) Keyword() keyword.Keyword {
switch {
case t.IsZero():
return keyword.Unknown
case t.IsSynthetic():
return t.synth().Keyword()
default:
return t.nat().Keyword()
}
}
// Text returns the text fragment referred to by this token. This does not
// return the text contained inside of non-leaf tokens; if this token refers to
// a token tree, this will return only the text of the open (or close) token.
//
// For example, for a matched pair of braces, this will only return the text of
// the open brace, "{".
//
// Returns empty string for the zero token.
func (t Token) Text() string {
if t.IsZero() {
return ""
}
if synth := t.synth(); synth != nil {
if synth.kind == String {
// If this is a string, we need to add quotes and escape it.
// This can be done on-demand.
var escaped strings.Builder
escaped.WriteRune('"')
for _, r := range synth.text {
switch {
case r == '\n':
escaped.WriteString("\\n")
case r == '\r':
escaped.WriteString("\\r")
case r == '\t':
escaped.WriteString("\\t")
case r == '\a':
escaped.WriteString("\\a")
case r == '\b':
escaped.WriteString("\\b")
case r == '\f':
escaped.WriteString("\\f")
case r == '\v':
escaped.WriteString("\\v")
case r == 0:
escaped.WriteString("\\0")
case r == '"':
escaped.WriteString("\\\"")
case r == '\\':
escaped.WriteString("\\\\")
case r < ' ' || r == '\x7f':
fmt.Fprintf(&escaped, "\\x%02x", r)
case unicode.IsGraphic(r):
escaped.WriteRune(r)
case r < 0x10000:
fmt.Fprintf(&escaped, "\\u%04x", r)
default:
fmt.Fprintf(&escaped, "\\U%08x", r)
}
}
escaped.WriteRune('"')
return escaped.String()
}
return synth.text
}
start, end := t.offsets()
return t.Context().Text()[start:end]
}
// SetKind overwrites the kind of this token.
//
// Panics if the token's stream is frozen.
func (t Token) SetKind(k Kind) {
if t.Context().frozen {
panic("protocompile/token: attempted to mutate frozen stream")
}
if raw := t.nat(); raw != nil {
*raw = raw.WithKind(k)
} else {
t.synth().kind = k
}
}
// Span implements [Spanner].
func (t Token) Span() source.Span {
if t.IsZero() || t.IsSynthetic() {
return source.Span{}
}
var a, b int
if !t.IsLeaf() {
start, end := t.StartEnd()
a, _ = start.offsets()
_, b = end.offsets()
} else {
a, b = t.offsets()
}
return t.Context().Span(a, b)
}
// LeafSpan returns the span that this token would have if it was a leaf token.
func (t Token) LeafSpan() source.Span {
if t.IsZero() || t.IsSynthetic() {
return source.Span{}
}
return t.Context().Span(t.offsets())
}
// StartEnd returns the open and close tokens for this token.
//
// If this is a leaf token, start and end will be the same token and will compare as equal.
//
// Panics if this is a zero token.
func (t Token) StartEnd() (start, end Token) {
if t.IsZero() {
return Zero, Zero
}
switch impl := t.nat(); {
case impl == nil:
switch synth := t.synth(); {
case synth.IsLeaf():
return t, t
case synth.IsOpen():
start = t
end = id.Wrap(t.Context(), synth.otherEnd)
case synth.IsClose():
start = id.Wrap(t.Context(), synth.otherEnd)
end = t
}
case impl.IsLeaf():
return t, t
case impl.IsOpen():
start = t
end = id.Wrap(t.Context(), t.ID()+ID(impl.Offset()))
case impl.IsClose():
start = id.Wrap(t.Context(), t.ID()+ID(impl.Offset()))
end = t
}
return
}
// Next returns the next token in this token's stream.
//
// Panics if this is not a natural token.
func (t Token) Next() Token {
c := NewCursorAt(t)
_ = c.Next()
return c.Next()
}
// Prev returns the previous token in this token's stream.
//
// Panics if this is not a natural token.
func (t Token) Prev() Token {
c := NewCursorAt(t)
return c.Prev()
}
// Fuse marks a pair of tokens as their respective open and close.
//
// If open or close are synthetic or not currently a leaf, have different
// contexts, or are part of a frozen [Stream], this function panics.
func Fuse(open, close Token) { //nolint:predeclared,revive // For close.
if open.Context() != close.Context() {
panic("protocompile/token: attempted to fuse tokens from different streams")
}
if open.Context().frozen {
panic("protocompile/token: attempted to mutate frozen stream")
}
impl1 := open.nat()
if impl1 == nil {
panic("protocompile/token: called FuseTokens() with a synthetic open token")
}
if !impl1.IsLeaf() {
panic("protocompile/token: called FuseTokens() with non-leaf as the open token")
}
impl2 := close.nat()
if impl2 == nil {
panic("protocompile/token: called FuseTokens() with a synthetic close token")
}
if !impl2.IsLeaf() {
panic("protocompile/token: called FuseTokens() with non-leaf as the close token")
}
fuseImpl(int32(close.ID()-open.ID()), impl1, impl2)
}
// Children returns a Cursor over the children of this token.
//
// If the token is zero or is a leaf token, returns nil.
func (t Token) Children() *Cursor {
if t.IsZero() || t.IsLeaf() {
return nil
}
if impl := t.nat(); impl != nil {
start, _ := t.StartEnd()
return &Cursor{
context: t.Context(),
idx: naturalIndex(start.ID()) + 1, // Skip the start!
}
}
synth := t.synth()
if synth.IsClose() {
return id.Wrap(t.Context(), synth.otherEnd).Children()
}
return NewSliceCursor(t.Context(), synth.children)
}
// SyntheticChildren returns a cursor over the given subslice of the children
// of this token.
//
// Panics if t is not synthetic.
func (t Token) SyntheticChildren(i, j int) *Cursor {
synth := t.synth()
if synth == nil {
panic("protocompile/token: called SyntheticChildren() on non-synthetic token")
}
if synth.IsClose() {
return id.Wrap(t.Context(), synth.otherEnd).SyntheticChildren(i, j)
}
return NewSliceCursor(t.Context(), synth.children[i:j])
}
// Name converts this token into its corresponding identifier name, potentially
// performing normalization.
//
// Currently, we perform no normalization, so this is the same value as Text(), but
// that may change in the future.
//
// Returns "" for non-identifiers.
func (t Token) Name() string {
if t.Kind() != Ident {
return ""
}
return t.Text()
}
// AsNumber returns number information for this token.
func (t Token) AsNumber() NumberToken {
if t.Kind() != Number {
return NumberToken{}
}
return id.Wrap(t.Context(), id.ID[NumberToken](t.ID()))
}
// AsString returns string information for this token.
func (t Token) AsString() StringToken {
if t.Kind() != String {
return StringToken{}
}
return id.Wrap(t.Context(), id.ID[StringToken](t.ID()))
}
// String implements [strings.Stringer].
func (t Token) String() string {
if debug && !t.IsZero() {
if t.IsSynthetic() {
return fmt.Sprintf("{%v %#v}", t.ID(), t.synth())
}
return fmt.Sprintf("{%v %#v}", t.ID(), t.nat())
}
return fmt.Sprintf("{%v %v}", t.ID(), t.Kind())
}
// offsets returns the byte offsets of this token within the file it came from.
//
// The return value for synthetic tokens is unspecified.
//
// Note that this DOES NOT include any child tokens!
func (t Token) offsets() (start, end int) {
if t.IsSynthetic() {
return
}
end = int(t.nat().end)
// If this is the first token, the start is implicitly zero.
if t.ID() == 1 {
return 0, end
}
prev := id.Wrap(t.Context(), t.ID()-1)
return int(prev.nat().end), end
}
func (t Token) nat() *nat {
if t.IsSynthetic() {
return nil
}
return &t.Context().nats[naturalIndex(t.ID())]
}
func (t Token) synth() *synth {
if !t.IsSynthetic() {
return nil
}
return &t.Context().synths[syntheticIndex(t.ID())]
}
// Copyright 2024 Google LLC
//
// 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 protocompile
import (
"bytes"
"context"
"io"
)
func FuzzProtoCompile(data []byte) int {
compiler := &Compiler{
Resolver: &SourceResolver{
Accessor: func(_ string) (closer io.ReadCloser, e error) {
return io.NopCloser(bytes.NewReader(data)), nil
},
},
}
_, err := compiler.Compile(context.Background(), "test.proto")
if err != nil {
return 0
}
return 1
}
// 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 arena defines an [Arena] type with compressed pointers.
//
// The benefits of using compressed pointers are as follows:
//
// 1. Pointers are only four bytes wide and four-byte aligned, saving on space
// in pointer-heavy graph data structures.
//
// 2. The GC has to do substantially less work on such graph data structures,
// because from its perspective, structures that only contain compressed
// pointers are not deeply-nested and require less traversal (remember,
// the bane of a GC is something that looks like a linked list).
//
// 3. Improved cache locality. All values inside of the same arena are likelier
// to be near each other.
package arena
import (
"fmt"
"iter"
"math/bits"
"strings"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
// pointersMinLenShift is the log2 of the size of the smallest slice in
// a pointers[T].
const (
pointersMinLenShift = 4
pointersMinLen = 1 << pointersMinLenShift
)
// An untyped arena pointer.
//
// The pointer value of a particular pointer in an arena is equal to one
// plus the number of elements allocated before it.
type Untyped uint32
// Nil returns a nil arena pointer.
func Nil() Untyped {
return 0
}
// Nil returns whether this pointer is nil.
func (p Untyped) Nil() bool {
return p == 0
}
// String implements [fmt.Stringer].
func (p Untyped) String() string {
if p.Nil() {
return "<nil>"
}
return fmt.Sprintf("0x%x", uint32(p))
}
// A compressed arena pointer.
//
// Cannot be dereferenced directly; see [Pointer.In].
//
// The zero value is nil.
type Pointer[T any] Untyped
// Nil returns whether this pointer is nil.
func (p Pointer[T]) Nil() bool {
return Untyped(p).Nil()
}
// Untyped erases this pointer's type.
//
// This function mostly exists for the aid of tab-completion.
func (p Pointer[T]) Untyped() Untyped {
return Untyped(p)
}
// String implements [fmt.Stringer].
func (p Pointer[T]) String() string {
return p.Untyped().String()
}
// Arena is an arena that offers compressed pointers. Conceptually, it is a slice
// of T that guarantees the Ts will never be moved.
//
// It does this by maintaining a table of logarithmically-growing slices that
// mimic the resizing behavior of an ordinary slice. This trades off the linear
// 8-byte overhead of []*T for a logarithmic 24-byte overhead. Lookup time
// remains O(1), at the cost of two pointer loads instead of one.
//
// It also does not discard already-allocated memory, reducing the amount of
// garbage it produces over time compared to a plain []T used as an allocation
// pool.
//
// A zero Arena[T] is empty and ready to use.
type Arena[T any] struct {
// Invariants:
// 1. cap(table[0]) == 1<<pointersMinLenShift.
// 2. cap(table[n]) == 2*cap(table[n-1]).
// 3. cap(table[n]) == len(table[n]) for n < len(table)-1.
//
// These invariants are needed for lookup to be O(1).
table [][]T
}
// New allocates a new value on the arena.
func (a *Arena[T]) New(value T) *T {
if a.table == nil {
a.table = [][]T{make([]T, 0, pointersMinLen)}
}
last := &a.table[len(a.table)-1]
if len(*last) == cap(*last) {
// If the last slice is full, grow by doubling the size
// of the next slice.
a.table = append(a.table, make([]T, 0, 2*cap(*last)))
last = &a.table[len(a.table)-1]
}
*last = append(*last, value)
return &(*last)[len(*last)-1]
}
// NewCompressed allocates a new value on the arena, returning the result of
// compressing the pointer.
func (a *Arena[T]) NewCompressed(value T) Pointer[T] {
_ = a.New(value)
return Pointer[T](a.len()) // Note that len, not len-1, is intentional.
}
// Compress returns a compressed pointer into this arena if ptr belongs to it;
// otherwise, returns nil.
func (a *Arena[T]) Compress(ptr *T) Pointer[T] {
if ptr == nil {
return 0
}
// Check the slices in reverse order: no matter the state of the arena,
// the majority of the allocated values will be in either the last or
// second-to-last slice.
for i := len(a.table) - 1; i >= 0; i-- {
idx := slicesx.PointerIndex(a.table[i], ptr)
if idx != -1 {
return Pointer[T](a.lenOfFirstNSlices(i) + idx + 1)
}
}
return 0
}
// Deref looks up a pointer in this arena.
//
// This arena must be the one tha allocated this pointer, otherwise this will
// either return an arbitrary pointer or panic.
//
// If p is nil, returns nil.
func (a *Arena[T]) Deref(ptr Pointer[T]) *T {
if ptr.Nil() {
return nil
}
slice, idx := a.coordinates(int(ptr) - 1)
return &a.table[slice][idx]
}
// Values returns an iterator that yields each value allocated on this arena.
//
// Values freshly allocated with [Arena.New] may or may not be yielded during
// iteration.
func (a *Arena[T]) Values() iter.Seq[*T] {
return func(yield func(*T) bool) {
for _, chunk := range a.table {
for i := range chunk {
if !yield(&chunk[i]) {
return
}
}
}
}
}
func (a *Arena[T]) len() int {
if len(a.table) == 0 {
return 0
}
// Only the last slice will be not-fully-filled.
return a.lenOfFirstNSlices(len(a.table)-1) + len(a.table[len(a.table)-1])
}
// String implements [strings.Stringer].
func (a Arena[T]) String() string {
var b strings.Builder
b.WriteRune('[')
// Don't use p.Iter, we want to subtly show off the boundaries of the
// subarrays.
for i, slice := range a.table {
if i != 0 {
b.WriteRune('|')
}
for i, v := range slice {
if i != 0 {
b.WriteRune(' ')
}
fmt.Fprint(&b, v)
}
}
b.WriteRune(']')
return b.String()
}
// lenOfNthSlice returns the length of the nth slice, even if it isn't
// allocated yet.
func (*Arena[T]) lenOfNthSlice(n int) int {
return pointersMinLen << n
}
// lenOfFirstNSlices returns the length of the first n slices.
func (a *Arena[T]) lenOfFirstNSlices(n int) int {
// Note the following identity:
//
// 2^m + 2^(m+1) + ... + 2^n = 2^(n+1) - 2^m
//
// This tells us that the sum of p.lenOfNthSlice(m) from 0 to n-1 (the first
// n slices) is
return max(0, a.lenOfNthSlice(n)-a.lenOfNthSlice(0))
}
// coordinates calculates the coordinates of the given index in table. It
// also performs a bounds check.
func (a *Arena[T]) coordinates(idx int) (int, int) {
if idx >= a.len() || idx < 0 {
panic(fmt.Sprintf("arena: pointer out of range: %#x", idx))
}
// Given pointersMinLenShift == n, the cumulative starting index of each slice is
//
// 0b0 << n, 0b1 << n, 0b11 << n, 0b111 << n
//
// Thus, to find which slice an index corresponds to, we add 0b1 << n (pointersMinLen).
// Because << distributes over addition, we get
//
// 0b1 << n, 0b10 << n, 0b100 << n, 0b1000 << n
//
// Taking the one-indexed high order bit, which maps this sequence to
//
// 1+n, 2+n, 3+n, 4+n
//
// We can subtract off n+1 to obtain the actual slice index:
//
// 0, 1, 2, 3
slice := bits.UintSize - bits.LeadingZeros(uint(idx)+pointersMinLen)
slice -= pointersMinLenShift + 1
// Then, the offset within table[slice] is given by subtracting off the
// length of all prior slices from idx.
idx -= a.lenOfFirstNSlices(slice)
return slice, idx
}
// 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 cases provides functions for inter-converting between different
// case styles.
package cases
import (
"iter"
"strings"
"unicode"
)
// Case is a target case style to convert to.
type Case int
const (
Snake Case = iota // snake_case
Enum // ENUM_CASE
Camel // camelCase
Pascal // PascalCase
)
// Convert converts str to the given case.
func (c Case) Convert(str string) string {
return Converter{Case: c}.Convert(str)
}
// Converter contains specific options for converting to a given case.
type Converter struct {
Case Case
// If set, word boundaries are only underscores, which is the naive
// word splitting algorithm used by protoc.
NaiveSplit bool
// If set, runes will not be converted to lowercase as part of the
// conversion.
NoLowercase bool
}
// Convert convert str according to the options set in this converter.
func (c Converter) Convert(str string) string {
buf := new(strings.Builder)
c.Append(buf, str)
return buf.String()
}
// Append is like [Converter.Convert], but it appends to the given buffer
// instead.
func (c Converter) Append(buf *strings.Builder, str string) {
var iter iter.Seq[string]
if c.NaiveSplit {
iter = strings.SplitSeq(str, "_")
} else {
iter = Words(str)
}
c.Case.convert(buf, !c.NoLowercase, iter)
}
func (c Case) convert(buf *strings.Builder, lowercase bool, words iter.Seq[string]) {
switch c {
case Snake, Enum:
uppercase := c == Enum
first := true
for word := range words {
if !first {
buf.WriteRune('_')
}
for _, r := range word {
if uppercase || lowercase {
buf.WriteRune(setCase(r, uppercase))
}
}
first = false
}
case Camel, Pascal:
uppercase := c == Pascal
firstWord := true
for word := range words {
firstRune := true
for _, r := range word {
uppercase := (uppercase || !firstWord) && firstRune
if uppercase || lowercase {
r = setCase(r, uppercase)
}
buf.WriteRune(r)
firstRune = false
}
firstWord = false
}
}
}
func setCase(r rune, upper bool) rune {
if upper {
return unicode.ToUpper(r)
}
return unicode.ToLower(r)
}
// 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 cases
import (
"iter"
"unicode"
"unicode/utf8"
)
// Words breaks up s into words according to the algorithm specified at
// https://docs.rs/heck/latest/heck/#definition-of-a-word-boundary.
func Words(str string) iter.Seq[string] {
return func(yield func(string) bool) {
input := str // Not yet yielded.
var prev rune
first := true
for str != "" {
next, n := utf8.DecodeRuneInString(str)
str = str[n:]
switch {
case !unicode.IsLetter(next) && !unicode.IsDigit(next):
// This is punctuation. Split the string around next and
// yield the result if it's nonempty.
word := input[:len(input)-len(str)-n]
input = input[len(input)-len(str):]
if word != "" && !yield(word) {
return
}
case unicode.IsUpper(prev) && unicode.IsLower(next):
// If the previous rune is uppercase and the next is lowercase,
// we want to insert a boundary before prev.
idx := len(input) - len(str) - n - utf8.RuneLen(prev)
word := input[:idx]
input = input[idx:]
if word != "" && !yield(word) {
return
}
case str == "":
if first { // Single-rune string.
yield(input)
return
}
// This is the last rune, which gets special handling. We want
// FooBAR and FooBar to become foo_bar but FooX to become foo_x.
// Hence, if next is uppercase and prev is not, then we insert a
// boundary between them.
if !unicode.IsUpper(prev) && unicode.IsUpper(next) {
idx := len(input) - len(str) - n
word := input[:idx]
input = input[idx:]
if word != "" && !yield(word) {
return
}
}
yield(input)
return
}
prev = next
first = false
}
}
}
// 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 editions contains helpers related to resolving features for
// Protobuf editions. These are lower-level helpers. Higher-level helpers
// (which use this package under the hood) can be found in the exported
// protoutil package.
package editions
import (
"fmt"
"strings"
"sync"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
)
const (
// MinSupportedEdition is the earliest edition supported by this module.
// It should be 2023 (the first edition) for the indefinite future.
MinSupportedEdition = descriptorpb.Edition_EDITION_2023
// MaxSupportedEdition is the most recent edition supported by this module.
MaxSupportedEdition = descriptorpb.Edition_EDITION_2023
// MaxKnownEdition is the most recent edition known by this module.
MaxKnownEdition = descriptorpb.Edition_EDITION_2024
)
var (
// SupportedEditions is the exhaustive set of editions that protocompile
// can support. We don't allow it to compile future/unknown editions, to
// make sure we don't generate incorrect descriptors, in the event that
// a future edition introduces a change or new feature that requires
// new logic in the compiler.
SupportedEditions = computeEditionsRange(MinSupportedEdition, MaxSupportedEdition)
// KnownEditions is the exhaustive set of editions that protocompile
// knowns. All known editions may not be supported, but all supported
// editions are known.
KnownEditions = computeEditionsRange(MinSupportedEdition, MaxKnownEdition)
// FeatureSetDescriptor is the message descriptor for the compiled-in
// version (in the descriptorpb package) of the google.protobuf.FeatureSet
// message type.
FeatureSetDescriptor = (*descriptorpb.FeatureSet)(nil).ProtoReflect().Descriptor()
// FeatureSetType is the message type for the compiled-in version (in
// the descriptorpb package) of google.protobuf.FeatureSet.
FeatureSetType = (*descriptorpb.FeatureSet)(nil).ProtoReflect().Type()
editionDefaults map[descriptorpb.Edition]*descriptorpb.FeatureSet
editionDefaultsInit sync.Once
)
// HasFeatures is implemented by all options messages and provides a
// nil-receiver-safe way of accessing the features explicitly configured
// in those options.
type HasFeatures interface {
GetFeatures() *descriptorpb.FeatureSet
}
var _ HasFeatures = (*descriptorpb.FileOptions)(nil)
var _ HasFeatures = (*descriptorpb.MessageOptions)(nil)
var _ HasFeatures = (*descriptorpb.FieldOptions)(nil)
var _ HasFeatures = (*descriptorpb.OneofOptions)(nil)
var _ HasFeatures = (*descriptorpb.ExtensionRangeOptions)(nil)
var _ HasFeatures = (*descriptorpb.EnumOptions)(nil)
var _ HasFeatures = (*descriptorpb.EnumValueOptions)(nil)
var _ HasFeatures = (*descriptorpb.ServiceOptions)(nil)
var _ HasFeatures = (*descriptorpb.MethodOptions)(nil)
// ResolveFeature resolves a feature for the given descriptor. This simple
// helper examines the given element and its ancestors, searching for an
// override. If there is no overridden value, it returns a zero value.
func ResolveFeature(
element protoreflect.Descriptor,
fields ...protoreflect.FieldDescriptor,
) (protoreflect.Value, error) {
for {
var features *descriptorpb.FeatureSet
if withFeatures, ok := element.Options().(HasFeatures); ok {
// It should not really be possible for 'ok' to ever be false...
features = withFeatures.GetFeatures()
}
// TODO: adaptFeatureSet is only looking at the first field. But if we needed to
// support an extension field inside a custom feature, we'd really need
// to check all fields. That gets particularly complicated if the traversal
// path of fields includes list and map values. Luckily, features are not
// supposed to be repeated and not supposed to themselves have extensions.
// So this should be fine, at least for now.
msgRef, err := adaptFeatureSet(features, fields[0])
if err != nil {
return protoreflect.Value{}, err
}
// Navigate the fields to find the value
var val protoreflect.Value
for i, field := range fields {
if i > 0 {
msgRef = val.Message()
}
if !msgRef.Has(field) {
val = protoreflect.Value{}
break
}
val = msgRef.Get(field)
}
if val.IsValid() {
// All fields were set!
return val, nil
}
parent := element.Parent()
if parent == nil {
// We've reached the end of the inheritance chain.
return protoreflect.Value{}, nil
}
element = parent
}
}
// HasEdition should be implemented by values that implement
// [protoreflect.FileDescriptor], to provide access to the file's
// edition when its syntax is [protoreflect.Editions].
type HasEdition interface {
// Edition returns the numeric value of a google.protobuf.Edition enum
// value that corresponds to the edition of this file. If the file does
// not use editions, it should return the enum value that corresponds
// to the syntax level, EDITION_PROTO2 or EDITION_PROTO3.
Edition() int32
}
// GetEdition returns the edition for a given element. It returns
// EDITION_PROTO2 or EDITION_PROTO3 if the element is in a file that
// uses proto2 or proto3 syntax, respectively. It returns EDITION_UNKNOWN
// if the syntax of the given element is not recognized or if the edition
// cannot be ascertained from the element's [protoreflect.FileDescriptor].
func GetEdition(d protoreflect.Descriptor) descriptorpb.Edition {
switch d.ParentFile().Syntax() {
case protoreflect.Proto2:
return descriptorpb.Edition_EDITION_PROTO2
case protoreflect.Proto3:
return descriptorpb.Edition_EDITION_PROTO3
case protoreflect.Editions:
withEdition, ok := d.ParentFile().(HasEdition)
if !ok {
// The parent file should always be a *result, so we should
// never be able to actually get in here. If we somehow did
// have another implementation of protoreflect.FileDescriptor,
// it doesn't provide a way to get the edition, other than the
// potentially expensive step of generating a FileDescriptorProto
// and then querying for the edition from that. :/
return descriptorpb.Edition_EDITION_UNKNOWN
}
return descriptorpb.Edition(withEdition.Edition())
default:
return descriptorpb.Edition_EDITION_UNKNOWN
}
}
// GetEditionDefaults returns the default feature values for the given edition.
// It returns nil if the given edition is not known.
//
// This only populates known features, those that are fields of [*descriptorpb.FeatureSet].
// It does not populate any extension fields.
//
// The returned value must not be mutated as it references shared package state.
func GetEditionDefaults(edition descriptorpb.Edition) *descriptorpb.FeatureSet {
editionDefaultsInit.Do(func() {
editionDefaults = make(map[descriptorpb.Edition]*descriptorpb.FeatureSet, len(descriptorpb.Edition_name))
// Compute default for all known editions in descriptorpb.
for editionInt := range descriptorpb.Edition_name {
edition := descriptorpb.Edition(editionInt)
defaults := &descriptorpb.FeatureSet{}
defaultsRef := defaults.ProtoReflect()
fields := defaultsRef.Descriptor().Fields()
// Note: we are not computing defaults for extensions. Those are not needed
// by anything in the compiler, so we can get away with just computing
// defaults for these static, non-extension fields.
for i, length := 0, fields.Len(); i < length; i++ {
field := fields.Get(i)
val, err := GetFeatureDefault(edition, FeatureSetType, field)
if err != nil {
// should we fail somehow??
continue
}
defaultsRef.Set(field, val)
}
editionDefaults[edition] = defaults
}
})
return editionDefaults[edition]
}
// GetFeatureDefault computes the default value for a feature. The given container
// is the message type that contains the field. This should usually be the descriptor
// for google.protobuf.FeatureSet, but can be a different message for computing the
// default value of custom features.
//
// Note that this always re-computes the default. For known fields of FeatureSet,
// it is more efficient to query from the statically computed default messages,
// like so:
//
// editions.GetEditionDefaults(edition).ProtoReflect().Get(feature)
func GetFeatureDefault(edition descriptorpb.Edition, container protoreflect.MessageType, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
opts, ok := feature.Options().(*descriptorpb.FieldOptions)
if !ok {
// this is most likely impossible except for contrived use cases...
return protoreflect.Value{}, fmt.Errorf("options is %T instead of *descriptorpb.FieldOptions", feature.Options())
}
maxEdition := descriptorpb.Edition(-1)
var maxVal string
for _, def := range opts.EditionDefaults {
if def.GetEdition() <= edition && def.GetEdition() > maxEdition {
maxEdition = def.GetEdition()
maxVal = def.GetValue()
}
}
if maxEdition == -1 {
// no matching default found
return protoreflect.Value{}, fmt.Errorf("no relevant default for edition %s", edition)
}
// We use a typed nil so that it won't fall back to the global registry. Features
// should not use extensions or google.protobuf.Any, so a nil *Types is fine.
unmarshaler := prototext.UnmarshalOptions{Resolver: (*protoregistry.Types)(nil)}
// The string value is in the text format: either a field value literal or a
// message literal. (Repeated and map features aren't supported, so there's no
// array or map literal syntax to worry about.)
if feature.Kind() == protoreflect.MessageKind || feature.Kind() == protoreflect.GroupKind {
fldVal := container.Zero().NewField(feature)
err := unmarshaler.Unmarshal([]byte(maxVal), fldVal.Message().Interface())
if err != nil {
return protoreflect.Value{}, err
}
return fldVal, nil
}
// The value is the textformat for the field. But prototext doesn't provide a way
// to unmarshal a single field value. To work around, we unmarshal into an enclosing
// message, which means we must prefix the value with the field name.
if feature.IsExtension() {
maxVal = fmt.Sprintf("[%s]: %s", feature.FullName(), maxVal)
} else {
maxVal = fmt.Sprintf("%s: %s", feature.Name(), maxVal)
}
empty := container.New()
err := unmarshaler.Unmarshal([]byte(maxVal), empty.Interface())
if err != nil {
return protoreflect.Value{}, err
}
return empty.Get(feature), nil
}
func adaptFeatureSet(msg *descriptorpb.FeatureSet, field protoreflect.FieldDescriptor) (protoreflect.Message, error) {
msgRef := msg.ProtoReflect()
var actualField protoreflect.FieldDescriptor
switch {
case field.IsExtension():
// Extensions can be used directly with the feature set, even if
// field.ContainingMessage() != FeatureSetDescriptor. But only if
// the value is either not a message or is a message with the
// right descriptor, i.e. val.Descriptor() == field.Message().
if actualField = actualDescriptor(msgRef, field); actualField == nil || actualField == field {
if msgRef.Has(field) || len(msgRef.GetUnknown()) == 0 {
return msgRef, nil
}
// The field is not present, but the message has unrecognized values. So
// let's try to parse the unrecognized bytes, just in case they contain
// this extension.
temp := &descriptorpb.FeatureSet{}
unmarshaler := proto.UnmarshalOptions{
AllowPartial: true,
Resolver: resolverForExtension{field},
}
if err := unmarshaler.Unmarshal(msgRef.GetUnknown(), temp); err != nil {
return nil, fmt.Errorf("failed to parse unrecognized fields of FeatureSet: %w", err)
}
return temp.ProtoReflect(), nil
}
case field.ContainingMessage() == FeatureSetDescriptor:
// Known field, not dynamically generated. Can directly use with the feature set.
return msgRef, nil
default:
actualField = FeatureSetDescriptor.Fields().ByNumber(field.Number())
}
// If we get here, we have a dynamic field descriptor or an extension
// descriptor whose message type does not match the descriptor of the
// stored value. We need to copy its value into a dynamic message,
// which requires marshalling/unmarshalling.
// We only need to copy over the unrecognized bytes (if any)
// and the same field (if present).
data := msgRef.GetUnknown()
if actualField != nil && msgRef.Has(actualField) {
subset := &descriptorpb.FeatureSet{}
subset.ProtoReflect().Set(actualField, msgRef.Get(actualField))
var err error
data, err = proto.MarshalOptions{AllowPartial: true}.MarshalAppend(data, subset)
if err != nil {
return nil, fmt.Errorf("failed to marshal FeatureSet field %s to bytes: %w", field.Name(), err)
}
}
if len(data) == 0 {
// No relevant data to copy over, so we can just return
// a zero value message
return dynamicpb.NewMessageType(field.ContainingMessage()).Zero(), nil
}
other := dynamicpb.NewMessage(field.ContainingMessage())
// We don't need to use a resolver for this step because we know that
// field is not an extension. And features are not allowed to themselves
// have extensions.
if err := (proto.UnmarshalOptions{AllowPartial: true}).Unmarshal(data, other); err != nil {
return nil, fmt.Errorf("failed to marshal FeatureSet field %s to bytes: %w", field.Name(), err)
}
return other, nil
}
type resolverForExtension struct {
ext protoreflect.ExtensionDescriptor
}
func (r resolverForExtension) FindMessageByName(_ protoreflect.FullName) (protoreflect.MessageType, error) {
return nil, protoregistry.NotFound
}
func (r resolverForExtension) FindMessageByURL(_ string) (protoreflect.MessageType, error) {
return nil, protoregistry.NotFound
}
func (r resolverForExtension) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
if field == r.ext.FullName() {
return asExtensionType(r.ext), nil
}
return nil, protoregistry.NotFound
}
func (r resolverForExtension) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
if message == r.ext.ContainingMessage().FullName() && field == r.ext.Number() {
return asExtensionType(r.ext), nil
}
return nil, protoregistry.NotFound
}
func asExtensionType(ext protoreflect.ExtensionDescriptor) protoreflect.ExtensionType {
if xtd, ok := ext.(protoreflect.ExtensionTypeDescriptor); ok {
return xtd.Type()
}
return dynamicpb.NewExtensionType(ext)
}
func computeEditionsRange(minEdition, maxEdition descriptorpb.Edition) map[string]descriptorpb.Edition { //nolint:unparam // minEdition is a parameter that may change
supportedEditions := map[string]descriptorpb.Edition{}
for editionNum := range descriptorpb.Edition_name {
edition := descriptorpb.Edition(editionNum)
if edition >= minEdition && edition <= maxEdition {
name := strings.TrimPrefix(edition.String(), "EDITION_")
supportedEditions[name] = edition
}
}
return supportedEditions
}
// actualDescriptor returns the actual field descriptor referenced by msg that
// corresponds to the given ext (i.e. same number). It returns nil if msg has
// no reference, if the actual descriptor is the same as ext, or if ext is
// otherwise safe to use as is.
func actualDescriptor(msg protoreflect.Message, ext protoreflect.ExtensionDescriptor) protoreflect.FieldDescriptor {
if !msg.Has(ext) || ext.Message() == nil {
// nothing to match; safe as is
return nil
}
val := msg.Get(ext)
switch {
case ext.IsMap(): // should not actually be possible
expectedDescriptor := ext.MapValue().Message()
if expectedDescriptor == nil {
return nil // nothing to match
}
// We know msg.Has(field) is true, from above, so there's at least one entry.
var matches bool
val.Map().Range(func(_ protoreflect.MapKey, val protoreflect.Value) bool {
matches = val.Message().Descriptor() == expectedDescriptor
return false
})
if matches {
return nil
}
case ext.IsList():
// We know msg.Has(field) is true, from above, so there's at least one entry.
if val.List().Get(0).Message().Descriptor() == ext.Message() {
return nil
}
case !ext.IsMap():
if val.Message().Descriptor() == ext.Message() {
return nil
}
}
// The underlying message descriptors do not match. So we need to return
// the actual field descriptor. Sadly, protoreflect.Message provides no way
// to query the field descriptor in a message by number. For non-extensions,
// one can query the associated message descriptor. But for extensions, we
// have to do the slow thing, and range through all fields looking for it.
var actualField protoreflect.FieldDescriptor
msg.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
if fd.Number() == ext.Number() {
actualField = fd
return false
}
return true
})
return actualField
}
// 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 bitsx contains extensions to Go's package math/bits.
package bitsx
import (
"math"
"math/bits"
)
// IsPowerOfTwo returns whether n is a power of 2.
func IsPowerOfTwo(n uint) bool {
// See https://github.com/mcy/best/blob/2d94f6b23aecddc46f792edb4c45800aa58074ca/best/math/bit.h#L147
return bits.OnesCount(n) == 1
}
// NextPowerOfTwo returns the next power of 2 after n, or zero if n is greater
// than the largest power of 2.
func NextPowerOfTwo(n uint) uint {
// For n == 0, LeadingZeros returns 64, and Go does not mask the shift
// amount, so -1 >> 64 is zero, which produces the correct answer.
//
// If LeadingZeros produces 0 (i.e., the highest bit is set, so it's
// larger than the largest power of 2) this addition will overflow back to
// 0 as desired.
return uint(math.MaxUint)>>uint(bits.LeadingZeros(n)) + 1
}
// MakePowerOfTwo snaps n to a power of 2: i.e., if it isn't already one,
// replaces it with the next power of two.
func MakePowerOfTwo(n uint) uint {
if IsPowerOfTwo(n) {
return n
}
return NextPowerOfTwo(n)
}
// 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 cmpx contains extensions to Go's package cmp.
package cmpx
import (
"cmp"
"fmt"
"math"
"reflect"
)
// Result is the type returned by an [Ordering], and in particular
// [cmp.Compare].
type Result = int
const (
// [cmp.Compare] guarantees these return values.
Less Result = -1
Equal Result = 0
Greater Result = 1
)
// Ordered is like [cmp.Ordered], but includes additional types.
type Ordered interface {
~bool | cmp.Ordered
}
// Ordering is an ordering for the type T, which is any function with the same
// signature as [Compare].
type Ordering[T any] func(T, T) Result
// Key returns an ordering for T according to a key function, which must return
// a [cmp.Ordered] value.
func Key[T any, U cmp.Ordered](key func(T) U) Ordering[T] {
return func(a, b T) Result { return cmp.Compare(key(a), key(b)) }
}
// Join returns an ordering for T which returns the first of cmps returns a
// non-[Equal] value.
func Join[T any](cmps ...Ordering[T]) Ordering[T] {
return func(a, b T) Result {
for _, cmp := range cmps {
if n := cmp(a, b); n != Equal {
return n
}
}
return Equal
}
}
// Map is like [Join], but it maps the inputs with the given function first.
func Map[T any, U any](f func(T) U, cmps ...Ordering[U]) Ordering[T] {
return func(x, y T) Result {
a, b := f(x), f(y)
for _, cmp := range cmps {
if n := cmp(a, b); n != Equal {
return n
}
}
return Equal
}
}
// Reverse returns an ordering which is the reverse of cmp.
func Reverse[T any](cmp Ordering[T]) Ordering[T] {
return func(a, b T) Result { return -cmp(a, b) }
}
// Bool compares two bools, where false < true.
//
// This works around a bug where bool does not satisfy [cmp.Ordered].
func Bool[B ~bool](a, b B) Result {
var ai, bi byte
if a {
ai = 1
}
if b {
bi = 1
}
return cmp.Compare(ai, bi)
}
// Any compares any two [cmp.Ordered] types, according to the following criteria:
//
// 1. any(nil) is least of all.
//
// 2. If the values are not mutually comparable, their [reflect.Kind]s are
// compared.
//
// 3. If either value is not of a [cmp.Ordered] type, this function panics.
//
// 4. Otherwise, the arguments are compared as-if by [cmp.Compare].
//
// For the purposes of this function, bool is treated as satisfying [cmp.Compare].
func Any(a, b any) Result {
if a == nil || b == nil {
return Bool(a != nil, b != nil)
}
ra := reflect.ValueOf(a)
rb := reflect.ValueOf(b)
type kind int
const (
kBool kind = 1 << iota
kInt
kUint
kFloat
kString
)
which := func(r reflect.Value) kind {
switch r.Kind() {
case reflect.Bool:
return kBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return kInt
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
return kUint
case reflect.Float32, reflect.Float64:
return kFloat
case reflect.String:
return kString
default:
panic(fmt.Sprintf("cmpx.Any: incomparable value %v (type %[1]T)", r.Interface()))
}
}
//nolint:revive // Recommends removing some else {} branches that make the code less symmetric
switch which(ra) | which(rb) {
case kBool:
return Bool(ra.Bool(), rb.Bool())
case kInt:
return cmp.Compare(ra.Int(), rb.Int())
case kUint:
return cmp.Compare(ra.Uint(), rb.Uint())
case kInt | kUint:
if rb.CanUint() {
v := rb.Uint()
if v > math.MaxInt64 {
return Less
}
return cmp.Compare(ra.Int(), int64(v))
} else {
v := ra.Uint()
if v > math.MaxInt64 {
return Greater
}
return cmp.Compare(int64(v), rb.Int())
}
case kFloat:
return cmp.Compare(ra.Float(), rb.Float())
case kFloat | kInt:
if ra.CanFloat() {
return cmp.Compare(ra.Float(), float64(rb.Int()))
} else {
return cmp.Compare(float64(ra.Int()), rb.Float())
}
case kFloat | kUint:
if ra.CanFloat() {
return cmp.Compare(ra.Float(), float64(rb.Uint()))
} else {
return cmp.Compare(float64(ra.Uint()), rb.Float())
}
case kString:
return cmp.Compare(ra.String(), rb.String())
default:
return cmp.Compare(ra.Kind(), rb.Kind())
}
}
// 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 cmpx
import (
"unsafe"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// MapWrapper is wrapper over a map[K]V that is identity comparable, so it can
// be used as a map key. Note that Go maps are implemented as a single pointer
// to an opaque value.
//
// This is the same comparison as comparing maps using reflect.Value.UnsafePointer.
type MapWrapper[K comparable, V any] struct {
// NOTE: This type has the same layout as a map[K]V.
_ [0]*map[K]V
p unsafe.Pointer
}
// WrapMap returns a [MapWrapper] that wraps m.
func NewMapWrapper[K comparable, V any](m map[K]V) MapWrapper[K, V] {
return unsafex.Bitcast[MapWrapper[K, V]](m)
}
// Nil returns whether this wraps the nil map.
func (m MapWrapper[K, V]) Nil() bool {
return m.p == nil
}
// Get returns the wrapped map.
func (m MapWrapper[K, V]) Get() map[K]V {
return unsafex.Bitcast[map[K]V](m)
}
// 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 iterx contains extensions to Go's package iter.
package iterx
import (
"fmt"
"iter"
"strings"
)
// Count counts the number of elements in seq.
func Count[T any](seq iter.Seq[T]) int {
var total int
for range seq {
total++
}
return total
}
// Count2 counts the number of elements in seq.
func Count2[T, U any](seq iter.Seq2[T, U]) int {
var total int
for range seq {
total++
}
return total
}
// 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
for i, v := range Enumerate(seq) {
if i > 0 {
out.WriteString(sep)
}
fmt.Fprint(&out, v)
}
return out.String()
}
// Every returns whether every element of an iterator satisfies the given
// predicate. Returns true if seq yields no values.
func Every[T any](seq iter.Seq[T], p func(T) bool) bool {
for v := range seq {
if !p(v) {
return false
}
}
return true
}
// Exhaust runs an iterator to completion for its side-effects.
//
// This mostly exists because there is a noisy lint that incorrectly thinks all
// range loops are side-effect free, and because gofmt won't format
// "for range iter {}" on one line.
func Exhaust[T any](seq iter.Seq[T]) {
//nolint:revive // Empty block has side-effects.
for range seq {
}
}
// 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 iterx
import (
"iter"
)
// This file contains the matrix of {Map, Filter, FilterMap} x {1, 2, 1to2, 2to1},
// except that Filter1to2 and Filter2to1 don't really make sense.
// 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 })
}
// FlatMap is like [Map], but expects the yielded type to itself be an iterator.
//
// Returns a new iterator over the concatenation of the iterators yielded by f.
func FlatMap[T, U any](seq iter.Seq[T], f func(T) iter.Seq[U]) iter.Seq[U] {
return func(yield func(U) bool) {
for x := range seq {
for y := range f(x) {
if !yield(y) {
break
}
}
}
}
}
// 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) {
for v := range seq {
if v2, ok := f(v); ok && !yield(v2) {
return
}
}
}
}
// Map2 returns a new iterator applying f to each element of seq.
func Map2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W)) iter.Seq2[V, W] {
return FilterMap2(seq, func(v1 T, v2 U) (V, W, bool) {
x1, x2 := f(v1, v2)
return x1, x2, true
})
}
// Filter2 returns a new iterator that only includes values satisfying p.
func Filter2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) iter.Seq2[T, U] {
return FilterMap2(seq, func(v1 T, v2 U) (T, U, bool) { return v1, v2, p(v1, v2) })
}
// FilterMap2 combines the operations of [Map] and [Filter].
func FilterMap2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W, bool)) iter.Seq2[V, W] {
return func(yield func(V, W) bool) {
seq(func(v1 T, v2 U) bool {
x1, x2, ok := f(v1, v2)
return !ok || yield(x1, x2)
})
}
}
// Map2to1 is like [Map], but it also acts a Y pipe for converting a two-element
// iterator into a one-element iterator.
func Map2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) V) iter.Seq[V] {
return FilterMap2to1(seq, func(v1 T, v2 U) (V, bool) {
return f(v1, v2), true
})
}
// FilterMap2to1 is like [FilterMap], but it also acts a Y pipe for converting
// a two-element iterator into a one-element iterator.
func FilterMap2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) (V, bool)) iter.Seq[V] {
return func(yield func(V) bool) {
seq(func(v1 T, v2 U) bool {
v, ok := f(v1, v2)
return !ok || yield(v)
})
}
}
// Map1To2 is like [Map], 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)
})
}
}
// 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 iterx
import (
"iter"
)
// First retrieves the first element of an iterator.
func First[T any](seq iter.Seq[T]) (v T, ok bool) {
for v = range seq {
ok = true
break
}
return v, ok
}
// First retrieves the first element a two-element iterator.
func First2[K, V any](seq iter.Seq2[K, V]) (k K, v V, ok bool) {
for k, v = range seq {
ok = true
break
}
return k, v, ok
}
// Last retrieves the last element of an iterator.
func Last[T any](seq iter.Seq[T]) (v T, ok bool) {
for v = range seq {
ok = true
}
return v, ok
}
// Last retrieves the last element of a two-element iterator.
func Last2[K, V any](seq iter.Seq2[K, V]) (k K, v V, ok bool) {
for k, v = range seq {
ok = true
}
return k, v, ok
}
// OnlyOne retrieves the only element of an iterator.
func OnlyOne[T any](seq iter.Seq[T]) (v T, ok bool) {
for i, x := range Enumerate(seq) {
if i > 0 {
var z T
// Ensure we return the zero value if there is more
// than one element.
return z, false
}
v = x
ok = true
}
return v, ok
}
// Find returns the first element that matches a predicate.
//
// Returns the value and the index at which it was found, or -1 if it wasn't
// found.
func Find[T any](seq iter.Seq[T], p func(T) bool) (int, T) {
for i, x := range Enumerate(seq) {
if p(x) {
return i, x
}
}
var z T
return -1, z
}
// Find2 is like [Find] but for two-element iterators.
func Find2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) (int, T, U) {
var i int
for x1, x2 := range seq {
if p(x1, x2) {
return i, x1, x2
}
i++
}
var z1 T
var z2 U
return -1, z1, z2
}
// Index returns the index of the first element of seq that satisfies p.
//
// if not found, returns -1.
func Index[T any](seq iter.Seq[T], p func(T) bool) int {
idx, _ := Find(seq, p)
return idx
}
// Index2 is like [Index], but for two-element iterators.
func Index2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) int {
idx, _, _ := Find2(seq, p)
return idx
}
// Contains whether an element exists that satisfies p.
func Contains[T any](seq iter.Seq[T], p func(T) bool) bool {
idx, _ := Find(seq, p)
return idx != -1
}
// Contains2 is like [Contains], but for two-element iterators.
func Contains2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) bool {
idx, _, _ := Find2(seq, p)
return idx != -1
}
// 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 iterx contains extensions to Go's package iter.
package iterx
import (
"fmt"
"iter"
"slices"
)
// Empty returns whether an iterator yields any elements.
func Empty[T any](seq iter.Seq[T]) bool {
for range seq {
return false
}
return true
}
// Empty2 returns whether an iterator yields any elements.
func Empty2[T, U any](seq iter.Seq2[T, U]) bool {
for range seq {
return false
}
return true
}
// Take returns an iterator over the first n elements of a sequence.
func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if n == 0 || !yield(v) {
return
}
n--
}
}
}
// 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
})
}
// 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)
})
}
// 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) {
for _, seq := range seqs {
for v := range seq {
if !yield(v) {
return
}
}
}
}
}
// Of returns an iterator that yields the given values.
func Of[T any](v ...T) iter.Seq[T] {
return slices.Values(v)
}
// 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 iterx
import (
"iter"
)
// Left returns a new iterator that drops the right value of a [iter.Seq2].
func Left[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] {
return Map2to1(seq, func(k K, _ V) K { return k })
}
// Right returns a new iterator that drops the left value of a [iter.Seq2].
func Right[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] {
return Map2to1(seq, func(_ K, v V) V { return v })
}
// 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 "iter"
// Set constructs a set-like map from the given elements.
func Set[K comparable](elems ...K) map[K]struct{} {
s := make(map[K]struct{}, len(elems))
for _, elem := range elems {
s[elem] = struct{}{}
}
return s
}
// CollectSet is like [maps.Collect], but it implicitly fills in each map value
// with a struct{} value.
func CollectSet[K comparable](seq iter.Seq[K]) map[K]struct{} {
return InsertKeys(make(map[K]struct{}), seq)
}
// InsertKeys is like [maps.Insert], but it implicitly fills in each map value
// with the zero value.
func InsertKeys[M ~map[K]V, K comparable, V any](m M, seq iter.Seq[K]) M {
for k := range seq {
var zero V
m[k] = zero
}
return m
}
// 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
// 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))
// 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
}
// Contains is a shorthand for _, ok := m[k] that allows it to be used in
// expression position.
func Contains[M ~map[K]V, K comparable, V any](m M, k K) bool {
_, ok := m[k]
return ok
}
// Add inserts k into the map if it is not present. Returns whether insertion
// occurred, and the value that k maps to in the map.
func Add[M ~map[K]V, K comparable, V any](m M, k K, v V) (mapped V, inserted bool) {
if v, ok := m[k]; ok {
return v, false
}
m[k] = v
return v, true
}
// AddZero inserts k into the map if it is not present, using the zero value of
// V as the value. Returns whether insertion occurred.
func AddZero[M ~map[K]V, K comparable, V any](m M, k K) (inserted bool) {
var z V
_, inserted = Add(m, k, z)
return inserted
}
// Append appends the given value to the slice in the entry for the given key.
func Append[M ~map[K][]V, K comparable, V any](m M, k K, v V) {
m[k] = append(m[k], v)
}
// 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
// Dedup replaces runs of consecutive equal elements with a single element.
func Dedup[S ~[]E, E comparable](s S) S {
return DedupKey(s, func(e E) E { return e }, func(e []E) E { return e[0] })
}
// DedupKey deduplicates consecutive elements in a slice, using key to obtain
// a key to deduplicate by, and choose to select which element in a run to keep.
func DedupKey[S ~[]E, E any, K comparable](
s S,
key func(E) K,
choose func([]E) E,
) S {
return dedup(s, func(a, b E) bool { return key(a) == key(b) }, choose)
}
// DedupFunc deduplicates consecutive elements in a slice based on the equal function. If
// equal returns true, then two elements are considered duplicates, and we always pick the
// first element to keep.
func DedupFunc[S ~[]E, E any](s S, equal func(E, E) bool) S {
return dedup(s, equal, func(e []E) E { return e[0] })
}
func dedup[S ~[]E, E any](s S, equal func(E, E) bool, choose func([]E) E) S {
if len(s) == 0 {
return s
}
i := 0 // Index to write the next value at.
j := 0 // Index of prev.
prev := s[i]
for k := 1; k < len(s); k++ {
next := s[k]
if equal(prev, next) {
continue
}
s[i] = choose(s[j:k])
i++
j = k
prev = next
}
s[i] = choose(s[j:])
return s[:i+1]
}
// 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 "cmp"
// Heap is a binary min-heap. This means that it is a complete binary tree that
// respects the heap invariant: each key is less than or equal to the keys of
// its children.
//
// This type resembles Go's [container/heap] package, but it uses generics
// instead of interface calls. Entries consist of a [cmp.Ordered] key, such
// as an integer, and additional data attached to that key.
//
// A zero heap is empty and ready to use.
type Heap[K cmp.Ordered, V any] struct {
keys []K
vals []V
}
// NewHeap returns a new heap with the given pre-allocated capacity.
//
//nolint:revive,predeclared // cap used as a variable.
func NewHeap[K cmp.Ordered, V any](cap int) *Heap[K, V] {
return &Heap[K, V]{
keys: make([]K, 0, cap),
vals: make([]V, 0, cap),
}
}
// Len returns the number of elements in the heap.
func (h *Heap[K, V]) Len() int {
return len(h.keys)
}
// Insert adds an entry to the heap.
func (h *Heap[K, V]) Insert(k K, v V) {
h.push(k, v)
h.up(h.Len() - 1)
}
// Peek returns the entry with the least key, but does not pop it.
func (h *Heap[K, V]) Peek() (K, V) {
return h.keys[0], h.vals[0]
}
// Pop removes and returns the entry with the least key from the heap.
func (h *Heap[K, V]) Pop() (K, V) {
h.swap(0, h.Len()-1)
k, v := h.pop()
h.down(0)
return k, v
}
// Update replaces the entry with the least key, as if by calling [Heap.Pop]
// followed by [Heap.Insert].
func (h *Heap[K, V]) Update(k K, v V) {
h.keys[0] = k
h.vals[0] = v
// We know that we always need to be moving this entry down, because it
// can't move up: it's at the top of the heap.
h.down(0)
}
// up moves the element at i up the queue until it is greater than
// its parent.
func (h *Heap[K, V]) up(i int) {
for {
parent, root := heapParent(i)
if root || !h.less(i, parent) {
break
}
h.swap(parent, i)
i = parent
}
}
// down moves the element at i down the tree until it is greater than or equal
// to its parent.
func (h *Heap[K, V]) down(i int) {
for {
left, right, overflow := heapChildren(i)
if overflow || left >= h.Len() {
break
}
child := left
if right < h.Len() && h.less(right, left) {
child = right
}
if !h.less(child, i) {
break
}
h.swap(i, child)
i = child
}
}
// less returns whether i's key is less than j's key.
func (h *Heap[K, V]) less(i, j int) bool {
return h.keys[i] < h.keys[j]
}
// swap swaps the entries at i and j.
func (h *Heap[K, V]) swap(i, j int) {
h.keys[i], h.keys[j] = h.keys[j], h.keys[i]
h.vals[i], h.vals[j] = h.vals[j], h.vals[i]
}
// push pushes a value onto the backing slices.
func (h *Heap[K, V]) push(k K, v V) {
h.keys = append(h.keys, k)
h.vals = append(h.vals, v)
}
// pop removes the final element of the backing slices and returns it.
func (h *Heap[K, V]) pop() (k K, v V) {
end := h.Len() - 1
k, h.keys = h.keys[end], h.keys[:end]
v, h.vals = h.vals[end], h.vals[:end]
return k, v
}
// heapParent returns the heapParent index of i. Returns false if i is the root.
func heapParent(i int) (parent int, isRoot bool) {
j := (i - 1) / 2
return j, i == j
}
// heapChildren returns the child indices of i.
//
// Returns false on overflow.
func heapChildren(i int) (left, right int, overflow bool) {
j := i*2 + 1
return j, j + 1, j < 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 (
"cmp"
"slices"
"unsafe"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// IndexFunc is like [slices.IndexFunc], but also takes the index of the element
// being examined as an input to the predicate.
func IndexFunc[S ~[]E, E any](s S, p func(int, E) bool) int {
return iterx.Index2(slices.All(s), p)
}
// BinarySearchKey is like [slices.BinarySearch], but each element is mapped
// to a comparable type.
func BinarySearchKey[S ~[]E, E any, T cmp.Ordered](s S, target T, key func(E) T) (int, bool) {
return slices.BinarySearchFunc(s, target, func(e E, t T) int { return cmp.Compare(key(e), t) })
}
// PointerIndex returns an integer n such that p == &s[n], or -1 if there is
// no such integer.
//
//go:nosplit
func PointerIndex[S ~[]E, E any](s S, p *E) int {
a := unsafe.Pointer(p)
b := unsafe.Pointer(unsafe.SliceData(s))
diff := uintptr(a) - uintptr(b)
size := unsafex.Size[E]()
byteLen := len(s) * size
// This comparison checks for the following things:
//
// 1. Obviously, that diff is not past the end of s.
//
// 2. That the subtraction did not overflow. If it did, diff will be
// negative two's complement, i.e. the MSB is set, so it will be
// greater than byteLen, which, due to allocation limitations on
// every platform ever, cannot be greater than MaxInt, which all
// "negative" uintptrs are greater than.
//
// 3. That byteLen is not zero. If it is zero, this branch is taken
// regardless of the value of diff
//
// 4. That p is not nil. If it is nil, then either diff will be huge
// (because s is a nonempty slice) or byteLen will be zero in which case
// (3) applies.
//
// Doing this as one branch is much faster than checking all four
// separately; this is a fairly involved strength reduction that not even
// LLVM can figure out in many cases, nor can Go tip as of 2024-10-28.
if diff >= uintptr(byteLen) {
return -1
}
// NOTE: A check for diff % size is not necessary. This would only be needed
// if the user passed in a pointer that points into the slice, but which
// does not point to the start of one of the slice's elements. However,
// because the pointer and slice must have the same type, this would mean
// that such a pointee straddles two elements of the slice, which Go does
// not permit (such pointers can only be created by abusing the unsafe
// package).
return int(diff) / size
}
// 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 (
"iter"
"slices"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// 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(slices.Values(s), f)
}
// Join is a helper for applying [iterx.Join] to a slice.
func Join[S ~[]E, E any](s S, sep string) string {
return iterx.Join(slices.Values(s), sep)
}
// Transform is like calling [slices.Collect] with [Map], but is able to
// preallocate.
func Transform[S ~[]E, E, U any](s S, f func(E) U) []U {
out := make([]U, len(s))
for i, e := range s {
out[i] = f(e)
}
return out
}
// Pointers returns an iterator over pointers to values in s.
func Pointers[S ~[]E, E any](s S) iter.Seq2[int, *E] {
return func(yield func(int, *E) bool) {
for i := range s {
if !yield(i, &s[i]) {
return
}
}
}
}
// 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:])
}
}
}
// PartitionFunc is like [Partition], but instead the subslices are split
// whenever split returns true for adjacent elements.
//
// [Partition] is PartitionFunc with != as the splitting function.
func PartitionFunc[S ~[]E, E any](s S, split func(E, E) bool) iter.Seq2[int, S] {
return func(yield func(int, S) bool) {
var start int
var prev E
for i, next := range s {
if i == 0 {
prev = next
continue
}
if !split(prev, next) {
prev = next
continue
}
if !yield(start, s[start:i]) {
return
}
start = i
prev = next
}
if start < len(s) {
yield(start, s[start:])
}
}
}
// 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 (
"cmp"
"iter"
"slices"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// MergeKey an n-way merge of sorted slices, using a function to extract a
// comparison key. This function will be called at most once per element.
// The key extraction function is passed both the element of the slice, and
// the index of which of the input slices it was extracted from.
//
// The resulting slice will be sorted, but not necessarily stably. In other
// words, the result is as if by calling Sort(Concat(slices)), but with
// better time complexity.
//
// Time complexity is O(m log n), where m is the total number of elements to
// merge, and n is the number of slices to merge from.
func MergeKey[S ~[]E, E any, K cmp.Ordered](s []S, key func(slice int, elem E) K) S {
switch len(s) {
case 0:
return nil
case 1:
return s[0]
// TODO: can implement other common cases here, such as a pair of slices
// where the last element of the first is less than the first element of
// the second.
}
return MergeKeySeq(slices.Values(s), key, func(_ int, e E) E { return e })
}
// MergeKeySeq is like [MergeKey], but instead requires callers to provide an
// iterator that yields slices.
//
// Unlike MergeKey, it also permits modifying each element of the output slice
// before it is appended, with the knowledge of which of the input
// slices it came from.
func MergeKeySeq[S ~[]E, E any, K cmp.Ordered, V any](
slices iter.Seq[S],
key func(slice int, elem E) K,
mapper func(slice int, elem E) V,
) []V {
type entry struct {
index int
slice S
}
// Holds the slices according to key(slice[0]).
heap := NewHeap[K, entry](0)
// Preload the heap with the first entry of each slice. This is also
// an opportunity to learn the total number of entries so we can allocate
// a slice of that size.
var total int
for i, slice := range iterx.Enumerate(slices) {
total += len(slice)
if len(slice) > 0 {
heap.Insert(key(i, slice[0]), entry{i, slice})
}
}
// As long as there are entries in the queue, pop the first one, whose
// first entry is the least among all of the slices. Pop the first entry
// of that slice, write it to output, and the push the rest of the
// slice back onto the heap.
output := make([]V, 0, total)
for heap.Len() > 0 {
_, entry := heap.Peek()
output = append(output, mapper(entry.index, entry.slice[0]))
if len(entry.slice) == 1 {
heap.Pop()
} else {
entry.slice = entry.slice[1:]
heap.Update(key(entry.index, entry.slice[0]), entry)
}
}
return output
}
// 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 (
"fmt"
"iter"
"slices"
"github.com/bufbuild/protocompile/internal/ext/bitsx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// Set to true to enable debug printing for queues.
const debugQueue = false
// Queue is a ring buffer.
//
// Values can be pushed and popped from either the front or the back of the
// buffer, making it usable as a double-ended queue.
//
// A zero [Queue] is empty and ready to use.
type Queue[E any] struct {
buf []E // Invariant: cap(buf) is always a power of 2, or zero.
start, end int
}
// NewQueue returns a [Queue] with the given capacity.
func NewQueue[E any](capacity int) *Queue[E] {
if capacity == 0 {
return &Queue[E]{}
}
// Buffer length must be capacity + 1 (one slot kept empty) and power of 2.
bufLen := int(bitsx.MakePowerOfTwo(uint(capacity + 1)))
return &Queue[E]{buf: make([]E, bufLen)}
}
// Len returns the number of elements currently in the buffer.
func (r *Queue[E]) Len() int {
// The in-use part wraps around the end of the buffer.
//
// |xxx------xxxx| len: 13
// ^end ^start start: 9
// end: 3
//
// Len() = len - start + end = 13 - 9 + 3 = 7
if r.start > r.end {
return len(r.buf) - r.start + r.end
}
// The in-use part doesn't wrap around.
//
// |---xxxxxxx---| len: 13
// ^start ^end start: 3
// end: 10
//
// Len() = end - start = 10 - 3 = 7
return r.end - r.start
}
// Cap returns the capacity of the buffer, i.e., the number of elements it can
// hold before being resized.
func (r *Queue[E]) Cap() int {
if len(r.buf) == 0 {
return 0
}
return len(r.buf) - 1
}
// Reserve ensures that the capacity is large enough to push an additional n
// elements.
func (r *Queue[E]) Reserve(n int) {
if r.Len()+n <= r.Cap() {
return
}
// Buffer length must be capacity + 1 (one slot kept empty) and power of 2.
newCap := r.Len() + n
r.resize(int(bitsx.MakePowerOfTwo(uint(newCap + 1))))
}
// Front returns a pointer to the element at the front of the queue.
func (r *Queue[E]) Front() *E {
if r.start == r.end {
return nil
}
return &r.buf[r.start]
}
// Back returns a pointer to the element at the back of the queue.
func (r *Queue[E]) Back() *E {
if r.start == r.end {
return nil
}
return &r.buf[(r.end-1)&(len(r.buf)-1)]
}
// PushFront pushes elements to the front of the queue.
func (r *Queue[E]) PushFront(v ...E) {
r.Reserve(len(v))
end := r.start
start := end - len(v)
r.start = start & (len(r.buf) - 1)
if start < r.start {
// We overflowed, so we need to do two copies.
count := copy(r.buf[r.start:], v)
copy(r.buf, v[count:])
} else {
copy(r.buf[r.start:end], v)
}
}
// PushBack pushes elements to the back of the queue.
func (r *Queue[E]) PushBack(v ...E) {
r.Reserve(len(v))
start := r.end
end := start + len(v)
r.end = end & (len(r.buf) - 1)
if r.end < end {
// We overflowed, so we need to do two copies.
count := copy(r.buf[start:], v)
copy(r.buf, v[count:])
} else {
copy(r.buf[start:r.end], v)
}
}
// PopFront pops the element at the front of the queue.
func (r *Queue[E]) PopFront() (E, bool) {
if r.start == r.end {
var z E
return z, false
}
v, _ := Take(r.buf, r.start)
r.start++
r.start &= len(r.buf) - 1
return v, true
}
// PopBack pops the element at the back of the queue.
func (r *Queue[E]) PopBack() (E, bool) {
if r.start == r.end {
var z E
return z, false
}
r.end--
r.end &= len(r.buf) - 1
return Take(r.buf, r.end)
}
// Values returns an iterator over the elements of the queue.
func (r *Queue[E]) Values() iter.Seq[E] {
return func(yield func(E) bool) {
if r.start <= r.end {
for _, v := range r.buf[r.start:r.end] {
if !yield(v) {
return
}
}
return
}
for _, v := range r.buf[r.start:] {
if !yield(v) {
return
}
}
for _, v := range r.buf[:r.end] {
if !yield(v) {
return
}
}
}
}
// Format implements [fmt.Formatter].
func (r Queue[E]) Format(out fmt.State, verb rune) {
if out.Flag('#') {
fmt.Fprintf(out, "%T{", r)
} else {
fmt.Fprint(out, "[")
}
if debugQueue {
for i, v := range slices.All(r.buf) {
if i > 0 {
if out.Flag('#') {
fmt.Fprint(out, ", ")
} else {
fmt.Fprint(out, " ")
}
}
if i == r.start {
fmt.Fprint(out, ">")
if i == r.end {
fmt.Fprint(out, "< ")
}
}
fmt.Fprintf(out, fmt.FormatString(out, verb), v)
if r.start != r.end && i == r.end-1 {
fmt.Fprint(out, "<")
}
}
} else {
for i, v := range iterx.Enumerate(r.Values()) {
if i > 0 {
if out.Flag('#') {
fmt.Fprint(out, ", ")
} else {
fmt.Fprint(out, " ")
}
}
fmt.Fprintf(out, fmt.FormatString(out, verb), v)
}
}
if out.Flag('#') {
fmt.Fprint(out, "}", r)
} else {
fmt.Fprint(out, "]")
}
}
// Clear clears the queue.
func (r *Queue[_]) Clear() {
clear(r.buf)
r.start, r.end = 0, 0
r.buf = r.buf[:0]
}
func (r *Queue[E]) resize(n int) {
var count int
old := r.buf
r.buf = make([]E, n)
if r.start > r.end {
count = copy(r.buf, old[r.start:])
count += copy(r.buf[count:], old[:r.end])
} else {
count = copy(r.buf, old[r.start:r.end])
}
r.start = 0
r.end = count
}
// 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 contains extensions to Go's package slices.
package slicesx
import (
"slices"
"unsafe"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// SliceIndex is a type that can be used to index into a slice.
type SliceIndex = unsafex.Int
// One returns a slice with a single element pointing to p.
//
// If p is nil, returns an empty slice.
func One[E any](p *E) []E {
if p == nil {
return nil
}
return unsafe.Slice(p, 1)
}
// Get performs a bounds check and returns the value at idx.
//
// 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 !BoundsCheck(idx, len(s)) {
return element, false
}
// Dodge the bounds check, since Go probably won't be able to
// eliminate it even after stenciling.
return *unsafex.Add(unsafe.SliceData(s), idx), true
}
// 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 !BoundsCheck(idx, len(s)) {
return nil
}
// Dodge the bounds check, since Go probably won't be able to
// eliminate it even after stenciling.
return unsafex.Add(unsafe.SliceData(s), idx)
}
// Last returns the last element of the slice, unless it is empty, in which
// case it returns the zero value and false.
func Last[S ~[]E, E any](s S) (element E, ok bool) {
return Get(s, len(s)-1)
}
// LastPointer is like [Last], but it returns a pointer to the last element
// instead, returning nil if s is empty.
func LastPointer[S ~[]E, E any](s S) *E {
return GetPointer(s, len(s)-1)
}
// Pop is like [Last], but it removes the last element.
func Pop[S ~[]E, E any](s *S) (E, bool) {
v, ok := Last(*s)
if ok {
*s = (*s)[len(*s)-1:]
}
return v, ok
}
// LastIndex is like [slices.Index], but from the end of the slice.
func LastIndex[S ~[]E, E comparable](s S, needle E) int {
for i, v := range slices.Backward(s) {
if v == needle {
return i
}
}
return -1
}
// LastIndex is like [slices.IndexFunc], but from the end of the slice.
func LastIndexFunc[S ~[]E, E any](s S, p func(E) bool) int {
for i, v := range slices.Backward(s) {
if p(v) {
return i
}
}
return -1
}
// Take is like Get, but zeros s[i] before returning.
//
// This is useful for cases where we are popping from a slice and we want
// the popped value to be garbage collected once the caller drops it on the
// ground.
func Take[S ~[]E, E any, I SliceIndex](s S, i I) (element E, ok bool) {
p := GetPointer(s, i)
if p == nil {
return element, false
}
element, *p = *p, element
return element, true
}
// Fill writes v to every value of s.
func Fill[S ~[]E, E any](s S, v E) {
for i := range s {
s[i] = v
}
}
// 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 || ...)
// more compact.
func Among[E comparable](needle E, haystack ...E) bool {
return slices.Contains(haystack, needle)
}
// PointerEqual returns whether two slices have the same data pointer.
func PointerEqual[S ~[]E, E any](a, b S) bool {
return unsafe.SliceData(a) == unsafe.SliceData(b)
}
// 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 (
"iter"
"slices"
)
// Cut is like [strings.Cut], but for slices.
func Cut[S ~[]E, E comparable](s S, needle E) (before, after S, found bool) {
idx := slices.Index(s, needle)
if idx == -1 {
return s, s[len(s):], false
}
return s[:idx], s[idx+1:], true
}
// CutAfter is like [Cut], but includes the needle in before.
func CutAfter[S ~[]E, E comparable](s S, needle E) (before, after S, found bool) {
idx := slices.Index(s, needle)
if idx == -1 {
return s, s[len(s):], false
}
return s[:idx+1], s[idx+1:], true
}
// CutFunc is like [Cut], but uses a function to select the cut-point.
func CutFunc[S ~[]E, E any](s S, p func(int, E) bool) (before, after S, found bool) {
idx := IndexFunc(s, p)
if idx == -1 {
return s, s[len(s):], false
}
return s[:idx], s[idx+1:], true
}
// CutAfterFunc is like [CutFunc], but includes the needle in before.
func CutAfterFunc[S ~[]E, E any](s S, p func(int, E) bool) (before, after S, found bool) {
idx := IndexFunc(s, p)
if idx == -1 {
return s, s[len(s):], false
}
return s[:idx+1], s[idx+1:], true
}
// Split is like [strings.Split], but for slices.
func Split[S ~[]E, E comparable](s S, sep E) iter.Seq[S] {
return func(yield func(S) bool) {
for {
before, after, found := Cut(s, sep)
if !yield(before) {
return
}
if !found {
break
}
s = after
}
}
}
// SplitAfter is like [strings.SplitAfter], but for slices.
func SplitAfter[S ~[]E, E comparable](s S, sep E) iter.Seq[S] {
return func(yield func(S) bool) {
for {
before, after, found := CutAfter(s, sep)
if !yield(before) {
return
}
if !found {
break
}
s = after
}
}
}
// SplitFunc is like [Split], but uses a function to select the cut-point.
func SplitFunc[S ~[]E, E any](s S, sep func(int, E) bool) iter.Seq[S] {
return func(yield func(S) bool) {
for {
before, after, found := CutFunc(s, sep)
if !yield(before) {
return
}
if !found {
break
}
s = after
}
}
}
// SplitAfterFunc is like [SplitAfter], but uses a function to select the cut-point.
func SplitAfterFunc[S ~[]E, E any](s S, sep func(int, E) bool) iter.Seq[S] {
return func(yield func(S) bool) {
for {
before, after, found := CutAfterFunc(s, sep)
if !yield(before) {
return
}
if !found {
break
}
s = after
}
}
}
// 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 (
"iter"
"strings"
"unicode"
"unicode/utf8"
"unsafe"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// Rune returns the rune at the given byte index.
//
// Returns 0, false if out of bounds. Returns -1, 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, n := utf8.DecodeRuneInString(s[idx:])
if r == utf8.RuneError && n < 2 {
// The success conditions for DecodeRune are kind of subtle; this makes
// sure we get the logic right every time. It is somewhat annoying that
// Go did not chose to make this easier to inspect.
return -1, false
}
return r, true
}
// Rune returns the previous rune at the given byte index.
//
// Returns 0, false if out of bounds. Returns -1, 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, n := utf8.DecodeLastRuneInString(s[:idx])
if r == utf8.RuneError && n < 2 {
// The success conditions for DecodeRune are kind of subtle; this makes
// sure we get the logic right every time. It is somewhat annoying that
// Go did not chose to make this easier to inspect.
return -1, false
}
return r, true
}
// Byte returns the rune at the given index.
func Byte[I slicesx.SliceIndex](s string, idx I) (byte, bool) {
return slicesx.Get(unsafex.BytesAlias[[]byte](s), idx)
}
// LastLine returns the substring after the last newline (U+000A) rune.
func LastLine(s string) string {
return s[strings.IndexByte(s, '\n')+1:]
}
// Every verifies that all runes in the string are the one given.
func Every(s string, r rune) bool {
buf := string(r)
if len(s)%len(buf) != 0 {
return false
}
for i := 0; i < len(s); i += len(buf) {
if s[i:i+len(buf)] != buf {
return false
}
}
return true
}
// EveryFunc verifies that all runes in the string satisfy the given predicate.
func EveryFunc(s string, p func(rune) bool) bool {
return iterx.Every(iterx.Map2to1(Runes(s), func(_ int, r rune) rune {
if r == -1 {
r = unicode.ReplacementChar
}
return r
}), p)
}
// Runes returns an iterator over the runes in a string, and their byte indices.
//
// Each non-UTF-8 byte in the string is yielded with a rune value of `-1`.
func Runes(s string) iter.Seq2[int, rune] {
return func(yield func(i int, r rune) bool) {
orig := len(s)
for {
r, n := utf8.DecodeRuneInString(s)
if n == 0 {
return
}
if r == utf8.RuneError && n < 2 {
r = -1
}
if !yield(orig-len(s), r) {
return
}
s = s[n:]
}
}
}
// 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 := range len(s) {
// Avoid performing a bounds check each loop step.
b := *unsafex.Add(unsafe.StringData(s), i)
if !yield(b) {
return
}
}
}
}
// Split polyfills [strings.SplitSeq].
//
// Remove in go 1.24.
func Split[Sep string | rune](s string, sep Sep) iter.Seq[string] {
r := string(sep)
return func(yield func(string) bool) {
for {
chunk, rest, found := strings.Cut(s, r)
s = rest
if !yield(chunk) || !found {
return
}
}
}
}
// Split polyfills [strings.Lines].
//
// Remove in go 1.24.
func Lines(s string) iter.Seq[string] {
return Split(s, '\n')
}
// CutLast is like [strings.Cut], but searches for the last occurrence of sep.
// If sep is not present in s, returns "", s, false.
func CutLast(s, sep string) (before, after string, found bool) {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return "", s, false
}
// 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:])
}
}
}
// 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 unicodex
// Digit parses a digit in the given base, up to base 36.
func Digit(d rune, base byte) (value byte, ok bool) {
switch {
case d >= '0' && d <= '9':
value = byte(d) - '0'
case d >= 'a' && d <= 'z':
value = byte(d) - 'a' + 10
case d >= 'A' && d <= 'Z':
value = byte(d) - 'A' + 10
default:
value = 0xff
}
if value >= base {
return 0, false
}
return value, true
}
// 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 unicodex
import (
"fmt"
"io"
"iter"
"strings"
"unicode"
"unicode/utf8"
"github.com/rivo/uniseg"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/stringsx"
)
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
// purposes of diagnostics", that is, whether it is a rune that the diagnostics
// engine will replace with <U+NNNN> when printing.
func NonPrint(r rune) bool {
return !strings.ContainsRune(" \r\t\n", r) && !unicode.IsPrint(r)
}
// Width is used for calculating the approximate width of a string in terminal
// columns.
type Width struct {
// The column at which the text is being rendered. This is necessary for
// tabstop calculations.
Column int
// The width of a tabstop in columns. If set to zero, a default value will
// be selected.
Tabstop int
// If set, non-printable characters are escaped in the format <U+NNNN>.
EscapeNonPrint bool
// If non-nil, text will be output to this function, converting tabs to
// spaces and escaping unprintables as requested.
Out io.StringWriter
}
// WriteString writes the given text, advancing w.Column and writing to w.Out.
func (w *Width) WriteString(text string) (int, error) {
// We can't just use StringWidth, because that doesn't respect tabstops
// correctly.
n := 0
write := func(s string) error {
if w.Out != nil {
m, err := w.Out.WriteString(s)
n += m
return err
}
return nil
}
tabstop := w.Tabstop
if tabstop <= 0 {
tabstop = TabstopWidth
}
for i, next := range iterx.Enumerate(stringsx.Split(text, '\t')) {
if i > 0 {
tab := tabstop - (w.Column % tabstop)
w.Column += tab
// Repeat(" ", n) will typically not allocate.
spaces := strings.Repeat(" ", tab)
if err := write(spaces); err != nil {
return n, err
}
}
if !w.EscapeNonPrint {
w.Column += uniseg.StringWidth(next)
if err := write(next); err != nil {
return n, err
}
continue
}
// Handle unprintable characters. We render those as <U+NNNN>.
for next != "" {
pos, nextNonPrint, nonPrint := iterx.Find2(stringsx.Runes(next), func(_ int, r rune) bool {
return r == -1 || NonPrint(r)
})
if pos == -1 {
w.Column += uniseg.StringWidth(next)
if err := write(next); err != nil {
return n, err
}
break
}
var chunk string
chunk, next = next[:nextNonPrint], next[nextNonPrint:]
var escape string
if nonPrint == -1 {
escape = fmt.Sprintf("<%02X>", next[0])
next = next[1:]
} else {
escape = fmt.Sprintf("<U+%04X>", nonPrint)
next = next[utf8.RuneLen(nonPrint):]
}
w.Column += uniseg.StringWidth(chunk) + len(escape)
if err := write(chunk); err != nil {
return n, err
}
if err := write(escape); err != nil {
return n, err
}
}
}
return n, nil
}
// WordWrap returns an iterator over chunks of s that are no wider than maxWidth,
// which can be printed as their own lines.
func (w *Width) WordWrap(text string, maxWidth 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.
for line := range stringsx.Lines(text) {
w.Column = 0
var nextIsSpace bool
var cursor int
for start, chunk := range stringsx.PartitionKey(line, unicode.IsSpace) {
isSpace := nextIsSpace
nextIsSpace = !nextIsSpace
if isSpace && w.Column == 0 {
continue
}
_, _ = w.WriteString(chunk)
if w.Column <= maxWidth {
continue
}
if !yield(strings.TrimSpace(line[cursor:start])) {
return
}
w.Column = 0
if isSpace {
cursor = start + len(chunk)
} else {
cursor = start
_, _ = w.WriteString(chunk)
}
}
rest := line[cursor:]
if rest != "" && !yield(rest) {
return
}
}
}
}
// 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 unicodex
import "unicode"
// IsASCIIIdent checks if s is a "traditional" ASCII-only identifier.
func IsASCIIIdent(s string) bool {
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 len(s) > 0
}
// IsXIDStart returns whether r has the XID_Start property.
func IsXIDStart(r rune) bool {
// ASCII fast path.
if r == '_' ||
(r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') {
return true
}
return unicode.In(r,
unicode.Letter,
unicode.Nl, // Number, letter.
unicode.Other_ID_Start,
) && !unicode.In(r,
unicode.Pattern_Syntax,
unicode.Pattern_White_Space,
)
}
// IsXIDContinue returns whether r has the XID_Continue property.
func IsXIDContinue(r rune) bool {
// ASCII fast path.
if r == '_' ||
(r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') {
return true
}
return unicode.In(r,
unicode.Letter,
unicode.Cf, // Other, format. This includes some joiners.
unicode.Mn, // Mark, nonspacing.
unicode.Mc, // Mark, combining. Handled above.
unicode.Nl, // Number, letter.
unicode.Nd, // Number, digit.
unicode.Pc, // Punctuation, connector.
unicode.Other_ID_Start,
) && !unicode.In(r,
unicode.Pattern_Syntax,
unicode.Pattern_White_Space,
)
}
// 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 unsafex contains extensions to Go's package unsafe.
//
// Importing this package should be treated as equivalent to importing unsafe.
package unsafex
import (
"fmt"
"sync"
"unsafe"
)
// NoCopy can be embedded in a type to trigger go vet's no copy lint.
type NoCopy struct {
_ [0]sync.Mutex
}
// Int is a constraint for any integer type.
type Int interface {
~int8 | ~int16 | ~int32 | ~int64 | ~int |
~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uint |
~uintptr
}
// Size is like [unsafe.Sizeof], but it is a generic function and it returns
// an int instead of a uintptr (Go does not have types so large they would
// overflow an int).
func Size[T any]() int {
var v T
return int(unsafe.Sizeof(v))
}
// Add is like [unsafe.Add], but it operates on a typed pointer and scales the
// offset by that type's size, similar to pointer arithmetic in Rust or C.
//
// This function has the same safety caveats as [unsafe.Add].
//
//go:nosplit
func Add[P ~*E, E any, I Int](p P, idx I) P {
raw := unsafe.Pointer(p)
raw = unsafe.Add(raw, int(idx)*Size[E]())
return P(raw)
}
// Bitcast bit-casts a value of type From to a value of type To.
//
// This operation is very dangerous, because it can be used to break package
// export barriers, read uninitialized memory, and forge pointers in violation
// of [unsafe.Pointer]'s contract, resulting in memory errors in the GC.
//
// Panics if To and From have different sizes.
//
//go:nosplit
func Bitcast[To, From any](v From) To {
// This function is correctly compiled down to a mov, as seen here:
// https://godbolt.org/z/qvndcYYba
//
// With redundant code removed, stenciling Bitcast[float64, int64] produces
// (as seen in the above Godbolt):
//
// TEXT unsafex.Bitcast[float64,int64]
// MOVQ 32(R14), R12
// TESTQ R12, R12
// JNE morestack
// XCHGL AX, AX
// MOVQ AX, X0
// RET
// This check is necessary because casting a smaller type into a larger
// type will result in reading uninitialized memory, especially in the
// presence of inlining that causes &aligned below to point into the heap.
// The equivalent functions in Rust and C++ perform this check statically,
// because it is so important.
if Size[To]() != Size[From]() {
// This check will always be inlined away, because Bitcast is
// manifestly inline-able.
//
// NOTE: This could potentially be replaced with a link error, by making
// this call a function with no body (and then not defining that
// function in a .s file; although, note we do need an empty.s to
// silence a compiler error in that case).
panic(badBitcast[To, From]{})
}
// To avoid an unaligned load below, we copy From into a struct aligned to
// To's alignment. Consider the following situation: we call
// Bitcast[int32, [4]byte]. There is no guarantee that &v will be aligned
// to the four byte boundary required for int32, and thus casting it to *To
// may result in an unaligned load.
//
// As seen in the Godbolt above, for cases where the alignment change
// is redundant, this gets optimized away.
aligned := struct {
_ [0]To
v From
}{v: v}
return *(*To)(unsafe.Pointer(&aligned.v))
}
type badBitcast[To, From any] struct{}
func (badBitcast[To, From]) Error() string {
var to To
var from From
return fmt.Sprintf(
"unsafex: %T and %T are of unequal size (%d != %d)",
to, from,
Size[To](), Size[From](),
)
}
// StringAlias returns a string that aliases a slice. This is useful for
// situations where we're allocating a string on the stack, or where we have
// a slice that will never be written to and we want to interpret as a string
// without a copy.
//
// data must not be written to: for the lifetime of the returned string (that
// is, until its final use in the program upon which a finalizer set on it could
// run), it must be treated as if goroutines are concurrently reading from it:
// data must not be mutated in any way.
//
//go:nosplit
func StringAlias[S ~[]E, E any](data S) string {
return unsafe.String(
Bitcast[*byte](unsafe.SliceData(data)),
len(data)*Size[E](),
)
}
// BytesAlias is the inverse of [StringAlias].
//
// The same caveats apply as with [StringAlias] around mutating `data`.
//
//go:nosplit
func BytesAlias[S ~[]B, B ~byte](data string) []B {
return unsafe.Slice(
Bitcast[*B](unsafe.StringData(data)),
len(data),
)
}
// NoEscape makes a copy of a pointer which is hidden from the compiler's
// escape analysis. If the return value of this function appears to escape, the
// compiler will *not* treat its input as escaping, potentially allowing stack
// promotion of values which escape. This function is essentially the opposite
// of runtime.KeepAlive.
//
// This is only safe when the return value does not actually escape to the heap,
// but only appears to, such as by being passed to a virtual call which does not
// actually result in a heap escape.
//
//go:nosplit
func NoEscape[P ~*E, E any](ptr P) P {
p := unsafe.Pointer(ptr)
// Xoring the address with zero is a reliable way to hide a pointer from
// the compiler.
p = unsafe.Pointer(uintptr(p) ^ 0) //nolint:staticcheck
return P(p)
}
// 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 featuresext provides file descriptors for the
// "google/protobuf/cpp_features.proto" and "google/protobuf/java_features.proto"
// standard import files. Unlike the other standard/well-known
// imports, these files have no standard Go package in their
// runtime with generated code. So in order to make them available
// as "standard imports" to compiler users, we must embed these
// descriptors into a Go package.
package featuresext
import (
_ "embed"
"fmt"
"sync"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
)
var (
//go:embed cpp_features.protoset
cppFeatures []byte
//go:embed java_features.protoset
javaFeatures []byte
initOnce sync.Once
initCppFeatures protoreflect.FileDescriptor
initCppErr error
initJavaFeatures protoreflect.FileDescriptor
initJavaErr error
)
func initDescriptors() {
initOnce.Do(func() {
initCppFeatures, initCppErr = buildDescriptor("google/protobuf/cpp_features.proto", cppFeatures)
initJavaFeatures, initJavaErr = buildDescriptor("google/protobuf/java_features.proto", javaFeatures)
})
}
func CppFeaturesDescriptor() (protoreflect.FileDescriptor, error) {
initDescriptors()
return initCppFeatures, initCppErr
}
func JavaFeaturesDescriptor() (protoreflect.FileDescriptor, error) {
initDescriptors()
return initJavaFeatures, initJavaErr
}
func buildDescriptor(name string, data []byte) (protoreflect.FileDescriptor, error) {
var files descriptorpb.FileDescriptorSet
err := proto.Unmarshal(data, &files)
if err != nil {
return nil, fmt.Errorf("failed to load descriptor for %q: %w", name, err)
}
if len(files.File) != 1 {
return nil, fmt.Errorf("failed to load descriptor for %q: expected embedded descriptor set to contain exactly one file but it instead has %d", name, len(files.File))
}
if files.File[0].GetName() != name {
return nil, fmt.Errorf("failed to load descriptor for %q: embedded descriptor contains wrong file %q", name, files.File[0].GetName())
}
descriptor, err := protodesc.NewFile(files.File[0], protoregistry.GlobalFiles)
if err != nil {
return nil, fmt.Errorf("failed to load descriptor for %q: %w", name, err)
}
return descriptor, nil
}
// 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 protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: buf/compiler/v1alpha1/ast.proto
package compilerv1alpha1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Path_Component_Separator int32
const (
Path_Component_SEPARATOR_UNSPECIFIED Path_Component_Separator = 0
Path_Component_SEPARATOR_DOT Path_Component_Separator = 1
Path_Component_SEPARATOR_SLASH Path_Component_Separator = 2
)
// Enum value maps for Path_Component_Separator.
var (
Path_Component_Separator_name = map[int32]string{
0: "SEPARATOR_UNSPECIFIED",
1: "SEPARATOR_DOT",
2: "SEPARATOR_SLASH",
}
Path_Component_Separator_value = map[string]int32{
"SEPARATOR_UNSPECIFIED": 0,
"SEPARATOR_DOT": 1,
"SEPARATOR_SLASH": 2,
}
)
func (x Path_Component_Separator) Enum() *Path_Component_Separator {
p := new(Path_Component_Separator)
*p = x
return p
}
func (x Path_Component_Separator) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Path_Component_Separator) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[0].Descriptor()
}
func (Path_Component_Separator) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[0]
}
func (x Path_Component_Separator) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Path_Component_Separator.Descriptor instead.
func (Path_Component_Separator) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{2, 0, 0}
}
type Decl_Syntax_Kind int32
const (
Decl_Syntax_KIND_UNSPECIFIED Decl_Syntax_Kind = 0
Decl_Syntax_KIND_SYNTAX Decl_Syntax_Kind = 1
Decl_Syntax_KIND_EDITION Decl_Syntax_Kind = 2
)
// Enum value maps for Decl_Syntax_Kind.
var (
Decl_Syntax_Kind_name = map[int32]string{
0: "KIND_UNSPECIFIED",
1: "KIND_SYNTAX",
2: "KIND_EDITION",
}
Decl_Syntax_Kind_value = map[string]int32{
"KIND_UNSPECIFIED": 0,
"KIND_SYNTAX": 1,
"KIND_EDITION": 2,
}
)
func (x Decl_Syntax_Kind) Enum() *Decl_Syntax_Kind {
p := new(Decl_Syntax_Kind)
*p = x
return p
}
func (x Decl_Syntax_Kind) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Decl_Syntax_Kind) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[1].Descriptor()
}
func (Decl_Syntax_Kind) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[1]
}
func (x Decl_Syntax_Kind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Decl_Syntax_Kind.Descriptor instead.
func (Decl_Syntax_Kind) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 1, 0}
}
type Decl_Import_Modifier int32
const (
Decl_Import_MODIFIER_UNSPECIFIED Decl_Import_Modifier = 0
Decl_Import_MODIFIER_WEAK Decl_Import_Modifier = 1
Decl_Import_MODIFIER_PUBLIC Decl_Import_Modifier = 2
)
// Enum value maps for Decl_Import_Modifier.
var (
Decl_Import_Modifier_name = map[int32]string{
0: "MODIFIER_UNSPECIFIED",
1: "MODIFIER_WEAK",
2: "MODIFIER_PUBLIC",
}
Decl_Import_Modifier_value = map[string]int32{
"MODIFIER_UNSPECIFIED": 0,
"MODIFIER_WEAK": 1,
"MODIFIER_PUBLIC": 2,
}
)
func (x Decl_Import_Modifier) Enum() *Decl_Import_Modifier {
p := new(Decl_Import_Modifier)
*p = x
return p
}
func (x Decl_Import_Modifier) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Decl_Import_Modifier) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[2].Descriptor()
}
func (Decl_Import_Modifier) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[2]
}
func (x Decl_Import_Modifier) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Decl_Import_Modifier.Descriptor instead.
func (Decl_Import_Modifier) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 3, 0}
}
type Decl_Range_Kind int32
const (
Decl_Range_KIND_UNSPECIFIED Decl_Range_Kind = 0
Decl_Range_KIND_EXTENSIONS Decl_Range_Kind = 1
Decl_Range_KIND_RESERVED Decl_Range_Kind = 2
)
// Enum value maps for Decl_Range_Kind.
var (
Decl_Range_Kind_name = map[int32]string{
0: "KIND_UNSPECIFIED",
1: "KIND_EXTENSIONS",
2: "KIND_RESERVED",
}
Decl_Range_Kind_value = map[string]int32{
"KIND_UNSPECIFIED": 0,
"KIND_EXTENSIONS": 1,
"KIND_RESERVED": 2,
}
)
func (x Decl_Range_Kind) Enum() *Decl_Range_Kind {
p := new(Decl_Range_Kind)
*p = x
return p
}
func (x Decl_Range_Kind) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Decl_Range_Kind) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[3].Descriptor()
}
func (Decl_Range_Kind) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[3]
}
func (x Decl_Range_Kind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Decl_Range_Kind.Descriptor instead.
func (Decl_Range_Kind) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 5, 0}
}
type Def_Kind int32
const (
Def_KIND_UNSPECIFIED Def_Kind = 0
Def_KIND_MESSAGE Def_Kind = 1
Def_KIND_ENUM Def_Kind = 2
Def_KIND_SERVICE Def_Kind = 3
Def_KIND_EXTEND Def_Kind = 4
Def_KIND_FIELD Def_Kind = 5
Def_KIND_ENUM_VALUE Def_Kind = 6
Def_KIND_ONEOF Def_Kind = 7
Def_KIND_GROUP Def_Kind = 8
Def_KIND_METHOD Def_Kind = 9
Def_KIND_OPTION Def_Kind = 10
)
// Enum value maps for Def_Kind.
var (
Def_Kind_name = map[int32]string{
0: "KIND_UNSPECIFIED",
1: "KIND_MESSAGE",
2: "KIND_ENUM",
3: "KIND_SERVICE",
4: "KIND_EXTEND",
5: "KIND_FIELD",
6: "KIND_ENUM_VALUE",
7: "KIND_ONEOF",
8: "KIND_GROUP",
9: "KIND_METHOD",
10: "KIND_OPTION",
}
Def_Kind_value = map[string]int32{
"KIND_UNSPECIFIED": 0,
"KIND_MESSAGE": 1,
"KIND_ENUM": 2,
"KIND_SERVICE": 3,
"KIND_EXTEND": 4,
"KIND_FIELD": 5,
"KIND_ENUM_VALUE": 6,
"KIND_ONEOF": 7,
"KIND_GROUP": 8,
"KIND_METHOD": 9,
"KIND_OPTION": 10,
}
)
func (x Def_Kind) Enum() *Def_Kind {
p := new(Def_Kind)
*p = x
return p
}
func (x Def_Kind) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Def_Kind) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[4].Descriptor()
}
func (Def_Kind) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[4]
}
func (x Def_Kind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Def_Kind.Descriptor instead.
func (Def_Kind) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{4, 0}
}
type Expr_Prefixed_Prefix int32
const (
Expr_Prefixed_PREFIX_UNSPECIFIED Expr_Prefixed_Prefix = 0
Expr_Prefixed_PREFIX_MINUS Expr_Prefixed_Prefix = 1
)
// Enum value maps for Expr_Prefixed_Prefix.
var (
Expr_Prefixed_Prefix_name = map[int32]string{
0: "PREFIX_UNSPECIFIED",
1: "PREFIX_MINUS",
}
Expr_Prefixed_Prefix_value = map[string]int32{
"PREFIX_UNSPECIFIED": 0,
"PREFIX_MINUS": 1,
}
)
func (x Expr_Prefixed_Prefix) Enum() *Expr_Prefixed_Prefix {
p := new(Expr_Prefixed_Prefix)
*p = x
return p
}
func (x Expr_Prefixed_Prefix) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Expr_Prefixed_Prefix) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[5].Descriptor()
}
func (Expr_Prefixed_Prefix) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[5]
}
func (x Expr_Prefixed_Prefix) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Expr_Prefixed_Prefix.Descriptor instead.
func (Expr_Prefixed_Prefix) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 1, 0}
}
type Type_Prefixed_Prefix int32
const (
Type_Prefixed_PREFIX_UNSPECIFIED Type_Prefixed_Prefix = 0
Type_Prefixed_PREFIX_OPTIONAL Type_Prefixed_Prefix = 1
Type_Prefixed_PREFIX_REPEATED Type_Prefixed_Prefix = 2
Type_Prefixed_PREFIX_REQUIRED Type_Prefixed_Prefix = 3
Type_Prefixed_PREFIX_STREAM Type_Prefixed_Prefix = 4
)
// Enum value maps for Type_Prefixed_Prefix.
var (
Type_Prefixed_Prefix_name = map[int32]string{
0: "PREFIX_UNSPECIFIED",
1: "PREFIX_OPTIONAL",
2: "PREFIX_REPEATED",
3: "PREFIX_REQUIRED",
4: "PREFIX_STREAM",
}
Type_Prefixed_Prefix_value = map[string]int32{
"PREFIX_UNSPECIFIED": 0,
"PREFIX_OPTIONAL": 1,
"PREFIX_REPEATED": 2,
"PREFIX_REQUIRED": 3,
"PREFIX_STREAM": 4,
}
)
func (x Type_Prefixed_Prefix) Enum() *Type_Prefixed_Prefix {
p := new(Type_Prefixed_Prefix)
*p = x
return p
}
func (x Type_Prefixed_Prefix) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Type_Prefixed_Prefix) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_ast_proto_enumTypes[6].Descriptor()
}
func (Type_Prefixed_Prefix) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_ast_proto_enumTypes[6]
}
func (x Type_Prefixed_Prefix) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Type_Prefixed_Prefix.Descriptor instead.
func (Type_Prefixed_Prefix) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{7, 0, 0}
}
// A parsed AST file. This is the root file for the whole Protocompile AST.
type File struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The original filesystem file this file was parsed from.
File *Report_File `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"`
// Declarations in this file.
Decls []*Decl `protobuf:"bytes,2,rep,name=decls,proto3" json:"decls,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *File) Reset() {
*x = File{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *File) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*File) ProtoMessage() {}
func (x *File) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use File.ProtoReflect.Descriptor instead.
func (*File) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{0}
}
func (x *File) GetFile() *Report_File {
if x != nil {
return x.File
}
return nil
}
func (x *File) GetDecls() []*Decl {
if x != nil {
return x.Decls
}
return nil
}
// A source code span for a specific `File`.
//
// This only contains byte offsets for the span; all other information
// (such as the line number) should be re-computed as needed.
type Span struct {
state protoimpl.MessageState `protogen:"open.v1"`
Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Span) Reset() {
*x = Span{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Span) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Span) ProtoMessage() {}
func (x *Span) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Span.ProtoReflect.Descriptor instead.
func (*Span) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{1}
}
func (x *Span) GetStart() uint32 {
if x != nil {
return x.Start
}
return 0
}
func (x *Span) GetEnd() uint32 {
if x != nil {
return x.End
}
return 0
}
// A path in a Protobuf file. This models all identifiers (simple, compound,
// and fully-qualified). It also models option names, which contain nested
// paths, and Any URLs, which contain a single slash.
//
// To do so, it models the maximal union of these syntax constructs, permitting
// arbitrary nesting and mixed . and / separators. Many paths representable with
// this message do not correspond to valid Protobuf syntax, but they are
// accepted by the parser.
type Path struct {
state protoimpl.MessageState `protogen:"open.v1"`
Components []*Path_Component `protobuf:"bytes,1,rep,name=components,proto3" json:"components,omitempty"`
// The span for the whole path.
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Path) Reset() {
*x = Path{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Path) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Path) ProtoMessage() {}
func (x *Path) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Path.ProtoReflect.Descriptor instead.
func (*Path) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{2}
}
func (x *Path) GetComponents() []*Path_Component {
if x != nil {
return x.Components
}
return nil
}
func (x *Path) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
// A declaration in a Protobuf file.
type Decl struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Decl:
//
// *Decl_Empty_
// *Decl_Syntax_
// *Decl_Import_
// *Decl_Package_
// *Decl_Def
// *Decl_Body_
// *Decl_Range_
Decl isDecl_Decl `protobuf_oneof:"decl"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl) Reset() {
*x = Decl{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl) ProtoMessage() {}
func (x *Decl) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl.ProtoReflect.Descriptor instead.
func (*Decl) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3}
}
func (x *Decl) GetDecl() isDecl_Decl {
if x != nil {
return x.Decl
}
return nil
}
func (x *Decl) GetEmpty() *Decl_Empty {
if x != nil {
if x, ok := x.Decl.(*Decl_Empty_); ok {
return x.Empty
}
}
return nil
}
func (x *Decl) GetSyntax() *Decl_Syntax {
if x != nil {
if x, ok := x.Decl.(*Decl_Syntax_); ok {
return x.Syntax
}
}
return nil
}
func (x *Decl) GetImport() *Decl_Import {
if x != nil {
if x, ok := x.Decl.(*Decl_Import_); ok {
return x.Import
}
}
return nil
}
func (x *Decl) GetPackage() *Decl_Package {
if x != nil {
if x, ok := x.Decl.(*Decl_Package_); ok {
return x.Package
}
}
return nil
}
func (x *Decl) GetDef() *Def {
if x != nil {
if x, ok := x.Decl.(*Decl_Def); ok {
return x.Def
}
}
return nil
}
func (x *Decl) GetBody() *Decl_Body {
if x != nil {
if x, ok := x.Decl.(*Decl_Body_); ok {
return x.Body
}
}
return nil
}
func (x *Decl) GetRange() *Decl_Range {
if x != nil {
if x, ok := x.Decl.(*Decl_Range_); ok {
return x.Range
}
}
return nil
}
type isDecl_Decl interface {
isDecl_Decl()
}
type Decl_Empty_ struct {
Empty *Decl_Empty `protobuf:"bytes,1,opt,name=empty,proto3,oneof"`
}
type Decl_Syntax_ struct {
Syntax *Decl_Syntax `protobuf:"bytes,2,opt,name=syntax,proto3,oneof"`
}
type Decl_Import_ struct {
Import *Decl_Import `protobuf:"bytes,3,opt,name=import,proto3,oneof"`
}
type Decl_Package_ struct {
Package *Decl_Package `protobuf:"bytes,4,opt,name=package,proto3,oneof"`
}
type Decl_Def struct {
Def *Def `protobuf:"bytes,5,opt,name=def,proto3,oneof"`
}
type Decl_Body_ struct {
Body *Decl_Body `protobuf:"bytes,6,opt,name=body,proto3,oneof"`
}
type Decl_Range_ struct {
Range *Decl_Range `protobuf:"bytes,7,opt,name=range,proto3,oneof"`
}
func (*Decl_Empty_) isDecl_Decl() {}
func (*Decl_Syntax_) isDecl_Decl() {}
func (*Decl_Import_) isDecl_Decl() {}
func (*Decl_Package_) isDecl_Decl() {}
func (*Decl_Def) isDecl_Decl() {}
func (*Decl_Body_) isDecl_Decl() {}
func (*Decl_Range_) isDecl_Decl() {}
// A definition is a particular kind of declaration that combines the syntactic
// elements of type definitions, fields, options, and service methods.
//
// This allows the parser to accept and represent many invalid but plausible productions.
type Def struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Definitions without a clear kind may be marked as `KIND_UNSPECIFIED`.
Kind Def_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=buf.compiler.v1alpha1.Def_Kind" json:"kind,omitempty"`
Name *Path `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
// The type for a `KIND_FIELD` definition.
Type *Type `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Signature *Def_Signature `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"`
// This is the tag number of `KIND_FIELD` or `KIND_ENUM_VALUE,
// or the value of `KIND_OPTION`.
Value *Expr `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
// This is options appearing in `[...]`, such as on `KIND_FIELD`
// or `KIND_GROUP`. This will NOT include options on a oneof, since
// those are represented as `KIND_OPTION` `Def` in `body`.
Options *Options `protobuf:"bytes,6,opt,name=options,proto3" json:"options,omitempty"`
// This is a braced body at the end of the definition.
Body *Decl_Body `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
KeywordSpan *Span `protobuf:"bytes,11,opt,name=keyword_span,json=keywordSpan,proto3" json:"keyword_span,omitempty"`
EqualsSpan *Span `protobuf:"bytes,12,opt,name=equals_span,json=equalsSpan,proto3" json:"equals_span,omitempty"`
SemicolonSpan *Span `protobuf:"bytes,13,opt,name=semicolon_span,json=semicolonSpan,proto3" json:"semicolon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Def) Reset() {
*x = Def{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Def) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Def) ProtoMessage() {}
func (x *Def) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Def.ProtoReflect.Descriptor instead.
func (*Def) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{4}
}
func (x *Def) GetKind() Def_Kind {
if x != nil {
return x.Kind
}
return Def_KIND_UNSPECIFIED
}
func (x *Def) GetName() *Path {
if x != nil {
return x.Name
}
return nil
}
func (x *Def) GetType() *Type {
if x != nil {
return x.Type
}
return nil
}
func (x *Def) GetSignature() *Def_Signature {
if x != nil {
return x.Signature
}
return nil
}
func (x *Def) GetValue() *Expr {
if x != nil {
return x.Value
}
return nil
}
func (x *Def) GetOptions() *Options {
if x != nil {
return x.Options
}
return nil
}
func (x *Def) GetBody() *Decl_Body {
if x != nil {
return x.Body
}
return nil
}
func (x *Def) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Def) GetKeywordSpan() *Span {
if x != nil {
return x.KeywordSpan
}
return nil
}
func (x *Def) GetEqualsSpan() *Span {
if x != nil {
return x.EqualsSpan
}
return nil
}
func (x *Def) GetSemicolonSpan() *Span {
if x != nil {
return x.SemicolonSpan
}
return nil
}
// Compact options after a declaration, in `[...]`.
type Options struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entries []*Options_Entry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Options) Reset() {
*x = Options{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Options) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Options) ProtoMessage() {}
func (x *Options) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Options.ProtoReflect.Descriptor instead.
func (*Options) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{5}
}
func (x *Options) GetEntries() []*Options_Entry {
if x != nil {
return x.Entries
}
return nil
}
func (x *Options) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
// An expression, such as the value of an option or the tag of a field.
type Expr struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Expr:
//
// *Expr_Literal_
// *Expr_Path
// *Expr_Prefixed_
// *Expr_Range_
// *Expr_Array_
// *Expr_Dict_
// *Expr_Field_
Expr isExpr_Expr `protobuf_oneof:"expr"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr) Reset() {
*x = Expr{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr) ProtoMessage() {}
func (x *Expr) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr.ProtoReflect.Descriptor instead.
func (*Expr) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6}
}
func (x *Expr) GetExpr() isExpr_Expr {
if x != nil {
return x.Expr
}
return nil
}
func (x *Expr) GetLiteral() *Expr_Literal {
if x != nil {
if x, ok := x.Expr.(*Expr_Literal_); ok {
return x.Literal
}
}
return nil
}
func (x *Expr) GetPath() *Path {
if x != nil {
if x, ok := x.Expr.(*Expr_Path); ok {
return x.Path
}
}
return nil
}
func (x *Expr) GetPrefixed() *Expr_Prefixed {
if x != nil {
if x, ok := x.Expr.(*Expr_Prefixed_); ok {
return x.Prefixed
}
}
return nil
}
func (x *Expr) GetRange() *Expr_Range {
if x != nil {
if x, ok := x.Expr.(*Expr_Range_); ok {
return x.Range
}
}
return nil
}
func (x *Expr) GetArray() *Expr_Array {
if x != nil {
if x, ok := x.Expr.(*Expr_Array_); ok {
return x.Array
}
}
return nil
}
func (x *Expr) GetDict() *Expr_Dict {
if x != nil {
if x, ok := x.Expr.(*Expr_Dict_); ok {
return x.Dict
}
}
return nil
}
func (x *Expr) GetField() *Expr_Field {
if x != nil {
if x, ok := x.Expr.(*Expr_Field_); ok {
return x.Field
}
}
return nil
}
type isExpr_Expr interface {
isExpr_Expr()
}
type Expr_Literal_ struct {
Literal *Expr_Literal `protobuf:"bytes,1,opt,name=literal,proto3,oneof"`
}
type Expr_Path struct {
Path *Path `protobuf:"bytes,2,opt,name=path,proto3,oneof"`
}
type Expr_Prefixed_ struct {
Prefixed *Expr_Prefixed `protobuf:"bytes,3,opt,name=prefixed,proto3,oneof"`
}
type Expr_Range_ struct {
Range *Expr_Range `protobuf:"bytes,4,opt,name=range,proto3,oneof"`
}
type Expr_Array_ struct {
Array *Expr_Array `protobuf:"bytes,5,opt,name=array,proto3,oneof"`
}
type Expr_Dict_ struct {
Dict *Expr_Dict `protobuf:"bytes,6,opt,name=dict,proto3,oneof"`
}
type Expr_Field_ struct {
Field *Expr_Field `protobuf:"bytes,7,opt,name=field,proto3,oneof"`
}
func (*Expr_Literal_) isExpr_Expr() {}
func (*Expr_Path) isExpr_Expr() {}
func (*Expr_Prefixed_) isExpr_Expr() {}
func (*Expr_Range_) isExpr_Expr() {}
func (*Expr_Array_) isExpr_Expr() {}
func (*Expr_Dict_) isExpr_Expr() {}
func (*Expr_Field_) isExpr_Expr() {}
// A type, such as the prefix of a field.
//
// This AST includes many types not present in ordinary Protobuf, such as representations
// for `repeated repeated int32` and `Arbitrary<int32>`, among others.
type Type struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Type:
//
// *Type_Path
// *Type_Prefixed_
// *Type_Generic_
Type isType_Type `protobuf_oneof:"type"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Type) Reset() {
*x = Type{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Type) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Type) ProtoMessage() {}
func (x *Type) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Type.ProtoReflect.Descriptor instead.
func (*Type) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{7}
}
func (x *Type) GetType() isType_Type {
if x != nil {
return x.Type
}
return nil
}
func (x *Type) GetPath() *Path {
if x != nil {
if x, ok := x.Type.(*Type_Path); ok {
return x.Path
}
}
return nil
}
func (x *Type) GetPrefixed() *Type_Prefixed {
if x != nil {
if x, ok := x.Type.(*Type_Prefixed_); ok {
return x.Prefixed
}
}
return nil
}
func (x *Type) GetGeneric() *Type_Generic {
if x != nil {
if x, ok := x.Type.(*Type_Generic_); ok {
return x.Generic
}
}
return nil
}
type isType_Type interface {
isType_Type()
}
type Type_Path struct {
Path *Path `protobuf:"bytes,1,opt,name=path,proto3,oneof"`
}
type Type_Prefixed_ struct {
Prefixed *Type_Prefixed `protobuf:"bytes,2,opt,name=prefixed,proto3,oneof"`
}
type Type_Generic_ struct {
Generic *Type_Generic `protobuf:"bytes,3,opt,name=generic,proto3,oneof"`
}
func (*Type_Path) isType_Type() {}
func (*Type_Prefixed_) isType_Type() {}
func (*Type_Generic_) isType_Type() {}
// A path component.
type Path_Component struct {
state protoimpl.MessageState `protogen:"open.v1"`
// May be missing altogether, for invalid paths like `foo..bar`.
//
// Types that are valid to be assigned to Component:
//
// *Path_Component_Ident
// *Path_Component_Extension
Component isPath_Component_Component `protobuf_oneof:"component"`
// The type of separator this component had before it.
// If this is SEPARATOR_UNSPECIFIED, this is the first
// component, and the path is not absolute.
Separator Path_Component_Separator `protobuf:"varint,3,opt,name=separator,proto3,enum=buf.compiler.v1alpha1.Path_Component_Separator" json:"separator,omitempty"`
// The span of the component's value.
ComponentSpan *Span `protobuf:"bytes,10,opt,name=component_span,json=componentSpan,proto3" json:"component_span,omitempty"`
// The span of this component's leading dot, if any.
SeparatorSpan *Span `protobuf:"bytes,11,opt,name=separator_span,json=separatorSpan,proto3" json:"separator_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Path_Component) Reset() {
*x = Path_Component{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Path_Component) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Path_Component) ProtoMessage() {}
func (x *Path_Component) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Path_Component.ProtoReflect.Descriptor instead.
func (*Path_Component) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{2, 0}
}
func (x *Path_Component) GetComponent() isPath_Component_Component {
if x != nil {
return x.Component
}
return nil
}
func (x *Path_Component) GetIdent() string {
if x != nil {
if x, ok := x.Component.(*Path_Component_Ident); ok {
return x.Ident
}
}
return ""
}
func (x *Path_Component) GetExtension() *Path {
if x != nil {
if x, ok := x.Component.(*Path_Component_Extension); ok {
return x.Extension
}
}
return nil
}
func (x *Path_Component) GetSeparator() Path_Component_Separator {
if x != nil {
return x.Separator
}
return Path_Component_SEPARATOR_UNSPECIFIED
}
func (x *Path_Component) GetComponentSpan() *Span {
if x != nil {
return x.ComponentSpan
}
return nil
}
func (x *Path_Component) GetSeparatorSpan() *Span {
if x != nil {
return x.SeparatorSpan
}
return nil
}
type isPath_Component_Component interface {
isPath_Component_Component()
}
type Path_Component_Ident struct {
// A single identifier.
Ident string `protobuf:"bytes,1,opt,name=ident,proto3,oneof"`
}
type Path_Component_Extension struct {
// A nested extension path.
Extension *Path `protobuf:"bytes,2,opt,name=extension,proto3,oneof"`
}
func (*Path_Component_Ident) isPath_Component_Component() {}
func (*Path_Component_Extension) isPath_Component_Component() {}
// An empty declaration.
type Decl_Empty struct {
state protoimpl.MessageState `protogen:"open.v1"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Empty) Reset() {
*x = Decl_Empty{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Empty) ProtoMessage() {}
func (x *Decl_Empty) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Empty.ProtoReflect.Descriptor instead.
func (*Decl_Empty) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 0}
}
func (x *Decl_Empty) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
// A language pragma, such as a syntax or edition declaration.
type Decl_Syntax struct {
state protoimpl.MessageState `protogen:"open.v1"`
Kind Decl_Syntax_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=buf.compiler.v1alpha1.Decl_Syntax_Kind" json:"kind,omitempty"`
Value *Expr `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
Options *Options `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
KeywordSpan *Span `protobuf:"bytes,11,opt,name=keyword_span,json=keywordSpan,proto3" json:"keyword_span,omitempty"`
EqualsSpan *Span `protobuf:"bytes,12,opt,name=equals_span,json=equalsSpan,proto3" json:"equals_span,omitempty"`
SemicolonSpan *Span `protobuf:"bytes,13,opt,name=semicolon_span,json=semicolonSpan,proto3" json:"semicolon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Syntax) Reset() {
*x = Decl_Syntax{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Syntax) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Syntax) ProtoMessage() {}
func (x *Decl_Syntax) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Syntax.ProtoReflect.Descriptor instead.
func (*Decl_Syntax) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 1}
}
func (x *Decl_Syntax) GetKind() Decl_Syntax_Kind {
if x != nil {
return x.Kind
}
return Decl_Syntax_KIND_UNSPECIFIED
}
func (x *Decl_Syntax) GetValue() *Expr {
if x != nil {
return x.Value
}
return nil
}
func (x *Decl_Syntax) GetOptions() *Options {
if x != nil {
return x.Options
}
return nil
}
func (x *Decl_Syntax) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Decl_Syntax) GetKeywordSpan() *Span {
if x != nil {
return x.KeywordSpan
}
return nil
}
func (x *Decl_Syntax) GetEqualsSpan() *Span {
if x != nil {
return x.EqualsSpan
}
return nil
}
func (x *Decl_Syntax) GetSemicolonSpan() *Span {
if x != nil {
return x.SemicolonSpan
}
return nil
}
// A package declaration.
type Decl_Package struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *Path `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
Options *Options `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
KeywordSpan *Span `protobuf:"bytes,11,opt,name=keyword_span,json=keywordSpan,proto3" json:"keyword_span,omitempty"`
SemicolonSpan *Span `protobuf:"bytes,12,opt,name=semicolon_span,json=semicolonSpan,proto3" json:"semicolon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Package) Reset() {
*x = Decl_Package{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Package) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Package) ProtoMessage() {}
func (x *Decl_Package) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Package.ProtoReflect.Descriptor instead.
func (*Decl_Package) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 2}
}
func (x *Decl_Package) GetPath() *Path {
if x != nil {
return x.Path
}
return nil
}
func (x *Decl_Package) GetOptions() *Options {
if x != nil {
return x.Options
}
return nil
}
func (x *Decl_Package) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Decl_Package) GetKeywordSpan() *Span {
if x != nil {
return x.KeywordSpan
}
return nil
}
func (x *Decl_Package) GetSemicolonSpan() *Span {
if x != nil {
return x.SemicolonSpan
}
return nil
}
// An import declaration.
type Decl_Import struct {
state protoimpl.MessageState `protogen:"open.v1"`
Modifier []Decl_Import_Modifier `protobuf:"varint,1,rep,packed,name=modifier,proto3,enum=buf.compiler.v1alpha1.Decl_Import_Modifier" json:"modifier,omitempty"`
ImportPath *Expr `protobuf:"bytes,2,opt,name=import_path,json=importPath,proto3" json:"import_path,omitempty"`
Options *Options `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
KeywordSpan *Span `protobuf:"bytes,11,opt,name=keyword_span,json=keywordSpan,proto3" json:"keyword_span,omitempty"`
ModifierSpan []*Span `protobuf:"bytes,12,rep,name=modifier_span,json=modifierSpan,proto3" json:"modifier_span,omitempty"`
ImportPathSpan *Span `protobuf:"bytes,13,opt,name=import_path_span,json=importPathSpan,proto3" json:"import_path_span,omitempty"`
SemicolonSpan *Span `protobuf:"bytes,14,opt,name=semicolon_span,json=semicolonSpan,proto3" json:"semicolon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Import) Reset() {
*x = Decl_Import{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Import) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Import) ProtoMessage() {}
func (x *Decl_Import) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Import.ProtoReflect.Descriptor instead.
func (*Decl_Import) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 3}
}
func (x *Decl_Import) GetModifier() []Decl_Import_Modifier {
if x != nil {
return x.Modifier
}
return nil
}
func (x *Decl_Import) GetImportPath() *Expr {
if x != nil {
return x.ImportPath
}
return nil
}
func (x *Decl_Import) GetOptions() *Options {
if x != nil {
return x.Options
}
return nil
}
func (x *Decl_Import) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Decl_Import) GetKeywordSpan() *Span {
if x != nil {
return x.KeywordSpan
}
return nil
}
func (x *Decl_Import) GetModifierSpan() []*Span {
if x != nil {
return x.ModifierSpan
}
return nil
}
func (x *Decl_Import) GetImportPathSpan() *Span {
if x != nil {
return x.ImportPathSpan
}
return nil
}
func (x *Decl_Import) GetSemicolonSpan() *Span {
if x != nil {
return x.SemicolonSpan
}
return nil
}
// The body of a message, enum, or similar declaration, which
// itself contains declarations.
type Decl_Body struct {
state protoimpl.MessageState `protogen:"open.v1"`
Decls []*Decl `protobuf:"bytes,1,rep,name=decls,proto3" json:"decls,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Body) Reset() {
*x = Decl_Body{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Body) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Body) ProtoMessage() {}
func (x *Decl_Body) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Body.ProtoReflect.Descriptor instead.
func (*Decl_Body) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 4}
}
func (x *Decl_Body) GetDecls() []*Decl {
if x != nil {
return x.Decls
}
return nil
}
func (x *Decl_Body) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
// An extensions or reserved range within a message. Both productions are
// extremely similar, so they share an AST node.
type Decl_Range struct {
state protoimpl.MessageState `protogen:"open.v1"`
Kind Decl_Range_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=buf.compiler.v1alpha1.Decl_Range_Kind" json:"kind,omitempty"`
Ranges []*Expr `protobuf:"bytes,2,rep,name=ranges,proto3" json:"ranges,omitempty"`
Options *Options `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
KeywordSpan *Span `protobuf:"bytes,11,opt,name=keyword_span,json=keywordSpan,proto3" json:"keyword_span,omitempty"`
SemicolonSpan *Span `protobuf:"bytes,12,opt,name=semicolon_span,json=semicolonSpan,proto3" json:"semicolon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Decl_Range) Reset() {
*x = Decl_Range{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Decl_Range) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Decl_Range) ProtoMessage() {}
func (x *Decl_Range) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Decl_Range.ProtoReflect.Descriptor instead.
func (*Decl_Range) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{3, 5}
}
func (x *Decl_Range) GetKind() Decl_Range_Kind {
if x != nil {
return x.Kind
}
return Decl_Range_KIND_UNSPECIFIED
}
func (x *Decl_Range) GetRanges() []*Expr {
if x != nil {
return x.Ranges
}
return nil
}
func (x *Decl_Range) GetOptions() *Options {
if x != nil {
return x.Options
}
return nil
}
func (x *Decl_Range) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Decl_Range) GetKeywordSpan() *Span {
if x != nil {
return x.KeywordSpan
}
return nil
}
func (x *Decl_Range) GetSemicolonSpan() *Span {
if x != nil {
return x.SemicolonSpan
}
return nil
}
// A method signature. This appears on `KIND_METHOD`, for example.
type Def_Signature struct {
state protoimpl.MessageState `protogen:"open.v1"`
Inputs []*Type `protobuf:"bytes,1,rep,name=inputs,proto3" json:"inputs,omitempty"`
Outputs []*Type `protobuf:"bytes,2,rep,name=outputs,proto3" json:"outputs,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
InputSpan *Span `protobuf:"bytes,11,opt,name=input_span,json=inputSpan,proto3" json:"input_span,omitempty"`
ReturnsSpan *Span `protobuf:"bytes,12,opt,name=returns_span,json=returnsSpan,proto3" json:"returns_span,omitempty"`
OutputSpan *Span `protobuf:"bytes,13,opt,name=output_span,json=outputSpan,proto3" json:"output_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Def_Signature) Reset() {
*x = Def_Signature{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Def_Signature) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Def_Signature) ProtoMessage() {}
func (x *Def_Signature) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Def_Signature.ProtoReflect.Descriptor instead.
func (*Def_Signature) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{4, 0}
}
func (x *Def_Signature) GetInputs() []*Type {
if x != nil {
return x.Inputs
}
return nil
}
func (x *Def_Signature) GetOutputs() []*Type {
if x != nil {
return x.Outputs
}
return nil
}
func (x *Def_Signature) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Def_Signature) GetInputSpan() *Span {
if x != nil {
return x.InputSpan
}
return nil
}
func (x *Def_Signature) GetReturnsSpan() *Span {
if x != nil {
return x.ReturnsSpan
}
return nil
}
func (x *Def_Signature) GetOutputSpan() *Span {
if x != nil {
return x.OutputSpan
}
return nil
}
type Options_Entry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *Path `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
Value *Expr `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
EqualsSpan *Span `protobuf:"bytes,10,opt,name=equals_span,json=equalsSpan,proto3" json:"equals_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Options_Entry) Reset() {
*x = Options_Entry{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Options_Entry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Options_Entry) ProtoMessage() {}
func (x *Options_Entry) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Options_Entry.ProtoReflect.Descriptor instead.
func (*Options_Entry) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{5, 0}
}
func (x *Options_Entry) GetPath() *Path {
if x != nil {
return x.Path
}
return nil
}
func (x *Options_Entry) GetValue() *Expr {
if x != nil {
return x.Value
}
return nil
}
func (x *Options_Entry) GetEqualsSpan() *Span {
if x != nil {
return x.EqualsSpan
}
return nil
}
// A literal value: a number or a string.
type Expr_Literal struct {
state protoimpl.MessageState `protogen:"open.v1"`
// None of these may be set, in the case of an integer with an invalid or
// out-of-range format.
//
// Types that are valid to be assigned to Value:
//
// *Expr_Literal_IntValue
// *Expr_Literal_FloatValue
// *Expr_Literal_StringValue
Value isExpr_Literal_Value `protobuf_oneof:"value"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Literal) Reset() {
*x = Expr_Literal{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Literal) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Literal) ProtoMessage() {}
func (x *Expr_Literal) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Literal.ProtoReflect.Descriptor instead.
func (*Expr_Literal) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 0}
}
func (x *Expr_Literal) GetValue() isExpr_Literal_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *Expr_Literal) GetIntValue() uint64 {
if x != nil {
if x, ok := x.Value.(*Expr_Literal_IntValue); ok {
return x.IntValue
}
}
return 0
}
func (x *Expr_Literal) GetFloatValue() float64 {
if x != nil {
if x, ok := x.Value.(*Expr_Literal_FloatValue); ok {
return x.FloatValue
}
}
return 0
}
func (x *Expr_Literal) GetStringValue() string {
if x != nil {
if x, ok := x.Value.(*Expr_Literal_StringValue); ok {
return x.StringValue
}
}
return ""
}
func (x *Expr_Literal) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
type isExpr_Literal_Value interface {
isExpr_Literal_Value()
}
type Expr_Literal_IntValue struct {
IntValue uint64 `protobuf:"varint,1,opt,name=int_value,json=intValue,proto3,oneof"`
}
type Expr_Literal_FloatValue struct {
FloatValue float64 `protobuf:"fixed64,2,opt,name=float_value,json=floatValue,proto3,oneof"`
}
type Expr_Literal_StringValue struct {
StringValue string `protobuf:"bytes,3,opt,name=string_value,json=stringValue,proto3,oneof"`
}
func (*Expr_Literal_IntValue) isExpr_Literal_Value() {}
func (*Expr_Literal_FloatValue) isExpr_Literal_Value() {}
func (*Expr_Literal_StringValue) isExpr_Literal_Value() {}
// An expression with some kind of prefix, such as a minus sign.
type Expr_Prefixed struct {
state protoimpl.MessageState `protogen:"open.v1"`
Prefix Expr_Prefixed_Prefix `protobuf:"varint,1,opt,name=prefix,proto3,enum=buf.compiler.v1alpha1.Expr_Prefixed_Prefix" json:"prefix,omitempty"`
Expr *Expr `protobuf:"bytes,2,opt,name=expr,proto3" json:"expr,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
PrefixSpan *Span `protobuf:"bytes,11,opt,name=prefix_span,json=prefixSpan,proto3" json:"prefix_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Prefixed) Reset() {
*x = Expr_Prefixed{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Prefixed) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Prefixed) ProtoMessage() {}
func (x *Expr_Prefixed) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Prefixed.ProtoReflect.Descriptor instead.
func (*Expr_Prefixed) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 1}
}
func (x *Expr_Prefixed) GetPrefix() Expr_Prefixed_Prefix {
if x != nil {
return x.Prefix
}
return Expr_Prefixed_PREFIX_UNSPECIFIED
}
func (x *Expr_Prefixed) GetExpr() *Expr {
if x != nil {
return x.Expr
}
return nil
}
func (x *Expr_Prefixed) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Expr_Prefixed) GetPrefixSpan() *Span {
if x != nil {
return x.PrefixSpan
}
return nil
}
// A range expression, i.e. something like `1 to 10`. `1 to max` is not
// special syntax; `max` is realized as a path expression.
//
// Ranges are inclusive.
type Expr_Range struct {
state protoimpl.MessageState `protogen:"open.v1"`
Start *Expr `protobuf:"bytes,1,opt,name=start,proto3" json:"start,omitempty"`
End *Expr `protobuf:"bytes,2,opt,name=end,proto3" json:"end,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
ToSpan *Span `protobuf:"bytes,11,opt,name=to_span,json=toSpan,proto3" json:"to_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Range) Reset() {
*x = Expr_Range{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Range) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Range) ProtoMessage() {}
func (x *Expr_Range) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Range.ProtoReflect.Descriptor instead.
func (*Expr_Range) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 2}
}
func (x *Expr_Range) GetStart() *Expr {
if x != nil {
return x.Start
}
return nil
}
func (x *Expr_Range) GetEnd() *Expr {
if x != nil {
return x.End
}
return nil
}
func (x *Expr_Range) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Expr_Range) GetToSpan() *Span {
if x != nil {
return x.ToSpan
}
return nil
}
// An array literal, a sequence of expressions bound by square brackets.
type Expr_Array struct {
state protoimpl.MessageState `protogen:"open.v1"`
Elements []*Expr `protobuf:"bytes,1,rep,name=elements,proto3" json:"elements,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
OpenSpan *Span `protobuf:"bytes,11,opt,name=open_span,json=openSpan,proto3" json:"open_span,omitempty"`
CloseSpan *Span `protobuf:"bytes,12,opt,name=close_span,json=closeSpan,proto3" json:"close_span,omitempty"`
CommaSpans []*Span `protobuf:"bytes,13,rep,name=comma_spans,json=commaSpans,proto3" json:"comma_spans,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Array) Reset() {
*x = Expr_Array{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Array) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Array) ProtoMessage() {}
func (x *Expr_Array) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Array.ProtoReflect.Descriptor instead.
func (*Expr_Array) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 3}
}
func (x *Expr_Array) GetElements() []*Expr {
if x != nil {
return x.Elements
}
return nil
}
func (x *Expr_Array) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Expr_Array) GetOpenSpan() *Span {
if x != nil {
return x.OpenSpan
}
return nil
}
func (x *Expr_Array) GetCloseSpan() *Span {
if x != nil {
return x.CloseSpan
}
return nil
}
func (x *Expr_Array) GetCommaSpans() []*Span {
if x != nil {
return x.CommaSpans
}
return nil
}
// A dictionary literal, a sequence of key-value pairs bound by curly braces.
type Expr_Dict struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entries []*Expr_Field `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
OpenSpan *Span `protobuf:"bytes,11,opt,name=open_span,json=openSpan,proto3" json:"open_span,omitempty"`
CloseSpan *Span `protobuf:"bytes,12,opt,name=close_span,json=closeSpan,proto3" json:"close_span,omitempty"`
CommaSpans []*Span `protobuf:"bytes,13,rep,name=comma_spans,json=commaSpans,proto3" json:"comma_spans,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Dict) Reset() {
*x = Expr_Dict{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Dict) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Dict) ProtoMessage() {}
func (x *Expr_Dict) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Dict.ProtoReflect.Descriptor instead.
func (*Expr_Dict) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 4}
}
func (x *Expr_Dict) GetEntries() []*Expr_Field {
if x != nil {
return x.Entries
}
return nil
}
func (x *Expr_Dict) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Expr_Dict) GetOpenSpan() *Span {
if x != nil {
return x.OpenSpan
}
return nil
}
func (x *Expr_Dict) GetCloseSpan() *Span {
if x != nil {
return x.CloseSpan
}
return nil
}
func (x *Expr_Dict) GetCommaSpans() []*Span {
if x != nil {
return x.CommaSpans
}
return nil
}
// A key-value pair expression, which usually will appear inside of an
// `Expr.Dict`.
type Expr_Field struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key *Expr `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value *Expr `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
ColonSpan *Span `protobuf:"bytes,11,opt,name=colon_span,json=colonSpan,proto3" json:"colon_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr_Field) Reset() {
*x = Expr_Field{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr_Field) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr_Field) ProtoMessage() {}
func (x *Expr_Field) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Expr_Field.ProtoReflect.Descriptor instead.
func (*Expr_Field) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{6, 5}
}
func (x *Expr_Field) GetKey() *Expr {
if x != nil {
return x.Key
}
return nil
}
func (x *Expr_Field) GetValue() *Expr {
if x != nil {
return x.Value
}
return nil
}
func (x *Expr_Field) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Expr_Field) GetColonSpan() *Span {
if x != nil {
return x.ColonSpan
}
return nil
}
// A type with a modifier prefix in front of it, such as `repeated` or `stream`.
type Type_Prefixed struct {
state protoimpl.MessageState `protogen:"open.v1"`
Prefix Type_Prefixed_Prefix `protobuf:"varint,1,opt,name=prefix,proto3,enum=buf.compiler.v1alpha1.Type_Prefixed_Prefix" json:"prefix,omitempty"`
Type *Type `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
PrefixSpan *Span `protobuf:"bytes,11,opt,name=prefix_span,json=prefixSpan,proto3" json:"prefix_span,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Type_Prefixed) Reset() {
*x = Type_Prefixed{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Type_Prefixed) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Type_Prefixed) ProtoMessage() {}
func (x *Type_Prefixed) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Type_Prefixed.ProtoReflect.Descriptor instead.
func (*Type_Prefixed) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{7, 0}
}
func (x *Type_Prefixed) GetPrefix() Type_Prefixed_Prefix {
if x != nil {
return x.Prefix
}
return Type_Prefixed_PREFIX_UNSPECIFIED
}
func (x *Type_Prefixed) GetType() *Type {
if x != nil {
return x.Type
}
return nil
}
func (x *Type_Prefixed) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Type_Prefixed) GetPrefixSpan() *Span {
if x != nil {
return x.PrefixSpan
}
return nil
}
// A type with generic arguments, such as `map<string, int32>`.
//
// Note that no other generic types are part of Protobuf, but we support arbitrary generic
// types since it is a more natural way to define the AST.
type Type_Generic struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *Path `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
Args []*Type `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
Span *Span `protobuf:"bytes,10,opt,name=span,proto3" json:"span,omitempty"`
OpenSpan *Span `protobuf:"bytes,11,opt,name=open_span,json=openSpan,proto3" json:"open_span,omitempty"`
CloseSpan *Span `protobuf:"bytes,12,opt,name=close_span,json=closeSpan,proto3" json:"close_span,omitempty"`
CommaSpans []*Span `protobuf:"bytes,13,rep,name=comma_spans,json=commaSpans,proto3" json:"comma_spans,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Type_Generic) Reset() {
*x = Type_Generic{}
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Type_Generic) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Type_Generic) ProtoMessage() {}
func (x *Type_Generic) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_ast_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Type_Generic.ProtoReflect.Descriptor instead.
func (*Type_Generic) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP(), []int{7, 1}
}
func (x *Type_Generic) GetPath() *Path {
if x != nil {
return x.Path
}
return nil
}
func (x *Type_Generic) GetArgs() []*Type {
if x != nil {
return x.Args
}
return nil
}
func (x *Type_Generic) GetSpan() *Span {
if x != nil {
return x.Span
}
return nil
}
func (x *Type_Generic) GetOpenSpan() *Span {
if x != nil {
return x.OpenSpan
}
return nil
}
func (x *Type_Generic) GetCloseSpan() *Span {
if x != nil {
return x.CloseSpan
}
return nil
}
func (x *Type_Generic) GetCommaSpans() []*Span {
if x != nil {
return x.CommaSpans
}
return nil
}
var File_buf_compiler_v1alpha1_ast_proto protoreflect.FileDescriptor
const file_buf_compiler_v1alpha1_ast_proto_rawDesc = "" +
"\n" +
"\x1fbuf/compiler/v1alpha1/ast.proto\x12\x15buf.compiler.v1alpha1\x1a\"buf/compiler/v1alpha1/report.proto\"q\n" +
"\x04File\x126\n" +
"\x04file\x18\x01 \x01(\v2\".buf.compiler.v1alpha1.Report.FileR\x04file\x121\n" +
"\x05decls\x18\x02 \x03(\v2\x1b.buf.compiler.v1alpha1.DeclR\x05decls\".\n" +
"\x04Span\x12\x14\n" +
"\x05start\x18\x01 \x01(\rR\x05start\x12\x10\n" +
"\x03end\x18\x02 \x01(\rR\x03end\"\x95\x04\n" +
"\x04Path\x12E\n" +
"\n" +
"components\x18\x01 \x03(\v2%.buf.compiler.v1alpha1.Path.ComponentR\n" +
"components\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x1a\x94\x03\n" +
"\tComponent\x12\x16\n" +
"\x05ident\x18\x01 \x01(\tH\x00R\x05ident\x12;\n" +
"\textension\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.PathH\x00R\textension\x12M\n" +
"\tseparator\x18\x03 \x01(\x0e2/.buf.compiler.v1alpha1.Path.Component.SeparatorR\tseparator\x12B\n" +
"\x0ecomponent_span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rcomponentSpan\x12B\n" +
"\x0eseparator_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rseparatorSpan\"N\n" +
"\tSeparator\x12\x19\n" +
"\x15SEPARATOR_UNSPECIFIED\x10\x00\x12\x11\n" +
"\rSEPARATOR_DOT\x10\x01\x12\x13\n" +
"\x0fSEPARATOR_SLASH\x10\x02B\v\n" +
"\tcomponent\"\xec\x12\n" +
"\x04Decl\x129\n" +
"\x05empty\x18\x01 \x01(\v2!.buf.compiler.v1alpha1.Decl.EmptyH\x00R\x05empty\x12<\n" +
"\x06syntax\x18\x02 \x01(\v2\".buf.compiler.v1alpha1.Decl.SyntaxH\x00R\x06syntax\x12<\n" +
"\x06import\x18\x03 \x01(\v2\".buf.compiler.v1alpha1.Decl.ImportH\x00R\x06import\x12?\n" +
"\apackage\x18\x04 \x01(\v2#.buf.compiler.v1alpha1.Decl.PackageH\x00R\apackage\x12.\n" +
"\x03def\x18\x05 \x01(\v2\x1a.buf.compiler.v1alpha1.DefH\x00R\x03def\x126\n" +
"\x04body\x18\x06 \x01(\v2 .buf.compiler.v1alpha1.Decl.BodyH\x00R\x04body\x129\n" +
"\x05range\x18\a \x01(\v2!.buf.compiler.v1alpha1.Decl.RangeH\x00R\x05range\x1a8\n" +
"\x05Empty\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x1a\xe6\x03\n" +
"\x06Syntax\x12;\n" +
"\x04kind\x18\x01 \x01(\x0e2'.buf.compiler.v1alpha1.Decl.Syntax.KindR\x04kind\x121\n" +
"\x05value\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x05value\x128\n" +
"\aoptions\x18\x03 \x01(\v2\x1e.buf.compiler.v1alpha1.OptionsR\aoptions\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12>\n" +
"\fkeyword_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vkeywordSpan\x12<\n" +
"\vequals_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"equalsSpan\x12B\n" +
"\x0esemicolon_span\x18\r \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rsemicolonSpan\"?\n" +
"\x04Kind\x12\x14\n" +
"\x10KIND_UNSPECIFIED\x10\x00\x12\x0f\n" +
"\vKIND_SYNTAX\x10\x01\x12\x10\n" +
"\fKIND_EDITION\x10\x02\x1a\xa9\x02\n" +
"\aPackage\x12/\n" +
"\x04path\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.PathR\x04path\x128\n" +
"\aoptions\x18\x02 \x01(\v2\x1e.buf.compiler.v1alpha1.OptionsR\aoptions\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12>\n" +
"\fkeyword_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vkeywordSpan\x12B\n" +
"\x0esemicolon_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rsemicolonSpan\x1a\xd5\x04\n" +
"\x06Import\x12G\n" +
"\bmodifier\x18\x01 \x03(\x0e2+.buf.compiler.v1alpha1.Decl.Import.ModifierR\bmodifier\x12<\n" +
"\vimport_path\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\n" +
"importPath\x128\n" +
"\aoptions\x18\x03 \x01(\v2\x1e.buf.compiler.v1alpha1.OptionsR\aoptions\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12>\n" +
"\fkeyword_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vkeywordSpan\x12@\n" +
"\rmodifier_span\x18\f \x03(\v2\x1b.buf.compiler.v1alpha1.SpanR\fmodifierSpan\x12E\n" +
"\x10import_path_span\x18\r \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x0eimportPathSpan\x12B\n" +
"\x0esemicolon_span\x18\x0e \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rsemicolonSpan\"L\n" +
"\bModifier\x12\x18\n" +
"\x14MODIFIER_UNSPECIFIED\x10\x00\x12\x11\n" +
"\rMODIFIER_WEAK\x10\x01\x12\x13\n" +
"\x0fMODIFIER_PUBLIC\x10\x02\x1aj\n" +
"\x04Body\x121\n" +
"\x05decls\x18\x01 \x03(\v2\x1b.buf.compiler.v1alpha1.DeclR\x05decls\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x1a\xad\x03\n" +
"\x05Range\x12:\n" +
"\x04kind\x18\x01 \x01(\x0e2&.buf.compiler.v1alpha1.Decl.Range.KindR\x04kind\x123\n" +
"\x06ranges\x18\x02 \x03(\v2\x1b.buf.compiler.v1alpha1.ExprR\x06ranges\x128\n" +
"\aoptions\x18\x03 \x01(\v2\x1e.buf.compiler.v1alpha1.OptionsR\aoptions\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12>\n" +
"\fkeyword_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vkeywordSpan\x12B\n" +
"\x0esemicolon_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rsemicolonSpan\"D\n" +
"\x04Kind\x12\x14\n" +
"\x10KIND_UNSPECIFIED\x10\x00\x12\x13\n" +
"\x0fKIND_EXTENSIONS\x10\x01\x12\x11\n" +
"\rKIND_RESERVED\x10\x02B\x06\n" +
"\x04decl\"\xa5\t\n" +
"\x03Def\x123\n" +
"\x04kind\x18\x01 \x01(\x0e2\x1f.buf.compiler.v1alpha1.Def.KindR\x04kind\x12/\n" +
"\x04name\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.PathR\x04name\x12/\n" +
"\x04type\x18\x03 \x01(\v2\x1b.buf.compiler.v1alpha1.TypeR\x04type\x12B\n" +
"\tsignature\x18\x04 \x01(\v2$.buf.compiler.v1alpha1.Def.SignatureR\tsignature\x121\n" +
"\x05value\x18\x05 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x05value\x128\n" +
"\aoptions\x18\x06 \x01(\v2\x1e.buf.compiler.v1alpha1.OptionsR\aoptions\x124\n" +
"\x04body\x18\a \x01(\v2 .buf.compiler.v1alpha1.Decl.BodyR\x04body\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12>\n" +
"\fkeyword_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vkeywordSpan\x12<\n" +
"\vequals_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"equalsSpan\x12B\n" +
"\x0esemicolon_span\x18\r \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\rsemicolonSpan\x1a\xe2\x02\n" +
"\tSignature\x123\n" +
"\x06inputs\x18\x01 \x03(\v2\x1b.buf.compiler.v1alpha1.TypeR\x06inputs\x125\n" +
"\aoutputs\x18\x02 \x03(\v2\x1b.buf.compiler.v1alpha1.TypeR\aoutputs\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12:\n" +
"\n" +
"input_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\tinputSpan\x12>\n" +
"\freturns_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\vreturnsSpan\x12<\n" +
"\voutput_span\x18\r \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"outputSpan\"\xc7\x01\n" +
"\x04Kind\x12\x14\n" +
"\x10KIND_UNSPECIFIED\x10\x00\x12\x10\n" +
"\fKIND_MESSAGE\x10\x01\x12\r\n" +
"\tKIND_ENUM\x10\x02\x12\x10\n" +
"\fKIND_SERVICE\x10\x03\x12\x0f\n" +
"\vKIND_EXTEND\x10\x04\x12\x0e\n" +
"\n" +
"KIND_FIELD\x10\x05\x12\x13\n" +
"\x0fKIND_ENUM_VALUE\x10\x06\x12\x0e\n" +
"\n" +
"KIND_ONEOF\x10\a\x12\x0e\n" +
"\n" +
"KIND_GROUP\x10\b\x12\x0f\n" +
"\vKIND_METHOD\x10\t\x12\x0f\n" +
"\vKIND_OPTION\x10\n" +
"\"\xa6\x02\n" +
"\aOptions\x12>\n" +
"\aentries\x18\x01 \x03(\v2$.buf.compiler.v1alpha1.Options.EntryR\aentries\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x1a\xa9\x01\n" +
"\x05Entry\x12/\n" +
"\x04path\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.PathR\x04path\x121\n" +
"\x05value\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x05value\x12<\n" +
"\vequals_span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"equalsSpan\"\x81\x0f\n" +
"\x04Expr\x12?\n" +
"\aliteral\x18\x01 \x01(\v2#.buf.compiler.v1alpha1.Expr.LiteralH\x00R\aliteral\x121\n" +
"\x04path\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.PathH\x00R\x04path\x12B\n" +
"\bprefixed\x18\x03 \x01(\v2$.buf.compiler.v1alpha1.Expr.PrefixedH\x00R\bprefixed\x129\n" +
"\x05range\x18\x04 \x01(\v2!.buf.compiler.v1alpha1.Expr.RangeH\x00R\x05range\x129\n" +
"\x05array\x18\x05 \x01(\v2!.buf.compiler.v1alpha1.Expr.ArrayH\x00R\x05array\x126\n" +
"\x04dict\x18\x06 \x01(\v2 .buf.compiler.v1alpha1.Expr.DictH\x00R\x04dict\x129\n" +
"\x05field\x18\a \x01(\v2!.buf.compiler.v1alpha1.Expr.FieldH\x00R\x05field\x1a\xaa\x01\n" +
"\aLiteral\x12\x1d\n" +
"\tint_value\x18\x01 \x01(\x04H\x00R\bintValue\x12!\n" +
"\vfloat_value\x18\x02 \x01(\x01H\x00R\n" +
"floatValue\x12#\n" +
"\fstring_value\x18\x03 \x01(\tH\x00R\vstringValue\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04spanB\a\n" +
"\x05value\x1a\xa3\x02\n" +
"\bPrefixed\x12C\n" +
"\x06prefix\x18\x01 \x01(\x0e2+.buf.compiler.v1alpha1.Expr.Prefixed.PrefixR\x06prefix\x12/\n" +
"\x04expr\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x04expr\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12<\n" +
"\vprefix_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"prefixSpan\"2\n" +
"\x06Prefix\x12\x16\n" +
"\x12PREFIX_UNSPECIFIED\x10\x00\x12\x10\n" +
"\fPREFIX_MINUS\x10\x01\x1a\xd0\x01\n" +
"\x05Range\x121\n" +
"\x05start\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x05start\x12-\n" +
"\x03end\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x03end\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x124\n" +
"\ato_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x06toSpan\x1a\xa5\x02\n" +
"\x05Array\x127\n" +
"\belements\x18\x01 \x03(\v2\x1b.buf.compiler.v1alpha1.ExprR\belements\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x128\n" +
"\topen_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\bopenSpan\x12:\n" +
"\n" +
"close_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\tcloseSpan\x12<\n" +
"\vcomma_spans\x18\r \x03(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"commaSpans\x1a\xa8\x02\n" +
"\x04Dict\x12;\n" +
"\aentries\x18\x01 \x03(\v2!.buf.compiler.v1alpha1.Expr.FieldR\aentries\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x128\n" +
"\topen_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\bopenSpan\x12:\n" +
"\n" +
"close_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\tcloseSpan\x12<\n" +
"\vcomma_spans\x18\r \x03(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"commaSpans\x1a\xd6\x01\n" +
"\x05Field\x12-\n" +
"\x03key\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x03key\x121\n" +
"\x05value\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.ExprR\x05value\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12:\n" +
"\n" +
"colon_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\tcolonSpanB\x06\n" +
"\x04expr\"\xff\x06\n" +
"\x04Type\x121\n" +
"\x04path\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.PathH\x00R\x04path\x12B\n" +
"\bprefixed\x18\x02 \x01(\v2$.buf.compiler.v1alpha1.Type.PrefixedH\x00R\bprefixed\x12?\n" +
"\ageneric\x18\x03 \x01(\v2#.buf.compiler.v1alpha1.Type.GenericH\x00R\ageneric\x1a\xe3\x02\n" +
"\bPrefixed\x12C\n" +
"\x06prefix\x18\x01 \x01(\x0e2+.buf.compiler.v1alpha1.Type.Prefixed.PrefixR\x06prefix\x12/\n" +
"\x04type\x18\x02 \x01(\v2\x1b.buf.compiler.v1alpha1.TypeR\x04type\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x12<\n" +
"\vprefix_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"prefixSpan\"r\n" +
"\x06Prefix\x12\x16\n" +
"\x12PREFIX_UNSPECIFIED\x10\x00\x12\x13\n" +
"\x0fPREFIX_OPTIONAL\x10\x01\x12\x13\n" +
"\x0fPREFIX_REPEATED\x10\x02\x12\x13\n" +
"\x0fPREFIX_REQUIRED\x10\x03\x12\x11\n" +
"\rPREFIX_STREAM\x10\x04\x1a\xd0\x02\n" +
"\aGeneric\x12/\n" +
"\x04path\x18\x01 \x01(\v2\x1b.buf.compiler.v1alpha1.PathR\x04path\x12/\n" +
"\x04args\x18\x02 \x03(\v2\x1b.buf.compiler.v1alpha1.TypeR\x04args\x12/\n" +
"\x04span\x18\n" +
" \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\x04span\x128\n" +
"\topen_span\x18\v \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\bopenSpan\x12:\n" +
"\n" +
"close_span\x18\f \x01(\v2\x1b.buf.compiler.v1alpha1.SpanR\tcloseSpan\x12<\n" +
"\vcomma_spans\x18\r \x03(\v2\x1b.buf.compiler.v1alpha1.SpanR\n" +
"commaSpansB\x06\n" +
"\x04typeB\xf1\x01\n" +
"\x19com.buf.compiler.v1alpha1B\bAstProtoP\x01ZTgithub.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1;compilerv1alpha1\xa2\x02\x03BCX\xaa\x02\x15Buf.Compiler.V1alpha1\xca\x02\x15Buf\\Compiler\\V1alpha1\xe2\x02!Buf\\Compiler\\V1alpha1\\GPBMetadata\xea\x02\x17Buf::Compiler::V1alpha1b\x06proto3"
var (
file_buf_compiler_v1alpha1_ast_proto_rawDescOnce sync.Once
file_buf_compiler_v1alpha1_ast_proto_rawDescData []byte
)
func file_buf_compiler_v1alpha1_ast_proto_rawDescGZIP() []byte {
file_buf_compiler_v1alpha1_ast_proto_rawDescOnce.Do(func() {
file_buf_compiler_v1alpha1_ast_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_ast_proto_rawDesc), len(file_buf_compiler_v1alpha1_ast_proto_rawDesc)))
})
return file_buf_compiler_v1alpha1_ast_proto_rawDescData
}
var file_buf_compiler_v1alpha1_ast_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
var file_buf_compiler_v1alpha1_ast_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
var file_buf_compiler_v1alpha1_ast_proto_goTypes = []any{
(Path_Component_Separator)(0), // 0: buf.compiler.v1alpha1.Path.Component.Separator
(Decl_Syntax_Kind)(0), // 1: buf.compiler.v1alpha1.Decl.Syntax.Kind
(Decl_Import_Modifier)(0), // 2: buf.compiler.v1alpha1.Decl.Import.Modifier
(Decl_Range_Kind)(0), // 3: buf.compiler.v1alpha1.Decl.Range.Kind
(Def_Kind)(0), // 4: buf.compiler.v1alpha1.Def.Kind
(Expr_Prefixed_Prefix)(0), // 5: buf.compiler.v1alpha1.Expr.Prefixed.Prefix
(Type_Prefixed_Prefix)(0), // 6: buf.compiler.v1alpha1.Type.Prefixed.Prefix
(*File)(nil), // 7: buf.compiler.v1alpha1.File
(*Span)(nil), // 8: buf.compiler.v1alpha1.Span
(*Path)(nil), // 9: buf.compiler.v1alpha1.Path
(*Decl)(nil), // 10: buf.compiler.v1alpha1.Decl
(*Def)(nil), // 11: buf.compiler.v1alpha1.Def
(*Options)(nil), // 12: buf.compiler.v1alpha1.Options
(*Expr)(nil), // 13: buf.compiler.v1alpha1.Expr
(*Type)(nil), // 14: buf.compiler.v1alpha1.Type
(*Path_Component)(nil), // 15: buf.compiler.v1alpha1.Path.Component
(*Decl_Empty)(nil), // 16: buf.compiler.v1alpha1.Decl.Empty
(*Decl_Syntax)(nil), // 17: buf.compiler.v1alpha1.Decl.Syntax
(*Decl_Package)(nil), // 18: buf.compiler.v1alpha1.Decl.Package
(*Decl_Import)(nil), // 19: buf.compiler.v1alpha1.Decl.Import
(*Decl_Body)(nil), // 20: buf.compiler.v1alpha1.Decl.Body
(*Decl_Range)(nil), // 21: buf.compiler.v1alpha1.Decl.Range
(*Def_Signature)(nil), // 22: buf.compiler.v1alpha1.Def.Signature
(*Options_Entry)(nil), // 23: buf.compiler.v1alpha1.Options.Entry
(*Expr_Literal)(nil), // 24: buf.compiler.v1alpha1.Expr.Literal
(*Expr_Prefixed)(nil), // 25: buf.compiler.v1alpha1.Expr.Prefixed
(*Expr_Range)(nil), // 26: buf.compiler.v1alpha1.Expr.Range
(*Expr_Array)(nil), // 27: buf.compiler.v1alpha1.Expr.Array
(*Expr_Dict)(nil), // 28: buf.compiler.v1alpha1.Expr.Dict
(*Expr_Field)(nil), // 29: buf.compiler.v1alpha1.Expr.Field
(*Type_Prefixed)(nil), // 30: buf.compiler.v1alpha1.Type.Prefixed
(*Type_Generic)(nil), // 31: buf.compiler.v1alpha1.Type.Generic
(*Report_File)(nil), // 32: buf.compiler.v1alpha1.Report.File
}
var file_buf_compiler_v1alpha1_ast_proto_depIdxs = []int32{
32, // 0: buf.compiler.v1alpha1.File.file:type_name -> buf.compiler.v1alpha1.Report.File
10, // 1: buf.compiler.v1alpha1.File.decls:type_name -> buf.compiler.v1alpha1.Decl
15, // 2: buf.compiler.v1alpha1.Path.components:type_name -> buf.compiler.v1alpha1.Path.Component
8, // 3: buf.compiler.v1alpha1.Path.span:type_name -> buf.compiler.v1alpha1.Span
16, // 4: buf.compiler.v1alpha1.Decl.empty:type_name -> buf.compiler.v1alpha1.Decl.Empty
17, // 5: buf.compiler.v1alpha1.Decl.syntax:type_name -> buf.compiler.v1alpha1.Decl.Syntax
19, // 6: buf.compiler.v1alpha1.Decl.import:type_name -> buf.compiler.v1alpha1.Decl.Import
18, // 7: buf.compiler.v1alpha1.Decl.package:type_name -> buf.compiler.v1alpha1.Decl.Package
11, // 8: buf.compiler.v1alpha1.Decl.def:type_name -> buf.compiler.v1alpha1.Def
20, // 9: buf.compiler.v1alpha1.Decl.body:type_name -> buf.compiler.v1alpha1.Decl.Body
21, // 10: buf.compiler.v1alpha1.Decl.range:type_name -> buf.compiler.v1alpha1.Decl.Range
4, // 11: buf.compiler.v1alpha1.Def.kind:type_name -> buf.compiler.v1alpha1.Def.Kind
9, // 12: buf.compiler.v1alpha1.Def.name:type_name -> buf.compiler.v1alpha1.Path
14, // 13: buf.compiler.v1alpha1.Def.type:type_name -> buf.compiler.v1alpha1.Type
22, // 14: buf.compiler.v1alpha1.Def.signature:type_name -> buf.compiler.v1alpha1.Def.Signature
13, // 15: buf.compiler.v1alpha1.Def.value:type_name -> buf.compiler.v1alpha1.Expr
12, // 16: buf.compiler.v1alpha1.Def.options:type_name -> buf.compiler.v1alpha1.Options
20, // 17: buf.compiler.v1alpha1.Def.body:type_name -> buf.compiler.v1alpha1.Decl.Body
8, // 18: buf.compiler.v1alpha1.Def.span:type_name -> buf.compiler.v1alpha1.Span
8, // 19: buf.compiler.v1alpha1.Def.keyword_span:type_name -> buf.compiler.v1alpha1.Span
8, // 20: buf.compiler.v1alpha1.Def.equals_span:type_name -> buf.compiler.v1alpha1.Span
8, // 21: buf.compiler.v1alpha1.Def.semicolon_span:type_name -> buf.compiler.v1alpha1.Span
23, // 22: buf.compiler.v1alpha1.Options.entries:type_name -> buf.compiler.v1alpha1.Options.Entry
8, // 23: buf.compiler.v1alpha1.Options.span:type_name -> buf.compiler.v1alpha1.Span
24, // 24: buf.compiler.v1alpha1.Expr.literal:type_name -> buf.compiler.v1alpha1.Expr.Literal
9, // 25: buf.compiler.v1alpha1.Expr.path:type_name -> buf.compiler.v1alpha1.Path
25, // 26: buf.compiler.v1alpha1.Expr.prefixed:type_name -> buf.compiler.v1alpha1.Expr.Prefixed
26, // 27: buf.compiler.v1alpha1.Expr.range:type_name -> buf.compiler.v1alpha1.Expr.Range
27, // 28: buf.compiler.v1alpha1.Expr.array:type_name -> buf.compiler.v1alpha1.Expr.Array
28, // 29: buf.compiler.v1alpha1.Expr.dict:type_name -> buf.compiler.v1alpha1.Expr.Dict
29, // 30: buf.compiler.v1alpha1.Expr.field:type_name -> buf.compiler.v1alpha1.Expr.Field
9, // 31: buf.compiler.v1alpha1.Type.path:type_name -> buf.compiler.v1alpha1.Path
30, // 32: buf.compiler.v1alpha1.Type.prefixed:type_name -> buf.compiler.v1alpha1.Type.Prefixed
31, // 33: buf.compiler.v1alpha1.Type.generic:type_name -> buf.compiler.v1alpha1.Type.Generic
9, // 34: buf.compiler.v1alpha1.Path.Component.extension:type_name -> buf.compiler.v1alpha1.Path
0, // 35: buf.compiler.v1alpha1.Path.Component.separator:type_name -> buf.compiler.v1alpha1.Path.Component.Separator
8, // 36: buf.compiler.v1alpha1.Path.Component.component_span:type_name -> buf.compiler.v1alpha1.Span
8, // 37: buf.compiler.v1alpha1.Path.Component.separator_span:type_name -> buf.compiler.v1alpha1.Span
8, // 38: buf.compiler.v1alpha1.Decl.Empty.span:type_name -> buf.compiler.v1alpha1.Span
1, // 39: buf.compiler.v1alpha1.Decl.Syntax.kind:type_name -> buf.compiler.v1alpha1.Decl.Syntax.Kind
13, // 40: buf.compiler.v1alpha1.Decl.Syntax.value:type_name -> buf.compiler.v1alpha1.Expr
12, // 41: buf.compiler.v1alpha1.Decl.Syntax.options:type_name -> buf.compiler.v1alpha1.Options
8, // 42: buf.compiler.v1alpha1.Decl.Syntax.span:type_name -> buf.compiler.v1alpha1.Span
8, // 43: buf.compiler.v1alpha1.Decl.Syntax.keyword_span:type_name -> buf.compiler.v1alpha1.Span
8, // 44: buf.compiler.v1alpha1.Decl.Syntax.equals_span:type_name -> buf.compiler.v1alpha1.Span
8, // 45: buf.compiler.v1alpha1.Decl.Syntax.semicolon_span:type_name -> buf.compiler.v1alpha1.Span
9, // 46: buf.compiler.v1alpha1.Decl.Package.path:type_name -> buf.compiler.v1alpha1.Path
12, // 47: buf.compiler.v1alpha1.Decl.Package.options:type_name -> buf.compiler.v1alpha1.Options
8, // 48: buf.compiler.v1alpha1.Decl.Package.span:type_name -> buf.compiler.v1alpha1.Span
8, // 49: buf.compiler.v1alpha1.Decl.Package.keyword_span:type_name -> buf.compiler.v1alpha1.Span
8, // 50: buf.compiler.v1alpha1.Decl.Package.semicolon_span:type_name -> buf.compiler.v1alpha1.Span
2, // 51: buf.compiler.v1alpha1.Decl.Import.modifier:type_name -> buf.compiler.v1alpha1.Decl.Import.Modifier
13, // 52: buf.compiler.v1alpha1.Decl.Import.import_path:type_name -> buf.compiler.v1alpha1.Expr
12, // 53: buf.compiler.v1alpha1.Decl.Import.options:type_name -> buf.compiler.v1alpha1.Options
8, // 54: buf.compiler.v1alpha1.Decl.Import.span:type_name -> buf.compiler.v1alpha1.Span
8, // 55: buf.compiler.v1alpha1.Decl.Import.keyword_span:type_name -> buf.compiler.v1alpha1.Span
8, // 56: buf.compiler.v1alpha1.Decl.Import.modifier_span:type_name -> buf.compiler.v1alpha1.Span
8, // 57: buf.compiler.v1alpha1.Decl.Import.import_path_span:type_name -> buf.compiler.v1alpha1.Span
8, // 58: buf.compiler.v1alpha1.Decl.Import.semicolon_span:type_name -> buf.compiler.v1alpha1.Span
10, // 59: buf.compiler.v1alpha1.Decl.Body.decls:type_name -> buf.compiler.v1alpha1.Decl
8, // 60: buf.compiler.v1alpha1.Decl.Body.span:type_name -> buf.compiler.v1alpha1.Span
3, // 61: buf.compiler.v1alpha1.Decl.Range.kind:type_name -> buf.compiler.v1alpha1.Decl.Range.Kind
13, // 62: buf.compiler.v1alpha1.Decl.Range.ranges:type_name -> buf.compiler.v1alpha1.Expr
12, // 63: buf.compiler.v1alpha1.Decl.Range.options:type_name -> buf.compiler.v1alpha1.Options
8, // 64: buf.compiler.v1alpha1.Decl.Range.span:type_name -> buf.compiler.v1alpha1.Span
8, // 65: buf.compiler.v1alpha1.Decl.Range.keyword_span:type_name -> buf.compiler.v1alpha1.Span
8, // 66: buf.compiler.v1alpha1.Decl.Range.semicolon_span:type_name -> buf.compiler.v1alpha1.Span
14, // 67: buf.compiler.v1alpha1.Def.Signature.inputs:type_name -> buf.compiler.v1alpha1.Type
14, // 68: buf.compiler.v1alpha1.Def.Signature.outputs:type_name -> buf.compiler.v1alpha1.Type
8, // 69: buf.compiler.v1alpha1.Def.Signature.span:type_name -> buf.compiler.v1alpha1.Span
8, // 70: buf.compiler.v1alpha1.Def.Signature.input_span:type_name -> buf.compiler.v1alpha1.Span
8, // 71: buf.compiler.v1alpha1.Def.Signature.returns_span:type_name -> buf.compiler.v1alpha1.Span
8, // 72: buf.compiler.v1alpha1.Def.Signature.output_span:type_name -> buf.compiler.v1alpha1.Span
9, // 73: buf.compiler.v1alpha1.Options.Entry.path:type_name -> buf.compiler.v1alpha1.Path
13, // 74: buf.compiler.v1alpha1.Options.Entry.value:type_name -> buf.compiler.v1alpha1.Expr
8, // 75: buf.compiler.v1alpha1.Options.Entry.equals_span:type_name -> buf.compiler.v1alpha1.Span
8, // 76: buf.compiler.v1alpha1.Expr.Literal.span:type_name -> buf.compiler.v1alpha1.Span
5, // 77: buf.compiler.v1alpha1.Expr.Prefixed.prefix:type_name -> buf.compiler.v1alpha1.Expr.Prefixed.Prefix
13, // 78: buf.compiler.v1alpha1.Expr.Prefixed.expr:type_name -> buf.compiler.v1alpha1.Expr
8, // 79: buf.compiler.v1alpha1.Expr.Prefixed.span:type_name -> buf.compiler.v1alpha1.Span
8, // 80: buf.compiler.v1alpha1.Expr.Prefixed.prefix_span:type_name -> buf.compiler.v1alpha1.Span
13, // 81: buf.compiler.v1alpha1.Expr.Range.start:type_name -> buf.compiler.v1alpha1.Expr
13, // 82: buf.compiler.v1alpha1.Expr.Range.end:type_name -> buf.compiler.v1alpha1.Expr
8, // 83: buf.compiler.v1alpha1.Expr.Range.span:type_name -> buf.compiler.v1alpha1.Span
8, // 84: buf.compiler.v1alpha1.Expr.Range.to_span:type_name -> buf.compiler.v1alpha1.Span
13, // 85: buf.compiler.v1alpha1.Expr.Array.elements:type_name -> buf.compiler.v1alpha1.Expr
8, // 86: buf.compiler.v1alpha1.Expr.Array.span:type_name -> buf.compiler.v1alpha1.Span
8, // 87: buf.compiler.v1alpha1.Expr.Array.open_span:type_name -> buf.compiler.v1alpha1.Span
8, // 88: buf.compiler.v1alpha1.Expr.Array.close_span:type_name -> buf.compiler.v1alpha1.Span
8, // 89: buf.compiler.v1alpha1.Expr.Array.comma_spans:type_name -> buf.compiler.v1alpha1.Span
29, // 90: buf.compiler.v1alpha1.Expr.Dict.entries:type_name -> buf.compiler.v1alpha1.Expr.Field
8, // 91: buf.compiler.v1alpha1.Expr.Dict.span:type_name -> buf.compiler.v1alpha1.Span
8, // 92: buf.compiler.v1alpha1.Expr.Dict.open_span:type_name -> buf.compiler.v1alpha1.Span
8, // 93: buf.compiler.v1alpha1.Expr.Dict.close_span:type_name -> buf.compiler.v1alpha1.Span
8, // 94: buf.compiler.v1alpha1.Expr.Dict.comma_spans:type_name -> buf.compiler.v1alpha1.Span
13, // 95: buf.compiler.v1alpha1.Expr.Field.key:type_name -> buf.compiler.v1alpha1.Expr
13, // 96: buf.compiler.v1alpha1.Expr.Field.value:type_name -> buf.compiler.v1alpha1.Expr
8, // 97: buf.compiler.v1alpha1.Expr.Field.span:type_name -> buf.compiler.v1alpha1.Span
8, // 98: buf.compiler.v1alpha1.Expr.Field.colon_span:type_name -> buf.compiler.v1alpha1.Span
6, // 99: buf.compiler.v1alpha1.Type.Prefixed.prefix:type_name -> buf.compiler.v1alpha1.Type.Prefixed.Prefix
14, // 100: buf.compiler.v1alpha1.Type.Prefixed.type:type_name -> buf.compiler.v1alpha1.Type
8, // 101: buf.compiler.v1alpha1.Type.Prefixed.span:type_name -> buf.compiler.v1alpha1.Span
8, // 102: buf.compiler.v1alpha1.Type.Prefixed.prefix_span:type_name -> buf.compiler.v1alpha1.Span
9, // 103: buf.compiler.v1alpha1.Type.Generic.path:type_name -> buf.compiler.v1alpha1.Path
14, // 104: buf.compiler.v1alpha1.Type.Generic.args:type_name -> buf.compiler.v1alpha1.Type
8, // 105: buf.compiler.v1alpha1.Type.Generic.span:type_name -> buf.compiler.v1alpha1.Span
8, // 106: buf.compiler.v1alpha1.Type.Generic.open_span:type_name -> buf.compiler.v1alpha1.Span
8, // 107: buf.compiler.v1alpha1.Type.Generic.close_span:type_name -> buf.compiler.v1alpha1.Span
8, // 108: buf.compiler.v1alpha1.Type.Generic.comma_spans:type_name -> buf.compiler.v1alpha1.Span
109, // [109:109] is the sub-list for method output_type
109, // [109:109] is the sub-list for method input_type
109, // [109:109] is the sub-list for extension type_name
109, // [109:109] is the sub-list for extension extendee
0, // [0:109] is the sub-list for field type_name
}
func init() { file_buf_compiler_v1alpha1_ast_proto_init() }
func file_buf_compiler_v1alpha1_ast_proto_init() {
if File_buf_compiler_v1alpha1_ast_proto != nil {
return
}
file_buf_compiler_v1alpha1_report_proto_init()
file_buf_compiler_v1alpha1_ast_proto_msgTypes[3].OneofWrappers = []any{
(*Decl_Empty_)(nil),
(*Decl_Syntax_)(nil),
(*Decl_Import_)(nil),
(*Decl_Package_)(nil),
(*Decl_Def)(nil),
(*Decl_Body_)(nil),
(*Decl_Range_)(nil),
}
file_buf_compiler_v1alpha1_ast_proto_msgTypes[6].OneofWrappers = []any{
(*Expr_Literal_)(nil),
(*Expr_Path)(nil),
(*Expr_Prefixed_)(nil),
(*Expr_Range_)(nil),
(*Expr_Array_)(nil),
(*Expr_Dict_)(nil),
(*Expr_Field_)(nil),
}
file_buf_compiler_v1alpha1_ast_proto_msgTypes[7].OneofWrappers = []any{
(*Type_Path)(nil),
(*Type_Prefixed_)(nil),
(*Type_Generic_)(nil),
}
file_buf_compiler_v1alpha1_ast_proto_msgTypes[8].OneofWrappers = []any{
(*Path_Component_Ident)(nil),
(*Path_Component_Extension)(nil),
}
file_buf_compiler_v1alpha1_ast_proto_msgTypes[17].OneofWrappers = []any{
(*Expr_Literal_IntValue)(nil),
(*Expr_Literal_FloatValue)(nil),
(*Expr_Literal_StringValue)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_ast_proto_rawDesc), len(file_buf_compiler_v1alpha1_ast_proto_rawDesc)),
NumEnums: 7,
NumMessages: 25,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_buf_compiler_v1alpha1_ast_proto_goTypes,
DependencyIndexes: file_buf_compiler_v1alpha1_ast_proto_depIdxs,
EnumInfos: file_buf_compiler_v1alpha1_ast_proto_enumTypes,
MessageInfos: file_buf_compiler_v1alpha1_ast_proto_msgTypes,
}.Build()
File_buf_compiler_v1alpha1_ast_proto = out.File
file_buf_compiler_v1alpha1_ast_proto_goTypes = nil
file_buf_compiler_v1alpha1_ast_proto_depIdxs = nil
}
// 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 protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: buf/compiler/v1alpha1/report.proto
package compilerv1alpha1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// A diagnostic level. This affects how (and whether) it is shown to users.
type Diagnostic_Level int32
const (
Diagnostic_LEVEL_UNSPECIFIED Diagnostic_Level = 0
Diagnostic_LEVEL_ICE Diagnostic_Level = 1
Diagnostic_LEVEL_ERROR Diagnostic_Level = 2
Diagnostic_LEVEL_WARNING Diagnostic_Level = 3
Diagnostic_LEVEL_REMARK Diagnostic_Level = 4
)
// Enum value maps for Diagnostic_Level.
var (
Diagnostic_Level_name = map[int32]string{
0: "LEVEL_UNSPECIFIED",
1: "LEVEL_ICE",
2: "LEVEL_ERROR",
3: "LEVEL_WARNING",
4: "LEVEL_REMARK",
}
Diagnostic_Level_value = map[string]int32{
"LEVEL_UNSPECIFIED": 0,
"LEVEL_ICE": 1,
"LEVEL_ERROR": 2,
"LEVEL_WARNING": 3,
"LEVEL_REMARK": 4,
}
)
func (x Diagnostic_Level) Enum() *Diagnostic_Level {
p := new(Diagnostic_Level)
*p = x
return p
}
func (x Diagnostic_Level) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Diagnostic_Level) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_report_proto_enumTypes[0].Descriptor()
}
func (Diagnostic_Level) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_report_proto_enumTypes[0]
}
func (x Diagnostic_Level) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Diagnostic_Level.Descriptor instead.
func (Diagnostic_Level) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{1, 0}
}
// A diagnostic report, consisting of `Diagnostics` and the `File`s they diagnose.
type Report struct {
state protoimpl.MessageState `protogen:"open.v1"`
Files []*Report_File `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"`
Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Report) Reset() {
*x = Report{}
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Report) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Report) ProtoMessage() {}
func (x *Report) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Report.ProtoReflect.Descriptor instead.
func (*Report) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{0}
}
func (x *Report) GetFiles() []*Report_File {
if x != nil {
return x.Files
}
return nil
}
func (x *Report) GetDiagnostics() []*Diagnostic {
if x != nil {
return x.Diagnostics
}
return nil
}
// A diagnostic within a `Report`.
type Diagnostic struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The message to show for this diagnostic. This should fit on one line.
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
// An optional machine-readable tag for the diagnostic.
Tag string `protobuf:"bytes,8,opt,name=tag,proto3" json:"tag,omitempty"`
// Required. The level for this diagnostic.
Level Diagnostic_Level `protobuf:"varint,2,opt,name=level,proto3,enum=buf.compiler.v1alpha1.Diagnostic_Level" json:"level,omitempty"`
// An optional path to show in the diagnostic, if it has no annotations.
// This is useful for e.g. diagnostics that would have no spans.
InFile string `protobuf:"bytes,3,opt,name=in_file,json=inFile,proto3" json:"in_file,omitempty"`
// Annotations for source code relevant to this diagnostic.
Annotations []*Diagnostic_Annotation `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty"`
// Notes about the error to show to the user. May span multiple lines.
Notes []string `protobuf:"bytes,5,rep,name=notes,proto3" json:"notes,omitempty"`
// Helpful suggestions to the user.
Help []string `protobuf:"bytes,6,rep,name=help,proto3" json:"help,omitempty"`
// Debugging information related to the diagnostic. This should only be
// used for information about debugging a tool or compiler that emits the
// diagnostic, not the code being diagnosed.
Debug []string `protobuf:"bytes,7,rep,name=debug,proto3" json:"debug,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Diagnostic) Reset() {
*x = Diagnostic{}
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Diagnostic) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Diagnostic) ProtoMessage() {}
func (x *Diagnostic) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Diagnostic.ProtoReflect.Descriptor instead.
func (*Diagnostic) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{1}
}
func (x *Diagnostic) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *Diagnostic) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *Diagnostic) GetLevel() Diagnostic_Level {
if x != nil {
return x.Level
}
return Diagnostic_LEVEL_UNSPECIFIED
}
func (x *Diagnostic) GetInFile() string {
if x != nil {
return x.InFile
}
return ""
}
func (x *Diagnostic) GetAnnotations() []*Diagnostic_Annotation {
if x != nil {
return x.Annotations
}
return nil
}
func (x *Diagnostic) GetNotes() []string {
if x != nil {
return x.Notes
}
return nil
}
func (x *Diagnostic) GetHelp() []string {
if x != nil {
return x.Help
}
return nil
}
func (x *Diagnostic) GetDebug() []string {
if x != nil {
return x.Debug
}
return nil
}
// A file involved in a diagnostic `Report`.
type Report_File struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The path to this file. Does not need to be meaningful as a file-system
// path.
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
// The textual contents of this file. Presumed to be UTF-8, although it need
// not be.
Text []byte `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Report_File) Reset() {
*x = Report_File{}
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Report_File) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Report_File) ProtoMessage() {}
func (x *Report_File) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Report_File.ProtoReflect.Descriptor instead.
func (*Report_File) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Report_File) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *Report_File) GetText() []byte {
if x != nil {
return x.Text
}
return nil
}
// A file annotation within a `Diagnostic`. This corresponds to a single
// span of source code in a `Report`'s file.
type Diagnostic_Annotation struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A message to show under this snippet. May be empty.
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
// Whether this is a "primary" snippet, which is used for deciding whether or not
// to mark the snippet with the same color as the overall diagnostic.
Primary bool `protobuf:"varint,2,opt,name=primary,proto3" json:"primary,omitempty"`
// Whether this annotation wants to be on its own window, separate from
// annotations that follow.
PageBreak bool `protobuf:"varint,7,opt,name=page_break,json=pageBreak,proto3" json:"page_break,omitempty"`
// The index of `Report.files` of the file this annotation is for.
//
// This is not a whole `Report.File` to help keep serialized reports slim. This
// avoids neeidng to duplicate the whole text of the file one for every annotation.
File uint32 `protobuf:"varint,3,opt,name=file,proto3" json:"file,omitempty"`
// The start offset of the annotated snippet, in bytes.
Start uint32 `protobuf:"varint,4,opt,name=start,proto3" json:"start,omitempty"`
// The end offset of the annotated snippet, in bytes.
End uint32 `protobuf:"varint,5,opt,name=end,proto3" json:"end,omitempty"`
Edits []*Diagnostic_Edit `protobuf:"bytes,6,rep,name=edits,proto3" json:"edits,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Diagnostic_Annotation) Reset() {
*x = Diagnostic_Annotation{}
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Diagnostic_Annotation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Diagnostic_Annotation) ProtoMessage() {}
func (x *Diagnostic_Annotation) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Diagnostic_Annotation.ProtoReflect.Descriptor instead.
func (*Diagnostic_Annotation) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{1, 0}
}
func (x *Diagnostic_Annotation) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *Diagnostic_Annotation) GetPrimary() bool {
if x != nil {
return x.Primary
}
return false
}
func (x *Diagnostic_Annotation) GetPageBreak() bool {
if x != nil {
return x.PageBreak
}
return false
}
func (x *Diagnostic_Annotation) GetFile() uint32 {
if x != nil {
return x.File
}
return 0
}
func (x *Diagnostic_Annotation) GetStart() uint32 {
if x != nil {
return x.Start
}
return 0
}
func (x *Diagnostic_Annotation) GetEnd() uint32 {
if x != nil {
return x.End
}
return 0
}
func (x *Diagnostic_Annotation) GetEdits() []*Diagnostic_Edit {
if x != nil {
return x.Edits
}
return nil
}
// Edit is an edit to suggest on an `Annotation`.
//
// A pure insertion is modeled by `start == end`.
// A pure deletion is modeled by empty `replace`.
type Diagnostic_Edit struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The start offset of the edit, relative to the containing snippet.
Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
// The end offset of the edit, relative to the containing snippet.
End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"`
// The text to insert in place of the selected region.
Replace string `protobuf:"bytes,3,opt,name=replace,proto3" json:"replace,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Diagnostic_Edit) Reset() {
*x = Diagnostic_Edit{}
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Diagnostic_Edit) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Diagnostic_Edit) ProtoMessage() {}
func (x *Diagnostic_Edit) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_report_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Diagnostic_Edit.ProtoReflect.Descriptor instead.
func (*Diagnostic_Edit) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_report_proto_rawDescGZIP(), []int{1, 1}
}
func (x *Diagnostic_Edit) GetStart() uint32 {
if x != nil {
return x.Start
}
return 0
}
func (x *Diagnostic_Edit) GetEnd() uint32 {
if x != nil {
return x.End
}
return 0
}
func (x *Diagnostic_Edit) GetReplace() string {
if x != nil {
return x.Replace
}
return ""
}
var File_buf_compiler_v1alpha1_report_proto protoreflect.FileDescriptor
const file_buf_compiler_v1alpha1_report_proto_rawDesc = "" +
"\n" +
"\"buf/compiler/v1alpha1/report.proto\x12\x15buf.compiler.v1alpha1\"\xb7\x01\n" +
"\x06Report\x128\n" +
"\x05files\x18\x01 \x03(\v2\".buf.compiler.v1alpha1.Report.FileR\x05files\x12C\n" +
"\vdiagnostics\x18\x02 \x03(\v2!.buf.compiler.v1alpha1.DiagnosticR\vdiagnostics\x1a.\n" +
"\x04File\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" +
"\x04text\x18\x02 \x01(\fR\x04text\"\xab\x05\n" +
"\n" +
"Diagnostic\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\x12\x10\n" +
"\x03tag\x18\b \x01(\tR\x03tag\x12=\n" +
"\x05level\x18\x02 \x01(\x0e2'.buf.compiler.v1alpha1.Diagnostic.LevelR\x05level\x12\x17\n" +
"\ain_file\x18\x03 \x01(\tR\x06inFile\x12N\n" +
"\vannotations\x18\x04 \x03(\v2,.buf.compiler.v1alpha1.Diagnostic.AnnotationR\vannotations\x12\x14\n" +
"\x05notes\x18\x05 \x03(\tR\x05notes\x12\x12\n" +
"\x04help\x18\x06 \x03(\tR\x04help\x12\x14\n" +
"\x05debug\x18\a \x03(\tR\x05debug\x1a\xd9\x01\n" +
"\n" +
"Annotation\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\x12\x18\n" +
"\aprimary\x18\x02 \x01(\bR\aprimary\x12\x1d\n" +
"\n" +
"page_break\x18\a \x01(\bR\tpageBreak\x12\x12\n" +
"\x04file\x18\x03 \x01(\rR\x04file\x12\x14\n" +
"\x05start\x18\x04 \x01(\rR\x05start\x12\x10\n" +
"\x03end\x18\x05 \x01(\rR\x03end\x12<\n" +
"\x05edits\x18\x06 \x03(\v2&.buf.compiler.v1alpha1.Diagnostic.EditR\x05edits\x1aH\n" +
"\x04Edit\x12\x14\n" +
"\x05start\x18\x01 \x01(\rR\x05start\x12\x10\n" +
"\x03end\x18\x02 \x01(\rR\x03end\x12\x18\n" +
"\areplace\x18\x03 \x01(\tR\areplace\"c\n" +
"\x05Level\x12\x15\n" +
"\x11LEVEL_UNSPECIFIED\x10\x00\x12\r\n" +
"\tLEVEL_ICE\x10\x01\x12\x0f\n" +
"\vLEVEL_ERROR\x10\x02\x12\x11\n" +
"\rLEVEL_WARNING\x10\x03\x12\x10\n" +
"\fLEVEL_REMARK\x10\x04B\xf4\x01\n" +
"\x19com.buf.compiler.v1alpha1B\vReportProtoP\x01ZTgithub.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1;compilerv1alpha1\xa2\x02\x03BCX\xaa\x02\x15Buf.Compiler.V1alpha1\xca\x02\x15Buf\\Compiler\\V1alpha1\xe2\x02!Buf\\Compiler\\V1alpha1\\GPBMetadata\xea\x02\x17Buf::Compiler::V1alpha1b\x06proto3"
var (
file_buf_compiler_v1alpha1_report_proto_rawDescOnce sync.Once
file_buf_compiler_v1alpha1_report_proto_rawDescData []byte
)
func file_buf_compiler_v1alpha1_report_proto_rawDescGZIP() []byte {
file_buf_compiler_v1alpha1_report_proto_rawDescOnce.Do(func() {
file_buf_compiler_v1alpha1_report_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_report_proto_rawDesc), len(file_buf_compiler_v1alpha1_report_proto_rawDesc)))
})
return file_buf_compiler_v1alpha1_report_proto_rawDescData
}
var file_buf_compiler_v1alpha1_report_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_buf_compiler_v1alpha1_report_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_buf_compiler_v1alpha1_report_proto_goTypes = []any{
(Diagnostic_Level)(0), // 0: buf.compiler.v1alpha1.Diagnostic.Level
(*Report)(nil), // 1: buf.compiler.v1alpha1.Report
(*Diagnostic)(nil), // 2: buf.compiler.v1alpha1.Diagnostic
(*Report_File)(nil), // 3: buf.compiler.v1alpha1.Report.File
(*Diagnostic_Annotation)(nil), // 4: buf.compiler.v1alpha1.Diagnostic.Annotation
(*Diagnostic_Edit)(nil), // 5: buf.compiler.v1alpha1.Diagnostic.Edit
}
var file_buf_compiler_v1alpha1_report_proto_depIdxs = []int32{
3, // 0: buf.compiler.v1alpha1.Report.files:type_name -> buf.compiler.v1alpha1.Report.File
2, // 1: buf.compiler.v1alpha1.Report.diagnostics:type_name -> buf.compiler.v1alpha1.Diagnostic
0, // 2: buf.compiler.v1alpha1.Diagnostic.level:type_name -> buf.compiler.v1alpha1.Diagnostic.Level
4, // 3: buf.compiler.v1alpha1.Diagnostic.annotations:type_name -> buf.compiler.v1alpha1.Diagnostic.Annotation
5, // 4: buf.compiler.v1alpha1.Diagnostic.Annotation.edits:type_name -> buf.compiler.v1alpha1.Diagnostic.Edit
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_buf_compiler_v1alpha1_report_proto_init() }
func file_buf_compiler_v1alpha1_report_proto_init() {
if File_buf_compiler_v1alpha1_report_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_report_proto_rawDesc), len(file_buf_compiler_v1alpha1_report_proto_rawDesc)),
NumEnums: 1,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_buf_compiler_v1alpha1_report_proto_goTypes,
DependencyIndexes: file_buf_compiler_v1alpha1_report_proto_depIdxs,
EnumInfos: file_buf_compiler_v1alpha1_report_proto_enumTypes,
MessageInfos: file_buf_compiler_v1alpha1_report_proto_msgTypes,
}.Build()
File_buf_compiler_v1alpha1_report_proto = out.File
file_buf_compiler_v1alpha1_report_proto_goTypes = nil
file_buf_compiler_v1alpha1_report_proto_depIdxs = nil
}
// 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 protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: buf/compiler/v1alpha1/symtab.proto
package compilerv1alpha1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Symbol_Kind int32
const (
// Numbers synced with those in symbol_kind.go
Symbol_KIND_UNSPECIFIED Symbol_Kind = 0
Symbol_KIND_PACKAGE Symbol_Kind = 1
Symbol_KIND_SCALAR Symbol_Kind = 2
Symbol_KIND_MESSAGE Symbol_Kind = 3
Symbol_KIND_ENUM Symbol_Kind = 4
Symbol_KIND_FIELD Symbol_Kind = 5
Symbol_KIND_ENUM_VALUE Symbol_Kind = 6
Symbol_KIND_EXTENSION Symbol_Kind = 7
Symbol_KIND_ONEOF Symbol_Kind = 8
)
// Enum value maps for Symbol_Kind.
var (
Symbol_Kind_name = map[int32]string{
0: "KIND_UNSPECIFIED",
1: "KIND_PACKAGE",
2: "KIND_SCALAR",
3: "KIND_MESSAGE",
4: "KIND_ENUM",
5: "KIND_FIELD",
6: "KIND_ENUM_VALUE",
7: "KIND_EXTENSION",
8: "KIND_ONEOF",
}
Symbol_Kind_value = map[string]int32{
"KIND_UNSPECIFIED": 0,
"KIND_PACKAGE": 1,
"KIND_SCALAR": 2,
"KIND_MESSAGE": 3,
"KIND_ENUM": 4,
"KIND_FIELD": 5,
"KIND_ENUM_VALUE": 6,
"KIND_EXTENSION": 7,
"KIND_ONEOF": 8,
}
)
func (x Symbol_Kind) Enum() *Symbol_Kind {
p := new(Symbol_Kind)
*p = x
return p
}
func (x Symbol_Kind) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Symbol_Kind) Descriptor() protoreflect.EnumDescriptor {
return file_buf_compiler_v1alpha1_symtab_proto_enumTypes[0].Descriptor()
}
func (Symbol_Kind) Type() protoreflect.EnumType {
return &file_buf_compiler_v1alpha1_symtab_proto_enumTypes[0]
}
func (x Symbol_Kind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Symbol_Kind.Descriptor instead.
func (Symbol_Kind) EnumDescriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{4, 0}
}
// A set of symbol tables.
type SymbolSet struct {
state protoimpl.MessageState `protogen:"open.v1"`
Tables map[string]*SymbolTable `protobuf:"bytes,1,rep,name=tables,proto3" json:"tables,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SymbolSet) Reset() {
*x = SymbolSet{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SymbolSet) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SymbolSet) ProtoMessage() {}
func (x *SymbolSet) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SymbolSet.ProtoReflect.Descriptor instead.
func (*SymbolSet) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{0}
}
func (x *SymbolSet) GetTables() map[string]*SymbolTable {
if x != nil {
return x.Tables
}
return nil
}
// Symbol information for a particular Protobuf file.
type SymbolTable struct {
state protoimpl.MessageState `protogen:"open.v1"`
Imports []*Import `protobuf:"bytes,1,rep,name=imports,proto3" json:"imports,omitempty"`
Features []*Feature `protobuf:"bytes,4,rep,name=features,proto3" json:"features,omitempty"`
Symbols []*Symbol `protobuf:"bytes,2,rep,name=symbols,proto3" json:"symbols,omitempty"`
Options *Value `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SymbolTable) Reset() {
*x = SymbolTable{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SymbolTable) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SymbolTable) ProtoMessage() {}
func (x *SymbolTable) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SymbolTable.ProtoReflect.Descriptor instead.
func (*SymbolTable) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{1}
}
func (x *SymbolTable) GetImports() []*Import {
if x != nil {
return x.Imports
}
return nil
}
func (x *SymbolTable) GetFeatures() []*Feature {
if x != nil {
return x.Features
}
return nil
}
func (x *SymbolTable) GetSymbols() []*Symbol {
if x != nil {
return x.Symbols
}
return nil
}
func (x *SymbolTable) GetOptions() *Value {
if x != nil {
return x.Options
}
return nil
}
// Metadata associated with a transitive import.
type Import struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
Public bool `protobuf:"varint,2,opt,name=public,proto3" json:"public,omitempty"`
Weak bool `protobuf:"varint,3,opt,name=weak,proto3" json:"weak,omitempty"`
Transitive bool `protobuf:"varint,4,opt,name=transitive,proto3" json:"transitive,omitempty"`
Visible bool `protobuf:"varint,5,opt,name=visible,proto3" json:"visible,omitempty"`
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Import) Reset() {
*x = Import{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Import) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Import) ProtoMessage() {}
func (x *Import) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Import.ProtoReflect.Descriptor instead.
func (*Import) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{2}
}
func (x *Import) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *Import) GetPublic() bool {
if x != nil {
return x.Public
}
return false
}
func (x *Import) GetWeak() bool {
if x != nil {
return x.Weak
}
return false
}
func (x *Import) GetTransitive() bool {
if x != nil {
return x.Transitive
}
return false
}
func (x *Import) GetVisible() bool {
if x != nil {
return x.Visible
}
return false
}
func (x *Import) GetUsed() bool {
if x != nil {
return x.Used
}
return false
}
// A feature associated with a symbol.
type Feature struct {
state protoimpl.MessageState `protogen:"open.v1"`
Extn string `protobuf:"bytes,1,opt,name=extn,proto3" json:"extn,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
// The value as a textproto string.
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
// Whether this feature is set explicitly.
Explicit bool `protobuf:"varint,4,opt,name=explicit,proto3" json:"explicit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Feature) Reset() {
*x = Feature{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Feature) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Feature) ProtoMessage() {}
func (x *Feature) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Feature.ProtoReflect.Descriptor instead.
func (*Feature) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{3}
}
func (x *Feature) GetExtn() string {
if x != nil {
return x.Extn
}
return ""
}
func (x *Feature) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Feature) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Feature) GetExplicit() bool {
if x != nil {
return x.Explicit
}
return false
}
// A symbol in a file.
type Symbol struct {
state protoimpl.MessageState `protogen:"open.v1"`
Fqn string `protobuf:"bytes,1,opt,name=fqn,proto3" json:"fqn,omitempty"`
Kind Symbol_Kind `protobuf:"varint,2,opt,name=kind,proto3,enum=buf.compiler.v1alpha1.Symbol_Kind" json:"kind,omitempty"`
// The file this symbol came from.
File string `protobuf:"bytes,3,opt,name=file,proto3" json:"file,omitempty"`
// The index of this kind of entity in that file.
Index uint32 `protobuf:"varint,4,opt,name=index,proto3" json:"index,omitempty"`
// Whether this symbol can be validly referenced in the current file.
Visible bool `protobuf:"varint,5,opt,name=visible,proto3" json:"visible,omitempty"`
OptionOnly bool `protobuf:"varint,8,opt,name=option_only,json=optionOnly,proto3" json:"option_only,omitempty"`
Options *Value `protobuf:"bytes,6,opt,name=options,proto3" json:"options,omitempty"`
Features []*Feature `protobuf:"bytes,7,rep,name=features,proto3" json:"features,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Symbol) Reset() {
*x = Symbol{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Symbol) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Symbol) ProtoMessage() {}
func (x *Symbol) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Symbol.ProtoReflect.Descriptor instead.
func (*Symbol) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{4}
}
func (x *Symbol) GetFqn() string {
if x != nil {
return x.Fqn
}
return ""
}
func (x *Symbol) GetKind() Symbol_Kind {
if x != nil {
return x.Kind
}
return Symbol_KIND_UNSPECIFIED
}
func (x *Symbol) GetFile() string {
if x != nil {
return x.File
}
return ""
}
func (x *Symbol) GetIndex() uint32 {
if x != nil {
return x.Index
}
return 0
}
func (x *Symbol) GetVisible() bool {
if x != nil {
return x.Visible
}
return false
}
func (x *Symbol) GetOptionOnly() bool {
if x != nil {
return x.OptionOnly
}
return false
}
func (x *Symbol) GetOptions() *Value {
if x != nil {
return x.Options
}
return nil
}
func (x *Symbol) GetFeatures() []*Feature {
if x != nil {
return x.Features
}
return nil
}
// An option value attached to a symbol.
type Value struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *Value_I32
// *Value_U32
// *Value_F32
// *Value_I64
// *Value_U64
// *Value_F64
// *Value_Bool
// *Value_String_
// *Value_Repeated_
// *Value_Message_
// *Value_Any_
// *Value_Cycle
Value isValue_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Value) Reset() {
*x = Value{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Value) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Value) ProtoMessage() {}
func (x *Value) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Value.ProtoReflect.Descriptor instead.
func (*Value) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{5}
}
func (x *Value) GetValue() isValue_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *Value) GetI32() int32 {
if x != nil {
if x, ok := x.Value.(*Value_I32); ok {
return x.I32
}
}
return 0
}
func (x *Value) GetU32() uint32 {
if x != nil {
if x, ok := x.Value.(*Value_U32); ok {
return x.U32
}
}
return 0
}
func (x *Value) GetF32() float32 {
if x != nil {
if x, ok := x.Value.(*Value_F32); ok {
return x.F32
}
}
return 0
}
func (x *Value) GetI64() int64 {
if x != nil {
if x, ok := x.Value.(*Value_I64); ok {
return x.I64
}
}
return 0
}
func (x *Value) GetU64() uint64 {
if x != nil {
if x, ok := x.Value.(*Value_U64); ok {
return x.U64
}
}
return 0
}
func (x *Value) GetF64() float64 {
if x != nil {
if x, ok := x.Value.(*Value_F64); ok {
return x.F64
}
}
return 0
}
func (x *Value) GetBool() bool {
if x != nil {
if x, ok := x.Value.(*Value_Bool); ok {
return x.Bool
}
}
return false
}
func (x *Value) GetString_() []byte {
if x != nil {
if x, ok := x.Value.(*Value_String_); ok {
return x.String_
}
}
return nil
}
func (x *Value) GetRepeated() *Value_Repeated {
if x != nil {
if x, ok := x.Value.(*Value_Repeated_); ok {
return x.Repeated
}
}
return nil
}
func (x *Value) GetMessage() *Value_Message {
if x != nil {
if x, ok := x.Value.(*Value_Message_); ok {
return x.Message
}
}
return nil
}
func (x *Value) GetAny() *Value_Any {
if x != nil {
if x, ok := x.Value.(*Value_Any_); ok {
return x.Any
}
}
return nil
}
func (x *Value) GetCycle() int32 {
if x != nil {
if x, ok := x.Value.(*Value_Cycle); ok {
return x.Cycle
}
}
return 0
}
type isValue_Value interface {
isValue_Value()
}
type Value_I32 struct {
I32 int32 `protobuf:"varint,1,opt,name=i32,proto3,oneof"`
}
type Value_U32 struct {
U32 uint32 `protobuf:"varint,2,opt,name=u32,proto3,oneof"`
}
type Value_F32 struct {
F32 float32 `protobuf:"fixed32,3,opt,name=f32,proto3,oneof"`
}
type Value_I64 struct {
I64 int64 `protobuf:"varint,4,opt,name=i64,proto3,oneof"`
}
type Value_U64 struct {
U64 uint64 `protobuf:"varint,5,opt,name=u64,proto3,oneof"`
}
type Value_F64 struct {
F64 float64 `protobuf:"fixed64,6,opt,name=f64,proto3,oneof"`
}
type Value_Bool struct {
Bool bool `protobuf:"varint,7,opt,name=bool,proto3,oneof"`
}
type Value_String_ struct {
String_ []byte `protobuf:"bytes,8,opt,name=string,proto3,oneof"`
}
type Value_Repeated_ struct {
Repeated *Value_Repeated `protobuf:"bytes,9,opt,name=repeated,proto3,oneof"`
}
type Value_Message_ struct {
Message *Value_Message `protobuf:"bytes,10,opt,name=message,proto3,oneof"`
}
type Value_Any_ struct {
Any *Value_Any `protobuf:"bytes,11,opt,name=any,proto3,oneof"`
}
type Value_Cycle struct {
// Cyclic message. This is how many uprefs away the cycle element is.
Cycle int32 `protobuf:"varint,20,opt,name=cycle,proto3,oneof"`
}
func (*Value_I32) isValue_Value() {}
func (*Value_U32) isValue_Value() {}
func (*Value_F32) isValue_Value() {}
func (*Value_I64) isValue_Value() {}
func (*Value_U64) isValue_Value() {}
func (*Value_F64) isValue_Value() {}
func (*Value_Bool) isValue_Value() {}
func (*Value_String_) isValue_Value() {}
func (*Value_Repeated_) isValue_Value() {}
func (*Value_Message_) isValue_Value() {}
func (*Value_Any_) isValue_Value() {}
func (*Value_Cycle) isValue_Value() {}
type Value_Message struct {
state protoimpl.MessageState `protogen:"open.v1"`
Fields map[string]*Value `protobuf:"bytes,1,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Extns map[string]*Value `protobuf:"bytes,2,rep,name=extns,proto3" json:"extns,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Value_Message) Reset() {
*x = Value_Message{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Value_Message) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Value_Message) ProtoMessage() {}
func (x *Value_Message) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Value_Message.ProtoReflect.Descriptor instead.
func (*Value_Message) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{5, 0}
}
func (x *Value_Message) GetFields() map[string]*Value {
if x != nil {
return x.Fields
}
return nil
}
func (x *Value_Message) GetExtns() map[string]*Value {
if x != nil {
return x.Extns
}
return nil
}
type Value_Repeated struct {
state protoimpl.MessageState `protogen:"open.v1"`
Values []*Value `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Value_Repeated) Reset() {
*x = Value_Repeated{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Value_Repeated) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Value_Repeated) ProtoMessage() {}
func (x *Value_Repeated) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Value_Repeated.ProtoReflect.Descriptor instead.
func (*Value_Repeated) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{5, 1}
}
func (x *Value_Repeated) GetValues() []*Value {
if x != nil {
return x.Values
}
return nil
}
type Value_Any struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Value *Value `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Value_Any) Reset() {
*x = Value_Any{}
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Value_Any) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Value_Any) ProtoMessage() {}
func (x *Value_Any) ProtoReflect() protoreflect.Message {
mi := &file_buf_compiler_v1alpha1_symtab_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Value_Any.ProtoReflect.Descriptor instead.
func (*Value_Any) Descriptor() ([]byte, []int) {
return file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP(), []int{5, 2}
}
func (x *Value_Any) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Value_Any) GetValue() *Value {
if x != nil {
return x.Value
}
return nil
}
var File_buf_compiler_v1alpha1_symtab_proto protoreflect.FileDescriptor
const file_buf_compiler_v1alpha1_symtab_proto_rawDesc = "" +
"\n" +
"\"buf/compiler/v1alpha1/symtab.proto\x12\x15buf.compiler.v1alpha1\"\xb0\x01\n" +
"\tSymbolSet\x12D\n" +
"\x06tables\x18\x01 \x03(\v2,.buf.compiler.v1alpha1.SymbolSet.TablesEntryR\x06tables\x1a]\n" +
"\vTablesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x128\n" +
"\x05value\x18\x02 \x01(\v2\".buf.compiler.v1alpha1.SymbolTableR\x05value:\x028\x01\"\xf3\x01\n" +
"\vSymbolTable\x127\n" +
"\aimports\x18\x01 \x03(\v2\x1d.buf.compiler.v1alpha1.ImportR\aimports\x12:\n" +
"\bfeatures\x18\x04 \x03(\v2\x1e.buf.compiler.v1alpha1.FeatureR\bfeatures\x127\n" +
"\asymbols\x18\x02 \x03(\v2\x1d.buf.compiler.v1alpha1.SymbolR\asymbols\x126\n" +
"\aoptions\x18\x03 \x01(\v2\x1c.buf.compiler.v1alpha1.ValueR\aoptions\"\x96\x01\n" +
"\x06Import\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" +
"\x06public\x18\x02 \x01(\bR\x06public\x12\x12\n" +
"\x04weak\x18\x03 \x01(\bR\x04weak\x12\x1e\n" +
"\n" +
"transitive\x18\x04 \x01(\bR\n" +
"transitive\x12\x18\n" +
"\avisible\x18\x05 \x01(\bR\avisible\x12\x12\n" +
"\x04used\x18\x06 \x01(\bR\x04used\"c\n" +
"\aFeature\x12\x12\n" +
"\x04extn\x18\x01 \x01(\tR\x04extn\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
"\x05value\x18\x03 \x01(\tR\x05value\x12\x1a\n" +
"\bexplicit\x18\x04 \x01(\bR\bexplicit\"\xd7\x03\n" +
"\x06Symbol\x12\x10\n" +
"\x03fqn\x18\x01 \x01(\tR\x03fqn\x126\n" +
"\x04kind\x18\x02 \x01(\x0e2\".buf.compiler.v1alpha1.Symbol.KindR\x04kind\x12\x12\n" +
"\x04file\x18\x03 \x01(\tR\x04file\x12\x14\n" +
"\x05index\x18\x04 \x01(\rR\x05index\x12\x18\n" +
"\avisible\x18\x05 \x01(\bR\avisible\x12\x1f\n" +
"\voption_only\x18\b \x01(\bR\n" +
"optionOnly\x126\n" +
"\aoptions\x18\x06 \x01(\v2\x1c.buf.compiler.v1alpha1.ValueR\aoptions\x12:\n" +
"\bfeatures\x18\a \x03(\v2\x1e.buf.compiler.v1alpha1.FeatureR\bfeatures\"\xa9\x01\n" +
"\x04Kind\x12\x14\n" +
"\x10KIND_UNSPECIFIED\x10\x00\x12\x10\n" +
"\fKIND_PACKAGE\x10\x01\x12\x0f\n" +
"\vKIND_SCALAR\x10\x02\x12\x10\n" +
"\fKIND_MESSAGE\x10\x03\x12\r\n" +
"\tKIND_ENUM\x10\x04\x12\x0e\n" +
"\n" +
"KIND_FIELD\x10\x05\x12\x13\n" +
"\x0fKIND_ENUM_VALUE\x10\x06\x12\x12\n" +
"\x0eKIND_EXTENSION\x10\a\x12\x0e\n" +
"\n" +
"KIND_ONEOF\x10\b\"\xea\x06\n" +
"\x05Value\x12\x12\n" +
"\x03i32\x18\x01 \x01(\x05H\x00R\x03i32\x12\x12\n" +
"\x03u32\x18\x02 \x01(\rH\x00R\x03u32\x12\x12\n" +
"\x03f32\x18\x03 \x01(\x02H\x00R\x03f32\x12\x12\n" +
"\x03i64\x18\x04 \x01(\x03H\x00R\x03i64\x12\x12\n" +
"\x03u64\x18\x05 \x01(\x04H\x00R\x03u64\x12\x12\n" +
"\x03f64\x18\x06 \x01(\x01H\x00R\x03f64\x12\x14\n" +
"\x04bool\x18\a \x01(\bH\x00R\x04bool\x12\x18\n" +
"\x06string\x18\b \x01(\fH\x00R\x06string\x12C\n" +
"\brepeated\x18\t \x01(\v2%.buf.compiler.v1alpha1.Value.RepeatedH\x00R\brepeated\x12@\n" +
"\amessage\x18\n" +
" \x01(\v2$.buf.compiler.v1alpha1.Value.MessageH\x00R\amessage\x124\n" +
"\x03any\x18\v \x01(\v2 .buf.compiler.v1alpha1.Value.AnyH\x00R\x03any\x12\x16\n" +
"\x05cycle\x18\x14 \x01(\x05H\x00R\x05cycle\x1a\xcb\x02\n" +
"\aMessage\x12H\n" +
"\x06fields\x18\x01 \x03(\v20.buf.compiler.v1alpha1.Value.Message.FieldsEntryR\x06fields\x12E\n" +
"\x05extns\x18\x02 \x03(\v2/.buf.compiler.v1alpha1.Value.Message.ExtnsEntryR\x05extns\x1aW\n" +
"\vFieldsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x122\n" +
"\x05value\x18\x02 \x01(\v2\x1c.buf.compiler.v1alpha1.ValueR\x05value:\x028\x01\x1aV\n" +
"\n" +
"ExtnsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x122\n" +
"\x05value\x18\x02 \x01(\v2\x1c.buf.compiler.v1alpha1.ValueR\x05value:\x028\x01\x1a@\n" +
"\bRepeated\x124\n" +
"\x06values\x18\x01 \x03(\v2\x1c.buf.compiler.v1alpha1.ValueR\x06values\x1aK\n" +
"\x03Any\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x122\n" +
"\x05value\x18\x02 \x01(\v2\x1c.buf.compiler.v1alpha1.ValueR\x05valueB\a\n" +
"\x05valueB\xf4\x01\n" +
"\x19com.buf.compiler.v1alpha1B\vSymtabProtoP\x01ZTgithub.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1;compilerv1alpha1\xa2\x02\x03BCX\xaa\x02\x15Buf.Compiler.V1alpha1\xca\x02\x15Buf\\Compiler\\V1alpha1\xe2\x02!Buf\\Compiler\\V1alpha1\\GPBMetadata\xea\x02\x17Buf::Compiler::V1alpha1b\x06proto3"
var (
file_buf_compiler_v1alpha1_symtab_proto_rawDescOnce sync.Once
file_buf_compiler_v1alpha1_symtab_proto_rawDescData []byte
)
func file_buf_compiler_v1alpha1_symtab_proto_rawDescGZIP() []byte {
file_buf_compiler_v1alpha1_symtab_proto_rawDescOnce.Do(func() {
file_buf_compiler_v1alpha1_symtab_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_symtab_proto_rawDesc), len(file_buf_compiler_v1alpha1_symtab_proto_rawDesc)))
})
return file_buf_compiler_v1alpha1_symtab_proto_rawDescData
}
var file_buf_compiler_v1alpha1_symtab_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_buf_compiler_v1alpha1_symtab_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_buf_compiler_v1alpha1_symtab_proto_goTypes = []any{
(Symbol_Kind)(0), // 0: buf.compiler.v1alpha1.Symbol.Kind
(*SymbolSet)(nil), // 1: buf.compiler.v1alpha1.SymbolSet
(*SymbolTable)(nil), // 2: buf.compiler.v1alpha1.SymbolTable
(*Import)(nil), // 3: buf.compiler.v1alpha1.Import
(*Feature)(nil), // 4: buf.compiler.v1alpha1.Feature
(*Symbol)(nil), // 5: buf.compiler.v1alpha1.Symbol
(*Value)(nil), // 6: buf.compiler.v1alpha1.Value
nil, // 7: buf.compiler.v1alpha1.SymbolSet.TablesEntry
(*Value_Message)(nil), // 8: buf.compiler.v1alpha1.Value.Message
(*Value_Repeated)(nil), // 9: buf.compiler.v1alpha1.Value.Repeated
(*Value_Any)(nil), // 10: buf.compiler.v1alpha1.Value.Any
nil, // 11: buf.compiler.v1alpha1.Value.Message.FieldsEntry
nil, // 12: buf.compiler.v1alpha1.Value.Message.ExtnsEntry
}
var file_buf_compiler_v1alpha1_symtab_proto_depIdxs = []int32{
7, // 0: buf.compiler.v1alpha1.SymbolSet.tables:type_name -> buf.compiler.v1alpha1.SymbolSet.TablesEntry
3, // 1: buf.compiler.v1alpha1.SymbolTable.imports:type_name -> buf.compiler.v1alpha1.Import
4, // 2: buf.compiler.v1alpha1.SymbolTable.features:type_name -> buf.compiler.v1alpha1.Feature
5, // 3: buf.compiler.v1alpha1.SymbolTable.symbols:type_name -> buf.compiler.v1alpha1.Symbol
6, // 4: buf.compiler.v1alpha1.SymbolTable.options:type_name -> buf.compiler.v1alpha1.Value
0, // 5: buf.compiler.v1alpha1.Symbol.kind:type_name -> buf.compiler.v1alpha1.Symbol.Kind
6, // 6: buf.compiler.v1alpha1.Symbol.options:type_name -> buf.compiler.v1alpha1.Value
4, // 7: buf.compiler.v1alpha1.Symbol.features:type_name -> buf.compiler.v1alpha1.Feature
9, // 8: buf.compiler.v1alpha1.Value.repeated:type_name -> buf.compiler.v1alpha1.Value.Repeated
8, // 9: buf.compiler.v1alpha1.Value.message:type_name -> buf.compiler.v1alpha1.Value.Message
10, // 10: buf.compiler.v1alpha1.Value.any:type_name -> buf.compiler.v1alpha1.Value.Any
2, // 11: buf.compiler.v1alpha1.SymbolSet.TablesEntry.value:type_name -> buf.compiler.v1alpha1.SymbolTable
11, // 12: buf.compiler.v1alpha1.Value.Message.fields:type_name -> buf.compiler.v1alpha1.Value.Message.FieldsEntry
12, // 13: buf.compiler.v1alpha1.Value.Message.extns:type_name -> buf.compiler.v1alpha1.Value.Message.ExtnsEntry
6, // 14: buf.compiler.v1alpha1.Value.Repeated.values:type_name -> buf.compiler.v1alpha1.Value
6, // 15: buf.compiler.v1alpha1.Value.Any.value:type_name -> buf.compiler.v1alpha1.Value
6, // 16: buf.compiler.v1alpha1.Value.Message.FieldsEntry.value:type_name -> buf.compiler.v1alpha1.Value
6, // 17: buf.compiler.v1alpha1.Value.Message.ExtnsEntry.value:type_name -> buf.compiler.v1alpha1.Value
18, // [18:18] is the sub-list for method output_type
18, // [18:18] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
}
func init() { file_buf_compiler_v1alpha1_symtab_proto_init() }
func file_buf_compiler_v1alpha1_symtab_proto_init() {
if File_buf_compiler_v1alpha1_symtab_proto != nil {
return
}
file_buf_compiler_v1alpha1_symtab_proto_msgTypes[5].OneofWrappers = []any{
(*Value_I32)(nil),
(*Value_U32)(nil),
(*Value_F32)(nil),
(*Value_I64)(nil),
(*Value_U64)(nil),
(*Value_F64)(nil),
(*Value_Bool)(nil),
(*Value_String_)(nil),
(*Value_Repeated_)(nil),
(*Value_Message_)(nil),
(*Value_Any_)(nil),
(*Value_Cycle)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_compiler_v1alpha1_symtab_proto_rawDesc), len(file_buf_compiler_v1alpha1_symtab_proto_rawDesc)),
NumEnums: 1,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_buf_compiler_v1alpha1_symtab_proto_goTypes,
DependencyIndexes: file_buf_compiler_v1alpha1_symtab_proto_depIdxs,
EnumInfos: file_buf_compiler_v1alpha1_symtab_proto_enumTypes,
MessageInfos: file_buf_compiler_v1alpha1_symtab_proto_msgTypes,
}.Build()
File_buf_compiler_v1alpha1_symtab_proto = out.File
file_buf_compiler_v1alpha1_symtab_proto_goTypes = nil
file_buf_compiler_v1alpha1_symtab_proto_depIdxs = nil
}
// 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 intern
import (
"strings"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
const (
maxInlined = 32 / 6
)
var (
// NOTE: We do not use exactly the same alphabet as LLVM: we swap _ and .,
// so that . is encoded as 0b111111, aka 077.
char6ToByte = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.")
byteToChar6 = func() []byte {
out := make([]byte, 256)
for i := range out {
out[i] = 0xff
}
for j, b := range char6ToByte {
out[int(b)] = byte(j)
}
return out
}()
)
// encodeChar6 attempts to encoding data using the char6 encoding. Returns
// whether encoding was successful, and an encoded value.
func encodeChar6(data string) (ID, bool) {
if data == "" {
return 0, true
}
if len(data) > maxInlined || strings.HasSuffix(data, ".") {
return 0, false
}
// The main encoding loop is outlined to promote inlining of the two
// above checks into Table.Intern.
return encodeOutlined(data)
}
func encodeOutlined(data string) (ID, bool) {
// Start by filling value with all ones. Once we shift in all of the
// encoded bytes from data, we will have two desired properties:
//
// 1. The sign bit will be set.
//
// 2. If there are less than five bytes, the trailing sextets will all
// be 077, aka '.'. Because we do not allow trailing periods, we can
// use this to determine the length of the original string.
//
// Thus, "foo" is encoded as if it was the string "foo..".
value := ID(-1)
for i := len(data) - 1; i >= 0; i-- {
sextet := byteToChar6[data[i]]
if sextet == 0xff {
return 0, false
}
value <<= 6
value |= ID(sextet)
}
return value, true
}
// decodeChar6 decodes id assuming it contains a char6-encoded string.
func decodeChar6(id ID) string {
// The main decoding loop is outlined to promote inlining of decodeChar6,
// and thus heap-promotion of the returned string.
data, len := decodeOutlined(id) //nolint:predeclared,revive // For `len`.
return unsafex.StringAlias(data[:len])
}
//nolint:predeclared,revive // For `len`.
func decodeOutlined(id ID) (data [maxInlined]byte, len int) {
for i := range data {
data[i] = char6ToByte[int(id&077)]
id >>= 6
}
// Figure out the length by removing a maximal suffix of
// '.' bytes. Note that an all-ones value will decode to "", but encode
// will never return that value.
len = maxInlined
for ; len > 0; len-- {
if data[len-1] != '.' {
break
}
}
return data, len
}
// 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 intern provides an interning table abstraction to optimize symbol
// resolution.
package intern
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/bufbuild/protocompile/internal/ext/mapsx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// ID is an interned string in a particular [Table].
//
// IDs can be compared very cheaply. The zero value of ID always
// corresponds to the empty string.
//
// # Representation
//
// Interned strings are represented thus. If the high bit is cleared, then this
// is an index into the stored strings inside of the [Table] that created it.
//
// Otherwise, it is up to five characters drawn from the [LLVM char6 encoding],
// represented in-line using the bits of the ID.
//
// NOTE: The 32-bit length is chosen because it is small. However, if we used a
// 64-bit ID this would allow us to inline 10-byte identifiers. We should
// investigate whether this results in improved memory usage overall.
//
// [LLVM char6 encoding]: https://llvm.org/docs/BitCodeFormat.html#bit-characters
type ID int32
// String implements [fmt.Stringer].
//
// Note that this will not convert the ID back into a string; to do that, you
// must call [Table.Value].
func (id ID) String() string {
if id == 0 {
return `intern.ID("")`
}
if id < 0 {
return fmt.Sprintf("intern.ID(%q)", decodeChar6(id))
}
return fmt.Sprintf("intern.ID(%d)", int(id))
}
// GoString implements [fmt.GoStringer].
func (id ID) GoString() string {
return id.String()
}
// Table is an interning table.
//
// A table can be used to convert strings into [ID]s and back again.
//
// The zero value of Table is empty and ready to use.
type Table struct {
mu sync.RWMutex
index map[string]ID
table []string
}
// Intern interns the given string into this table.
//
// This function may be called by multiple goroutines concurrently.
func (t *Table) Intern(s string) ID {
// Fast path for strings that have already been interned. In the common case
// all strings are interned, so we can take a read lock to avoid needing
// to trap to the scheduler on concurrent access (all calls to Intern() will
// still contend mu.readCount, because RLock atomically increments it).
if id, ok := t.Query(s); ok {
return id
}
// Outline the fallback for when we haven't interned, to promote inlining
// of Intern().
return t.internSlow(s)
}
// Query will query whether s has already been interned.
//
// If s has never been interned, returns false. This is useful for e.g. querying
// an intern-keyed map using a string: a failed query indicates that the string
// has never been seen before, so searching the map will be futile.
//
// If s is small enough to be inlined in an ID, it is treated as always being
// interned.
func (t *Table) Query(s string) (ID, bool) {
if char6, ok := encodeChar6(s); ok {
// This also handles s == "".
return char6, true
}
t.mu.RLock()
id, ok := t.index[s]
t.mu.RUnlock()
return id, ok
}
func (t *Table) internSlow(s string) ID {
// Intern tables are expected to be long-lived. Avoid holding onto a larger
// buffer that s is an internal pointer to by cloning it.
//
// This is also necessary for the correctness of InternBytes, which aliases
// a []byte as a string temporarily for querying the intern table.
s = strings.Clone(s)
t.mu.Lock()
defer t.mu.Unlock()
// Check if someone raced us to intern this string. We have to check again
// because in the unsynchronized section between RUnlock and Lock, another
// goroutine might have successfully interned s.
//
// TODO: We can reduce the number of map hits if we switch to a different
// Map implementation that provides an upsert primitive.
if id, ok := t.index[s]; ok {
return id
}
// As of here, we have unique ownership of the table, and s has not been
// inserted yet.
t.table = append(t.table, s)
// The first ID will have value 1. ID 0 is reserved for "".
id := ID(len(t.table))
if id < 0 {
panic(fmt.Sprintf("internal/intern: %d interning IDs exhausted", len(t.table)))
}
if t.index == nil {
t.index = make(map[string]ID)
}
t.index[s] = id
return id
}
// InternBytes interns the given byte string into this table.
//
// This function may be called by multiple goroutines concurrently, but bytes
// must not be modified until this function returns.
func (t *Table) InternBytes(bytes []byte) ID {
// Intern() will not modify its argument, since it believes that it is a
// string. It will also clone the string if it needs to write it to the
// intern table, so it does not hold onto its argument after it returns.
//
// Thus, we can simply turn bytes into a string temporarily to pass to
// Intern.
return t.Intern(unsafex.StringAlias(bytes))
}
// QueryBytes will query whether bytes has already been interned.
//
// This function may be called by multiple goroutines concurrently, but bytes
// must not be modified until this function returns.
func (t *Table) QueryBytes(bytes []byte) (ID, bool) {
// See InternBytes's comment.
return t.Query(unsafex.StringAlias(bytes))
}
// Value converts an [ID] back into its corresponding string.
//
// If id was created by a different [Table], the results are unspecified,
// including potentially a panic.
//
// This function may be called by multiple goroutines concurrently.
func (t *Table) Value(id ID) string {
if id == 0 {
return ""
}
if id < 0 {
return decodeChar6(id)
}
// The locking part of Get is outlined to promote inlining of the two
// fast paths above. This in turn allows decodeChar6 to be inlined, which
// allows the returned string to be stack-promoted.
return t.getSlow(id)
}
// Preload takes a pointer to a struct type and initializes [ID]-typed fields
// with statically-specified strings.
//
// Specifically, every exported field whose type is [ID] and which has a struct
// tag "intern" will be set to t.Intern(...) with that tag's value.
//
// Panics if ids is not a pointer to a struct type.
func (t *Table) Preload(ids any) {
r := reflect.ValueOf(ids).Elem()
for i := range r.NumField() {
f := r.Type().Field(i)
if !f.IsExported() || f.Type != reflect.TypeFor[ID]() {
continue
}
text, ok := f.Tag.Lookup("intern")
if ok {
r.Field(i).Set(reflect.ValueOf(t.Intern(text)))
}
}
}
func (t *Table) getSlow(id ID) string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.table[int(id)-1]
}
// Set is a set of intern IDs.
type Set map[ID]struct{}
// ContainsID returns whether s contains the given ID.
func (s Set) ContainsID(id ID) bool {
_, ok := s[id]
return ok
}
// Contains returns whether s contains the given string.
func (s Set) Contains(table *Table, key string) bool {
k, ok := table.Query(key)
if !ok {
return false
}
_, ok = s[k]
return ok
}
// AddID adds an ID to s, and returns whether it was added.
func (s Set) AddID(id ID) (inserted bool) {
return mapsx.AddZero(s, id)
}
// Add adds a string to s, and returns whether it was added.
func (s Set) Add(table *Table, key string) (inserted bool) {
k := table.Intern(key)
_, ok := s[k]
if !ok {
s[k] = struct{}{}
}
return !ok
}
// Map is a map keyed by intern IDs.
type Map[T any] map[ID]T
// Get returns the value that key maps to.
func (m Map[T]) Get(table *Table, key string) (T, bool) {
k, ok := table.Query(key)
if !ok {
var z T
return z, false
}
v, ok := m[k]
return v, ok
}
// AddID adds an ID to m, and returns whether it was added.
func (m Map[T]) AddID(id ID, v T) (mapped T, inserted bool) {
return mapsx.Add(m, id, v)
}
// Add adds a string to m, and returns whether it was added.
func (m Map[T]) Add(table *Table, key string, v T) (mapped T, inserted bool) {
return m.AddID(table.Intern(key), v)
}
// 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 interval
import (
"fmt"
"iter"
"slices"
"github.com/tidwall/btree"
"golang.org/x/exp/constraints" //nolint:exptostd // Tries to replace w/ cmp.
)
// Intersection is an interval intersection map: a collection of intervals,
// such that given a point in K, one can query for the intersection of all
// intervals in the collection which contain it, along with the values
// associated with each of those intervals.
//
// A zero value is ready to use.
type Intersect[K Endpoint, V any] struct {
// Keys in this map are the ends of intervals in the map.
tree btree.Map[K, *Entry[K, []V]]
pending []*Entry[K, []V] // Scratch space for Insert().
}
// Endpoint is a type that may be used as an interval endpoint.
type Endpoint = constraints.Integer
// Entry is an entry in a [Intersect]. This means that it is the intersection
// of all intervals which contain a particular point.
type Entry[K Endpoint, V any] struct {
Start, End K // The interval range, inclusive.
Value V
}
// Contains returns whether an entry contains a given point.
func (e Entry[K, V]) Contains(point K) bool {
return e.Start <= point && point <= e.End
}
// Get returns the intersection of all intervals which contain point.
//
// If no such interval exists, the [Entry].Values will be nil.
func (m *Intersect[K, V]) Get(point K) Entry[K, []V] {
iter := m.tree.Iter()
found := iter.Seek(point)
if !found || point < iter.Value().Start {
// Check that the interval actually contains key. It is implicit
// already that key <= end.
return Entry[K, []V]{}
}
return *iter.Value()
}
// Entries returns an iterator over the entries in this map.
//
// There exists one entry per maximal subset of the map with non-empty
// intersection. Entries are yielded in order, and are pairwise disjoint.
func (m *Intersect[K, V]) Entries() iter.Seq[Entry[K, []V]] {
return func(yield func(Entry[K, []V]) bool) {
iter := m.tree.Iter()
for more := iter.First(); more; more = iter.Next() {
if !yield(*iter.Value()) {
return
}
}
}
}
// Ranges returns an iterator over the contiguous ranges in this map.
//
// If values is true, the yielded entries will include all of the values that
// fall within those contiguous ranges.
func (m *Intersect[K, V]) Contiguous(values bool) iter.Seq[Entry[K, []V]] {
return func(yield func(Entry[K, []V]) bool) {
iter := m.tree.Iter()
var current Entry[K, []V]
first := true
for more := iter.First(); more; more = iter.Next() {
entry := iter.Value()
switch {
case first:
current = *entry
if !values {
current.Value = nil
}
first = false
case current.End+1 == entry.Start:
current.End = entry.End
if values {
current.Value = append(current.Value, entry.Value...)
}
default:
if !yield(current) {
return
}
current = *entry
if !values {
current.Value = nil
}
}
}
if !first {
yield(current)
}
}
}
// Insert inserts a new interval into this map, with the given associated value.
// Both endpoints are inclusive.
//
// Returns true if the interval was disjoint from all others in the set.
func (m *Intersect[K, V]) Insert(start, end K, value V) (disjoint bool) {
if start > end {
panic(fmt.Sprintf("interval: start (%#v) > end (%#v)", start, end))
}
var prev *Entry[K, []V]
for entry := range m.intersect(start, end) {
if prev == nil && start < entry.Start {
// Need to insert an extra entry for the stuff between start and the
// first interval.
m.pending = append(m.pending, &Entry[K, []V]{
Start: start,
End: entry.Start - 1,
Value: []V{value},
})
}
// Make a copy of entry.Values; entry.Values may get modified in a way
// where appending to it results in value appearing twice.
//
// NB: the values array may be shared across different entries where one
// is a prefix of the other.
orig := entry.Value
// If the entry contains end, we need to split it at end.
if entry.Contains(end) && end < entry.End {
next := &Entry[K, []V]{
Start: entry.Start,
End: end,
Value: append(slices.Clip(orig), value),
}
// Shorten the existing entry.
entry.Start = end + 1
// Add next to the pending queue and use it as the entry here
// onwards.
m.pending = append(m.pending, next)
entry = next
}
// If the entry contains start, we also need to split it.
if entry.Contains(start) && entry.Start < start {
next := &Entry[K, []V]{
Start: entry.Start,
End: start - 1,
Value: orig,
}
// Add next to the pending queue, but *don't* use it as entry,
// because it does not overlap!
m.pending = append(m.pending, next)
// Shorten the existing entry (this one overlaps [a, b]).
entry.Start = start
}
// Add the value to this overlap.
//nolint:gocritic // Slice assignment false positive.
entry.Value = append(orig, value)
if prev != nil && prev.End < entry.Start {
// Add a new interval in between this one and the previous.
m.pending = append(m.pending, &Entry[K, []V]{
Start: prev.End + 1,
End: entry.Start - 1,
Value: []V{value},
})
}
prev = entry
}
if prev != nil && prev.End < end {
// Need to insert an extra entry for the stuff between the
// last interval and end.
m.pending = append(m.pending, &Entry[K, []V]{
Start: prev.End + 1,
End: end,
Value: []V{value},
})
}
for _, entry := range m.pending {
m.tree.Set(entry.End, entry)
}
m.pending = m.pending[:0]
if prev == nil {
m.tree.Set(end, &Entry[K, []V]{
Start: start,
End: end,
Value: []V{value},
})
}
return prev == nil
}
// Format implements [fmt.Formatter].
func (m *Intersect[K, V]) Format(s fmt.State, v rune) {
fmt.Fprint(s, "{")
first := true
m.tree.Scan(func(end K, entry *Entry[K, []V]) bool {
if !first {
fmt.Fprint(s, ", ")
}
first = false
if entry.Start == end {
fmt.Fprintf(s, "%#v: ", entry.Start)
} else {
fmt.Fprintf(s, "[%#v, %#v]: ", entry.Start, end)
}
fmt.Fprintf(s, fmt.FormatString(s, v), entry.Value)
return true
})
fmt.Fprint(s, "}")
}
// intersect returns an iterator over the intervals that intersect [start, end].
func (m *Intersect[K, V]) intersect(start, end K) iter.Seq[*Entry[K, []V]] {
return func(yield func(*Entry[K, []V]) bool) {
// Here, [a, b] is the query interval, and [c, d] is the current
// interval we're looking at.
a, b := start, end
iter := m.tree.Iter()
// We need to walk the tree forwards, finding overlapping intervals,
// until we find an interval that contains b, is greater than b, or we
// reach the end of the tree.
//
// This logic conveniently handles the case where the map is empty,
// and when [a, b] is greater than all other intervals, because Seek()
// will return false.
for more := iter.Seek(a); more; more = iter.Next() {
c, _ := iter.Value().Start, iter.Value().End
// By construction, we already know that a <= d, so if [c, d]
// is less than b or contains it, it intersects [a, b].
//
// If b is less than [c, d], we're already done, so we don't
// need to yield this value. yield() is only called when !(b < c),
// i.e. c <= b.
if b < c || !yield(iter.Value()) {
return
}
}
}
}
// 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 interval
import (
"iter"
"github.com/tidwall/btree"
)
// Nesting is a collection of intervals (and associated values) arranged in
// such a way that splits the collection into strictly nesting sets:
// a strictly nesting set of intervals is one such that no intervals in it
// overlap, except when one set is a strict subset of another.
//
// Inserting n intervals into this set is worst-case O(n^2 log n). Insertion
// order matters: larger intervals should be inserted first. To prevent nesting,
// insert *shorter* intervals first, instead.
type Nesting[K Endpoint, V any] struct {
// Keys in each tree are the ends of the intervals.
sets []*btree.Map[K, *Entry[K, V]]
}
// Clear resets this collection without discarding allocated memory
// (where possible).
func (n *Nesting[K, V]) Clear() {
for _, set := range n.sets {
set.Clear()
}
}
// Entries returns an iterator over the nesting sets in this collection.
//
// Within each set, the order they are yielded in is unspecified.
func (n *Nesting[K, V]) Sets() iter.Seq[iter.Seq[Entry[K, V]]] {
return func(yield func(iter.Seq[Entry[K, V]]) bool) {
for _, set := range n.sets {
if set.Len() == 0 {
return
}
iter := func(yield func(Entry[K, V]) bool) {
set.Scan(func(_ K, value *Entry[K, V]) bool { return yield(*value) })
}
if !yield(iter) {
return
}
}
}
}
// Insert adds a new interval to the collection.
func (n *Nesting[K, V]) Insert(start, end K, value V) {
var found *btree.Map[K, *Entry[K, V]]
for _, set := range n.sets {
// Two cases under which we insert:
//
// 1. We do not intersect anything currently in the set.
// 2. We overlap precisely one interval.
iter := set.Iter()
if !iter.Seek(end) {
// This would be the greatest end in the set, so we need only
// check we don't overlap with the greatest interval currently in
// the set.
if !iter.Last() || iter.Value().End < start {
found = set
break // We're done.
}
continue // Partial overlap with last.
}
// Check if we lie completely inside of the interval we found or
// completely outside of it. If the found interval is [c, d], then
// we want either a < b < c < d or c < a < b < d.
//
// Equivalently, the error condition is a <= c <= b
if start <= iter.Value().Start && iter.Value().Start <= end {
continue
}
// Finally, check that we don't overlap the previous interval. If
// that interval is [c, d], then this is asking for c < d < a < b.
//
// Equivalently, the error condition is a <= d
if iter.Prev() && start <= iter.Value().End {
continue
}
found = set
break // We're done.
}
if found == nil {
found = new(btree.Map[K, *Entry[K, V]])
n.sets = append(n.sets, found)
}
found.Set(end, &Entry[K, V]{Start: start, End: end, Value: value})
}
// 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 internal
import (
"bytes"
"fmt"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
)
// ParsedFile wraps an optional AST and required FileDescriptorProto.
// This is used so types like parser.Result can be passed to this internal package avoiding circular imports.
// Additionally, it makes it less likely that users might specify one or the other.
type ParsedFile interface {
// AST returns the parsed abstract syntax tree. This returns nil if the
// Result was created without an AST.
AST() *ast.FileNode
// FileDescriptorProto returns the file descriptor proto.
FileDescriptorProto() *descriptorpb.FileDescriptorProto
}
// MessageContext provides information about the location in a descriptor
// hierarchy, for adding context to warnings and error messages.
type MessageContext struct {
// The relevant file
File ParsedFile
// The type and fully-qualified name of the element within the file.
ElementType string
ElementName string
// If the element being processed is an option (or *in* an option)
// on the named element above, this will be non-nil.
Option *descriptorpb.UninterpretedOption
// If the element being processed is inside a message literal in an
// option value, this will be non-empty and represent a traversal
// to the element in question.
OptAggPath string
}
func (c *MessageContext) String() string {
var ctx bytes.Buffer
if c.ElementType != "file" {
_, _ = fmt.Fprintf(&ctx, "%s %s: ", c.ElementType, c.ElementName)
}
if c.Option != nil && c.Option.Name != nil {
ctx.WriteString("option ")
writeOptionName(&ctx, c.Option.Name)
if c.File.AST() == nil {
// if we have no source position info, try to provide as much context
// as possible (if nodes != nil, we don't need this because any errors
// will actually have file and line numbers)
if c.OptAggPath != "" {
_, _ = fmt.Fprintf(&ctx, " at %s", c.OptAggPath)
}
}
ctx.WriteString(": ")
}
return ctx.String()
}
func writeOptionName(buf *bytes.Buffer, parts []*descriptorpb.UninterpretedOption_NamePart) {
first := true
for _, p := range parts {
if first {
first = false
} else {
buf.WriteByte('.')
}
nm := p.GetNamePart()
if nm[0] == '.' {
// skip leading dot
nm = nm[1:]
}
if p.GetIsExtension() {
buf.WriteByte('(')
buf.WriteString(nm)
buf.WriteByte(')')
} else {
buf.WriteString(nm)
}
}
}
// 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 messageset
import (
"math"
"sync"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/types/descriptorpb"
)
var (
messageSetSupport bool
messageSetSupportInit sync.Once
)
// CanSupportMessageSets returns true if the protobuf-go runtime supports
// serializing messages with the message set wire format.
func CanSupportMessageSets() bool {
messageSetSupportInit.Do(func() {
// We check using the protodesc package, instead of just relying
// on protolegacy build tag, in case someone links in a fork of
// the protobuf-go runtime that supports legacy proto1 features
// or in case the protobuf-go runtime adds another mechanism to
// enable or disable it (such as environment variable).
_, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
Name: proto.String("test.proto"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("MessageSet"),
Options: &descriptorpb.MessageOptions{
MessageSetWireFormat: proto.Bool(true),
},
ExtensionRange: []*descriptorpb.DescriptorProto_ExtensionRange{
{
Start: proto.Int32(1),
End: proto.Int32(math.MaxInt32),
},
},
},
},
}, nil)
// When message sets are not supported, the above returns an error:
// message "MessageSet" is a MessageSet, which is a legacy proto1 feature that is no longer supported
messageSetSupport = err == nil
})
return messageSetSupport
}
// 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.
//go:build !debug
// See debug.go.
package internal
const Debug = false
func DebugLog([]any, string, string, ...any) {}
// 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 internal
import (
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
)
type hasOptionNode interface {
OptionNode(part *descriptorpb.UninterpretedOption) ast.OptionDeclNode
FileNode() ast.FileDeclNode // needed in order to query for NodeInfo
}
type errorHandler func(span ast.SourceSpan, format string, args ...any) error
func FindFirstOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
return findOption(res, handler, scope, opts, name, false, true)
}
func FindOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string) (int, error) {
return findOption(res, handler, scope, opts, name, true, false)
}
func findOption(res hasOptionNode, handler errorHandler, scope string, opts []*descriptorpb.UninterpretedOption, name string, exact, first bool) (int, error) {
found := -1
for i, opt := range opts {
if exact && len(opt.Name) != 1 {
continue
}
if opt.Name[0].GetIsExtension() || opt.Name[0].GetNamePart() != name {
continue
}
if first {
return i, nil
}
if found >= 0 {
optNode := res.OptionNode(opt)
fn := res.FileNode()
node := optNode.GetName()
nodeInfo := fn.NodeInfo(node)
return -1, handler(nodeInfo, "%s: option %s cannot be defined more than once", scope, name)
}
found = i
}
return found, nil
}
func RemoveOption(uo []*descriptorpb.UninterpretedOption, indexToRemove int) []*descriptorpb.UninterpretedOption {
switch {
case indexToRemove == 0:
return uo[1:]
case indexToRemove == len(uo)-1:
return uo[:len(uo)-1]
default:
return append(uo[:indexToRemove], uo[indexToRemove+1:]...)
}
}
// 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 prototest
import (
"path/filepath"
"runtime"
"testing"
)
// CallerDir returns the directory of the file in which this function is called.
//
// This function is intended for tests to find their test data only. Panics
// if called within a stripped binary.
func CallerDir(t *testing.T) string {
return CallerDirWithSkip(t, 1)
}
// CallerDirWithSkip returns the directory of the file in which this function is
// called.
//
// skip is the number of callers to skip, like in [runtime.Caller]. A value of
// zero represents the caller of CallerDirWithSkip.
//
// This function is intended for tests to find their test data only. Panics
// if called within a stripped binary.
func CallerDirWithSkip(t *testing.T, skip int) string {
_, file, _, ok := runtime.Caller(skip + 1)
if !ok {
t.Fatal("protocompile/internal: could not determine test file's directory; the binary may have been stripped")
}
return filepath.Dir(file)
}
// 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 prototest
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
// RequireSameLayout generates require assertions for ensuring that a and b have
// the same layout.
//
// This is useful for verifying that a type used for unsafe.Pointer shenanigans
// matches another.
//
// NOTE: This will currently recurse infinitely on a type such as
//
// type T struct { p *T }
//
// This function is only intended for testing so actually making sure we don't
// hit that case is not currently necessary.
func RequireSameLayout(t *testing.T, a, b reflect.Type) {
t.Helper()
if a == b {
return // No need to check further.
}
require.Equal(
t, a.Kind(), b.Kind(),
"mismatched kinds: %s is %s; %s is %s", a, a.Kind(), b, b.Kind())
switch a.Kind() {
case reflect.Struct:
require.Equal(t, a.NumField(), b.NumField(),
"mismatched field counts: %s has %d fields; %s has %d fields", a, a.NumField(), b, b.NumField())
for i := range a.NumField() {
RequireSameLayout(t, a.Field(i).Type, b.Field(i).Type)
}
case reflect.Slice, reflect.Chan, reflect.Pointer:
RequireSameLayout(t, a.Elem(), b.Elem())
case reflect.Array:
RequireSameLayout(t, a.Elem(), b.Elem())
require.Equal(t, a.Len(), b.Len(), "mismatched array lengths: %s != %s", a, b)
case reflect.Map:
RequireSameLayout(t, a.Key(), b.Key())
RequireSameLayout(t, a.Elem(), b.Elem())
case reflect.Interface:
require.True(t, a.Implements(b), "mismatched interface types: %s != %s", a, b)
require.True(t, b.Implements(a), "mismatched interface types: %s != %s", a, b)
case reflect.Func:
require.True(t, a.ConvertibleTo(b), "mismatched function types: %s != %s", a, b)
require.True(t, b.ConvertibleTo(a), "mismatched function types: %s != %s", a, b)
default:
// The others are simple scalars, so same kind is sufficient.
}
}
// 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 prototest
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/linker"
"github.com/bufbuild/protocompile/protoutil"
)
func LoadDescriptorSet(t *testing.T, path string, res linker.Resolver) *descriptorpb.FileDescriptorSet {
t.Helper()
data, err := os.ReadFile(path)
require.NoError(t, err)
var fdset descriptorpb.FileDescriptorSet
err = proto.UnmarshalOptions{Resolver: res}.Unmarshal(data, &fdset)
require.NoError(t, err)
return &fdset
}
func CheckFiles(t *testing.T, act protoreflect.FileDescriptor, expSet *descriptorpb.FileDescriptorSet, recursive bool) bool {
t.Helper()
return checkFiles(t, act, expSet, recursive, map[string]struct{}{})
}
func checkFiles(t *testing.T, act protoreflect.FileDescriptor, expSet *descriptorpb.FileDescriptorSet, recursive bool, checked map[string]struct{}) bool {
if _, ok := checked[act.Path()]; ok {
// already checked
return true
}
checked[act.Path()] = struct{}{}
expProto := findFileInSet(expSet, act.Path())
actProto := protoutil.ProtoFromFileDescriptor(act)
ret := AssertMessagesEqual(t, expProto, actProto, expProto.GetName())
if recursive {
for i := range act.Imports().Len() {
if !checkFiles(t, act.Imports().Get(i), expSet, true, checked) {
ret = false
}
}
}
return ret
}
func findFileInSet(fps *descriptorpb.FileDescriptorSet, name string) *descriptorpb.FileDescriptorProto {
files := fps.File
for _, fd := range files {
if fd.GetName() == name {
return fd
}
}
return nil
}
func AssertMessagesEqual(t *testing.T, exp, act proto.Message, description string) bool {
t.Helper()
if diff := cmp.Diff(exp, act, protocmp.Transform(), cmpopts.EquateNaNs()); diff != "" {
t.Errorf("%s: message mismatch (-want, +got):\n%s", description, diff)
return false
}
return true
}
// 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 prototest
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/protocolbuffers/protoscope"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/bufbuild/protocompile/experimental/dom"
"github.com/bufbuild/protocompile/internal/ext/cmpx"
"github.com/bufbuild/protocompile/internal/ext/iterx"
)
// ToYAMLOptions contains configuration for [ToYAML].
type ToYAMLOptions struct {
// The maximum column width before wrapping starts to occur.
MaxWidth int
// The indentation string to use. If empty, defaults to " ".
Indent string
}
// ToYAML converts a Protobuf message into a YAML document in a deterministic
// manner. This is intended for generating YAML for golden outputs.
//
// The result will use a compressed representation where possible.
func ToYAML(m proto.Message, opts ToYAMLOptions) string {
if opts.MaxWidth == 0 {
opts.MaxWidth = 80
}
if len(opts.Indent) < 2 {
opts.Indent = " "
}
d := opts.message(m.ProtoReflect())
d.prepare()
return dom.Render(dom.Options{
MaxWidth: opts.MaxWidth,
}, func(push dom.Sink) { d.render(renderArgs{ToYAMLOptions: opts, root: true}, push) })
}
// message converts a Protobuf message into a [doc], which is used as an
// intermediate processing stage to help make formatting decisions
// (such as compressing nested messages).
func (y ToYAMLOptions) message(m protoreflect.Message) *doc {
d := new(doc)
entries := slices.Collect(iterx.Left(m.Range))
slices.SortFunc(entries, cmpx.Join(
cmpx.Map(protoreflect.FieldDescriptor.IsExtension, cmpx.Bool),
cmpx.Key(protoreflect.FieldDescriptor.Index),
cmpx.Key(protoreflect.FieldDescriptor.Number),
))
for _, f := range entries {
y := y.value(m.Get(f), f)
if f.IsExtension() {
d.push("("+f.FullName()+")", y)
} else {
d.push(f.Name(), y)
}
}
unknown := m.GetUnknown()
if len(unknown) > 0 {
d.pairs = append(d.pairs, [2]any{
protoreflect.Name("$unknown"),
protoscopeString(protoscope.Write(unknown, protoscope.WriterOptions{})),
})
}
return d
}
// value converts a Protobuf value into a value that can be placed into a
// [doc].
func (y ToYAMLOptions) value(v protoreflect.Value, f protoreflect.FieldDescriptor) any {
switch v := v.Interface().(type) {
case protoreflect.Message:
return y.message(v)
case protoreflect.List:
d := new(doc)
for i := range v.Len() {
d.push(nil, y.value(v.Get(i), f))
}
return d
case protoreflect.Map:
d := new(doc)
d.needsSort = true
v.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
d.push(
y.value(k.Value(), f.MapKey()),
y.value(v, f.MapValue()),
)
return true
})
return d
case protoreflect.EnumNumber:
enum := f.Enum()
if value := enum.Values().ByNumber(v); value != nil {
return value.Name()
}
return int32(v)
case []byte:
return string(v)
default:
return v
}
}
// prepare prepares a document for printing by compressing elements as
// appropriate.
func (d *doc) prepare() {
if d.needsSort {
slices.SortFunc(d.pairs, func(a, b [2]any) int {
return cmpx.Any(a[0], b[0])
})
}
for i := range d.pairs {
pair := &d.pairs[i]
if v, ok := pair[1].(*doc); ok {
v.prepare()
}
for {
v, ok := pair[1].(*doc)
if !ok || len(v.pairs) != 1 {
break
}
outer, ok1 := pair[0].(protoreflect.Name)
inner, ok2 := v.pairs[0][0].(protoreflect.Name)
if !ok1 || !ok2 {
break
}
//nolint:unconvert // Conversion below is included for readability.
pair[0] = protoreflect.Name(outer + "." + inner)
pair[1] = v.pairs[0][1]
}
}
}
type renderArgs struct {
ToYAMLOptions
root bool
inList bool
}
type protoscopeString string
func (d *doc) render(args renderArgs, push dom.Sink) {
value := func(args renderArgs, v any, push dom.Sink) {
switch v := v.(type) {
case protoscopeString:
{
v := strings.TrimSpace(string(v))
if strings.Contains(v, "\n") {
push(
dom.Text("|"), dom.Text("\n"),
dom.Indent(args.Indent, func(push dom.Sink) {
for chunk := range strings.SplitSeq(v, "\n") {
push(dom.Text(chunk), dom.Text("\n"))
}
}),
)
} else {
push(dom.Text(strconv.Quote(v)))
}
}
case string:
push(dom.Text(strconv.Quote(v)))
case []byte:
push(dom.Text(strconv.Quote(string(v))))
case *doc:
v.render(args, push)
default:
push(dom.Text(fmt.Sprint(v)))
}
}
if d.isArray {
push(dom.Group(0, func(push dom.Sink) {
push(
dom.TextIf(dom.Flat, "["),
dom.TextIf(dom.Broken, "\n"),
dom.Indent(args.Indent[2:], func(push dom.Sink) {
for i, pair := range d.pairs {
if i > 0 {
push(dom.TextIf(dom.Flat, ","), dom.TextIf(dom.Flat, " "))
}
push(
dom.TextIf(dom.Broken, "-"), dom.TextIf(dom.Broken, " "),
dom.Indent(" ", func(push dom.Sink) {
if v, ok := pair[1].(*doc); ok && len(v.pairs) == 1 {
push(
dom.GroupIf(dom.Broken, 0, func(push dom.Sink) {
args := args
args.root = false
args.inList = false
pair := v.pairs[0]
value(args, pair[0], push)
push(dom.Text(":"), dom.Text(" "))
value(args, pair[1], push)
}),
dom.GroupIf(dom.Flat, 0, func(push dom.Sink) {
args := args
args.root = false
args.inList = true
value(args, pair[1], push)
}),
)
} else {
args := args
args.root = false
args.inList = true
value(args, pair[1], push)
}
push(dom.TextIf(dom.Broken, "\n"))
}))
}
}),
dom.TextIf(dom.Flat, "]"))
}))
return
}
if len(d.pairs) == 0 {
push(dom.Text("{}"))
return
}
push(dom.Group(0, func(push dom.Sink) {
if !args.root && !args.inList {
push(dom.TextIf(dom.Broken, "\n"))
}
indent := args.Indent
if args.root || args.inList {
indent = ""
}
push(
dom.TextIf(dom.Flat, "{"), dom.TextIf(dom.Flat, " "),
dom.Indent(indent, func(push dom.Sink) {
for i, pair := range d.pairs {
if i > 0 {
push(dom.TextIf(dom.Flat, ","), dom.TextIf(dom.Flat, " "))
}
args := args
args.root = false
args.inList = false
value(args, pair[0], push)
push(dom.Text(":"), dom.Text(" "))
value(args, pair[1], push)
push(dom.TextIf(dom.Broken, "\n"))
}
}),
dom.TextIf(dom.Flat, " "), dom.TextIf(dom.Flat, "}"),
)
if args.root {
push(dom.Text("\n"))
}
}))
}
// doc is a generic document structure used as an intermediate for generating
// the compressed output of ToYAML.
//
// It is composed of an array of pairs of arbitrary values.
type doc struct {
pairs [][2]any
isArray, needsSort bool
}
// push adds a new entry to this document.
//
// All pushes entries must either have a non-nil key OR a nil key.
func (d *doc) push(k, v any) {
if len(d.pairs) == 0 {
d.isArray = k == nil
} else if d.isArray != (k == nil) {
panic("misuse of doc.push()")
}
d.pairs = append(d.pairs, [2]any{k, v})
}
// 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 dualcompiler
import (
"io"
"io/fs"
"github.com/bufbuild/protocompile"
"github.com/bufbuild/protocompile/experimental/source"
)
// resolverOpener adapts a protocompile.Resolver to the source.Opener interface
// used by the experimental compiler.
type resolverOpener struct {
resolver protocompile.Resolver
}
// ResolverToOpener converts a Resolver to an Opener.
// Note: This adapter only supports SearchResult.Source. Other result types
// (AST, Proto, ParseResult, Desc) will return an error.
func ResolverToOpener(resolver protocompile.Resolver) source.Opener {
return &resolverOpener{resolver: resolver}
}
// Open implements source.Opener.
func (r *resolverOpener) Open(path string) (*source.File, error) {
result, err := r.resolver.FindFileByPath(path)
if err != nil {
return nil, err
}
// Handle the Source result type (most common in tests)
if result.Source != nil {
data, err := io.ReadAll(result.Source)
if err != nil {
return nil, err
}
return source.NewFile(path, string(data)), nil
}
// For other result types, we need to convert them to source.
// For now, we don't support these cases.
//
// For AST and Proto, return an error since these should be converted.
// For Desc, return ErrNotExist to allow fallback to WKTs source files.
// This is important because protocompile.WithStandardImports returns
// Desc for WKTs, but the experimental compiler needs source files.
if result.AST != nil {
return nil, fs.ErrNotExist
}
if result.Proto != nil {
return nil, fs.ErrNotExist
}
if result.Desc != nil {
// Return not found so the Openers can try the next opener (WKTs)
return nil, fs.ErrNotExist
}
// Note: We skip checking ParseResult as it can cause nil pointer issues
// and we primarily support Source-based resolution for tests.
// No result found
return nil, fs.ErrNotExist
}
// 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 dualcompiler
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile"
)
const (
// defaultTestdataPath is the relative path from internal/testing/dualcompiler to testdata.
defaultTestdataPath = "../../testdata"
)
// SkipConfig controls which compilers to skip for a particular test.
type SkipConfig struct {
// SkipOld causes the old compiler to be skipped.
SkipOld bool
// SkipNew causes the new compiler to be skipped.
SkipNew bool
// SkipReason is the reason for skipping (used in t.Skip message).
SkipReason string
}
// RunWithBothCompilers runs the given test function with both the old and new compilers.
// The test function receives a CompilerInterface that can be used to compile proto files.
// Uses default configuration (testdata ImportPaths with standard imports).
//
// The test will run in parallel subtests, one for each compiler.
//
// Example usage:
//
// func TestMyFeature(t *testing.T) {
// t.Parallel()
// dualcompiler.RunWithBothCompilers(t, func(t *testing.T, compiler dualcompiler.CompilerInterface) {
// result, err := compiler.Compile(t.Context(), "test.proto")
// require.NoError(t, err)
// // ... test logic
// })
// }
func RunWithBothCompilers(t *testing.T, testFunc func(t *testing.T, compiler CompilerInterface)) {
t.Helper()
RunWithBothCompilersIf(t, SkipConfig{}, nil, testFunc)
}
// RunWithBothCompilersIf is like RunWithBothCompilers but allows skipping specific compilers.
// It also accepts compiler options to customize the compiler configuration.
//
// Example usage:
//
// func TestSourceCodeInfo(t *testing.T) {
// t.Parallel()
// skip := dualcompiler.SkipConfig{
// SkipNew: true,
// SkipReason: "source code info not yet implemented in experimental compiler",
// }
// resolver := protocompile.WithStandardImports(&protocompile.SourceResolver{
// ImportPaths: []string{"internal/testdata"},
// })
// opts := []dualcompiler.CompilerOption{
// dualcompiler.WithResolver(resolver),
// dualcompiler.WithSourceInfoMode(protocompile.SourceInfoStandard),
// }
// dualcompiler.RunWithBothCompilersIf(t, skip, opts, func(t *testing.T, compiler dualcompiler.CompilerInterface) {
// // ... test logic
// })
// }
func RunWithBothCompilersIf(
t *testing.T,
skip SkipConfig,
opts []CompilerOption,
testFunc func(t *testing.T, compiler CompilerInterface),
) {
t.Helper()
if !skip.SkipOld {
t.Run("old_compiler", func(t *testing.T) {
t.Helper()
t.Parallel()
compiler := SetupOldCompilerWithOptions(t, opts)
testFunc(t, compiler)
})
}
if !skip.SkipNew {
t.Run("new_compiler", func(t *testing.T) {
t.Helper()
t.Parallel()
if skip.SkipReason != "" {
t.Skip(skip.SkipReason)
}
compiler := SetupNewCompilerWithOptions(t, opts)
testFunc(t, compiler)
})
}
}
// SetupOldCompiler creates a CompilerInterface for the old compiler with standard test configuration.
// The returned compiler will:
// - Use a SourceResolver with ImportPaths set to internal/testdata
// - Include standard imports (WKTs).
func SetupOldCompiler(t *testing.T) CompilerInterface {
t.Helper()
resolver := protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{defaultTestdataPath},
})
return NewOldCompiler(WithResolver(resolver))
}
// SetupOldCompilerWithOptions creates a CompilerInterface for the old compiler with custom configuration.
// If no resolver is specified in opts, a default resolver with standard imports will be used.
func SetupOldCompilerWithOptions(t *testing.T, opts []CompilerOption) CompilerInterface {
t.Helper()
config := &compilerConfig{}
for _, opt := range opts {
opt(config)
}
if config.resolver == nil {
resolver := protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{defaultTestdataPath},
})
opts = append([]CompilerOption{WithResolver(resolver)}, opts...)
}
return NewOldCompiler(opts...)
}
// SetupNewCompiler creates a CompilerInterface for the new compiler with standard test configuration.
// The returned compiler will:
// - Use a SourceResolver with ImportPaths set to internal/testdata
// - Include WKTs via source.WKTs().
func SetupNewCompiler(t *testing.T) CompilerInterface {
t.Helper()
resolver := &protocompile.SourceResolver{
ImportPaths: []string{defaultTestdataPath},
}
return NewNewCompiler(WithResolver(resolver))
}
// SetupNewCompilerWithOptions creates a CompilerInterface for the new compiler with custom configuration.
// If no resolver is specified in opts, a default resolver will be used.
func SetupNewCompilerWithOptions(t *testing.T, opts []CompilerOption) CompilerInterface {
t.Helper()
config := &compilerConfig{}
for _, opt := range opts {
opt(config)
}
if config.resolver == nil {
resolver := &protocompile.SourceResolver{
ImportPaths: []string{defaultTestdataPath},
}
opts = append([]CompilerOption{WithResolver(resolver)}, opts...)
}
return NewNewCompiler(opts...)
}
// RunAndCompare runs a test with both compilers and compares their outputs.
// This is a convenience wrapper that combines RunWithBothCompilers with CompareCompilationResults.
// Uses default configuration (testdata ImportPaths with standard imports).
//
// The compile function should compile the same files with both compilers and return the results.
// Source code info is stripped by default before comparison.
//
// Example usage:
//
// func TestMyFeature(t *testing.T) {
// t.Parallel()
// dualcompiler.RunAndCompare(t, func(t *testing.T, oldCompiler, newCompiler dualcompiler.CompilerInterface) (old, new dualcompiler.CompilationResult) {
// // Compile with both
// oldResult, err := oldCompiler.Compile(t.Context(), "test.proto")
// require.NoError(t, err)
// newResult, err := newCompiler.Compile(t.Context(), "test.proto")
// require.NoError(t, err)
// return oldResult, newResult
// })
// }
func RunAndCompare(
t *testing.T,
compileFunc func(t *testing.T, oldCompiler, newCompiler CompilerInterface) (oldResult, newResult CompilationResult),
) {
t.Helper()
RunAndCompareIf(t, SkipConfig{}, nil, compileFunc)
}
// RunAndCompareIf is like RunAndCompare but allows skipping specific compilers
// and customizing the compiler configuration with options.
func RunAndCompareIf(
t *testing.T,
skip SkipConfig,
opts []CompilerOption,
compileFunc func(t *testing.T, oldCompiler, newCompiler CompilerInterface) (oldResult, newResult CompilationResult),
) {
t.Helper()
// If either compiler is skipped, we can't compare
if skip.SkipOld || skip.SkipNew {
t.Skip("Skipping comparison test because one compiler is skipped:", skip.SkipReason)
return
}
// Set up both compilers
oldCompiler := SetupOldCompilerWithOptions(t, opts)
newCompiler := SetupNewCompilerWithOptions(t, opts)
// Compile with both
oldResult, newResult := compileFunc(t, oldCompiler, newCompiler)
// Compare results (strip source info by default)
compareCompilationResults(t, oldResult, newResult, true)
}
// compareCompilationResults compares two CompilationResults to ensure they produce
// equivalent FileDescriptorProtos. This is useful for verifying that both compilers
// produce the same output.
//
// By default, source code info is stripped before comparison since the new compiler
// doesn't yet support it. Set stripSourceInfo to false to include source info in the comparison.
func compareCompilationResults(t *testing.T, result1, result2 CompilationResult, stripSourceInfo bool) {
t.Helper()
files1 := result1.Files()
files2 := result2.Files()
require.Equal(t, len(files1), len(files2), "different number of files")
for i := range files1 {
fdp1, err := files1[i].FileDescriptorProto()
require.NoError(t, err, "failed to get FileDescriptorProto from file %d", i)
fdp2, err := files2[i].FileDescriptorProto()
require.NoError(t, err, "failed to get FileDescriptorProto from file %d", i)
if stripSourceInfo {
fdp1 = stripSourceCodeInfo(fdp1)
fdp2 = stripSourceCodeInfo(fdp2)
}
assert.Empty(t, cmp.Diff(fdp1, fdp2, protocmp.Transform()))
}
}
// stripSourceCodeInfo returns a copy of the FileDescriptorProto with source_code_info removed.
func stripSourceCodeInfo(fdp *descriptorpb.FileDescriptorProto) *descriptorpb.FileDescriptorProto {
clone := proto.CloneOf(fdp)
clone.SourceCodeInfo = nil
return clone
}
// 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 dualcompiler provides a test abstraction layer for running tests
// with both the old protocompile.Compiler and the new experimental compiler.
package dualcompiler
import (
"context"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile"
)
// CompilerInterface abstracts the differences between the old protocompile.Compiler
// and the new experimental compiler.
type CompilerInterface interface {
// Compile compiles the given proto files and returns the compilation result.
Compile(ctx context.Context, files ...string) (CompilationResult, error)
// Name returns a descriptive name for this compiler (used in test output).
Name() string
}
// CompilationResult wraps the output of compilation from either compiler.
type CompilationResult interface {
// Files returns all compiled files.
Files() []CompiledFile
}
// CompiledFile represents a single compiled proto file from either compiler.
type CompiledFile interface {
// Path returns the file path.
Path() string
// FileDescriptor returns the file as a protoreflect.FileDescriptor.
FileDescriptor() (protoreflect.FileDescriptor, error)
// FileDescriptorProto returns the file as a descriptorpb.FileDescriptorProto.
// This is the primary method for comparing outputs between compilers.
FileDescriptorProto() (*descriptorpb.FileDescriptorProto, error)
}
// CompilerOption is a function that configures a compiler adapter.
type CompilerOption func(config *compilerConfig)
// compilerConfig holds configuration for compiler adapters.
type compilerConfig struct {
// Resolver is the protocompile.Resolver to use for finding proto files.
// If nil, a default resolver will be used.
resolver protocompile.Resolver
// SourceInfoMode controls source code info generation (old compiler only).
sourceInfoMode protocompile.SourceInfoMode
}
// WithResolver sets the resolver to use for finding proto files.
func WithResolver(resolver protocompile.Resolver) CompilerOption {
return func(config *compilerConfig) {
config.resolver = resolver
}
}
// WithSourceInfoMode sets the source info mode for the old compiler.
func WithSourceInfoMode(mode protocompile.SourceInfoMode) CompilerOption {
return func(config *compilerConfig) {
config.sourceInfoMode = mode
}
}
// 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 dualcompiler
import (
"context"
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/experimental/fdp"
"github.com/bufbuild/protocompile/experimental/incremental"
"github.com/bufbuild/protocompile/experimental/incremental/queries"
"github.com/bufbuild/protocompile/experimental/ir"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/source"
)
// newCompilerAdapter wraps the experimental incremental compiler.
type newCompilerAdapter struct {
executor *incremental.Executor
opener source.Opener
session *ir.Session
includeSourceCodeInfo bool
}
// NewNewCompiler creates a new CompilerInterface wrapping the experimental compiler.
// Use WithResolver option to specify a custom resolver.
// The resolver will be converted to an Opener and combined with WKTs.
func NewNewCompiler(opts ...CompilerOption) CompilerInterface {
config := &compilerConfig{}
for _, opt := range opts {
opt(config)
}
// Create an opener that combines the resolver with WKTs.
// WKTs are checked first so they're returned as source files, not descriptors.
var opener source.Opener
if config.resolver != nil {
resolverOpener := ResolverToOpener(config.resolver)
wkts := source.WKTs()
opener = &source.Openers{wkts, resolverOpener}
} else {
opener = source.WKTs()
}
var includeSourceCodeInfo bool
if config.sourceInfoMode != 0 {
includeSourceCodeInfo = true
}
return &newCompilerAdapter{
executor: incremental.New(),
opener: opener,
session: &ir.Session{},
includeSourceCodeInfo: includeSourceCodeInfo,
}
}
// Name implements CompilerInterface.
func (a *newCompilerAdapter) Name() string {
return "new_compiler"
}
// Compile implements CompilerInterface.
func (a *newCompilerAdapter) Compile(ctx context.Context, files ...string) (CompilationResult, error) {
// Create IR queries for each file
qs := make([]incremental.Query[*ir.File], len(files))
for i, file := range files {
qs[i] = queries.IR{
Opener: a.opener,
Session: a.session,
Path: file,
}
}
// Run the queries
results, rpt, err := incremental.Run(ctx, a.executor, qs...)
if err != nil {
return nil, err
}
// Check for fatal errors in individual results
irFiles := make([]*ir.File, 0, len(results))
for i, result := range results {
if result.Fatal != nil {
return nil, fmt.Errorf("compilation failed for %s: %w", files[i], result.Fatal)
}
irFiles = append(irFiles, result.Value)
}
// Check for errors in the report
for _, diag := range rpt.Diagnostics {
if diag.Level() == report.Error || diag.Level() == report.ICE {
return nil, fmt.Errorf("%v", diag)
}
}
return &newCompilationResult{
files: irFiles,
includeSourceCodeInfo: a.includeSourceCodeInfo,
}, nil
}
// newCompilationResult wraps IR files.
type newCompilationResult struct {
files []*ir.File
includeSourceCodeInfo bool
}
// Files implements CompilationResult.
func (r *newCompilationResult) Files() []CompiledFile {
result := make([]CompiledFile, len(r.files))
for i, file := range r.files {
result[i] = &newCompiledFile{
file: file,
includeSourceCodeInfo: r.includeSourceCodeInfo,
}
}
return result
}
// newCompiledFile wraps an ir.File.
type newCompiledFile struct {
file *ir.File
includeSourceCodeInfo bool
}
// Path implements CompiledFile.
func (f *newCompiledFile) Path() string {
return f.file.Path()
}
// FileDescriptor implements CompiledFile.
// Converts the FileDescriptorProto to a FileDescriptor using protodesc.
// Dependencies are resolved using the global registry (includes WKTs and other registered files).
func (f *newCompiledFile) FileDescriptor() (protoreflect.FileDescriptor, error) {
fdp, err := f.FileDescriptorProto()
if err != nil {
return nil, err
}
return protodesc.NewFile(fdp, protoregistry.GlobalFiles)
}
// FileDescriptorProto implements CompiledFile.
func (f *newCompiledFile) FileDescriptorProto() (*descriptorpb.FileDescriptorProto, error) {
data, err := fdp.DescriptorProtoBytes(
f.file,
fdp.IncludeSourceCodeInfo(f.includeSourceCodeInfo),
)
if err != nil {
return nil, err
}
fdp := &descriptorpb.FileDescriptorProto{}
if err := proto.Unmarshal(data, fdp); err != nil {
return nil, err
}
return fdp, nil
}
// 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 dualcompiler
import (
"context"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile"
"github.com/bufbuild/protocompile/linker"
"github.com/bufbuild/protocompile/protoutil"
)
// oldCompilerAdapter wraps the old protocompile.Compiler.
type oldCompilerAdapter struct {
compiler *protocompile.Compiler
}
// NewOldCompiler creates a new CompilerInterface wrapping the old protocompile.Compiler.
// Use WithResolver option to specify a custom resolver.
func NewOldCompiler(opts ...CompilerOption) CompilerInterface {
config := &compilerConfig{}
for _, opt := range opts {
opt(config)
}
compiler := &protocompile.Compiler{
Resolver: config.resolver,
}
if config.sourceInfoMode != 0 {
compiler.SourceInfoMode = config.sourceInfoMode
}
return &oldCompilerAdapter{
compiler: compiler,
}
}
// Name implements CompilerInterface.
func (a *oldCompilerAdapter) Name() string {
return "old_compiler"
}
// Compile implements CompilerInterface.
func (a *oldCompilerAdapter) Compile(ctx context.Context, files ...string) (CompilationResult, error) {
linkerFiles, err := a.compiler.Compile(ctx, files...)
if err != nil {
return nil, err
}
return &oldCompilationResult{
files: linkerFiles,
}, nil
}
// oldCompilationResult wraps linker.Files.
type oldCompilationResult struct {
files linker.Files
}
// Files implements CompilationResult.
func (r *oldCompilationResult) Files() []CompiledFile {
result := make([]CompiledFile, len(r.files))
for i, file := range r.files {
result[i] = &oldCompiledFile{file: file}
}
return result
}
// oldCompiledFile wraps a linker.File.
type oldCompiledFile struct {
file linker.File
}
// Path implements CompiledFile.
func (f *oldCompiledFile) Path() string {
return f.file.Path()
}
// FileDescriptor implements CompiledFile.
func (f *oldCompiledFile) FileDescriptor() (protoreflect.FileDescriptor, error) {
// linker.File already implements protoreflect.FileDescriptor
return f.file, nil
}
// FileDescriptorProto implements CompiledFile.
func (f *oldCompiledFile) FileDescriptorProto() (*descriptorpb.FileDescriptorProto, error) {
return protoutil.ProtoFromFileDescriptor(f.file), nil
}
// 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 toposort provides a generic topological sort implementation.
package toposort
import (
"fmt"
"iter"
"github.com/bufbuild/protocompile/internal/ext/slicesx"
)
const (
unsorted byte = iota
walking
sorted
)
// Sort sorts a DAG topologically.
//
// Roots are the nodes whose dependencies we are querying. key returns a
// comparable key for each node. dag contains the data of the DAG being sorted,
// and returns the children of a node.
func Sort[Node any, Key comparable](
roots []Node,
key func(Node) Key,
dag func(Node) iter.Seq[Node],
) iter.Seq[Node] {
s := Sorter[Node, Key]{Key: key}
return s.Sort(roots, dag)
}
// Sorter is reusable scratch space for a particular stencil of [Sort], which
// needs to allocate memory for book-keeping. This struct allows amortizing that
// cost.
type Sorter[Node any, Key comparable] struct {
// A function to extract a unique key from each node, for marking.
Key func(Node) Key
state map[Key]byte
stack []Node
iterating bool
}
// Sort is like [Sort], but re-uses allocated resources stored in s.
func (s *Sorter[Node, Key]) Sort(
roots []Node,
dag func(Node) iter.Seq[Node],
) iter.Seq[Node] {
if s.state == nil {
s.state = make(map[Key]byte)
}
return func(yield func(Node) bool) {
if s.iterating {
panic("internal/toposort: Sort() called reëntrantly")
}
s.iterating = true
defer func() {
clear(s.state)
clear(s.stack)
s.stack = s.stack[:0]
s.iterating = false
}()
for _, root := range roots {
s.push(root)
// This algorithm is DFS that has been tail-call-optimized into a loop.
// Each node is visited twice in the loop: once to add its children to
// the stack, and once to pop it and add it to the output. The state
// tracks whether this node has been visisted and if its the first
// or second visit through the loop.
for len(s.stack) > 0 {
node, _ := slicesx.Last(s.stack)
k := s.Key(node)
state := s.state[k]
if state == unsorted {
s.state[k] = walking
for child := range dag(node) {
s.push(child)
}
continue
}
s.stack = s.stack[:len(s.stack)-1]
if state != sorted {
if !yield(node) {
return
}
s.state[k] = sorted
}
}
}
}
}
func (s *Sorter[Node, Key]) push(v Node) {
k := s.Key(v)
switch s.state[k] {
case unsorted:
s.stack = append(s.stack, v)
case walking:
prev := slicesx.LastIndexFunc(s.stack, func(n Node) bool {
return s.Key(n) == k
})
suffix := s.stack[prev:]
panic(fmt.Sprintf("protocompile/internal: cycle detected: %v -> %v", slicesx.Join(suffix, "->"), v))
case sorted:
return
}
}
// 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 trie
import (
"fmt"
"math"
"math/bits"
"slices"
"strings"
"unsafe"
)
var allOnes = slices.Repeat([]uint64{math.MaxUint64}, 16)
// nybbles is a nybble radix trie, with different choices of index size
// for potential memory compaction.
type nybbles[N uint8 | uint16 | uint32 | uint64] struct {
// A prefix trie for byte strings.
//
// To walk to the node corresponding to a given string, we follow the
// indices as follows:
//
// n := 0
// for b := range bytes(s) {
// n = lo[hi[n][b.hi]][b.lo]
// }
//
// The resulting value of n is the index into values which
// contains the corresponding value. If any index in hi/lo turns up
// MaxUint, iteration stops, indicating the tree ends there.
hi, lo [][16]N
hasValue []uint
}
// search walks the trie along the path given by the
// given key, yielding prefixes and indices for each node visited.
func (t *nybbles[N]) search(key string, yield func(string, int) bool) {
if t.has(0) && !yield("", 0) {
return
}
var n int
for i := range len(key) {
b := key[i]
lo, hi := b&0xf, b>>4
if len(t.hi) <= n {
break
}
m := int(t.hi[n][hi])
if len(t.lo) <= m {
break
}
n = int(t.lo[m][lo])
if t.has(n) && !yield(key[:i+1], n) {
return
}
}
}
// insert adds a new key to the trie; returns the index to insert the
// corresponding value at.
//
// Returns -1 if the trie becomes full and needs to have its index grown.
func (t *nybbles[N]) insert(key string) int {
if t.hi == nil {
t.appendAllOnes(&t.hi)
}
n := 0
for i := range len(key) {
b := key[i]
lo, hi := b&0xf, b>>4
m1 := &t.hi[n][hi]
if len(t.lo) <= int(*m1) {
*m1 = N(len(t.lo))
t.appendAllOnes(&t.lo)
}
m2 := &t.lo[uint32(*m1)][lo]
if len(t.hi) <= int(*m2) {
if len(t.hi) == int(^N(0)) {
return -1
}
*m2 = N(len(t.hi))
t.appendAllOnes(&t.hi)
}
n = int(*m2)
}
t.set(n)
return n
}
func (t *nybbles[N]) appendAllOnes(s *[][16]N) {
ptr := (*[16]N)(unsafe.Pointer(unsafe.SliceData(allOnes)))
*s = append(*s, *ptr)
}
func (t *nybbles[N]) has(n int) bool {
i := n / bits.UintSize
j := n % bits.UintSize
return i < len(t.hasValue) && t.hasValue[i]&(uint(1)<<j) != 0
}
func (t *nybbles[N]) set(n int) {
i := n / bits.UintSize
j := n % bits.UintSize
if len(t.hasValue) <= i {
t.hasValue = append(t.hasValue, make([]uint, i+1-len(t.hasValue))...)
}
t.hasValue[i] |= uint(1) << j
}
func (t *nybbles[N]) dump(buf *strings.Builder) {
var z N
fmt.Fprintf(buf, "type: *nybbles[%T]\n", z)
for i, v := range t.hi {
fmt.Fprintf(buf, "hi[%#x]:", i)
for _, i := range v {
if ^i == 0 {
buf.WriteString(" --")
} else {
fmt.Fprintf(buf, " %02x", i)
}
}
fmt.Fprintln(buf)
}
for i, v := range t.lo {
fmt.Fprintf(buf, "lo[%#x]:", i)
for _, i := range v {
if ^i == 0 {
buf.WriteString(" --")
} else {
fmt.Fprintf(buf, " %02x", i)
}
}
fmt.Fprintln(buf)
}
}
func grow[To, From uint8 | uint16 | uint32 | uint64](in *nybbles[From]) *nybbles[To] {
conv := func(in [][16]From) [][16]To {
out := make([][16]To, len(in))
for i, x := range in {
var y [16]To
for i := range 16 {
y[i] = To(x[i])
if ^x[i] == 0 {
y[i] = ^To(0)
}
}
out[i] = y
}
return out
}
return &nybbles[To]{
hi: conv(in.hi),
lo: conv(in.lo),
hasValue: in.hasValue,
}
}
// 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 trie
import (
"iter"
"strings"
"github.com/bufbuild/protocompile/internal/ext/iterx"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)
// Trie implements a map from strings to V, except lookups return the key
// which is the longest prefix of a given query.
//
// The zero value is empty and ready to use.
type Trie[V any] struct {
impl interface {
search(key string, yield func(string, int) bool)
insert(key string) int
dump(*strings.Builder)
}
values []V
}
// Get returns the value corresponding to the longest prefix of key present
// in the trie, such that the prefix and its associated value satisfy the
// predicate ok (ok == nil implies that all values are ok).
//
// If no key in the trie is a prefix of key, returns "" and the zero value of v.
// The match is exact when len(key) == len(prefix).
func (t *Trie[V]) Get(key string) (prefix string, value V) {
prefix, value, _ = iterx.Last2(t.Prefixes(key))
return prefix, value
}
// Prefixes returns an iterator over prefixes of key within the trie, and their
// associated values.
func (t *Trie[V]) Prefixes(key string) iter.Seq2[string, V] {
return func(yield func(string, V) bool) {
if t.impl == nil {
return
}
adapt := func(prefix string, index int) bool {
return yield(prefix, t.values[index])
}
// No implementation of impl will ever cause adapt to escape. This
// avoids a heap allocation.
adapt = *unsafex.NoEscape(&adapt)
t.impl.search(key, adapt)
}
}
// Insert adds a new value to this trie.
func (t *Trie[V]) Insert(key string, value V) {
if t.impl == nil {
t.impl = &nybbles[uint8]{}
}
again:
n := t.impl.insert(key)
if n == -1 {
switch impl := t.impl.(type) {
case *nybbles[uint8]:
t.impl = grow[uint16](impl)
case *nybbles[uint16]:
t.impl = grow[uint32](impl)
case *nybbles[uint32]:
t.impl = grow[uint64](impl)
default:
panic("unreachable")
}
goto again
}
if len(t.values) <= n {
t.values = append(t.values, make([]V, n+1-len(t.values))...)
}
t.values[n] = value
}
// 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 internal
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/bufbuild/protocompile/internal/cases"
)
// JSONName returns the default JSON name for a field with the given name.
// This mirrors the algorithm in protoc:
//
// https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L95
func JSONName(name string) string {
return cases.Converter{
Case: cases.Camel,
NaiveSplit: true,
NoLowercase: true,
}.Convert(name)
}
// MapEntry returns the map entry name for a field with the given name.
// This mirrors the algorithm in protoc:
//
// https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L95
func MapEntry(name string) string {
buf := new(strings.Builder)
cases.Converter{
Case: cases.Pascal,
NaiveSplit: true,
NoLowercase: true,
}.Append(buf, name)
_, _ = buf.WriteString("Entry")
return buf.String()
}
// TrimPrefix is used to remove the given prefix from the given str. It does not require
// an exact match and ignores case and underscores. If the all non-underscore characters
// would be removed from str, str is returned unchanged. If str does not have the given
// prefix (even with the very lenient matching, in regard to case and underscores), then
// str is returned unchanged.
//
// The algorithm is adapted from the protoc source:
//
// https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L922
func TrimPrefix(str, prefix string) string {
j := 0
for i, r := range str {
if r == '_' {
// skip underscores in the input
continue
}
p, sz := utf8.DecodeRuneInString(prefix[j:])
for p == '_' {
j += sz // consume/skip underscore
p, sz = utf8.DecodeRuneInString(prefix[j:])
}
if j == len(prefix) {
// matched entire prefix; return rest of str
// but skipping any leading underscores
result := strings.TrimLeft(str[i:], "_")
if len(result) == 0 {
// result can't be empty string
return str
}
return result
}
if unicode.ToLower(r) != unicode.ToLower(p) {
// does not match prefix
return str
}
j += sz // consume matched rune of prefix
}
return str
}
// CreatePrefixList returns a list of package prefixes to search when resolving
// a symbol name. If the given package is blank, it returns only the empty
// string. If the given package contains only one token, e.g. "foo", it returns
// that token and the empty string, e.g. ["foo", ""]. Otherwise, it returns
// successively shorter prefixes of the package and then the empty string. For
// example, for a package named "foo.bar.baz" it will return the following list:
//
// ["foo.bar.baz", "foo.bar", "foo", ""]
func CreatePrefixList(pkg string) []string {
if pkg == "" {
return []string{""}
}
numDots := 0
// one pass to pre-allocate the returned slice
for i := range len(pkg) {
if pkg[i] == '.' {
numDots++
}
}
if numDots == 0 {
return []string{pkg, ""}
}
prefixes := make([]string, numDots+2)
// second pass to fill in returned slice
for i := range len(pkg) {
if pkg[i] == '.' {
prefixes[numDots] = pkg[:i]
numDots--
}
}
prefixes[0] = pkg
return prefixes
}
func WriteEscapedBytes(buf *bytes.Buffer, b []byte) {
// This uses the same algorithm as the protoc C++ code for escaping strings.
// The protoc C++ code in turn uses the abseil C++ library's CEscape function:
// https://github.com/abseil/abseil-cpp/blob/934f613818ffcb26c942dff4a80be9a4031c662c/absl/strings/escaping.cc#L406
for _, c := range b {
switch c {
case '\n':
buf.WriteString("\\n")
case '\r':
buf.WriteString("\\r")
case '\t':
buf.WriteString("\\t")
case '"':
buf.WriteString("\\\"")
case '\'':
buf.WriteString("\\'")
case '\\':
buf.WriteString("\\\\")
default:
if c >= 0x20 && c < 0x7f {
// simple printable characters
buf.WriteByte(c)
} else {
// use octal escape for all other values
buf.WriteRune('\\')
buf.WriteByte('0' + ((c >> 6) & 0x7))
buf.WriteByte('0' + ((c >> 3) & 0x7))
buf.WriteByte('0' + (c & 0x7))
}
}
}
}
// IsZeroLocation returns true if the given loc is a zero value
// (which is returned from queries that have no result).
func IsZeroLocation(loc protoreflect.SourceLocation) bool {
return loc.Path == nil &&
loc.StartLine == 0 &&
loc.StartColumn == 0 &&
loc.EndLine == 0 &&
loc.EndColumn == 0 &&
loc.LeadingDetachedComments == nil &&
loc.LeadingComments == "" &&
loc.TrailingComments == "" &&
loc.Next == 0
}
// ComputePath computes the source location path for the given descriptor.
// The boolean value indicates whether the result is valid. If the path
// cannot be computed for d, the function returns nil, false.
func ComputePath(d protoreflect.Descriptor) (protoreflect.SourcePath, bool) {
_, ok := d.(protoreflect.FileDescriptor)
if ok {
return nil, true
}
var path protoreflect.SourcePath
for {
p := d.Parent()
switch d := d.(type) {
case protoreflect.FileDescriptor:
return reverse(path), true
case protoreflect.MessageDescriptor:
path = append(path, int32(d.Index()))
switch p.(type) {
case protoreflect.FileDescriptor:
path = append(path, FileMessagesTag)
case protoreflect.MessageDescriptor:
path = append(path, MessageNestedMessagesTag)
default:
return nil, false
}
case protoreflect.FieldDescriptor:
path = append(path, int32(d.Index()))
switch p.(type) {
case protoreflect.FileDescriptor:
if d.IsExtension() {
path = append(path, FileExtensionsTag)
} else {
return nil, false
}
case protoreflect.MessageDescriptor:
if d.IsExtension() {
path = append(path, MessageExtensionsTag)
} else {
path = append(path, MessageFieldsTag)
}
default:
return nil, false
}
case protoreflect.OneofDescriptor:
path = append(path, int32(d.Index()))
if _, ok := p.(protoreflect.MessageDescriptor); ok {
path = append(path, MessageOneofsTag)
} else {
return nil, false
}
case protoreflect.EnumDescriptor:
path = append(path, int32(d.Index()))
switch p.(type) {
case protoreflect.FileDescriptor:
path = append(path, FileEnumsTag)
case protoreflect.MessageDescriptor:
path = append(path, MessageEnumsTag)
default:
return nil, false
}
case protoreflect.EnumValueDescriptor:
path = append(path, int32(d.Index()))
if _, ok := p.(protoreflect.EnumDescriptor); ok {
path = append(path, EnumValuesTag)
} else {
return nil, false
}
case protoreflect.ServiceDescriptor:
path = append(path, int32(d.Index()))
if _, ok := p.(protoreflect.FileDescriptor); ok {
path = append(path, FileServicesTag)
} else {
return nil, false
}
case protoreflect.MethodDescriptor:
path = append(path, int32(d.Index()))
if _, ok := p.(protoreflect.ServiceDescriptor); ok {
path = append(path, ServiceMethodsTag)
} else {
return nil, false
}
}
d = p
}
}
// CanPack returns true if a repeated field of the given kind
// can use packed encoding.
func CanPack(k protoreflect.Kind) bool {
switch k {
case protoreflect.MessageKind, protoreflect.GroupKind, protoreflect.StringKind, protoreflect.BytesKind:
return false
default:
return true
}
}
func ClonePath(path protoreflect.SourcePath) protoreflect.SourcePath {
clone := make(protoreflect.SourcePath, len(path))
copy(clone, path)
return clone
}
func reverse(p protoreflect.SourcePath) protoreflect.SourcePath {
for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 {
p[i], p[j] = p[j], p[i]
}
return p
}
// 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 linker
import (
"fmt"
"slices"
"strconv"
"strings"
"unicode/utf8"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/editions"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/protoutil"
)
var (
// These "noOp*" values are all descriptors. The protoreflect.Descriptor
// interface and its sub-interfaces are all marked with an unexported
// method so that they cannot be implemented outside of the google.golang.org/protobuf
// module. So, to provide implementations from this package, we must embed
// them. If we simply left the embedded interface field nil, then if/when
// new methods are added to the interfaces, it could induce panics in this
// package or users of this module (since trying to invoke one of these new
// methods would end up trying to call a method on a nil interface value).
//
// So instead of leaving the embedded interface fields nil, we embed an actual
// value. While new methods are unlikely to return the correct value (since
// the calls will be delegated to these no-op instances), it is a less
// dangerous latent bug than inducing a nil-dereference panic.
noOpFile protoreflect.FileDescriptor
noOpMessage protoreflect.MessageDescriptor
noOpOneof protoreflect.OneofDescriptor
noOpField protoreflect.FieldDescriptor
noOpEnum protoreflect.EnumDescriptor
noOpEnumValue protoreflect.EnumValueDescriptor
noOpExtension protoreflect.ExtensionDescriptor
noOpService protoreflect.ServiceDescriptor
noOpMethod protoreflect.MethodDescriptor
)
var (
fieldPresenceField = editions.FeatureSetDescriptor.Fields().ByName("field_presence")
repeatedFieldEncodingField = editions.FeatureSetDescriptor.Fields().ByName("repeated_field_encoding")
messageEncodingField = editions.FeatureSetDescriptor.Fields().ByName("message_encoding")
enumTypeField = editions.FeatureSetDescriptor.Fields().ByName("enum_type")
jsonFormatField = editions.FeatureSetDescriptor.Fields().ByName("json_format")
)
func init() {
noOpFile, _ = protodesc.NewFile(
&descriptorpb.FileDescriptorProto{
Name: proto.String("no-op.proto"),
Syntax: proto.String("proto2"),
Dependency: []string{"google/protobuf/descriptor.proto"},
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("NoOpMsg"),
Field: []*descriptorpb.FieldDescriptorProto{
{
Name: proto.String("no_op"),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Number: proto.Int32(1),
JsonName: proto.String("noOp"),
OneofIndex: proto.Int32(0),
},
},
OneofDecl: []*descriptorpb.OneofDescriptorProto{
{
Name: proto.String("no_op_oneof"),
},
},
},
},
EnumType: []*descriptorpb.EnumDescriptorProto{
{
Name: proto.String("NoOpEnum"),
Value: []*descriptorpb.EnumValueDescriptorProto{
{
Name: proto.String("NO_OP"),
Number: proto.Int32(0),
},
},
},
},
Extension: []*descriptorpb.FieldDescriptorProto{
{
Extendee: proto.String(".google.protobuf.FileOptions"),
Name: proto.String("no_op"),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Number: proto.Int32(50000),
},
},
Service: []*descriptorpb.ServiceDescriptorProto{
{
Name: proto.String("NoOpService"),
Method: []*descriptorpb.MethodDescriptorProto{
{
Name: proto.String("NoOp"),
InputType: proto.String(".NoOpMsg"),
OutputType: proto.String(".NoOpMsg"),
},
},
},
},
},
protoregistry.GlobalFiles,
)
noOpMessage = noOpFile.Messages().Get(0)
noOpOneof = noOpMessage.Oneofs().Get(0)
noOpField = noOpMessage.Fields().Get(0)
noOpEnum = noOpFile.Enums().Get(0)
noOpEnumValue = noOpEnum.Values().Get(0)
noOpExtension = noOpFile.Extensions().Get(0)
noOpService = noOpFile.Services().Get(0)
noOpMethod = noOpService.Methods().Get(0)
}
// This file contains implementations of protoreflect.Descriptor. Note that
// this is a hack since those interfaces have a "doNotImplement" tag
// interface therein. We do just enough to make dynamicpb happy; constructing
// a regular descriptor would fail because we haven't yet interpreted options
// at the point we need these, and some validations will fail if the options
// aren't present.
type result struct {
protoreflect.FileDescriptor
parser.Result
prefix string
deps Files
// A map of all descriptors keyed by their fully-qualified name (without
// any leading dot).
descriptors map[string]protoreflect.Descriptor
// A set of imports that have been used in the course of linking and
// interpreting options.
usedImports map[string]struct{}
// A map of AST nodes that represent identifiers in ast.FieldReferenceNodes
// to their fully-qualified name. The identifiers are for field names in
// message literals (in option values) that are extension fields. These names
// are resolved during linking and stored here, to be used to interpret options.
optionQualifiedNames map[ast.IdentValueNode]string
imports fileImports
optionImports fileImports
messages msgDescriptors
enums enumDescriptors
extensions extDescriptors
services svcDescriptors
srcLocations srcLocs
}
var _ protoreflect.FileDescriptor = (*result)(nil)
var _ Result = (*result)(nil)
var _ protoutil.DescriptorProtoWrapper = (*result)(nil)
var _ editions.HasEdition = (*result)(nil)
func (r *result) RemoveAST() {
r.Result = parser.ResultWithoutAST(r.FileDescriptorProto())
r.optionQualifiedNames = nil
}
func (r *result) AsProto() proto.Message {
return r.FileDescriptorProto()
}
func (r *result) ParentFile() protoreflect.FileDescriptor {
return r
}
func (r *result) Parent() protoreflect.Descriptor {
return nil
}
func (r *result) Index() int {
return 0
}
func (r *result) Syntax() protoreflect.Syntax {
switch r.FileDescriptorProto().GetSyntax() {
case "proto2", "":
return protoreflect.Proto2
case "proto3":
return protoreflect.Proto3
case "editions":
return protoreflect.Editions
default:
return 0 // ???
}
}
func (r *result) Edition() int32 {
switch r.Syntax() {
case protoreflect.Proto2:
return int32(descriptorpb.Edition_EDITION_PROTO2)
case protoreflect.Proto3:
return int32(descriptorpb.Edition_EDITION_PROTO3)
case protoreflect.Editions:
return int32(r.FileDescriptorProto().GetEdition())
default:
return int32(descriptorpb.Edition_EDITION_UNKNOWN) // ???
}
}
func (r *result) OptionImports() protoreflect.FileImports {
return &r.optionImports
}
func (r *result) Name() protoreflect.Name {
return ""
}
func (r *result) FullName() protoreflect.FullName {
return r.Package()
}
func (r *result) IsPlaceholder() bool {
return false
}
func (r *result) Options() protoreflect.ProtoMessage {
return r.FileDescriptorProto().Options
}
func (r *result) Path() string {
return r.FileDescriptorProto().GetName()
}
func (r *result) Package() protoreflect.FullName {
return protoreflect.FullName(r.FileDescriptorProto().GetPackage())
}
func (r *result) Imports() protoreflect.FileImports {
return &r.imports
}
func (r *result) Enums() protoreflect.EnumDescriptors {
return &r.enums
}
func (r *result) Messages() protoreflect.MessageDescriptors {
return &r.messages
}
func (r *result) Extensions() protoreflect.ExtensionDescriptors {
return &r.extensions
}
func (r *result) Services() protoreflect.ServiceDescriptors {
return &r.services
}
func (r *result) PopulateSourceCodeInfo() {
srcLocProtos, srcLocIndex := asSourceLocations(r.FileDescriptorProto().GetSourceCodeInfo().GetLocation())
r.srcLocations = srcLocs{file: r, locs: srcLocProtos, index: srcLocIndex}
}
func (r *result) SourceLocations() protoreflect.SourceLocations {
return &r.srcLocations
}
func asSourceLocations(srcInfoProtos []*descriptorpb.SourceCodeInfo_Location) ([]protoreflect.SourceLocation, map[sourcePathKey]int) {
locs := make([]protoreflect.SourceLocation, len(srcInfoProtos))
index := make(map[sourcePathKey]int, len(srcInfoProtos))
prev := make(map[sourcePathKey]*protoreflect.SourceLocation, len(srcInfoProtos))
for i, loc := range srcInfoProtos {
var stLin, stCol, enLin, enCol int
if len(loc.Span) == 3 {
stLin, stCol, enCol = int(loc.Span[0]), int(loc.Span[1]), int(loc.Span[2])
enLin = stLin
} else {
stLin, stCol, enLin, enCol = int(loc.Span[0]), int(loc.Span[1]), int(loc.Span[2]), int(loc.Span[3])
}
locs[i] = protoreflect.SourceLocation{
Path: loc.Path,
LeadingComments: loc.GetLeadingComments(),
LeadingDetachedComments: loc.GetLeadingDetachedComments(),
TrailingComments: loc.GetTrailingComments(),
StartLine: stLin,
StartColumn: stCol,
EndLine: enLin,
EndColumn: enCol,
}
str := pathKey(loc.Path)
pr := prev[str]
if pr == nil {
index[str] = i
} else {
pr.Next = i
}
prev[str] = &locs[i]
}
return locs, index
}
type fileImports struct {
protoreflect.FileImports
files []protoreflect.FileImport
}
func (r *result) createImports() fileImports {
fd := r.FileDescriptorProto()
imps := r.createBasicImports(fd.Dependency)
for _, publicIndex := range fd.PublicDependency {
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}
}
func (r *result) createBasicImports(deps []string) []protoreflect.FileImport {
imps := make([]protoreflect.FileImport, len(deps))
for i, dep := range deps {
desc := r.deps.FindFileByPath(dep)
imps[i] = protoreflect.FileImport{FileDescriptor: unwrap(desc)}
}
return imps
}
func unwrap(descriptor protoreflect.FileDescriptor) protoreflect.FileDescriptor {
wrapped, ok := descriptor.(interface {
Unwrap() protoreflect.FileDescriptor
})
if !ok {
return descriptor
}
unwrapped := wrapped.Unwrap()
if unwrapped == nil {
return descriptor // shouldn't ever happen
}
return unwrapped
}
func (f *fileImports) Len() int {
return len(f.files)
}
func (f *fileImports) Get(i int) protoreflect.FileImport {
return f.files[i]
}
type srcLocs struct {
protoreflect.SourceLocations
file *result
locs []protoreflect.SourceLocation
index map[sourcePathKey]int
}
func (s *srcLocs) Len() int {
return len(s.locs)
}
func (s *srcLocs) Get(i int) protoreflect.SourceLocation {
return s.locs[i]
}
func (s *srcLocs) ByPath(p protoreflect.SourcePath) protoreflect.SourceLocation {
index, ok := s.index[pathKeyNoCopy(p)]
if !ok {
return protoreflect.SourceLocation{}
}
return s.locs[index]
}
func (s *srcLocs) ByDescriptor(d protoreflect.Descriptor) protoreflect.SourceLocation {
if d.ParentFile() != s.file {
return protoreflect.SourceLocation{}
}
path, ok := internal.ComputePath(d)
if !ok {
return protoreflect.SourceLocation{}
}
return s.ByPath(path)
}
type msgDescriptors struct {
protoreflect.MessageDescriptors
msgs []msgDescriptor
}
func (r *result) createMessages(prefix string, parent protoreflect.Descriptor, msgProtos []*descriptorpb.DescriptorProto, pool *allocPool) msgDescriptors {
msgs := pool.getMessages(len(msgProtos))
for i, msgProto := range msgProtos {
r.createMessageDescriptor(&msgs[i], msgProto, parent, i, prefix+msgProto.GetName(), pool)
}
return msgDescriptors{msgs: msgs}
}
func (m *msgDescriptors) Len() int {
return len(m.msgs)
}
func (m *msgDescriptors) Get(i int) protoreflect.MessageDescriptor {
return &m.msgs[i]
}
func (m *msgDescriptors) ByName(s protoreflect.Name) protoreflect.MessageDescriptor {
for i := range m.msgs {
msg := &m.msgs[i]
if msg.Name() == s {
return msg
}
}
return nil
}
type msgDescriptor struct {
protoreflect.MessageDescriptor
file *result
parent protoreflect.Descriptor
index int
proto *descriptorpb.DescriptorProto
fqn string
fields fldDescriptors
oneofs oneofDescriptors
nestedMessages msgDescriptors
nestedEnums enumDescriptors
nestedExtensions extDescriptors
extRanges fieldRanges
rsvdRanges fieldRanges
rsvdNames names
}
var _ protoreflect.MessageDescriptor = (*msgDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*msgDescriptor)(nil)
func (r *result) createMessageDescriptor(ret *msgDescriptor, md *descriptorpb.DescriptorProto, parent protoreflect.Descriptor, index int, fqn string, pool *allocPool) {
r.descriptors[fqn] = ret
ret.MessageDescriptor = noOpMessage
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = md
ret.fqn = fqn
prefix := fqn + "."
// NB: We MUST create fields before oneofs so that we can populate the
// set of fields that belong to the oneof
ret.fields = r.createFields(prefix, ret, md.Field, pool)
ret.oneofs = r.createOneofs(prefix, ret, md.OneofDecl, pool)
ret.nestedMessages = r.createMessages(prefix, ret, md.NestedType, pool)
ret.nestedEnums = r.createEnums(prefix, ret, md.EnumType, pool)
ret.nestedExtensions = r.createExtensions(prefix, ret, md.Extension, pool)
ret.extRanges = createFieldRanges(md.ExtensionRange)
ret.rsvdRanges = createFieldRanges(md.ReservedRange)
ret.rsvdNames = names{s: md.ReservedName}
}
func (m *msgDescriptor) MessageDescriptorProto() *descriptorpb.DescriptorProto {
return m.proto
}
func (m *msgDescriptor) AsProto() proto.Message {
return m.proto
}
func (m *msgDescriptor) ParentFile() protoreflect.FileDescriptor {
return m.file
}
func (m *msgDescriptor) Parent() protoreflect.Descriptor {
return m.parent
}
func (m *msgDescriptor) Index() int {
return m.index
}
func (m *msgDescriptor) Syntax() protoreflect.Syntax {
return m.file.Syntax()
}
func (m *msgDescriptor) Name() protoreflect.Name {
return protoreflect.Name(m.proto.GetName())
}
func (m *msgDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(m.fqn)
}
func (m *msgDescriptor) IsPlaceholder() bool {
return false
}
func (m *msgDescriptor) Visibility() int32 {
return int32(m.proto.GetVisibility())
}
func (m *msgDescriptor) Options() protoreflect.ProtoMessage {
return m.proto.Options
}
func (m *msgDescriptor) IsMapEntry() bool {
return m.proto.Options.GetMapEntry()
}
func (m *msgDescriptor) Fields() protoreflect.FieldDescriptors {
return &m.fields
}
func (m *msgDescriptor) Oneofs() protoreflect.OneofDescriptors {
return &m.oneofs
}
func (m *msgDescriptor) ReservedNames() protoreflect.Names {
return m.rsvdNames
}
func (m *msgDescriptor) ReservedRanges() protoreflect.FieldRanges {
return m.rsvdRanges
}
func (m *msgDescriptor) RequiredNumbers() protoreflect.FieldNumbers {
var indexes fieldNums
for _, fld := range m.proto.Field {
if fld.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED {
indexes.s = append(indexes.s, fld.GetNumber())
}
}
return indexes
}
func (m *msgDescriptor) ExtensionRanges() protoreflect.FieldRanges {
return m.extRanges
}
func (m *msgDescriptor) ExtensionRangeOptions(i int) protoreflect.ProtoMessage {
return m.proto.ExtensionRange[i].Options
}
func (m *msgDescriptor) Enums() protoreflect.EnumDescriptors {
return &m.nestedEnums
}
func (m *msgDescriptor) Messages() protoreflect.MessageDescriptors {
return &m.nestedMessages
}
func (m *msgDescriptor) Extensions() protoreflect.ExtensionDescriptors {
return &m.nestedExtensions
}
type names struct {
protoreflect.Names
s []string
}
func (n names) Len() int {
return len(n.s)
}
func (n names) Get(i int) protoreflect.Name {
return protoreflect.Name(n.s[i])
}
func (n names) Has(s protoreflect.Name) bool {
for _, name := range n.s {
if name == string(s) {
return true
}
}
return false
}
type fieldNums struct {
protoreflect.FieldNumbers
s []int32
}
func (n fieldNums) Len() int {
return len(n.s)
}
func (n fieldNums) Get(i int) protoreflect.FieldNumber {
return protoreflect.FieldNumber(n.s[i])
}
func (n fieldNums) Has(s protoreflect.FieldNumber) bool {
for _, num := range n.s {
if num == int32(s) {
return true
}
}
return false
}
type fieldRanges struct {
protoreflect.FieldRanges
ranges [][2]protoreflect.FieldNumber
}
type fieldRange interface {
GetStart() int32
GetEnd() int32
}
func createFieldRanges[T fieldRange](rangeProtos []T) fieldRanges {
ranges := make([][2]protoreflect.FieldNumber, len(rangeProtos))
for i, r := range rangeProtos {
ranges[i] = [2]protoreflect.FieldNumber{
protoreflect.FieldNumber(r.GetStart()),
protoreflect.FieldNumber(r.GetEnd()),
}
}
return fieldRanges{ranges: ranges}
}
func (f fieldRanges) Len() int {
return len(f.ranges)
}
func (f fieldRanges) Get(i int) [2]protoreflect.FieldNumber {
return f.ranges[i]
}
func (f fieldRanges) Has(n protoreflect.FieldNumber) bool {
for _, r := range f.ranges {
if r[0] <= n && r[1] > n {
return true
}
}
return false
}
type enumDescriptors struct {
protoreflect.EnumDescriptors
enums []enumDescriptor
}
func (r *result) createEnums(prefix string, parent protoreflect.Descriptor, enumProtos []*descriptorpb.EnumDescriptorProto, pool *allocPool) enumDescriptors {
enums := pool.getEnums(len(enumProtos))
for i, enumProto := range enumProtos {
r.createEnumDescriptor(&enums[i], enumProto, parent, i, prefix+enumProto.GetName(), pool)
}
return enumDescriptors{enums: enums}
}
func (e *enumDescriptors) Len() int {
return len(e.enums)
}
func (e *enumDescriptors) Get(i int) protoreflect.EnumDescriptor {
return &e.enums[i]
}
func (e *enumDescriptors) ByName(s protoreflect.Name) protoreflect.EnumDescriptor {
for i := range e.enums {
enum := &e.enums[i]
if enum.Name() == s {
return enum
}
}
return nil
}
type enumDescriptor struct {
protoreflect.EnumDescriptor
file *result
parent protoreflect.Descriptor
index int
proto *descriptorpb.EnumDescriptorProto
fqn string
values enValDescriptors
rsvdRanges enumRanges
rsvdNames names
}
var _ protoreflect.EnumDescriptor = (*enumDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*enumDescriptor)(nil)
func (r *result) createEnumDescriptor(ret *enumDescriptor, ed *descriptorpb.EnumDescriptorProto, parent protoreflect.Descriptor, index int, fqn string, pool *allocPool) {
r.descriptors[fqn] = ret
ret.EnumDescriptor = noOpEnum
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = ed
ret.fqn = fqn
// Unlike all other elements, the fully-qualified names of enum values
// are NOT scoped to their parent element (the enum), but rather to
// the enum's parent element. This follows C++ scoping rules for
// enum values.
prefix := strings.TrimSuffix(fqn, ed.GetName())
ret.values = r.createEnumValues(prefix, ret, ed.Value, pool)
ret.rsvdRanges = createEnumRanges(ed.ReservedRange)
ret.rsvdNames = names{s: ed.ReservedName}
}
func (e *enumDescriptor) EnumDescriptorProto() *descriptorpb.EnumDescriptorProto {
return e.proto
}
func (e *enumDescriptor) AsProto() proto.Message {
return e.proto
}
func (e *enumDescriptor) ParentFile() protoreflect.FileDescriptor {
return e.file
}
func (e *enumDescriptor) Parent() protoreflect.Descriptor {
return e.parent
}
func (e *enumDescriptor) Index() int {
return e.index
}
func (e *enumDescriptor) Syntax() protoreflect.Syntax {
return e.file.Syntax()
}
func (e *enumDescriptor) Name() protoreflect.Name {
return protoreflect.Name(e.proto.GetName())
}
func (e *enumDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(e.fqn)
}
func (e *enumDescriptor) IsPlaceholder() bool {
return false
}
func (e *enumDescriptor) Visibility() int32 {
return int32(e.proto.GetVisibility())
}
func (e *enumDescriptor) Options() protoreflect.ProtoMessage {
return e.proto.Options
}
func (e *enumDescriptor) Values() protoreflect.EnumValueDescriptors {
return &e.values
}
func (e *enumDescriptor) ReservedNames() protoreflect.Names {
return e.rsvdNames
}
func (e *enumDescriptor) ReservedRanges() protoreflect.EnumRanges {
return e.rsvdRanges
}
func (e *enumDescriptor) IsClosed() bool {
enumType := resolveFeature(e, enumTypeField)
return descriptorpb.FeatureSet_EnumType(enumType.Enum()) == descriptorpb.FeatureSet_CLOSED
}
type enumRanges struct {
protoreflect.EnumRanges
ranges [][2]protoreflect.EnumNumber
}
func createEnumRanges(rangeProtos []*descriptorpb.EnumDescriptorProto_EnumReservedRange) enumRanges {
ranges := make([][2]protoreflect.EnumNumber, len(rangeProtos))
for i, r := range rangeProtos {
ranges[i] = [2]protoreflect.EnumNumber{
protoreflect.EnumNumber(r.GetStart()),
protoreflect.EnumNumber(r.GetEnd()),
}
}
return enumRanges{ranges: ranges}
}
func (e enumRanges) Len() int {
return len(e.ranges)
}
func (e enumRanges) Get(i int) [2]protoreflect.EnumNumber {
return e.ranges[i]
}
func (e enumRanges) Has(n protoreflect.EnumNumber) bool {
for _, r := range e.ranges {
if r[0] <= n && r[1] >= n {
return true
}
}
return false
}
type enValDescriptors struct {
protoreflect.EnumValueDescriptors
vals []enValDescriptor
}
func (r *result) createEnumValues(prefix string, parent *enumDescriptor, enValProtos []*descriptorpb.EnumValueDescriptorProto, pool *allocPool) enValDescriptors {
vals := pool.getEnumValues(len(enValProtos))
for i, enValProto := range enValProtos {
r.createEnumValueDescriptor(&vals[i], enValProto, parent, i, prefix+enValProto.GetName())
}
return enValDescriptors{vals: vals}
}
func (e *enValDescriptors) Len() int {
return len(e.vals)
}
func (e *enValDescriptors) Get(i int) protoreflect.EnumValueDescriptor {
return &e.vals[i]
}
func (e *enValDescriptors) ByName(s protoreflect.Name) protoreflect.EnumValueDescriptor {
for i := range e.vals {
val := &e.vals[i]
if val.Name() == s {
return val
}
}
return nil
}
func (e *enValDescriptors) ByNumber(n protoreflect.EnumNumber) protoreflect.EnumValueDescriptor {
for i := range e.vals {
val := &e.vals[i]
if val.Number() == n {
return val
}
}
return nil
}
type enValDescriptor struct {
protoreflect.EnumValueDescriptor
file *result
parent *enumDescriptor
index int
proto *descriptorpb.EnumValueDescriptorProto
fqn string
}
var _ protoreflect.EnumValueDescriptor = (*enValDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*enValDescriptor)(nil)
func (r *result) createEnumValueDescriptor(ret *enValDescriptor, ed *descriptorpb.EnumValueDescriptorProto, parent *enumDescriptor, index int, fqn string) {
r.descriptors[fqn] = ret
ret.EnumValueDescriptor = noOpEnumValue
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = ed
ret.fqn = fqn
}
func (e *enValDescriptor) EnumValueDescriptorProto() *descriptorpb.EnumValueDescriptorProto {
return e.proto
}
func (e *enValDescriptor) AsProto() proto.Message {
return e.proto
}
func (e *enValDescriptor) ParentFile() protoreflect.FileDescriptor {
return e.file
}
func (e *enValDescriptor) Parent() protoreflect.Descriptor {
return e.parent
}
func (e *enValDescriptor) Index() int {
return e.index
}
func (e *enValDescriptor) Syntax() protoreflect.Syntax {
return e.file.Syntax()
}
func (e *enValDescriptor) Name() protoreflect.Name {
return protoreflect.Name(e.proto.GetName())
}
func (e *enValDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(e.fqn)
}
func (e *enValDescriptor) IsPlaceholder() bool {
return false
}
func (e *enValDescriptor) Options() protoreflect.ProtoMessage {
return e.proto.Options
}
func (e *enValDescriptor) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(e.proto.GetNumber())
}
type extDescriptors struct {
protoreflect.ExtensionDescriptors
exts []extTypeDescriptor
}
func (r *result) createExtensions(prefix string, parent protoreflect.Descriptor, extProtos []*descriptorpb.FieldDescriptorProto, pool *allocPool) extDescriptors {
exts := pool.getExtensions(len(extProtos))
for i, extProto := range extProtos {
r.createExtTypeDescriptor(&exts[i], extProto, parent, i, prefix+extProto.GetName())
}
return extDescriptors{exts: exts}
}
func (e *extDescriptors) Len() int {
return len(e.exts)
}
func (e *extDescriptors) Get(i int) protoreflect.ExtensionDescriptor {
return &e.exts[i]
}
func (e *extDescriptors) ByName(s protoreflect.Name) protoreflect.ExtensionDescriptor {
for i := range e.exts {
ext := &e.exts[i]
if ext.Name() == s {
return ext
}
}
return nil
}
type extTypeDescriptor struct {
protoreflect.ExtensionTypeDescriptor
field fldDescriptor
}
var _ protoutil.DescriptorProtoWrapper = &extTypeDescriptor{}
func (r *result) createExtTypeDescriptor(ret *extTypeDescriptor, fd *descriptorpb.FieldDescriptorProto, parent protoreflect.Descriptor, index int, fqn string) {
r.descriptors[fqn] = ret
ret.field = fldDescriptor{FieldDescriptor: noOpExtension, file: r, parent: parent, index: index, proto: fd, fqn: fqn}
ret.ExtensionTypeDescriptor = dynamicpb.NewExtensionType(&ret.field).TypeDescriptor()
}
func (e *extTypeDescriptor) FieldDescriptorProto() *descriptorpb.FieldDescriptorProto {
return e.field.proto
}
func (e *extTypeDescriptor) AsProto() proto.Message {
return e.field.proto
}
type fldDescriptors struct {
protoreflect.FieldDescriptors
// We use pointers here, instead of flattened slice, because oneofs
// also have fields, but need to point to values in the parent
// message's fields. Even though they are pointers, in the containing
// message, we always allocate a flattened slice and then point into
// that, so we're still doing fewer allocations (2 per set of fields
// instead of 1 per each field).
fields []*fldDescriptor
}
func (r *result) createFields(prefix string, parent *msgDescriptor, fldProtos []*descriptorpb.FieldDescriptorProto, pool *allocPool) fldDescriptors {
fields := pool.getFields(len(fldProtos))
fieldPtrs := make([]*fldDescriptor, len(fldProtos))
for i, fldProto := range fldProtos {
r.createFieldDescriptor(&fields[i], fldProto, parent, i, prefix+fldProto.GetName())
fieldPtrs[i] = &fields[i]
}
return fldDescriptors{fields: fieldPtrs}
}
func (f *fldDescriptors) Len() int {
return len(f.fields)
}
func (f *fldDescriptors) Get(i int) protoreflect.FieldDescriptor {
return f.fields[i]
}
func (f *fldDescriptors) ByName(s protoreflect.Name) protoreflect.FieldDescriptor {
for _, fld := range f.fields {
if fld.Name() == s {
return fld
}
}
return nil
}
func (f *fldDescriptors) ByJSONName(s string) protoreflect.FieldDescriptor {
for _, fld := range f.fields {
if fld.JSONName() == s {
return fld
}
}
return nil
}
func (f *fldDescriptors) ByTextName(s string) protoreflect.FieldDescriptor {
fld := f.ByName(protoreflect.Name(s))
if fld != nil {
return fld
}
// Groups use type name instead, so we fallback to slow search
for _, fld := range f.fields {
if fld.TextName() == s {
return fld
}
}
return nil
}
func (f *fldDescriptors) ByNumber(n protoreflect.FieldNumber) protoreflect.FieldDescriptor {
for _, fld := range f.fields {
if fld.Number() == n {
return fld
}
}
return nil
}
type fldDescriptor struct {
protoreflect.FieldDescriptor
file *result
parent protoreflect.Descriptor
index int
proto *descriptorpb.FieldDescriptorProto
fqn string
msgType protoreflect.MessageDescriptor
extendee protoreflect.MessageDescriptor
enumType protoreflect.EnumDescriptor
oneof protoreflect.OneofDescriptor
}
var _ protoreflect.FieldDescriptor = (*fldDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*fldDescriptor)(nil)
func (r *result) createFieldDescriptor(ret *fldDescriptor, fd *descriptorpb.FieldDescriptorProto, parent *msgDescriptor, index int, fqn string) {
r.descriptors[fqn] = ret
ret.FieldDescriptor = noOpField
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = fd
ret.fqn = fqn
}
func (f *fldDescriptor) FieldDescriptorProto() *descriptorpb.FieldDescriptorProto {
return f.proto
}
func (f *fldDescriptor) AsProto() proto.Message {
return f.proto
}
func (f *fldDescriptor) ParentFile() protoreflect.FileDescriptor {
return f.file
}
func (f *fldDescriptor) Parent() protoreflect.Descriptor {
return f.parent
}
func (f *fldDescriptor) Index() int {
return f.index
}
func (f *fldDescriptor) Syntax() protoreflect.Syntax {
return f.file.Syntax()
}
func (f *fldDescriptor) Name() protoreflect.Name {
return protoreflect.Name(f.proto.GetName())
}
func (f *fldDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(f.fqn)
}
func (f *fldDescriptor) IsPlaceholder() bool {
return false
}
func (f *fldDescriptor) Options() protoreflect.ProtoMessage {
return f.proto.Options
}
func (f *fldDescriptor) Number() protoreflect.FieldNumber {
return protoreflect.FieldNumber(f.proto.GetNumber())
}
func (f *fldDescriptor) Cardinality() protoreflect.Cardinality {
switch f.proto.GetLabel() {
case descriptorpb.FieldDescriptorProto_LABEL_REPEATED:
return protoreflect.Repeated
case descriptorpb.FieldDescriptorProto_LABEL_REQUIRED:
return protoreflect.Required
case descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL:
if f.Syntax() == protoreflect.Editions {
// Editions does not use label to indicate required. It instead
// uses a feature, and label is always optional.
fieldPresence := descriptorpb.FeatureSet_FieldPresence(resolveFeature(f, fieldPresenceField).Enum())
if fieldPresence == descriptorpb.FeatureSet_LEGACY_REQUIRED {
return protoreflect.Required
}
}
return protoreflect.Optional
default:
return 0
}
}
func (f *fldDescriptor) Kind() protoreflect.Kind {
if f.proto.GetType() == descriptorpb.FieldDescriptorProto_TYPE_MESSAGE && f.Syntax() == protoreflect.Editions &&
!f.IsMap() && !f.parentIsMap() {
// In editions, "group encoding" (aka "delimited encoding") is toggled
// via a feature. So we report group kind when that feature is enabled.
messageEncoding := resolveFeature(f, messageEncodingField)
if descriptorpb.FeatureSet_MessageEncoding(messageEncoding.Enum()) == descriptorpb.FeatureSet_DELIMITED {
return protoreflect.GroupKind
}
}
return protoreflect.Kind(f.proto.GetType())
}
func (f *fldDescriptor) HasJSONName() bool {
return f.proto.JsonName != nil
}
func (f *fldDescriptor) JSONName() string {
if f.IsExtension() {
return f.TextName()
}
return f.proto.GetJsonName()
}
func (f *fldDescriptor) TextName() string {
if f.IsExtension() {
return fmt.Sprintf("[%s]", f.FullName())
}
if f.looksLikeGroup() {
// groups use the type name
return string(protoreflect.FullName(f.proto.GetTypeName()).Name())
}
return string(f.Name())
}
func (f *fldDescriptor) looksLikeGroup() bool {
// It looks like a group if it uses group/delimited encoding (checked via f.Kind)
// and the message type is a sibling whose name is a mixed-case version of the field name.
return f.Kind() == protoreflect.GroupKind &&
f.Message().FullName().Parent() == f.FullName().Parent() &&
string(f.Name()) == strings.ToLower(string(f.Message().Name()))
}
func (f *fldDescriptor) HasPresence() bool {
if f.proto.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
return false
}
if f.IsExtension() ||
f.Kind() == protoreflect.MessageKind || f.Kind() == protoreflect.GroupKind ||
f.proto.OneofIndex != nil {
return true
}
fieldPresence := descriptorpb.FeatureSet_FieldPresence(resolveFeature(f, fieldPresenceField).Enum())
return fieldPresence == descriptorpb.FeatureSet_EXPLICIT || fieldPresence == descriptorpb.FeatureSet_LEGACY_REQUIRED
}
func (f *fldDescriptor) IsExtension() bool {
return f.proto.GetExtendee() != ""
}
func (f *fldDescriptor) HasOptionalKeyword() bool {
if f.proto.GetLabel() != descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL {
return false
}
if f.proto.GetProto3Optional() {
// NB: This smells weird to return false here. If the proto3_optional field
// is set, it's because the keyword WAS present. However, the Go runtime
// returns false for this case, so we mirror that behavior.
return !f.IsExtension()
}
// If it's optional, but not a proto3 optional, then the keyword is only
// present for proto2 files, for fields that are not part of a oneof.
return f.file.Syntax() == protoreflect.Proto2 && f.proto.OneofIndex == nil
}
func (f *fldDescriptor) IsWeak() bool {
return f.proto.Options.GetWeak() //nolint:staticcheck // yes, is_weak is deprecated; but we still have to query it to implement this interface
}
func (f *fldDescriptor) IsPacked() bool {
if f.Cardinality() != protoreflect.Repeated || !internal.CanPack(f.Kind()) {
return false
}
opts := f.proto.GetOptions()
if opts != nil && opts.Packed != nil {
// packed option is set explicitly
return *opts.Packed
}
fieldEncoding := resolveFeature(f, repeatedFieldEncodingField)
return descriptorpb.FeatureSet_RepeatedFieldEncoding(fieldEncoding.Enum()) == descriptorpb.FeatureSet_PACKED
}
func (f *fldDescriptor) IsList() bool {
if f.proto.GetLabel() != descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
return false
}
return !f.isMapEntry()
}
func (f *fldDescriptor) IsMap() bool {
if f.proto.GetLabel() != descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
return false
}
if f.IsExtension() {
return false
}
return f.isMapEntry()
}
func (f *fldDescriptor) isMapEntry() bool {
if f.proto.GetType() != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE {
return false
}
return f.Message().IsMapEntry()
}
func (f *fldDescriptor) parentIsMap() bool {
parent, ok := f.parent.(protoreflect.MessageDescriptor)
return ok && parent.IsMapEntry()
}
func (f *fldDescriptor) MapKey() protoreflect.FieldDescriptor {
if !f.IsMap() {
return nil
}
return f.Message().Fields().ByNumber(1)
}
func (f *fldDescriptor) MapValue() protoreflect.FieldDescriptor {
if !f.IsMap() {
return nil
}
return f.Message().Fields().ByNumber(2)
}
func (f *fldDescriptor) HasDefault() bool {
return f.proto.DefaultValue != nil
}
func (f *fldDescriptor) Default() protoreflect.Value {
// We only return a valid value for scalar fields
if f.proto.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED ||
f.Kind() == protoreflect.GroupKind || f.Kind() == protoreflect.MessageKind {
return protoreflect.Value{}
}
if f.proto.DefaultValue != nil {
defVal := f.parseDefaultValue(f.proto.GetDefaultValue())
if defVal.IsValid() {
return defVal
}
// if we cannot parse a valid value, fall back to zero value below
}
// No custom default value, so return the zero value for the type
switch f.Kind() {
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
return protoreflect.ValueOfInt32(0)
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
return protoreflect.ValueOfInt64(0)
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
return protoreflect.ValueOfUint32(0)
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return protoreflect.ValueOfUint64(0)
case protoreflect.FloatKind:
return protoreflect.ValueOfFloat32(0)
case protoreflect.DoubleKind:
return protoreflect.ValueOfFloat64(0)
case protoreflect.BoolKind:
return protoreflect.ValueOfBool(false)
case protoreflect.BytesKind:
return protoreflect.ValueOfBytes(nil)
case protoreflect.StringKind:
return protoreflect.ValueOfString("")
case protoreflect.EnumKind:
return protoreflect.ValueOfEnum(f.Enum().Values().Get(0).Number())
case protoreflect.GroupKind, protoreflect.MessageKind:
return protoreflect.ValueOfMessage(dynamicpb.NewMessage(f.Message()))
default:
panic(fmt.Sprintf("unknown kind: %v", f.Kind()))
}
}
func (f *fldDescriptor) parseDefaultValue(val string) protoreflect.Value {
switch f.Kind() {
case protoreflect.EnumKind:
vd := f.Enum().Values().ByName(protoreflect.Name(val))
if vd != nil {
return protoreflect.ValueOfEnum(vd.Number())
}
return protoreflect.Value{}
case protoreflect.BoolKind:
switch val {
case "true":
return protoreflect.ValueOfBool(true)
case "false":
return protoreflect.ValueOfBool(false)
default:
return protoreflect.Value{}
}
case protoreflect.BytesKind:
return protoreflect.ValueOfBytes([]byte(unescape(val)))
case protoreflect.StringKind:
return protoreflect.ValueOfString(val)
case protoreflect.FloatKind:
if f, err := strconv.ParseFloat(val, 32); err == nil {
return protoreflect.ValueOfFloat32(float32(f))
}
return protoreflect.Value{}
case protoreflect.DoubleKind:
if f, err := strconv.ParseFloat(val, 64); err == nil {
return protoreflect.ValueOfFloat64(f)
}
return protoreflect.Value{}
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
if i, err := strconv.ParseInt(val, 10, 32); err == nil {
return protoreflect.ValueOfInt32(int32(i))
}
return protoreflect.Value{}
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
if i, err := strconv.ParseUint(val, 10, 32); err == nil {
return protoreflect.ValueOfUint32(uint32(i))
}
return protoreflect.Value{}
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
return protoreflect.ValueOfInt64(i)
}
return protoreflect.Value{}
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
if i, err := strconv.ParseUint(val, 10, 64); err == nil {
return protoreflect.ValueOfUint64(i)
}
return protoreflect.Value{}
default:
return protoreflect.Value{}
}
}
func unescape(s string) string {
// protoc encodes default values for 'bytes' fields using C escaping,
// so this function reverses that escaping
out := make([]byte, 0, len(s))
var buf [4]byte
for len(s) > 0 {
if s[0] != '\\' || len(s) < 2 {
// not escape sequence, or too short to be well-formed escape
out = append(out, s[0])
s = s[1:]
continue
}
nextIndex := 2 // by default, skip '\' + escaped character
switch s[1] {
case 'x', 'X':
n := matchPrefix(s[2:], 2, isHex)
if n == 0 {
// bad escape
out = append(out, s[:2]...)
} else {
c, err := strconv.ParseUint(s[2:2+n], 16, 8)
if err != nil {
// shouldn't really happen...
out = append(out, s[:2+n]...)
} else {
out = append(out, byte(c))
}
nextIndex = 2 + n
}
case '0', '1', '2', '3', '4', '5', '6', '7':
n := 1 + matchPrefix(s[2:], 2, isOctal)
c, err := strconv.ParseUint(s[1:1+n], 8, 8)
if err != nil || c > 0xff {
out = append(out, s[:1+n]...)
} else {
out = append(out, byte(c))
}
nextIndex = 1 + n
case 'u':
if len(s) < 6 {
// bad escape
out = append(out, s...)
nextIndex = len(s)
} else {
c, err := strconv.ParseUint(s[2:6], 16, 16)
if err != nil {
// bad escape
out = append(out, s[:6]...)
} else {
w := utf8.EncodeRune(buf[:], rune(c))
out = append(out, buf[:w]...)
}
nextIndex = 6
}
case 'U':
if len(s) < 10 {
// bad escape
out = append(out, s...)
nextIndex = len(s)
} else {
c, err := strconv.ParseUint(s[2:10], 16, 32)
if err != nil || c > 0x10ffff {
// bad escape
out = append(out, s[:10]...)
} else {
w := utf8.EncodeRune(buf[:], rune(c))
out = append(out, buf[:w]...)
}
nextIndex = 10
}
case 'a':
out = append(out, '\a')
case 'b':
out = append(out, '\b')
case 'f':
out = append(out, '\f')
case 'n':
out = append(out, '\n')
case 'r':
out = append(out, '\r')
case 't':
out = append(out, '\t')
case 'v':
out = append(out, '\v')
case '\\', '\'', '"', '?':
out = append(out, s[1])
default:
// invalid escape, just copy it as-is
out = append(out, s[:2]...)
}
s = s[nextIndex:]
}
return string(out)
}
func isOctal(b byte) bool { return b >= '0' && b <= '7' }
func isHex(b byte) bool {
return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
}
func matchPrefix(s string, limit int, fn func(byte) bool) int {
l := len(s)
if l > limit {
l = limit
}
i := 0
for ; i < l; i++ {
if !fn(s[i]) {
return i
}
}
return i
}
func (f *fldDescriptor) DefaultEnumValue() protoreflect.EnumValueDescriptor {
ed := f.Enum()
if ed == nil {
return nil
}
if f.proto.DefaultValue != nil {
if val := ed.Values().ByName(protoreflect.Name(f.proto.GetDefaultValue())); val != nil {
return val
}
}
// if no default specified in source, return nil
return nil
}
func (f *fldDescriptor) ContainingOneof() protoreflect.OneofDescriptor {
return f.oneof
}
func (f *fldDescriptor) ContainingMessage() protoreflect.MessageDescriptor {
if f.extendee != nil {
return f.extendee
}
return f.parent.(protoreflect.MessageDescriptor) //nolint:errcheck
}
func (f *fldDescriptor) Enum() protoreflect.EnumDescriptor {
return f.enumType
}
func (f *fldDescriptor) Message() protoreflect.MessageDescriptor {
return f.msgType
}
type oneofDescriptors struct {
protoreflect.OneofDescriptors
oneofs []oneofDescriptor
}
func (r *result) createOneofs(prefix string, parent *msgDescriptor, ooProtos []*descriptorpb.OneofDescriptorProto, pool *allocPool) oneofDescriptors {
oos := pool.getOneofs(len(ooProtos))
for i, fldProto := range ooProtos {
r.createOneofDescriptor(&oos[i], fldProto, parent, i, prefix+fldProto.GetName())
}
return oneofDescriptors{oneofs: oos}
}
func (o *oneofDescriptors) Len() int {
return len(o.oneofs)
}
func (o *oneofDescriptors) Get(i int) protoreflect.OneofDescriptor {
return &o.oneofs[i]
}
func (o *oneofDescriptors) ByName(s protoreflect.Name) protoreflect.OneofDescriptor {
for i := range o.oneofs {
oo := &o.oneofs[i]
if oo.Name() == s {
return oo
}
}
return nil
}
type oneofDescriptor struct {
protoreflect.OneofDescriptor
file *result
parent *msgDescriptor
index int
proto *descriptorpb.OneofDescriptorProto
fqn string
fields fldDescriptors
}
var _ protoreflect.OneofDescriptor = (*oneofDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*oneofDescriptor)(nil)
func (r *result) createOneofDescriptor(ret *oneofDescriptor, ood *descriptorpb.OneofDescriptorProto, parent *msgDescriptor, index int, fqn string) {
r.descriptors[fqn] = ret
ret.OneofDescriptor = noOpOneof
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = ood
ret.fqn = fqn
var fields []*fldDescriptor
for _, fld := range parent.fields.fields {
if fld.proto.OneofIndex != nil && int(fld.proto.GetOneofIndex()) == index {
fields = append(fields, fld)
}
}
ret.fields = fldDescriptors{fields: fields}
}
func (o *oneofDescriptor) OneofDescriptorProto() *descriptorpb.OneofDescriptorProto {
return o.proto
}
func (o *oneofDescriptor) AsProto() proto.Message {
return o.proto
}
func (o *oneofDescriptor) ParentFile() protoreflect.FileDescriptor {
return o.file
}
func (o *oneofDescriptor) Parent() protoreflect.Descriptor {
return o.parent
}
func (o *oneofDescriptor) Index() int {
return o.index
}
func (o *oneofDescriptor) Syntax() protoreflect.Syntax {
return o.file.Syntax()
}
func (o *oneofDescriptor) Name() protoreflect.Name {
return protoreflect.Name(o.proto.GetName())
}
func (o *oneofDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(o.fqn)
}
func (o *oneofDescriptor) IsPlaceholder() bool {
return false
}
func (o *oneofDescriptor) Options() protoreflect.ProtoMessage {
return o.proto.Options
}
func (o *oneofDescriptor) IsSynthetic() bool {
for _, fld := range o.parent.proto.GetField() {
if fld.OneofIndex != nil && int(fld.GetOneofIndex()) == o.index {
return fld.GetProto3Optional()
}
}
return false // NB: we should never get here
}
func (o *oneofDescriptor) Fields() protoreflect.FieldDescriptors {
return &o.fields
}
type svcDescriptors struct {
protoreflect.ServiceDescriptors
svcs []svcDescriptor
}
func (r *result) createServices(prefix string, svcProtos []*descriptorpb.ServiceDescriptorProto, pool *allocPool) svcDescriptors {
svcs := pool.getServices(len(svcProtos))
for i, svcProto := range svcProtos {
r.createServiceDescriptor(&svcs[i], svcProto, i, prefix+svcProto.GetName(), pool)
}
return svcDescriptors{svcs: svcs}
}
func (s *svcDescriptors) Len() int {
return len(s.svcs)
}
func (s *svcDescriptors) Get(i int) protoreflect.ServiceDescriptor {
return &s.svcs[i]
}
func (s *svcDescriptors) ByName(n protoreflect.Name) protoreflect.ServiceDescriptor {
for i := range s.svcs {
svc := &s.svcs[i]
if svc.Name() == n {
return svc
}
}
return nil
}
type svcDescriptor struct {
protoreflect.ServiceDescriptor
file *result
index int
proto *descriptorpb.ServiceDescriptorProto
fqn string
methods mtdDescriptors
}
var _ protoreflect.ServiceDescriptor = (*svcDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*svcDescriptor)(nil)
func (r *result) createServiceDescriptor(ret *svcDescriptor, sd *descriptorpb.ServiceDescriptorProto, index int, fqn string, pool *allocPool) {
r.descriptors[fqn] = ret
ret.ServiceDescriptor = noOpService
ret.file = r
ret.index = index
ret.proto = sd
ret.fqn = fqn
prefix := fqn + "."
ret.methods = r.createMethods(prefix, ret, sd.Method, pool)
}
func (s *svcDescriptor) ServiceDescriptorProto() *descriptorpb.ServiceDescriptorProto {
return s.proto
}
func (s *svcDescriptor) AsProto() proto.Message {
return s.proto
}
func (s *svcDescriptor) ParentFile() protoreflect.FileDescriptor {
return s.file
}
func (s *svcDescriptor) Parent() protoreflect.Descriptor {
return s.file
}
func (s *svcDescriptor) Index() int {
return s.index
}
func (s *svcDescriptor) Syntax() protoreflect.Syntax {
return s.file.Syntax()
}
func (s *svcDescriptor) Name() protoreflect.Name {
return protoreflect.Name(s.proto.GetName())
}
func (s *svcDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(s.fqn)
}
func (s *svcDescriptor) IsPlaceholder() bool {
return false
}
func (s *svcDescriptor) Options() protoreflect.ProtoMessage {
return s.proto.Options
}
func (s *svcDescriptor) Methods() protoreflect.MethodDescriptors {
return &s.methods
}
type mtdDescriptors struct {
protoreflect.MethodDescriptors
mtds []mtdDescriptor
}
func (r *result) createMethods(prefix string, parent *svcDescriptor, mtdProtos []*descriptorpb.MethodDescriptorProto, pool *allocPool) mtdDescriptors {
mtds := pool.getMethods(len(mtdProtos))
for i, mtdProto := range mtdProtos {
r.createMethodDescriptor(&mtds[i], mtdProto, parent, i, prefix+mtdProto.GetName())
}
return mtdDescriptors{mtds: mtds}
}
func (m *mtdDescriptors) Len() int {
return len(m.mtds)
}
func (m *mtdDescriptors) Get(i int) protoreflect.MethodDescriptor {
return &m.mtds[i]
}
func (m *mtdDescriptors) ByName(n protoreflect.Name) protoreflect.MethodDescriptor {
for i := range m.mtds {
mtd := &m.mtds[i]
if mtd.Name() == n {
return mtd
}
}
return nil
}
type mtdDescriptor struct {
protoreflect.MethodDescriptor
file *result
parent *svcDescriptor
index int
proto *descriptorpb.MethodDescriptorProto
fqn string
inputType, outputType protoreflect.MessageDescriptor
}
var _ protoreflect.MethodDescriptor = (*mtdDescriptor)(nil)
var _ protoutil.DescriptorProtoWrapper = (*mtdDescriptor)(nil)
func (r *result) createMethodDescriptor(ret *mtdDescriptor, mtd *descriptorpb.MethodDescriptorProto, parent *svcDescriptor, index int, fqn string) {
r.descriptors[fqn] = ret
ret.MethodDescriptor = noOpMethod
ret.file = r
ret.parent = parent
ret.index = index
ret.proto = mtd
ret.fqn = fqn
}
func (m *mtdDescriptor) MethodDescriptorProto() *descriptorpb.MethodDescriptorProto {
return m.proto
}
func (m *mtdDescriptor) AsProto() proto.Message {
return m.proto
}
func (m *mtdDescriptor) ParentFile() protoreflect.FileDescriptor {
return m.file
}
func (m *mtdDescriptor) Parent() protoreflect.Descriptor {
return m.parent
}
func (m *mtdDescriptor) Index() int {
return m.index
}
func (m *mtdDescriptor) Syntax() protoreflect.Syntax {
return m.file.Syntax()
}
func (m *mtdDescriptor) Name() protoreflect.Name {
return protoreflect.Name(m.proto.GetName())
}
func (m *mtdDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(m.fqn)
}
func (m *mtdDescriptor) IsPlaceholder() bool {
return false
}
func (m *mtdDescriptor) Options() protoreflect.ProtoMessage {
return m.proto.Options
}
func (m *mtdDescriptor) Input() protoreflect.MessageDescriptor {
return m.inputType
}
func (m *mtdDescriptor) Output() protoreflect.MessageDescriptor {
return m.outputType
}
func (m *mtdDescriptor) IsStreamingClient() bool {
return m.proto.GetClientStreaming()
}
func (m *mtdDescriptor) IsStreamingServer() bool {
return m.proto.GetServerStreaming()
}
func (r *result) FindImportByPath(path string) File {
return r.deps.FindFileByPath(path)
}
func (r *result) FindExtensionByNumber(msg protoreflect.FullName, tag protoreflect.FieldNumber) protoreflect.ExtensionTypeDescriptor {
return findExtension(r, msg, tag)
}
func (r *result) FindDescriptorByName(name protoreflect.FullName) protoreflect.Descriptor {
fqn := strings.TrimPrefix(string(name), ".")
return r.descriptors[fqn]
}
func (r *result) hasSource() bool {
n := r.FileNode()
_, ok := n.(*ast.FileNode)
return ok
}
// resolveFeature resolves a feature for the given descriptor. If the given element
// is in a proto2 or proto3 syntax file, this skips resolution and just returns the
// relevant default (since such files are not allowed to override features).
//
// If neither the given element nor any of its ancestors override the given feature,
// the relevant default is returned.
func resolveFeature(element protoreflect.Descriptor, feature protoreflect.FieldDescriptor) protoreflect.Value {
edition := editions.GetEdition(element)
if edition == descriptorpb.Edition_EDITION_PROTO2 || edition == descriptorpb.Edition_EDITION_PROTO3 {
// these syntax levels can't specify features, so we can short-circuit the search
// through the descriptor hierarchy for feature overrides
defaults := editions.GetEditionDefaults(edition)
return defaults.ProtoReflect().Get(feature) // returns default value if field is not present
}
val, err := editions.ResolveFeature(element, feature)
if err == nil && val.IsValid() {
return val
}
defaults := editions.GetEditionDefaults(edition)
return defaults.ProtoReflect().Get(feature)
}
func isJSONCompliant(d protoreflect.Descriptor) bool {
jsonFormat := resolveFeature(d, jsonFormatField)
return descriptorpb.FeatureSet_JsonFormat(jsonFormat.Enum()) == descriptorpb.FeatureSet_ALLOW
}
type sourcePathKey string
func pathKey(p protoreflect.SourcePath) sourcePathKey {
return pathKeyNoCopy(slices.Clone(p))
}
func pathKeyNoCopy(p protoreflect.SourcePath) sourcePathKey {
return sourcePathKey(unsafex.StringAlias(p))
}
// 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 linker
import (
"fmt"
"strings"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/bufbuild/protocompile/walk"
)
// File is like a super-powered protoreflect.FileDescriptor. It includes helpful
// methods for looking up elements in the descriptor and can be used to create a
// resolver for the entire transitive closure of the file's dependencies. (See
// ResolverFromFile.)
type File interface {
protoreflect.FileDescriptor
// FindDescriptorByName returns the given named element that is defined in
// this file. If no such element exists, nil is returned.
FindDescriptorByName(name protoreflect.FullName) protoreflect.Descriptor
// FindImportByPath returns the File corresponding to the given import path.
// If this file does not import the given path, nil is returned.
FindImportByPath(path string) File
// FindExtensionByNumber returns the extension descriptor for the given tag
// that extends the given message name. If no such extension is defined in this
// file, nil is returned.
FindExtensionByNumber(message protoreflect.FullName, tag protoreflect.FieldNumber) protoreflect.ExtensionTypeDescriptor
}
// NewFile converts a protoreflect.FileDescriptor to a File. The given deps must
// contain all dependencies/imports of f. Also see NewFileRecursive.
func NewFile(f protoreflect.FileDescriptor, deps Files) (File, error) {
if asFile, ok := f.(File); ok {
return asFile, nil
}
checkedDeps := make(Files, f.Imports().Len())
for i := range f.Imports().Len() {
imprt := f.Imports().Get(i)
dep := deps.FindFileByPath(imprt.Path())
if dep == nil {
return nil, fmt.Errorf("cannot create File for %q: missing dependency for %q", f.Path(), imprt.Path())
}
checkedDeps[i] = dep
}
return newFile(f, checkedDeps)
}
func newFile(f protoreflect.FileDescriptor, deps Files) (File, error) {
descs := map[protoreflect.FullName]protoreflect.Descriptor{}
err := walk.Descriptors(f, func(d protoreflect.Descriptor) error {
if _, ok := descs[d.FullName()]; ok {
return fmt.Errorf("file %q contains multiple elements with the name %s", f.Path(), d.FullName())
}
descs[d.FullName()] = d
return nil
})
if err != nil {
return nil, err
}
return &file{
FileDescriptor: f,
descs: descs,
deps: deps,
}, nil
}
// NewFileRecursive recursively converts a protoreflect.FileDescriptor to a File.
// If f has any dependencies/imports, they are converted, too, including any and
// all transitive dependencies.
//
// If f already implements File, it is returned unchanged.
func NewFileRecursive(f protoreflect.FileDescriptor) (File, error) {
if asFile, ok := f.(File); ok {
return asFile, nil
}
return newFileRecursive(f, map[protoreflect.FileDescriptor]File{})
}
func newFileRecursive(fd protoreflect.FileDescriptor, seen map[protoreflect.FileDescriptor]File) (File, error) {
if res, ok := seen[fd]; ok {
if res == nil {
return nil, fmt.Errorf("import cycle encountered: file %s transitively imports itself", fd.Path())
}
return res, nil
}
if f, ok := fd.(File); ok {
seen[fd] = f
return f, nil
}
seen[fd] = nil
deps := make([]File, fd.Imports().Len())
for i := range fd.Imports().Len() {
imprt := fd.Imports().Get(i)
dep, err := newFileRecursive(imprt, seen)
if err != nil {
return nil, err
}
deps[i] = dep
}
f, err := newFile(fd, deps)
if err != nil {
return nil, err
}
seen[fd] = f
return f, nil
}
type file struct {
protoreflect.FileDescriptor
descs map[protoreflect.FullName]protoreflect.Descriptor
deps Files
}
var _ File = (*file)(nil)
func (f *file) FindDescriptorByName(name protoreflect.FullName) protoreflect.Descriptor {
return f.descs[name]
}
func (f *file) FindImportByPath(path string) File {
return f.deps.FindFileByPath(path)
}
func (f *file) FindExtensionByNumber(msg protoreflect.FullName, tag protoreflect.FieldNumber) protoreflect.ExtensionTypeDescriptor {
return findExtension(f, msg, tag)
}
func (f *file) Unwrap() protoreflect.FileDescriptor {
return f.FileDescriptor
}
// Files represents a set of protobuf files. It is a slice of File values, but
// also provides a method for easily looking up files by path and name.
type Files []File
// FindFileByPath finds a file in f that has the given path and name. If f
// contains no such file, nil is returned.
func (f Files) FindFileByPath(path string) File {
for _, file := range f {
if file.Path() == path {
return file
}
}
return nil
}
// AsResolver returns a Resolver that uses f as the source of descriptors. If
// a given query cannot be answered with the files in f, the query will fail
// with a protoregistry.NotFound error. The implementation just delegates calls
// to each file until a result is found.
//
// Also see ResolverFromFile.
func (f Files) AsResolver() Resolver {
return filesResolver(f)
}
// Resolver is an interface that can resolve various kinds of queries about
// descriptors. It satisfies the resolver interfaces defined in protodesc
// and protoregistry packages.
type Resolver interface {
protodesc.Resolver
protoregistry.MessageTypeResolver
protoregistry.ExtensionTypeResolver
}
// ResolverFromFile returns a Resolver that can resolve any element that is
// visible to the given file. It will search the given file, its imports, and
// any transitive public imports.
//
// Note that this function does not compute any additional indexes for efficient
// search, so queries generally take linear time, O(n) where n is the number of
// files whose elements are visible to the given file. Queries for an extension
// by number have runtime complexity that is linear with the number of messages
// and extensions defined across those files.
func ResolverFromFile(f File) Resolver {
return fileResolver{f: f}
}
type fileResolver struct {
f File
}
func (r fileResolver) FindFileByPath(path string) (protoreflect.FileDescriptor, error) {
return resolveInFile(r.f, false, nil, func(f File) (protoreflect.FileDescriptor, error) {
if f.Path() == path {
return f, nil
}
return nil, protoregistry.NotFound
})
}
func (r fileResolver) FindDescriptorByName(name protoreflect.FullName) (protoreflect.Descriptor, error) {
return resolveInFile(r.f, false, nil, func(f File) (protoreflect.Descriptor, error) {
if d := f.FindDescriptorByName(name); d != nil {
return d, nil
}
return nil, protoregistry.NotFound
})
}
func (r fileResolver) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error) {
return resolveInFile(r.f, false, nil, func(f File) (protoreflect.MessageType, error) {
d := f.FindDescriptorByName(message)
if d != nil {
md, ok := d.(protoreflect.MessageDescriptor)
if !ok {
return nil, fmt.Errorf("%q is %s, not a message", message, descriptorTypeWithArticle(d))
}
return dynamicpb.NewMessageType(md), nil
}
return nil, protoregistry.NotFound
})
}
func (r fileResolver) FindMessageByURL(url string) (protoreflect.MessageType, error) {
fullName := messageNameFromURL(url)
return r.FindMessageByName(protoreflect.FullName(fullName))
}
func messageNameFromURL(url string) string {
lastSlash := strings.LastIndexByte(url, '/')
return url[lastSlash+1:]
}
func (r fileResolver) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
return resolveInFile(r.f, false, nil, func(f File) (protoreflect.ExtensionType, error) {
d := f.FindDescriptorByName(field)
if d != nil {
fld, ok := d.(protoreflect.FieldDescriptor)
if !ok || !fld.IsExtension() {
return nil, fmt.Errorf("%q is %s, not an extension", field, descriptorTypeWithArticle(d))
}
if extd, ok := fld.(protoreflect.ExtensionTypeDescriptor); ok {
return extd.Type(), nil
}
return dynamicpb.NewExtensionType(fld), nil
}
return nil, protoregistry.NotFound
})
}
func (r fileResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
return resolveInFile(r.f, false, nil, func(f File) (protoreflect.ExtensionType, error) {
ext := findExtension(f, message, field)
if ext != nil {
return ext.Type(), nil
}
return nil, protoregistry.NotFound
})
}
type filesResolver []File
func (r filesResolver) FindFileByPath(path string) (protoreflect.FileDescriptor, error) {
for _, f := range r {
if f.Path() == path {
return f, nil
}
}
return nil, protoregistry.NotFound
}
func (r filesResolver) FindDescriptorByName(name protoreflect.FullName) (protoreflect.Descriptor, error) {
for _, f := range r {
result := f.FindDescriptorByName(name)
if result != nil {
return result, nil
}
}
return nil, protoregistry.NotFound
}
func (r filesResolver) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error) {
for _, f := range r {
d := f.FindDescriptorByName(message)
if d != nil {
if md, ok := d.(protoreflect.MessageDescriptor); ok {
return dynamicpb.NewMessageType(md), nil
}
return nil, protoregistry.NotFound
}
}
return nil, protoregistry.NotFound
}
func (r filesResolver) FindMessageByURL(url string) (protoreflect.MessageType, error) {
name := messageNameFromURL(url)
return r.FindMessageByName(protoreflect.FullName(name))
}
func (r filesResolver) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
for _, f := range r {
d := f.FindDescriptorByName(field)
if d != nil {
if extd, ok := d.(protoreflect.ExtensionTypeDescriptor); ok {
return extd.Type(), nil
}
if fld, ok := d.(protoreflect.FieldDescriptor); ok && fld.IsExtension() {
return dynamicpb.NewExtensionType(fld), nil
}
return nil, protoregistry.NotFound
}
}
return nil, protoregistry.NotFound
}
func (r filesResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
for _, f := range r {
ext := findExtension(f, message, field)
if ext != nil {
return ext.Type(), nil
}
}
return nil, protoregistry.NotFound
}
type hasExtensionsAndMessages interface {
Messages() protoreflect.MessageDescriptors
Extensions() protoreflect.ExtensionDescriptors
}
func findExtension(d hasExtensionsAndMessages, message protoreflect.FullName, field protoreflect.FieldNumber) protoreflect.ExtensionTypeDescriptor {
for i := range d.Extensions().Len() {
if extType := isExtensionMatch(d.Extensions().Get(i), message, field); extType != nil {
return extType
}
}
for i := range d.Messages().Len() {
if extType := findExtension(d.Messages().Get(i), message, field); extType != nil {
return extType
}
}
return nil // could not be found
}
func isExtensionMatch(ext protoreflect.ExtensionDescriptor, message protoreflect.FullName, field protoreflect.FieldNumber) protoreflect.ExtensionTypeDescriptor {
if ext.Number() != field || ext.ContainingMessage().FullName() != message {
return nil
}
if extType, ok := ext.(protoreflect.ExtensionTypeDescriptor); ok {
return extType
}
return dynamicpb.NewExtensionType(ext).TypeDescriptor()
}
// 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 linker
import (
"fmt"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/reporter"
)
// Link handles linking a parsed descriptor proto into a fully-linked descriptor.
// If the given parser.Result has imports, they must all be present in the given
// dependencies.
//
// The symbols value is optional and may be nil. If it is not nil, it must be the
// same instance used to create and link all of the given result's dependencies
// (or otherwise already have all dependencies imported). Otherwise, linking may
// fail with spurious errors resolving symbols.
//
// The handler value is used to report any link errors. If any such errors are
// reported, this function returns a non-nil error. The Result value returned
// also implements protoreflect.FileDescriptor.
//
// Note that linking does NOT interpret options. So options messages in the
// returned value have all values stored in UninterpretedOptions fields.
func Link(parsed parser.Result, dependencies Files, symbols *Symbols, handler *reporter.Handler) (Result, error) {
if symbols == nil {
symbols = &Symbols{}
}
prefix := parsed.FileDescriptorProto().GetPackage()
if prefix != "" {
prefix += "."
}
for _, imp := range parsed.FileDescriptorProto().Dependency {
dep := dependencies.FindFileByPath(imp)
if dep == nil {
return nil, fmt.Errorf("dependencies is missing import %q", imp)
}
if err := symbols.Import(dep, handler); err != nil {
return nil, err
}
}
r := &result{
FileDescriptor: noOpFile,
Result: parsed,
deps: dependencies,
descriptors: map[string]protoreflect.Descriptor{},
usedImports: map[string]struct{}{},
prefix: prefix,
optionQualifiedNames: map[ast.IdentValueNode]string{},
}
// First, we create the hierarchy of descendant descriptors.
r.createDescendants()
// Then we can put all symbols into a single pool, which lets us ensure there
// are no duplicate symbols and will also let us resolve and revise all type
// references in next step.
if err := symbols.importResult(r, handler); err != nil {
return nil, err
}
// After we've populated the pool, we can now try to resolve all type
// references. All references must be checked for correct type, any fields
// with enum types must be corrected (since we parse them as if they are
// message references since we don't actually know message or enum until
// link time), and references will be re-written to be fully-qualified
// references (e.g. start with a dot ".").
if err := r.resolveReferences(handler, symbols); err != nil {
return nil, err
}
return r, handler.Error()
}
// Result is the result of linking. This is a protoreflect.FileDescriptor, but
// with some additional methods for exposing additional information, such as the
// for accessing the input AST or file descriptor.
//
// It also provides Resolve* methods, for looking up enums, messages, and
// extensions that are available to the protobuf source file this result
// represents. An element is "available" if it meets any of the following
// criteria:
// 1. The element is defined in this file itself.
// 2. The element is defined in a file that is directly imported by this file.
// 3. The element is "available" to a file that is directly imported by this
// file as a public import.
//
// Other elements, even if in the transitive closure of this file, are not
// available and thus won't be returned by these methods.
type Result interface {
File
parser.Result
// ResolveMessageLiteralExtensionName returns the fully qualified name for
// an identifier for extension field names in message literals.
ResolveMessageLiteralExtensionName(ast.IdentValueNode) string
// ValidateOptions runs some validation checks on the descriptor that can only
// be done after options are interpreted. Any errors or warnings encountered
// will be reported via the given handler. If any error is reported, this
// function returns a non-nil error.
ValidateOptions(handler *reporter.Handler, symbols *Symbols) error
// CheckForUnusedImports is used to report warnings for unused imports. This
// should be called after options have been interpreted. Otherwise, the logic
// could incorrectly report imports as unused if the only symbol used were a
// custom option.
CheckForUnusedImports(handler *reporter.Handler)
// PopulateSourceCodeInfo is used to populate source code info for the file
// descriptor. This step requires that the underlying descriptor proto have
// its `source_code_info` field populated. This is typically a post-process
// step separate from linking, because computing source code info requires
// interpreting options (which is done after linking).
PopulateSourceCodeInfo()
// RemoveAST drops the AST information from this result.
RemoveAST()
}
// ErrorUnusedImport may be passed to a warning reporter when an unused
// import is detected. The error the reporter receives will be wrapped
// with source position that indicates the file and line where the import
// statement appeared.
type ErrorUnusedImport interface {
error
UnusedImport() string
}
type errUnusedImport string
func (e errUnusedImport) Error() string {
return fmt.Sprintf("import %q not used", string(e))
}
func (e errUnusedImport) UnusedImport() string {
return string(e)
}
// 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 linker
import "google.golang.org/protobuf/types/descriptorpb"
// allocPool helps allocate descriptor instances. Instead of allocating
// them one at a time, we allocate a pool -- a large, flat slice to hold
// all descriptors of a particular kind for a file. We then use capacity
// in the pool when we need space for individual descriptors.
type allocPool struct {
numMessages int
numFields int
numOneofs int
numEnums int
numEnumValues int
numExtensions int
numServices int
numMethods int
messages []msgDescriptor
fields []fldDescriptor
oneofs []oneofDescriptor
enums []enumDescriptor
enumVals []enValDescriptor
extensions []extTypeDescriptor
services []svcDescriptor
methods []mtdDescriptor
}
func newAllocPool(file *descriptorpb.FileDescriptorProto) *allocPool {
var pool allocPool
pool.countElements(file)
pool.messages = make([]msgDescriptor, pool.numMessages)
pool.fields = make([]fldDescriptor, pool.numFields)
pool.oneofs = make([]oneofDescriptor, pool.numOneofs)
pool.enums = make([]enumDescriptor, pool.numEnums)
pool.enumVals = make([]enValDescriptor, pool.numEnumValues)
pool.extensions = make([]extTypeDescriptor, pool.numExtensions)
pool.services = make([]svcDescriptor, pool.numServices)
pool.methods = make([]mtdDescriptor, pool.numMethods)
return &pool
}
func (p *allocPool) getMessages(count int) []msgDescriptor {
allocated := p.messages[:count]
p.messages = p.messages[count:]
return allocated
}
func (p *allocPool) getFields(count int) []fldDescriptor {
allocated := p.fields[:count]
p.fields = p.fields[count:]
return allocated
}
func (p *allocPool) getOneofs(count int) []oneofDescriptor {
allocated := p.oneofs[:count]
p.oneofs = p.oneofs[count:]
return allocated
}
func (p *allocPool) getEnums(count int) []enumDescriptor {
allocated := p.enums[:count]
p.enums = p.enums[count:]
return allocated
}
func (p *allocPool) getEnumValues(count int) []enValDescriptor {
allocated := p.enumVals[:count]
p.enumVals = p.enumVals[count:]
return allocated
}
func (p *allocPool) getExtensions(count int) []extTypeDescriptor {
allocated := p.extensions[:count]
p.extensions = p.extensions[count:]
return allocated
}
func (p *allocPool) getServices(count int) []svcDescriptor {
allocated := p.services[:count]
p.services = p.services[count:]
return allocated
}
func (p *allocPool) getMethods(count int) []mtdDescriptor {
allocated := p.methods[:count]
p.methods = p.methods[count:]
return allocated
}
func (p *allocPool) countElements(file *descriptorpb.FileDescriptorProto) {
p.countElementsInMessages(file.MessageType)
p.countElementsInEnums(file.EnumType)
p.numExtensions += len(file.Extension)
p.numServices += len(file.Service)
for _, svc := range file.Service {
p.numMethods += len(svc.Method)
}
}
func (p *allocPool) countElementsInMessages(msgs []*descriptorpb.DescriptorProto) {
p.numMessages += len(msgs)
for _, msg := range msgs {
p.numFields += len(msg.Field)
p.numOneofs += len(msg.OneofDecl)
p.countElementsInMessages(msg.NestedType)
p.countElementsInEnums(msg.EnumType)
p.numExtensions += len(msg.Extension)
}
}
func (p *allocPool) countElementsInEnums(enums []*descriptorpb.EnumDescriptorProto) {
p.numEnums += len(enums)
for _, enum := range enums {
p.numEnumValues += len(enum.Value)
}
}
// 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 linker
import (
"errors"
"fmt"
"strings"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/walk"
)
func (r *result) ResolveMessageLiteralExtensionName(node ast.IdentValueNode) string {
return r.optionQualifiedNames[node]
}
func (r *result) resolveElement(name protoreflect.FullName, checkedCache []string) protoreflect.Descriptor {
if len(name) > 0 && name[0] == '.' {
name = name[1:]
}
res, _ := resolveInFile(r, false, checkedCache[:0], func(f File) (protoreflect.Descriptor, error) {
d := resolveElementInFile(name, f)
if d != nil {
return d, nil
}
return nil, protoregistry.NotFound
})
return res
}
func resolveInFile[T any](f File, publicImportsOnly bool, checked []string, fn func(File) (T, error)) (T, error) {
var zero T
path := f.Path()
for _, str := range checked {
if str == path {
// already checked
return zero, protoregistry.NotFound
}
}
checked = append(checked, path)
res, err := fn(f)
if err == nil {
// found it
return res, nil
}
if !errors.Is(err, protoregistry.NotFound) {
return zero, err
}
imports := f.Imports()
for i, l := 0, imports.Len(); i < l; i++ {
imp := imports.Get(i)
if publicImportsOnly && !imp.IsPublic {
continue
}
res, err := resolveInFile(f.FindImportByPath(imp.Path()), true, checked, fn)
if errors.Is(err, protoregistry.NotFound) {
continue
}
if err != nil {
return zero, err
}
if !imp.IsPublic {
if r, ok := f.(*result); ok {
r.markUsed(imp.Path())
}
}
return res, nil
}
return zero, err
}
func (r *result) markUsed(importPath string) {
r.usedImports[importPath] = struct{}{}
}
func (r *result) CheckForUnusedImports(handler *reporter.Handler) {
fd := r.FileDescriptorProto()
file, _ := r.FileNode().(*ast.FileNode)
for i, dep := range fd.Dependency {
if _, ok := r.usedImports[dep]; !ok {
isPublic := false
// it's fine if it's a public import
for _, j := range fd.PublicDependency {
if i == int(j) {
isPublic = true
break
}
}
if isPublic {
continue
}
span := ast.UnknownSpan(fd.GetName())
if file != nil {
for _, decl := range file.Decls {
imp, ok := decl.(*ast.ImportNode)
if ok && imp.Name.AsString() == dep {
span = file.NodeInfo(imp)
}
}
}
handler.HandleWarningWithPos(span, errUnusedImport(dep))
}
}
}
func descriptorTypeWithArticle(d protoreflect.Descriptor) string {
switch d := d.(type) {
case protoreflect.MessageDescriptor:
return "a message"
case protoreflect.FieldDescriptor:
if d.IsExtension() {
return "an extension"
}
return "a field"
case protoreflect.OneofDescriptor:
return "a oneof"
case protoreflect.EnumDescriptor:
return "an enum"
case protoreflect.EnumValueDescriptor:
return "an enum value"
case protoreflect.ServiceDescriptor:
return "a service"
case protoreflect.MethodDescriptor:
return "a method"
case protoreflect.FileDescriptor:
return "a file"
default:
// shouldn't be possible
return fmt.Sprintf("a %T", d)
}
}
func (r *result) createDescendants() {
fd := r.FileDescriptorProto()
pool := newAllocPool(fd)
prefix := ""
if fd.GetPackage() != "" {
prefix = fd.GetPackage() + "."
}
r.imports = r.createImports()
r.optionImports = fileImports{files: r.createBasicImports(fd.OptionDependency)}
r.messages = r.createMessages(prefix, r, fd.MessageType, pool)
r.enums = r.createEnums(prefix, r, fd.EnumType, pool)
r.extensions = r.createExtensions(prefix, r, fd.Extension, pool)
r.services = r.createServices(prefix, fd.Service, pool)
}
func (r *result) resolveReferences(handler *reporter.Handler, s *Symbols) error {
fd := r.FileDescriptorProto()
checkedCache := make([]string, 0, 16)
scopes := []scope{fileScope(r, checkedCache)}
if fd.Options != nil {
if err := r.resolveOptions(handler, "file", protoreflect.FullName(fd.GetName()), fd.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
// This is to de-dupe extendee-releated error messages when the same
// extendee is referenced from multiple extension field definitions.
// We leave it nil if there's no AST.
var extendeeNodes map[ast.Node]struct{}
return walk.DescriptorsEnterAndExit(r,
func(d protoreflect.Descriptor) error {
fqn := d.FullName()
switch d := d.(type) {
case *msgDescriptor:
// Strangely, when protoc resolves extension names, it uses the *enclosing* scope
// instead of the message's scope. So if the message contains an extension named "i",
// an option cannot refer to it as simply "i" but must qualify it (at a minimum "Msg.i").
// So we don't add this messages scope to our scopes slice until *after* we do options.
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "message", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
scopes = append(scopes, messageScope(r, fqn)) // push new scope on entry
// walk only visits descriptors, so we need to loop over extension ranges ourselves
for _, er := range d.proto.ExtensionRange {
if er.Options != nil {
erName := protoreflect.FullName(fmt.Sprintf("%s:%d-%d", fqn, er.GetStart(), er.GetEnd()-1))
if err := r.resolveOptions(handler, "extension range", erName, er.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
}
case *extTypeDescriptor:
if d.field.proto.Options != nil {
if err := r.resolveOptions(handler, "extension", fqn, d.field.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
if extendeeNodes == nil && r.AST() != nil {
extendeeNodes = map[ast.Node]struct{}{}
}
if err := resolveFieldTypes(&d.field, handler, extendeeNodes, s, scopes, checkedCache); err != nil {
return err
}
if r.Syntax() == protoreflect.Proto3 && !allowedProto3Extendee(d.field.proto.GetExtendee()) {
file := r.FileNode()
node := r.FieldNode(d.field.proto).FieldExtendee()
if err := handler.HandleErrorf(file.NodeInfo(node), "extend blocks in proto3 can only be used to define custom options"); err != nil {
return err
}
}
case *fldDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "field", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
if err := resolveFieldTypes(d, handler, nil, s, scopes, checkedCache); err != nil {
return err
}
case *oneofDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "oneof", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
case *enumDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "enum", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
case *enValDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "enum value", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
case *svcDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "service", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
// not a message, but same scoping rules for nested elements as if it were
scopes = append(scopes, messageScope(r, fqn)) // push new scope on entry
case *mtdDescriptor:
if d.proto.Options != nil {
if err := r.resolveOptions(handler, "method", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
return err
}
}
if err := resolveMethodTypes(d, handler, scopes, checkedCache); err != nil {
return err
}
}
return nil
},
func(d protoreflect.Descriptor) error {
switch d.(type) {
case protoreflect.MessageDescriptor, protoreflect.ServiceDescriptor:
// pop message scope on exit
scopes = scopes[:len(scopes)-1]
}
return nil
})
}
var allowedProto3Extendees = map[string]struct{}{
".google.protobuf.FileOptions": {},
".google.protobuf.MessageOptions": {},
".google.protobuf.FieldOptions": {},
".google.protobuf.OneofOptions": {},
".google.protobuf.ExtensionRangeOptions": {},
".google.protobuf.EnumOptions": {},
".google.protobuf.EnumValueOptions": {},
".google.protobuf.ServiceOptions": {},
".google.protobuf.MethodOptions": {},
}
func allowedProto3Extendee(n string) bool {
if n == "" {
// not an extension, allowed
return true
}
_, ok := allowedProto3Extendees[n]
return ok
}
func resolveFieldTypes(f *fldDescriptor, handler *reporter.Handler, extendees map[ast.Node]struct{}, s *Symbols, scopes []scope, checkedCache []string) error {
r := f.file
fld := f.proto
file := r.FileNode()
node := r.FieldNode(fld)
kind := "field"
if fld.GetExtendee() != "" {
kind = "extension"
var alreadyReported bool
if extendees != nil {
_, alreadyReported = extendees[node.FieldExtendee()]
if !alreadyReported {
extendees[node.FieldExtendee()] = struct{}{}
}
}
dsc := r.resolve(fld.GetExtendee(), false, scopes, checkedCache)
if dsc == nil {
if alreadyReported {
return nil
}
var extendeePrefix string
if extendees == nil {
extendeePrefix = kind + " " + f.fqn + ": "
}
return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sunknown extendee type %s", extendeePrefix, fld.GetExtendee())
}
if isSentinelDescriptor(dsc) {
if alreadyReported {
return nil
}
var extendeePrefix string
if extendees == nil {
extendeePrefix = kind + " " + f.fqn + ": "
}
return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sunknown extendee type %s; resolved to %s which is not defined; consider using a leading dot", extendeePrefix, fld.GetExtendee(), dsc.FullName())
}
extd, ok := dsc.(protoreflect.MessageDescriptor)
if !ok {
if alreadyReported {
return nil
}
var extendeePrefix string
if extendees == nil {
extendeePrefix = kind + " " + f.fqn + ": "
}
return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sextendee is invalid: %s is %s, not a message", extendeePrefix, dsc.FullName(), descriptorTypeWithArticle(dsc))
}
f.extendee = extd
extendeeName := "." + string(dsc.FullName())
if fld.GetExtendee() != extendeeName {
fld.Extendee = proto.String(extendeeName)
}
// make sure the tag number is in range
found := false
tag := protoreflect.FieldNumber(fld.GetNumber())
for i := range extd.ExtensionRanges().Len() {
rng := extd.ExtensionRanges().Get(i)
if tag >= rng[0] && tag < rng[1] {
found = true
break
}
}
if !found {
if err := handler.HandleErrorf(file.NodeInfo(node.FieldTag()), "%s %s: tag %d is not in valid range for extended type %s", kind, f.fqn, tag, dsc.FullName()); err != nil {
return err
}
} else {
// make sure tag is not a duplicate
if err := s.AddExtension(packageFor(dsc), dsc.FullName(), tag, file.NodeInfo(node.FieldTag()), handler); err != nil {
return err
}
}
} else if f.proto.OneofIndex != nil {
parent := f.parent.(protoreflect.MessageDescriptor) //nolint:errcheck
index := int(f.proto.GetOneofIndex())
f.oneof = parent.Oneofs().Get(index)
}
if fld.GetTypeName() == "" {
// scalar type; no further resolution required
return nil
}
dsc := r.resolve(fld.GetTypeName(), true, scopes, checkedCache)
if dsc == nil {
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: unknown type %s", kind, f.fqn, fld.GetTypeName())
}
if isSentinelDescriptor(dsc) {
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: unknown type %s; resolved to %s which is not defined; consider using a leading dot", kind, f.fqn, fld.GetTypeName(), dsc.FullName())
}
switch dsc := dsc.(type) {
case protoreflect.MessageDescriptor:
if dsc.IsMapEntry() {
isValid := false
switch node.(type) {
case *ast.MapFieldNode:
// We have an AST for this file and can see this field is from a map declaration
isValid = true
case *ast.NoSourceNode:
// We don't have an AST for the file (it came from a provided descriptor). So we
// need to validate that it's not an illegal reference. To be valid, the field
// must be repeated and the entry type must be nested in the same enclosing
// message as the field.
isValid = isValidMap(f, dsc)
if isValid && f.index > 0 {
// also make sure there are no earlier fields that are valid for this map entry
flds := f.Parent().(protoreflect.MessageDescriptor).Fields() //nolint:errcheck
for i := range f.index {
if isValidMap(flds.Get(i), dsc) {
isValid = false
break
}
}
}
}
if !isValid {
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: %s is a synthetic map entry and may not be referenced explicitly", kind, f.fqn, dsc.FullName())
}
}
typeName := "." + string(dsc.FullName())
if fld.GetTypeName() != typeName {
fld.TypeName = proto.String(typeName)
}
if fld.Type == nil {
// if type was tentatively unset, we now know it's actually a message
fld.Type = descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
} else if fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE && fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_GROUP {
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: descriptor proto indicates type %v but should be %v", kind, f.fqn, fld.GetType(), descriptorpb.FieldDescriptorProto_TYPE_MESSAGE)
}
f.msgType = dsc
case protoreflect.EnumDescriptor:
typeName := "." + string(dsc.FullName())
if fld.GetTypeName() != typeName {
fld.TypeName = proto.String(typeName)
}
if fld.Type == nil {
// the type was tentatively unset, but now we know it's actually an enum
fld.Type = descriptorpb.FieldDescriptorProto_TYPE_ENUM.Enum()
} else if fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_ENUM {
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: descriptor proto indicates type %v but should be %v", kind, f.fqn, fld.GetType(), descriptorpb.FieldDescriptorProto_TYPE_ENUM)
}
f.enumType = dsc
default:
return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: invalid type: %s is %s, not a message or enum", kind, f.fqn, dsc.FullName(), descriptorTypeWithArticle(dsc))
}
return nil
}
func packageFor(dsc protoreflect.Descriptor) protoreflect.FullName {
if dsc.ParentFile() != nil {
return dsc.ParentFile().Package()
}
// Can't access package? Make a best effort guess.
return dsc.FullName().Parent()
}
func isValidMap(mapField protoreflect.FieldDescriptor, mapEntry protoreflect.MessageDescriptor) bool {
return !mapField.IsExtension() &&
mapEntry.Parent() == mapField.ContainingMessage() &&
mapField.Cardinality() == protoreflect.Repeated &&
string(mapEntry.Name()) == internal.MapEntry(string(mapField.Name()))
}
func resolveMethodTypes(m *mtdDescriptor, handler *reporter.Handler, scopes []scope, checkedCache []string) error {
scope := "method " + m.fqn
r := m.file
mtd := m.proto
file := r.FileNode()
node := r.MethodNode(mtd)
dsc := r.resolve(mtd.GetInputType(), false, scopes, checkedCache)
if dsc == nil {
if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: unknown request type %s", scope, mtd.GetInputType()); err != nil {
return err
}
} else if isSentinelDescriptor(dsc) {
if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: unknown request type %s; resolved to %s which is not defined; consider using a leading dot", scope, mtd.GetInputType(), dsc.FullName()); err != nil {
return err
}
} else if msg, ok := dsc.(protoreflect.MessageDescriptor); !ok {
if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: invalid request type: %s is %s, not a message", scope, dsc.FullName(), descriptorTypeWithArticle(dsc)); err != nil {
return err
}
} else {
typeName := "." + string(dsc.FullName())
if mtd.GetInputType() != typeName {
mtd.InputType = proto.String(typeName)
}
m.inputType = msg
}
// TODO: make input and output type resolution more DRY
dsc = r.resolve(mtd.GetOutputType(), false, scopes, checkedCache)
if dsc == nil {
if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: unknown response type %s", scope, mtd.GetOutputType()); err != nil {
return err
}
} else if isSentinelDescriptor(dsc) {
if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: unknown response type %s; resolved to %s which is not defined; consider using a leading dot", scope, mtd.GetOutputType(), dsc.FullName()); err != nil {
return err
}
} else if msg, ok := dsc.(protoreflect.MessageDescriptor); !ok {
if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: invalid response type: %s is %s, not a message", scope, dsc.FullName(), descriptorTypeWithArticle(dsc)); err != nil {
return err
}
} else {
typeName := "." + string(dsc.FullName())
if mtd.GetOutputType() != typeName {
mtd.OutputType = proto.String(typeName)
}
m.outputType = msg
}
return nil
}
func (r *result) resolveOptions(handler *reporter.Handler, elemType string, elemName protoreflect.FullName, opts []*descriptorpb.UninterpretedOption, scopes []scope, checkedCache []string) error {
mc := &internal.MessageContext{
File: r,
ElementName: string(elemName),
ElementType: elemType,
}
file := r.FileNode()
opts:
for _, opt := range opts {
// resolve any extension names found in option names
for _, nm := range opt.Name {
if nm.GetIsExtension() {
node := r.OptionNamePartNode(nm)
fqn, err := r.resolveExtensionName(nm.GetNamePart(), scopes, checkedCache)
if err != nil {
if err := handler.HandleErrorf(file.NodeInfo(node), "%v%v", mc, err); err != nil {
return err
}
continue opts
}
nm.NamePart = proto.String(fqn)
}
}
// also resolve any extension names found inside message literals in option values
mc.Option = opt
optVal := r.OptionNode(opt).GetValue()
if err := r.resolveOptionValue(handler, mc, optVal, scopes, checkedCache); err != nil {
return err
}
mc.Option = nil
}
return nil
}
func (r *result) resolveOptionValue(handler *reporter.Handler, mc *internal.MessageContext, val ast.ValueNode, scopes []scope, checkedCache []string) error {
optVal := val.Value()
switch optVal := optVal.(type) {
case []ast.ValueNode:
origPath := mc.OptAggPath
defer func() {
mc.OptAggPath = origPath
}()
for i, v := range optVal {
mc.OptAggPath = fmt.Sprintf("%s[%d]", origPath, i)
if err := r.resolveOptionValue(handler, mc, v, scopes, checkedCache); err != nil {
return err
}
}
case []*ast.MessageFieldNode:
origPath := mc.OptAggPath
defer func() {
mc.OptAggPath = origPath
}()
for _, fld := range optVal {
// check for extension name
if fld.Name.IsExtension() {
// Confusingly, an extension reference inside a message literal cannot refer to
// elements in the same enclosing message without a qualifier. Basically, we
// treat this as if there were no message scopes, so only the package name is
// used for resolving relative references. (Inconsistent protoc behavior, but
// likely due to how it re-uses C++ text format implementation, and normal text
// format doesn't expect that kind of relative reference.)
scopes := scopes[:1] // first scope is file, the rest are enclosing messages
fqn, err := r.resolveExtensionName(string(fld.Name.Name.AsIdentifier()), scopes, checkedCache)
if err != nil {
if err := handler.HandleErrorf(r.FileNode().NodeInfo(fld.Name.Name), "%v%v", mc, err); err != nil {
return err
}
} else {
r.optionQualifiedNames[fld.Name.Name] = fqn
}
}
// recurse into value
mc.OptAggPath = origPath
if origPath != "" {
mc.OptAggPath += "."
}
if fld.Name.IsExtension() {
mc.OptAggPath = fmt.Sprintf("%s[%s]", mc.OptAggPath, string(fld.Name.Name.AsIdentifier()))
} else {
mc.OptAggPath = fmt.Sprintf("%s%s", mc.OptAggPath, string(fld.Name.Name.AsIdentifier()))
}
if err := r.resolveOptionValue(handler, mc, fld.Val, scopes, checkedCache); err != nil {
return err
}
}
}
return nil
}
func (r *result) resolveExtensionName(name string, scopes []scope, checkedCache []string) (string, error) {
dsc := r.resolve(name, false, scopes, checkedCache)
if dsc == nil {
return "", fmt.Errorf("unknown extension %s", name)
}
if isSentinelDescriptor(dsc) {
return "", fmt.Errorf("unknown extension %s; resolved to %s which is not defined; consider using a leading dot", name, dsc.FullName())
}
if ext, ok := dsc.(protoreflect.FieldDescriptor); !ok {
return "", fmt.Errorf("invalid extension: %s is %s, not an extension", name, descriptorTypeWithArticle(dsc))
} else if !ext.IsExtension() {
return "", fmt.Errorf("invalid extension: %s is a field but not an extension", name)
}
return string("." + dsc.FullName()), nil
}
func (r *result) resolve(name string, onlyTypes bool, scopes []scope, checkedCache []string) protoreflect.Descriptor {
if strings.HasPrefix(name, ".") {
// already fully-qualified
return r.resolveElement(protoreflect.FullName(name[1:]), checkedCache)
}
// unqualified, so we look in the enclosing (last) scope first and move
// towards outermost (first) scope, trying to resolve the symbol
pos := strings.IndexByte(name, '.')
firstName := name
if pos > 0 {
firstName = name[:pos]
}
var bestGuess protoreflect.Descriptor
for i := len(scopes) - 1; i >= 0; i-- {
d := scopes[i](firstName, name)
if d != nil {
// In `protoc`, it will skip a match of the wrong type and move on
// to the next scope, but only if the reference is unqualified. So
// we mirror that behavior here. When we skip and move on, we go
// ahead and save the match of the wrong type so we can at least use
// it to construct a better error in the event that we don't find
// any match of the right type.
if !onlyTypes || isType(d) || firstName != name {
return d
}
if bestGuess == nil {
bestGuess = d
}
}
}
// we return best guess, even though it was not an allowed kind of
// descriptor, so caller can print a better error message (e.g.
// indicating that the name was found but that it's the wrong type)
return bestGuess
}
func isType(d protoreflect.Descriptor) bool {
switch d.(type) {
case protoreflect.MessageDescriptor, protoreflect.EnumDescriptor:
return true
}
return false
}
// scope represents a lexical scope in a proto file in which messages and enums
// can be declared.
type scope func(firstName, fullName string) protoreflect.Descriptor
func fileScope(r *result, checkedCache []string) scope {
// we search symbols in this file, but also symbols in other files that have
// the same package as this file or a "parent" package (in protobuf,
// packages are a hierarchy like C++ namespaces)
prefixes := internal.CreatePrefixList(r.FileDescriptorProto().GetPackage())
querySymbol := func(n string) protoreflect.Descriptor {
return r.resolveElement(protoreflect.FullName(n), checkedCache)
}
return func(firstName, fullName string) protoreflect.Descriptor {
for _, prefix := range prefixes {
var n1, n string
if prefix == "" {
// exhausted all prefixes, so it must be in this one
n1, n = fullName, fullName
} else {
n = prefix + "." + fullName
n1 = prefix + "." + firstName
}
d := resolveElementRelative(n1, n, querySymbol)
if d != nil {
return d
}
}
return nil
}
}
func messageScope(r *result, messageName protoreflect.FullName) scope {
querySymbol := func(n string) protoreflect.Descriptor {
return resolveElementInFile(protoreflect.FullName(n), r)
}
return func(firstName, fullName string) protoreflect.Descriptor {
n1 := string(messageName) + "." + firstName
n := string(messageName) + "." + fullName
return resolveElementRelative(n1, n, querySymbol)
}
}
func resolveElementRelative(firstName, fullName string, query func(name string) protoreflect.Descriptor) protoreflect.Descriptor {
d := query(firstName)
if d == nil {
return nil
}
if firstName == fullName {
return d
}
if !isAggregateDescriptor(d) {
// can't possibly find the rest of full name if
// the first name indicated a leaf descriptor
return nil
}
d = query(fullName)
if d == nil {
return newSentinelDescriptor(fullName)
}
return d
}
func resolveElementInFile(name protoreflect.FullName, f File) protoreflect.Descriptor {
d := f.FindDescriptorByName(name)
if d != nil {
return d
}
if matchesPkgNamespace(name, f.Package()) {
// this sentinel means the name is a valid namespace but
// does not refer to a descriptor
return newSentinelDescriptor(string(name))
}
return nil
}
func matchesPkgNamespace(fqn, pkg protoreflect.FullName) bool {
if pkg == "" {
return false
}
if fqn == pkg {
return true
}
if len(pkg) > len(fqn) && strings.HasPrefix(string(pkg), string(fqn)) {
// if char after fqn is a dot, then fqn is a namespace
if pkg[len(fqn)] == '.' {
return true
}
}
return false
}
func isAggregateDescriptor(d protoreflect.Descriptor) bool {
if isSentinelDescriptor(d) {
// this indicates the name matched a package, not a
// descriptor, but a package is an aggregate, so
// we return true
return true
}
switch d.(type) {
case protoreflect.MessageDescriptor, protoreflect.EnumDescriptor, protoreflect.ServiceDescriptor:
return true
default:
return false
}
}
func isSentinelDescriptor(d protoreflect.Descriptor) bool {
_, ok := d.(*sentinelDescriptor)
return ok
}
func newSentinelDescriptor(name string) protoreflect.Descriptor {
return &sentinelDescriptor{name: name}
}
// sentinelDescriptor is a placeholder descriptor. It is used instead of nil to
// distinguish between two situations:
// 1. The given name could not be found.
// 2. The given name *cannot* be a valid result so stop searching.
//
// In these cases, attempts to resolve an element name will return nil for the
// first case and will return a sentinelDescriptor in the second. The sentinel
// contains the fully-qualified name which caused the search to stop (which may
// be a prefix of the actual name being resolved).
type sentinelDescriptor struct {
protoreflect.Descriptor
name string
}
func (p *sentinelDescriptor) ParentFile() protoreflect.FileDescriptor {
return nil
}
func (p *sentinelDescriptor) Parent() protoreflect.Descriptor {
return nil
}
func (p *sentinelDescriptor) Index() int {
return 0
}
func (p *sentinelDescriptor) Syntax() protoreflect.Syntax {
return 0
}
func (p *sentinelDescriptor) Name() protoreflect.Name {
return protoreflect.Name(p.name)
}
func (p *sentinelDescriptor) FullName() protoreflect.FullName {
return protoreflect.FullName(p.name)
}
func (p *sentinelDescriptor) IsPlaceholder() bool {
return false
}
func (p *sentinelDescriptor) Options() protoreflect.ProtoMessage {
return nil
}
var _ protoreflect.Descriptor = (*sentinelDescriptor)(nil)
// 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 linker
import (
"strings"
"sync"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/protoutil"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/walk"
)
const unknownFilePath = "<unknown file>"
// Symbols is a symbol table that maps names for all program elements to their
// location in source. It also tracks extension tag numbers. This can be used
// to enforce uniqueness for symbol names and tag numbers across many files and
// many link operations.
//
// This type is thread-safe.
type Symbols struct {
pkgTrie packageSymbols
// We don't know the packages for these symbols, so we can't
// keep them in the pkgTrie. In vast majority of cases, this
// will always be empty/unused. When used, it ensures that
// multiple extension declarations don't refer to the same
// extension.
extDeclsMu sync.Mutex
extDecls map[protoreflect.FullName]extDecl
}
type packageSymbols struct {
mu sync.RWMutex
children map[protoreflect.FullName]*packageSymbols
files map[protoreflect.FileDescriptor]struct{}
symbols map[protoreflect.FullName]symbolEntry
exts map[extNumber]ast.SourceSpan
}
type extNumber struct {
extendee protoreflect.FullName
tag protoreflect.FieldNumber
}
type symbolEntry struct {
span ast.SourceSpan
isEnumValue bool
isPackage bool
}
type extDecl struct {
span ast.SourceSpan
extendee protoreflect.FullName
tag protoreflect.FieldNumber
}
// Import populates the symbol table with all symbols/elements and extension
// tags present in the given file descriptor. If s is nil or if fd has already
// been imported into s, this returns immediately without doing anything. If any
// collisions in symbol names or extension tags are identified, an error will be
// returned and the symbol table will not be updated.
func (s *Symbols) Import(fd protoreflect.FileDescriptor, handler *reporter.Handler) error {
if s == nil {
return nil
}
if f, ok := fd.(protoreflect.FileImport); ok {
// unwrap any import instance
fd = f.FileDescriptor
}
if f, ok := fd.(*file); ok {
// unwrap any file instance
fd = f.FileDescriptor
}
var pkgSpan ast.SourceSpan
if res, ok := fd.(*result); ok {
pkgSpan = packageNameSpan(res)
} else {
pkgSpan = sourceSpanForPackage(fd)
}
pkg, err := s.importPackages(pkgSpan, fd.Package(), handler)
if err != nil || pkg == nil {
return err
}
pkg.mu.RLock()
_, alreadyImported := pkg.files[fd]
pkg.mu.RUnlock()
if alreadyImported {
return nil
}
for i := range fd.Imports().Len() {
if err := s.Import(fd.Imports().Get(i).FileDescriptor, handler); err != nil {
return err
}
}
if res, ok := fd.(*result); ok && res.hasSource() {
return s.importResultWithExtensions(pkg, res, handler)
}
return s.importFileWithExtensions(pkg, fd, handler)
}
func (s *Symbols) importFileWithExtensions(pkg *packageSymbols, fd protoreflect.FileDescriptor, handler *reporter.Handler) error {
imported, err := pkg.importFile(fd, handler)
if err != nil {
return err
}
if !imported {
// nothing else to do
return nil
}
return walk.Descriptors(fd, func(d protoreflect.Descriptor) error {
fld, ok := d.(protoreflect.FieldDescriptor)
if !ok || !fld.IsExtension() {
return nil
}
span := sourceSpanForNumber(fld)
extendee := fld.ContainingMessage()
return s.AddExtension(packageFor(extendee), extendee.FullName(), fld.Number(), span, handler)
})
}
func (s *packageSymbols) importFile(fd protoreflect.FileDescriptor, handler *reporter.Handler) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.files[fd]; ok {
// have to double-check if it's already imported, in case
// it was added after above read-locked check
return false, nil
}
// first pass: check for conflicts
if err := s.checkFileLocked(fd, handler); err != nil {
return false, err
}
if err := handler.Error(); err != nil {
return false, err
}
// second pass: commit all symbols
s.commitFileLocked(fd)
return true, nil
}
func (s *Symbols) importPackages(pkgSpan ast.SourceSpan, pkg protoreflect.FullName, handler *reporter.Handler) (*packageSymbols, error) {
if pkg == "" {
return &s.pkgTrie, nil
}
cur := &s.pkgTrie
enumerator := nameEnumerator{name: pkg}
for {
p, ok := enumerator.next()
if !ok {
return cur, nil
}
var err error
cur, err = cur.importPackage(pkgSpan, p, handler)
if err != nil {
return nil, err
}
if cur == nil {
return nil, nil
}
}
}
func (s *packageSymbols) importPackage(pkgSpan ast.SourceSpan, pkg protoreflect.FullName, handler *reporter.Handler) (*packageSymbols, error) {
s.mu.RLock()
existing, ok := s.symbols[pkg]
var child *packageSymbols
if ok && existing.isPackage {
child = s.children[pkg]
}
s.mu.RUnlock()
if ok && existing.isPackage {
// package already exists
return child, nil
} else if ok {
return nil, reportSymbolCollision(pkgSpan, pkg, false, existing, handler)
}
s.mu.Lock()
defer s.mu.Unlock()
// have to double-check in case it was added while upgrading to write lock
existing, ok = s.symbols[pkg]
if ok && existing.isPackage {
// package already exists
return s.children[pkg], nil
} else if ok {
return nil, reportSymbolCollision(pkgSpan, pkg, false, existing, handler)
}
if s.symbols == nil {
s.symbols = map[protoreflect.FullName]symbolEntry{}
}
s.symbols[pkg] = symbolEntry{span: pkgSpan, isPackage: true}
child = &packageSymbols{}
if s.children == nil {
s.children = map[protoreflect.FullName]*packageSymbols{}
}
s.children[pkg] = child
return child, nil
}
func (s *Symbols) getPackage(pkg protoreflect.FullName, exact bool) *packageSymbols {
if pkg == "" {
return &s.pkgTrie
}
cur := &s.pkgTrie
enumerator := nameEnumerator{name: pkg}
for {
p, ok := enumerator.next()
if !ok {
return cur
}
cur.mu.RLock()
next := cur.children[p]
cur.mu.RUnlock()
if next == nil {
if exact {
return nil
}
return cur
}
cur = next
}
}
func reportSymbolCollision(span ast.SourceSpan, fqn protoreflect.FullName, additionIsEnumVal bool, existing symbolEntry, handler *reporter.Handler) error {
// because of weird scoping for enum values, provide more context in error message
// if this conflict is with an enum value
var isPkg, suffix string
if additionIsEnumVal || existing.isEnumValue {
suffix = "; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum"
}
if existing.isPackage {
isPkg = " as a package"
}
orig := existing.span
conflict := span
if posLess(conflict.Start(), orig.Start()) {
orig, conflict = conflict, orig
}
return handler.HandleErrorf(conflict, "symbol %q already defined%s at %v%s", fqn, isPkg, orig.Start(), suffix)
}
func posLess(a, b ast.SourcePos) bool {
if a.Filename == b.Filename {
if a.Line == b.Line {
return a.Col < b.Col
}
return a.Line < b.Line
}
return false
}
func (s *packageSymbols) checkFileLocked(f protoreflect.FileDescriptor, handler *reporter.Handler) error {
return walk.Descriptors(f, func(d protoreflect.Descriptor) error {
span := sourceSpanFor(d)
if existing, ok := s.symbols[d.FullName()]; ok {
_, isEnumVal := d.(protoreflect.EnumValueDescriptor)
if err := reportSymbolCollision(span, d.FullName(), isEnumVal, existing, handler); err != nil {
return err
}
}
return nil
})
}
func sourceSpanForPackage(fd protoreflect.FileDescriptor) ast.SourceSpan {
loc := fd.SourceLocations().ByPath([]int32{internal.FilePackageTag})
if internal.IsZeroLocation(loc) {
return ast.UnknownSpan(fd.Path())
}
return ast.NewSourceSpan(
ast.SourcePos{
Filename: fd.Path(),
Line: loc.StartLine,
Col: loc.StartColumn,
},
ast.SourcePos{
Filename: fd.Path(),
Line: loc.EndLine,
Col: loc.EndColumn,
},
)
}
func sourceSpanFor(d protoreflect.Descriptor) ast.SourceSpan {
file := d.ParentFile()
if file == nil {
return ast.UnknownSpan(unknownFilePath)
}
if result, ok := file.(*result); ok {
return nameSpan(result.FileNode(), result.Node(protoutil.ProtoFromDescriptor(d)))
}
path, ok := internal.ComputePath(d)
if !ok {
return ast.UnknownSpan(file.Path())
}
namePath := path
switch d.(type) {
case protoreflect.FieldDescriptor:
namePath = append(namePath, internal.FieldNameTag)
case protoreflect.MessageDescriptor:
namePath = append(namePath, internal.MessageNameTag)
case protoreflect.OneofDescriptor:
namePath = append(namePath, internal.OneofNameTag)
case protoreflect.EnumDescriptor:
namePath = append(namePath, internal.EnumNameTag)
case protoreflect.EnumValueDescriptor:
namePath = append(namePath, internal.EnumValNameTag)
case protoreflect.ServiceDescriptor:
namePath = append(namePath, internal.ServiceNameTag)
case protoreflect.MethodDescriptor:
namePath = append(namePath, internal.MethodNameTag)
default:
// NB: shouldn't really happen, but just in case fall back to path to
// descriptor, sans name field
}
loc := file.SourceLocations().ByPath(namePath)
if internal.IsZeroLocation(loc) {
loc = file.SourceLocations().ByPath(path)
if internal.IsZeroLocation(loc) {
return ast.UnknownSpan(file.Path())
}
}
return ast.NewSourceSpan(
ast.SourcePos{
Filename: file.Path(),
Line: loc.StartLine,
Col: loc.StartColumn,
},
ast.SourcePos{
Filename: file.Path(),
Line: loc.EndLine,
Col: loc.EndColumn,
},
)
}
func sourceSpanForNumber(fd protoreflect.FieldDescriptor) ast.SourceSpan {
file := fd.ParentFile()
if file == nil {
return ast.UnknownSpan(unknownFilePath)
}
path, ok := internal.ComputePath(fd)
if !ok {
return ast.UnknownSpan(file.Path())
}
numberPath := path
numberPath = append(numberPath, internal.FieldNumberTag)
loc := file.SourceLocations().ByPath(numberPath)
if internal.IsZeroLocation(loc) {
loc = file.SourceLocations().ByPath(path)
if internal.IsZeroLocation(loc) {
return ast.UnknownSpan(file.Path())
}
}
return ast.NewSourceSpan(
ast.SourcePos{
Filename: file.Path(),
Line: loc.StartLine,
Col: loc.StartColumn,
},
ast.SourcePos{
Filename: file.Path(),
Line: loc.EndLine,
Col: loc.EndColumn,
},
)
}
func (s *packageSymbols) commitFileLocked(f protoreflect.FileDescriptor) {
if s.symbols == nil {
s.symbols = map[protoreflect.FullName]symbolEntry{}
}
if s.exts == nil {
s.exts = map[extNumber]ast.SourceSpan{}
}
_ = walk.Descriptors(f, func(d protoreflect.Descriptor) error {
span := sourceSpanFor(d)
name := d.FullName()
_, isEnumValue := d.(protoreflect.EnumValueDescriptor)
s.symbols[name] = symbolEntry{span: span, isEnumValue: isEnumValue}
return nil
})
if s.files == nil {
s.files = map[protoreflect.FileDescriptor]struct{}{}
}
s.files[f] = struct{}{}
}
func (s *Symbols) importResultWithExtensions(pkg *packageSymbols, r *result, handler *reporter.Handler) error {
imported, err := pkg.importResult(r, handler)
if err != nil {
return err
}
if !imported {
// nothing else to do
return nil
}
return walk.Descriptors(r, func(d protoreflect.Descriptor) error {
fd, ok := d.(*extTypeDescriptor)
if !ok {
return nil
}
file := r.FileNode()
node := r.FieldNode(fd.FieldDescriptorProto())
info := file.NodeInfo(node.FieldTag())
extendee := fd.ContainingMessage()
return s.AddExtension(packageFor(extendee), extendee.FullName(), fd.Number(), info, handler)
})
}
func (s *Symbols) importResult(r *result, handler *reporter.Handler) error {
pkg, err := s.importPackages(packageNameSpan(r), r.Package(), handler)
if err != nil || pkg == nil {
return err
}
_, err = pkg.importResult(r, handler)
return err
}
func (s *packageSymbols) importResult(r *result, handler *reporter.Handler) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.files[r]; ok {
// already imported
return false, nil
}
// first pass: check for conflicts
if err := s.checkResultLocked(r, handler); err != nil {
return false, err
}
if err := handler.Error(); err != nil {
return false, err
}
// second pass: commit all symbols
s.commitFileLocked(r)
return true, nil
}
func (s *packageSymbols) checkResultLocked(r *result, handler *reporter.Handler) error {
resultSyms := map[protoreflect.FullName]symbolEntry{}
return walk.Descriptors(r, func(d protoreflect.Descriptor) error {
_, isEnumVal := d.(protoreflect.EnumValueDescriptor)
file := r.FileNode()
name := d.FullName()
node := r.Node(protoutil.ProtoFromDescriptor(d))
span := nameSpan(file, node)
// check symbols already in this symbol table
if existing, ok := s.symbols[name]; ok {
if err := reportSymbolCollision(span, name, isEnumVal, existing, handler); err != nil {
return err
}
}
// also check symbols from this result (that are not yet in symbol table)
if existing, ok := resultSyms[name]; ok {
if err := reportSymbolCollision(span, name, isEnumVal, existing, handler); err != nil {
return err
}
}
resultSyms[name] = symbolEntry{
span: span,
isEnumValue: isEnumVal,
}
return nil
})
}
func packageNameSpan(r *result) ast.SourceSpan {
if node, ok := r.FileNode().(*ast.FileNode); ok {
for _, decl := range node.Decls {
if pkgNode, ok := decl.(*ast.PackageNode); ok {
return r.FileNode().NodeInfo(pkgNode.Name)
}
}
}
return ast.UnknownSpan(r.Path())
}
func nameSpan(file ast.FileDeclNode, n ast.Node) ast.SourceSpan {
// TODO: maybe ast package needs a NamedNode interface to simplify this?
switch n := n.(type) {
case ast.FieldDeclNode:
return file.NodeInfo(n.FieldName())
case ast.MessageDeclNode:
return file.NodeInfo(n.MessageName())
case ast.OneofDeclNode:
return file.NodeInfo(n.OneofName())
case ast.EnumValueDeclNode:
return file.NodeInfo(n.GetName())
case *ast.EnumNode:
return file.NodeInfo(n.Name)
case *ast.ServiceNode:
return file.NodeInfo(n.Name)
case ast.RPCDeclNode:
return file.NodeInfo(n.GetName())
default:
return file.NodeInfo(n)
}
}
// AddExtension records the given extension, which is used to ensure that no two files
// attempt to extend the same message using the same tag. The given pkg should be the
// package that defines extendee.
func (s *Symbols) AddExtension(pkg, extendee protoreflect.FullName, tag protoreflect.FieldNumber, span ast.SourceSpan, handler *reporter.Handler) error {
if pkg != "" {
if !strings.HasPrefix(string(extendee), string(pkg)+".") {
return handler.HandleErrorf(span, "could not register extension: extendee %q does not match package %q", extendee, pkg)
}
}
pkgSyms := s.getPackage(pkg, true)
if pkgSyms == nil {
// should never happen
return handler.HandleErrorf(span, "could not register extension: missing package symbols for %q", pkg)
}
return pkgSyms.addExtension(extendee, tag, span, handler)
}
func (s *packageSymbols) addExtension(extendee protoreflect.FullName, tag protoreflect.FieldNumber, span ast.SourceSpan, handler *reporter.Handler) error {
s.mu.Lock()
defer s.mu.Unlock()
extNum := extNumber{extendee: extendee, tag: tag}
if existing, ok := s.exts[extNum]; ok {
return handler.HandleErrorf(span, "extension with tag %d for message %s already defined at %v", tag, extendee, existing.Start())
}
if s.exts == nil {
s.exts = map[extNumber]ast.SourceSpan{}
}
s.exts[extNum] = span
return nil
}
// AddExtensionDeclaration records the given extension declaration, which is used to
// ensure that no two declarations refer to the same extension.
func (s *Symbols) AddExtensionDeclaration(extension, extendee protoreflect.FullName, tag protoreflect.FieldNumber, span ast.SourceSpan, handler *reporter.Handler) error {
s.extDeclsMu.Lock()
defer s.extDeclsMu.Unlock()
existing, ok := s.extDecls[extension]
if ok {
if existing.extendee == extendee && existing.tag == tag {
// This is a declaration that has already been added. Ignore.
return nil
}
return handler.HandleErrorf(span, "extension %s already declared as extending %s with tag %d at %v", extension, existing.extendee, existing.tag, existing.span.Start())
}
if s.extDecls == nil {
s.extDecls = map[protoreflect.FullName]extDecl{}
}
s.extDecls[extension] = extDecl{
span: span,
extendee: extendee,
tag: tag,
}
return nil
}
// Lookup finds the registered location of the given name. If the given name has
// not been seen/registered, nil is returned.
func (s *Symbols) Lookup(name protoreflect.FullName) ast.SourceSpan {
// note: getPackage never returns nil when exact=false
pkgSyms := s.getPackage(name, false)
if entry, ok := pkgSyms.symbols[name]; ok {
return entry.span
}
return nil
}
// LookupExtension finds the registered location of the given extension. If the given
// extension has not been seen/registered, nil is returned.
func (s *Symbols) LookupExtension(messageName protoreflect.FullName, extensionNumber protoreflect.FieldNumber) ast.SourceSpan {
// note: getPackage never returns nil when exact=false
pkgSyms := s.getPackage(messageName, false)
return pkgSyms.exts[extNumber{messageName, extensionNumber}]
}
type nameEnumerator struct {
name protoreflect.FullName
start int
}
func (e *nameEnumerator) next() (protoreflect.FullName, bool) {
if e.start < 0 {
return "", false
}
pos := strings.IndexByte(string(e.name[e.start:]), '.')
if pos == -1 {
e.start = -1
return e.name, true
}
pos += e.start
e.start = pos + 1
return e.name[:pos], true
}
// 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 linker
import (
"fmt"
"math"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/cases"
"github.com/bufbuild/protocompile/protoutil"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/walk"
)
// ValidateOptions runs some validation checks on the result that can only
// be done after options are interpreted.
func (r *result) ValidateOptions(handler *reporter.Handler, symbols *Symbols) error {
if err := r.validateFile(handler); err != nil {
return err
}
return walk.Descriptors(r, func(d protoreflect.Descriptor) error {
switch d := d.(type) {
case protoreflect.FieldDescriptor:
if err := r.validateField(d, handler); err != nil {
return err
}
case protoreflect.MessageDescriptor:
if symbols == nil {
symbols = &Symbols{}
}
if err := r.validateMessage(d, handler, symbols); err != nil {
return err
}
case protoreflect.EnumDescriptor:
if err := r.validateEnum(d, handler); err != nil {
return err
}
}
return nil
})
}
func (r *result) validateFile(handler *reporter.Handler) error {
opts := r.FileDescriptorProto().GetOptions()
if opts.GetOptimizeFor() != descriptorpb.FileOptions_LITE_RUNTIME {
// Non-lite files may not import lite files.
imports := r.Imports()
for i, length := 0, imports.Len(); i < length; i++ {
dep := imports.Get(i)
depOpts, ok := dep.Options().(*descriptorpb.FileOptions)
if !ok {
continue // what else to do?
}
if depOpts.GetOptimizeFor() == descriptorpb.FileOptions_LITE_RUNTIME {
err := handler.HandleErrorf(r.getImportLocation(dep.Path()), "a file that does not use optimize_for=LITE_RUNTIME may not import file %q that does", dep.Path())
if err != nil {
return err
}
}
}
}
if isEditions(r) {
// Validate features
if opts.GetFeatures().GetFieldPresence() == descriptorpb.FeatureSet_LEGACY_REQUIRED {
span := r.findOptionSpan(r, internal.FileOptionsFeaturesTag, internal.FeatureSetFieldPresenceTag)
err := handler.HandleErrorf(span, "LEGACY_REQUIRED field presence cannot be set as the default for a file")
if err != nil {
return err
}
}
if opts != nil && opts.JavaStringCheckUtf8 != nil {
span := r.findOptionSpan(r, internal.FileOptionsJavaStringCheckUTF8Tag)
err := handler.HandleErrorf(span, `file option java_string_check_utf8 is not allowed with editions; import "google/protobuf/java_features.proto" and use (pb.java).utf8_validation instead`)
if err != nil {
return err
}
}
}
return nil
}
func (r *result) validateField(fld protoreflect.FieldDescriptor, handler *reporter.Handler) error {
if xtd, ok := fld.(protoreflect.ExtensionTypeDescriptor); ok {
fld = xtd.Descriptor()
}
fd, ok := fld.(*fldDescriptor)
if !ok {
// should not be possible
return fmt.Errorf("field descriptor is wrong type: expecting %T, got %T", (*fldDescriptor)(nil), fld)
}
if err := r.validatePacked(fd, handler); err != nil {
return err
}
if fd.Kind() == protoreflect.EnumKind {
requiresOpen := !fd.IsList() && !fd.HasPresence()
if requiresOpen && fd.Enum().IsClosed() {
// Fields in a proto3 message cannot refer to proto2 enums.
// In editions, this translates to implicit presence fields
// not being able to refer to closed enums.
// TODO: This really should be based solely on whether the enum's first
// value is zero, NOT based on if it's open vs closed.
// https://github.com/protocolbuffers/protobuf/issues/16249
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldType())
if err := handler.HandleErrorf(info, "cannot use closed enum %s in a field with implicit presence", fd.Enum().FullName()); err != nil {
return err
}
}
}
if fd.HasDefault() && !fd.HasPresence() {
span := r.findScalarOptionSpan(r.FieldNode(fd.proto), "default")
err := handler.HandleErrorf(span, "default value is not allowed on fields with implicit presence")
if err != nil {
return err
}
}
if fd.proto.Options != nil && fd.proto.Options.Ctype != nil {
if descriptorpb.Edition(r.Edition()) >= descriptorpb.Edition_EDITION_2024 {
// We don't support edition 2024 yet, but we went ahead and mimic'ed this check
// from protoc, which currently has experimental support for 2024.
span := r.findOptionSpan(fd, internal.FieldOptionsCTypeTag)
if err := handler.HandleErrorf(span, "ctype option cannot be used as of edition 2024; use features.string_type instead"); err != nil {
return err
}
}
}
if (fd.proto.Options.GetLazy() || fd.proto.Options.GetUnverifiedLazy()) && fd.Kind() != protoreflect.MessageKind {
var span ast.SourceSpan
var optionName string
if fd.proto.Options.GetLazy() {
span = r.findOptionSpan(fd, internal.FieldOptionsLazyTag)
optionName = "lazy"
} else {
span = r.findOptionSpan(fd, internal.FieldOptionsUnverifiedLazyTag)
optionName = "unverified_lazy"
}
var suffix string
if fd.Kind() == protoreflect.GroupKind {
if isEditions(r) {
suffix = " that use length-prefixed encoding"
} else {
suffix = ", not groups"
}
}
if err := handler.HandleErrorf(span, "%s option can only be used with message fields%s", optionName, suffix); err != nil {
return err
}
}
if fd.proto.Options.GetJstype() != descriptorpb.FieldOptions_JS_NORMAL {
switch fd.Kind() {
case protoreflect.Int64Kind, protoreflect.Uint64Kind, protoreflect.Sint64Kind,
protoreflect.Fixed64Kind, protoreflect.Sfixed64Kind:
// allowed only for 64-bit integer types
default:
span := r.findOptionSpan(fd, internal.FieldOptionsJSTypeTag)
err := handler.HandleErrorf(span, "only 64-bit integer fields (int64, uint64, sint64, fixed64, and sfixed64) can specify a jstype other than JS_NORMAL")
if err != nil {
return err
}
}
}
if isEditions(r) {
if err := r.validateFieldFeatures(fd, handler); err != nil {
return err
}
}
if fld.IsExtension() {
// More checks if this is an extension field.
if err := r.validateExtension(fd, handler); err != nil {
return err
}
}
return nil
}
func (r *result) validateExtension(fd *fldDescriptor, handler *reporter.Handler) error {
// NB: It's a little gross that we don't enforce these in validateBasic().
// But it requires linking to resolve the extendee, so we can interrogate
// its descriptor.
msg := fd.ContainingMessage()
if msg.Options().(*descriptorpb.MessageOptions).GetMessageSetWireFormat() { //nolint:errcheck
// Message set wire format requires that all extensions be messages
// themselves (no scalar extensions)
if fd.Kind() != protoreflect.MessageKind {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldType())
err := handler.HandleErrorf(info, "messages with message-set wire format cannot contain scalar extensions, only messages")
if err != nil {
return err
}
}
if fd.Cardinality() == protoreflect.Repeated {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldLabel())
err := handler.HandleErrorf(info, "messages with message-set wire format cannot contain repeated extensions, only optional")
if err != nil {
return err
}
}
} else if fd.Number() > internal.MaxNormalTag {
// In validateBasic() we just made sure these were within bounds for any message. But
// now that things are linked, we can check if the extendee is messageset wire format
// and, if not, enforce tighter limit.
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldTag())
err := handler.HandleErrorf(info, "tag number %d is higher than max allowed tag number (%d)", fd.Number(), internal.MaxNormalTag)
if err != nil {
return err
}
}
fileOpts := r.FileDescriptorProto().GetOptions()
if fileOpts.GetOptimizeFor() == descriptorpb.FileOptions_LITE_RUNTIME {
extendeeFileOpts, _ := msg.ParentFile().Options().(*descriptorpb.FileOptions)
if extendeeFileOpts.GetOptimizeFor() != descriptorpb.FileOptions_LITE_RUNTIME {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto))
err := handler.HandleErrorf(info, "extensions in a file that uses optimize_for=LITE_RUNTIME may not extend messages in file %q which does not", msg.ParentFile().Path())
if err != nil {
return err
}
}
}
// If the extendee uses extension declarations, make sure this extension matches.
md := protoutil.ProtoFromMessageDescriptor(msg)
for i, extRange := range md.ExtensionRange {
if int32(fd.Number()) < extRange.GetStart() || int32(fd.Number()) >= extRange.GetEnd() {
continue
}
extRangeOpts := extRange.GetOptions()
if extRangeOpts == nil {
break
}
if len(extRangeOpts.Declaration) == 0 && extRangeOpts.GetVerification() != descriptorpb.ExtensionRangeOptions_DECLARATION {
break
}
var found bool
for j, extDecl := range extRangeOpts.Declaration {
if extDecl.GetNumber() != int32(fd.Number()) {
continue
}
found = true
if extDecl.GetReserved() {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldTag())
span, _ := findExtensionRangeOptionSpan(msg.ParentFile(), msg, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(j), internal.ExtensionRangeOptionsDeclarationReservedTag)
err := handler.HandleErrorf(info, "cannot use field number %d for an extension because it is reserved in declaration at %v",
fd.Number(), span.Start())
if err != nil {
return err
}
break
}
if extDecl.GetFullName() != "."+string(fd.FullName()) {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldName())
span, _ := findExtensionRangeOptionSpan(msg.ParentFile(), msg, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(j), internal.ExtensionRangeOptionsDeclarationFullNameTag)
err := handler.HandleErrorf(info, "expected extension with number %d to be named %s, not %s, per declaration at %v",
fd.Number(), strings.TrimPrefix(extDecl.GetFullName(), "."), fd.FullName(), span.Start())
if err != nil {
return err
}
}
if extDecl.GetType() != getTypeName(fd) {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldType())
span, _ := findExtensionRangeOptionSpan(msg.ParentFile(), msg, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(j), internal.ExtensionRangeOptionsDeclarationTypeTag)
err := handler.HandleErrorf(info, "expected extension with number %d to have type %s, not %s, per declaration at %v",
fd.Number(), strings.TrimPrefix(extDecl.GetType(), "."), getTypeName(fd), span.Start())
if err != nil {
return err
}
}
if extDecl.GetRepeated() != (fd.Cardinality() == protoreflect.Repeated) {
expected, actual := "repeated", "optional"
if !extDecl.GetRepeated() {
expected, actual = actual, expected
}
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldLabel())
span, _ := findExtensionRangeOptionSpan(msg.ParentFile(), msg, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(j), internal.ExtensionRangeOptionsDeclarationRepeatedTag)
err := handler.HandleErrorf(info, "expected extension with number %d to be %s, not %s, per declaration at %v",
fd.Number(), expected, actual, span.Start())
if err != nil {
return err
}
}
break
}
if !found {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldTag())
span, _ := findExtensionRangeOptionSpan(fd.ParentFile(), msg, i, extRange,
internal.ExtensionRangeOptionsVerificationTag)
err := handler.HandleErrorf(info, "expected extension with number %d to be declared in type %s, but no declaration found at %v",
fd.Number(), fd.ContainingMessage().FullName(), span.Start())
if err != nil {
return err
}
}
}
return nil
}
func (r *result) validatePacked(fd *fldDescriptor, handler *reporter.Handler) error {
if fd.proto.Options != nil && fd.proto.Options.Packed != nil && isEditions(r) {
span := r.findOptionSpan(fd, internal.FieldOptionsPackedTag)
err := handler.HandleErrorf(span, "packed option cannot be used with editions; use features.repeated_field_encoding=PACKED instead")
if err != nil {
return err
}
}
if !fd.proto.GetOptions().GetPacked() {
// if packed isn't true, nothing to validate
return nil
}
if fd.proto.GetLabel() != descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldLabel())
err := handler.HandleErrorf(info, "packed option is only allowed on repeated fields")
if err != nil {
return err
}
}
switch fd.proto.GetType() {
case descriptorpb.FieldDescriptorProto_TYPE_STRING, descriptorpb.FieldDescriptorProto_TYPE_BYTES,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, descriptorpb.FieldDescriptorProto_TYPE_GROUP:
file := r.FileNode()
info := file.NodeInfo(r.FieldNode(fd.proto).FieldType())
err := handler.HandleErrorf(info, "packed option is only allowed on numeric, boolean, and enum fields")
if err != nil {
return err
}
}
return nil
}
func (r *result) validateFieldFeatures(fld *fldDescriptor, handler *reporter.Handler) error {
if msg, ok := fld.Parent().(*msgDescriptor); ok && msg.proto.GetOptions().GetMapEntry() {
// Skip validating features on fields of synthetic map entry messages.
// We blindly propagate them from the map field's features, but some may
// really only apply to the map field and not to a key or value entry field.
return nil
}
features := fld.proto.GetOptions().GetFeatures()
if features == nil {
// No features to validate.
return nil
}
if features.FieldPresence != nil {
switch {
case fld.proto.OneofIndex != nil:
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetFieldPresenceTag)
if err := handler.HandleErrorf(span, "oneof fields may not specify field presence"); err != nil {
return err
}
case fld.Cardinality() == protoreflect.Repeated:
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetFieldPresenceTag)
if err := handler.HandleErrorf(span, "repeated fields may not specify field presence"); err != nil {
return err
}
case fld.IsExtension():
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetFieldPresenceTag)
if err := handler.HandleErrorf(span, "extension fields may not specify field presence"); err != nil {
return err
}
case fld.Message() != nil && features.GetFieldPresence() == descriptorpb.FeatureSet_IMPLICIT:
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetFieldPresenceTag)
if err := handler.HandleErrorf(span, "message fields may not specify implicit presence"); err != nil {
return err
}
}
}
if features.RepeatedFieldEncoding != nil {
if fld.Cardinality() != protoreflect.Repeated {
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetRepeatedFieldEncodingTag)
if err := handler.HandleErrorf(span, "only repeated fields may specify repeated field encoding"); err != nil {
return err
}
} else if !internal.CanPack(fld.Kind()) && features.GetRepeatedFieldEncoding() == descriptorpb.FeatureSet_PACKED {
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetRepeatedFieldEncodingTag)
if err := handler.HandleErrorf(span, "only repeated primitive fields may specify packed encoding"); err != nil {
return err
}
}
}
if features.Utf8Validation != nil {
isMap := fld.IsMap()
if (!isMap && fld.Kind() != protoreflect.StringKind) ||
(isMap &&
fld.MapKey().Kind() != protoreflect.StringKind &&
fld.MapValue().Kind() != protoreflect.StringKind) {
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetUTF8ValidationTag)
if err := handler.HandleErrorf(span, "only string fields may specify UTF8 validation"); err != nil {
return err
}
}
}
if features.MessageEncoding != nil {
if fld.Message() == nil || fld.IsMap() {
span := r.findOptionSpan(fld, internal.FieldOptionsFeaturesTag, internal.FeatureSetMessageEncodingTag)
if err := handler.HandleErrorf(span, "only message fields may specify message encoding"); err != nil {
return err
}
}
}
return nil
}
func (r *result) validateMessage(d protoreflect.MessageDescriptor, handler *reporter.Handler, symbols *Symbols) error {
md, ok := d.(*msgDescriptor)
if !ok {
// should not be possible
return fmt.Errorf("message descriptor is wrong type: expecting %T, got %T", (*msgDescriptor)(nil), d)
}
if err := r.validateJSONNamesInMessage(md, handler); err != nil {
return err
}
return r.validateExtensionDeclarations(md, handler, symbols)
}
func (r *result) validateJSONNamesInMessage(md *msgDescriptor, handler *reporter.Handler) error {
if err := r.validateFieldJSONNames(md, false, handler); err != nil {
return err
}
if err := r.validateFieldJSONNames(md, true, handler); err != nil {
return err
}
return nil
}
func (r *result) validateEnum(d protoreflect.EnumDescriptor, handler *reporter.Handler) error {
ed, ok := d.(*enumDescriptor)
if !ok {
// should not be possible
return fmt.Errorf("enum descriptor is wrong type: expecting %T, got %T", (*enumDescriptor)(nil), d)
}
firstValue := ed.Values().Get(0)
if !ed.IsClosed() && firstValue.Number() != 0 {
// TODO: This check doesn't really belong here. Whether the
// first value is zero s/b orthogonal to whether the
// allowed values are open or closed.
// https://github.com/protocolbuffers/protobuf/issues/16249
file := r.FileNode()
evd, ok := firstValue.(*enValDescriptor)
if !ok {
// should not be possible
return fmt.Errorf("enum value descriptor is wrong type: expecting %T, got %T", (*enValDescriptor)(nil), firstValue)
}
info := file.NodeInfo(r.EnumValueNode(evd.proto).GetNumber())
if err := handler.HandleErrorf(info, "first value of open enum %s must have numeric value zero", ed.FullName()); err != nil {
return err
}
}
if err := r.validateJSONNamesInEnum(ed, handler); err != nil {
return err
}
return nil
}
func (r *result) validateJSONNamesInEnum(ed *enumDescriptor, handler *reporter.Handler) error {
seen := map[string]*descriptorpb.EnumValueDescriptorProto{}
for _, evd := range ed.proto.GetValue() {
scope := "enum value " + ed.proto.GetName() + "." + evd.GetName()
name := canonicalEnumValueName(evd.GetName(), ed.proto.GetName())
if existing, ok := seen[name]; ok && evd.GetNumber() != existing.GetNumber() {
fldNode := r.EnumValueNode(evd)
existingNode := r.EnumValueNode(existing)
conflictErr := fmt.Errorf("%s: camel-case name (with optional enum name prefix removed) %q conflicts with camel-case name of enum value %s, defined at %v",
scope, name, existing.GetName(), r.FileNode().NodeInfo(existingNode).Start())
// Since proto2 did not originally have a JSON format, we report conflicts as just warnings.
// With editions, not fully supporting JSON is allowed via feature: json_format == BEST_EFFORT
if !isJSONCompliant(ed) {
handler.HandleWarningWithPos(r.FileNode().NodeInfo(fldNode), conflictErr)
} else if err := handler.HandleErrorWithPos(r.FileNode().NodeInfo(fldNode), conflictErr); err != nil {
return err
}
} else {
seen[name] = evd
}
}
return nil
}
func (r *result) validateFieldJSONNames(md *msgDescriptor, useCustom bool, handler *reporter.Handler) error {
type jsonName struct {
source *descriptorpb.FieldDescriptorProto
// true if orig is a custom JSON name (vs. the field's default JSON name)
custom bool
}
seen := map[string]jsonName{}
for _, fd := range md.proto.GetField() {
scope := "field " + md.proto.GetName() + "." + fd.GetName()
defaultName := internal.JSONName(fd.GetName())
name := defaultName
custom := false
if useCustom {
n := fd.GetJsonName()
if n != defaultName || r.hasCustomJSONName(fd) {
name = n
custom = true
}
}
if existing, ok := seen[name]; ok {
// When useCustom is true, we'll only report an issue when a conflict is
// due to a custom name. That way, we don't double report conflicts on
// non-custom names.
if !useCustom || custom || existing.custom {
fldNode := r.FieldNode(fd)
customStr, srcCustomStr := "custom", "custom"
if !custom {
customStr = "default"
}
if !existing.custom {
srcCustomStr = "default"
}
info := r.FileNode().NodeInfo(fldNode)
conflictErr := reporter.Errorf(info, "%s: %s JSON name %q conflicts with %s JSON name of field %s, defined at %v",
scope, customStr, name, srcCustomStr, existing.source.GetName(), r.FileNode().NodeInfo(r.FieldNode(existing.source)).Start())
// Since proto2 did not originally have default JSON names, we report conflicts
// between default names (neither is a custom name) as just warnings.
// With editions, not fully supporting JSON is allowed via feature: json_format == BEST_EFFORT
if !isJSONCompliant(md) && !custom && !existing.custom {
handler.HandleWarning(conflictErr)
} else if err := handler.HandleError(conflictErr); err != nil {
return err
}
}
} else {
seen[name] = jsonName{source: fd, custom: custom}
}
}
return nil
}
func (r *result) validateExtensionDeclarations(md *msgDescriptor, handler *reporter.Handler, symbols *Symbols) error {
for i, extRange := range md.proto.ExtensionRange {
opts := extRange.GetOptions()
if len(opts.GetDeclaration()) == 0 {
// nothing to check
continue
}
// If any declarations are present, verification is assumed to be
// DECLARATION. It's an error for declarations to be present but the
// verification field explicitly set to something other than that.
if opts.Verification != nil && opts.GetVerification() != descriptorpb.ExtensionRangeOptions_DECLARATION {
span, ok := findExtensionRangeOptionSpan(r, md, i, extRange, internal.ExtensionRangeOptionsVerificationTag)
if !ok {
span, _ = findExtensionRangeOptionSpan(r, md, i, extRange, internal.ExtensionRangeOptionsDeclarationTag, 0)
}
if err := handler.HandleErrorf(span, "extension range cannot have declarations and have verification of %s", opts.GetVerification()); err != nil {
return err
}
}
declsByTag := map[int32]ast.SourcePos{}
for i, extDecl := range extRange.GetOptions().GetDeclaration() {
if extDecl.Number == nil {
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange, internal.ExtensionRangeOptionsDeclarationTag, int32(i))
if err := handler.HandleErrorf(span, "extension declaration is missing required field number"); err != nil {
return err
}
} else {
extensionNumberSpan, _ := findExtensionRangeOptionSpan(r, md, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(i), internal.ExtensionRangeOptionsDeclarationNumberTag)
if extDecl.GetNumber() < extRange.GetStart() || extDecl.GetNumber() >= extRange.GetEnd() {
// Number is out of range.
// See if one of the other ranges on the same extends statement includes the number,
// so we can provide a helpful message.
var suffix string
if extRange, ok := r.ExtensionsNode(extRange).(*ast.ExtensionRangeNode); ok {
for _, rng := range extRange.Ranges {
start, _ := rng.StartVal.AsInt64()
var end int64
switch {
case rng.Max != nil:
end = math.MaxInt64
case rng.EndVal != nil:
end, _ = rng.EndVal.AsInt64()
default:
end = start
}
if int64(extDecl.GetNumber()) >= start && int64(extDecl.GetNumber()) <= end {
// Found another range that matches
suffix = "; when using declarations, extends statements should indicate only a single span of field numbers"
break
}
}
}
err := handler.HandleErrorf(extensionNumberSpan, "extension declaration has number outside the range: %d not in [%d,%d]%s",
extDecl.GetNumber(), extRange.GetStart(), extRange.GetEnd()-1, suffix)
if err != nil {
return err
}
} else {
// Valid number; make sure it's not a duplicate
if existing, ok := declsByTag[extDecl.GetNumber()]; ok {
err := handler.HandleErrorf(extensionNumberSpan, "extension for tag number %d already declared at %v",
extDecl.GetNumber(), existing)
if err != nil {
return err
}
} else {
declsByTag[extDecl.GetNumber()] = extensionNumberSpan.Start()
}
}
}
if extDecl.FullName == nil && !extDecl.GetReserved() {
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange, internal.ExtensionRangeOptionsDeclarationTag, int32(i))
if err := handler.HandleErrorf(span, "extension declaration that is not marked reserved must have a full_name"); err != nil {
return err
}
} else if extDecl.FullName != nil {
var extensionFullName protoreflect.FullName
extensionNameSpan, _ := findExtensionRangeOptionSpan(r, md, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(i), internal.ExtensionRangeOptionsDeclarationFullNameTag)
if !strings.HasPrefix(extDecl.GetFullName(), ".") {
if err := handler.HandleErrorf(extensionNameSpan, "extension declaration full name %q should start with a leading dot (.)", extDecl.GetFullName()); err != nil {
return err
}
extensionFullName = protoreflect.FullName(extDecl.GetFullName())
} else {
extensionFullName = protoreflect.FullName(extDecl.GetFullName()[1:])
}
if !extensionFullName.IsValid() {
if err := handler.HandleErrorf(extensionNameSpan, "extension declaration full name %q is not a valid qualified name", extDecl.GetFullName()); err != nil {
return err
}
}
if err := symbols.AddExtensionDeclaration(extensionFullName, md.FullName(), protoreflect.FieldNumber(extDecl.GetNumber()), extensionNameSpan, handler); err != nil {
return err
}
}
if extDecl.Type == nil && !extDecl.GetReserved() {
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange, internal.ExtensionRangeOptionsDeclarationTag, int32(i))
if err := handler.HandleErrorf(span, "extension declaration that is not marked reserved must have a type"); err != nil {
return err
}
} else if extDecl.Type != nil {
if strings.HasPrefix(extDecl.GetType(), ".") {
if !protoreflect.FullName(extDecl.GetType()[1:]).IsValid() {
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(i), internal.ExtensionRangeOptionsDeclarationTypeTag)
if err := handler.HandleErrorf(span, "extension declaration type %q is not a valid qualified name", extDecl.GetType()); err != nil {
return err
}
}
} else if !isBuiltinTypeName(extDecl.GetType()) {
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(i), internal.ExtensionRangeOptionsDeclarationTypeTag)
if err := handler.HandleErrorf(span, "extension declaration type %q must be a builtin type or start with a leading dot (.)", extDecl.GetType()); err != nil {
return err
}
}
}
if extDecl.GetReserved() && (extDecl.FullName == nil) != (extDecl.Type == nil) {
var fieldTag int32
if extDecl.FullName != nil {
fieldTag = internal.ExtensionRangeOptionsDeclarationFullNameTag
} else {
fieldTag = internal.ExtensionRangeOptionsDeclarationTypeTag
}
span, _ := findExtensionRangeOptionSpan(r, md, i, extRange,
internal.ExtensionRangeOptionsDeclarationTag, int32(i), fieldTag)
if err := handler.HandleErrorf(span, "extension declarations that are reserved should specify both full_name and type or neither"); err != nil {
return err
}
}
}
}
return nil
}
func (r *result) hasCustomJSONName(fdProto *descriptorpb.FieldDescriptorProto) bool {
// if we have the AST, we can more precisely determine if there was a custom
// JSON named defined, even if it is explicitly configured to the same
// as the default JSON name for the field.
opts := r.FieldNode(fdProto).GetOptions()
if opts == nil {
return false
}
for _, opt := range opts.Options {
if len(opt.Name.Parts) == 1 &&
opt.Name.Parts[0].Name.AsIdentifier() == "json_name" &&
!opt.Name.Parts[0].IsExtension() {
return true
}
}
return false
}
func canonicalEnumValueName(enumValueName, enumName string) string {
suffix := internal.TrimPrefix(enumValueName, enumName)
return cases.Converter{Case: cases.Pascal, NaiveSplit: true}.Convert(suffix)
}
func isBuiltinTypeName(typeName string) bool {
switch typeName {
case "int32", "int64", "uint32", "uint64", "sint32", "sint64",
"fixed32", "fixed64", "sfixed32", "sfixed64",
"bool", "double", "float", "string", "bytes":
return true
default:
return false
}
}
func getTypeName(fd protoreflect.FieldDescriptor) string {
switch fd.Kind() {
case protoreflect.MessageKind, protoreflect.GroupKind:
return "." + string(fd.Message().FullName())
case protoreflect.EnumKind:
return "." + string(fd.Enum().FullName())
default:
return fd.Kind().String()
}
}
func findExtensionRangeOptionSpan(
file protoreflect.FileDescriptor,
extended protoreflect.MessageDescriptor,
extRangeIndex int,
extRange *descriptorpb.DescriptorProto_ExtensionRange,
path ...int32,
) (ast.SourceSpan, bool) {
// NB: Typically, we have an AST for a file and NOT source code info, because the
// compiler validates options before computing source code info. However, we might
// be validating an extension (whose source/AST we have), but whose extendee (and
// thus extension range options for declarations) could be in some other file, which
// could be provided to the compiler as an already-compiled descriptor. So this
// function can fallback to using source code info if an AST is not available.
if r, ok := file.(Result); ok && r.AST() != nil {
// Find the location using the AST, which will generally be higher fidelity
// than what we might find in a file descriptor's source code info.
exts := r.ExtensionsNode(extRange)
return findOptionSpan(r.FileNode(), exts, extRange.Options.ProtoReflect().Descriptor(), path...)
}
srcLocs := file.SourceLocations()
if srcLocs.Len() == 0 {
// no source code info, can't do any better than the filename. We
// return true as the boolean so the caller doesn't try again with
// an alternate path, since we won't be able to do any better.
return ast.UnknownSpan(file.Path()), true
}
msgPath, ok := internal.ComputePath(extended)
if !ok {
// Same as above: return true since no subsequent query can do better.
return ast.UnknownSpan(file.Path()), true
}
//nolint:gocritic // intentionally assigning to different slice variables
extRangePath := append(msgPath, internal.MessageExtensionRangesTag, int32(extRangeIndex))
optsPath := append(extRangePath, internal.ExtensionRangeOptionsTag) //nolint:gocritic
fullPath := append(optsPath, path...) //nolint:gocritic
srcLoc := srcLocs.ByPath(fullPath)
if srcLoc.Path != nil {
// found it
return asSpan(file.Path(), srcLoc), true
}
// Slow path to find closest match :/
// We look for longest matching path that is at least len(extRangePath)
// long. If we find a path that is longer (meaning a path that points INSIDE
// the request element), accept the first such location.
var bestMatch protoreflect.SourceLocation
var bestMatchPathLen int
for i, length := 0, srcLocs.Len(); i < length; i++ {
srcLoc := srcLocs.Get(i)
if len(srcLoc.Path) >= len(extRangePath) &&
isDescendantPath(fullPath, srcLoc.Path) &&
len(srcLoc.Path) > bestMatchPathLen {
bestMatch = srcLoc
bestMatchPathLen = len(srcLoc.Path)
} else if isDescendantPath(srcLoc.Path, path) {
return asSpan(file.Path(), srcLoc), false
}
}
if bestMatchPathLen > 0 {
return asSpan(file.Path(), bestMatch), false
}
return ast.UnknownSpan(file.Path()), false
}
func (r *result) findScalarOptionSpan(
root ast.NodeWithOptions,
name string,
) ast.SourceSpan {
match := ast.Node(root)
root.RangeOptions(func(n *ast.OptionNode) bool {
if len(n.Name.Parts) == 1 && !n.Name.Parts[0].IsExtension() &&
string(n.Name.Parts[0].Name.AsIdentifier()) == name {
match = n
return false
}
return true
})
return r.FileNode().NodeInfo(match)
}
func (r *result) findOptionSpan(
d protoutil.DescriptorProtoWrapper,
path ...int32,
) ast.SourceSpan {
node := r.Node(d.AsProto())
nodeWithOpts, ok := node.(ast.NodeWithOptions)
if !ok {
return r.FileNode().NodeInfo(node)
}
span, _ := findOptionSpan(r.FileNode(), nodeWithOpts, d.Options().ProtoReflect().Descriptor(), path...)
return span
}
func findOptionSpan(
file ast.FileDeclNode,
root ast.NodeWithOptions,
md protoreflect.MessageDescriptor,
path ...int32,
) (ast.SourceSpan, bool) {
bestMatch := ast.Node(root)
var bestMatchLen int
var repeatedIndices []int
root.RangeOptions(func(n *ast.OptionNode) bool {
desc := md
limit := len(n.Name.Parts)
if limit > len(path) {
limit = len(path)
}
var nextIsIndex bool
for i := range limit {
if desc == nil || nextIsIndex {
// Can't match anymore. Try next option.
return true
}
wantField := desc.Fields().ByNumber(protoreflect.FieldNumber(path[i]))
if wantField == nil {
// Should not be possible... next option won't fare any better since
// it's a disagreement between given path and given descriptor so bail.
return false
}
if n.Name.Parts[i].Open != nil ||
string(n.Name.Parts[i].Name.AsIdentifier()) != string(wantField.Name()) {
// This is an extension/custom option or indicates the wrong name.
// Try the next one.
return true
}
desc = wantField.Message()
nextIsIndex = wantField.Cardinality() == protoreflect.Repeated
}
// If we made it this far, we've matched everything so far.
if len(n.Name.Parts) >= len(path) {
// Either an exact match (if equal) or this option points *inside* the
// item we care about (if greater). Either way, the first such result
// is a keeper.
bestMatch = n.Name.Parts[len(path)-1]
bestMatchLen = len(n.Name.Parts)
return false
}
// We've got more path elements to try to match with the value.
match, matchLen := findMatchingValueNode(
desc,
path[len(n.Name.Parts):],
nextIsIndex,
0,
&repeatedIndices,
n,
n.Val)
if match != nil {
totalMatchLen := matchLen + len(n.Name.Parts)
if totalMatchLen > bestMatchLen {
bestMatch, bestMatchLen = match, totalMatchLen
}
}
return bestMatchLen != len(path) // no exact match, so keep looking
})
return file.NodeInfo(bestMatch), bestMatchLen == len(path)
}
func findMatchingValueNode(
md protoreflect.MessageDescriptor,
path protoreflect.SourcePath,
currIsRepeated bool,
repeatedCount int,
repeatedIndices *[]int,
node ast.Node,
val ast.ValueNode,
) (ast.Node, int) {
var matchLen int
var index int
if currIsRepeated {
// Compute the index of the current value (or, if an array literal, the
// index of the first value in the array).
if len(*repeatedIndices) > repeatedCount {
(*repeatedIndices)[repeatedCount]++
index = (*repeatedIndices)[repeatedCount]
} else {
*repeatedIndices = append(*repeatedIndices, 0)
index = 0
}
repeatedCount++
}
if arrayVal, ok := val.(*ast.ArrayLiteralNode); ok {
if !currIsRepeated {
// This should not happen.
return nil, 0
}
offset := int(path[0]) - index
if offset >= len(arrayVal.Elements) {
// The index we are looking for is not in this array.
return nil, 0
}
elem := arrayVal.Elements[offset]
// We've matched the index!
matchLen++
path = path[1:]
// Recurse into array element.
nextMatch, nextMatchLen := findMatchingValueNode(
md,
path,
false,
repeatedCount,
repeatedIndices,
elem,
elem,
)
return nextMatch, nextMatchLen + matchLen
}
if currIsRepeated {
if index != int(path[0]) {
// Not a match!
return nil, 0
}
// We've matched the index!
matchLen++
path = path[1:]
if len(path) == 0 {
// We're done matching!
return node, matchLen
}
}
msgValue, ok := val.(*ast.MessageLiteralNode)
if !ok {
// We can't go any further
return node, matchLen
}
var wantField protoreflect.FieldDescriptor
if md != nil {
wantField = md.Fields().ByNumber(protoreflect.FieldNumber(path[0]))
}
if wantField == nil {
// Should not be possible... next option won't fare any better since
// it's a disagreement between given path and given descriptor so bail.
return nil, 0
}
for _, field := range msgValue.Elements {
if field.Name.Open != nil ||
string(field.Name.Name.AsIdentifier()) != string(wantField.Name()) {
// This is an extension/custom option or indicates the wrong name.
// Try the next one.
continue
}
// We've matched this field.
matchLen++
path = path[1:]
if len(path) == 0 {
// Perfect match!
return field, matchLen
}
nextMatch, nextMatchLen := findMatchingValueNode(
wantField.Message(),
path,
wantField.Cardinality() == protoreflect.Repeated,
repeatedCount,
repeatedIndices,
field,
field.Val,
)
return nextMatch, nextMatchLen + matchLen
}
// If we didn't find the right field, just return what we have so far.
return node, matchLen
}
func isDescendantPath(descendant, ancestor protoreflect.SourcePath) bool {
if len(descendant) < len(ancestor) {
return false
}
for i := range ancestor {
if descendant[i] != ancestor[i] {
return false
}
}
return true
}
func asSpan(file string, srcLoc protoreflect.SourceLocation) ast.SourceSpan {
return ast.NewSourceSpan(
ast.SourcePos{
Filename: file,
Line: srcLoc.StartLine + 1,
Col: srcLoc.StartColumn + 1,
},
ast.SourcePos{
Filename: file,
Line: srcLoc.EndLine + 1,
Col: srcLoc.EndColumn + 1,
},
)
}
func (r *result) getImportLocation(path string) ast.SourceSpan {
node, ok := r.FileNode().(*ast.FileNode)
if !ok {
return ast.UnknownSpan(path)
}
for _, decl := range node.Decls {
imp, ok := decl.(*ast.ImportNode)
if !ok {
continue
}
if imp.Name.AsString() == path {
return node.NodeInfo(imp.Name)
}
}
// Couldn't find it? Should never happen...
return ast.UnknownSpan(path)
}
func isEditions(r *result) bool {
return descriptorpb.Edition(r.Edition()) >= descriptorpb.Edition_EDITION_2023
}
// 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 options contains the logic for interpreting options. The parse step
// of compilation stores the options in uninterpreted form, which contains raw
// identifiers and literal values.
//
// The process of interpreting an option is to resolve identifiers, by examining
// descriptors for the google.protobuf.*Options types and their available
// extensions (custom options). As field names are resolved, the values can be
// type-checked against the types indicated in field descriptors.
//
// On success, the various fields and extensions of the options message are
// populated and the field holding the uninterpreted form is cleared.
package options
import (
"bytes"
"errors"
"fmt"
"math"
"strings"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/messageset"
"github.com/bufbuild/protocompile/linker"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/sourceinfo"
)
type interpreter struct {
file file
resolver linker.Resolver
overrideDescriptorProto linker.File
index sourceinfo.OptionIndex
pathBuffer []int32
reporter *reporter.Handler
lenient bool
// lenienceEnabled is set to true when errors reported to reporter
// should be lenient
lenienceEnabled bool
lenientErrReported bool
}
type file interface {
parser.Result
ResolveMessageLiteralExtensionName(ast.IdentValueNode) string
}
type noResolveFile struct {
parser.Result
}
func (n noResolveFile) ResolveMessageLiteralExtensionName(ast.IdentValueNode) string {
return ""
}
// InterpreterOption is an option that can be passed to InterpretOptions and
// its variants.
type InterpreterOption func(*interpreter)
// WithOverrideDescriptorProto returns an option that indicates that the given file
// should be consulted when looking up a definition for an option type. The given
// file should usually have the path "google/protobuf/descriptor.proto". The given
// file will only be consulted if the option type is otherwise not visible to the
// file whose options are being interpreted.
func WithOverrideDescriptorProto(f linker.File) InterpreterOption {
return func(interp *interpreter) {
interp.overrideDescriptorProto = f
}
}
// InterpretOptions interprets options in the given linked result, returning
// an index that can be used to generate source code info. This step mutates
// the linked result's underlying proto to move option elements out of the
// "uninterpreted_option" fields and into proper option fields and extensions.
//
// The given handler is used to report errors and warnings. If any errors are
// reported, this function returns a non-nil error.
func InterpretOptions(linked linker.Result, handler *reporter.Handler, opts ...InterpreterOption) (sourceinfo.OptionIndex, error) {
return interpretOptions(false, linked, linker.ResolverFromFile(linked), handler, opts)
}
// InterpretOptionsLenient interprets options in a lenient/best-effort way in
// the given linked result, returning an index that can be used to generate
// source code info. This step mutates the linked result's underlying proto to
// move option elements out of the "uninterpreted_option" fields and into proper
// option fields and extensions.
//
// In lenient more, errors resolving option names and type errors are ignored.
// Any options that are uninterpretable (due to such errors) will remain in the
// "uninterpreted_option" fields.
func InterpretOptionsLenient(linked linker.Result, opts ...InterpreterOption) (sourceinfo.OptionIndex, error) {
return interpretOptions(true, linked, linker.ResolverFromFile(linked), reporter.NewHandler(nil), opts)
}
// InterpretUnlinkedOptions does a best-effort attempt to interpret options in
// the given parsed result, returning an index that can be used to generate
// source code info. This step mutates the parsed result's underlying proto to
// move option elements out of the "uninterpreted_option" fields and into proper
// option fields and extensions.
//
// This is the same as InterpretOptionsLenient except that it accepts an
// unlinked result. Because the file is unlinked, custom options cannot be
// interpreted. Other errors resolving option names or type errors will be
// effectively ignored. Any options that are uninterpretable (due to such
// errors) will remain in the "uninterpreted_option" fields.
func InterpretUnlinkedOptions(parsed parser.Result, opts ...InterpreterOption) (sourceinfo.OptionIndex, error) {
return interpretOptions(true, noResolveFile{parsed}, nil, reporter.NewHandler(nil), opts)
}
func interpretOptions(lenient bool, file file, res linker.Resolver, handler *reporter.Handler, interpOpts []InterpreterOption) (sourceinfo.OptionIndex, error) {
interp := &interpreter{
file: file,
resolver: res,
lenient: lenient,
reporter: handler,
index: sourceinfo.OptionIndex{},
pathBuffer: make([]int32, 0, 16),
}
for _, opt := range interpOpts {
opt(interp)
}
// We have to do this in two phases. First we interpret non-custom options.
// This allows us to handle standard options and features that may needed to
// correctly reference the custom options in the second phase.
if err := interp.interpretFileOptions(file, false); err != nil {
return nil, err
}
// Now we can do custom options.
if err := interp.interpretFileOptions(file, true); err != nil {
return nil, err
}
return interp.index, nil
}
func (interp *interpreter) handleErrorf(span ast.SourceSpan, msg string, args ...any) error {
if interp.lenienceEnabled {
interp.lenientErrReported = true
return nil
}
return interp.reporter.HandleErrorf(span, msg, args...)
}
func (interp *interpreter) handleErrorWithPos(span ast.SourceSpan, err error) error {
if interp.lenienceEnabled {
interp.lenientErrReported = true
return nil
}
return interp.reporter.HandleErrorWithPos(span, err)
}
func (interp *interpreter) handleError(err error) error {
if interp.lenienceEnabled {
interp.lenientErrReported = true
return nil
}
return interp.reporter.HandleError(err)
}
func (interp *interpreter) interpretFileOptions(file file, customOpts bool) error {
fd := file.FileDescriptorProto()
prefix := fd.GetPackage()
if prefix != "" {
prefix += "."
}
err := interpretElementOptions(interp, fd.GetName(), targetTypeFile, fd, customOpts)
if err != nil {
return err
}
for _, md := range fd.GetMessageType() {
fqn := prefix + md.GetName()
if err := interp.interpretMessageOptions(fqn, md, customOpts); err != nil {
return err
}
}
for _, fld := range fd.GetExtension() {
fqn := prefix + fld.GetName()
if err := interp.interpretFieldOptions(fqn, fld, customOpts); err != nil {
return err
}
}
for _, ed := range fd.GetEnumType() {
fqn := prefix + ed.GetName()
if err := interp.interpretEnumOptions(fqn, ed, customOpts); err != nil {
return err
}
}
for _, sd := range fd.GetService() {
fqn := prefix + sd.GetName()
err := interpretElementOptions(interp, fqn, targetTypeService, sd, customOpts)
if err != nil {
return err
}
for _, mtd := range sd.GetMethod() {
mtdFqn := fqn + "." + mtd.GetName()
err := interpretElementOptions(interp, mtdFqn, targetTypeMethod, mtd, customOpts)
if err != nil {
return err
}
}
}
return nil
}
func resolveDescriptor[T protoreflect.Descriptor](res linker.Resolver, name string) T {
var zero T
if res == nil {
return zero
}
if len(name) > 0 && name[0] == '.' {
name = name[1:]
}
desc, _ := res.FindDescriptorByName(protoreflect.FullName(name))
typedDesc, ok := desc.(T)
if ok {
return typedDesc
}
return zero
}
func (interp *interpreter) resolveExtensionType(name string) (protoreflect.ExtensionTypeDescriptor, error) {
if interp.resolver == nil {
return nil, protoregistry.NotFound
}
if len(name) > 0 && name[0] == '.' {
name = name[1:]
}
ext, err := interp.resolver.FindExtensionByName(protoreflect.FullName(name))
if err != nil {
return nil, err
}
return ext.TypeDescriptor(), nil
}
func (interp *interpreter) resolveOptionsType(name string) protoreflect.MessageDescriptor {
md := resolveDescriptor[protoreflect.MessageDescriptor](interp.resolver, name)
if md != nil {
return md
}
if interp.overrideDescriptorProto == nil {
return nil
}
if len(name) > 0 && name[0] == '.' {
name = name[1:]
}
desc := interp.overrideDescriptorProto.FindDescriptorByName(protoreflect.FullName(name))
if md, ok := desc.(protoreflect.MessageDescriptor); ok {
return md
}
return nil
}
func (interp *interpreter) nodeInfo(n ast.Node) ast.NodeInfo {
return interp.file.FileNode().NodeInfo(n)
}
func (interp *interpreter) interpretMessageOptions(fqn string, md *descriptorpb.DescriptorProto, customOpts bool) error {
err := interpretElementOptions(interp, fqn, targetTypeMessage, md, customOpts)
if err != nil {
return err
}
for _, fld := range md.GetField() {
fldFqn := fqn + "." + fld.GetName()
if err := interp.interpretFieldOptions(fldFqn, fld, customOpts); err != nil {
return err
}
}
for _, ood := range md.GetOneofDecl() {
oodFqn := fqn + "." + ood.GetName()
err := interpretElementOptions(interp, oodFqn, targetTypeOneof, ood, customOpts)
if err != nil {
return err
}
}
for _, fld := range md.GetExtension() {
fldFqn := fqn + "." + fld.GetName()
if err := interp.interpretFieldOptions(fldFqn, fld, customOpts); err != nil {
return err
}
}
for _, er := range md.GetExtensionRange() {
erFqn := fmt.Sprintf("%s.%d-%d", fqn, er.GetStart(), er.GetEnd())
err := interpretElementOptions(interp, erFqn, targetTypeExtensionRange, er, customOpts)
if err != nil {
return err
}
}
for _, nmd := range md.GetNestedType() {
nmdFqn := fqn + "." + nmd.GetName()
if err := interp.interpretMessageOptions(nmdFqn, nmd, customOpts); err != nil {
return err
}
}
for _, ed := range md.GetEnumType() {
edFqn := fqn + "." + ed.GetName()
if err := interp.interpretEnumOptions(edFqn, ed, customOpts); err != nil {
return err
}
}
// We also copy features for map fields down to their synthesized key and value fields.
for _, fld := range md.GetField() {
entryName := internal.MapEntry(fld.GetName())
if fld.GetLabel() != descriptorpb.FieldDescriptorProto_LABEL_REPEATED ||
fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE &&
fld.GetTypeName() != "."+fqn+"."+entryName {
// can't be a map field
continue
}
if fld.Options == nil || fld.Options.Features == nil {
// no features to propagate
continue
}
for _, nmd := range md.GetNestedType() {
if nmd.GetName() == entryName {
// found the entry message
if !nmd.GetOptions().GetMapEntry() {
break // not a map
}
for _, mapField := range nmd.Field {
if mapField.Options == nil {
mapField.Options = &descriptorpb.FieldOptions{}
}
features := proto.Clone(fld.Options.Features).(*descriptorpb.FeatureSet) //nolint:errcheck
if mapField.Options.Features != nil {
proto.Merge(features, mapField.Options.Features)
}
mapField.Options.Features = features
}
break
}
}
}
return nil
}
var emptyFieldOptions = &descriptorpb.FieldOptions{}
func (interp *interpreter) interpretFieldOptions(fqn string, fld *descriptorpb.FieldDescriptorProto, customOpts bool) error {
opts := fld.GetOptions()
emptyOptionsAlreadyPresent := opts != nil && len(opts.GetUninterpretedOption()) == 0
// For non-custom phase, first process pseudo-options
if len(opts.GetUninterpretedOption()) > 0 && !customOpts {
interp.enableLenience(true)
err := interp.interpretFieldPseudoOptions(fqn, fld, opts)
interp.enableLenience(false)
if err != nil {
return err
}
}
// Must re-check length of uninterpreted options since above step could remove some.
if len(opts.GetUninterpretedOption()) == 0 {
// If the message has no other interpreted options, we clear it out. But don't
// do that if the descriptor came in with empty options or if it already has
// interpreted option fields.
if opts != nil && !emptyOptionsAlreadyPresent && proto.Equal(fld.Options, emptyFieldOptions) {
fld.Options = nil
}
return nil
}
// Then process actual options.
return interpretElementOptions(interp, fqn, targetTypeField, fld, customOpts)
}
func (interp *interpreter) interpretFieldPseudoOptions(fqn string, fld *descriptorpb.FieldDescriptorProto, opts *descriptorpb.FieldOptions) error {
scope := "field " + fqn
uo := opts.UninterpretedOption
// process json_name pseudo-option
if index, err := internal.FindOption(interp.file, interp.handleErrorf, scope, uo, "json_name"); err != nil {
return err
} else if index >= 0 {
opt := uo[index]
optNode := interp.file.OptionNode(opt)
if opt.StringValue == nil {
return interp.handleErrorf(interp.nodeInfo(optNode.GetValue()), "%s: expecting string value for json_name option", scope)
}
jsonName := string(opt.StringValue)
// Extensions don't support custom json_name values.
// If the value is already set (via the descriptor) and doesn't match the default value, return an error.
if fld.GetExtendee() != "" && jsonName != "" && jsonName != internal.JSONName(fld.GetName()) {
return interp.handleErrorf(interp.nodeInfo(optNode.GetName()), "%s: option json_name is not allowed on extensions", scope)
}
// attribute source code info
if on, ok := optNode.(*ast.OptionNode); ok {
interp.index[on] = &sourceinfo.OptionSourceInfo{Path: []int32{-1, internal.FieldJSONNameTag}}
}
uo = internal.RemoveOption(uo, index)
if strings.HasPrefix(jsonName, "[") && strings.HasSuffix(jsonName, "]") {
return interp.handleErrorf(interp.nodeInfo(optNode.GetValue()), "%s: option json_name value cannot start with '[' and end with ']'; that is reserved for representing extensions", scope)
}
fld.JsonName = proto.String(jsonName)
}
// and process default pseudo-option
if index, err := interp.processDefaultOption(scope, fqn, fld, uo); err != nil {
return err
} else if index >= 0 {
// attribute source code info
optNode := interp.file.OptionNode(uo[index])
if on, ok := optNode.(*ast.OptionNode); ok {
interp.index[on] = &sourceinfo.OptionSourceInfo{Path: []int32{-1, internal.FieldDefaultTag}}
}
uo = internal.RemoveOption(uo, index)
}
opts.UninterpretedOption = uo
return nil
}
func (interp *interpreter) processDefaultOption(scope string, fqn string, fld *descriptorpb.FieldDescriptorProto, uos []*descriptorpb.UninterpretedOption) (defaultIndex int, err error) {
found, err := internal.FindOption(interp.file, interp.handleErrorf, scope, uos, "default")
if err != nil || found == -1 {
return -1, err
}
opt := uos[found]
optNode := interp.file.OptionNode(opt)
if fld.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
return -1, interp.handleErrorf(interp.nodeInfo(optNode.GetName()), "%s: default value cannot be set because field is repeated", scope)
}
if fld.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP || fld.GetType() == descriptorpb.FieldDescriptorProto_TYPE_MESSAGE {
return -1, interp.handleErrorf(interp.nodeInfo(optNode.GetName()), "%s: default value cannot be set because field is a message", scope)
}
mc := &internal.MessageContext{
File: interp.file,
ElementName: fqn,
ElementType: descriptorType(fld),
Option: opt,
}
val := optNode.GetValue()
var v any
if val.Value() == nil {
// no value in the AST, so we dig the value out of the uninterpreted option proto
v, err = interp.defaultValueFromProto(mc, fld, opt, val)
} else {
// compute value from AST
v, err = interp.defaultValue(mc, fld, val)
}
if err != nil {
return -1, interp.handleError(err)
}
if str, ok := v.(string); ok {
fld.DefaultValue = proto.String(str)
} else if b, ok := v.([]byte); ok {
fld.DefaultValue = proto.String(encodeDefaultBytes(b))
} else {
var flt float64
var ok bool
if flt, ok = v.(float64); !ok {
var flt32 float32
if flt32, ok = v.(float32); ok {
flt = float64(flt32)
}
}
if ok {
switch {
case math.IsInf(flt, 1):
fld.DefaultValue = proto.String("inf")
case math.IsInf(flt, -1):
fld.DefaultValue = proto.String("-inf")
case math.IsNaN(flt):
fld.DefaultValue = proto.String("nan")
default:
fld.DefaultValue = proto.String(fmt.Sprintf("%v", v))
}
} else {
fld.DefaultValue = proto.String(fmt.Sprintf("%v", v))
}
}
return found, nil
}
func (interp *interpreter) defaultValue(mc *internal.MessageContext, fld *descriptorpb.FieldDescriptorProto, val ast.ValueNode) (any, error) {
if _, ok := val.(*ast.MessageLiteralNode); ok {
return -1, reporter.Errorf(interp.nodeInfo(val), "%vdefault value cannot be a message", mc)
}
if fld.GetType() == descriptorpb.FieldDescriptorProto_TYPE_ENUM {
ed := resolveDescriptor[protoreflect.EnumDescriptor](interp.resolver, fld.GetTypeName())
if ed == nil {
return -1, reporter.Errorf(interp.nodeInfo(val), "%vunable to resolve enum type %q for field %q", mc, fld.GetTypeName(), fld.GetName())
}
_, name, err := interp.enumFieldValue(mc, ed, val, false)
if err != nil {
return -1, err
}
return string(name), nil
}
return interp.scalarFieldValue(mc, fld.GetType(), val, false)
}
func (interp *interpreter) defaultValueFromProto(mc *internal.MessageContext, fld *descriptorpb.FieldDescriptorProto, opt *descriptorpb.UninterpretedOption, node ast.Node) (any, error) {
if opt.AggregateValue != nil {
return -1, reporter.Errorf(interp.nodeInfo(node), "%vdefault value cannot be a message", mc)
}
if fld.GetType() == descriptorpb.FieldDescriptorProto_TYPE_ENUM {
ed := resolveDescriptor[protoreflect.EnumDescriptor](interp.resolver, fld.GetTypeName())
if ed == nil {
return -1, reporter.Errorf(interp.nodeInfo(node), "%vunable to resolve enum type %q for field %q", mc, fld.GetTypeName(), fld.GetName())
}
_, name, err := interp.enumFieldValueFromProto(mc, ed, opt, node)
if err != nil {
return nil, err
}
return string(name), nil
}
return interp.scalarFieldValueFromProto(mc, fld.GetType(), opt, node)
}
func encodeDefaultBytes(b []byte) string {
var buf bytes.Buffer
internal.WriteEscapedBytes(&buf, b)
return buf.String()
}
func (interp *interpreter) interpretEnumOptions(fqn string, ed *descriptorpb.EnumDescriptorProto, customOpts bool) error {
err := interpretElementOptions(interp, fqn, targetTypeEnum, ed, customOpts)
if err != nil {
return err
}
for _, evd := range ed.GetValue() {
evdFqn := fqn + "." + evd.GetName()
err := interpretElementOptions(interp, evdFqn, targetTypeEnumValue, evd, customOpts)
if err != nil {
return err
}
}
return nil
}
func interpretElementOptions[Elem elementType[OptsStruct, Opts], OptsStruct any, Opts optionsType[OptsStruct]](
interp *interpreter,
fqn string,
target *targetType[Elem, OptsStruct, Opts],
elem Elem,
customOpts bool,
) error {
opts := elem.GetOptions()
uninterpreted := opts.GetUninterpretedOption()
if len(uninterpreted) > 0 {
remain, err := interp.interpretOptions(fqn, target.t, elem, opts, uninterpreted, customOpts)
if err != nil {
return err
}
target.setUninterpretedOptions(opts, remain)
} else if customOpts {
// If customOpts is true, we are in second pass of interpreting.
// For second pass, even if there are no options to interpret, we still
// need to verify feature usage.
features := opts.GetFeatures()
var msg protoreflect.Message
if len(features.ProtoReflect().GetUnknown()) > 0 {
// We need to first convert to a message that uses the sources' definition
// of FeatureSet.
optsDesc := opts.ProtoReflect().Descriptor()
optsFqn := string(optsDesc.FullName())
if md := interp.resolveOptionsType(optsFqn); md != nil {
dm := dynamicpb.NewMessage(md)
if err := cloneInto(dm, opts, interp.resolver); err != nil {
node := interp.file.Node(elem)
return interp.handleError(reporter.Error(interp.nodeInfo(node), err))
}
msg = dm
}
}
if msg == nil {
msg = opts.ProtoReflect()
}
err := interp.validateRecursive(false, msg, "", elem, nil, false, false, false)
if err != nil {
return err
}
}
return nil
}
// interpretOptions processes the options in uninterpreted, which are interpreted as fields
// of the given opts message. The first return value is the features to use for child elements.
// On success, the latter two return values will usually be nil, nil. But if the current
// operation is lenient, it may return a non-nil slice of uninterpreted options on success.
// In such a case, the returned slice contains the options which could not be interpreted.
func (interp *interpreter) interpretOptions(
fqn string,
targetType descriptorpb.FieldOptions_OptionTargetType,
element, opts proto.Message,
uninterpreted []*descriptorpb.UninterpretedOption,
customOpts bool,
) ([]*descriptorpb.UninterpretedOption, error) {
optsDesc := opts.ProtoReflect().Descriptor()
optsFqn := string(optsDesc.FullName())
var msg protoreflect.Message
// see if the parse included an override copy for these options
if md := interp.resolveOptionsType(optsFqn); md != nil {
dm := dynamicpb.NewMessage(md)
if err := cloneInto(dm, opts, interp.resolver); err != nil {
node := interp.file.Node(element)
return nil, interp.handleError(reporter.Error(interp.nodeInfo(node), err))
}
msg = dm
} else {
msg = proto.Clone(opts).ProtoReflect()
}
mc := &internal.MessageContext{
File: interp.file,
ElementName: fqn,
ElementType: descriptorType(element),
}
var remain []*descriptorpb.UninterpretedOption
for _, uo := range uninterpreted {
isCustom := uo.Name[0].GetIsExtension()
if isCustom != customOpts {
// We're not looking at these this phase.
remain = append(remain, uo)
continue
}
firstName := uo.Name[0].GetNamePart()
if targetType == descriptorpb.FieldOptions_TARGET_TYPE_FIELD &&
!isCustom && (firstName == "default" || firstName == "json_name") {
// Field pseudo-option that we can skip and is handled elsewhere.
remain = append(remain, uo)
continue
}
node := interp.file.OptionNode(uo)
if !isCustom && firstName == "uninterpreted_option" {
if interp.lenient {
remain = append(remain, uo)
continue
}
// uninterpreted_option might be found reflectively, but is not actually valid for use
if err := interp.handleErrorf(interp.nodeInfo(node.GetName()), "%vinvalid option 'uninterpreted_option'", mc); err != nil {
return nil, err
}
}
mc.Option = uo
interp.enableLenience(true)
srcInfo, err := interp.interpretField(targetType, mc, msg, uo, 0, interp.pathBuffer)
interp.enableLenience(false)
if err != nil {
return nil, err
}
if interp.lenientErrReported {
remain = append(remain, uo)
continue
}
if srcInfo != nil {
if optn, ok := node.(*ast.OptionNode); ok {
interp.index[optn] = srcInfo
}
}
}
// customOpts is true for the second pass, which is also when we want to validate feature usage.
doValidation := customOpts
if doValidation {
validateRequiredFields := !interp.lenient
err := interp.validateRecursive(validateRequiredFields, msg, "", element, nil, false, false, false)
if err != nil {
return nil, err
}
}
if interp.lenient {
// If we're lenient, then we don't want to clobber the passed in message
// and leave it partially populated. So we convert into a copy first
optsClone := opts.ProtoReflect().New().Interface()
if err := cloneInto(optsClone, msg.Interface(), interp.resolver); err != nil {
// TODO: do this in a more granular way, so we can convert individual
// fields and leave bad ones uninterpreted instead of skipping all of
// the work we've done so far.
return uninterpreted, nil
}
if doValidation {
if err := proto.CheckInitialized(optsClone); err != nil {
// Conversion from dynamic message failed to set some required fields.
// TODO above applies here as well...
return uninterpreted, nil
}
}
// conversion from dynamic message above worked, so now
// it is safe to overwrite the passed in message
proto.Reset(opts)
proto.Merge(opts, optsClone)
return remain, nil
}
// now try to convert into the passed in message and fail if not successful
if err := cloneInto(opts, msg.Interface(), interp.resolver); err != nil {
node := interp.file.Node(element)
return nil, interp.handleError(reporter.Error(interp.nodeInfo(node), err))
}
return remain, nil
}
// checkFieldUsage verifies that the given option field can be used
// for the given target type. It reports an error if not and returns
// a non-nil error if the handler returned a non-nil error.
func (interp *interpreter) checkFieldUsage(
targetType descriptorpb.FieldOptions_OptionTargetType,
fld protoreflect.FieldDescriptor,
node ast.Node,
) error {
msgOpts, _ := fld.ContainingMessage().Options().(*descriptorpb.MessageOptions)
if msgOpts.GetMessageSetWireFormat() && !messageset.CanSupportMessageSets() {
err := interp.handleErrorf(interp.nodeInfo(node), "field %q may not be used in an option: it uses 'message set wire format' legacy proto1 feature which is not supported", fld.FullName())
if err != nil {
return err
}
}
opts, ok := fld.Options().(*descriptorpb.FieldOptions)
if !ok {
return nil
}
targetTypes := opts.GetTargets()
if len(targetTypes) == 0 {
return nil
}
for _, allowedType := range targetTypes {
if allowedType == targetType {
return nil
}
}
allowedTypes := make([]string, len(targetTypes))
for i, t := range targetTypes {
allowedTypes[i] = targetTypeString(t)
}
if len(targetTypes) == 1 && targetTypes[0] == descriptorpb.FieldOptions_TARGET_TYPE_UNKNOWN {
return interp.handleErrorf(interp.nodeInfo(node), "field %q may not be used in an option (it declares no allowed target types)", fld.FullName())
}
return interp.handleErrorf(interp.nodeInfo(node), "field %q is allowed on [%s], not on %s", fld.FullName(), strings.Join(allowedTypes, ","), targetTypeString(targetType))
}
func targetTypeString(t descriptorpb.FieldOptions_OptionTargetType) string {
return strings.ToLower(strings.ReplaceAll(strings.TrimPrefix(t.String(), "TARGET_TYPE_"), "_", " "))
}
func editionString(t descriptorpb.Edition) string {
return strings.ToLower(strings.ReplaceAll(strings.TrimPrefix(t.String(), "EDITION_"), "_", "-"))
}
func cloneInto(dest proto.Message, src proto.Message, res linker.Resolver) error {
if dest.ProtoReflect().Descriptor() == src.ProtoReflect().Descriptor() {
proto.Reset(dest)
proto.Merge(dest, src)
return nil
}
// If descriptors are not the same, we could have field descriptors in src that
// don't match the ones in dest. There's no easy/sane way to handle that. So we
// just marshal to bytes and back to do this
marshaler := proto.MarshalOptions{
// We've already validated required fields before this point,
// so we can allow partial here.
AllowPartial: true,
}
data, err := marshaler.Marshal(src)
if err != nil {
return err
}
unmarshaler := proto.UnmarshalOptions{AllowPartial: true}
if res != nil {
unmarshaler.Resolver = res
} else {
// Use a typed nil, which returns "not found" to all queries
// and prevents fallback to protoregistry.GlobalTypes.
unmarshaler.Resolver = (*protoregistry.Types)(nil)
}
return unmarshaler.Unmarshal(data, dest)
}
func (interp *interpreter) validateRecursive(
validateRequiredFields bool,
msg protoreflect.Message,
prefix string,
element proto.Message,
path []int32,
isFeatures bool,
inFeatures bool,
inMap bool,
) error {
if validateRequiredFields {
flds := msg.Descriptor().Fields()
var missingFields []string
for i := range flds.Len() {
fld := flds.Get(i)
if fld.Cardinality() == protoreflect.Required && !msg.Has(fld) {
missingFields = append(missingFields, fmt.Sprintf("%s%s", prefix, fld.Name()))
}
}
if len(missingFields) > 0 {
node := interp.findOptionNode(path, element)
err := interp.handleErrorf(interp.nodeInfo(node), "error in %s options: some required fields missing: %v", descriptorType(element), strings.Join(missingFields, ", "))
if err != nil {
return err
}
}
}
var err error
msg.Range(func(fld protoreflect.FieldDescriptor, val protoreflect.Value) bool {
chpath := path
if !inMap {
chpath = append(chpath, int32(fld.Number()))
}
chInFeatures := isFeatures || inFeatures
chIsFeatures := !chInFeatures && len(path) == 0 && fld.Name() == "features"
if (isFeatures || (inFeatures && fld.IsExtension())) &&
interp.file.FileNode().Name() == fld.ParentFile().Path() {
var what, name string
if fld.IsExtension() {
what = "custom feature"
name = "(" + string(fld.FullName()) + ")"
} else {
what = "feature"
name = string(fld.Name())
}
node := interp.findOptionNode(path, element)
err = interp.handleErrorf(interp.nodeInfo(node), "%s %s cannot be used from the same file in which it is defined", what, name)
if err != nil {
return false
}
}
if chInFeatures {
// Validate feature usage against feature settings.
// First, check the feature support settings of the field.
opts, _ := fld.Options().(*descriptorpb.FieldOptions)
edition := interp.file.FileDescriptorProto().GetEdition()
if opts != nil && opts.FeatureSupport != nil {
err = interp.validateFeatureSupport(edition, opts.FeatureSupport, "field", string(fld.FullName()), chpath, element)
if err != nil {
return false
}
}
// Then, if it's an enum or has an enum, check the feature support settings of the enum values.
var enum protoreflect.EnumDescriptor
if fld.Enum() != nil {
enum = fld.Enum()
} else if fld.IsMap() && fld.MapValue().Enum() != nil {
enum = fld.MapValue().Enum()
}
if enum != nil {
switch {
case fld.IsMap():
val.Map().Range(func(_ protoreflect.MapKey, v protoreflect.Value) bool {
// Can't construct path to particular map entry since we don't this entry's index.
// So we leave chpath alone, and it will have to point to the whole map value (or
// the first entry if the map is de-structured across multiple option statements).
err = interp.validateEnumValueFeatureSupport(edition, enum, v.Enum(), chpath, element)
return err == nil
})
if err != nil {
return false
}
case fld.IsList():
sl := val.List()
for i := range sl.Len() {
v := sl.Get(i)
err = interp.validateEnumValueFeatureSupport(edition, enum, v.Enum(), append(chpath, int32(i)), element)
if err != nil {
return false
}
}
default:
err = interp.validateEnumValueFeatureSupport(edition, enum, val.Enum(), chpath, element)
if err != nil {
return false
}
}
}
}
// If it's a message or contains a message, recursively validate fields in those messages.
switch {
case fld.IsMap() && fld.MapValue().Message() != nil:
val.Map().Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
chprefix := fmt.Sprintf("%s%s[%v].", prefix, fieldName(fld), k)
err = interp.validateRecursive(validateRequiredFields, v.Message(), chprefix, element, chpath, chIsFeatures, chInFeatures, true)
return err == nil
})
if err != nil {
return false
}
case fld.IsList() && fld.Message() != nil:
sl := val.List()
for i := range sl.Len() {
v := sl.Get(i)
chprefix := fmt.Sprintf("%s%s[%d].", prefix, fieldName(fld), i)
if !inMap {
chpath = append(chpath, int32(i))
}
err = interp.validateRecursive(validateRequiredFields, v.Message(), chprefix, element, chpath, chIsFeatures, chInFeatures, inMap)
if err != nil {
return false
}
}
case !fld.IsMap() && fld.Message() != nil:
chprefix := fmt.Sprintf("%s%s.", prefix, fieldName(fld))
err = interp.validateRecursive(validateRequiredFields, val.Message(), chprefix, element, chpath, chIsFeatures, chInFeatures, inMap)
if err != nil {
return false
}
}
return true
})
return err
}
func (interp *interpreter) validateEnumValueFeatureSupport(
edition descriptorpb.Edition,
enum protoreflect.EnumDescriptor,
number protoreflect.EnumNumber,
path []int32,
element proto.Message,
) error {
enumVal := enum.Values().ByNumber(number)
if enumVal == nil {
return nil
}
enumValOpts, _ := enumVal.Options().(*descriptorpb.EnumValueOptions)
if enumValOpts == nil || enumValOpts.FeatureSupport == nil {
return nil
}
return interp.validateFeatureSupport(edition, enumValOpts.FeatureSupport, "enum value", string(enumVal.Name()), path, element)
}
func (interp *interpreter) validateFeatureSupport(
edition descriptorpb.Edition,
featureSupport *descriptorpb.FieldOptions_FeatureSupport,
what string,
name string,
path []int32,
element proto.Message,
) error {
if featureSupport.EditionIntroduced != nil && edition < featureSupport.GetEditionIntroduced() {
node := interp.findOptionNode(path, element)
err := interp.handleErrorf(interp.nodeInfo(node), "%s %q was not introduced until edition %s", what, name, editionString(featureSupport.GetEditionIntroduced()))
if err != nil {
return err
}
}
if featureSupport.EditionRemoved != nil && edition >= featureSupport.GetEditionRemoved() {
node := interp.findOptionNode(path, element)
err := interp.handleErrorf(interp.nodeInfo(node), "%s %q was removed in edition %s", what, name, editionString(featureSupport.GetEditionRemoved()))
if err != nil {
return err
}
}
if featureSupport.EditionDeprecated != nil && edition >= featureSupport.GetEditionDeprecated() {
node := interp.findOptionNode(path, element)
var suffix string
if featureSupport.GetDeprecationWarning() != "" {
suffix = ": " + featureSupport.GetDeprecationWarning()
}
interp.reporter.HandleWarningf(interp.nodeInfo(node), "%s %q is deprecated as of edition %s%s", what, name, editionString(featureSupport.GetEditionDeprecated()), suffix)
}
return nil
}
func (interp *interpreter) findOptionNode(
path []int32,
element proto.Message,
) ast.Node {
elementNode := interp.file.Node(element)
nodeWithOpts, _ := elementNode.(ast.NodeWithOptions)
if nodeWithOpts == nil {
return elementNode
}
node, _ := findOptionNode[*ast.OptionNode](
path,
optionsRanger{nodeWithOpts},
func(n *ast.OptionNode) *sourceinfo.OptionSourceInfo {
return interp.index[n]
},
)
if node != nil {
return node
}
return elementNode
}
func findOptionNode[N ast.Node](
path []int32,
nodes interface {
Range(func(N, ast.ValueNode) bool)
},
srcInfoAccessor func(N) *sourceinfo.OptionSourceInfo,
) (ast.Node, int) {
var bestMatch ast.Node
var bestMatchLen int
nodes.Range(func(node N, val ast.ValueNode) bool {
srcInfo := srcInfoAccessor(node)
if srcInfo == nil {
// can happen if we are lenient when interpreting -- this node
// could not be interpreted and thus has no source info; skip
return true
}
if srcInfo.Path[0] < 0 {
// negative first value means it's a field pseudo-option; skip
return true
}
match, matchLen := findOptionValueNode(path, node, val, srcInfo)
if matchLen > bestMatchLen {
bestMatch = match
bestMatchLen = matchLen
if matchLen >= len(path) {
// not going to find a better one
return false
}
}
return true
})
return bestMatch, bestMatchLen
}
type optionsRanger struct {
node ast.NodeWithOptions
}
func (r optionsRanger) Range(f func(*ast.OptionNode, ast.ValueNode) bool) {
r.node.RangeOptions(func(optNode *ast.OptionNode) bool {
return f(optNode, optNode.Val)
})
}
type valueRanger []ast.ValueNode
func (r valueRanger) Range(f func(ast.ValueNode, ast.ValueNode) bool) {
for _, elem := range r {
if !f(elem, elem) {
return
}
}
}
type fieldRanger map[*ast.MessageFieldNode]*sourceinfo.OptionSourceInfo
func (r fieldRanger) Range(f func(*ast.MessageFieldNode, ast.ValueNode) bool) {
for elem := range r {
if !f(elem, elem.Val) {
return
}
}
}
func isPathMatch(a, b []int32) bool {
length := len(a)
if len(b) < length {
length = len(b)
}
for i := range length {
if a[i] != b[i] {
return false
}
}
return true
}
func findOptionValueNode(
path []int32,
node ast.Node,
value ast.ValueNode,
srcInfo *sourceinfo.OptionSourceInfo,
) (ast.Node, int) {
srcInfoPath := srcInfo.Path
if _, ok := srcInfo.Children.(*sourceinfo.ArrayLiteralSourceInfo); ok {
// Last path element for array source info is the index of the
// first element. So exclude in the comparison, since path could
// indicate a later index, which is present in the array.
srcInfoPath = srcInfo.Path[:len(srcInfo.Path)-1]
}
if !isPathMatch(path, srcInfoPath) {
return nil, 0
}
if len(srcInfoPath) >= len(path) {
return node, len(path)
}
switch children := srcInfo.Children.(type) {
case *sourceinfo.ArrayLiteralSourceInfo:
array, ok := value.(*ast.ArrayLiteralNode)
if !ok {
break // should never happen
}
var i int
match, matchLen := findOptionNode[ast.ValueNode](
path,
valueRanger(array.Elements),
func(_ ast.ValueNode) *sourceinfo.OptionSourceInfo {
val := &children.Elements[i]
i++
return val
},
)
if match != nil {
return match, matchLen
}
case *sourceinfo.MessageLiteralSourceInfo:
match, matchLen := findOptionNode[*ast.MessageFieldNode](
path,
fieldRanger(children.Fields),
func(n *ast.MessageFieldNode) *sourceinfo.OptionSourceInfo {
return children.Fields[n]
},
)
if match != nil {
return match, matchLen
}
}
return node, len(srcInfoPath)
}
// interpretField interprets the option described by opt, as a field inside the given msg. This
// interprets components of the option name starting at nameIndex. When nameIndex == 0, then
// msg must be an options message. For nameIndex > 0, msg is a nested message inside of the
// options message. The given pathPrefix is the path (sequence of field numbers and indices
// with a FileDescriptorProto as the start) up to but not including the given nameIndex.
//
// Any errors encountered will be handled, so the returned error will only be non-nil if
// the handler returned non-nil. Callers must check that the source info is non-nil before
// using it since it can be nil (in the event of a problem) even if the error is nil.
func (interp *interpreter) interpretField(
targetType descriptorpb.FieldOptions_OptionTargetType,
mc *internal.MessageContext,
msg protoreflect.Message,
opt *descriptorpb.UninterpretedOption,
nameIndex int,
pathPrefix []int32,
) (*sourceinfo.OptionSourceInfo, error) {
var fld protoreflect.FieldDescriptor
nm := opt.GetName()[nameIndex]
node := interp.file.OptionNamePartNode(nm)
if nm.GetIsExtension() {
extName := nm.GetNamePart()
if extName[0] == '.' {
extName = extName[1:] /* skip leading dot */
}
var err error
fld, err = interp.resolveExtensionType(extName)
if errors.Is(err, protoregistry.NotFound) {
return nil, interp.handleErrorf(interp.nodeInfo(node),
"%vunrecognized extension %s of %s",
mc, extName, msg.Descriptor().FullName())
} else if err != nil {
return nil, interp.handleErrorWithPos(interp.nodeInfo(node), err)
}
if fld.ContainingMessage().FullName() != msg.Descriptor().FullName() {
return nil, interp.handleErrorf(interp.nodeInfo(node),
"%vextension %s should extend %s but instead extends %s",
mc, extName, msg.Descriptor().FullName(), fld.ContainingMessage().FullName())
}
} else {
fld = msg.Descriptor().Fields().ByName(protoreflect.Name(nm.GetNamePart()))
if fld == nil {
return nil, interp.handleErrorf(interp.nodeInfo(node),
"%vfield %s of %s does not exist",
mc, nm.GetNamePart(), msg.Descriptor().FullName())
}
}
pathPrefix = append(pathPrefix, int32(fld.Number()))
if err := interp.checkFieldUsage(targetType, fld, node); err != nil {
return nil, err
}
if len(opt.GetName()) > nameIndex+1 {
nextnm := opt.GetName()[nameIndex+1]
nextnode := interp.file.OptionNamePartNode(nextnm)
k := fld.Kind()
if k != protoreflect.MessageKind && k != protoreflect.GroupKind {
return nil, interp.handleErrorf(interp.nodeInfo(nextnode),
"%vcannot set field %s because %s is not a message",
mc, nextnm.GetNamePart(), nm.GetNamePart())
}
if fld.Cardinality() == protoreflect.Repeated {
return nil, interp.handleErrorf(interp.nodeInfo(nextnode),
"%vcannot set field %s because %s is repeated (must use an aggregate)",
mc, nextnm.GetNamePart(), nm.GetNamePart())
}
var fdm protoreflect.Message
if msg.Has(fld) {
v := msg.Mutable(fld)
fdm = v.Message()
} else {
if ood := fld.ContainingOneof(); ood != nil {
existingFld := msg.WhichOneof(ood)
if existingFld != nil && existingFld.Number() != fld.Number() {
return nil, interp.handleErrorf(interp.nodeInfo(node),
"%voneof %q already has field %q set",
mc, ood.Name(), fieldName(existingFld))
}
}
fldVal := msg.NewField(fld)
fdm = fldVal.Message()
msg.Set(fld, fldVal)
}
// recurse to set next part of name
return interp.interpretField(targetType, mc, fdm, opt, nameIndex+1, pathPrefix)
}
optNode := interp.file.OptionNode(opt)
optValNode := optNode.GetValue()
var srcInfo *sourceinfo.OptionSourceInfo
var err error
if optValNode.Value() == nil {
err = interp.setOptionFieldFromProto(targetType, mc, msg, fld, node, opt, optValNode)
srcInfoVal := newSrcInfo(pathPrefix, nil)
srcInfo = &srcInfoVal
} else {
srcInfo, err = interp.setOptionField(targetType, mc, msg, fld, node, optValNode, false, pathPrefix)
}
if err != nil {
return nil, err
}
return srcInfo, nil
}
// setOptionField sets the value for field fld in the given message msg to the value represented
// by AST node val. The given name is the AST node that corresponds to the name of fld. On success,
// it returns additional metadata about the field that was set.
func (interp *interpreter) setOptionField(
targetType descriptorpb.FieldOptions_OptionTargetType,
mc *internal.MessageContext,
msg protoreflect.Message,
fld protoreflect.FieldDescriptor,
name ast.Node,
val ast.ValueNode,
insideMsgLiteral bool,
pathPrefix []int32,
) (*sourceinfo.OptionSourceInfo, error) {
v := val.Value()
if sl, ok := v.([]ast.ValueNode); ok {
// handle slices a little differently than the others
if fld.Cardinality() != protoreflect.Repeated {
return nil, interp.handleErrorf(interp.nodeInfo(val), "%vvalue is an array but field is not repeated", mc)
}
origPath := mc.OptAggPath
defer func() {
mc.OptAggPath = origPath
}()
childVals := make([]sourceinfo.OptionSourceInfo, len(sl))
var firstIndex int
if fld.IsMap() {
firstIndex = msg.Get(fld).Map().Len()
} else {
firstIndex = msg.Get(fld).List().Len()
}
for index, item := range sl {
mc.OptAggPath = fmt.Sprintf("%s[%d]", origPath, index)
value, srcInfo, err := interp.fieldValue(targetType, mc, msg, fld, item, insideMsgLiteral, append(pathPrefix, int32(firstIndex+index)))
if err != nil || !value.IsValid() {
return nil, err
}
if fld.IsMap() {
mv := msg.Mutable(fld).Map()
setMapEntry(fld, msg, mv, value.Message())
} else {
lv := msg.Mutable(fld).List()
lv.Append(value)
}
childVals[index] = srcInfo
}
srcInfo := newSrcInfo(append(pathPrefix, int32(firstIndex)), &sourceinfo.ArrayLiteralSourceInfo{Elements: childVals})
return &srcInfo, nil
}
if fld.IsMap() {
pathPrefix = append(pathPrefix, int32(msg.Get(fld).Map().Len()))
} else if fld.IsList() {
pathPrefix = append(pathPrefix, int32(msg.Get(fld).List().Len()))
}
value, srcInfo, err := interp.fieldValue(targetType, mc, msg, fld, val, insideMsgLiteral, pathPrefix)
if err != nil || !value.IsValid() {
return nil, err
}
if ood := fld.ContainingOneof(); ood != nil {
existingFld := msg.WhichOneof(ood)
if existingFld != nil && existingFld.Number() != fld.Number() {
return nil, interp.handleErrorf(interp.nodeInfo(name), "%voneof %q already has field %q set", mc, ood.Name(), fieldName(existingFld))
}
}
switch {
case fld.IsMap():
mv := msg.Mutable(fld).Map()
setMapEntry(fld, msg, mv, value.Message())
case fld.IsList():
lv := msg.Mutable(fld).List()
lv.Append(value)
default:
if msg.Has(fld) {
return nil, interp.handleErrorf(interp.nodeInfo(name), "%vnon-repeated option field %s already set", mc, fieldName(fld))
}
msg.Set(fld, value)
}
return &srcInfo, nil
}
// setOptionFieldFromProto sets the value for field fld in the given message msg to the value
// represented by the given uninterpreted option. The given ast.Node, if non-nil, will be used
// to report source positions in error messages. On success, it returns additional metadata
// about the field that was set.
func (interp *interpreter) setOptionFieldFromProto(
targetType descriptorpb.FieldOptions_OptionTargetType,
mc *internal.MessageContext,
msg protoreflect.Message,
fld protoreflect.FieldDescriptor,
name ast.Node,
opt *descriptorpb.UninterpretedOption,
node ast.Node,
) error {
k := fld.Kind()
var value protoreflect.Value
switch k {
case protoreflect.EnumKind:
num, _, err := interp.enumFieldValueFromProto(mc, fld.Enum(), opt, node)
if err != nil {
return interp.handleError(err)
}
value = protoreflect.ValueOfEnum(num)
case protoreflect.MessageKind, protoreflect.GroupKind:
if opt.AggregateValue == nil {
return interp.handleErrorf(interp.nodeInfo(node), "%vexpecting message, got %s", mc, optionValueKind(opt))
}
// We must parse the text format from the aggregate value string
var elem protoreflect.Message
switch {
case fld.IsMap():
elem = dynamicpb.NewMessage(fld.Message())
case fld.IsList():
elem = msg.Get(fld).List().NewElement().Message()
default:
elem = msg.NewField(fld).Message()
}
err := prototext.UnmarshalOptions{
Resolver: &msgLiteralResolver{interp: interp, pkg: fld.ParentFile().Package()},
AllowPartial: true,
}.Unmarshal([]byte(opt.GetAggregateValue()), elem.Interface())
if err != nil {
return interp.handleErrorf(interp.nodeInfo(node), "%vfailed to parse message literal %w", mc, err)
}
if err := interp.checkFieldUsagesInMessage(targetType, elem, node); err != nil {
return err
}
value = protoreflect.ValueOfMessage(elem)
default:
v, err := interp.scalarFieldValueFromProto(mc, descriptorpb.FieldDescriptorProto_Type(k), opt, node)
if err != nil {
return interp.handleError(err)
}
value = protoreflect.ValueOf(v)
}
if ood := fld.ContainingOneof(); ood != nil {
existingFld := msg.WhichOneof(ood)
if existingFld != nil && existingFld.Number() != fld.Number() {
return interp.handleErrorf(interp.nodeInfo(name), "%voneof %q already has field %q set", mc, ood.Name(), fieldName(existingFld))
}
}
switch {
case fld.IsMap():
mv := msg.Mutable(fld).Map()
setMapEntry(fld, msg, mv, value.Message())
case fld.IsList():
msg.Mutable(fld).List().Append(value)
default:
if msg.Has(fld) {
return interp.handleErrorf(interp.nodeInfo(name), "%vnon-repeated option field %s already set", mc, fieldName(fld))
}
msg.Set(fld, value)
}
return nil
}
// checkFieldUsagesInMessage verifies that all fields present in the given
// message can be used for the given target type. When an AST is
// present, we validate each field as it is processed. But without
// an AST, we unmarshal a message from an uninterpreted option's
// aggregate value string, and then must make sure that all fields
// set in that message are valid. This reports an error for each
// invalid field it encounters and returns a non-nil error if/when
// the handler returns a non-nil error.
func (interp *interpreter) checkFieldUsagesInMessage(
targetType descriptorpb.FieldOptions_OptionTargetType,
msg protoreflect.Message,
node ast.Node,
) error {
var err error
msg.Range(func(fld protoreflect.FieldDescriptor, val protoreflect.Value) bool {
err = interp.checkFieldUsage(targetType, fld, node)
if err != nil {
return false
}
switch {
case fld.IsList() && fld.Message() != nil:
listVal := val.List()
for i, length := 0, listVal.Len(); i < length; i++ {
err = interp.checkFieldUsagesInMessage(targetType, listVal.Get(i).Message(), node)
if err != nil {
return false
}
}
case fld.IsMap() && fld.MapValue().Message() != nil:
mapVal := val.Map()
mapVal.Range(func(_ protoreflect.MapKey, val protoreflect.Value) bool {
err = interp.checkFieldUsagesInMessage(targetType, val.Message(), node)
return err == nil
})
case !fld.IsMap() && fld.Message() != nil:
err = interp.checkFieldUsagesInMessage(targetType, val.Message(), node)
}
return err == nil
})
return err
}
func (interp *interpreter) enableLenience(enable bool) {
if !interp.lenient {
return // nothing to do
}
if enable {
// reset the flag that tracks if an error has been reported
interp.lenientErrReported = false
}
interp.lenienceEnabled = enable
}
func setMapEntry(
fld protoreflect.FieldDescriptor,
msg protoreflect.Message,
mapVal protoreflect.Map,
entry protoreflect.Message,
) {
keyFld, valFld := fld.MapKey(), fld.MapValue()
key := entry.Get(keyFld)
val := entry.Get(valFld)
if fld.MapValue().Kind() == protoreflect.MessageKind {
// Replace any nil/invalid values with an empty message
dm, valIsDynamic := val.Interface().(*dynamicpb.Message)
if (valIsDynamic && dm == nil) || !val.Message().IsValid() {
val = protoreflect.ValueOfMessage(dynamicpb.NewMessage(valFld.Message()))
}
_, containerIsDynamic := msg.Interface().(*dynamicpb.Message)
if valIsDynamic && !containerIsDynamic {
// This happens because we create dynamic messages to represent map entries,
// but the container of the map may expect a non-dynamic, generated type.
dest := mapVal.NewValue()
_, destIsDynamic := dest.Message().Interface().(*dynamicpb.Message)
if !destIsDynamic {
// reflection Set methods do not support cases where destination is
// generated but source is dynamic (or vice versa). But proto.Merge
// *DOES* support that, as long as dest and source use the same
// descriptor.
proto.Merge(dest.Message().Interface(), val.Message().Interface())
val = dest
}
}
}
// TODO: error if key is already present
mapVal.Set(key.MapKey(), val)
}
type msgLiteralResolver struct {
interp *interpreter
pkg protoreflect.FullName
}
func (r *msgLiteralResolver) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error) {
if r.interp.resolver == nil {
return nil, protoregistry.NotFound
}
return r.interp.resolver.FindMessageByName(message)
}
func (r *msgLiteralResolver) FindMessageByURL(url string) (protoreflect.MessageType, error) {
// In a message literal, we don't allow arbitrary URL prefixes
pos := strings.LastIndexByte(url, '/')
var urlPrefix string
if pos > 0 {
urlPrefix = url[:pos]
}
if urlPrefix != "type.googleapis.com" && urlPrefix != "type.googleprod.com" {
return nil, fmt.Errorf("could not resolve type reference %s", url)
}
return r.FindMessageByName(protoreflect.FullName(url[pos+1:]))
}
func (r *msgLiteralResolver) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
if r.interp.resolver == nil {
return nil, protoregistry.NotFound
}
// In a message literal, extension name may be partially qualified, relative to package.
// So we have to search through package scopes.
pkg := r.pkg
for {
// TODO: This does not *fully* implement the insane logic of protoc with regards
// to resolving relative references.
// https://protobuf.com/docs/language-spec#reference-resolution
name := pkg.Append(protoreflect.Name(field))
ext, err := r.interp.resolver.FindExtensionByName(name)
if err == nil {
return ext, nil
}
if pkg == "" {
// no more namespaces to check
return nil, err
}
pkg = pkg.Parent()
}
}
func (r *msgLiteralResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
if r.interp.resolver == nil {
return nil, protoregistry.NotFound
}
return r.interp.resolver.FindExtensionByNumber(message, field)
}
func fieldName(fld protoreflect.FieldDescriptor) string {
if fld.IsExtension() {
return fmt.Sprintf("(%s)", fld.FullName())
}
return string(fld.Name())
}
func valueKind(val any) string {
switch val := val.(type) {
case ast.Identifier:
return "identifier"
case bool:
return "bool"
case int64:
if val < 0 {
return "negative integer"
}
return "integer"
case uint64:
return "integer"
case float64:
return "double"
case string, []byte:
return "string"
case []*ast.MessageFieldNode:
return "message"
case []ast.ValueNode:
return "array"
default:
return fmt.Sprintf("%T", val)
}
}
func optionValueKind(opt *descriptorpb.UninterpretedOption) string {
switch {
case opt.IdentifierValue != nil:
return "identifier"
case opt.PositiveIntValue != nil:
return "integer"
case opt.NegativeIntValue != nil:
return "negative integer"
case opt.DoubleValue != nil:
return "double"
case opt.StringValue != nil:
return "string"
case opt.AggregateValue != nil:
return "message"
default:
// should not be possible
return "<nil>"
}
}
// fieldValue computes a compile-time value (constant or list or message literal) for the given
// AST node val. The value in val must be assignable to the field fld.
//
// If the returned value is not valid, then an error occurred during processing.
// The returned err may be nil, however, as any errors will already have been
// handled (so the resulting error could be nil if the handler returned nil).
func (interp *interpreter) fieldValue(
targetType descriptorpb.FieldOptions_OptionTargetType,
mc *internal.MessageContext,
msg protoreflect.Message,
fld protoreflect.FieldDescriptor,
val ast.ValueNode,
insideMsgLiteral bool,
pathPrefix []int32,
) (protoreflect.Value, sourceinfo.OptionSourceInfo, error) {
k := fld.Kind()
switch k {
case protoreflect.EnumKind:
num, _, err := interp.enumFieldValue(mc, fld.Enum(), val, insideMsgLiteral)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, interp.handleError(err)
}
return protoreflect.ValueOfEnum(num), newSrcInfo(pathPrefix, nil), nil
case protoreflect.MessageKind, protoreflect.GroupKind:
v := val.Value()
if aggs, ok := v.([]*ast.MessageFieldNode); ok {
var childMsg protoreflect.Message
switch {
case fld.IsList():
// List of messages
val := msg.NewField(fld)
childMsg = val.List().NewElement().Message()
case fld.IsMap():
// No generated type for map entries, so we use a dynamic type
childMsg = dynamicpb.NewMessage(fld.Message())
default:
// Normal message field
childMsg = msg.NewField(fld).Message()
}
return interp.messageLiteralValue(targetType, mc, aggs, childMsg, pathPrefix)
}
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{},
interp.handleErrorf(interp.nodeInfo(val), "%vexpecting message, got %s", mc, valueKind(v))
default:
v, err := interp.scalarFieldValue(mc, descriptorpb.FieldDescriptorProto_Type(k), val, insideMsgLiteral)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, interp.handleError(err)
}
return protoreflect.ValueOf(v), newSrcInfo(pathPrefix, nil), nil
}
}
// enumFieldValue resolves the given AST node val as an enum value descriptor. If the given
// value is not a valid identifier (or number if allowed), an error is returned instead.
func (interp *interpreter) enumFieldValue(
mc *internal.MessageContext,
ed protoreflect.EnumDescriptor,
val ast.ValueNode,
allowNumber bool,
) (protoreflect.EnumNumber, protoreflect.Name, error) {
v := val.Value()
var num protoreflect.EnumNumber
switch v := v.(type) {
case ast.Identifier:
name := protoreflect.Name(v)
ev := ed.Values().ByName(name)
if ev == nil {
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%venum %s has no value named %s", mc, ed.FullName(), v)
}
return ev.Number(), name, nil
case int64:
if !allowNumber {
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%vexpecting enum name, got %s", mc, valueKind(v))
}
if v > math.MaxInt32 || v < math.MinInt32 {
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for an enum", mc, v)
}
num = protoreflect.EnumNumber(v)
case uint64:
if !allowNumber {
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%vexpecting enum name, got %s", mc, valueKind(v))
}
if v > math.MaxInt32 {
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for an enum", mc, v)
}
num = protoreflect.EnumNumber(v)
default:
return 0, "", reporter.Errorf(interp.nodeInfo(val), "%vexpecting enum, got %s", mc, valueKind(v))
}
ev := ed.Values().ByNumber(num)
if ev != nil {
return num, ev.Name(), nil
}
if ed.IsClosed() {
return num, "", reporter.Errorf(interp.nodeInfo(val), "%vclosed enum %s has no value with number %d", mc, ed.FullName(), num)
}
// unknown value, but enum is open, so we allow it and return blank name
return num, "", nil
}
// enumFieldValueFromProto resolves the given uninterpreted option value as an enum value descriptor.
// If the given value is not a valid identifier, an error is returned instead.
func (interp *interpreter) enumFieldValueFromProto(
mc *internal.MessageContext,
ed protoreflect.EnumDescriptor,
opt *descriptorpb.UninterpretedOption,
node ast.Node,
) (protoreflect.EnumNumber, protoreflect.Name, error) {
// We don't have to worry about allowing numbers because numbers are never allowed
// in uninterpreted values; they are only allowed inside aggregate values (i.e.
// message literals).
switch {
case opt.IdentifierValue != nil:
name := protoreflect.Name(opt.GetIdentifierValue())
ev := ed.Values().ByName(name)
if ev == nil {
return 0, "", reporter.Errorf(interp.nodeInfo(node), "%venum %s has no value named %s", mc, ed.FullName(), name)
}
return ev.Number(), name, nil
default:
return 0, "", reporter.Errorf(interp.nodeInfo(node), "%vexpecting enum, got %s", mc, optionValueKind(opt))
}
}
// scalarFieldValue resolves the given AST node val as a value whose type is assignable to a
// field with the given fldType.
func (interp *interpreter) scalarFieldValue(
mc *internal.MessageContext,
fldType descriptorpb.FieldDescriptorProto_Type,
val ast.ValueNode,
insideMsgLiteral bool,
) (any, error) {
v := val.Value()
switch fldType {
case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
if b, ok := v.(bool); ok {
return b, nil
}
if id, ok := v.(ast.Identifier); ok {
if insideMsgLiteral {
// inside a message literal, values use the protobuf text format,
// which is lenient in that it accepts "t" and "f" or "True" and "False"
switch id {
case "t", "true", "True":
return true, nil
case "f", "false", "False":
return false, nil
}
} else {
// options with simple scalar values (no message literal) are stricter
switch id {
case "true":
return true, nil
case "false":
return false, nil
}
}
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting bool, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
if str, ok := v.(string); ok {
return []byte(str), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting bytes, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_STRING:
if str, ok := v.(string); ok {
return str, nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting string, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_INT32, descriptorpb.FieldDescriptorProto_TYPE_SINT32, descriptorpb.FieldDescriptorProto_TYPE_SFIXED32:
if i, ok := v.(int64); ok {
if i > math.MaxInt32 || i < math.MinInt32 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for int32", mc, i)
}
return int32(i), nil
}
if ui, ok := v.(uint64); ok {
if ui > math.MaxInt32 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for int32", mc, ui)
}
return int32(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting int32, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_UINT32, descriptorpb.FieldDescriptorProto_TYPE_FIXED32:
if i, ok := v.(int64); ok {
if i > math.MaxUint32 || i < 0 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for uint32", mc, i)
}
return uint32(i), nil
}
if ui, ok := v.(uint64); ok {
if ui > math.MaxUint32 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for uint32", mc, ui)
}
return uint32(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting uint32, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_INT64, descriptorpb.FieldDescriptorProto_TYPE_SINT64, descriptorpb.FieldDescriptorProto_TYPE_SFIXED64:
if i, ok := v.(int64); ok {
return i, nil
}
if ui, ok := v.(uint64); ok {
if ui > math.MaxInt64 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for int64", mc, ui)
}
return int64(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting int64, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_UINT64, descriptorpb.FieldDescriptorProto_TYPE_FIXED64:
if i, ok := v.(int64); ok {
if i < 0 {
return nil, reporter.Errorf(interp.nodeInfo(val), "%vvalue %d is out of range for uint64", mc, i)
}
return uint64(i), nil
}
if ui, ok := v.(uint64); ok {
return ui, nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting uint64, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE:
if id, ok := v.(ast.Identifier); ok {
switch id {
case "inf":
return math.Inf(1), nil
case "nan":
return math.NaN(), nil
}
}
if d, ok := v.(float64); ok {
return d, nil
}
if i, ok := v.(int64); ok {
return float64(i), nil
}
if u, ok := v.(uint64); ok {
return float64(u), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting double, got %s", mc, valueKind(v))
case descriptorpb.FieldDescriptorProto_TYPE_FLOAT:
if id, ok := v.(ast.Identifier); ok {
switch id {
case "inf":
return float32(math.Inf(1)), nil
case "nan":
return float32(math.NaN()), nil
}
}
if d, ok := v.(float64); ok {
return float32(d), nil
}
if i, ok := v.(int64); ok {
return float32(i), nil
}
if u, ok := v.(uint64); ok {
return float32(u), nil
}
return nil, reporter.Errorf(interp.nodeInfo(val), "%vexpecting float, got %s", mc, valueKind(v))
default:
return nil, reporter.Errorf(interp.nodeInfo(val), "%vunrecognized field type: %s", mc, fldType)
}
}
// scalarFieldValue resolves the given uninterpreted option value as a value whose type is
// assignable to a field with the given fldType.
func (interp *interpreter) scalarFieldValueFromProto(
mc *internal.MessageContext,
fldType descriptorpb.FieldDescriptorProto_Type,
opt *descriptorpb.UninterpretedOption,
node ast.Node,
) (any, error) {
switch fldType {
case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
if opt.IdentifierValue != nil {
switch opt.GetIdentifierValue() {
case "true":
return true, nil
case "false":
return false, nil
}
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting bool, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
if opt.StringValue != nil {
return opt.GetStringValue(), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting bytes, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_STRING:
if opt.StringValue != nil {
return string(opt.GetStringValue()), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting string, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_INT32, descriptorpb.FieldDescriptorProto_TYPE_SINT32, descriptorpb.FieldDescriptorProto_TYPE_SFIXED32:
if opt.NegativeIntValue != nil {
i := opt.GetNegativeIntValue()
if i > math.MaxInt32 || i < math.MinInt32 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for int32", mc, i)
}
return int32(i), nil
}
if opt.PositiveIntValue != nil {
ui := opt.GetPositiveIntValue()
if ui > math.MaxInt32 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for int32", mc, ui)
}
return int32(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting int32, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_UINT32, descriptorpb.FieldDescriptorProto_TYPE_FIXED32:
if opt.NegativeIntValue != nil {
i := opt.GetNegativeIntValue()
if i > math.MaxUint32 || i < 0 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for uint32", mc, i)
}
return uint32(i), nil
}
if opt.PositiveIntValue != nil {
ui := opt.GetPositiveIntValue()
if ui > math.MaxUint32 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for uint32", mc, ui)
}
return uint32(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting uint32, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_INT64, descriptorpb.FieldDescriptorProto_TYPE_SINT64, descriptorpb.FieldDescriptorProto_TYPE_SFIXED64:
if opt.NegativeIntValue != nil {
return opt.GetNegativeIntValue(), nil
}
if opt.PositiveIntValue != nil {
ui := opt.GetPositiveIntValue()
if ui > math.MaxInt64 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for int64", mc, ui)
}
return int64(ui), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting int64, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_UINT64, descriptorpb.FieldDescriptorProto_TYPE_FIXED64:
if opt.NegativeIntValue != nil {
i := opt.GetNegativeIntValue()
if i < 0 {
return nil, reporter.Errorf(interp.nodeInfo(node), "%vvalue %d is out of range for uint64", mc, i)
}
// should not be possible since i should always be negative...
return uint64(i), nil
}
if opt.PositiveIntValue != nil {
return opt.GetPositiveIntValue(), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting uint64, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE:
if opt.IdentifierValue != nil {
switch opt.GetIdentifierValue() {
case "inf":
return math.Inf(1), nil
case "nan":
return math.NaN(), nil
}
}
if opt.DoubleValue != nil {
return opt.GetDoubleValue(), nil
}
if opt.NegativeIntValue != nil {
return float64(opt.GetNegativeIntValue()), nil
}
if opt.PositiveIntValue != nil {
return float64(opt.GetPositiveIntValue()), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting double, got %s", mc, optionValueKind(opt))
case descriptorpb.FieldDescriptorProto_TYPE_FLOAT:
if opt.IdentifierValue != nil {
switch opt.GetIdentifierValue() {
case "inf":
return float32(math.Inf(1)), nil
case "nan":
return float32(math.NaN()), nil
}
}
if opt.DoubleValue != nil {
return float32(opt.GetDoubleValue()), nil
}
if opt.NegativeIntValue != nil {
return float32(opt.GetNegativeIntValue()), nil
}
if opt.PositiveIntValue != nil {
return float32(opt.GetPositiveIntValue()), nil
}
return nil, reporter.Errorf(interp.nodeInfo(node), "%vexpecting float, got %s", mc, optionValueKind(opt))
default:
return nil, reporter.Errorf(interp.nodeInfo(node), "%vunrecognized field type: %s", mc, fldType)
}
}
func descriptorType(m proto.Message) string {
switch m := m.(type) {
case *descriptorpb.DescriptorProto:
return "message"
case *descriptorpb.DescriptorProto_ExtensionRange:
return "extension range"
case *descriptorpb.FieldDescriptorProto:
if m.GetExtendee() == "" {
return "field"
}
return "extension"
case *descriptorpb.EnumDescriptorProto:
return "enum"
case *descriptorpb.EnumValueDescriptorProto:
return "enum value"
case *descriptorpb.ServiceDescriptorProto:
return "service"
case *descriptorpb.MethodDescriptorProto:
return "method"
case *descriptorpb.FileDescriptorProto:
return "file"
default:
// shouldn't be possible
return fmt.Sprintf("%T", m)
}
}
// messageLiteralValue processes a message literal value.
//
// If the returned value is not valid, then an error occurred during processing.
// The returned err may be nil, however, as any errors will already have been
// handled (so the resulting error could be nil if the handler returned nil).
func (interp *interpreter) messageLiteralValue(
targetType descriptorpb.FieldOptions_OptionTargetType,
mc *internal.MessageContext,
fieldNodes []*ast.MessageFieldNode,
msg protoreflect.Message,
pathPrefix []int32,
) (protoreflect.Value, sourceinfo.OptionSourceInfo, error) {
fmd := msg.Descriptor()
origPath := mc.OptAggPath
defer func() {
mc.OptAggPath = origPath
}()
flds := make(map[*ast.MessageFieldNode]*sourceinfo.OptionSourceInfo, len(fieldNodes))
var hadError bool
for _, fieldNode := range fieldNodes {
if origPath == "" {
mc.OptAggPath = fieldNode.Name.Value()
} else {
mc.OptAggPath = origPath + "." + fieldNode.Name.Value()
}
if fieldNode.Name.IsAnyTypeReference() {
if len(fieldNodes) > 1 {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name.URLPrefix), "%vany type references cannot be repeated or mixed with other fields", mc)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
}
if fmd.FullName() != "google.protobuf.Any" {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name.URLPrefix), "%vtype references are only allowed for google.protobuf.Any, but this type is %s", mc, fmd.FullName())
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
typeURLDescriptor := fmd.Fields().ByNumber(internal.AnyTypeURLTag)
var err error
switch {
case typeURLDescriptor == nil:
err = fmt.Errorf("message schema is missing type_url field (number %d)", internal.AnyTypeURLTag)
case typeURLDescriptor.IsList():
err = fmt.Errorf("message schema has type_url field (number %d) that is a list but should be singular", internal.AnyTypeURLTag)
case typeURLDescriptor.Kind() != protoreflect.StringKind:
err = fmt.Errorf("message schema has type_url field (number %d) that is %s but should be string", internal.AnyTypeURLTag, typeURLDescriptor.Kind())
}
if err != nil {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name), "%v%w", mc, err)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
valueDescriptor := fmd.Fields().ByNumber(internal.AnyValueTag)
switch {
case valueDescriptor == nil:
err = fmt.Errorf("message schema is missing value field (number %d)", internal.AnyValueTag)
case valueDescriptor.IsList():
err = fmt.Errorf("message schema has value field (number %d) that is a list but should be singular", internal.AnyValueTag)
case valueDescriptor.Kind() != protoreflect.BytesKind:
err = fmt.Errorf("message schema has value field (number %d) that is %s but should be bytes", internal.AnyValueTag, valueDescriptor.Kind())
}
if err != nil {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name), "%v%w", mc, err)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
urlPrefix := fieldNode.Name.URLPrefix.AsIdentifier()
msgName := fieldNode.Name.Name.AsIdentifier()
fullURL := fmt.Sprintf("%s/%s", urlPrefix, msgName)
// TODO: Support other URLs dynamically -- the caller of protocompile
// should be able to provide a custom resolver that can resolve type
// URLs into message descriptors. The default resolver would be
// implemented as below, only accepting "type.googleapis.com" and
// "type.googleprod.com" as hosts/prefixes and using the compiled
// file's transitive closure to find the named message, since that
// is what protoc does.
if urlPrefix != "type.googleapis.com" && urlPrefix != "type.googleprod.com" {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name.URLPrefix), "%vcould not resolve type reference %s", mc, fullURL)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
anyFields, ok := fieldNode.Val.Value().([]*ast.MessageFieldNode)
if !ok {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Val), "%vtype references for google.protobuf.Any must have message literal value", mc)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
anyMd := resolveDescriptor[protoreflect.MessageDescriptor](interp.resolver, string(msgName))
if anyMd == nil {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name.URLPrefix), "%vcould not resolve type reference %s", mc, fullURL)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
// parse the message value
msgVal, valueSrcInfo, err := interp.messageLiteralValue(targetType, mc, anyFields, dynamicpb.NewMessage(anyMd), append(pathPrefix, internal.AnyValueTag))
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
} else if !msgVal.IsValid() {
hadError = true
continue
}
b, err := (proto.MarshalOptions{Deterministic: true}).Marshal(msgVal.Message().Interface())
if err != nil {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Val), "%vfailed to serialize message value: %w", mc, err)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
// Success!
if !hadError {
msg.Set(typeURLDescriptor, protoreflect.ValueOfString(fullURL))
msg.Set(valueDescriptor, protoreflect.ValueOfBytes(b))
flds[fieldNode] = &valueSrcInfo
}
continue
}
// Not expanded Any syntax; handle normal field.
var ffld protoreflect.FieldDescriptor
var err error
if fieldNode.Name.IsExtension() {
n := interp.file.ResolveMessageLiteralExtensionName(fieldNode.Name.Name)
if n == "" {
// this should not be possible!
n = string(fieldNode.Name.Name.AsIdentifier())
}
ffld, err = interp.resolveExtensionType(n)
if errors.Is(err, protoregistry.NotFound) {
// may need to qualify with package name
// (this should not be necessary!)
pkg := mc.File.FileDescriptorProto().GetPackage()
if pkg != "" {
ffld, err = interp.resolveExtensionType(pkg + "." + n)
}
}
} else {
ffld = fmd.Fields().ByName(protoreflect.Name(fieldNode.Name.Value()))
if ffld == nil {
err = protoregistry.NotFound
// It could be a proto2 group, where the text format refers to the group type
// name, and the field name is the lower-cased form of that.
ffld = fmd.Fields().ByName(protoreflect.Name(strings.ToLower(fieldNode.Name.Value())))
if ffld != nil {
// In editions, we support using the group type name only for fields that
// "look like" proto2 groups.
if protoreflect.Name(fieldNode.Name.Value()) == ffld.Message().Name() && // text format uses type name
ffld.Message().FullName().Parent() == ffld.FullName().Parent() && // message and field declared in same scope
ffld.Kind() == protoreflect.GroupKind /* uses delimited encoding */ {
// This one looks like a proto2 group, so it's a keeper.
err = nil
} else {
// It doesn't look like a proto2 group, so this is not a match.
ffld = nil
}
}
}
}
if errors.Is(err, protoregistry.NotFound) {
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Name), "%vfield %s not found", mc, string(fieldNode.Name.Name.AsIdentifier()))
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
} else if err != nil {
err := interp.handleErrorWithPos(interp.nodeInfo(fieldNode.Name), err)
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
if err := interp.checkFieldUsage(targetType, ffld, fieldNode.Name); err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
if fieldNode.Sep == nil && ffld.Message() == nil {
// If there is no separator, the field type should be a message.
// Otherwise, it is an error in the text format.
err := interp.handleErrorf(interp.nodeInfo(fieldNode.Val), "syntax error: unexpected value, expecting ':'")
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
hadError = true
continue
}
srcInfo, err := interp.setOptionField(targetType, mc, msg, ffld, fieldNode.Name, fieldNode.Val, true, append(pathPrefix, int32(ffld.Number())))
if err != nil {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, err
}
if srcInfo != nil {
flds[fieldNode] = srcInfo
}
}
if hadError {
return protoreflect.Value{}, sourceinfo.OptionSourceInfo{}, nil
}
return protoreflect.ValueOfMessage(msg),
newSrcInfo(pathPrefix, &sourceinfo.MessageLiteralSourceInfo{Fields: flds}),
nil
}
func newSrcInfo(path []int32, children sourceinfo.OptionChildrenSourceInfo) sourceinfo.OptionSourceInfo {
return sourceinfo.OptionSourceInfo{
Path: internal.ClonePath(path),
Children: children,
}
}
// 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 options
import (
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/internal"
)
// StripSourceRetentionOptionsFromFile returns a file descriptor proto that omits any
// options in file that are defined to be retained only in source. If file has no
// such options, then it is returned as is. If it does have such options, a copy is
// made; the given file will not be mutated.
//
// Even when a copy is returned, it is not a deep copy: it may share data with the
// original file. So callers should not mutate the returned file unless mutating the
// input file is also safe.
func StripSourceRetentionOptionsFromFile(file *descriptorpb.FileDescriptorProto) (*descriptorpb.FileDescriptorProto, error) {
var path sourcePath
var removedPaths *sourcePathTrie
if file.SourceCodeInfo != nil && len(file.SourceCodeInfo.Location) > 0 {
path = make(sourcePath, 0, 16)
removedPaths = &sourcePathTrie{}
}
var dirty bool
optionsPath := path.push(internal.FileOptionsTag)
newOpts, err := stripSourceRetentionOptions(file.GetOptions(), optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts != file.GetOptions() {
dirty = true
}
msgsPath := path.push(internal.FileMessagesTag)
newMsgs, changed, err := stripOptionsFromAll(file.GetMessageType(), stripSourceRetentionOptionsFromMessage, msgsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
enumsPath := path.push(internal.FileEnumsTag)
newEnums, changed, err := stripOptionsFromAll(file.GetEnumType(), stripSourceRetentionOptionsFromEnum, enumsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
extsPath := path.push(internal.FileExtensionsTag)
newExts, changed, err := stripOptionsFromAll(file.GetExtension(), stripSourceRetentionOptionsFromField, extsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
svcsPath := path.push(internal.FileServicesTag)
newSvcs, changed, err := stripOptionsFromAll(file.GetService(), stripSourceRetentionOptionsFromService, svcsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
if !dirty {
return file, nil
}
newFile, err := shallowCopy(file)
if err != nil {
return nil, err
}
newFile.Options = newOpts
newFile.MessageType = newMsgs
newFile.EnumType = newEnums
newFile.Extension = newExts
newFile.Service = newSvcs
newFile.SourceCodeInfo = stripSourcePathsForSourceRetentionOptions(newFile.SourceCodeInfo, removedPaths)
return newFile, nil
}
type sourcePath protoreflect.SourcePath
func (p sourcePath) push(element int32) sourcePath {
if p == nil {
return nil
}
return append(p, element)
}
type sourcePathTrie struct {
removed bool
children map[int32]*sourcePathTrie
}
func (t *sourcePathTrie) addPath(p sourcePath) {
if t == nil {
return
}
if len(p) == 0 {
t.removed = true
return
}
child := t.children[p[0]]
if child == nil {
if t.children == nil {
t.children = map[int32]*sourcePathTrie{}
}
child = &sourcePathTrie{}
t.children[p[0]] = child
}
child.addPath(p[1:])
}
func (t *sourcePathTrie) isRemoved(p []int32) bool {
if t == nil {
return false
}
if t.removed {
return true
}
if len(p) == 0 {
return false
}
child := t.children[p[0]]
if child == nil {
return false
}
return child.isRemoved(p[1:])
}
func stripSourceRetentionOptions[M proto.Message](
options M,
path sourcePath,
removedPaths *sourcePathTrie,
) (M, error) {
optionsRef := options.ProtoReflect()
// See if there are any options to strip.
var hasFieldToStrip bool
var numFieldsToKeep int
var err error
optionsRef.Range(func(field protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
fieldOpts, ok := field.Options().(*descriptorpb.FieldOptions)
if !ok {
err = fmt.Errorf("field options is unexpected type: got %T, want %T", field.Options(), fieldOpts)
return false
}
if fieldOpts.GetRetention() == descriptorpb.FieldOptions_RETENTION_SOURCE {
hasFieldToStrip = true
} else {
numFieldsToKeep++
}
return true
})
var zero M
if err != nil {
return zero, err
}
if !hasFieldToStrip {
return options, nil
}
if numFieldsToKeep == 0 {
// Stripping the message would remove *all* options. In that case,
// we'll clear out the options by returning the zero value (i.e. nil).
removedPaths.addPath(path) // clear out all source locations, too
return zero, nil
}
// There is at least one option to remove. So we need to make a copy that does not have those options.
newOptions := optionsRef.New()
ret, ok := newOptions.Interface().(M)
if !ok {
return zero, fmt.Errorf("creating new message of same type resulted in unexpected type; got %T, want %T", newOptions.Interface(), zero)
}
optionsRef.Range(func(field protoreflect.FieldDescriptor, val protoreflect.Value) bool {
fieldOpts, ok := field.Options().(*descriptorpb.FieldOptions)
if !ok {
err = fmt.Errorf("field options is unexpected type: got %T, want %T", field.Options(), fieldOpts)
return false
}
if fieldOpts.GetRetention() != descriptorpb.FieldOptions_RETENTION_SOURCE {
newOptions.Set(field, val)
} else {
removedPaths.addPath(path.push(int32(field.Number())))
}
return true
})
if err != nil {
return zero, err
}
return ret, nil
}
func stripSourceRetentionOptionsFromMessage(
msg *descriptorpb.DescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.DescriptorProto, error) {
var dirty bool
optionsPath := path.push(internal.MessageOptionsTag)
newOpts, err := stripSourceRetentionOptions(msg.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts != msg.Options {
dirty = true
}
fieldsPath := path.push(internal.MessageFieldsTag)
newFields, changed, err := stripOptionsFromAll(msg.Field, stripSourceRetentionOptionsFromField, fieldsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
oneofsPath := path.push(internal.MessageOneofsTag)
newOneofs, changed, err := stripOptionsFromAll(msg.OneofDecl, stripSourceRetentionOptionsFromOneof, oneofsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
extRangesPath := path.push(internal.MessageExtensionRangesTag)
newExtRanges, changed, err := stripOptionsFromAll(msg.ExtensionRange, stripSourceRetentionOptionsFromExtensionRange, extRangesPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
msgsPath := path.push(internal.MessageNestedMessagesTag)
newMsgs, changed, err := stripOptionsFromAll(msg.NestedType, stripSourceRetentionOptionsFromMessage, msgsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
enumsPath := path.push(internal.MessageEnumsTag)
newEnums, changed, err := stripOptionsFromAll(msg.EnumType, stripSourceRetentionOptionsFromEnum, enumsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
extsPath := path.push(internal.MessageExtensionsTag)
newExts, changed, err := stripOptionsFromAll(msg.Extension, stripSourceRetentionOptionsFromField, extsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
if !dirty {
return msg, nil
}
newMsg, err := shallowCopy(msg)
if err != nil {
return nil, err
}
newMsg.Options = newOpts
newMsg.Field = newFields
newMsg.OneofDecl = newOneofs
newMsg.ExtensionRange = newExtRanges
newMsg.NestedType = newMsgs
newMsg.EnumType = newEnums
newMsg.Extension = newExts
return newMsg, nil
}
func stripSourceRetentionOptionsFromField(
field *descriptorpb.FieldDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.FieldDescriptorProto, error) {
optionsPath := path.push(internal.FieldOptionsTag)
newOpts, err := stripSourceRetentionOptions(field.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts == field.Options {
return field, nil
}
newField, err := shallowCopy(field)
if err != nil {
return nil, err
}
newField.Options = newOpts
return newField, nil
}
func stripSourceRetentionOptionsFromOneof(
oneof *descriptorpb.OneofDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.OneofDescriptorProto, error) {
optionsPath := path.push(internal.OneofOptionsTag)
newOpts, err := stripSourceRetentionOptions(oneof.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts == oneof.Options {
return oneof, nil
}
newOneof, err := shallowCopy(oneof)
if err != nil {
return nil, err
}
newOneof.Options = newOpts
return newOneof, nil
}
func stripSourceRetentionOptionsFromExtensionRange(
extRange *descriptorpb.DescriptorProto_ExtensionRange,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.DescriptorProto_ExtensionRange, error) {
optionsPath := path.push(internal.ExtensionRangeOptionsTag)
newOpts, err := stripSourceRetentionOptions(extRange.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts == extRange.Options {
return extRange, nil
}
newExtRange, err := shallowCopy(extRange)
if err != nil {
return nil, err
}
newExtRange.Options = newOpts
return newExtRange, nil
}
func stripSourceRetentionOptionsFromEnum(
enum *descriptorpb.EnumDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.EnumDescriptorProto, error) {
var dirty bool
optionsPath := path.push(internal.EnumOptionsTag)
newOpts, err := stripSourceRetentionOptions(enum.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts != enum.Options {
dirty = true
}
valsPath := path.push(internal.EnumValuesTag)
newVals, changed, err := stripOptionsFromAll(enum.Value, stripSourceRetentionOptionsFromEnumValue, valsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
if !dirty {
return enum, nil
}
newEnum, err := shallowCopy(enum)
if err != nil {
return nil, err
}
newEnum.Options = newOpts
newEnum.Value = newVals
return newEnum, nil
}
func stripSourceRetentionOptionsFromEnumValue(
enumVal *descriptorpb.EnumValueDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.EnumValueDescriptorProto, error) {
optionsPath := path.push(internal.EnumValOptionsTag)
newOpts, err := stripSourceRetentionOptions(enumVal.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts == enumVal.Options {
return enumVal, nil
}
newEnumVal, err := shallowCopy(enumVal)
if err != nil {
return nil, err
}
newEnumVal.Options = newOpts
return newEnumVal, nil
}
func stripSourceRetentionOptionsFromService(
svc *descriptorpb.ServiceDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.ServiceDescriptorProto, error) {
var dirty bool
optionsPath := path.push(internal.ServiceOptionsTag)
newOpts, err := stripSourceRetentionOptions(svc.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts != svc.Options {
dirty = true
}
methodsPath := path.push(internal.ServiceMethodsTag)
newMethods, changed, err := stripOptionsFromAll(svc.Method, stripSourceRetentionOptionsFromMethod, methodsPath, removedPaths)
if err != nil {
return nil, err
}
if changed {
dirty = true
}
if !dirty {
return svc, nil
}
newSvc, err := shallowCopy(svc)
if err != nil {
return nil, err
}
newSvc.Options = newOpts
newSvc.Method = newMethods
return newSvc, nil
}
func stripSourceRetentionOptionsFromMethod(
method *descriptorpb.MethodDescriptorProto,
path sourcePath,
removedPaths *sourcePathTrie,
) (*descriptorpb.MethodDescriptorProto, error) {
optionsPath := path.push(internal.MethodOptionsTag)
newOpts, err := stripSourceRetentionOptions(method.Options, optionsPath, removedPaths)
if err != nil {
return nil, err
}
if newOpts == method.Options {
return method, nil
}
newMethod, err := shallowCopy(method)
if err != nil {
return nil, err
}
newMethod.Options = newOpts
return newMethod, nil
}
func stripSourcePathsForSourceRetentionOptions(
sourceInfo *descriptorpb.SourceCodeInfo,
removedPaths *sourcePathTrie,
) *descriptorpb.SourceCodeInfo {
if sourceInfo == nil || len(sourceInfo.Location) == 0 || removedPaths == nil {
// nothing to do
return sourceInfo
}
newLocations := make([]*descriptorpb.SourceCodeInfo_Location, len(sourceInfo.Location))
var i int
for _, loc := range sourceInfo.Location {
if removedPaths.isRemoved(loc.Path) {
continue
}
newLocations[i] = loc
i++
}
newLocations = newLocations[:i]
return &descriptorpb.SourceCodeInfo{Location: newLocations}
}
func shallowCopy[M proto.Message](msg M) (M, error) {
msgRef := msg.ProtoReflect()
other := msgRef.New()
ret, ok := other.Interface().(M)
if !ok {
return ret, fmt.Errorf("creating new message of same type resulted in unexpected type; got %T, want %T", other.Interface(), ret)
}
msgRef.Range(func(field protoreflect.FieldDescriptor, val protoreflect.Value) bool {
other.Set(field, val)
return true
})
return ret, nil
}
// stripOptionsFromAll applies the given function to each element in the given
// slice in order to remove source-retention options from it. It returns the new
// slice and a bool indicating whether anything was actually changed. If the
// second value is false, then the returned slice is the same slice as the input
// slice. Usually, T is a pointer type, in which case the given updateFunc should
// NOT mutate the input value. Instead, it should return the input value if only
// if there is no update needed. If a mutation is needed, it should return a new
// value.
func stripOptionsFromAll[T comparable](
slice []T,
updateFunc func(T, sourcePath, *sourcePathTrie) (T, error),
path sourcePath,
removedPaths *sourcePathTrie,
) ([]T, bool, error) {
var updated []T // initialized lazily, only when/if a copy is needed
for i, item := range slice {
newItem, err := updateFunc(item, path.push(int32(i)), removedPaths)
if err != nil {
return nil, false, err
}
if updated != nil {
updated[i] = newItem
} else if newItem != item {
updated = make([]T, len(slice))
copy(updated[:i], slice)
updated[i] = newItem
}
}
if updated != nil {
return updated, true, nil
}
return slice, false, nil
}
// 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 options
import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
)
type optionsType[T any] interface {
*T
proto.Message
GetFeatures() *descriptorpb.FeatureSet
GetUninterpretedOption() []*descriptorpb.UninterpretedOption
}
type elementType[OptsStruct any, Opts optionsType[OptsStruct]] interface {
proto.Message
GetOptions() Opts
}
type targetType[Elem elementType[OptsStruct, Opts], OptsStruct any, Opts optionsType[OptsStruct]] struct {
t descriptorpb.FieldOptions_OptionTargetType
setUninterpretedOptions func(opts Opts, uninterpreted []*descriptorpb.UninterpretedOption)
setOptions func(elem Elem, opts Opts)
}
var (
targetTypeFile = newTargetType[*descriptorpb.FileDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_FILE, setUninterpretedFileOptions, setFileOptions,
)
targetTypeMessage = newTargetType[*descriptorpb.DescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_MESSAGE, setUninterpretedMessageOptions, setMessageOptions,
)
targetTypeField = newTargetType[*descriptorpb.FieldDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_FIELD, setUninterpretedFieldOptions, setFieldOptions,
)
targetTypeOneof = newTargetType[*descriptorpb.OneofDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_ONEOF, setUninterpretedOneofOptions, setOneofOptions,
)
targetTypeExtensionRange = newTargetType[*descriptorpb.DescriptorProto_ExtensionRange](
descriptorpb.FieldOptions_TARGET_TYPE_EXTENSION_RANGE, setUninterpretedExtensionRangeOptions, setExtensionRangeOptions,
)
targetTypeEnum = newTargetType[*descriptorpb.EnumDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_ENUM, setUninterpretedEnumOptions, setEnumOptions,
)
targetTypeEnumValue = newTargetType[*descriptorpb.EnumValueDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_ENUM_ENTRY, setUninterpretedEnumValueOptions, setEnumValueOptions,
)
targetTypeService = newTargetType[*descriptorpb.ServiceDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_SERVICE, setUninterpretedServiceOptions, setServiceOptions,
)
targetTypeMethod = newTargetType[*descriptorpb.MethodDescriptorProto](
descriptorpb.FieldOptions_TARGET_TYPE_METHOD, setUninterpretedMethodOptions, setMethodOptions,
)
)
func newTargetType[Elem elementType[OptsStruct, Opts], OptsStruct any, Opts optionsType[OptsStruct]](
t descriptorpb.FieldOptions_OptionTargetType,
setUninterpretedOptions func(opts Opts, uninterpreted []*descriptorpb.UninterpretedOption),
setOptions func(elem Elem, opts Opts),
) *targetType[Elem, OptsStruct, Opts] {
return &targetType[Elem, OptsStruct, Opts]{
t: t,
setUninterpretedOptions: setUninterpretedOptions,
setOptions: setOptions,
}
}
func setUninterpretedFileOptions(opts *descriptorpb.FileOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedMessageOptions(opts *descriptorpb.MessageOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedFieldOptions(opts *descriptorpb.FieldOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedOneofOptions(opts *descriptorpb.OneofOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedExtensionRangeOptions(opts *descriptorpb.ExtensionRangeOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedEnumOptions(opts *descriptorpb.EnumOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedEnumValueOptions(opts *descriptorpb.EnumValueOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedServiceOptions(opts *descriptorpb.ServiceOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setUninterpretedMethodOptions(opts *descriptorpb.MethodOptions, uninterpreted []*descriptorpb.UninterpretedOption) {
opts.UninterpretedOption = uninterpreted
}
func setFileOptions(desc *descriptorpb.FileDescriptorProto, opts *descriptorpb.FileOptions) {
desc.Options = opts
}
func setMessageOptions(desc *descriptorpb.DescriptorProto, opts *descriptorpb.MessageOptions) {
desc.Options = opts
}
func setFieldOptions(desc *descriptorpb.FieldDescriptorProto, opts *descriptorpb.FieldOptions) {
desc.Options = opts
}
func setOneofOptions(desc *descriptorpb.OneofDescriptorProto, opts *descriptorpb.OneofOptions) {
desc.Options = opts
}
func setExtensionRangeOptions(desc *descriptorpb.DescriptorProto_ExtensionRange, opts *descriptorpb.ExtensionRangeOptions) {
desc.Options = opts
}
func setEnumOptions(desc *descriptorpb.EnumDescriptorProto, opts *descriptorpb.EnumOptions) {
desc.Options = opts
}
func setEnumValueOptions(desc *descriptorpb.EnumValueDescriptorProto, opts *descriptorpb.EnumValueOptions) {
desc.Options = opts
}
func setServiceOptions(desc *descriptorpb.ServiceDescriptorProto, opts *descriptorpb.ServiceOptions) {
desc.Options = opts
}
func setMethodOptions(desc *descriptorpb.MethodDescriptorProto, opts *descriptorpb.MethodOptions) {
desc.Options = opts
}
// 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/ast"
)
// the types below are accumulator types, just used in intermediate productions
// to accumulate slices that will get stored in AST nodes
type compactOptionSlices struct {
options []*ast.OptionNode
commas []*ast.RuneNode
}
func toStringValueNode(strs []*ast.StringLiteralNode) ast.StringValueNode {
if len(strs) == 1 {
return strs[0]
}
return ast.NewCompoundLiteralStringNode(strs...)
}
type nameSlices struct {
// only names or idents will be set, never both
names []ast.StringValueNode
idents []*ast.IdentNode
commas []*ast.RuneNode
}
type rangeSlices struct {
ranges []*ast.RangeNode
commas []*ast.RuneNode
}
type valueSlices struct {
vals []ast.ValueNode
commas []*ast.RuneNode
}
type fieldRefSlices struct {
refs []*ast.FieldReferenceNode
dots []*ast.RuneNode
}
type identSlices struct {
idents []*ast.IdentNode
dots []*ast.RuneNode
}
func (s *identSlices) prefix(ident *ast.IdentNode, dot *ast.RuneNode) {
s.idents = append(s.idents, nil)
copy(s.idents[1:], s.idents)
s.idents[0] = ident
s.dots = append(s.dots, nil)
copy(s.dots[1:], s.dots)
s.dots[0] = dot
}
func (s *identSlices) toIdentValueNode(leadingDot *ast.RuneNode) ast.IdentValueNode {
if len(s.idents) == 1 && leadingDot == nil {
// single simple name
return s.idents[0]
}
return ast.NewCompoundIdentNode(leadingDot, s.idents, s.dots)
}
type messageFieldList struct {
field *ast.MessageFieldNode
delimiter *ast.RuneNode
next *messageFieldList
}
func (list *messageFieldList) toNodes() ([]*ast.MessageFieldNode, []*ast.RuneNode) {
if list == nil {
return nil, nil
}
l := 0
for cur := list; cur != nil; cur = cur.next {
l++
}
fields := make([]*ast.MessageFieldNode, l)
delimiters := make([]*ast.RuneNode, l)
for cur, i := list, 0; cur != nil; cur, i = cur.next, i+1 {
fields[i] = cur.field
if cur.delimiter != nil {
delimiters[i] = cur.delimiter
}
}
return fields, delimiters
}
func prependRunes[T ast.Node](convert func(*ast.RuneNode) T, runes []*ast.RuneNode, elements []T) []T {
elems := make([]T, 0, len(runes)+len(elements))
for _, rune := range runes {
elems = append(elems, convert(rune))
}
elems = append(elems, elements...)
return elems
}
func toServiceElement(semi *ast.RuneNode) ast.ServiceElement {
return ast.NewEmptyDeclNode(semi)
}
func toMethodElement(semi *ast.RuneNode) ast.RPCElement {
return ast.NewEmptyDeclNode(semi)
}
func toFileElement(semi *ast.RuneNode) ast.FileElement {
return ast.NewEmptyDeclNode(semi)
}
func toEnumElement(semi *ast.RuneNode) ast.EnumElement {
return ast.NewEmptyDeclNode(semi)
}
func toMessageElement(semi *ast.RuneNode) ast.MessageElement {
return ast.NewEmptyDeclNode(semi)
}
type nodeWithRunes[T ast.Node] struct {
Node T
Runes []*ast.RuneNode
}
func newNodeWithRunes[T ast.Node](node T, trailingRunes ...*ast.RuneNode) nodeWithRunes[T] {
return nodeWithRunes[T]{
Node: node,
Runes: trailingRunes,
}
}
func toElements[T ast.Node](convert func(*ast.RuneNode) T, node T, runes []*ast.RuneNode) []T {
elements := make([]T, 1+len(runes))
elements[0] = node
for i, rune := range runes {
elements[i+1] = convert(rune)
}
return elements
}
// 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 (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/reporter"
)
// Clone returns a copy of the given result. Since descriptor protos may be
// mutated during linking, this can return a defensive copy so that mutations
// don't impact concurrent operations in an unsafe way. This is called if the
// parse result could be re-used across concurrent operations and has unresolved
// references and options which will require mutation by the linker.
//
// If the given value has a method with the following signature, it will be
// called to perform the operation:
//
// Clone() Result
//
// If the given value does not provide a Clone method and is not the implementation
// provided by this package, it is possible for an error to occur in creating the
// copy, which may result in a panic. This can happen if the AST of the given result
// is not actually valid and a file descriptor proto cannot be successfully derived
// from it.
func Clone(r Result) Result {
if cl, ok := r.(interface{ Clone() Result }); ok {
return cl.Clone()
}
if res, ok := r.(*result); ok {
newProto := proto.Clone(res.proto).(*descriptorpb.FileDescriptorProto) //nolint:errcheck
newNodes := make(map[proto.Message]ast.Node, len(res.nodes))
newResult := &result{
file: res.file,
proto: newProto,
nodes: newNodes,
}
recreateNodeIndexForFile(res, newResult, res.proto, newProto)
return newResult
}
// Can't do the deep-copy we know how to do. So we have to take a
// different tactic.
if r.AST() == nil {
// no AST? all we have to do is copy the proto
fileProto := proto.Clone(r.FileDescriptorProto()).(*descriptorpb.FileDescriptorProto) //nolint:errcheck
return ResultWithoutAST(fileProto)
}
// Otherwise, we have an AST, but no way to clone the result's
// internals. So just re-create them from scratch.
res, err := ResultFromAST(r.AST(), false, reporter.NewHandler(nil))
if err != nil {
panic(err)
}
return res
}
func recreateNodeIndexForFile(orig, clone *result, origProto, cloneProto *descriptorpb.FileDescriptorProto) {
updateNodeIndexWithOptions[*descriptorpb.FileOptions](orig, clone, origProto, cloneProto)
for i, origMd := range origProto.MessageType {
cloneMd := cloneProto.MessageType[i]
recreateNodeIndexForMessage(orig, clone, origMd, cloneMd)
}
for i, origEd := range origProto.EnumType {
cloneEd := cloneProto.EnumType[i]
recreateNodeIndexForEnum(orig, clone, origEd, cloneEd)
}
for i, origExtd := range origProto.Extension {
cloneExtd := cloneProto.Extension[i]
updateNodeIndexWithOptions[*descriptorpb.FieldOptions](orig, clone, origExtd, cloneExtd)
}
for i, origSd := range origProto.Service {
cloneSd := cloneProto.Service[i]
updateNodeIndexWithOptions[*descriptorpb.ServiceOptions](orig, clone, origSd, cloneSd)
for j, origMtd := range origSd.Method {
cloneMtd := cloneSd.Method[j]
updateNodeIndexWithOptions[*descriptorpb.MethodOptions](orig, clone, origMtd, cloneMtd)
}
}
}
func recreateNodeIndexForMessage(orig, clone *result, origProto, cloneProto *descriptorpb.DescriptorProto) {
updateNodeIndexWithOptions[*descriptorpb.MessageOptions](orig, clone, origProto, cloneProto)
for i, origFld := range origProto.Field {
cloneFld := cloneProto.Field[i]
updateNodeIndexWithOptions[*descriptorpb.FieldOptions](orig, clone, origFld, cloneFld)
}
for i, origOod := range origProto.OneofDecl {
cloneOod := cloneProto.OneofDecl[i]
updateNodeIndexWithOptions[*descriptorpb.OneofOptions](orig, clone, origOod, cloneOod)
}
for i, origExtr := range origProto.ExtensionRange {
cloneExtr := cloneProto.ExtensionRange[i]
updateNodeIndex(orig, clone, asExtsNode(origExtr), asExtsNode(cloneExtr))
updateNodeIndexWithOptions[*descriptorpb.ExtensionRangeOptions](orig, clone, origExtr, cloneExtr)
}
for i, origRr := range origProto.ReservedRange {
cloneRr := cloneProto.ReservedRange[i]
updateNodeIndex(orig, clone, origRr, cloneRr)
}
for i, origNmd := range origProto.NestedType {
cloneNmd := cloneProto.NestedType[i]
recreateNodeIndexForMessage(orig, clone, origNmd, cloneNmd)
}
for i, origEd := range origProto.EnumType {
cloneEd := cloneProto.EnumType[i]
recreateNodeIndexForEnum(orig, clone, origEd, cloneEd)
}
for i, origExtd := range origProto.Extension {
cloneExtd := cloneProto.Extension[i]
updateNodeIndexWithOptions[*descriptorpb.FieldOptions](orig, clone, origExtd, cloneExtd)
}
}
func recreateNodeIndexForEnum(orig, clone *result, origProto, cloneProto *descriptorpb.EnumDescriptorProto) {
updateNodeIndexWithOptions[*descriptorpb.EnumOptions](orig, clone, origProto, cloneProto)
for i, origEvd := range origProto.Value {
cloneEvd := cloneProto.Value[i]
updateNodeIndexWithOptions[*descriptorpb.EnumValueOptions](orig, clone, origEvd, cloneEvd)
}
for i, origRr := range origProto.ReservedRange {
cloneRr := cloneProto.ReservedRange[i]
updateNodeIndex(orig, clone, origRr, cloneRr)
}
}
func recreateNodeIndexForOptions(orig, clone *result, origProtos, cloneProtos []*descriptorpb.UninterpretedOption) {
for i, origOpt := range origProtos {
cloneOpt := cloneProtos[i]
updateNodeIndex(orig, clone, origOpt, cloneOpt)
for j, origName := range origOpt.Name {
cloneName := cloneOpt.Name[j]
updateNodeIndex(orig, clone, origName, cloneName)
}
}
}
func updateNodeIndex[M proto.Message](orig, clone *result, origProto, cloneProto M) {
node := orig.nodes[origProto]
if node != nil {
clone.nodes[cloneProto] = node
}
}
type pointerMessage[T any] interface {
*T
proto.Message
}
type options[T any] interface {
// need this type instead of just proto.Message so we can check for nil pointer
pointerMessage[T]
GetUninterpretedOption() []*descriptorpb.UninterpretedOption
}
type withOptions[O options[T], T any] interface {
proto.Message
GetOptions() O
}
func updateNodeIndexWithOptions[O options[T], M withOptions[O, T], T any](orig, clone *result, origProto, cloneProto M) {
updateNodeIndex(orig, clone, origProto, cloneProto)
origOpts := origProto.GetOptions()
cloneOpts := cloneProto.GetOptions()
if origOpts != nil {
recreateNodeIndexForOptions(orig, clone, origOpts.GetUninterpretedOption(), cloneOpts.GetUninterpretedOption())
}
}
// 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 (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"math"
"strconv"
"strings"
"unicode/utf8"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/reporter"
)
type runeReader struct {
data []byte
pos int
err error
mark int
// Enable this check to make input required to be valid UTF-8.
// For now, since protoc allows invalid UTF-8, default to false.
utf8Strict bool
}
func (rr *runeReader) readRune() (r rune, size int, err error) {
if rr.err != nil {
return 0, 0, rr.err
}
if rr.pos == len(rr.data) {
rr.err = io.EOF
return 0, 0, rr.err
}
r, sz := utf8.DecodeRune(rr.data[rr.pos:])
if rr.utf8Strict && r == utf8.RuneError {
rr.err = fmt.Errorf("invalid UTF8 at offset %d: %x", rr.pos, rr.data[rr.pos])
return 0, 0, rr.err
}
rr.pos += sz
return r, sz, nil
}
func (rr *runeReader) offset() int {
return rr.pos
}
func (rr *runeReader) unreadRune(sz int) {
newPos := rr.pos - sz
if newPos < rr.mark {
panic("unread past mark")
}
rr.pos = newPos
}
func (rr *runeReader) setMark() {
rr.mark = rr.pos
}
func (rr *runeReader) getMark() string {
return string(rr.data[rr.mark:rr.pos])
}
type comment struct {
tok ast.Token
isBlock bool
}
type protoLex struct {
input *runeReader
info *ast.FileInfo
handler *reporter.Handler
res *ast.FileNode
prevSym ast.TerminalNode
prevOffset int
eof ast.Token
prevLine, curLine int
maybeDonateComment int
comments []comment
}
var utf8Bom = []byte{0xEF, 0xBB, 0xBF}
func newLexer(in io.Reader, filename string, handler *reporter.Handler) (*protoLex, error) {
br := bufio.NewReader(in)
// if file has UTF8 byte order marker preface, consume it
marker, err := br.Peek(3)
if err == nil && bytes.Equal(marker, utf8Bom) {
_, _ = br.Discard(3)
}
contents, err := io.ReadAll(br)
if err != nil {
return nil, err
}
return &protoLex{
input: &runeReader{data: contents},
info: ast.NewFileInfo(filename, contents),
handler: handler,
}, nil
}
var keywords = map[string]int{
"syntax": _SYNTAX,
"edition": _EDITION,
"import": _IMPORT,
"weak": _WEAK,
"public": _PUBLIC,
"package": _PACKAGE,
"option": _OPTION,
"true": _TRUE,
"false": _FALSE,
"inf": _INF,
"nan": _NAN,
"repeated": _REPEATED,
"optional": _OPTIONAL,
"required": _REQUIRED,
"double": _DOUBLE,
"float": _FLOAT,
"int32": _INT32,
"int64": _INT64,
"uint32": _UINT32,
"uint64": _UINT64,
"sint32": _SINT32,
"sint64": _SINT64,
"fixed32": _FIXED32,
"fixed64": _FIXED64,
"sfixed32": _SFIXED32,
"sfixed64": _SFIXED64,
"bool": _BOOL,
"string": _STRING,
"bytes": _BYTES,
"group": _GROUP,
"oneof": _ONEOF,
"map": _MAP,
"extensions": _EXTENSIONS,
"to": _TO,
"max": _MAX,
"reserved": _RESERVED,
"enum": _ENUM,
"message": _MESSAGE,
"extend": _EXTEND,
"service": _SERVICE,
"rpc": _RPC,
"stream": _STREAM,
"returns": _RETURNS,
"export": _EXPORT,
"local": _LOCAL,
}
func (l *protoLex) maybeNewLine(r rune) {
if r == '\n' {
l.info.AddLine(l.input.offset())
l.curLine++
if len(l.comments) > 0 && l.comments[0].isBlock && l.maybeDonateComment > 0 {
// Newline after trailing block comment? Increment the signal that
// we may be able to donate comment to previous token.
l.maybeDonateComment++
}
}
}
func (l *protoLex) prev() ast.SourcePos {
return l.info.SourcePos(l.prevOffset)
}
func (l *protoLex) Lex(lval *protoSymType) int {
if l.handler.ReporterError() != nil {
// if error reporter already returned non-nil error,
// we can skip the rest of the input
return 0
}
l.comments = nil
for {
l.input.setMark()
l.prevOffset = l.input.offset()
c, _, err := l.input.readRune()
if err == io.EOF {
// we're not actually returning a rune, but this will associate
// accumulated comments as a trailing comment on last symbol
// (if appropriate)
l.setRune(lval, 0)
l.eof = lval.b.Token()
return 0
}
if err != nil {
l.setError(lval, err)
return _ERROR
}
if strings.ContainsRune("\n\r\t\f\v ", c) {
// skip whitespace
l.maybeNewLine(c)
continue
}
if c == '.' {
// decimal literals could start with a dot
cn, szn, err := l.input.readRune()
if err != nil {
l.setRune(lval, c)
return int(c)
}
if cn >= '0' && cn <= '9' {
l.readNumber()
token := l.input.getMark()
f, err := parseFloat(token)
if err != nil {
l.setError(lval, numError(err, "float", token))
return _ERROR
}
l.setFloat(lval, f)
return _FLOAT_LIT
}
l.input.unreadRune(szn)
l.setRune(lval, c)
return int(c)
}
if c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
// identifier
l.readIdentifier()
str := l.input.getMark()
if t, ok := keywords[str]; ok {
l.setIdent(lval, str)
return t
}
l.setIdent(lval, str)
return _NAME
}
if c >= '0' && c <= '9' {
// integer or float literal
l.readNumber()
token := l.input.getMark()
if strings.HasPrefix(token, "0x") || strings.HasPrefix(token, "0X") {
// hexadecimal
ui, err := strconv.ParseUint(token[2:], 16, 64)
if err != nil {
l.setError(lval, numError(err, "hexadecimal integer", token[2:]))
return _ERROR
}
l.setInt(lval, ui)
return _INT_LIT
}
if strings.ContainsAny(token, ".eE") {
// floating point!
f, err := parseFloat(token)
if err != nil {
l.setError(lval, numError(err, "float", token))
return _ERROR
}
l.setFloat(lval, f)
return _FLOAT_LIT
}
// integer! (decimal or octal)
base := 10
if token[0] == '0' {
base = 8
}
ui, err := strconv.ParseUint(token, base, 64)
if err != nil {
kind := "integer"
if base == 8 {
kind = "octal integer"
} else if numErr, ok := err.(*strconv.NumError); ok && numErr.Err == strconv.ErrRange {
// if it's too big to be an int, parse it as a float
var f float64
kind = "float"
f, err = parseFloat(token)
if err == nil {
l.setFloat(lval, f)
return _FLOAT_LIT
}
}
l.setError(lval, numError(err, kind, token))
return _ERROR
}
l.setInt(lval, ui)
return _INT_LIT
}
if c == '\'' || c == '"' {
// string literal
str, err := l.readStringLiteral(c)
if err != nil {
l.setError(lval, err)
return _ERROR
}
l.setString(lval, str)
return _STRING_LIT
}
if c == '/' {
// comment
cn, szn, err := l.input.readRune()
if err != nil {
l.setRune(lval, '/')
return int(c)
}
if cn == '/' {
startLine := l.curLine
if hasErr := l.skipToEndOfLineComment(lval); hasErr {
return _ERROR
}
l.addComment(false, startLine)
continue
}
if cn == '*' {
startLine := l.curLine
ok, hasErr := l.skipToEndOfBlockComment(lval)
if hasErr {
return _ERROR
}
if !ok {
l.setError(lval, errors.New("block comment never terminates, unexpected EOF"))
return _ERROR
}
l.addComment(true, startLine)
continue
}
l.input.unreadRune(szn)
}
if c < 32 || c == 127 {
l.setError(lval, errors.New("invalid control character"))
return _ERROR
}
if !strings.ContainsRune(";,.:=-+(){}[]<>/", c) {
l.setError(lval, errors.New("invalid character"))
return _ERROR
}
l.setRune(lval, c)
return int(c)
}
}
func parseFloat(token string) (float64, error) {
// strconv.ParseFloat allows _ to separate digits, but protobuf does not
if strings.ContainsRune(token, '_') {
return 0, &strconv.NumError{
Func: "parseFloat",
Num: token,
Err: strconv.ErrSyntax,
}
}
f, err := strconv.ParseFloat(token, 64)
if err == nil {
return f, nil
}
if numErr, ok := err.(*strconv.NumError); ok && numErr.Err == strconv.ErrRange && math.IsInf(f, 1) {
// protoc doesn't complain about float overflow and instead just uses "infinity"
// so we mirror that behavior by just returning infinity and ignoring the error
return f, nil
}
return f, err
}
func (l *protoLex) newToken() ast.Token {
offset := l.input.mark
length := l.input.pos - l.input.mark
return l.info.AddToken(offset, length)
}
func (l *protoLex) addComment(isBlock bool, startLine int) {
if len(l.comments) == 0 && startLine == l.prevLine {
l.maybeDonateComment++
}
l.comments = append(l.comments, comment{l.newToken(), isBlock})
}
func (l *protoLex) setPrevAndAddComments(n ast.TerminalNode) {
comments, maybeDonateComment := l.comments, l.maybeDonateComment
l.comments, l.maybeDonateComment = nil, 0
var prevTrailingComments []comment
if l.prevSym != nil && len(comments) > 0 {
cur := l.curLine
if cur == l.prevLine {
if rn, ok := n.(*ast.RuneNode); ok && rn.Rune == 0 {
// if current token is EOF, pretend it's on separate line
// so that the logic below can attribute a final trailing
// comment to the previous token
cur++
}
}
if cur > l.prevLine && maybeDonateComment > 0 {
// Comment starts right after the previous token. If it's a
// line comment, we record that as a trailing comment.
//
// But if it's a block comment, it is only a trailing comment
// if there are multiple comments or if the block comment ends
// on a line before n. This lattermost condition is signaled
// via l.maybeDonateComment > 1.
canDonate := !comments[0].isBlock ||
len(comments) > 1 || maybeDonateComment > 1
if canDonate {
prevTrailingComments = comments[:1]
comments = comments[1:]
}
}
}
// now we can associate comments
for _, c := range prevTrailingComments {
l.info.AddComment(c.tok, l.prevSym.Token())
}
for _, c := range comments {
l.info.AddComment(c.tok, n.Token())
}
l.prevSym = n
l.prevLine = l.curLine
}
func (l *protoLex) setString(lval *protoSymType, val string) {
lval.s = ast.NewStringLiteralNode(val, l.newToken())
l.setPrevAndAddComments(lval.s)
}
func (l *protoLex) setIdent(lval *protoSymType, val string) {
lval.id = ast.NewIdentNode(val, l.newToken())
l.setPrevAndAddComments(lval.id)
}
func (l *protoLex) setInt(lval *protoSymType, val uint64) {
lval.i = ast.NewUintLiteralNode(val, l.newToken())
l.setPrevAndAddComments(lval.i)
}
func (l *protoLex) setFloat(lval *protoSymType, val float64) {
lval.f = ast.NewFloatLiteralNode(val, l.newToken())
l.setPrevAndAddComments(lval.f)
}
func (l *protoLex) setRune(lval *protoSymType, val rune) {
lval.b = ast.NewRuneNode(val, l.newToken())
l.setPrevAndAddComments(lval.b)
}
func (l *protoLex) setError(lval *protoSymType, err error) {
lval.err, _ = l.addSourceError(err)
}
func (l *protoLex) readNumber() {
allowExpSign := false
for {
c, sz, err := l.input.readRune()
if err != nil {
break
}
if (c == '-' || c == '+') && !allowExpSign {
l.input.unreadRune(sz)
break
}
allowExpSign = false
if c != '.' && c != '_' && (c < '0' || c > '9') &&
(c < 'a' || c > 'z') && (c < 'A' || c > 'Z') &&
c != '-' && c != '+' {
// no more chars in the number token
l.input.unreadRune(sz)
break
}
if c == 'e' || c == 'E' {
// scientific notation char can be followed by
// an exponent sign
allowExpSign = true
}
}
}
func numError(err error, kind, s string) error {
ne, ok := err.(*strconv.NumError)
if !ok {
return err
}
if ne.Err == strconv.ErrRange {
return fmt.Errorf("value out of range for %s: %s", kind, s)
}
// syntax error
return fmt.Errorf("invalid syntax in %s value: %s", kind, s)
}
func (l *protoLex) readIdentifier() {
for {
c, sz, err := l.input.readRune()
if err != nil {
break
}
if c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') {
l.input.unreadRune(sz)
break
}
}
}
func (l *protoLex) readStringLiteral(quote rune) (string, error) {
var buf bytes.Buffer
var escapeError reporter.ErrorWithPos
var noMoreErrors bool
reportErr := func(msg, badEscape string) {
if noMoreErrors {
return
}
if escapeError != nil {
// report previous one
_, ok := l.addSourceError(escapeError)
if !ok {
noMoreErrors = true
}
}
var err error
if strings.HasSuffix(msg, "%s") {
err = fmt.Errorf(msg, badEscape)
} else {
err = errors.New(msg)
}
// we've now consumed the bad escape and lexer position is after it, so we need
// to back up to the beginning of the escape to report the correct position
escapeError = l.errWithCurrentPos(err, -len(badEscape))
}
for {
c, _, err := l.input.readRune()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return "", err
}
if c == '\n' {
return "", errors.New("encountered end-of-line before end of string literal")
}
if c == quote {
break
}
if c == 0 {
reportErr("null character ('\\0') not allowed in string literal", string(rune(0)))
continue
}
if c == '\\' {
// escape sequence
c, _, err = l.input.readRune()
if err != nil {
return "", err
}
switch {
case c == 'x' || c == 'X':
// hex escape
c1, sz1, err := l.input.readRune()
if err != nil {
return "", err
}
if c1 == quote || c1 == '\\' {
l.input.unreadRune(sz1)
reportErr("invalid hex escape: %s", "\\"+string(c))
continue
}
c2, sz2, err := l.input.readRune()
if err != nil {
return "", err
}
var hex string
if (c2 < '0' || c2 > '9') && (c2 < 'a' || c2 > 'f') && (c2 < 'A' || c2 > 'F') {
l.input.unreadRune(sz2)
hex = string(c1)
} else {
hex = string([]rune{c1, c2})
}
i, err := strconv.ParseInt(hex, 16, 32)
if err != nil {
reportErr("invalid hex escape: %s", "\\"+string(c)+hex)
continue
}
buf.WriteByte(byte(i))
case c >= '0' && c <= '7':
// octal escape
c2, sz2, err := l.input.readRune()
if err != nil {
return "", err
}
var octal string
if c2 < '0' || c2 > '7' {
l.input.unreadRune(sz2)
octal = string(c)
} else {
c3, sz3, err := l.input.readRune()
if err != nil {
return "", err
}
if c3 < '0' || c3 > '7' {
l.input.unreadRune(sz3)
octal = string([]rune{c, c2})
} else {
octal = string([]rune{c, c2, c3})
}
}
i, err := strconv.ParseInt(octal, 8, 32)
if err != nil {
reportErr("invalid octal escape: %s", "\\"+octal)
continue
}
if i > 0xff {
reportErr("octal escape is out range, must be between 0 and 377: %s", "\\"+octal)
continue
}
buf.WriteByte(byte(i))
case c == 'u':
// short unicode escape
u := make([]rune, 4)
for i := range u {
c2, sz2, err := l.input.readRune()
if err != nil {
return "", err
}
if c2 == quote || c2 == '\\' {
l.input.unreadRune(sz2)
u = u[:i]
break
}
u[i] = c2
}
codepointStr := string(u)
if len(u) < 4 {
reportErr("invalid unicode escape: %s", "\\u"+codepointStr)
continue
}
i, err := strconv.ParseInt(codepointStr, 16, 32)
if err != nil {
reportErr("invalid unicode escape: %s", "\\u"+codepointStr)
continue
}
buf.WriteRune(rune(i))
case c == 'U':
// long unicode escape
u := make([]rune, 8)
for i := range u {
c2, sz2, err := l.input.readRune()
if err != nil {
return "", err
}
if c2 == quote || c2 == '\\' {
l.input.unreadRune(sz2)
u = u[:i]
break
}
u[i] = c2
}
codepointStr := string(u)
if len(u) < 8 {
reportErr("invalid unicode escape: %s", "\\U"+codepointStr)
continue
}
i, err := strconv.ParseInt(string(u), 16, 32)
if err != nil {
reportErr("invalid unicode escape: %s", "\\U"+codepointStr)
continue
}
if i > 0x10ffff || i < 0 {
reportErr("unicode escape is out of range, must be between 0 and 0x10ffff: %s", "\\U"+codepointStr)
continue
}
buf.WriteRune(rune(i))
case c == 'a':
buf.WriteByte('\a')
case c == 'b':
buf.WriteByte('\b')
case c == 'f':
buf.WriteByte('\f')
case c == 'n':
buf.WriteByte('\n')
case c == 'r':
buf.WriteByte('\r')
case c == 't':
buf.WriteByte('\t')
case c == 'v':
buf.WriteByte('\v')
case c == '\\':
buf.WriteByte('\\')
case c == '\'':
buf.WriteByte('\'')
case c == '"':
buf.WriteByte('"')
case c == '?':
buf.WriteByte('?')
default:
reportErr("invalid escape sequence: %s", "\\"+string(c))
continue
}
} else {
buf.WriteRune(c)
}
}
if escapeError != nil {
return "", escapeError
}
return buf.String(), nil
}
func (l *protoLex) skipToEndOfLineComment(lval *protoSymType) (hasErr bool) {
for {
c, sz, err := l.input.readRune()
if err != nil {
// eof
return false
}
switch c {
case '\n':
// don't include newline in the comment
l.input.unreadRune(sz)
return false
case 0:
l.setError(lval, errors.New("invalid control character"))
return true
}
}
}
func (l *protoLex) skipToEndOfBlockComment(lval *protoSymType) (ok, hasErr bool) {
for {
c, _, err := l.input.readRune()
if err != nil {
return false, false
}
if c == 0 {
l.setError(lval, errors.New("invalid control character"))
return false, true
}
l.maybeNewLine(c)
if c == '*' {
c, sz, err := l.input.readRune()
if err != nil {
return false, false
}
if c == '/' {
return true, false
}
l.input.unreadRune(sz)
}
}
}
func (l *protoLex) addSourceError(err error) (reporter.ErrorWithPos, bool) {
ewp, ok := err.(reporter.ErrorWithPos)
if !ok {
// TODO: Store the previous span instead of just the position.
ewp = reporter.Error(ast.NewSourceSpan(l.prev(), l.prev()), err)
}
handlerErr := l.handler.HandleError(ewp)
return ewp, handlerErr == nil
}
func (l *protoLex) Error(s string) {
_, _ = l.addSourceError(errors.New(s))
}
// TODO: Accept both a start and end offset, and use that to create a span.
func (l *protoLex) errWithCurrentPos(err error, offset int) reporter.ErrorWithPos {
if ewp, ok := err.(reporter.ErrorWithPos); ok {
return ewp
}
pos := l.info.SourcePos(l.input.offset() + offset)
return reporter.Error(ast.NewSourceSpan(pos, pos), err)
}
func (l *protoLex) requireSemicolon(semicolons []*ast.RuneNode) (*ast.RuneNode, []*ast.RuneNode) {
if len(semicolons) == 0 {
l.Error("syntax error: expecting ';'")
return nil, nil
}
return semicolons[0], semicolons[1:]
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"io"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/reporter"
)
// The path ../.tmp/bin/goyacc is built when using `make generate` from repo root.
//go:generate ../.tmp/bin/goyacc -o proto.y.go -l -p proto proto.y
func init() {
protoErrorVerbose = true
// fix up the generated "token name" array so that error messages are nicer
setTokenName(_STRING_LIT, "string literal")
setTokenName(_INT_LIT, "int literal")
setTokenName(_FLOAT_LIT, "float literal")
setTokenName(_NAME, "identifier")
setTokenName(_ERROR, "error")
// for keywords, just show the keyword itself wrapped in quotes
for str, i := range keywords {
setTokenName(i, fmt.Sprintf(`"%s"`, str))
}
}
func setTokenName(token int, text string) {
// NB: this is based on logic in generated parse code that translates the
// int returned from the lexer into an internal token number.
var intern int8
if token < len(protoTok1) {
intern = protoTok1[token]
} else {
if token >= protoPrivate {
if token < protoPrivate+len(protoTok2) {
intern = protoTok2[token-protoPrivate]
}
}
if intern == 0 {
for i := 0; i+1 < len(protoTok3); i += 2 {
if int(protoTok3[i]) == token {
intern = protoTok3[i+1]
break
}
}
}
}
if intern >= 1 && int(intern-1) < len(protoToknames) {
protoToknames[intern-1] = text
return
}
panic(fmt.Sprintf("Unknown token value: %d", token))
}
// Parse parses the given source code info and returns an AST. The given filename
// is used to construct error messages and position information. The given reader
// supplies the source code. The given handler is used to report errors and
// warnings encountered while parsing. If any errors are reported, this function
// returns a non-nil error.
//
// If the error returned is due to a syntax error in the source, then a non-nil
// AST is also returned. If the handler chooses to not abort the parse (e.g. the
// underlying error reporter returns nil instead of an error), the parser will
// attempt to recover and keep going. This allows multiple syntax errors to be
// reported in a single pass. And it also means that more of the AST can be
// populated (erroneous productions around the syntax error will of course be
// absent).
//
// The degree to which the parser can recover from errors and populate the AST
// depends on the nature of the syntax error and if there are any tokens after the
// syntax error that can help the parser recover. This error recovery and partial
// AST production is best effort.
func Parse(filename string, r io.Reader, handler *reporter.Handler) (*ast.FileNode, error) {
lx, err := newLexer(r, filename, handler)
if err != nil {
return nil, err
}
protoParse(lx)
if lx.res == nil {
// nil AST means there was an error that prevented any parsing
// or the file was empty; synthesize empty non-nil AST
lx.res = ast.NewEmptyFileNode(filename)
}
return lx.res, handler.Error()
}
// Result is the result of constructing a descriptor proto from a parsed AST.
// From this result, the AST and the file descriptor proto can be had. This
// also contains numerous lookup functions, for looking up AST nodes that
// correspond to various elements of the descriptor hierarchy.
//
// Results can be created without AST information, using the ResultWithoutAST()
// function. All functions other than AST() will still return non-nil values,
// allowing compile operations to work with files that have only intermediate
// descriptor protos and no source code. For such results, the function that
// return AST nodes will return placeholder nodes. The position information for
// placeholder nodes contains only the filename.
type Result interface {
// AST returns the parsed abstract syntax tree. This returns nil if the
// Result was created without an AST.
AST() *ast.FileNode
// FileDescriptorProto returns the file descriptor proto.
FileDescriptorProto() *descriptorpb.FileDescriptorProto
// FileNode returns the root of the AST. If this result has no AST then a
// placeholder node is returned.
FileNode() ast.FileDeclNode
// Node returns the AST node from which the given message was created. This
// can return nil, such as if the given message is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
Node(proto.Message) ast.Node
// OptionNode returns the AST node corresponding to the given uninterpreted
// option. This can return nil, such as if the given option is not part of
// the FileDescriptorProto hierarchy. If this result has no AST, this
// returns a placeholder node.
OptionNode(*descriptorpb.UninterpretedOption) ast.OptionDeclNode
// OptionNamePartNode returns the AST node corresponding to the given name
// part for an uninterpreted option. This can return nil, such as if the
// given name part is not part of the FileDescriptorProto hierarchy. If this
// result has no AST, this returns a placeholder node.
OptionNamePartNode(*descriptorpb.UninterpretedOption_NamePart) ast.Node
// MessageNode returns the AST node corresponding to the given message. This
// can return nil, such as if the given message is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
MessageNode(*descriptorpb.DescriptorProto) ast.MessageDeclNode
// FieldNode returns the AST node corresponding to the given field. This can
// return nil, such as if the given field is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
FieldNode(*descriptorpb.FieldDescriptorProto) ast.FieldDeclNode
// OneofNode returns the AST node corresponding to the given oneof. This can
// return nil, such as if the given oneof is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
OneofNode(*descriptorpb.OneofDescriptorProto) ast.OneofDeclNode
// ExtensionRangeNode returns the AST node corresponding to the given
// extension range. This can return nil, such as if the given range is not
// part of the FileDescriptorProto hierarchy. If this result has no AST,
// this returns a placeholder node.
ExtensionRangeNode(*descriptorpb.DescriptorProto_ExtensionRange) ast.RangeDeclNode
// ExtensionsNode returns the AST node corresponding to the "extensions"
// statement in a message that corresponds to the given range. This will be
// the parent of the node returned by ExtensionRangeNode, which contains the
// options that apply to all child ranges.
ExtensionsNode(*descriptorpb.DescriptorProto_ExtensionRange) ast.NodeWithOptions
// MessageReservedRangeNode returns the AST node corresponding to the given
// reserved range. This can return nil, such as if the given range is not
// part of the FileDescriptorProto hierarchy. If this result has no AST,
// this returns a placeholder node.
MessageReservedRangeNode(*descriptorpb.DescriptorProto_ReservedRange) ast.RangeDeclNode
// EnumNode returns the AST node corresponding to the given enum. This can
// return nil, such as if the given enum is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
EnumNode(*descriptorpb.EnumDescriptorProto) ast.NodeWithOptions
// EnumValueNode returns the AST node corresponding to the given enum. This
// can return nil, such as if the given enum value is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
EnumValueNode(*descriptorpb.EnumValueDescriptorProto) ast.EnumValueDeclNode
// EnumReservedRangeNode returns the AST node corresponding to the given
// reserved range. This can return nil, such as if the given range is not
// part of the FileDescriptorProto hierarchy. If this result has no AST,
// this returns a placeholder node.
EnumReservedRangeNode(*descriptorpb.EnumDescriptorProto_EnumReservedRange) ast.RangeDeclNode
// ServiceNode returns the AST node corresponding to the given service. This
// can return nil, such as if the given service is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
ServiceNode(*descriptorpb.ServiceDescriptorProto) ast.NodeWithOptions
// MethodNode returns the AST node corresponding to the given method. This
// can return nil, such as if the given method is not part of the
// FileDescriptorProto hierarchy. If this result has no AST, this returns a
// placeholder node.
MethodNode(*descriptorpb.MethodDescriptorProto) ast.RPCDeclNode
}
// 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 goyacc -o proto.y.go -l -p proto proto.y. DO NOT EDIT.
package parser
import __yyfmt__ "fmt"
//lint:file-ignore SA4006 generated parser has unused values
import (
"math"
"strings"
"github.com/bufbuild/protocompile/ast"
)
type protoSymType struct {
yys int
file *ast.FileNode
syn *ast.SyntaxNode
ed *ast.EditionNode
fileElements []ast.FileElement
pkg nodeWithRunes[*ast.PackageNode]
imprt nodeWithRunes[*ast.ImportNode]
msg nodeWithRunes[*ast.MessageNode]
msgElements []ast.MessageElement
fld *ast.FieldNode
msgFld nodeWithRunes[*ast.FieldNode]
mapFld nodeWithRunes[*ast.MapFieldNode]
mapType *ast.MapTypeNode
grp *ast.GroupNode
msgGrp nodeWithRunes[*ast.GroupNode]
oo nodeWithRunes[*ast.OneofNode]
ooElement ast.OneofElement
ooElements []ast.OneofElement
ext nodeWithRunes[*ast.ExtensionRangeNode]
resvd nodeWithRunes[*ast.ReservedNode]
en nodeWithRunes[*ast.EnumNode]
enElements []ast.EnumElement
env nodeWithRunes[*ast.EnumValueNode]
extend nodeWithRunes[*ast.ExtendNode]
extElement ast.ExtendElement
extElements []ast.ExtendElement
svc nodeWithRunes[*ast.ServiceNode]
svcElements []ast.ServiceElement
mtd nodeWithRunes[*ast.RPCNode]
mtdMsgType *ast.RPCTypeNode
mtdElements []ast.RPCElement
optRaw *ast.OptionNode
opt nodeWithRunes[*ast.OptionNode]
opts *compactOptionSlices
refRaw *ast.FieldReferenceNode
ref nodeWithRunes[*ast.FieldReferenceNode]
optNms *fieldRefSlices
cmpctOpts *ast.CompactOptionsNode
rng *ast.RangeNode
rngs *rangeSlices
names *nameSlices
cidPart nodeWithRunes[*ast.IdentNode]
cid *identSlices
tid ast.IdentValueNode
sl *valueSlices
msgLitFlds *messageFieldList
msgLitFld *ast.MessageFieldNode
v ast.ValueNode
il ast.IntValueNode
str []*ast.StringLiteralNode
s *ast.StringLiteralNode
i *ast.UintLiteralNode
f *ast.FloatLiteralNode
id *ast.IdentNode
b *ast.RuneNode
bs []*ast.RuneNode
err error
}
const _STRING_LIT = 57346
const _INT_LIT = 57347
const _FLOAT_LIT = 57348
const _NAME = 57349
const _SYNTAX = 57350
const _EDITION = 57351
const _IMPORT = 57352
const _WEAK = 57353
const _PUBLIC = 57354
const _PACKAGE = 57355
const _OPTION = 57356
const _TRUE = 57357
const _FALSE = 57358
const _INF = 57359
const _NAN = 57360
const _REPEATED = 57361
const _OPTIONAL = 57362
const _REQUIRED = 57363
const _DOUBLE = 57364
const _FLOAT = 57365
const _INT32 = 57366
const _INT64 = 57367
const _UINT32 = 57368
const _UINT64 = 57369
const _SINT32 = 57370
const _SINT64 = 57371
const _FIXED32 = 57372
const _FIXED64 = 57373
const _SFIXED32 = 57374
const _SFIXED64 = 57375
const _BOOL = 57376
const _STRING = 57377
const _BYTES = 57378
const _GROUP = 57379
const _ONEOF = 57380
const _MAP = 57381
const _EXTENSIONS = 57382
const _TO = 57383
const _MAX = 57384
const _RESERVED = 57385
const _ENUM = 57386
const _MESSAGE = 57387
const _EXTEND = 57388
const _SERVICE = 57389
const _RPC = 57390
const _STREAM = 57391
const _RETURNS = 57392
const _EXPORT = 57393
const _LOCAL = 57394
const _ERROR = 57395
var protoToknames = [...]string{
"$end",
"error",
"$unk",
"_STRING_LIT",
"_INT_LIT",
"_FLOAT_LIT",
"_NAME",
"_SYNTAX",
"_EDITION",
"_IMPORT",
"_WEAK",
"_PUBLIC",
"_PACKAGE",
"_OPTION",
"_TRUE",
"_FALSE",
"_INF",
"_NAN",
"_REPEATED",
"_OPTIONAL",
"_REQUIRED",
"_DOUBLE",
"_FLOAT",
"_INT32",
"_INT64",
"_UINT32",
"_UINT64",
"_SINT32",
"_SINT64",
"_FIXED32",
"_FIXED64",
"_SFIXED32",
"_SFIXED64",
"_BOOL",
"_STRING",
"_BYTES",
"_GROUP",
"_ONEOF",
"_MAP",
"_EXTENSIONS",
"_TO",
"_MAX",
"_RESERVED",
"_ENUM",
"_MESSAGE",
"_EXTEND",
"_SERVICE",
"_RPC",
"_STREAM",
"_RETURNS",
"_EXPORT",
"_LOCAL",
"_ERROR",
"'='",
"';'",
"':'",
"'{'",
"'}'",
"'\\\\'",
"'/'",
"'?'",
"'.'",
"','",
"'>'",
"'<'",
"'+'",
"'-'",
"'('",
"')'",
"'['",
"']'",
"'*'",
"'&'",
"'^'",
"'%'",
"'$'",
"'#'",
"'@'",
"'!'",
"'~'",
"'`'",
}
var protoStatenames = [...]string{}
const protoEofCode = 1
const protoErrCode = 2
const protoInitialStackSize = 16
var protoExca = [...]int16{
-1, 0,
1, 6,
-2, 21,
-1, 1,
1, -1,
-2, 0,
-1, 2,
1, 1,
-2, 21,
-1, 3,
1, 2,
-2, 21,
-1, 14,
1, 7,
-2, 0,
-1, 94,
54, 61,
63, 61,
71, 61,
-2, 62,
-1, 110,
57, 38,
60, 38,
64, 38,
69, 38,
71, 38,
-2, 35,
-1, 122,
54, 61,
63, 61,
71, 61,
-2, 63,
-1, 132,
58, 266,
-2, 0,
-1, 135,
57, 38,
60, 38,
64, 38,
69, 38,
71, 38,
-2, 36,
-1, 155,
58, 232,
-2, 0,
-1, 161,
58, 219,
-2, 0,
-1, 163,
58, 267,
-2, 0,
-1, 219,
58, 279,
-2, 0,
-1, 224,
58, 84,
64, 84,
-2, 0,
-1, 235,
58, 233,
-2, 0,
-1, 298,
58, 220,
-2, 0,
-1, 408,
58, 280,
-2, 0,
-1, 436,
55, 546,
-2, 629,
-1, 437,
55, 547,
-2, 630,
-1, 441,
54, 592,
70, 592,
-2, 548,
-1, 442,
54, 593,
70, 593,
-2, 549,
-1, 443,
54, 594,
70, 594,
-2, 550,
-1, 444,
54, 595,
70, 595,
-2, 551,
-1, 445,
54, 596,
70, 596,
-2, 552,
-1, 446,
54, 597,
70, 597,
-2, 553,
-1, 447,
54, 598,
70, 598,
-2, 554,
-1, 448,
54, 599,
70, 599,
-2, 555,
-1, 449,
54, 600,
70, 600,
-2, 556,
-1, 450,
54, 601,
70, 601,
-2, 557,
-1, 451,
54, 602,
70, 602,
-2, 558,
-1, 452,
54, 603,
70, 603,
-2, 559,
-1, 453,
54, 604,
70, 604,
-2, 560,
-1, 454,
54, 605,
70, 605,
-2, 561,
-1, 455,
54, 606,
70, 606,
-2, 562,
-1, 456,
54, 607,
70, 607,
-2, 563,
-1, 457,
54, 608,
70, 608,
-2, 564,
-1, 458,
54, 609,
70, 609,
-2, 565,
-1, 459,
54, 610,
70, 610,
-2, 566,
-1, 460,
54, 611,
70, 611,
-2, 567,
-1, 461,
54, 612,
70, 612,
-2, 568,
-1, 462,
54, 613,
70, 613,
-2, 569,
-1, 463,
54, 614,
70, 614,
-2, 570,
-1, 464,
54, 615,
70, 615,
-2, 571,
-1, 465,
54, 616,
70, 616,
-2, 572,
-1, 466,
54, 617,
70, 617,
-2, 573,
-1, 467,
54, 618,
70, 618,
-2, 574,
-1, 468,
54, 619,
70, 619,
-2, 575,
-1, 469,
54, 620,
70, 620,
-2, 576,
-1, 470,
54, 621,
70, 621,
-2, 577,
-1, 471,
54, 622,
70, 622,
-2, 578,
-1, 472,
54, 623,
70, 623,
-2, 579,
-1, 473,
54, 624,
70, 624,
-2, 580,
-1, 474,
54, 625,
70, 625,
-2, 581,
-1, 475,
54, 626,
70, 626,
-2, 582,
-1, 476,
54, 627,
70, 627,
-2, 583,
-1, 477,
54, 628,
70, 628,
-2, 584,
-1, 478,
54, 631,
70, 631,
-2, 585,
-1, 479,
54, 632,
70, 632,
-2, 586,
-1, 480,
54, 633,
70, 633,
-2, 587,
-1, 481,
54, 634,
70, 634,
-2, 588,
-1, 482,
54, 635,
70, 635,
-2, 589,
-1, 483,
54, 636,
70, 636,
-2, 590,
-1, 484,
54, 637,
70, 637,
-2, 591,
-1, 486,
55, 546,
-2, 629,
-1, 487,
55, 547,
-2, 630,
-1, 565,
58, 158,
-2, 0,
-1, 628,
71, 147,
-2, 144,
-1, 640,
58, 159,
-2, 0,
-1, 718,
69, 53,
-2, 50,
-1, 778,
71, 147,
-2, 145,
-1, 807,
69, 53,
-2, 51,
-1, 851,
58, 290,
-2, 0,
-1, 864,
58, 291,
-2, 0,
}
const protoPrivate = 57344
const protoLast = 2259
var protoAct = [...]int16{
155, 7, 865, 7, 7, 528, 154, 18, 531, 530,
142, 527, 641, 607, 105, 425, 715, 707, 718, 629,
601, 625, 34, 36, 104, 628, 512, 42, 513, 221,
492, 8, 409, 109, 38, 526, 549, 256, 299, 115,
491, 439, 440, 119, 21, 20, 90, 223, 355, 116,
117, 118, 164, 107, 19, 168, 160, 91, 110, 43,
95, 98, 776, 236, 103, 94, 111, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
87, 88, 89, 540, 543, 421, 149, 138, 139, 140,
768, 133, 125, 766, 426, 542, 95, 709, 97, 427,
146, 145, 161, 127, 128, 129, 130, 144, 219, 613,
822, 709, 550, 220, 825, 765, 544, 826, 858, 615,
123, 602, 616, 764, 141, 148, 550, 161, 122, 161,
550, 550, 134, 243, 294, 550, 296, 135, 149, 300,
618, 9, 612, 566, 9, 9, 426, 561, 547, 9,
611, 836, 539, 228, 810, 563, 550, 804, 514, 550,
550, 797, 550, 557, 550, 153, 552, 550, 514, 306,
240, 238, 406, 404, 550, 503, 149, 830, 43, 550,
239, 248, 550, 293, 426, 295, 352, 610, 419, 420,
594, 9, 9, 569, 417, 704, 418, 410, 593, 573,
571, 563, 416, 423, 779, 430, 698, 120, 808, 9,
791, 519, 501, 243, 428, 872, 120, 405, 136, 124,
515, 710, 879, 877, 873, 869, 863, 862, 228, 23,
515, 860, 852, 848, 840, 499, 9, 24, 835, 847,
25, 26, 415, 812, 785, 507, 506, 505, 504, 414,
240, 238, 407, 434, 438, 488, 502, 494, 495, 500,
239, 248, 43, 489, 490, 351, 496, 433, 508, 431,
297, 30, 27, 31, 32, 234, 300, 838, 28, 29,
832, 771, 565, 159, 113, 499, 158, 157, 156, 137,
132, 131, 126, 5, 6, 113, 113, 9, 709, 787,
33, 353, 821, 780, 598, 597, 306, 509, 521, 500,
510, 121, 13, 12, 412, 101, 102, 99, 100, 637,
595, 35, 516, 564, 231, 230, 26, 867, 39, 40,
15, 41, 845, 843, 772, 9, 232, 233, 769, 26,
9, 706, 705, 850, 693, 37, 114, 112, 493, 632,
630, 621, 600, 596, 113, 520, 638, 35, 517, 518,
413, 43, 576, 577, 578, 579, 580, 581, 582, 583,
584, 585, 586, 587, 864, 411, 4, 218, 523, 10,
11, 408, 22, 162, 163, 301, 410, 298, 241, 511,
302, 246, 498, 497, 639, 640, 235, 254, 245, 242,
644, 166, 244, 541, 165, 548, 643, 237, 225, 522,
554, 524, 224, 538, 604, 712, 647, 525, 169, 249,
716, 108, 713, 356, 533, 533, 536, 535, 649, 173,
228, 257, 304, 551, 717, 358, 545, 546, 651, 43,
127, 128, 555, 175, 260, 553, 575, 422, 424, 529,
147, 143, 92, 93, 227, 96, 626, 623, 642, 627,
559, 17, 16, 14, 3, 2, 567, 1, 570, 572,
0, 0, 0, 0, 0, 588, 589, 590, 591, 0,
0, 0, 0, 556, 0, 0, 0, 0, 0, 43,
129, 130, 560, 0, 0, 558, 0, 0, 562, 0,
0, 0, 568, 0, 0, 574, 0, 0, 0, 592,
609, 0, 0, 0, 0, 0, 608, 619, 0, 0,
622, 0, 0, 599, 631, 0, 0, 0, 614, 633,
0, 0, 634, 635, 0, 0, 603, 0, 533, 694,
695, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 696, 617, 620, 95, 0, 0, 0, 0, 0,
0, 0, 0, 699, 636, 702, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 697, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 767, 0,
701, 773, 774, 770, 703, 0, 0, 0, 0, 700,
0, 781, 0, 783, 0, 0, 0, 708, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 777, 0,
0, 0, 778, 786, 0, 0, 0, 0, 775, 0,
95, 0, 0, 0, 0, 0, 788, 782, 0, 784,
0, 0, 0, 0, 0, 0, 0, 0, 792, 0,
95, 789, 790, 0, 43, 0, 0, 0, 0, 0,
0, 0, 0, 0, 793, 0, 0, 0, 0, 0,
0, 796, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 795, 799, 0, 801, 0, 0, 805,
794, 802, 0, 806, 0, 807, 803, 0, 0, 0,
0, 0, 798, 800, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 609, 0, 814, 0, 0, 816, 608, 813, 809,
0, 0, 0, 818, 0, 819, 820, 0, 0, 146,
145, 817, 0, 0, 0, 829, 144, 0, 0, 533,
0, 0, 0, 834, 831, 0, 811, 823, 0, 815,
0, 0, 0, 839, 148, 0, 841, 837, 833, 0,
0, 0, 0, 0, 0, 828, 824, 827, 0, 0,
146, 145, 842, 846, 0, 0, 851, 144, 0, 849,
0, 854, 844, 0, 0, 853, 0, 0, 0, 0,
0, 0, 0, 868, 861, 148, 0, 0, 0, 866,
855, 856, 0, 0, 874, 871, 0, 875, 0, 0,
876, 0, 866, 0, 0, 870, 0, 0, 0, 878,
857, 606, 859, 35, 152, 150, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
68, 69, 70, 71, 72, 73, 74, 75, 76, 77,
78, 79, 80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 0, 0, 0, 0, 149, 0, 0, 0,
0, 0, 0, 0, 426, 0, 532, 0, 0, 0,
605, 35, 152, 150, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
0, 0, 0, 0, 149, 0, 0, 0, 0, 0,
0, 0, 426, 0, 532, 0, 0, 534, 35, 152,
150, 44, 45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
83, 84, 85, 86, 87, 88, 89, 0, 0, 0,
0, 149, 0, 0, 0, 0, 0, 0, 0, 426,
0, 532, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 226, 97, 0, 0, 624, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
87, 88, 89, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 537, 0, 226, 0, 0, 0,
229, 44, 45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
83, 84, 85, 86, 87, 88, 89, 0, 0, 0,
0, 0, 222, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 229, 35, 152, 150, 44, 45,
46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75,
76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
86, 87, 88, 89, 0, 0, 0, 0, 149, 0,
0, 0, 0, 0, 226, 0, 0, 0, 151, 44,
45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
35, 514, 229, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
81, 82, 83, 84, 85, 86, 87, 88, 89, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 515, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
0, 0, 0, 0, 0, 0, 0, 0, 0, 106,
719, 720, 721, 722, 723, 724, 725, 726, 727, 728,
729, 730, 731, 732, 733, 734, 735, 736, 737, 738,
739, 740, 741, 742, 743, 744, 745, 746, 747, 748,
749, 750, 751, 752, 753, 754, 755, 756, 757, 758,
759, 760, 711, 761, 762, 763, 0, 0, 0, 0,
0, 0, 0, 0, 0, 714, 441, 442, 443, 444,
445, 446, 447, 448, 449, 450, 451, 452, 453, 454,
455, 456, 457, 458, 459, 460, 461, 462, 463, 464,
465, 466, 467, 468, 469, 470, 471, 472, 473, 474,
475, 476, 477, 486, 487, 478, 479, 480, 481, 482,
483, 484, 0, 0, 0, 0, 0, 0, 0, 0,
0, 485, 441, 442, 443, 444, 445, 446, 447, 448,
449, 450, 451, 452, 453, 454, 455, 456, 457, 458,
459, 460, 461, 462, 463, 464, 465, 466, 467, 468,
469, 470, 471, 472, 473, 474, 475, 476, 477, 436,
437, 478, 479, 480, 481, 482, 483, 484, 0, 0,
0, 0, 0, 0, 0, 0, 0, 435, 359, 360,
361, 362, 363, 364, 365, 366, 367, 368, 369, 370,
371, 372, 373, 374, 375, 376, 377, 378, 379, 380,
381, 382, 383, 384, 385, 386, 387, 388, 432, 389,
390, 391, 392, 393, 394, 395, 396, 397, 398, 399,
400, 401, 402, 403, 0, 0, 0, 0, 0, 247,
0, 0, 0, 357, 261, 262, 263, 264, 265, 266,
267, 26, 268, 269, 270, 271, 172, 171, 170, 272,
273, 274, 275, 276, 277, 278, 279, 280, 281, 282,
283, 284, 285, 286, 0, 253, 259, 252, 287, 288,
255, 30, 27, 31, 289, 290, 291, 292, 250, 251,
0, 0, 0, 0, 0, 0, 0, 0, 0, 258,
359, 360, 361, 362, 363, 364, 365, 366, 367, 368,
369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
379, 380, 381, 382, 383, 384, 385, 386, 387, 388,
354, 389, 390, 391, 392, 393, 394, 395, 396, 397,
398, 399, 400, 401, 402, 403, 0, 0, 0, 0,
0, 167, 0, 0, 0, 357, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 172, 171,
170, 188, 189, 190, 191, 192, 193, 194, 195, 196,
197, 198, 199, 200, 201, 202, 0, 203, 204, 205,
206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
216, 217, 0, 0, 0, 0, 0, 645, 0, 0,
0, 174, 652, 653, 654, 655, 656, 657, 658, 646,
659, 660, 661, 662, 0, 0, 0, 663, 664, 665,
666, 667, 668, 669, 670, 671, 672, 673, 674, 675,
676, 677, 648, 678, 679, 680, 681, 682, 683, 684,
685, 686, 687, 688, 689, 690, 691, 692, 0, 0,
0, 0, 0, 0, 0, 0, 0, 650, 231, 230,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 35, 493, 0, 44,
45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 303, 0, 0, 0, 0,
307, 308, 309, 310, 311, 312, 313, 26, 314, 315,
316, 317, 318, 319, 320, 321, 322, 323, 324, 325,
326, 327, 328, 329, 330, 331, 332, 333, 334, 335,
336, 337, 338, 339, 340, 341, 305, 342, 343, 344,
345, 346, 347, 348, 349, 350, 429, 0, 0, 0,
0, 44, 45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
83, 84, 85, 86, 87, 88, 89, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
87, 88, 89, 719, 720, 721, 722, 723, 724, 725,
726, 727, 728, 729, 730, 731, 732, 733, 734, 735,
736, 737, 738, 739, 740, 741, 742, 743, 744, 745,
746, 747, 748, 749, 750, 751, 752, 753, 754, 755,
756, 757, 758, 759, 760, 0, 761, 762, 763,
}
var protoPact = [...]int16{
315, -1000, 272, 272, -1000, 289, 288, 257, 275, -1000,
-1000, -1000, 383, 383, 257, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, 347, 2160, 60, 2160, 303, 301,
2160, 1457, 2160, -1000, 322, -1000, 321, -1000, 310, 383,
383, 383, 184, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
287, -1000, 60, 187, -1000, -1000, -1000, 1457, 265, 2160,
2160, 2160, 2160, 264, 263, -1000, 2160, -1000, 2160, 186,
-1000, 262, -1000, -1000, -1000, -1000, 310, 310, 310, -1000,
2160, 1271, -1000, -1000, -1000, 126, 272, 261, 260, 259,
256, 272, 1849, -1000, -1000, -1000, -1000, 272, -1000, -1000,
-1000, -1000, 272, -1000, -1000, 380, -1000, -1000, -1000, 1204,
-1000, 349, -1000, -1000, 247, 1737, 272, 272, 272, 272,
242, 2063, 237, 1849, -1000, -1000, -1000, 276, 1793, 2160,
-1000, -1000, -1000, 185, 2160, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 224, 342,
-1000, 221, -1000, -1000, 1332, 169, 163, 59, -1000, 2114,
-1000, -1000, -1000, -1000, 272, 1737, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 1681, 2160,
1625, 1569, 373, 2160, 2160, 2012, -1000, 180, 2160, 140,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, 220, 219, 218, 217, 272, 2063, -1000,
-1000, -1000, -1000, -1000, 286, 1396, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, 272, -1000, -1000, 2160, 2160, 179, 2160, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, 284, 2160, 175, 272, 342, -1000,
-1000, -1000, -1000, 2160, -1000, -1000, -1000, -1000, -1000, -1000,
947, 947, -1000, -1000, -1000, -1000, 1140, 111, 54, 75,
-1000, -1000, 2160, 2160, 124, 2160, 2160, 2160, 142, 272,
272, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, 2160, 2160, 2160, 139, 272,
272, 122, -1000, 312, 255, 119, 168, 167, 166, 380,
-1000, 2160, 175, 368, 272, 272, 272, 272, -1000, -1000,
193, 165, -1000, 309, -1000, 378, -1000, 281, 280, 2160,
175, 377, -1000, -1000, -1000, 83, -1000, -1000, -1000, -1000,
380, -1000, 1963, -1000, 879, -1000, 153, -1000, 109, -1000,
68, -1000, -1000, 2160, -1000, 95, 116, 376, -1000, 272,
1075, 175, 375, 272, -1000, 275, 175, 374, 272, -1000,
275, 272, 272, 373, 344, 1905, 369, -1000, 272, 272,
-1000, 383, -1000, 2160, -1000, 173, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, 120, 193, 272, 183, -1000, 367, 366, -1000,
72, 201, 1513, -1000, 82, -1000, 52, -1000, -1000, -1000,
-1000, -1000, 149, -1000, 49, 363, 272, 254, 359, -1000,
272, 120, -1000, -9, -1000, -1000, 60, 171, -1000, 279,
120, -1000, 120, -1000, -1000, -1000, -1000, -1000, -1000, 216,
1905, -1000, -1000, -1000, -1000, 274, 60, 2160, 2160, 178,
2160, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, 120, -1000, -1000, 380, -1000, 1457, -1000,
272, -1000, -1000, -1000, -1000, 134, 72, -1000, 273, -1000,
83, 1457, 118, -1000, 2160, -1000, 2206, 176, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000,
-1000, -1000, -1000, -1000, -1000, 1014, -1000, -1000, -1000, 127,
215, 272, 120, -1000, -1000, 272, -1000, -1000, -1000, -1000,
1271, -1000, 272, -1000, 272, 272, -1000, -1000, 278, 86,
90, 2160, 175, -1000, 272, 143, -1000, 272, 253, -1000,
273, -1000, 211, 112, -1000, -1000, -1000, -1000, -1000, -1000,
272, 250, 272, 206, -1000, 272, -1000, -1000, -1000, -1000,
-1000, 1271, 358, -1000, 273, 357, 272, 212, -1000, -1000,
-1000, 205, 272, -1000, -1000, 272, -1000, 204, 272, -1000,
272, -1000, 273, 72, -1000, 91, 203, 272, -1000, 199,
198, 355, 272, 197, -1000, -1000, -1000, 273, 272, 188,
-1000, 196, -1000, 272, 355, -1000, -1000, -1000, -1000, 272,
-1000, 195, 272, -1000, -1000, -1000, -1000, -1000, 194, -1000,
}
var protoPgo = [...]int16{
0, 497, 495, 494, 406, 360, 493, 492, 491, 489,
488, 7, 25, 21, 487, 486, 485, 484, 483, 65,
57, 19, 482, 36, 35, 10, 481, 11, 15, 5,
8, 480, 479, 13, 478, 477, 28, 33, 476, 474,
473, 468, 465, 464, 462, 55, 42, 41, 58, 53,
18, 16, 27, 461, 459, 458, 453, 452, 14, 451,
450, 24, 449, 448, 446, 48, 445, 444, 443, 442,
47, 29, 438, 437, 436, 434, 432, 431, 430, 429,
428, 427, 54, 63, 426, 6, 12, 425, 424, 423,
422, 421, 420, 37, 30, 26, 40, 419, 418, 45,
38, 417, 56, 415, 44, 52, 414, 413, 9, 412,
32, 411, 407, 405, 2, 404, 373, 20, 17, 0,
31,
}
var protoR1 = [...]int8{
0, 1, 1, 1, 1, 1, 1, 4, 6, 6,
5, 5, 5, 5, 5, 5, 5, 5, 120, 120,
119, 119, 118, 118, 2, 3, 7, 7, 7, 7,
8, 52, 52, 58, 58, 59, 59, 49, 49, 48,
53, 53, 54, 54, 55, 55, 56, 56, 57, 57,
60, 60, 51, 51, 50, 10, 11, 18, 18, 19,
20, 20, 22, 22, 21, 21, 16, 25, 25, 26,
26, 26, 26, 30, 30, 30, 30, 31, 31, 108,
108, 28, 28, 71, 70, 70, 69, 69, 69, 69,
69, 69, 72, 72, 72, 17, 17, 17, 17, 24,
24, 24, 27, 27, 27, 27, 35, 35, 29, 29,
29, 32, 32, 32, 67, 67, 33, 33, 34, 34,
34, 68, 68, 61, 61, 62, 62, 62, 62, 63,
63, 64, 64, 65, 65, 66, 66, 45, 45, 45,
23, 23, 14, 14, 15, 15, 13, 13, 12, 9,
9, 77, 77, 79, 79, 79, 79, 76, 88, 88,
87, 87, 86, 86, 86, 86, 86, 74, 74, 74,
74, 78, 78, 78, 78, 80, 80, 80, 80, 81,
38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
38, 38, 98, 98, 96, 96, 94, 94, 94, 97,
97, 95, 95, 95, 36, 36, 91, 91, 92, 92,
93, 93, 89, 89, 90, 90, 99, 99, 99, 102,
102, 101, 101, 100, 100, 100, 100, 103, 103, 82,
82, 82, 85, 85, 84, 84, 83, 83, 83, 83,
83, 83, 83, 83, 83, 83, 83, 73, 73, 73,
73, 73, 73, 73, 73, 73, 73, 73, 73, 73,
73, 73, 73, 73, 73, 104, 107, 107, 106, 106,
105, 105, 105, 105, 75, 75, 75, 75, 109, 112,
112, 111, 111, 110, 110, 110, 113, 113, 117, 117,
116, 116, 115, 115, 114, 114, 39, 39, 39, 39,
39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
39, 39, 39, 39, 39, 39, 39, 39, 39, 40,
40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
40, 44, 44, 44, 44, 44, 44, 44, 44, 44,
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
44, 44, 44, 44, 44, 41, 41, 41, 41, 41,
41, 41, 41, 41, 41, 41, 41, 41, 41, 41,
41, 41, 41, 41, 41, 41, 41, 41, 41, 41,
41, 41, 41, 41, 41, 41, 41, 41, 41, 41,
41, 41, 41, 41, 41, 41, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 46, 46, 47, 47,
47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
47, 47, 37, 37, 37, 37, 37, 37, 37, 37,
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
37, 37, 37, 37, 37, 37, 37, 37,
}
var protoR2 = [...]int8{
0, 1, 1, 1, 2, 2, 0, 2, 2, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 0, 1, 0, 4, 4, 3, 4, 4, 4,
3, 1, 3, 1, 2, 1, 2, 1, 1, 2,
1, 3, 1, 3, 1, 3, 1, 3, 1, 2,
1, 2, 1, 1, 2, 5, 5, 1, 1, 2,
1, 1, 1, 2, 1, 2, 3, 1, 1, 1,
1, 1, 1, 1, 2, 1, 2, 2, 2, 1,
2, 3, 2, 1, 1, 2, 1, 2, 2, 2,
2, 1, 3, 2, 3, 1, 3, 5, 3, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
2, 3, 2, 3, 1, 3, 1, 1, 3, 2,
3, 1, 3, 1, 2, 1, 3, 3, 2, 1,
2, 1, 2, 1, 2, 1, 2, 1, 1, 1,
3, 2, 1, 2, 1, 2, 1, 1, 2, 3,
1, 8, 9, 9, 10, 7, 8, 6, 0, 1,
2, 1, 1, 1, 1, 2, 1, 5, 6, 3,
4, 7, 8, 5, 6, 5, 6, 3, 4, 6,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 4, 4, 1, 3, 1, 3, 3, 1,
3, 1, 3, 3, 1, 2, 4, 1, 4, 1,
3, 3, 1, 3, 1, 3, 6, 7, 7, 1,
2, 2, 1, 1, 1, 1, 1, 4, 5, 6,
7, 7, 1, 2, 2, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 6, 7, 5,
6, 4, 5, 3, 4, 5, 6, 4, 5, 6,
4, 3, 3, 3, 3, 6, 0, 1, 2, 1,
1, 1, 2, 1, 6, 7, 5, 6, 6, 1,
2, 2, 1, 1, 1, 1, 6, 9, 4, 3,
1, 2, 2, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
}
var protoChk = [...]int16{
-1000, -1, -2, -3, -4, 8, 9, -119, -120, 55,
-4, -4, 54, 54, -6, -5, -7, -8, -11, -82,
-99, -104, -109, 2, 10, 13, 14, 45, 51, 52,
44, 46, 47, 55, -108, 4, -108, -5, -108, 11,
12, 14, -52, -37, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
-21, -20, -22, -18, -19, -37, -16, 68, -37, 44,
45, 44, 45, -37, -61, -58, 62, -49, -59, -37,
-48, -37, 55, 4, 55, -119, -108, -108, -108, -119,
62, 54, -19, -20, 62, -61, 57, -37, -37, -37,
-37, 57, 57, -58, -49, -48, 62, 57, -119, -119,
-119, -37, -25, -26, -28, -108, -30, -31, -37, 57,
6, 67, 5, 69, -85, -119, 57, 57, 57, 57,
-102, -119, -107, -106, -105, -75, -77, 2, -45, -63,
21, 20, 19, -54, 62, -40, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52, -112, -119,
-119, -71, 58, -70, -69, -72, 2, -17, -37, 70,
6, 5, 17, 18, 58, -84, -83, -73, -99, -82,
-104, -98, -79, -11, -76, -80, -91, 2, -45, -62,
51, 52, 40, 38, -81, 43, -93, -53, 62, 39,
-39, 7, 8, 9, 10, 11, 12, 13, 15, 16,
17, 18, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36, 41, 42, 47,
48, 49, 50, -102, -85, -102, -85, 58, -101, -100,
-11, -103, -92, 2, -44, 43, -93, 7, 8, 9,
10, 11, 12, 13, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 44, 45, 46, 47, 48, 49, 50, 51,
52, 58, -105, 55, 37, -65, -56, 62, -42, 7,
8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, -37, 62, -52, 58, -111, -110,
-11, -113, 2, 48, 58, -70, 63, 55, 63, 55,
56, 56, -35, -29, -34, -28, 65, 70, -58, 2,
-119, -83, 37, -65, -37, 62, 44, 45, -37, -47,
-46, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 46, 47,
48, 49, 50, 51, 52, 62, 44, 45, -37, -47,
-46, -96, -94, 5, -37, -37, -96, -89, -90, -108,
-37, 62, -52, 65, 58, 58, 58, 58, -119, -100,
54, -97, -95, -36, 5, 67, -119, -37, -37, 62,
-52, 54, -37, -119, -110, -37, -24, -27, -29, -32,
-108, -30, 67, -37, 70, -24, -71, 64, -68, 71,
2, -29, 71, 60, 71, -37, -37, 54, -119, -23,
70, -52, 54, -23, -119, -120, -52, 54, -23, -119,
-120, 55, -23, 63, 41, 57, 54, -119, -23, 55,
-119, 63, -119, 63, -37, -38, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, -119, -119,
-119, -119, -36, 63, 55, 41, 5, 54, 54, -37,
5, -117, 68, -37, -67, 71, 2, -33, -27, -29,
64, 71, 63, 71, -58, 54, 57, -23, 54, -119,
-23, 5, -119, -14, 71, -13, -15, -9, -12, -21,
5, -119, 5, -119, -119, -119, -94, 5, 42, -88,
-87, -86, -10, -74, -78, 2, 14, -64, 37, -55,
62, -41, 7, 8, 9, 10, 11, 12, 13, 15,
16, 17, 18, 22, 23, 24, 25, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 5, -119, -119, -108, -37, 63, -119,
-23, -95, -119, -36, 42, 5, 5, -118, -23, 55,
50, 49, -66, -57, 62, -51, -60, -43, -50, 7,
8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 50, 51, 52, 71, 63, 71, -29, 71, 5,
-85, 57, 5, -119, -119, -23, 71, -13, -12, 63,
54, -119, -23, -119, -23, 58, -86, 55, -21, -37,
-37, 62, -52, -119, -23, -61, -119, 57, -23, -118,
-23, -118, -117, -61, 69, -58, -51, -50, 62, -33,
57, -23, 58, -85, -119, -23, -119, -25, -119, -119,
-119, 54, 54, -118, -23, 54, 57, -23, -37, -119,
64, -85, 57, -118, -119, 57, 69, -85, 57, -119,
58, -119, -25, 5, -118, 5, -85, 57, 58, -85,
-116, -119, 58, -85, -119, -118, -118, -23, 57, -23,
58, -85, 58, 58, -115, -114, -11, 2, -119, 58,
-118, -85, 57, 58, -119, -114, -119, 58, -85, 58,
}
var protoDef = [...]int16{
-2, -2, -2, -2, 3, 0, 0, 0, 20, 18,
4, 5, 0, 0, -2, 9, 10, 11, 12, 13,
14, 15, 16, 17, 0, 0, 0, 0, 0, 0,
0, 0, 0, 19, 0, 79, 0, 8, 21, 0,
0, 0, 21, 31, 592, 593, 594, 595, 596, 597,
598, 599, 600, 601, 602, 603, 604, 605, 606, 607,
608, 609, 610, 611, 612, 613, 614, 615, 616, 617,
618, 619, 620, 621, 622, 623, 624, 625, 626, 627,
628, 629, 630, 631, 632, 633, 634, 635, 636, 637,
0, 64, 0, 60, -2, 57, 58, 0, 0, 0,
0, 0, 0, 0, 0, 123, 0, 33, 0, 37,
-2, 0, 24, 80, 25, 26, 21, 21, 21, 30,
0, 0, -2, 65, 59, 0, 21, 0, 0, 0,
0, 21, -2, 124, 34, -2, 39, 21, 27, 28,
29, 32, 21, 67, 68, 69, 70, 71, 72, 0,
73, 0, 75, 66, 0, -2, 21, 21, 21, 21,
0, -2, 0, -2, 269, 270, 271, 273, 0, 0,
137, 138, 139, 129, 0, 42, 329, 330, 331, 332,
333, 334, 335, 336, 337, 338, 339, 340, 341, 342,
343, 344, 345, 346, 347, 348, 349, 350, 351, 352,
353, 354, 355, 356, 357, 358, 359, 360, 361, 362,
363, 364, 365, 366, 367, 368, 369, 370, 0, -2,
56, 0, 82, 83, -2, 86, 91, 0, 95, 0,
74, 76, 77, 78, 21, -2, 235, 236, 237, 238,
239, 240, 241, 242, 243, 244, 245, 246, 0, 0,
0, 0, 0, 0, 0, 0, 207, 125, 0, 322,
40, 296, 297, 298, 299, 300, 301, 302, 303, 304,
305, 306, 307, 308, 309, 310, 311, 312, 313, 314,
315, 316, 317, 318, 319, 320, 321, 323, 324, 325,
326, 327, 328, 0, 0, 0, 0, 21, -2, 222,
223, 224, 225, 226, 0, 0, 209, 371, 372, 373,
374, 375, 376, 377, 378, 379, 380, 381, 382, 383,
384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
394, 395, 396, 397, 398, 399, 400, 401, 402, 403,
404, 405, 406, 407, 408, 409, 410, 411, 412, 413,
414, 21, 268, 272, 0, 0, 133, 0, 46, 456,
457, 458, 459, 460, 461, 462, 463, 464, 465, 466,
467, 468, 469, 470, 471, 472, 473, 474, 475, 476,
477, 478, 479, 480, 481, 482, 483, 484, 485, 486,
487, 488, 489, 490, 491, 492, 493, 494, 495, 496,
497, 498, 499, 500, 0, 0, 130, 21, -2, 282,
283, 284, 285, 0, 81, 85, 87, 88, 89, 90,
0, 0, 93, 106, 107, 108, 0, 0, 0, 0,
229, 234, 0, 0, 21, 0, -2, -2, 0, 21,
0, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, 0, -2, -2, 0, 21,
0, 0, 194, 196, 0, 21, 0, 21, 21, 212,
214, 0, 128, 0, 21, 21, 21, 21, 216, 221,
0, 0, 199, 201, 204, 0, 265, 0, 0, 0,
134, 0, 43, 278, 281, 0, 94, 99, 100, 101,
102, 103, 0, 105, 0, 92, 0, 110, 0, 119,
0, 121, 96, 0, 98, 0, 21, 0, 253, 21,
0, 126, 0, 21, 261, 262, 127, 0, 21, 263,
264, 21, 21, 0, 0, -2, 0, 177, 21, 21,
210, 0, 211, 0, 41, 0, 180, 181, 182, 183,
184, 185, 186, 187, 188, 189, 190, 191, 217, 230,
218, 231, 21, 0, 21, 0, 205, 0, 0, 47,
23, 0, 0, 104, 0, 112, 0, 114, 116, 117,
109, 118, 0, 120, 0, 0, 21, 0, 0, 251,
21, 21, 254, 0, 141, 142, 0, 146, -2, 150,
21, 257, 21, 260, 192, 193, 195, 197, 198, 0,
-2, 161, 162, 163, 164, 166, 0, 0, 0, 131,
0, 44, 415, 416, 417, 418, 419, 420, 421, 422,
423, 424, 425, 426, 427, 428, 429, 430, 431, 432,
433, 434, 435, 436, 437, 438, 439, 440, 441, 442,
443, 444, 445, 446, 447, 448, 449, 450, 451, 452,
453, 454, 455, 21, 178, 206, 213, 215, 0, 227,
21, 200, 208, 202, 203, 0, 23, 276, 23, 22,
0, 0, 0, 135, 0, 48, 0, 52, -2, 501,
502, 503, 504, 505, 506, 507, 508, 509, 510, 511,
512, 513, 514, 515, 516, 517, 518, 519, 520, 521,
522, 523, 524, 525, 526, 527, 528, 529, 530, 531,
532, 533, 534, 535, 536, 537, 538, 539, 540, 541,
542, 543, 544, 545, 111, 0, 113, 122, 97, 0,
0, 21, 21, 252, 249, 21, 140, 143, -2, 148,
0, 255, 21, 258, 21, 21, 160, 165, 0, 23,
0, 0, 132, 175, 21, 0, 228, 21, 0, 274,
23, 277, 21, 0, 289, 136, 49, -2, 54, 115,
21, 0, 21, 0, 247, 21, 250, 149, 256, 259,
157, 0, 0, 169, 23, 0, 21, 0, 45, 176,
179, 0, 21, 275, 286, 21, 288, 0, 21, 155,
21, 248, 23, 23, 170, 0, 0, 21, 151, 0,
0, -2, 21, 0, 156, 55, 167, 23, 21, 0,
173, 0, 152, 21, -2, 293, 294, 295, 153, 21,
168, 0, 21, 174, 287, 292, 154, 171, 0, 172,
}
var protoTok1 = [...]int8{
1, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 79, 3, 77, 76, 75, 73, 3,
68, 69, 72, 66, 63, 67, 62, 60, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 56, 55,
65, 54, 64, 61, 78, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 70, 59, 71, 74, 3, 81, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 57, 3, 58, 80,
}
var protoTok2 = [...]int8{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53,
}
var protoTok3 = [...]int8{
0,
}
var protoErrorMessages = [...]struct {
state int
token int
msg string
}{}
/* parser for yacc output */
var (
protoDebug = 0
protoErrorVerbose = false
)
type protoLexer interface {
Lex(lval *protoSymType) int
Error(s string)
}
type protoParser interface {
Parse(protoLexer) int
Lookahead() int
}
type protoParserImpl struct {
lval protoSymType
stack [protoInitialStackSize]protoSymType
char int
}
func (p *protoParserImpl) Lookahead() int {
return p.char
}
func protoNewParser() protoParser {
return &protoParserImpl{}
}
const protoFlag = -1000
func protoTokname(c int) string {
if c >= 1 && c-1 < len(protoToknames) {
if protoToknames[c-1] != "" {
return protoToknames[c-1]
}
}
return __yyfmt__.Sprintf("tok-%v", c)
}
func protoStatname(s int) string {
if s >= 0 && s < len(protoStatenames) {
if protoStatenames[s] != "" {
return protoStatenames[s]
}
}
return __yyfmt__.Sprintf("state-%v", s)
}
func protoErrorMessage(state, lookAhead int) string {
const TOKSTART = 4
if !protoErrorVerbose {
return "syntax error"
}
for _, e := range protoErrorMessages {
if e.state == state && e.token == lookAhead {
return "syntax error: " + e.msg
}
}
res := "syntax error: unexpected " + protoTokname(lookAhead)
// To match Bison, suggest at most four expected tokens.
expected := make([]int, 0, 4)
// Look for shiftable tokens.
base := int(protoPact[state])
for tok := TOKSTART; tok-1 < len(protoToknames); tok++ {
if n := base + tok; n >= 0 && n < protoLast && int(protoChk[int(protoAct[n])]) == tok {
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
}
if protoDef[state] == -2 {
i := 0
for protoExca[i] != -1 || int(protoExca[i+1]) != state {
i += 2
}
// Look for tokens that we accept or reduce.
for i += 2; protoExca[i] >= 0; i += 2 {
tok := int(protoExca[i])
if tok < TOKSTART || protoExca[i+1] == 0 {
continue
}
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
// If the default action is to accept or reduce, give up.
if protoExca[i+1] != 0 {
return res
}
}
for i, tok := range expected {
if i == 0 {
res += ", expecting "
} else {
res += " or "
}
res += protoTokname(tok)
}
return res
}
func protolex1(lex protoLexer, lval *protoSymType) (char, token int) {
token = 0
char = lex.Lex(lval)
if char <= 0 {
token = int(protoTok1[0])
goto out
}
if char < len(protoTok1) {
token = int(protoTok1[char])
goto out
}
if char >= protoPrivate {
if char < protoPrivate+len(protoTok2) {
token = int(protoTok2[char-protoPrivate])
goto out
}
}
for i := 0; i < len(protoTok3); i += 2 {
token = int(protoTok3[i+0])
if token == char {
token = int(protoTok3[i+1])
goto out
}
}
out:
if token == 0 {
token = int(protoTok2[1]) /* unknown char */
}
if protoDebug >= 3 {
__yyfmt__.Printf("lex %s(%d)\n", protoTokname(token), uint(char))
}
return char, token
}
func protoParse(protolex protoLexer) int {
return protoNewParser().Parse(protolex)
}
func (protorcvr *protoParserImpl) Parse(protolex protoLexer) int {
var proton int
var protoVAL protoSymType
var protoDollar []protoSymType
_ = protoDollar // silence set and not used
protoS := protorcvr.stack[:]
Nerrs := 0 /* number of errors */
Errflag := 0 /* error recovery flag */
protostate := 0
protorcvr.char = -1
prototoken := -1 // protorcvr.char translated into internal numbering
defer func() {
// Make sure we report no lookahead when not parsing.
protostate = -1
protorcvr.char = -1
prototoken = -1
}()
protop := -1
goto protostack
ret0:
return 0
ret1:
return 1
protostack:
/* put a state and value onto the stack */
if protoDebug >= 4 {
__yyfmt__.Printf("char %v in %v\n", protoTokname(prototoken), protoStatname(protostate))
}
protop++
if protop >= len(protoS) {
nyys := make([]protoSymType, len(protoS)*2)
copy(nyys, protoS)
protoS = nyys
}
protoS[protop] = protoVAL
protoS[protop].yys = protostate
protonewstate:
proton = int(protoPact[protostate])
if proton <= protoFlag {
goto protodefault /* simple state */
}
if protorcvr.char < 0 {
protorcvr.char, prototoken = protolex1(protolex, &protorcvr.lval)
}
proton += prototoken
if proton < 0 || proton >= protoLast {
goto protodefault
}
proton = int(protoAct[proton])
if int(protoChk[proton]) == prototoken { /* valid shift */
protorcvr.char = -1
prototoken = -1
protoVAL = protorcvr.lval
protostate = proton
if Errflag > 0 {
Errflag--
}
goto protostack
}
protodefault:
/* default state action */
proton = int(protoDef[protostate])
if proton == -2 {
if protorcvr.char < 0 {
protorcvr.char, prototoken = protolex1(protolex, &protorcvr.lval)
}
/* look through exception table */
xi := 0
for {
if protoExca[xi+0] == -1 && int(protoExca[xi+1]) == protostate {
break
}
xi += 2
}
for xi += 2; ; xi += 2 {
proton = int(protoExca[xi+0])
if proton < 0 || proton == prototoken {
break
}
}
proton = int(protoExca[xi+1])
if proton < 0 {
goto ret0
}
}
if proton == 0 {
/* error ... attempt to resume parsing */
switch Errflag {
case 0: /* brand new error */
protolex.Error(protoErrorMessage(protostate, prototoken))
Nerrs++
if protoDebug >= 1 {
__yyfmt__.Printf("%s", protoStatname(protostate))
__yyfmt__.Printf(" saw %s\n", protoTokname(prototoken))
}
fallthrough
case 1, 2: /* incompletely recovered error ... try again */
Errflag = 3
/* find a state where "error" is a legal shift action */
for protop >= 0 {
proton = int(protoPact[protoS[protop].yys]) + protoErrCode
if proton >= 0 && proton < protoLast {
protostate = int(protoAct[proton]) /* simulate a shift of "error" */
if int(protoChk[protostate]) == protoErrCode {
goto protostack
}
}
/* the current p has no shift on "error", pop stack */
if protoDebug >= 2 {
__yyfmt__.Printf("error recovery pops state %d\n", protoS[protop].yys)
}
protop--
}
/* there is no state on the stack with an error shift ... abort */
goto ret1
case 3: /* no shift yet; clobber input char */
if protoDebug >= 2 {
__yyfmt__.Printf("error recovery discards %s\n", protoTokname(prototoken))
}
if prototoken == protoEofCode {
goto ret1
}
protorcvr.char = -1
prototoken = -1
goto protonewstate /* try again in the same state */
}
}
/* reduction by production proton */
if protoDebug >= 2 {
__yyfmt__.Printf("reduce %v in:\n\t%v\n", proton, protoStatname(protostate))
}
protont := proton
protopt := protop
_ = protopt // guard against "declared and not used"
protop -= int(protoR2[proton])
// protop is now the index of $0. Perform the default action. Iff the
// reduced production is ε, $1 is possibly out of range.
if protop+1 >= len(protoS) {
nyys := make([]protoSymType, len(protoS)*2)
copy(nyys, protoS)
protoS = nyys
}
protoVAL = protoS[protop+1]
/* consult goto table to find next state */
proton = int(protoR1[proton])
protog := int(protoPgo[proton])
protoj := protog + protoS[protop].yys + 1
if protoj >= protoLast {
protostate = int(protoAct[protog])
} else {
protostate = int(protoAct[protoj])
if int(protoChk[protostate]) != -proton {
protostate = int(protoAct[protog])
}
}
// dummy call; replaced with literal code
switch protont {
case 1:
protoDollar = protoS[protopt-1 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNode(lex.info, protoDollar[1].syn, nil, lex.eof)
lex.res = protoVAL.file
}
case 2:
protoDollar = protoS[protopt-1 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNodeWithEdition(lex.info, protoDollar[1].ed, nil, lex.eof)
lex.res = protoVAL.file
}
case 3:
protoDollar = protoS[protopt-1 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNode(lex.info, nil, protoDollar[1].fileElements, lex.eof)
lex.res = protoVAL.file
}
case 4:
protoDollar = protoS[protopt-2 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNode(lex.info, protoDollar[1].syn, protoDollar[2].fileElements, lex.eof)
lex.res = protoVAL.file
}
case 5:
protoDollar = protoS[protopt-2 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNodeWithEdition(lex.info, protoDollar[1].ed, protoDollar[2].fileElements, lex.eof)
lex.res = protoVAL.file
}
case 6:
protoDollar = protoS[protopt-0 : protopt+1]
{
lex := protolex.(*protoLex)
protoVAL.file = ast.NewFileNode(lex.info, nil, nil, lex.eof)
lex.res = protoVAL.file
}
case 7:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.fileElements = prependRunes(toFileElement, protoDollar[1].bs, protoDollar[2].fileElements)
}
case 8:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.fileElements = append(protoDollar[1].fileElements, protoDollar[2].fileElements...)
}
case 9:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = protoDollar[1].fileElements
}
case 10:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].imprt.Node, protoDollar[1].imprt.Runes)
}
case 11:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].pkg.Node, protoDollar[1].pkg.Runes)
}
case 12:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].opt.Node, protoDollar[1].opt.Runes)
}
case 13:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].msg.Node, protoDollar[1].msg.Runes)
}
case 14:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].en.Node, protoDollar[1].en.Runes)
}
case 15:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].extend.Node, protoDollar[1].extend.Runes)
}
case 16:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = toElements[ast.FileElement](toFileElement, protoDollar[1].svc.Node, protoDollar[1].svc.Runes)
}
case 17:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.fileElements = nil
}
case 18:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.bs = []*ast.RuneNode{protoDollar[1].b}
}
case 19:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.bs = append(protoDollar[1].bs, protoDollar[2].b)
}
case 20:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.bs = protoDollar[1].bs
}
case 21:
protoDollar = protoS[protopt-0 : protopt+1]
{
protoVAL.bs = nil
}
case 22:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.b = protoDollar[1].b
}
case 23:
protoDollar = protoS[protopt-0 : protopt+1]
{
protolex.(*protoLex).Error("syntax error: expecting ';'")
protoVAL.b = nil
}
case 24:
protoDollar = protoS[protopt-4 : protopt+1]
{
protoVAL.syn = ast.NewSyntaxNode(protoDollar[1].id.ToKeyword(), protoDollar[2].b, toStringValueNode(protoDollar[3].str), protoDollar[4].b)
}
case 25:
protoDollar = protoS[protopt-4 : protopt+1]
{
protoVAL.ed = ast.NewEditionNode(protoDollar[1].id.ToKeyword(), protoDollar[2].b, toStringValueNode(protoDollar[3].str), protoDollar[4].b)
}
case 26:
protoDollar = protoS[protopt-3 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.imprt = newNodeWithRunes(ast.NewImportNodeWithModifier(protoDollar[1].id.ToKeyword(), nil, toStringValueNode(protoDollar[2].str), semi), extra...)
}
case 27:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.imprt = newNodeWithRunes(ast.NewImportNodeWithModifier(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), toStringValueNode(protoDollar[3].str), semi), extra...)
}
case 28:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.imprt = newNodeWithRunes(ast.NewImportNodeWithModifier(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), toStringValueNode(protoDollar[3].str), semi), extra...)
}
case 29:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.imprt = newNodeWithRunes(ast.NewImportNodeWithModifier(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), toStringValueNode(protoDollar[3].str), semi), extra...)
}
case 30:
protoDollar = protoS[protopt-3 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.pkg = newNodeWithRunes(ast.NewPackageNode(protoDollar[1].id.ToKeyword(), protoDollar[2].cid.toIdentValueNode(nil), semi), extra...)
}
case 31:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 32:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[3].id)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].b)
protoVAL.cid = protoDollar[1].cid
}
case 33:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].cidPart.Node}, dots: protoDollar[1].cidPart.Runes}
}
case 34:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[2].cidPart.Node)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].cidPart.Runes...)
protoVAL.cid = protoDollar[1].cid
}
case 35:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].cidPart.Node}, dots: protoDollar[1].cidPart.Runes}
}
case 36:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[2].cidPart.Node)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].cidPart.Runes...)
protoVAL.cid = protoDollar[1].cid
}
case 37:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cidPart = newNodeWithRunes(protoDollar[1].id)
}
case 38:
protoDollar = protoS[protopt-1 : protopt+1]
{
protolex.(*protoLex).Error("syntax error: unexpected '.'")
protoVAL.cidPart = protoDollar[1].cidPart
}
case 39:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.cidPart = newNodeWithRunes(protoDollar[1].id, protoDollar[2].b)
}
case 40:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 41:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[3].id)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].b)
protoVAL.cid = protoDollar[1].cid
}
case 42:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 43:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[3].id)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].b)
protoVAL.cid = protoDollar[1].cid
}
case 44:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 45:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[3].id)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].b)
protoVAL.cid = protoDollar[1].cid
}
case 46:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 47:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[3].id)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].b)
protoVAL.cid = protoDollar[1].cid
}
case 48:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].cidPart.Node}, dots: protoDollar[1].cidPart.Runes}
}
case 49:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[2].cidPart.Node)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].cidPart.Runes...)
protoVAL.cid = protoDollar[1].cid
}
case 50:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cid = &identSlices{idents: []*ast.IdentNode{protoDollar[1].cidPart.Node}, dots: protoDollar[1].cidPart.Runes}
}
case 51:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].cid.idents = append(protoDollar[1].cid.idents, protoDollar[2].cidPart.Node)
protoDollar[1].cid.dots = append(protoDollar[1].cid.dots, protoDollar[2].cidPart.Runes...)
protoVAL.cid = protoDollar[1].cid
}
case 52:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.cidPart = newNodeWithRunes(protoDollar[1].id)
}
case 53:
protoDollar = protoS[protopt-1 : protopt+1]
{
protolex.(*protoLex).Error("syntax error: unexpected '.'")
protoVAL.cidPart = protoDollar[1].cidPart
}
case 54:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.cidPart = newNodeWithRunes(protoDollar[1].id, protoDollar[2].b)
}
case 55:
protoDollar = protoS[protopt-5 : protopt+1]
{
optName := ast.NewOptionNameNode(protoDollar[2].optNms.refs, protoDollar[2].optNms.dots)
protoVAL.optRaw = ast.NewOptionNode(protoDollar[1].id.ToKeyword(), optName, protoDollar[3].b, protoDollar[4].v, protoDollar[5].b)
}
case 56:
protoDollar = protoS[protopt-5 : protopt+1]
{
optName := ast.NewOptionNameNode(protoDollar[2].optNms.refs, protoDollar[2].optNms.dots)
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.opt = newNodeWithRunes(ast.NewOptionNode(protoDollar[1].id.ToKeyword(), optName, protoDollar[3].b, protoDollar[4].v, semi), extra...)
}
case 57:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.refRaw = ast.NewFieldReferenceNode(protoDollar[1].id)
}
case 58:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.refRaw = protoDollar[1].refRaw
}
case 59:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.ref = newNodeWithRunes(protoDollar[1].refRaw, protoDollar[2].b)
}
case 60:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.ref = newNodeWithRunes(protoDollar[1].refRaw)
}
case 61:
protoDollar = protoS[protopt-1 : protopt+1]
{
protolex.(*protoLex).Error("syntax error: unexpected '.'")
protoVAL.ref = protoDollar[1].ref
}
case 62:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.optNms = &fieldRefSlices{refs: []*ast.FieldReferenceNode{protoDollar[1].ref.Node}, dots: protoDollar[1].ref.Runes}
}
case 63:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].optNms.refs = append(protoDollar[1].optNms.refs, protoDollar[2].ref.Node)
protoDollar[1].optNms.dots = append(protoDollar[1].optNms.dots, protoDollar[2].ref.Runes...)
protoVAL.optNms = protoDollar[1].optNms
}
case 64:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.optNms = &fieldRefSlices{refs: []*ast.FieldReferenceNode{protoDollar[1].ref.Node}, dots: protoDollar[1].ref.Runes}
}
case 65:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].optNms.refs = append(protoDollar[1].optNms.refs, protoDollar[2].ref.Node)
protoDollar[1].optNms.dots = append(protoDollar[1].optNms.dots, protoDollar[2].ref.Runes...)
protoVAL.optNms = protoDollar[1].optNms
}
case 66:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.refRaw = ast.NewExtensionFieldReferenceNode(protoDollar[1].b, protoDollar[2].tid, protoDollar[3].b)
}
case 69:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = toStringValueNode(protoDollar[1].str)
}
case 72:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = protoDollar[1].id
}
case 73:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = protoDollar[1].f
}
case 74:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.v = ast.NewSignedFloatLiteralNode(protoDollar[1].b, protoDollar[2].f)
}
case 75:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = protoDollar[1].i
}
case 76:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[2].i.Val > math.MaxInt64+1 {
// can't represent as int so treat as float literal
protoVAL.v = ast.NewSignedFloatLiteralNode(protoDollar[1].b, protoDollar[2].i)
} else {
protoVAL.v = ast.NewNegativeIntLiteralNode(protoDollar[1].b, protoDollar[2].i)
}
}
case 77:
protoDollar = protoS[protopt-2 : protopt+1]
{
f := ast.NewSpecialFloatLiteralNode(protoDollar[2].id.ToKeyword())
protoVAL.v = ast.NewSignedFloatLiteralNode(protoDollar[1].b, f)
}
case 78:
protoDollar = protoS[protopt-2 : protopt+1]
{
f := ast.NewSpecialFloatLiteralNode(protoDollar[2].id.ToKeyword())
protoVAL.v = ast.NewSignedFloatLiteralNode(protoDollar[1].b, f)
}
case 79:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.str = []*ast.StringLiteralNode{protoDollar[1].s}
}
case 80:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.str = append(protoDollar[1].str, protoDollar[2].s)
}
case 81:
protoDollar = protoS[protopt-3 : protopt+1]
{
if protoDollar[2].msgLitFlds == nil {
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
} else {
fields, delimiters := protoDollar[2].msgLitFlds.toNodes()
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, fields, delimiters, protoDollar[3].b)
}
}
case 82:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, nil, nil, protoDollar[2].b)
}
case 85:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[1].msgLitFlds != nil {
protoDollar[1].msgLitFlds.next = protoDollar[2].msgLitFlds
protoVAL.msgLitFlds = protoDollar[1].msgLitFlds
} else {
protoVAL.msgLitFlds = protoDollar[2].msgLitFlds
}
}
case 86:
protoDollar = protoS[protopt-1 : protopt+1]
{
if protoDollar[1].msgLitFld != nil {
protoVAL.msgLitFlds = &messageFieldList{field: protoDollar[1].msgLitFld}
} else {
protoVAL.msgLitFlds = nil
}
}
case 87:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[1].msgLitFld != nil {
protoVAL.msgLitFlds = &messageFieldList{field: protoDollar[1].msgLitFld, delimiter: protoDollar[2].b}
} else {
protoVAL.msgLitFlds = nil
}
}
case 88:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[1].msgLitFld != nil {
protoVAL.msgLitFlds = &messageFieldList{field: protoDollar[1].msgLitFld, delimiter: protoDollar[2].b}
} else {
protoVAL.msgLitFlds = nil
}
}
case 89:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.msgLitFlds = nil
}
case 90:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.msgLitFlds = nil
}
case 91:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgLitFlds = nil
}
case 92:
protoDollar = protoS[protopt-3 : protopt+1]
{
if protoDollar[1].refRaw != nil && protoDollar[2].b != nil {
protoVAL.msgLitFld = ast.NewMessageFieldNode(protoDollar[1].refRaw, protoDollar[2].b, protoDollar[3].v)
} else {
protoVAL.msgLitFld = nil
}
}
case 93:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[1].refRaw != nil && protoDollar[2].v != nil {
protoVAL.msgLitFld = ast.NewMessageFieldNode(protoDollar[1].refRaw, nil, protoDollar[2].v)
} else {
protoVAL.msgLitFld = nil
}
}
case 94:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.msgLitFld = nil
}
case 95:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.refRaw = ast.NewFieldReferenceNode(protoDollar[1].id)
}
case 96:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.refRaw = ast.NewExtensionFieldReferenceNode(protoDollar[1].b, protoDollar[2].cid.toIdentValueNode(nil), protoDollar[3].b)
}
case 97:
protoDollar = protoS[protopt-5 : protopt+1]
{
protoVAL.refRaw = ast.NewAnyTypeReferenceNode(protoDollar[1].b, protoDollar[2].cid.toIdentValueNode(nil), protoDollar[3].b, protoDollar[4].cid.toIdentValueNode(nil), protoDollar[5].b)
}
case 98:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.refRaw = nil
}
case 102:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = toStringValueNode(protoDollar[1].str)
}
case 104:
protoDollar = protoS[protopt-2 : protopt+1]
{
kw := protoDollar[2].id.ToKeyword()
switch strings.ToLower(kw.Val) {
case "inf", "infinity", "nan":
// these are acceptable
default:
// anything else is not
protolex.(*protoLex).Error(`only identifiers "inf", "infinity", or "nan" may appear after negative sign`)
}
// we'll validate the identifier later
f := ast.NewSpecialFloatLiteralNode(kw)
protoVAL.v = ast.NewSignedFloatLiteralNode(protoDollar[1].b, f)
}
case 105:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.v = protoDollar[1].id
}
case 109:
protoDollar = protoS[protopt-3 : protopt+1]
{
if protoDollar[2].msgLitFlds == nil {
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
} else {
fields, delimiters := protoDollar[2].msgLitFlds.toNodes()
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, fields, delimiters, protoDollar[3].b)
}
}
case 110:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.v = ast.NewMessageLiteralNode(protoDollar[1].b, nil, nil, protoDollar[2].b)
}
case 111:
protoDollar = protoS[protopt-3 : protopt+1]
{
if protoDollar[2].sl == nil {
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
} else {
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, protoDollar[2].sl.vals, protoDollar[2].sl.commas, protoDollar[3].b)
}
}
case 112:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[2].b)
}
case 113:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
}
case 114:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.sl = &valueSlices{vals: []ast.ValueNode{protoDollar[1].v}}
}
case 115:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].sl.vals = append(protoDollar[1].sl.vals, protoDollar[3].v)
protoDollar[1].sl.commas = append(protoDollar[1].sl.commas, protoDollar[2].b)
protoVAL.sl = protoDollar[1].sl
}
case 118:
protoDollar = protoS[protopt-3 : protopt+1]
{
if protoDollar[2].sl == nil {
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
} else {
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, protoDollar[2].sl.vals, protoDollar[2].sl.commas, protoDollar[3].b)
}
}
case 119:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[2].b)
}
case 120:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.v = ast.NewArrayLiteralNode(protoDollar[1].b, nil, nil, protoDollar[3].b)
}
case 121:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.sl = &valueSlices{vals: []ast.ValueNode{protoDollar[1].v}}
}
case 122:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].sl.vals = append(protoDollar[1].sl.vals, protoDollar[3].v)
protoDollar[1].sl.commas = append(protoDollar[1].sl.commas, protoDollar[2].b)
protoVAL.sl = protoDollar[1].sl
}
case 123:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 124:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 125:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 126:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[3].cid.prefix(protoDollar[1].id, protoDollar[2].b)
protoVAL.tid = protoDollar[3].cid.toIdentValueNode(nil)
}
case 127:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[3].cid.prefix(protoDollar[1].id, protoDollar[2].b)
protoVAL.tid = protoDollar[3].cid.toIdentValueNode(nil)
}
case 128:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 129:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 130:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 131:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 132:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 133:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 134:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 135:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.tid = protoDollar[1].cid.toIdentValueNode(nil)
}
case 136:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.tid = protoDollar[2].cid.toIdentValueNode(protoDollar[1].b)
}
case 140:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.cmpctOpts = ast.NewCompactOptionsNode(protoDollar[1].b, protoDollar[2].opts.options, protoDollar[2].opts.commas, protoDollar[3].b)
}
case 141:
protoDollar = protoS[protopt-2 : protopt+1]
{
protolex.(*protoLex).Error("compact options must have at least one option")
protoVAL.cmpctOpts = ast.NewCompactOptionsNode(protoDollar[1].b, nil, nil, protoDollar[2].b)
}
case 142:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.opts = &compactOptionSlices{options: []*ast.OptionNode{protoDollar[1].opt.Node}, commas: protoDollar[1].opt.Runes}
}
case 143:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].opts.options = append(protoDollar[1].opts.options, protoDollar[2].opt.Node)
protoDollar[1].opts.commas = append(protoDollar[1].opts.commas, protoDollar[2].opt.Runes...)
protoVAL.opts = protoDollar[1].opts
}
case 144:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.opts = &compactOptionSlices{options: []*ast.OptionNode{protoDollar[1].opt.Node}, commas: protoDollar[1].opt.Runes}
}
case 145:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoDollar[1].opts.options = append(protoDollar[1].opts.options, protoDollar[2].opt.Node)
protoDollar[1].opts.commas = append(protoDollar[1].opts.commas, protoDollar[2].opt.Runes...)
protoVAL.opts = protoDollar[1].opts
}
case 146:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.opt = newNodeWithRunes(protoDollar[1].optRaw)
}
case 147:
protoDollar = protoS[protopt-1 : protopt+1]
{
protolex.(*protoLex).Error("syntax error: unexpected ','")
protoVAL.opt = protoDollar[1].opt
}
case 148:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.opt = newNodeWithRunes(protoDollar[1].optRaw, protoDollar[2].b)
}
case 149:
protoDollar = protoS[protopt-3 : protopt+1]
{
optName := ast.NewOptionNameNode(protoDollar[1].optNms.refs, protoDollar[1].optNms.dots)
protoVAL.optRaw = ast.NewCompactOptionNode(optName, protoDollar[2].b, protoDollar[3].v)
}
case 150:
protoDollar = protoS[protopt-1 : protopt+1]
{
optName := ast.NewOptionNameNode(protoDollar[1].optNms.refs, protoDollar[1].optNms.dots)
protolex.(*protoLex).Error("compact option must have a value")
protoVAL.optRaw = ast.NewCompactOptionNode(optName, nil, nil)
}
case 151:
protoDollar = protoS[protopt-8 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, nil, protoDollar[6].b, protoDollar[7].msgElements, protoDollar[8].b)
}
case 152:
protoDollar = protoS[protopt-9 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, protoDollar[6].cmpctOpts, protoDollar[7].b, protoDollar[8].msgElements, protoDollar[9].b)
}
case 153:
protoDollar = protoS[protopt-9 : protopt+1]
{
protoVAL.msgGrp = newNodeWithRunes(ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, nil, protoDollar[6].b, protoDollar[7].msgElements, protoDollar[8].b), protoDollar[9].bs...)
}
case 154:
protoDollar = protoS[protopt-10 : protopt+1]
{
protoVAL.msgGrp = newNodeWithRunes(ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, protoDollar[6].cmpctOpts, protoDollar[7].b, protoDollar[8].msgElements, protoDollar[9].b), protoDollar[10].bs...)
}
case 155:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.msgGrp = newNodeWithRunes(ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, nil, nil, nil, protoDollar[4].b, protoDollar[5].msgElements, protoDollar[6].b), protoDollar[7].bs...)
}
case 156:
protoDollar = protoS[protopt-8 : protopt+1]
{
protoVAL.msgGrp = newNodeWithRunes(ast.NewGroupNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, nil, nil, protoDollar[4].cmpctOpts, protoDollar[5].b, protoDollar[6].msgElements, protoDollar[7].b), protoDollar[8].bs...)
}
case 157:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.oo = newNodeWithRunes(ast.NewOneofNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].ooElements, protoDollar[5].b), protoDollar[6].bs...)
}
case 158:
protoDollar = protoS[protopt-0 : protopt+1]
{
protoVAL.ooElements = nil
}
case 160:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[2].ooElement != nil {
protoVAL.ooElements = append(protoDollar[1].ooElements, protoDollar[2].ooElement)
} else {
protoVAL.ooElements = protoDollar[1].ooElements
}
}
case 161:
protoDollar = protoS[protopt-1 : protopt+1]
{
if protoDollar[1].ooElement != nil {
protoVAL.ooElements = []ast.OneofElement{protoDollar[1].ooElement}
} else {
protoVAL.ooElements = nil
}
}
case 162:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.ooElement = protoDollar[1].optRaw
}
case 163:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.ooElement = protoDollar[1].fld
}
case 164:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.ooElement = protoDollar[1].grp
}
case 165:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.ooElement = nil
}
case 166:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.ooElement = nil
}
case 167:
protoDollar = protoS[protopt-5 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, protoDollar[5].b)
}
case 168:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, protoDollar[6].b)
}
case 169:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, nil, nil, nil, protoDollar[3].b)
}
case 170:
protoDollar = protoS[protopt-4 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, protoDollar[4].b)
}
case 171:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(nil, protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, protoDollar[5].b, protoDollar[6].msgElements, protoDollar[7].b)
}
case 172:
protoDollar = protoS[protopt-8 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(nil, protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, protoDollar[6].b, protoDollar[7].msgElements, protoDollar[8].b)
}
case 173:
protoDollar = protoS[protopt-5 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(nil, protoDollar[1].id.ToKeyword(), protoDollar[2].id, nil, nil, nil, protoDollar[3].b, protoDollar[4].msgElements, protoDollar[5].b)
}
case 174:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.grp = ast.NewGroupNode(nil, protoDollar[1].id.ToKeyword(), protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, protoDollar[4].b, protoDollar[5].msgElements, protoDollar[6].b)
}
case 175:
protoDollar = protoS[protopt-5 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.mapFld = newNodeWithRunes(ast.NewMapFieldNode(protoDollar[1].mapType, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, semi), extra...)
}
case 176:
protoDollar = protoS[protopt-6 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.mapFld = newNodeWithRunes(ast.NewMapFieldNode(protoDollar[1].mapType, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, semi), extra...)
}
case 177:
protoDollar = protoS[protopt-3 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.mapFld = newNodeWithRunes(ast.NewMapFieldNode(protoDollar[1].mapType, protoDollar[2].id, nil, nil, nil, semi), extra...)
}
case 178:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.mapFld = newNodeWithRunes(ast.NewMapFieldNode(protoDollar[1].mapType, protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, semi), extra...)
}
case 179:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.mapType = ast.NewMapTypeNode(protoDollar[1].id.ToKeyword(), protoDollar[2].b, protoDollar[3].id, protoDollar[4].b, protoDollar[5].tid, protoDollar[6].b)
}
case 192:
protoDollar = protoS[protopt-4 : protopt+1]
{
// TODO: Tolerate a missing semicolon here. This currnelty creates a shift/reduce conflict
// between `extensions 1 to 10` and `extensions 1` followed by `to = 10`.
protoVAL.ext = newNodeWithRunes(ast.NewExtensionRangeNode(protoDollar[1].id.ToKeyword(), protoDollar[2].rngs.ranges, protoDollar[2].rngs.commas, nil, protoDollar[3].b), protoDollar[4].bs...)
}
case 193:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.ext = newNodeWithRunes(ast.NewExtensionRangeNode(protoDollar[1].id.ToKeyword(), protoDollar[2].rngs.ranges, protoDollar[2].rngs.commas, protoDollar[3].cmpctOpts, semi), extra...)
}
case 194:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.rngs = &rangeSlices{ranges: []*ast.RangeNode{protoDollar[1].rng}}
}
case 195:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].rngs.ranges = append(protoDollar[1].rngs.ranges, protoDollar[3].rng)
protoDollar[1].rngs.commas = append(protoDollar[1].rngs.commas, protoDollar[2].b)
protoVAL.rngs = protoDollar[1].rngs
}
case 196:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].i, nil, nil, nil)
}
case 197:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].i, protoDollar[2].id.ToKeyword(), protoDollar[3].i, nil)
}
case 198:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].i, protoDollar[2].id.ToKeyword(), nil, protoDollar[3].id.ToKeyword())
}
case 199:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.rngs = &rangeSlices{ranges: []*ast.RangeNode{protoDollar[1].rng}}
}
case 200:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].rngs.ranges = append(protoDollar[1].rngs.ranges, protoDollar[3].rng)
protoDollar[1].rngs.commas = append(protoDollar[1].rngs.commas, protoDollar[2].b)
protoVAL.rngs = protoDollar[1].rngs
}
case 201:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].il, nil, nil, nil)
}
case 202:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].il, protoDollar[2].id.ToKeyword(), protoDollar[3].il, nil)
}
case 203:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.rng = ast.NewRangeNode(protoDollar[1].il, protoDollar[2].id.ToKeyword(), nil, protoDollar[3].id.ToKeyword())
}
case 204:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.il = protoDollar[1].i
}
case 205:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.il = ast.NewNegativeIntLiteralNode(protoDollar[1].b, protoDollar[2].i)
}
case 206:
protoDollar = protoS[protopt-4 : protopt+1]
{
// TODO: Tolerate a missing semicolon here. This currnelty creates a shift/reduce conflict
// between `reserved 1 to 10` and `reserved 1` followed by `to = 10`.
protoVAL.resvd = newNodeWithRunes(ast.NewReservedRangesNode(protoDollar[1].id.ToKeyword(), protoDollar[2].rngs.ranges, protoDollar[2].rngs.commas, protoDollar[3].b), protoDollar[4].bs...)
}
case 208:
protoDollar = protoS[protopt-4 : protopt+1]
{
// TODO: Tolerate a missing semicolon here. This currnelty creates a shift/reduce conflict
// between `reserved 1 to 10` and `reserved 1` followed by `to = 10`.
protoVAL.resvd = newNodeWithRunes(ast.NewReservedRangesNode(protoDollar[1].id.ToKeyword(), protoDollar[2].rngs.ranges, protoDollar[2].rngs.commas, protoDollar[3].b), protoDollar[4].bs...)
}
case 210:
protoDollar = protoS[protopt-3 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.resvd = newNodeWithRunes(ast.NewReservedNamesNode(protoDollar[1].id.ToKeyword(), protoDollar[2].names.names, protoDollar[2].names.commas, semi), extra...)
}
case 211:
protoDollar = protoS[protopt-3 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.resvd = newNodeWithRunes(ast.NewReservedIdentifiersNode(protoDollar[1].id.ToKeyword(), protoDollar[2].names.idents, protoDollar[2].names.commas, semi), extra...)
}
case 212:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.names = &nameSlices{names: []ast.StringValueNode{toStringValueNode(protoDollar[1].str)}}
}
case 213:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].names.names = append(protoDollar[1].names.names, toStringValueNode(protoDollar[3].str))
protoDollar[1].names.commas = append(protoDollar[1].names.commas, protoDollar[2].b)
protoVAL.names = protoDollar[1].names
}
case 214:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.names = &nameSlices{idents: []*ast.IdentNode{protoDollar[1].id}}
}
case 215:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoDollar[1].names.idents = append(protoDollar[1].names.idents, protoDollar[3].id)
protoDollar[1].names.commas = append(protoDollar[1].names.commas, protoDollar[2].b)
protoVAL.names = protoDollar[1].names
}
case 216:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.en = newNodeWithRunes(ast.NewEnumNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].enElements, protoDollar[5].b), protoDollar[6].bs...)
}
case 217:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.en = newNodeWithRunes(ast.NewEnumNodeWithVisibility(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].enElements, protoDollar[6].b), protoDollar[7].bs...)
}
case 218:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.en = newNodeWithRunes(ast.NewEnumNodeWithVisibility(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].enElements, protoDollar[6].b), protoDollar[7].bs...)
}
case 219:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = prependRunes(toEnumElement, protoDollar[1].bs, nil)
}
case 220:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.enElements = prependRunes(toEnumElement, protoDollar[1].bs, protoDollar[2].enElements)
}
case 221:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.enElements = append(protoDollar[1].enElements, protoDollar[2].enElements...)
}
case 222:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = protoDollar[1].enElements
}
case 223:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = toElements[ast.EnumElement](toEnumElement, protoDollar[1].opt.Node, protoDollar[1].opt.Runes)
}
case 224:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = toElements[ast.EnumElement](toEnumElement, protoDollar[1].env.Node, protoDollar[1].env.Runes)
}
case 225:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = toElements[ast.EnumElement](toEnumElement, protoDollar[1].resvd.Node, protoDollar[1].resvd.Runes)
}
case 226:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.enElements = nil
}
case 227:
protoDollar = protoS[protopt-4 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.env = newNodeWithRunes(ast.NewEnumValueNode(protoDollar[1].id, protoDollar[2].b, protoDollar[3].il, nil, semi), extra...)
}
case 228:
protoDollar = protoS[protopt-5 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.env = newNodeWithRunes(ast.NewEnumValueNode(protoDollar[1].id, protoDollar[2].b, protoDollar[3].il, protoDollar[4].cmpctOpts, semi), extra...)
}
case 229:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.msg = newNodeWithRunes(ast.NewMessageNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].msgElements, protoDollar[5].b), protoDollar[6].bs...)
}
case 230:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.msg = newNodeWithRunes(ast.NewMessageNodeWithVisibility(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].msgElements, protoDollar[6].b), protoDollar[7].bs...)
}
case 231:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.msg = newNodeWithRunes(ast.NewMessageNodeWithVisibility(protoDollar[1].id.ToKeyword(), protoDollar[2].id.ToKeyword(), protoDollar[3].id, protoDollar[4].b, protoDollar[5].msgElements, protoDollar[6].b), protoDollar[7].bs...)
}
case 232:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = prependRunes(toMessageElement, protoDollar[1].bs, nil)
}
case 233:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.msgElements = prependRunes(toMessageElement, protoDollar[1].bs, protoDollar[2].msgElements)
}
case 234:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.msgElements = append(protoDollar[1].msgElements, protoDollar[2].msgElements...)
}
case 235:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = protoDollar[1].msgElements
}
case 236:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].msgFld.Node, protoDollar[1].msgFld.Runes)
}
case 237:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].en.Node, protoDollar[1].en.Runes)
}
case 238:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].msg.Node, protoDollar[1].msg.Runes)
}
case 239:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].extend.Node, protoDollar[1].extend.Runes)
}
case 240:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].ext.Node, protoDollar[1].ext.Runes)
}
case 241:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].msgGrp.Node, protoDollar[1].msgGrp.Runes)
}
case 242:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].opt.Node, protoDollar[1].opt.Runes)
}
case 243:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].oo.Node, protoDollar[1].oo.Runes)
}
case 244:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].mapFld.Node, protoDollar[1].mapFld.Runes)
}
case 245:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = toElements[ast.MessageElement](toMessageElement, protoDollar[1].resvd.Node, protoDollar[1].resvd.Runes)
}
case 246:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.msgElements = nil
}
case 247:
protoDollar = protoS[protopt-6 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, nil, semis), extra...)
}
case 248:
protoDollar = protoS[protopt-7 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[7].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, protoDollar[6].cmpctOpts, semis), extra...)
}
case 249:
protoDollar = protoS[protopt-5 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, semis), extra...)
}
case 250:
protoDollar = protoS[protopt-6 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, semis), extra...)
}
case 251:
protoDollar = protoS[protopt-4 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, nil, nil, nil, semis), extra...)
}
case 252:
protoDollar = protoS[protopt-5 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, nil, nil, protoDollar[4].cmpctOpts, semis), extra...)
}
case 253:
protoDollar = protoS[protopt-3 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, nil, nil, nil, semis), extra...)
}
case 254:
protoDollar = protoS[protopt-4 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, semis), extra...)
}
case 255:
protoDollar = protoS[protopt-5 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, semis), extra...)
}
case 256:
protoDollar = protoS[protopt-6 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, semis), extra...)
}
case 257:
protoDollar = protoS[protopt-4 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, semis), extra...)
}
case 258:
protoDollar = protoS[protopt-5 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[5].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, semis), extra...)
}
case 259:
protoDollar = protoS[protopt-6 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, semis), extra...)
}
case 260:
protoDollar = protoS[protopt-4 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[4].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, protoDollar[3].cmpctOpts, semis), extra...)
}
case 261:
protoDollar = protoS[protopt-3 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, nil, semis), extra...)
}
case 262:
protoDollar = protoS[protopt-3 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, nil, semis), extra...)
}
case 263:
protoDollar = protoS[protopt-3 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, nil, semis), extra...)
}
case 264:
protoDollar = protoS[protopt-3 : protopt+1]
{
semis, extra := protolex.(*protoLex).requireSemicolon(protoDollar[3].bs)
protoVAL.msgFld = newNodeWithRunes(ast.NewFieldNode(nil, protoDollar[1].id, protoDollar[2].id, nil, nil, nil, semis), extra...)
}
case 265:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.extend = newNodeWithRunes(ast.NewExtendNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].b, protoDollar[4].extElements, protoDollar[5].b), protoDollar[6].bs...)
}
case 266:
protoDollar = protoS[protopt-0 : protopt+1]
{
protoVAL.extElements = nil
}
case 268:
protoDollar = protoS[protopt-2 : protopt+1]
{
if protoDollar[2].extElement != nil {
protoVAL.extElements = append(protoDollar[1].extElements, protoDollar[2].extElement)
} else {
protoVAL.extElements = protoDollar[1].extElements
}
}
case 269:
protoDollar = protoS[protopt-1 : protopt+1]
{
if protoDollar[1].extElement != nil {
protoVAL.extElements = []ast.ExtendElement{protoDollar[1].extElement}
} else {
protoVAL.extElements = nil
}
}
case 270:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.extElement = protoDollar[1].fld
}
case 271:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.extElement = protoDollar[1].grp
}
case 272:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.extElement = nil
}
case 273:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.extElement = nil
}
case 274:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, nil, protoDollar[6].b)
}
case 275:
protoDollar = protoS[protopt-7 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(protoDollar[1].id.ToKeyword(), protoDollar[2].tid, protoDollar[3].id, protoDollar[4].b, protoDollar[5].i, protoDollar[6].cmpctOpts, protoDollar[7].b)
}
case 276:
protoDollar = protoS[protopt-5 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, nil, protoDollar[5].b)
}
case 277:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.fld = ast.NewFieldNode(nil, protoDollar[1].tid, protoDollar[2].id, protoDollar[3].b, protoDollar[4].i, protoDollar[5].cmpctOpts, protoDollar[6].b)
}
case 278:
protoDollar = protoS[protopt-6 : protopt+1]
{
protoVAL.svc = newNodeWithRunes(ast.NewServiceNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].b, protoDollar[4].svcElements, protoDollar[5].b), protoDollar[6].bs...)
}
case 279:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.svcElements = prependRunes(toServiceElement, protoDollar[1].bs, nil)
}
case 280:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.svcElements = prependRunes(toServiceElement, protoDollar[1].bs, protoDollar[2].svcElements)
}
case 281:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.svcElements = append(protoDollar[1].svcElements, protoDollar[2].svcElements...)
}
case 282:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.svcElements = protoDollar[1].svcElements
}
case 283:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.svcElements = toElements[ast.ServiceElement](toServiceElement, protoDollar[1].opt.Node, protoDollar[1].opt.Runes)
}
case 284:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.svcElements = toElements[ast.ServiceElement](toServiceElement, protoDollar[1].mtd.Node, protoDollar[1].mtd.Runes)
}
case 285:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.svcElements = nil
}
case 286:
protoDollar = protoS[protopt-6 : protopt+1]
{
semi, extra := protolex.(*protoLex).requireSemicolon(protoDollar[6].bs)
protoVAL.mtd = newNodeWithRunes(ast.NewRPCNode(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].mtdMsgType, protoDollar[4].id.ToKeyword(), protoDollar[5].mtdMsgType, semi), extra...)
}
case 287:
protoDollar = protoS[protopt-9 : protopt+1]
{
protoVAL.mtd = newNodeWithRunes(ast.NewRPCNodeWithBody(protoDollar[1].id.ToKeyword(), protoDollar[2].id, protoDollar[3].mtdMsgType, protoDollar[4].id.ToKeyword(), protoDollar[5].mtdMsgType, protoDollar[6].b, protoDollar[7].mtdElements, protoDollar[8].b), protoDollar[9].bs...)
}
case 288:
protoDollar = protoS[protopt-4 : protopt+1]
{
protoVAL.mtdMsgType = ast.NewRPCTypeNode(protoDollar[1].b, protoDollar[2].id.ToKeyword(), protoDollar[3].tid, protoDollar[4].b)
}
case 289:
protoDollar = protoS[protopt-3 : protopt+1]
{
protoVAL.mtdMsgType = ast.NewRPCTypeNode(protoDollar[1].b, nil, protoDollar[2].tid, protoDollar[3].b)
}
case 290:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.mtdElements = prependRunes(toMethodElement, protoDollar[1].bs, nil)
}
case 291:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.mtdElements = prependRunes(toMethodElement, protoDollar[1].bs, protoDollar[2].mtdElements)
}
case 292:
protoDollar = protoS[protopt-2 : protopt+1]
{
protoVAL.mtdElements = append(protoDollar[1].mtdElements, protoDollar[2].mtdElements...)
}
case 293:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.mtdElements = protoDollar[1].mtdElements
}
case 294:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.mtdElements = toElements[ast.RPCElement](toMethodElement, protoDollar[1].opt.Node, protoDollar[1].opt.Runes)
}
case 295:
protoDollar = protoS[protopt-1 : protopt+1]
{
protoVAL.mtdElements = nil
}
}
goto protostack /* stack new state and value */
}
// 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 (
"bytes"
"fmt"
"math"
"sort"
"strings"
"unicode"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/internal/editions"
"github.com/bufbuild/protocompile/reporter"
)
type result struct {
file *ast.FileNode
proto *descriptorpb.FileDescriptorProto
nodes map[proto.Message]ast.Node
ifNoAST *ast.NoSourceNode
}
// ResultWithoutAST returns a parse result that has no AST. All methods for
// looking up AST nodes return a placeholder node that contains only the filename
// in position information.
func ResultWithoutAST(proto *descriptorpb.FileDescriptorProto) Result {
return &result{proto: proto, ifNoAST: ast.NewNoSourceNode(proto.GetName())}
}
// ResultFromAST constructs a descriptor proto from the given AST. The returned
// result includes the descriptor proto and also contains an index that can be
// used to lookup AST node information for elements in the descriptor proto
// hierarchy.
//
// If validate is true, some basic validation is performed, to make sure the
// resulting descriptor proto is valid per protobuf rules and semantics. Only
// some language elements can be validated since some rules and semantics can
// only be checked after all symbols are all resolved, which happens in the
// linking step.
//
// The given handler is used to report any errors or warnings encountered. If any
// errors are reported, this function returns a non-nil error.
func ResultFromAST(file *ast.FileNode, validate bool, handler *reporter.Handler) (Result, error) {
filename := file.Name()
r := &result{file: file, nodes: map[proto.Message]ast.Node{}}
r.createFileDescriptor(filename, file, handler)
if validate {
validateBasic(r, handler)
}
// Now that we're done validating, we can set any missing labels to optional
// (we leave them absent in first pass if label was missing in source, so we
// can do validation on presence of label, but final descriptors are expected
// to always have them present).
fillInMissingLabels(r.proto)
return r, handler.Error()
}
func (r *result) AST() *ast.FileNode {
return r.file
}
func (r *result) FileDescriptorProto() *descriptorpb.FileDescriptorProto {
return r.proto
}
func (r *result) createFileDescriptor(filename string, file *ast.FileNode, handler *reporter.Handler) {
fd := &descriptorpb.FileDescriptorProto{Name: proto.String(filename)}
r.proto = fd
r.putFileNode(fd, file)
var syntax protoreflect.Syntax
switch {
case file.Syntax != nil:
switch file.Syntax.Syntax.AsString() {
case "proto3":
syntax = protoreflect.Proto3
case "proto2":
syntax = protoreflect.Proto2
default:
nodeInfo := file.NodeInfo(file.Syntax.Syntax)
if handler.HandleErrorf(nodeInfo, `syntax value must be "proto2" or "proto3"`) != nil {
return
}
}
// proto2 is the default, so no need to set for that value
if syntax != protoreflect.Proto2 {
fd.Syntax = proto.String(file.Syntax.Syntax.AsString())
}
case file.Edition != nil:
edition := file.Edition.Edition.AsString()
syntax = protoreflect.Editions
fd.Syntax = proto.String("editions")
editionEnum, ok := editions.SupportedEditions[edition]
if !ok {
nodeInfo := file.NodeInfo(file.Edition.Edition)
if _, isKnown := editions.KnownEditions[edition]; isKnown {
if handler.HandleErrorf(nodeInfo, `edition %q not yet fully supported; latest supported edition %q`, edition, strings.TrimPrefix(editions.MaxSupportedEdition.String(), "EDITION_")) != nil {
return
}
} else {
editionStrs := make([]string, 0, len(editions.SupportedEditions))
for supportedEdition := range editions.SupportedEditions {
editionStrs = append(editionStrs, fmt.Sprintf("%q", supportedEdition))
}
sort.Strings(editionStrs)
if handler.HandleErrorf(nodeInfo, `edition value %q not recognized; should be one of [%s]`, edition, strings.Join(editionStrs, ",")) != nil {
return
}
}
}
fd.Edition = editionEnum.Enum()
default:
syntax = protoreflect.Proto2
nodeInfo := file.NodeInfo(file)
handler.HandleWarningWithPos(nodeInfo, ErrNoSyntax)
}
for _, decl := range file.Decls {
if handler.ReporterError() != nil {
return
}
switch decl := decl.(type) {
case *ast.EnumNode:
fd.EnumType = append(fd.EnumType, r.asEnumDescriptor(decl, syntax, handler))
case *ast.ExtendNode:
r.addExtensions(decl, &fd.Extension, &fd.MessageType, syntax, handler, 0)
case *ast.ImportNode:
index := len(fd.Dependency)
fd.Dependency = append(fd.Dependency, decl.Name.AsString())
if decl.Public != nil {
fd.PublicDependency = append(fd.PublicDependency, int32(index))
} else if decl.Weak != nil {
fd.WeakDependency = append(fd.WeakDependency, int32(index))
}
case *ast.MessageNode:
fd.MessageType = append(fd.MessageType, r.asMessageDescriptor(decl, syntax, handler, 1))
case *ast.OptionNode:
if fd.Options == nil {
fd.Options = &descriptorpb.FileOptions{}
}
fd.Options.UninterpretedOption = append(fd.Options.UninterpretedOption, r.asUninterpretedOption(decl))
case *ast.ServiceNode:
fd.Service = append(fd.Service, r.asServiceDescriptor(decl))
case *ast.PackageNode:
if fd.Package != nil {
nodeInfo := file.NodeInfo(decl)
if handler.HandleErrorf(nodeInfo, "files should have only one package declaration") != nil {
return
}
}
pkgName := string(decl.Name.AsIdentifier())
if len(pkgName) >= 512 {
nodeInfo := file.NodeInfo(decl.Name)
if handler.HandleErrorf(nodeInfo, "package name (with whitespace removed) must be less than 512 characters long") != nil {
return
}
}
if strings.Count(pkgName, ".") > 100 {
nodeInfo := file.NodeInfo(decl.Name)
if handler.HandleErrorf(nodeInfo, "package name may not contain more than 100 periods") != nil {
return
}
}
fd.Package = proto.String(string(decl.Name.AsIdentifier()))
}
}
}
func (r *result) asUninterpretedOptions(nodes []*ast.OptionNode) []*descriptorpb.UninterpretedOption {
if len(nodes) == 0 {
return nil
}
opts := make([]*descriptorpb.UninterpretedOption, len(nodes))
for i, n := range nodes {
opts[i] = r.asUninterpretedOption(n)
}
return opts
}
func (r *result) asUninterpretedOption(node *ast.OptionNode) *descriptorpb.UninterpretedOption {
opt := &descriptorpb.UninterpretedOption{Name: r.asUninterpretedOptionName(node.Name.Parts)}
r.putOptionNode(opt, node)
switch val := node.Val.Value().(type) {
case bool:
if val {
opt.IdentifierValue = proto.String("true")
} else {
opt.IdentifierValue = proto.String("false")
}
case int64:
opt.NegativeIntValue = proto.Int64(val)
case uint64:
opt.PositiveIntValue = proto.Uint64(val)
case float64:
opt.DoubleValue = proto.Float64(val)
case string:
opt.StringValue = []byte(val)
case ast.Identifier:
opt.IdentifierValue = proto.String(string(val))
default:
// the grammar does not allow arrays here, so the only possible case
// left should be []*ast.MessageFieldNode, which corresponds to an
// *ast.MessageLiteralNode
if n, ok := node.Val.(*ast.MessageLiteralNode); ok {
var buf bytes.Buffer
for i, el := range n.Elements {
flattenNode(r.file, el, &buf)
if len(n.Seps) > i && n.Seps[i] != nil {
buf.WriteRune(' ')
buf.WriteRune(n.Seps[i].Rune)
}
}
aggStr := buf.String()
opt.AggregateValue = proto.String(aggStr)
}
// TODO: else that reports an error or panics??
}
return opt
}
func flattenNode(f *ast.FileNode, n ast.Node, buf *bytes.Buffer) {
if cn, ok := n.(ast.CompositeNode); ok {
for _, ch := range cn.Children() {
flattenNode(f, ch, buf)
}
return
}
if buf.Len() > 0 {
buf.WriteRune(' ')
}
buf.WriteString(f.NodeInfo(n).RawText())
}
func (r *result) asUninterpretedOptionName(parts []*ast.FieldReferenceNode) []*descriptorpb.UninterpretedOption_NamePart {
ret := make([]*descriptorpb.UninterpretedOption_NamePart, len(parts))
for i, part := range parts {
np := &descriptorpb.UninterpretedOption_NamePart{
NamePart: proto.String(string(part.Name.AsIdentifier())),
IsExtension: proto.Bool(part.IsExtension()),
}
r.putOptionNamePartNode(np, part)
ret[i] = np
}
return ret
}
func (r *result) addExtensions(ext *ast.ExtendNode, flds *[]*descriptorpb.FieldDescriptorProto, msgs *[]*descriptorpb.DescriptorProto, syntax protoreflect.Syntax, handler *reporter.Handler, depth int) {
extendee := string(ext.Extendee.AsIdentifier())
count := 0
for _, decl := range ext.Decls {
switch decl := decl.(type) {
case *ast.FieldNode:
count++
// use higher limit since we don't know yet whether extendee is messageset wire format
fd := r.asFieldDescriptor(decl, internal.MaxTag, syntax, handler)
fd.Extendee = proto.String(extendee)
*flds = append(*flds, fd)
case *ast.GroupNode:
count++
// ditto: use higher limit right now
fd, md := r.asGroupDescriptors(decl, syntax, internal.MaxTag, handler, depth+1)
fd.Extendee = proto.String(extendee)
*flds = append(*flds, fd)
*msgs = append(*msgs, md)
}
}
if count == 0 {
nodeInfo := r.file.NodeInfo(ext)
_ = handler.HandleErrorf(nodeInfo, "extend sections must define at least one extension")
}
}
func asLabel(lbl *ast.FieldLabel) *descriptorpb.FieldDescriptorProto_Label {
if !lbl.IsPresent() {
return nil
}
switch {
case lbl.Repeated:
return descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
case lbl.Required:
return descriptorpb.FieldDescriptorProto_LABEL_REQUIRED.Enum()
default:
return descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
}
}
func (r *result) asFieldDescriptor(node *ast.FieldNode, maxTag int32, syntax protoreflect.Syntax, handler *reporter.Handler) *descriptorpb.FieldDescriptorProto {
var tag *int32
if node.Tag != nil {
if err := r.checkTag(node.Tag, node.Tag.Val, maxTag); err != nil {
_ = handler.HandleError(err)
}
tag = proto.Int32(int32(node.Tag.Val))
}
fd := newFieldDescriptor(node.Name.Val, string(node.FldType.AsIdentifier()), tag, asLabel(&node.Label))
r.putFieldNode(fd, node)
if opts := node.Options.GetElements(); len(opts) > 0 {
fd.Options = &descriptorpb.FieldOptions{UninterpretedOption: r.asUninterpretedOptions(opts)}
}
if syntax == protoreflect.Proto3 && fd.Label != nil && fd.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL {
fd.Proto3Optional = proto.Bool(true)
}
return fd
}
var fieldTypes = map[string]descriptorpb.FieldDescriptorProto_Type{
"double": descriptorpb.FieldDescriptorProto_TYPE_DOUBLE,
"float": descriptorpb.FieldDescriptorProto_TYPE_FLOAT,
"int32": descriptorpb.FieldDescriptorProto_TYPE_INT32,
"int64": descriptorpb.FieldDescriptorProto_TYPE_INT64,
"uint32": descriptorpb.FieldDescriptorProto_TYPE_UINT32,
"uint64": descriptorpb.FieldDescriptorProto_TYPE_UINT64,
"sint32": descriptorpb.FieldDescriptorProto_TYPE_SINT32,
"sint64": descriptorpb.FieldDescriptorProto_TYPE_SINT64,
"fixed32": descriptorpb.FieldDescriptorProto_TYPE_FIXED32,
"fixed64": descriptorpb.FieldDescriptorProto_TYPE_FIXED64,
"sfixed32": descriptorpb.FieldDescriptorProto_TYPE_SFIXED32,
"sfixed64": descriptorpb.FieldDescriptorProto_TYPE_SFIXED64,
"bool": descriptorpb.FieldDescriptorProto_TYPE_BOOL,
"string": descriptorpb.FieldDescriptorProto_TYPE_STRING,
"bytes": descriptorpb.FieldDescriptorProto_TYPE_BYTES,
}
func newFieldDescriptor(name string, fieldType string, tag *int32, lbl *descriptorpb.FieldDescriptorProto_Label) *descriptorpb.FieldDescriptorProto {
fd := &descriptorpb.FieldDescriptorProto{
Name: proto.String(name),
JsonName: proto.String(internal.JSONName(name)),
Number: tag,
Label: lbl,
}
t, ok := fieldTypes[fieldType]
if ok {
fd.Type = t.Enum()
} else {
// NB: we don't have enough info to determine whether this is an enum
// or a message type, so we'll leave Type nil and set it later
// (during linking)
fd.TypeName = proto.String(fieldType)
}
return fd
}
func (r *result) asGroupDescriptors(group *ast.GroupNode, syntax protoreflect.Syntax, maxTag int32, handler *reporter.Handler, depth int) (*descriptorpb.FieldDescriptorProto, *descriptorpb.DescriptorProto) {
var tag *int32
if group.Tag != nil {
if err := r.checkTag(group.Tag, group.Tag.Val, maxTag); err != nil {
_ = handler.HandleError(err)
}
tag = proto.Int32(int32(group.Tag.Val))
}
if !unicode.IsUpper(rune(group.Name.Val[0])) {
nameNodeInfo := r.file.NodeInfo(group.Name)
_ = handler.HandleErrorf(nameNodeInfo, "group %s should have a name that starts with a capital letter", group.Name.Val)
}
fieldName := strings.ToLower(group.Name.Val)
fd := &descriptorpb.FieldDescriptorProto{
Name: proto.String(fieldName),
JsonName: proto.String(internal.JSONName(fieldName)),
Number: tag,
Label: asLabel(&group.Label),
Type: descriptorpb.FieldDescriptorProto_TYPE_GROUP.Enum(),
TypeName: proto.String(group.Name.Val),
}
r.putFieldNode(fd, group)
if opts := group.Options.GetElements(); len(opts) > 0 {
fd.Options = &descriptorpb.FieldOptions{UninterpretedOption: r.asUninterpretedOptions(opts)}
}
md := &descriptorpb.DescriptorProto{Name: proto.String(group.Name.Val)}
groupMsg := group.AsMessage()
r.putMessageNode(md, groupMsg)
// don't bother processing body if we've exceeded depth
if r.checkDepth(depth, groupMsg, handler) {
r.addMessageBody(md, &group.MessageBody, syntax, handler, depth)
}
return fd, md
}
func (r *result) asMapDescriptors(mapField *ast.MapFieldNode, syntax protoreflect.Syntax, maxTag int32, handler *reporter.Handler, depth int) (*descriptorpb.FieldDescriptorProto, *descriptorpb.DescriptorProto) {
var tag *int32
if mapField.Tag != nil {
if err := r.checkTag(mapField.Tag, mapField.Tag.Val, maxTag); err != nil {
_ = handler.HandleError(err)
}
tag = proto.Int32(int32(mapField.Tag.Val))
}
mapEntry := mapField.AsMessage()
r.checkDepth(depth, mapEntry, handler)
var lbl *descriptorpb.FieldDescriptorProto_Label
if syntax == protoreflect.Proto2 {
lbl = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
}
keyFd := newFieldDescriptor("key", mapField.MapType.KeyType.Val, proto.Int32(1), lbl)
r.putFieldNode(keyFd, mapField.KeyField())
valFd := newFieldDescriptor("value", string(mapField.MapType.ValueType.AsIdentifier()), proto.Int32(2), lbl)
r.putFieldNode(valFd, mapField.ValueField())
entryName := internal.MapEntry(mapField.Name.Val)
fd := newFieldDescriptor(mapField.Name.Val, entryName, tag, descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum())
if opts := mapField.Options.GetElements(); len(opts) > 0 {
fd.Options = &descriptorpb.FieldOptions{UninterpretedOption: r.asUninterpretedOptions(opts)}
}
r.putFieldNode(fd, mapField)
md := &descriptorpb.DescriptorProto{
Name: proto.String(entryName),
Options: &descriptorpb.MessageOptions{MapEntry: proto.Bool(true)},
Field: []*descriptorpb.FieldDescriptorProto{keyFd, valFd},
}
r.putMessageNode(md, mapEntry)
return fd, md
}
func (r *result) asExtensionRanges(node *ast.ExtensionRangeNode, maxTag int32, handler *reporter.Handler) []*descriptorpb.DescriptorProto_ExtensionRange {
opts := r.asUninterpretedOptions(node.Options.GetElements())
ers := make([]*descriptorpb.DescriptorProto_ExtensionRange, len(node.Ranges))
for i, rng := range node.Ranges {
start, end := r.getRangeBounds(rng, 1, maxTag, handler)
er := &descriptorpb.DescriptorProto_ExtensionRange{
Start: proto.Int32(start),
End: proto.Int32(end + 1),
}
if len(opts) > 0 {
er.Options = &descriptorpb.ExtensionRangeOptions{UninterpretedOption: opts}
}
r.putExtensionRangeNode(er, node, rng)
ers[i] = er
}
return ers
}
func (r *result) asEnumValue(ev *ast.EnumValueNode, handler *reporter.Handler) *descriptorpb.EnumValueDescriptorProto {
num, ok := ast.AsInt32(ev.Number, math.MinInt32, math.MaxInt32)
if !ok {
numberNodeInfo := r.file.NodeInfo(ev.Number)
_ = handler.HandleErrorf(numberNodeInfo, "value %d is out of range: should be between %d and %d", ev.Number.Value(), math.MinInt32, math.MaxInt32)
}
evd := &descriptorpb.EnumValueDescriptorProto{Name: proto.String(ev.Name.Val), Number: proto.Int32(num)}
r.putEnumValueNode(evd, ev)
if opts := ev.Options.GetElements(); len(opts) > 0 {
evd.Options = &descriptorpb.EnumValueOptions{UninterpretedOption: r.asUninterpretedOptions(opts)}
}
return evd
}
func (r *result) asMethodDescriptor(node *ast.RPCNode) *descriptorpb.MethodDescriptorProto {
md := &descriptorpb.MethodDescriptorProto{
Name: proto.String(node.Name.Val),
InputType: proto.String(string(node.Input.MessageType.AsIdentifier())),
OutputType: proto.String(string(node.Output.MessageType.AsIdentifier())),
}
r.putMethodNode(md, node)
if node.Input.Stream != nil {
md.ClientStreaming = proto.Bool(true)
}
if node.Output.Stream != nil {
md.ServerStreaming = proto.Bool(true)
}
// protoc always adds a MethodOptions if there are brackets
// We do the same to match protoc as closely as possible
// https://github.com/protocolbuffers/protobuf/blob/0c3f43a6190b77f1f68b7425d1b7e1a8257a8d0c/src/google/protobuf/compiler/parser.cc#L2152
if node.OpenBrace != nil {
md.Options = &descriptorpb.MethodOptions{}
for _, decl := range node.Decls {
if option, ok := decl.(*ast.OptionNode); ok {
md.Options.UninterpretedOption = append(md.Options.UninterpretedOption, r.asUninterpretedOption(option))
}
}
}
return md
}
func (r *result) asEnumDescriptor(en *ast.EnumNode, syntax protoreflect.Syntax, handler *reporter.Handler) *descriptorpb.EnumDescriptorProto {
ed := &descriptorpb.EnumDescriptorProto{Name: proto.String(en.Name.Val)}
r.putEnumNode(ed, en)
rsvdNames := map[string]ast.SourcePos{}
for _, decl := range en.Decls {
switch decl := decl.(type) {
case *ast.OptionNode:
if ed.Options == nil {
ed.Options = &descriptorpb.EnumOptions{}
}
ed.Options.UninterpretedOption = append(ed.Options.UninterpretedOption, r.asUninterpretedOption(decl))
case *ast.EnumValueNode:
ed.Value = append(ed.Value, r.asEnumValue(decl, handler))
case *ast.ReservedNode:
r.addReservedNames(&ed.ReservedName, decl, syntax, handler, rsvdNames)
for _, rng := range decl.Ranges {
ed.ReservedRange = append(ed.ReservedRange, r.asEnumReservedRange(rng, handler))
}
}
}
return ed
}
func (r *result) asEnumReservedRange(rng *ast.RangeNode, handler *reporter.Handler) *descriptorpb.EnumDescriptorProto_EnumReservedRange {
start, end := r.getRangeBounds(rng, math.MinInt32, math.MaxInt32, handler)
rr := &descriptorpb.EnumDescriptorProto_EnumReservedRange{
Start: proto.Int32(start),
End: proto.Int32(end),
}
r.putEnumReservedRangeNode(rr, rng)
return rr
}
func (r *result) asMessageDescriptor(node *ast.MessageNode, syntax protoreflect.Syntax, handler *reporter.Handler, depth int) *descriptorpb.DescriptorProto {
msgd := &descriptorpb.DescriptorProto{Name: proto.String(node.Name.Val)}
r.putMessageNode(msgd, node)
// don't bother processing body if we've exceeded depth
if r.checkDepth(depth, node, handler) {
r.addMessageBody(msgd, &node.MessageBody, syntax, handler, depth)
}
return msgd
}
func (r *result) addReservedNames(names *[]string, node *ast.ReservedNode, syntax protoreflect.Syntax, handler *reporter.Handler, alreadyReserved map[string]ast.SourcePos) {
if syntax == protoreflect.Editions {
if len(node.Names) > 0 {
nameNodeInfo := r.file.NodeInfo(node.Names[0])
_ = handler.HandleErrorf(nameNodeInfo, `must use identifiers, not string literals, to reserved names with editions`)
}
for _, n := range node.Identifiers {
name := string(n.AsIdentifier())
nameNodeInfo := r.file.NodeInfo(n)
if existing, ok := alreadyReserved[name]; ok {
_ = handler.HandleErrorf(nameNodeInfo, "name %q is already reserved at %s", name, existing)
continue
}
alreadyReserved[name] = nameNodeInfo.Start()
*names = append(*names, name)
}
return
}
if len(node.Identifiers) > 0 {
nameNodeInfo := r.file.NodeInfo(node.Identifiers[0])
_ = handler.HandleErrorf(nameNodeInfo, `must use string literals, not identifiers, to reserved names with proto2 and proto3`)
}
for _, n := range node.Names {
name := n.AsString()
nameNodeInfo := r.file.NodeInfo(n)
if existing, ok := alreadyReserved[name]; ok {
_ = handler.HandleErrorf(nameNodeInfo, "name %q is already reserved at %s", name, existing)
continue
}
alreadyReserved[name] = nameNodeInfo.Start()
*names = append(*names, name)
}
}
func (r *result) checkDepth(depth int, node ast.MessageDeclNode, handler *reporter.Handler) bool {
if depth < 32 {
return true
}
n := ast.Node(node)
if grp, ok := n.(*ast.SyntheticGroupMessageNode); ok {
// pinpoint the group keyword if the source is a group
n = grp.Keyword
}
_ = handler.HandleErrorf(r.file.NodeInfo(n), "message nesting depth must be less than 32")
return false
}
func (r *result) addMessageBody(msgd *descriptorpb.DescriptorProto, body *ast.MessageBody, syntax protoreflect.Syntax, handler *reporter.Handler, depth int) {
// first process any options
for _, decl := range body.Decls {
if opt, ok := decl.(*ast.OptionNode); ok {
if msgd.Options == nil {
msgd.Options = &descriptorpb.MessageOptions{}
}
msgd.Options.UninterpretedOption = append(msgd.Options.UninterpretedOption, r.asUninterpretedOption(opt))
}
}
// now that we have options, we can see if this uses messageset wire format, which
// impacts how we validate tag numbers in any fields in the message
maxTag := int32(internal.MaxNormalTag)
messageSetOpt, err := r.isMessageSetWireFormat("message "+msgd.GetName(), msgd, handler)
if err != nil {
return
} else if messageSetOpt != nil {
if syntax == protoreflect.Proto3 {
node := r.OptionNode(messageSetOpt)
nodeInfo := r.file.NodeInfo(node)
_ = handler.HandleErrorf(nodeInfo, "messages with message-set wire format are not allowed with proto3 syntax")
}
maxTag = internal.MaxTag // higher limit for messageset wire format
}
rsvdNames := map[string]ast.SourcePos{}
// now we can process the rest
for _, decl := range body.Decls {
switch decl := decl.(type) {
case *ast.EnumNode:
msgd.EnumType = append(msgd.EnumType, r.asEnumDescriptor(decl, syntax, handler))
case *ast.ExtendNode:
r.addExtensions(decl, &msgd.Extension, &msgd.NestedType, syntax, handler, depth)
case *ast.ExtensionRangeNode:
msgd.ExtensionRange = append(msgd.ExtensionRange, r.asExtensionRanges(decl, maxTag, handler)...)
case *ast.FieldNode:
fd := r.asFieldDescriptor(decl, maxTag, syntax, handler)
msgd.Field = append(msgd.Field, fd)
case *ast.MapFieldNode:
fd, md := r.asMapDescriptors(decl, syntax, maxTag, handler, depth+1)
msgd.Field = append(msgd.Field, fd)
msgd.NestedType = append(msgd.NestedType, md)
case *ast.GroupNode:
fd, md := r.asGroupDescriptors(decl, syntax, maxTag, handler, depth+1)
msgd.Field = append(msgd.Field, fd)
msgd.NestedType = append(msgd.NestedType, md)
case *ast.OneofNode:
oodIndex := len(msgd.OneofDecl)
ood := &descriptorpb.OneofDescriptorProto{Name: proto.String(decl.Name.Val)}
r.putOneofNode(ood, decl)
msgd.OneofDecl = append(msgd.OneofDecl, ood)
ooFields := 0
for _, oodecl := range decl.Decls {
switch oodecl := oodecl.(type) {
case *ast.OptionNode:
if ood.Options == nil {
ood.Options = &descriptorpb.OneofOptions{}
}
ood.Options.UninterpretedOption = append(ood.Options.UninterpretedOption, r.asUninterpretedOption(oodecl))
case *ast.FieldNode:
fd := r.asFieldDescriptor(oodecl, maxTag, syntax, handler)
fd.OneofIndex = proto.Int32(int32(oodIndex))
msgd.Field = append(msgd.Field, fd)
ooFields++
case *ast.GroupNode:
fd, md := r.asGroupDescriptors(oodecl, syntax, maxTag, handler, depth+1)
fd.OneofIndex = proto.Int32(int32(oodIndex))
msgd.Field = append(msgd.Field, fd)
msgd.NestedType = append(msgd.NestedType, md)
ooFields++
}
}
if ooFields == 0 {
declNodeInfo := r.file.NodeInfo(decl)
_ = handler.HandleErrorf(declNodeInfo, "oneof must contain at least one field")
}
case *ast.MessageNode:
msgd.NestedType = append(msgd.NestedType, r.asMessageDescriptor(decl, syntax, handler, depth+1))
case *ast.ReservedNode:
r.addReservedNames(&msgd.ReservedName, decl, syntax, handler, rsvdNames)
for _, rng := range decl.Ranges {
msgd.ReservedRange = append(msgd.ReservedRange, r.asMessageReservedRange(rng, maxTag, handler))
}
}
}
if messageSetOpt != nil {
if len(msgd.Field) > 0 {
node := r.FieldNode(msgd.Field[0])
nodeInfo := r.file.NodeInfo(node)
_ = handler.HandleErrorf(nodeInfo, "messages with message-set wire format cannot contain non-extension fields")
}
if len(msgd.ExtensionRange) == 0 {
node := r.OptionNode(messageSetOpt)
nodeInfo := r.file.NodeInfo(node)
_ = handler.HandleErrorf(nodeInfo, "messages with message-set wire format must contain at least one extension range")
}
}
// process any proto3_optional fields
if syntax == protoreflect.Proto3 {
r.processProto3OptionalFields(msgd)
}
}
func (r *result) isMessageSetWireFormat(scope string, md *descriptorpb.DescriptorProto, handler *reporter.Handler) (*descriptorpb.UninterpretedOption, error) {
uo := md.GetOptions().GetUninterpretedOption()
index, err := internal.FindOption(r, handler.HandleErrorf, scope, uo, "message_set_wire_format")
if err != nil {
return nil, err
}
if index == -1 {
// no such option
return nil, nil
}
opt := uo[index]
switch opt.GetIdentifierValue() {
case "true":
return opt, nil
case "false":
return nil, nil
default:
optNode := r.OptionNode(opt)
optNodeInfo := r.file.NodeInfo(optNode.GetValue())
return nil, handler.HandleErrorf(optNodeInfo, "%s: expecting bool value for message_set_wire_format option", scope)
}
}
func (r *result) asMessageReservedRange(rng *ast.RangeNode, maxTag int32, handler *reporter.Handler) *descriptorpb.DescriptorProto_ReservedRange {
start, end := r.getRangeBounds(rng, 1, maxTag, handler)
rr := &descriptorpb.DescriptorProto_ReservedRange{
Start: proto.Int32(start),
End: proto.Int32(end + 1),
}
r.putMessageReservedRangeNode(rr, rng)
return rr
}
func (r *result) getRangeBounds(rng *ast.RangeNode, minVal, maxVal int32, handler *reporter.Handler) (int32, int32) {
checkOrder := true
start, ok := rng.StartValueAsInt32(minVal, maxVal)
if !ok {
checkOrder = false
startValNodeInfo := r.file.NodeInfo(rng.StartVal)
_ = handler.HandleErrorf(startValNodeInfo, "range start %d is out of range: should be between %d and %d", rng.StartValue(), minVal, maxVal)
}
end, ok := rng.EndValueAsInt32(minVal, maxVal)
if !ok {
checkOrder = false
if rng.EndVal != nil {
endValNodeInfo := r.file.NodeInfo(rng.EndVal)
_ = handler.HandleErrorf(endValNodeInfo, "range end %d is out of range: should be between %d and %d", rng.EndValue(), minVal, maxVal)
}
}
if checkOrder && start > end {
rangeStartNodeInfo := r.file.NodeInfo(rng.RangeStart())
_ = handler.HandleErrorf(rangeStartNodeInfo, "range, %d to %d, is invalid: start must be <= end", start, end)
}
return start, end
}
func (r *result) asServiceDescriptor(svc *ast.ServiceNode) *descriptorpb.ServiceDescriptorProto {
sd := &descriptorpb.ServiceDescriptorProto{Name: proto.String(svc.Name.Val)}
r.putServiceNode(sd, svc)
for _, decl := range svc.Decls {
switch decl := decl.(type) {
case *ast.OptionNode:
if sd.Options == nil {
sd.Options = &descriptorpb.ServiceOptions{}
}
sd.Options.UninterpretedOption = append(sd.Options.UninterpretedOption, r.asUninterpretedOption(decl))
case *ast.RPCNode:
sd.Method = append(sd.Method, r.asMethodDescriptor(decl))
}
}
return sd
}
func (r *result) checkTag(n ast.Node, v uint64, maxTag int32) error {
switch {
case v < 1:
return reporter.Errorf(r.file.NodeInfo(n), "tag number %d must be greater than zero", v)
case v > uint64(maxTag):
return reporter.Errorf(r.file.NodeInfo(n), "tag number %d is higher than max allowed tag number (%d)", v, maxTag)
case v >= internal.SpecialReservedStart && v <= internal.SpecialReservedEnd:
return reporter.Errorf(r.file.NodeInfo(n), "tag number %d is in disallowed reserved range %d-%d", v, internal.SpecialReservedStart, internal.SpecialReservedEnd)
default:
return nil
}
}
// processProto3OptionalFields adds synthetic oneofs to the given message descriptor
// for each proto3 optional field. It also updates the fields to have the correct
// oneof index reference.
func (r *result) processProto3OptionalFields(msgd *descriptorpb.DescriptorProto) {
// add synthetic oneofs to the given message descriptor for each proto3
// optional field, and update each field to have correct oneof index
var allNames map[string]struct{}
for _, fd := range msgd.Field {
if fd.GetProto3Optional() {
// lazy init the set of all names
if allNames == nil {
allNames = map[string]struct{}{}
for _, fd := range msgd.Field {
allNames[fd.GetName()] = struct{}{}
}
for _, od := range msgd.OneofDecl {
allNames[od.GetName()] = struct{}{}
}
// NB: protoc only considers names of other fields and oneofs
// when computing the synthetic oneof name. But that feels like
// a bug, since it means it could generate a name that conflicts
// with some other symbol defined in the message. If it's decided
// that's NOT a bug and is desirable, then we should remove the
// following four loops to mimic protoc's behavior.
for _, fd := range msgd.Extension {
allNames[fd.GetName()] = struct{}{}
}
for _, ed := range msgd.EnumType {
allNames[ed.GetName()] = struct{}{}
for _, evd := range ed.Value {
allNames[evd.GetName()] = struct{}{}
}
}
for _, fd := range msgd.NestedType {
allNames[fd.GetName()] = struct{}{}
}
}
// Compute a name for the synthetic oneof. This uses the same
// algorithm as used in protoc:
// https://github.com/protocolbuffers/protobuf/blob/74ad62759e0a9b5a21094f3fb9bb4ebfaa0d1ab8/src/google/protobuf/compiler/parser.cc#L785-L803
ooName := fd.GetName()
if !strings.HasPrefix(ooName, "_") {
ooName = "_" + ooName
}
for {
_, ok := allNames[ooName]
if !ok {
// found a unique name
allNames[ooName] = struct{}{}
break
}
ooName = "X" + ooName
}
fd.OneofIndex = proto.Int32(int32(len(msgd.OneofDecl)))
ood := &descriptorpb.OneofDescriptorProto{Name: proto.String(ooName)}
msgd.OneofDecl = append(msgd.OneofDecl, ood)
ooident := r.FieldNode(fd).(*ast.FieldNode) //nolint:errcheck
r.putOneofNode(ood, ast.NewSyntheticOneof(ooident))
}
}
}
func (r *result) Node(m proto.Message) ast.Node {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[m]
}
func (r *result) FileNode() ast.FileDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[r.proto].(ast.FileDeclNode) //nolint:errcheck
}
func (r *result) OptionNode(o *descriptorpb.UninterpretedOption) ast.OptionDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[o].(ast.OptionDeclNode) //nolint:errcheck
}
func (r *result) OptionNamePartNode(o *descriptorpb.UninterpretedOption_NamePart) ast.Node {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[o]
}
func (r *result) MessageNode(m *descriptorpb.DescriptorProto) ast.MessageDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[m].(ast.MessageDeclNode) //nolint:errcheck
}
func (r *result) FieldNode(f *descriptorpb.FieldDescriptorProto) ast.FieldDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[f].(ast.FieldDeclNode) //nolint:errcheck
}
func (r *result) OneofNode(o *descriptorpb.OneofDescriptorProto) ast.OneofDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[o].(ast.OneofDeclNode) //nolint:errcheck
}
func (r *result) ExtensionsNode(e *descriptorpb.DescriptorProto_ExtensionRange) ast.NodeWithOptions {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[asExtsNode(e)].(ast.NodeWithOptions) //nolint:errcheck
}
func (r *result) ExtensionRangeNode(e *descriptorpb.DescriptorProto_ExtensionRange) ast.RangeDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[e].(ast.RangeDeclNode) //nolint:errcheck
}
func (r *result) MessageReservedRangeNode(rr *descriptorpb.DescriptorProto_ReservedRange) ast.RangeDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[rr].(ast.RangeDeclNode) //nolint:errcheck
}
func (r *result) EnumNode(e *descriptorpb.EnumDescriptorProto) ast.NodeWithOptions {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[e].(ast.NodeWithOptions) //nolint:errcheck
}
func (r *result) EnumValueNode(e *descriptorpb.EnumValueDescriptorProto) ast.EnumValueDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[e].(ast.EnumValueDeclNode) //nolint:errcheck
}
func (r *result) EnumReservedRangeNode(rr *descriptorpb.EnumDescriptorProto_EnumReservedRange) ast.RangeDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[rr].(ast.RangeDeclNode) //nolint:errcheck
}
func (r *result) ServiceNode(s *descriptorpb.ServiceDescriptorProto) ast.NodeWithOptions {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[s].(ast.NodeWithOptions) //nolint:errcheck
}
func (r *result) MethodNode(m *descriptorpb.MethodDescriptorProto) ast.RPCDeclNode {
if r.nodes == nil {
return r.ifNoAST
}
return r.nodes[m].(ast.RPCDeclNode) //nolint:errcheck
}
func (r *result) putFileNode(f *descriptorpb.FileDescriptorProto, n *ast.FileNode) {
r.nodes[f] = n
}
func (r *result) putOptionNode(o *descriptorpb.UninterpretedOption, n *ast.OptionNode) {
r.nodes[o] = n
}
func (r *result) putOptionNamePartNode(o *descriptorpb.UninterpretedOption_NamePart, n *ast.FieldReferenceNode) {
r.nodes[o] = n
}
func (r *result) putMessageNode(m *descriptorpb.DescriptorProto, n ast.MessageDeclNode) {
r.nodes[m] = n
}
func (r *result) putFieldNode(f *descriptorpb.FieldDescriptorProto, n ast.FieldDeclNode) {
r.nodes[f] = n
}
func (r *result) putOneofNode(o *descriptorpb.OneofDescriptorProto, n ast.OneofDeclNode) {
r.nodes[o] = n
}
func (r *result) putExtensionRangeNode(e *descriptorpb.DescriptorProto_ExtensionRange, er *ast.ExtensionRangeNode, n *ast.RangeNode) {
r.nodes[asExtsNode(e)] = er
r.nodes[e] = n
}
func (r *result) putMessageReservedRangeNode(rr *descriptorpb.DescriptorProto_ReservedRange, n *ast.RangeNode) {
r.nodes[rr] = n
}
func (r *result) putEnumNode(e *descriptorpb.EnumDescriptorProto, n *ast.EnumNode) {
r.nodes[e] = n
}
func (r *result) putEnumValueNode(e *descriptorpb.EnumValueDescriptorProto, n *ast.EnumValueNode) {
r.nodes[e] = n
}
func (r *result) putEnumReservedRangeNode(rr *descriptorpb.EnumDescriptorProto_EnumReservedRange, n *ast.RangeNode) {
r.nodes[rr] = n
}
func (r *result) putServiceNode(s *descriptorpb.ServiceDescriptorProto, n *ast.ServiceNode) {
r.nodes[s] = n
}
func (r *result) putMethodNode(m *descriptorpb.MethodDescriptorProto, n *ast.RPCNode) {
r.nodes[m] = n
}
// NB: If we ever add other put*Node methods, to index other kinds of elements in the descriptor
// proto hierarchy, we need to update the index recreation logic in clone.go, too.
func asExtsNode(er *descriptorpb.DescriptorProto_ExtensionRange) proto.Message {
return extsParent{er}
}
// a simple marker type that allows us to have two distinct keys in a map for
// the same ExtensionRange proto -- one for the range itself and another to
// associate with the enclosing/parent AST node.
type extsParent struct {
*descriptorpb.DescriptorProto_ExtensionRange
}
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"sort"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/walk"
)
func validateBasic(res *result, handler *reporter.Handler) {
fd := res.proto
var syntax protoreflect.Syntax
switch fd.GetSyntax() {
case "", "proto2":
syntax = protoreflect.Proto2
case "proto3":
syntax = protoreflect.Proto3
case "editions":
syntax = protoreflect.Editions
// TODO: default: error?
}
if err := validateImports(res, handler); err != nil {
return
}
if err := validateExportLocal(res, handler); err != nil {
return
}
if err := validateNoFeatures(res, syntax, "file options", fd.Options.GetUninterpretedOption(), handler); err != nil {
return
}
_ = walk.DescriptorProtos(fd,
func(name protoreflect.FullName, d proto.Message) error {
switch d := d.(type) {
case *descriptorpb.DescriptorProto:
if err := validateMessage(res, syntax, name, d, handler); err != nil {
// exit func is not called when enter returns error
return err
}
case *descriptorpb.FieldDescriptorProto:
if err := validateField(res, syntax, name, d, handler); err != nil {
return err
}
case *descriptorpb.OneofDescriptorProto:
if err := validateNoFeatures(res, syntax, fmt.Sprintf("oneof %s", name), d.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
case *descriptorpb.EnumDescriptorProto:
if err := validateEnum(res, syntax, name, d, handler); err != nil {
return err
}
case *descriptorpb.EnumValueDescriptorProto:
if err := validateNoFeatures(res, syntax, fmt.Sprintf("enum value %s", name), d.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
case *descriptorpb.ServiceDescriptorProto:
if err := validateNoFeatures(res, syntax, fmt.Sprintf("service %s", name), d.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
case *descriptorpb.MethodDescriptorProto:
if err := validateNoFeatures(res, syntax, fmt.Sprintf("method %s", name), d.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
}
return nil
})
}
func validateImports(res *result, handler *reporter.Handler) error {
fileNode := res.file
if fileNode == nil {
return nil
}
supportsImportOption := false
if fileNode.Edition != nil {
editionStr := fileNode.Edition.Edition.AsString()
supportsImportOption = editionStr == "2024"
}
imports := make(map[string]ast.SourcePos)
for _, decl := range fileNode.Decls {
imp, ok := decl.(*ast.ImportNode)
if !ok {
continue
}
info := fileNode.NodeInfo(decl)
name := imp.Name.AsString()
// check if "import option" syntax is used and supported
if imp.Modifier != nil && imp.Modifier.Val == "option" && !supportsImportOption {
optionInfo := fileNode.NodeInfo(imp.Modifier)
return handler.HandleErrorf(optionInfo, "import option syntax is only allowed in edition 2024")
}
if prev, ok := imports[name]; ok {
return handler.HandleErrorf(info, "%q was already imported at %v", name, prev)
}
imports[name] = info.Start()
}
return nil
}
func validateExportLocal(res *result, handler *reporter.Handler) error {
fileNode := res.file
if fileNode == nil {
return nil
}
if fileNode.Edition != nil && fileNode.Edition.Edition.AsString() == "2024" {
return nil // export/local modifiers supported
}
for _, decl := range fileNode.Decls {
if err := validateExportLocalNode(res, decl, handler); err != nil {
return err
}
}
return nil
}
func validateExportLocalInMessageBody(res *result, body *ast.MessageBody, handler *reporter.Handler) error {
for _, decl := range body.Decls {
if err := validateExportLocalNode(res, decl, handler); err != nil {
return err
}
}
return nil
}
func validateExportLocalNode(res *result, node ast.Node, handler *reporter.Handler) error {
fileNode := res.file
if fileNode == nil {
return nil
}
switch node := node.(type) {
case *ast.MessageNode:
if node.Visibility != nil {
visibilityInfo := fileNode.NodeInfo(node.Visibility)
visibilityKeyword := node.Visibility.Val
return handler.HandleErrorf(visibilityInfo, "%s keyword is only allowed in edition 2024", visibilityKeyword)
}
// check nested messages and enums recursively
if err := validateExportLocalInMessageBody(res, &node.MessageBody, handler); err != nil {
return err
}
case *ast.EnumNode:
if node.Visibility != nil {
visibilityInfo := fileNode.NodeInfo(node.Visibility)
visibilityKeyword := node.Visibility.Val
return handler.HandleErrorf(visibilityInfo, "%s keyword is only allowed in edition 2024", visibilityKeyword)
}
}
return nil
}
func validateNoFeatures(res *result, syntax protoreflect.Syntax, scope string, opts []*descriptorpb.UninterpretedOption, handler *reporter.Handler) error {
if syntax == protoreflect.Editions {
// Editions is allowed to use features
return nil
}
if index, err := internal.FindFirstOption(res, handler.HandleErrorf, scope, opts, "features"); err != nil {
return err
} else if index >= 0 {
optNode := res.OptionNode(opts[index])
optNameNodeInfo := res.file.NodeInfo(optNode.GetName())
if err := handler.HandleErrorf(optNameNodeInfo, "%s: option 'features' may only be used with editions but file uses %s syntax", scope, syntax); err != nil {
return err
}
}
return nil
}
func validateMessage(res *result, syntax protoreflect.Syntax, name protoreflect.FullName, md *descriptorpb.DescriptorProto, handler *reporter.Handler) error {
scope := fmt.Sprintf("message %s", name)
if syntax == protoreflect.Proto3 && len(md.ExtensionRange) > 0 {
n := res.ExtensionRangeNode(md.ExtensionRange[0])
nInfo := res.file.NodeInfo(n)
if err := handler.HandleErrorf(nInfo, "%s: extension ranges are not allowed in proto3", scope); err != nil {
return err
}
}
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, md.Options.GetUninterpretedOption(), "map_entry"); err != nil {
return err
} else if index >= 0 {
optNode := res.OptionNode(md.Options.GetUninterpretedOption()[index])
optNameNodeInfo := res.file.NodeInfo(optNode.GetName())
if err := handler.HandleErrorf(optNameNodeInfo, "%s: map_entry option should not be set explicitly; use map type instead", scope); err != nil {
return err
}
}
if err := validateNoFeatures(res, syntax, scope, md.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
// reserved ranges should not overlap
rsvd := make(tagRanges, len(md.ReservedRange))
for i, r := range md.ReservedRange {
n := res.MessageReservedRangeNode(r)
rsvd[i] = tagRange{start: r.GetStart(), end: r.GetEnd(), node: n}
}
sort.Sort(rsvd)
for i := 1; i < len(rsvd); i++ {
if rsvd[i].start < rsvd[i-1].end {
rangeNodeInfo := res.file.NodeInfo(rsvd[i].node)
if err := handler.HandleErrorf(rangeNodeInfo, "%s: reserved ranges overlap: %d to %d and %d to %d", scope, rsvd[i-1].start, rsvd[i-1].end-1, rsvd[i].start, rsvd[i].end-1); err != nil {
return err
}
}
}
// extensions ranges should not overlap
exts := make(tagRanges, len(md.ExtensionRange))
for i, r := range md.ExtensionRange {
if err := validateNoFeatures(res, syntax, scope, r.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
n := res.ExtensionRangeNode(r)
exts[i] = tagRange{start: r.GetStart(), end: r.GetEnd(), node: n}
}
sort.Sort(exts)
for i := 1; i < len(exts); i++ {
if exts[i].start < exts[i-1].end {
rangeNodeInfo := res.file.NodeInfo(exts[i].node)
if err := handler.HandleErrorf(rangeNodeInfo, "%s: extension ranges overlap: %d to %d and %d to %d", scope, exts[i-1].start, exts[i-1].end-1, exts[i].start, exts[i].end-1); err != nil {
return err
}
}
}
// see if any extension range overlaps any reserved range
var i, j int // i indexes rsvd; j indexes exts
for i < len(rsvd) && j < len(exts) {
if rsvd[i].start >= exts[j].start && rsvd[i].start < exts[j].end ||
exts[j].start >= rsvd[i].start && exts[j].start < rsvd[i].end {
var span ast.SourceSpan
if rsvd[i].start >= exts[j].start && rsvd[i].start < exts[j].end {
rangeNodeInfo := res.file.NodeInfo(rsvd[i].node)
span = rangeNodeInfo
} else {
rangeNodeInfo := res.file.NodeInfo(exts[j].node)
span = rangeNodeInfo
}
// ranges overlap
if err := handler.HandleErrorf(span, "%s: extension range %d to %d overlaps reserved range %d to %d", scope, exts[j].start, exts[j].end-1, rsvd[i].start, rsvd[i].end-1); err != nil {
return err
}
}
if rsvd[i].start < exts[j].start {
i++
} else {
j++
}
}
// now, check that fields don't re-use tags and don't try to use extension
// or reserved ranges or reserved names
rsvdNames := map[string]struct{}{}
for _, n := range md.ReservedName {
// validate reserved name while we're here
if !isIdentifier(n) {
node := findMessageReservedNameNode(res.MessageNode(md), n)
nodeInfo := res.file.NodeInfo(node)
if err := handler.HandleErrorf(nodeInfo, "%s: reserved name %q is not a valid identifier", scope, n); err != nil {
return err
}
}
rsvdNames[n] = struct{}{}
}
fieldTags := map[int32]string{}
for _, fld := range md.Field {
fn := res.FieldNode(fld)
if _, ok := rsvdNames[fld.GetName()]; ok {
fieldNameNodeInfo := res.file.NodeInfo(fn.FieldName())
if err := handler.HandleErrorf(fieldNameNodeInfo, "%s: field %s is using a reserved name", scope, fld.GetName()); err != nil {
return err
}
}
if existing := fieldTags[fld.GetNumber()]; existing != "" {
fieldTagNodeInfo := res.file.NodeInfo(fn.FieldTag())
if err := handler.HandleErrorf(fieldTagNodeInfo, "%s: fields %s and %s both have the same tag %d", scope, existing, fld.GetName(), fld.GetNumber()); err != nil {
return err
}
}
fieldTags[fld.GetNumber()] = fld.GetName()
// check reserved ranges
r := sort.Search(len(rsvd), func(index int) bool { return rsvd[index].end > fld.GetNumber() })
if r < len(rsvd) && rsvd[r].start <= fld.GetNumber() {
fieldTagNodeInfo := res.file.NodeInfo(fn.FieldTag())
if err := handler.HandleErrorf(fieldTagNodeInfo, "%s: field %s is using tag %d which is in reserved range %d to %d", scope, fld.GetName(), fld.GetNumber(), rsvd[r].start, rsvd[r].end-1); err != nil {
return err
}
}
// and check extension ranges
e := sort.Search(len(exts), func(index int) bool { return exts[index].end > fld.GetNumber() })
if e < len(exts) && exts[e].start <= fld.GetNumber() {
fieldTagNodeInfo := res.file.NodeInfo(fn.FieldTag())
if err := handler.HandleErrorf(fieldTagNodeInfo, "%s: field %s is using tag %d which is in extension range %d to %d", scope, fld.GetName(), fld.GetNumber(), exts[e].start, exts[e].end-1); err != nil {
return err
}
}
}
return nil
}
func isIdentifier(s string) bool {
if len(s) == 0 {
return false
}
for i, r := range s {
if i == 0 && r >= '0' && r <= '9' {
// can't start with number
return false
}
// alphanumeric and underscore ok; everything else bad
switch {
case r >= '0' && r <= '9':
case r >= 'a' && r <= 'z':
case r >= 'A' && r <= 'Z':
case r == '_':
default:
return false
}
}
return true
}
func findMessageReservedNameNode(msgNode ast.MessageDeclNode, name string) ast.Node {
var decls []ast.MessageElement
switch msgNode := msgNode.(type) {
case *ast.MessageNode:
decls = msgNode.Decls
case *ast.SyntheticGroupMessageNode:
decls = msgNode.Decls
default:
// leave decls empty
}
return findReservedNameNode(msgNode, decls, name)
}
func findReservedNameNode[T ast.Node](parent ast.Node, decls []T, name string) ast.Node {
for _, decl := range decls {
// NB: We have to convert to empty interface first, before we can do a type
// assertion because type assertions on type parameters aren't allowed. (The
// compiler cannot yet know whether T is an interface type or not.)
rsvd, ok := any(decl).(*ast.ReservedNode)
if !ok {
continue
}
for _, rsvdName := range rsvd.Names {
if rsvdName.AsString() == name {
return rsvdName
}
}
}
// couldn't find it? Instead of puking, report position of the parent.
return parent
}
func validateEnum(res *result, syntax protoreflect.Syntax, name protoreflect.FullName, ed *descriptorpb.EnumDescriptorProto, handler *reporter.Handler) error {
scope := fmt.Sprintf("enum %s", name)
if len(ed.Value) == 0 {
enNode := res.EnumNode(ed)
enNodeInfo := res.file.NodeInfo(enNode)
if err := handler.HandleErrorf(enNodeInfo, "%s: enums must define at least one value", scope); err != nil {
return err
}
}
if err := validateNoFeatures(res, syntax, scope, ed.Options.GetUninterpretedOption(), handler); err != nil {
return err
}
allowAlias := false
var allowAliasOpt *descriptorpb.UninterpretedOption
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, ed.Options.GetUninterpretedOption(), "allow_alias"); err != nil {
return err
} else if index >= 0 {
allowAliasOpt = ed.Options.UninterpretedOption[index]
valid := false
if allowAliasOpt.IdentifierValue != nil {
if allowAliasOpt.GetIdentifierValue() == "true" {
allowAlias = true
valid = true
} else if allowAliasOpt.GetIdentifierValue() == "false" {
valid = true
}
}
if !valid {
optNode := res.OptionNode(allowAliasOpt)
optNodeInfo := res.file.NodeInfo(optNode.GetValue())
if err := handler.HandleErrorf(optNodeInfo, "%s: expecting bool value for allow_alias option", scope); err != nil {
return err
}
}
}
if syntax == protoreflect.Proto3 && len(ed.Value) > 0 && ed.Value[0].GetNumber() != 0 {
evNode := res.EnumValueNode(ed.Value[0])
evNodeInfo := res.file.NodeInfo(evNode.GetNumber())
if err := handler.HandleErrorf(evNodeInfo, "%s: proto3 requires that first value of enum have numeric value zero", scope); err != nil {
return err
}
}
// check for aliases
vals := map[int32]string{}
hasAlias := false
for _, evd := range ed.Value {
existing := vals[evd.GetNumber()]
if existing != "" {
if allowAlias {
hasAlias = true
} else {
evNode := res.EnumValueNode(evd)
evNodeInfo := res.file.NodeInfo(evNode.GetNumber())
if err := handler.HandleErrorf(evNodeInfo, "%s: values %s and %s both have the same numeric value %d; use allow_alias option if intentional", scope, existing, evd.GetName(), evd.GetNumber()); err != nil {
return err
}
}
}
vals[evd.GetNumber()] = evd.GetName()
}
if allowAlias && !hasAlias {
optNode := res.OptionNode(allowAliasOpt)
optNodeInfo := res.file.NodeInfo(optNode.GetValue())
if err := handler.HandleErrorf(optNodeInfo, "%s: allow_alias is true but no values are aliases", scope); err != nil {
return err
}
}
// reserved ranges should not overlap
rsvd := make(tagRanges, len(ed.ReservedRange))
for i, r := range ed.ReservedRange {
n := res.EnumReservedRangeNode(r)
rsvd[i] = tagRange{start: r.GetStart(), end: r.GetEnd(), node: n}
}
sort.Sort(rsvd)
for i := 1; i < len(rsvd); i++ {
if rsvd[i].start <= rsvd[i-1].end {
rangeNodeInfo := res.file.NodeInfo(rsvd[i].node)
if err := handler.HandleErrorf(rangeNodeInfo, "%s: reserved ranges overlap: %d to %d and %d to %d", scope, rsvd[i-1].start, rsvd[i-1].end, rsvd[i].start, rsvd[i].end); err != nil {
return err
}
}
}
// now, check that fields don't re-use tags and don't try to use extension
// or reserved ranges or reserved names
rsvdNames := map[string]struct{}{}
for _, n := range ed.ReservedName {
// validate reserved name while we're here
if !isIdentifier(n) {
node := findEnumReservedNameNode(res.EnumNode(ed), n)
nodeInfo := res.file.NodeInfo(node)
if err := handler.HandleErrorf(nodeInfo, "%s: reserved name %q is not a valid identifier", scope, n); err != nil {
return err
}
}
rsvdNames[n] = struct{}{}
}
for _, ev := range ed.Value {
evn := res.EnumValueNode(ev)
if _, ok := rsvdNames[ev.GetName()]; ok {
enumValNodeInfo := res.file.NodeInfo(evn.GetName())
if err := handler.HandleErrorf(enumValNodeInfo, "%s: value %s is using a reserved name", scope, ev.GetName()); err != nil {
return err
}
}
// check reserved ranges
r := sort.Search(len(rsvd), func(index int) bool { return rsvd[index].end >= ev.GetNumber() })
if r < len(rsvd) && rsvd[r].start <= ev.GetNumber() {
enumValNodeInfo := res.file.NodeInfo(evn.GetNumber())
if err := handler.HandleErrorf(enumValNodeInfo, "%s: value %s is using number %d which is in reserved range %d to %d", scope, ev.GetName(), ev.GetNumber(), rsvd[r].start, rsvd[r].end); err != nil {
return err
}
}
}
return nil
}
func findEnumReservedNameNode(enumNode ast.Node, name string) ast.Node {
var decls []ast.EnumElement
if enumNode, ok := enumNode.(*ast.EnumNode); ok {
decls = enumNode.Decls
// if not the right type, we leave decls empty
}
return findReservedNameNode(enumNode, decls, name)
}
func validateField(res *result, syntax protoreflect.Syntax, name protoreflect.FullName, fld *descriptorpb.FieldDescriptorProto, handler *reporter.Handler) error {
var scope string
if fld.Extendee != nil {
scope = fmt.Sprintf("extension %s", name)
} else {
scope = fmt.Sprintf("field %s", name)
}
node := res.FieldNode(fld)
if fld.Number == nil {
fieldTagNodeInfo := res.file.NodeInfo(node)
if err := handler.HandleErrorf(fieldTagNodeInfo, "%s: missing field tag number", scope); err != nil {
return err
}
}
if syntax != protoreflect.Proto2 {
if fld.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP {
groupNodeInfo := res.file.NodeInfo(node.GetGroupKeyword())
if err := handler.HandleErrorf(groupNodeInfo, "%s: groups are not allowed in proto3 or editions", scope); err != nil {
return err
}
} else if fld.Label != nil && fld.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED {
fieldLabelNodeInfo := res.file.NodeInfo(node.FieldLabel())
if err := handler.HandleErrorf(fieldLabelNodeInfo, "%s: label 'required' is not allowed in proto3 or editions", scope); err != nil {
return err
}
}
if syntax == protoreflect.Editions {
if fld.Label != nil && fld.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL {
fieldLabelNodeInfo := res.file.NodeInfo(node.FieldLabel())
if err := handler.HandleErrorf(fieldLabelNodeInfo, "%s: label 'optional' is not allowed in editions; use option features.field_presence instead", scope); err != nil {
return err
}
}
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, fld.Options.GetUninterpretedOption(), "packed"); err != nil {
return err
} else if index >= 0 {
optNode := res.OptionNode(fld.Options.GetUninterpretedOption()[index])
optNameNodeInfo := res.file.NodeInfo(optNode.GetName())
if err := handler.HandleErrorf(optNameNodeInfo, "%s: packed option is not allowed in editions; use option features.repeated_field_encoding instead", scope); err != nil {
return err
}
}
} else if syntax == protoreflect.Proto3 {
if index, err := internal.FindOption(res, handler.HandleErrorf, scope, fld.Options.GetUninterpretedOption(), "default"); err != nil {
return err
} else if index >= 0 {
optNode := res.OptionNode(fld.Options.GetUninterpretedOption()[index])
optNameNodeInfo := res.file.NodeInfo(optNode.GetName())
if err := handler.HandleErrorf(optNameNodeInfo, "%s: default values are not allowed in proto3", scope); err != nil {
return err
}
}
}
} else {
if fld.Label == nil && fld.OneofIndex == nil {
fieldNameNodeInfo := res.file.NodeInfo(node.FieldName())
if err := handler.HandleErrorf(fieldNameNodeInfo, "%s: field has no label; proto2 requires explicit 'optional' label", scope); err != nil {
return err
}
}
if fld.GetExtendee() != "" && fld.Label != nil && fld.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED {
fieldLabelNodeInfo := res.file.NodeInfo(node.FieldLabel())
if err := handler.HandleErrorf(fieldLabelNodeInfo, "%s: extension fields cannot be 'required'", scope); err != nil {
return err
}
}
}
return validateNoFeatures(res, syntax, scope, fld.Options.GetUninterpretedOption(), handler)
}
type tagRange struct {
start int32
end int32
node ast.RangeDeclNode
}
type tagRanges []tagRange
func (r tagRanges) Len() int {
return len(r)
}
func (r tagRanges) Less(i, j int) bool {
return r[i].start < r[j].start ||
(r[i].start == r[j].start && r[i].end < r[j].end)
}
func (r tagRanges) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func fillInMissingLabels(fd *descriptorpb.FileDescriptorProto) {
for _, md := range fd.MessageType {
fillInMissingLabelsInMsg(md)
}
for _, extd := range fd.Extension {
fillInMissingLabel(extd)
}
}
func fillInMissingLabelsInMsg(md *descriptorpb.DescriptorProto) {
for _, fld := range md.Field {
fillInMissingLabel(fld)
}
for _, nmd := range md.NestedType {
fillInMissingLabelsInMsg(nmd)
}
for _, extd := range md.Extension {
fillInMissingLabel(extd)
}
}
func fillInMissingLabel(fld *descriptorpb.FieldDescriptorProto) {
if fld.Label == nil {
fld.Label = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
}
}
// 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 protoutil
import (
"fmt"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/bufbuild/protocompile/internal/editions"
)
// GetFeatureDefault gets the default value for the given feature and the given
// edition. The given feature must represent a field of the google.protobuf.FeatureSet
// message and must not be an extension.
//
// If the given field is from a dynamically built descriptor (i.e. it's containing
// message descriptor is different from the linked-in descriptor for
// [*descriptorpb.FeatureSet]), the returned value may be a dynamic value. In such
// cases, the value may not be directly usable using [protoreflect.Message.Set] with
// an instance of [*descriptorpb.FeatureSet] and must instead be used with a
// [*dynamicpb.Message].
//
// To get the default value of a custom feature, use [GetCustomFeatureDefault]
// instead.
func GetFeatureDefault(edition descriptorpb.Edition, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
if feature.ContainingMessage().FullName() != editions.FeatureSetDescriptor.FullName() {
return protoreflect.Value{}, fmt.Errorf("feature %s is a field of %s but should be a field of %s",
feature.Name(), feature.ContainingMessage().FullName(), editions.FeatureSetDescriptor.FullName())
}
var msgType protoreflect.MessageType
if feature.ContainingMessage() == editions.FeatureSetDescriptor {
msgType = editions.FeatureSetType
} else {
msgType = dynamicpb.NewMessageType(feature.ContainingMessage())
}
return editions.GetFeatureDefault(edition, msgType, feature)
}
// GetCustomFeatureDefault gets the default value for the given custom feature
// and given edition. A custom feature is a field whose containing message is the
// type of an extension field of google.protobuf.FeatureSet. The given extension
// describes that extension field and message type. The given feature must be a
// field of that extension's message type.
func GetCustomFeatureDefault(edition descriptorpb.Edition, extension protoreflect.ExtensionType, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
extDesc := extension.TypeDescriptor()
if extDesc.ContainingMessage().FullName() != editions.FeatureSetDescriptor.FullName() {
return protoreflect.Value{}, fmt.Errorf("extension %s does not extend %s", extDesc.FullName(), editions.FeatureSetDescriptor.FullName())
}
if extDesc.Message() == nil {
return protoreflect.Value{}, fmt.Errorf("extensions of %s should be messages; %s is instead %s",
editions.FeatureSetDescriptor.FullName(), extDesc.FullName(), extDesc.Kind().String())
}
if feature.IsExtension() {
return protoreflect.Value{}, fmt.Errorf("feature %s is an extension, but feature extension %s may not itself have extensions",
feature.FullName(), extDesc.FullName())
}
if feature.ContainingMessage().FullName() != extDesc.Message().FullName() {
return protoreflect.Value{}, fmt.Errorf("feature %s is a field of %s but should be a field of %s",
feature.Name(), feature.ContainingMessage().FullName(), extDesc.Message().FullName())
}
if feature.ContainingMessage() != extDesc.Message() {
return protoreflect.Value{}, fmt.Errorf("feature %s has a different message descriptor from the given extension type for %s",
feature.Name(), extDesc.Message().FullName())
}
return editions.GetFeatureDefault(edition, extension.Zero().Message().Type(), feature)
}
// ResolveFeature resolves a feature for the given descriptor.
//
// If the given element is in a proto2 or proto3 syntax file, this skips
// resolution and just returns the relevant default (since such files are not
// allowed to override features). If neither the given element nor any of its
// ancestors override the given feature, the relevant default is returned.
//
// This has the same caveat as GetFeatureDefault if the given feature is from a
// dynamically built descriptor.
func ResolveFeature(element protoreflect.Descriptor, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
edition := editions.GetEdition(element)
defaultVal, err := GetFeatureDefault(edition, feature)
if err != nil {
return protoreflect.Value{}, err
}
return resolveFeature(edition, defaultVal, element, feature)
}
// ResolveCustomFeature resolves a custom feature for the given extension and
// field descriptor.
//
// The given extension must be an extension of google.protobuf.FeatureSet that
// represents a non-repeated message value. The given feature is a field in
// that extension's message type.
//
// If the given element is in a proto2 or proto3 syntax file, this skips
// resolution and just returns the relevant default (since such files are not
// allowed to override features). If neither the given element nor any of its
// ancestors override the given feature, the relevant default is returned.
func ResolveCustomFeature(element protoreflect.Descriptor, extension protoreflect.ExtensionType, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
edition := editions.GetEdition(element)
defaultVal, err := GetCustomFeatureDefault(edition, extension, feature)
if err != nil {
return protoreflect.Value{}, err
}
return resolveFeature(edition, defaultVal, element, extension.TypeDescriptor(), feature)
}
func resolveFeature(
edition descriptorpb.Edition,
defaultVal protoreflect.Value,
element protoreflect.Descriptor,
fields ...protoreflect.FieldDescriptor,
) (protoreflect.Value, error) {
if edition == descriptorpb.Edition_EDITION_PROTO2 || edition == descriptorpb.Edition_EDITION_PROTO3 {
// these syntax levels can't specify features, so we can short-circuit the search
// through the descriptor hierarchy for feature overrides
return defaultVal, nil
}
val, err := editions.ResolveFeature(element, fields...)
if err != nil {
return protoreflect.Value{}, err
}
if val.IsValid() {
return val, nil
}
return defaultVal, nil
}
// 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 protoutil contains useful functions for interacting with descriptors.
// For now these include only functions for efficiently converting descriptors
// produced by the compiler to descriptor protos and functions for resolving
// "features" (a core concept of Protobuf Editions).
//
// Despite the fact that descriptor protos are mutable, calling code should NOT
// mutate any of the protos returned from this package. For efficiency, some
// values returned from this package may reference internal state of a compiler
// result, and mutating the proto could corrupt or invalidate parts of that
// result.
package protoutil
import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
)
// DescriptorProtoWrapper is a protoreflect.Descriptor that wraps an
// underlying descriptor proto. It provides the same interface as
// Descriptor but with one extra operation, to efficiently query for
// the underlying descriptor proto.
//
// Descriptors that implement this will also implement another method
// whose specified return type is the concrete type returned by the
// AsProto method. The name of this method varies by the type of this
// descriptor:
//
// Descriptor Type Other Method Name
// ---------------------+------------------------------------
// FileDescriptor | FileDescriptorProto()
// MessageDescriptor | MessageDescriptorProto()
// FieldDescriptor | FieldDescriptorProto()
// OneofDescriptor | OneofDescriptorProto()
// EnumDescriptor | EnumDescriptorProto()
// EnumValueDescriptor | EnumValueDescriptorProto()
// ServiceDescriptor | ServiceDescriptorProto()
// MethodDescriptor | MethodDescriptorProto()
//
// For example, a DescriptorProtoWrapper that implements FileDescriptor
// returns a *descriptorpb.FileDescriptorProto value from its AsProto
// method and also provides a method with the following signature:
//
// FileDescriptorProto() *descriptorpb.FileDescriptorProto
type DescriptorProtoWrapper interface {
protoreflect.Descriptor
// AsProto returns the underlying descriptor proto. The concrete
// type of the proto message depends on the type of this
// descriptor:
// Descriptor Type Proto Message Type
// ---------------------+------------------------------------
// FileDescriptor | *descriptorpb.FileDescriptorProto
// MessageDescriptor | *descriptorpb.DescriptorProto
// FieldDescriptor | *descriptorpb.FieldDescriptorProto
// OneofDescriptor | *descriptorpb.OneofDescriptorProto
// EnumDescriptor | *descriptorpb.EnumDescriptorProto
// EnumValueDescriptor | *descriptorpb.EnumValueDescriptorProto
// ServiceDescriptor | *descriptorpb.ServiceDescriptorProto
// MethodDescriptor | *descriptorpb.MethodDescriptorProto
AsProto() proto.Message
}
// ProtoFromDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromDescriptor(d protoreflect.Descriptor) proto.Message {
switch d := d.(type) {
case protoreflect.FileDescriptor:
return ProtoFromFileDescriptor(d)
case protoreflect.MessageDescriptor:
return ProtoFromMessageDescriptor(d)
case protoreflect.FieldDescriptor:
return ProtoFromFieldDescriptor(d)
case protoreflect.OneofDescriptor:
return ProtoFromOneofDescriptor(d)
case protoreflect.EnumDescriptor:
return ProtoFromEnumDescriptor(d)
case protoreflect.EnumValueDescriptor:
return ProtoFromEnumValueDescriptor(d)
case protoreflect.ServiceDescriptor:
return ProtoFromServiceDescriptor(d)
case protoreflect.MethodDescriptor:
return ProtoFromMethodDescriptor(d)
default:
// WTF??
if res, ok := d.(DescriptorProtoWrapper); ok {
return res.AsProto()
}
return nil
}
}
// ProtoFromFileDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For file descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. File descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromFileDescriptor(d protoreflect.FileDescriptor) *descriptorpb.FileDescriptorProto {
if imp, ok := d.(protoreflect.FileImport); ok {
d = imp.FileDescriptor
}
type canProto interface {
FileDescriptorProto() *descriptorpb.FileDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.FileDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if fd, ok := res.AsProto().(*descriptorpb.FileDescriptorProto); ok {
return fd
}
}
return protodesc.ToFileDescriptorProto(d)
}
// ProtoFromMessageDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For message descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Message descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromMessageDescriptor(d protoreflect.MessageDescriptor) *descriptorpb.DescriptorProto {
type canProto interface {
MessageDescriptorProto() *descriptorpb.DescriptorProto
}
if res, ok := d.(canProto); ok {
return res.MessageDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if md, ok := res.AsProto().(*descriptorpb.DescriptorProto); ok {
return md
}
}
return protodesc.ToDescriptorProto(d)
}
// ProtoFromFieldDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For field descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Field descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromFieldDescriptor(d protoreflect.FieldDescriptor) *descriptorpb.FieldDescriptorProto {
type canProto interface {
FieldDescriptorProto() *descriptorpb.FieldDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.FieldDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if fd, ok := res.AsProto().(*descriptorpb.FieldDescriptorProto); ok {
return fd
}
}
return protodesc.ToFieldDescriptorProto(d)
}
// ProtoFromOneofDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For oneof descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Oneof descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromOneofDescriptor(d protoreflect.OneofDescriptor) *descriptorpb.OneofDescriptorProto {
type canProto interface {
OneofDescriptorProto() *descriptorpb.OneofDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.OneofDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if ood, ok := res.AsProto().(*descriptorpb.OneofDescriptorProto); ok {
return ood
}
}
return protodesc.ToOneofDescriptorProto(d)
}
// ProtoFromEnumDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For enum descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Enum descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromEnumDescriptor(d protoreflect.EnumDescriptor) *descriptorpb.EnumDescriptorProto {
type canProto interface {
EnumDescriptorProto() *descriptorpb.EnumDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.EnumDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if ed, ok := res.AsProto().(*descriptorpb.EnumDescriptorProto); ok {
return ed
}
}
return protodesc.ToEnumDescriptorProto(d)
}
// ProtoFromEnumValueDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For enum value descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Enum value descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromEnumValueDescriptor(d protoreflect.EnumValueDescriptor) *descriptorpb.EnumValueDescriptorProto {
type canProto interface {
EnumValueDescriptorProto() *descriptorpb.EnumValueDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.EnumValueDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if ed, ok := res.AsProto().(*descriptorpb.EnumValueDescriptorProto); ok {
return ed
}
}
return protodesc.ToEnumValueDescriptorProto(d)
}
// ProtoFromServiceDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For service descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Service descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromServiceDescriptor(d protoreflect.ServiceDescriptor) *descriptorpb.ServiceDescriptorProto {
type canProto interface {
ServiceDescriptorProto() *descriptorpb.ServiceDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.ServiceDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if sd, ok := res.AsProto().(*descriptorpb.ServiceDescriptorProto); ok {
return sd
}
}
return protodesc.ToServiceDescriptorProto(d)
}
// ProtoFromMethodDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For method descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Method descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromMethodDescriptor(d protoreflect.MethodDescriptor) *descriptorpb.MethodDescriptorProto {
type canProto interface {
MethodDescriptorProto() *descriptorpb.MethodDescriptorProto
}
if res, ok := d.(canProto); ok {
return res.MethodDescriptorProto()
}
if res, ok := d.(DescriptorProtoWrapper); ok {
if md, ok := res.AsProto().(*descriptorpb.MethodDescriptorProto); ok {
return md
}
}
return protodesc.ToMethodDescriptorProto(d)
}
// 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 reporter
import (
"errors"
"fmt"
"github.com/bufbuild/protocompile/ast"
)
// ErrInvalidSource is a sentinel error that is returned by compilation and
// stand-alone compilation steps (such as parsing, linking) when one or more
// errors is reported but the configured ErrorReporter always returns nil.
var ErrInvalidSource = errors.New("parse failed: invalid proto source")
// ErrorWithPos is an error about a proto source file that adds information
// about the location in the file that caused the error.
type ErrorWithPos interface {
error
ast.SourceSpan
// GetPosition returns the start source position that caused the underlying error.
GetPosition() ast.SourcePos
// Unwrap returns the underlying error.
Unwrap() error
}
// Error creates a new ErrorWithPos from the given error and source position.
func Error(span ast.SourceSpan, err error) ErrorWithPos {
var ewp ErrorWithPos
if errors.As(err, &ewp) {
// replace existing position with given one
return &errorWithSpan{SourceSpan: span, underlying: ewp.Unwrap()}
}
return &errorWithSpan{SourceSpan: span, underlying: err}
}
// Errorf creates a new ErrorWithPos whose underlying error is created using the
// given message format and arguments (via fmt.Errorf).
func Errorf(span ast.SourceSpan, format string, args ...any) ErrorWithPos {
return Error(span, fmt.Errorf(format, args...))
}
type errorWithSpan struct {
ast.SourceSpan
underlying error
}
func (e *errorWithSpan) Error() string {
sourcePos := e.GetPosition()
return fmt.Sprintf("%s: %v", sourcePos, e.underlying)
}
func (e *errorWithSpan) GetPosition() ast.SourcePos {
return e.Start()
}
func (e *errorWithSpan) Unwrap() error {
return e.underlying
}
var _ ErrorWithPos = (*errorWithSpan)(nil)
// 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 reporter contains the types used for reporting errors from
// protocompile operations. It contains error types as well as interfaces
// for reporting and handling errors and warnings.
package reporter
import (
"sync"
"github.com/bufbuild/protocompile/ast"
)
// ErrorReporter is responsible for reporting the given error. If the reporter
// returns a non-nil error, parsing/linking will abort with that error. If the
// reporter returns nil, parsing will continue, allowing the parser to try to
// report as many syntax and/or link errors as it can find.
type ErrorReporter func(err ErrorWithPos) error
// WarningReporter is responsible for reporting the given warning. This is used
// for indicating non-error messages to the calling program for things that do
// not cause the parse to fail but are considered bad practice. Though they are
// just warnings, the details are supplied to the reporter via an error type.
type WarningReporter func(ErrorWithPos)
// Reporter is a type that handles reporting both errors and warnings.
// A reporter does not need to be thread-safe. Safe concurrent access is
// managed by a Handler.
type Reporter interface {
// Error is called when the given error is encountered and needs to be
// reported to the calling program. This signature matches ErrorReporter
// because it has the same semantics. If this function returns non-nil
// then the operation will abort immediately with the given error. But
// if it returns nil, the operation will continue, reporting more errors
// as they are encountered. If the reporter never returns non-nil then
// the operation will eventually fail with ErrInvalidSource.
Error(ErrorWithPos) error
// Warning is called when the given warnings is encountered and needs to be
// reported to the calling program. Despite the argument being an error
// type, a warning will never cause the operation to abort or fail (unless
// the reporter's implementation of this method panics).
Warning(ErrorWithPos)
}
// NewReporter creates a new reporter that invokes the given functions on error
// or warning.
func NewReporter(errs ErrorReporter, warnings WarningReporter) Reporter {
return reporterFuncs{errs: errs, warnings: warnings}
}
type reporterFuncs struct {
errs ErrorReporter
warnings WarningReporter
}
func (r reporterFuncs) Error(err ErrorWithPos) error {
if r.errs == nil {
return err
}
return r.errs(err)
}
func (r reporterFuncs) Warning(err ErrorWithPos) {
if r.warnings != nil {
r.warnings(err)
}
}
// Handler is used by protocompile operations for handling errors and warnings.
// This type is thread-safe. It uses a mutex to serialize calls to its reporter
// so that reporter instances do not have to be thread-safe (unless re-used
// across multiple handlers).
type Handler struct {
parent *Handler
mu sync.Mutex
reporter Reporter
errsReported bool
err error
}
// NewHandler creates a new Handler that reports errors and warnings using the
// given reporter.
func NewHandler(rep Reporter) *Handler {
if rep == nil {
rep = NewReporter(nil, nil)
}
return &Handler{reporter: rep}
}
// SubHandler returns a "child" of h. Use of a child handler is the same as use
// of the parent, except that the Error() and ReporterError() functions only
// report non-nil for errors that were reported using the child handler. So
// errors reported directly to the parent or to a different child handler won't
// be returned. This is useful for making concurrent access to the handler more
// deterministic: if a child handler is only used from one goroutine, its view
// of reported errors is consistent and unimpacted by concurrent operations.
func (h *Handler) SubHandler() *Handler {
return &Handler{parent: h}
}
// HandleError handles the given error. If the given err is an ErrorWithPos, it
// is reported, and this function returns the error returned by the reporter. If
// the given err is NOT an ErrorWithPos, the current operation will abort
// immediately.
//
// If the handler has already aborted (by returning a non-nil error from a prior
// call to HandleError or HandleErrorf), that same error is returned and the
// given error is not reported.
func (h *Handler) HandleError(err error) error {
if h.parent != nil {
_, isErrWithPos := err.(ErrorWithPos)
err = h.parent.HandleError(err)
// update child state
h.mu.Lock()
defer h.mu.Unlock()
if isErrWithPos {
h.errsReported = true
}
h.err = err
return err
}
h.mu.Lock()
defer h.mu.Unlock()
if h.err != nil {
return h.err
}
if ewp, ok := err.(ErrorWithPos); ok {
h.errsReported = true
err = h.reporter.Error(ewp)
}
h.err = err
return err
}
// HandleErrorWithPos handles an error with the given source position.
//
// If the handler has already aborted (by returning a non-nil error from a prior
// call to HandleError or HandleErrorf), that same error is returned and the
// given error is not reported.
func (h *Handler) HandleErrorWithPos(span ast.SourceSpan, err error) error {
return h.HandleError(Error(span, err))
}
// HandleErrorf handles an error with the given source position, creating the
// error using the given message format and arguments.
//
// If the handler has already aborted (by returning a non-nil error from a call
// to HandleError or HandleErrorf), that same error is returned and the given
// error is not reported.
func (h *Handler) HandleErrorf(span ast.SourceSpan, format string, args ...any) error {
return h.HandleError(Errorf(span, format, args...))
}
// HandleWarning handles the given warning. This will delegate to the handler's
// configured reporter.
func (h *Handler) HandleWarning(err ErrorWithPos) {
if h.parent != nil {
h.parent.HandleWarning(err)
return
}
// even though we aren't touching mutable fields, we acquire lock anyway so
// that underlying reporter does not have to be thread-safe
h.mu.Lock()
defer h.mu.Unlock()
h.reporter.Warning(err)
}
// HandleWarningWithPos handles a warning with the given source position. This will
// delegate to the handler's configured reporter.
func (h *Handler) HandleWarningWithPos(span ast.SourceSpan, err error) {
h.HandleWarning(Error(span, err))
}
// HandleWarningf handles a warning with the given source position, creating the
// actual error value using the given message format and arguments.
func (h *Handler) HandleWarningf(span ast.SourceSpan, format string, args ...any) {
h.HandleWarning(Errorf(span, format, args...))
}
// Error returns the handler result. If any errors have been reported then this
// returns a non-nil error. If the reporter never returned a non-nil error then
// ErrInvalidSource is returned. Otherwise, this returns the error returned by
// the handler's reporter (the same value returned by ReporterError).
func (h *Handler) Error() error {
h.mu.Lock()
defer h.mu.Unlock()
if h.errsReported && h.err == nil {
return ErrInvalidSource
}
return h.err
}
// ReporterError returns the error returned by the handler's reporter. If
// the reporter has either not been invoked (no errors handled) or has not
// returned any non-nil value, then this returns nil.
func (h *Handler) ReporterError() error {
h.mu.Lock()
defer h.mu.Unlock()
return h.err
}
// 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 protocompile
import (
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/parser"
)
// Resolver is used by the compiler to resolve a proto source file name
// into some unit that is usable by the compiler. The result could be source
// for a proto file or it could be an already-parsed AST or descriptor.
//
// Resolver implementations must be thread-safe as a single compilation
// operation could invoke FindFileByPath from multiple goroutines.
type Resolver interface {
// FindFileByPath searches for information for the given file path. If no
// result is available, it should return a non-nil error, such as
// protoregistry.NotFound.
FindFileByPath(path string) (SearchResult, error)
}
// SearchResult represents information about a proto source file. Only one of
// the various fields must be set, based on what is available for a file. If
// multiple fields are set, the compiler prefers them in opposite order listed:
// so it uses a descriptor if present and only falls back to source if nothing
// else is available.
type SearchResult struct {
// Represents source code for the file. This should be nil if source code
// is not available. If no field below is set, then the compiler will parse
// the source code into an AST.
Source io.Reader
// Represents the abstract syntax tree for the file. If no field below is
// set, then the compiler will convert the AST into a descriptor proto.
AST *ast.FileNode
// A descriptor proto that represents the file. If the field below is not
// set, then the compiler will link this proto with its dependencies to
// produce a linked descriptor.
Proto *descriptorpb.FileDescriptorProto
// A parse result for the file. This packages both an AST and a descriptor
// proto in one. When a parser result is available, it is more efficient
// than using an AST search result, since the descriptor proto need not be
// re-created. And it provides better error messages than a descriptor proto
// search result, since the AST has greater fidelity with regard to source
// positions (even if the descriptor proto includes source code info).
ParseResult parser.Result
// A fully linked descriptor that represents the file. If this field is set,
// then the compiler has little or no additional work to do for this file as
// it is already compiled. If this value implements linker.File, there is no
// additional work. Otherwise, the additional work is to compute an index of
// symbols in the file, for efficient lookup.
Desc protoreflect.FileDescriptor
}
// ResolverFunc is a simple function type that implements Resolver.
type ResolverFunc func(string) (SearchResult, error)
var _ Resolver = ResolverFunc(nil)
func (f ResolverFunc) FindFileByPath(path string) (SearchResult, error) {
return f(path)
}
// CompositeResolver is a slice of resolvers, which are consulted in order
// until one can supply a result. If none of the constituent resolvers can
// supply a result, the error returned by the first resolver is returned. If
// the slice of resolvers is empty, all operations return
// protoregistry.NotFound.
type CompositeResolver []Resolver
var _ Resolver = CompositeResolver(nil)
func (f CompositeResolver) FindFileByPath(path string) (SearchResult, error) {
if len(f) == 0 {
return SearchResult{}, protoregistry.NotFound
}
var firstErr error
for _, res := range f {
r, err := res.FindFileByPath(path)
if err == nil {
return r, nil
}
if firstErr == nil {
firstErr = err
}
}
return SearchResult{}, firstErr
}
// SourceResolver can resolve file names by returning source code. It uses
// an optional list of import paths to search. By default, it searches the
// file system.
type SourceResolver struct {
// Optional list of import paths. If present and not empty, then all
// file paths to find are assumed to be relative to one of these paths.
// If nil or empty, all file paths to find are assumed to be relative to
// the current working directory.
ImportPaths []string
// Optional function for returning a file's contents. If nil, then
// os.Open is used to open files on the file system.
//
// This function must be thread-safe as a single compilation operation
// could result in concurrent invocations of this function from
// multiple goroutines.
Accessor func(path string) (io.ReadCloser, error)
}
var _ Resolver = (*SourceResolver)(nil)
func (r *SourceResolver) FindFileByPath(path string) (SearchResult, error) {
if len(r.ImportPaths) == 0 {
reader, err := r.accessFile(path)
if err != nil {
return SearchResult{}, err
}
return SearchResult{Source: reader}, nil
}
var e error
for _, importPath := range r.ImportPaths {
reader, err := r.accessFile(filepath.Join(importPath, path))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
e = err
continue
}
return SearchResult{}, err
}
return SearchResult{Source: reader}, nil
}
return SearchResult{}, e
}
func (r *SourceResolver) accessFile(path string) (io.ReadCloser, error) {
if r.Accessor != nil {
return r.Accessor(path)
}
return os.Open(path)
}
// SourceAccessorFromMap returns a function that can be used as the Accessor
// field of a SourceResolver that uses the given map to load source. The map
// keys are file names and the values are the corresponding file contents.
//
// The given map is used directly and not copied. Since accessor functions
// must be thread-safe, this means that the provided map must not be mutated
// once this accessor is provided to a compile operation.
func SourceAccessorFromMap(srcs map[string]string) func(string) (io.ReadCloser, error) {
return func(path string) (io.ReadCloser, error) {
src, ok := srcs[path]
if !ok {
return nil, os.ErrNotExist
}
return io.NopCloser(strings.NewReader(src)), nil
}
}
// WithStandardImports returns a new resolver that knows about the same standard
// imports that are included with protoc.
//
// Note that this uses the descriptors embedded in generated code in the packages
// of the Protobuf Go module, except for "google/protobuf/cpp_features.proto" and
// "google/protobuf/java_features.proto". For those two files, compiled descriptors
// are embedded in this module because there is no package in the Protobuf Go module
// that contains generated code for those files. This resolver also provides results
// for the "google/protobuf/go_features.proto", which is technically not a standard
// file (it is not included with protoc) but is included in generated code in the
// Protobuf Go module.
//
// As of v0.14.0 of this module (and v1.34.2 of the Protobuf Go module and v27.0 of
// Protobuf), the contents of the standard import "google/protobuf/descriptor.proto"
// contain extension declarations which are *absent* from the descriptors that this
// resolver returns. That is because extension declarations are only retained in
// source, not at runtime, which means they are not available in the embedded
// descriptors in generated code.
//
// To use versions of the standard imports that *do* include these extension
// declarations, see wellknownimports.WithStandardImports instead. As of this
// writing, the declarations are only needed to prevent source files from
// illegally re-defining the custom features for C++, Java, and Go.
func WithStandardImports(r Resolver) Resolver {
return ResolverFunc(func(name string) (SearchResult, error) {
res, err := r.FindFileByPath(name)
if err != nil {
// error from given resolver? see if it's a known standard file
if d, ok := standardImports[name]; ok {
return SearchResult{Desc: d}, nil
}
}
return res, err
})
}
// 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 sourceinfo contains the logic for computing source code info for a
// file descriptor.
//
// The inputs to the computation are an AST for a file as well as the index of
// interpreted options for that file.
package sourceinfo
import (
"bytes"
"fmt"
"strings"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/internal"
)
// OptionIndex is a mapping of AST nodes that define options to corresponding
// paths into the containing file descriptor. The path is a sequence of field
// tags and indexes that define a traversal path from the root (the file
// descriptor) to the resolved option field. The info also includes similar
// information about child elements, for options whose values are composite
// (like a list or message literal).
type OptionIndex map[*ast.OptionNode]*OptionSourceInfo
// OptionSourceInfo describes the source info path for an option value and
// contains information about the value's descendants in the AST.
type OptionSourceInfo struct {
// The source info path to this element. If this element represents a
// declaration with an array-literal value, the last element of the
// path is the index of the first item in the array.
//
// This path is relative to the options message. So the first element
// is a field number of the options message.
//
// If the first element is negative, it indicates the number of path
// components to remove from the path to the relevant options. This is
// used for field pseudo-options, so that the path indicates a field on
// the descriptor, which is a parent of the options message (since that
// is how the pseudo-options are actually stored).
Path []int32
// Children can be an *ArrayLiteralSourceInfo, a *MessageLiteralSourceInfo,
// or nil, depending on whether the option's value is an
// [*ast.ArrayLiteralNode], an [*ast.MessageLiteralNode], or neither.
// For [*ast.ArrayLiteralNode] values, this is only populated if the
// value is a non-empty array of messages. (Empty arrays and arrays
// of scalar values do not need any additional info.)
Children OptionChildrenSourceInfo
}
// OptionChildrenSourceInfo represents source info paths for child elements of
// an option value.
type OptionChildrenSourceInfo interface {
isChildSourceInfo()
}
// ArrayLiteralSourceInfo represents source info paths for the child
// elements of an [*ast.ArrayLiteralNode]. This value is only useful for
// non-empty array literals that contain messages.
type ArrayLiteralSourceInfo struct {
Elements []OptionSourceInfo
}
func (*ArrayLiteralSourceInfo) isChildSourceInfo() {}
// MessageLiteralSourceInfo represents source info paths for the child
// elements of an [*ast.MessageLiteralNode].
type MessageLiteralSourceInfo struct {
Fields map[*ast.MessageFieldNode]*OptionSourceInfo
}
func (*MessageLiteralSourceInfo) isChildSourceInfo() {}
// GenerateSourceInfo generates source code info for the given AST. If the given
// opts is present, it can generate source code info for interpreted options.
// Otherwise, any options in the AST will get source code info as uninterpreted
// options.
func GenerateSourceInfo(file *ast.FileNode, opts OptionIndex, genOpts ...GenerateOption) *descriptorpb.SourceCodeInfo {
if file == nil {
return nil
}
sci := sourceCodeInfo{file: file, commentsUsed: map[ast.SourcePos]struct{}{}}
for _, sourceInfoOpt := range genOpts {
sourceInfoOpt.apply(&sci)
}
generateSourceInfoForFile(opts, &sci, file)
return &descriptorpb.SourceCodeInfo{Location: sci.locs}
}
// GenerateOption represents an option for how source code info is generated.
type GenerateOption interface {
apply(*sourceCodeInfo)
}
// WithExtraComments will result in source code info that contains extra comments.
// By default, comments are only generated for full declarations. Inline comments
// around elements of a declaration are not included in source code info. This option
// changes that behavior so that as many comments as possible are described in the
// source code info.
func WithExtraComments() GenerateOption {
return extraCommentsOption{}
}
// WithExtraOptionLocations will result in source code info that contains extra
// locations to describe elements inside of a message literal. By default, option
// values are treated as opaque, so the only locations included are for the entire
// option value. But with this option, paths to the various fields set inside a
// message literal will also have locations. This makes it possible for usages of
// the source code info to report precise locations for specific fields inside the
// value.
func WithExtraOptionLocations() GenerateOption {
return extraOptionLocationsOption{}
}
type extraCommentsOption struct{}
func (e extraCommentsOption) apply(info *sourceCodeInfo) {
info.extraComments = true
}
type extraOptionLocationsOption struct{}
func (e extraOptionLocationsOption) apply(info *sourceCodeInfo) {
info.extraOptionLocs = true
}
func generateSourceInfoForFile(opts OptionIndex, sci *sourceCodeInfo, file *ast.FileNode) {
path := make([]int32, 0, 16)
sci.newLocWithoutComments(file, nil)
if file.Syntax != nil {
sci.newLocWithComments(file.Syntax, append(path, internal.FileSyntaxTag))
}
if file.Edition != nil {
// Despite editions having its own field, protoc behavior sets the path in source code
// info as [internal.FileSyntaxTag] and this is vaguely outlined in descriptor.proto
// https://github.com/protocolbuffers/protobuf/blob/22e1e6bd90aa8dc35f8cc28b5d7fc03858060f0b/src/google/protobuf/descriptor.proto#L137-L144
sci.newLocWithComments(file.Edition, append(path, internal.FileSyntaxTag))
}
var depIndex, pubDepIndex, weakDepIndex, optIndex, msgIndex, enumIndex, extendIndex, svcIndex int32
for _, child := range file.Decls {
switch child := child.(type) {
case *ast.ImportNode:
sci.newLocWithComments(child, append(path, internal.FileDependencyTag, depIndex))
depIndex++
if child.Public != nil {
sci.newLoc(child.Public, append(path, internal.FilePublicDependencyTag, pubDepIndex))
pubDepIndex++
} else if child.Weak != nil {
sci.newLoc(child.Weak, append(path, internal.FileWeakDependencyTag, weakDepIndex))
weakDepIndex++
}
case *ast.PackageNode:
sci.newLocWithComments(child, append(path, internal.FilePackageTag))
case *ast.OptionNode:
generateSourceCodeInfoForOption(opts, sci, child, false, &optIndex, append(path, internal.FileOptionsTag))
case *ast.MessageNode:
generateSourceCodeInfoForMessage(opts, sci, child, nil, append(path, internal.FileMessagesTag, msgIndex))
msgIndex++
case *ast.EnumNode:
generateSourceCodeInfoForEnum(opts, sci, child, append(path, internal.FileEnumsTag, enumIndex))
enumIndex++
case *ast.ExtendNode:
extsPath := append(path, internal.FileExtensionsTag) //nolint:gocritic // intentionally creating new slice var
// we clone the path here so that append can't mutate extsPath, since they may share storage
msgsPath := append(internal.ClonePath(path), internal.FileMessagesTag)
generateSourceCodeInfoForExtensions(opts, sci, child, &extendIndex, &msgIndex, extsPath, msgsPath)
case *ast.ServiceNode:
generateSourceCodeInfoForService(opts, sci, child, append(path, internal.FileServicesTag, svcIndex))
svcIndex++
}
}
}
func generateSourceCodeInfoForOption(opts OptionIndex, sci *sourceCodeInfo, n *ast.OptionNode, compact bool, uninterpIndex *int32, path []int32) {
if !compact {
sci.newLocWithoutComments(n, path)
}
optInfo := opts[n]
if optInfo != nil {
fullPath := combinePathsForOption(path, optInfo.Path)
if compact {
sci.newLoc(n, fullPath)
} else {
sci.newLocWithComments(n, fullPath)
}
if sci.extraOptionLocs {
generateSourceInfoForOptionChildren(sci, n.Val, path, fullPath, optInfo.Children)
}
return
}
// it's an uninterpreted option
optPath := path
optPath = append(optPath, internal.UninterpretedOptionsTag, *uninterpIndex)
*uninterpIndex++
sci.newLoc(n, optPath)
var valTag int32
switch n.Val.(type) {
case ast.IdentValueNode:
valTag = internal.UninterpretedIdentTag
case *ast.NegativeIntLiteralNode:
valTag = internal.UninterpretedNegIntTag
case ast.IntValueNode:
valTag = internal.UninterpretedPosIntTag
case ast.FloatValueNode:
valTag = internal.UninterpretedDoubleTag
case ast.StringValueNode:
valTag = internal.UninterpretedStringTag
case *ast.MessageLiteralNode:
valTag = internal.UninterpretedAggregateTag
}
if valTag != 0 {
sci.newLoc(n.Val, append(optPath, valTag))
}
for j, nn := range n.Name.Parts {
optNmPath := optPath
optNmPath = append(optNmPath, internal.UninterpretedNameTag, int32(j))
sci.newLoc(nn, optNmPath)
sci.newLoc(nn.Name, append(optNmPath, internal.UninterpretedNameNameTag))
}
}
func combinePathsForOption(prefix, optionPath []int32) []int32 {
fullPath := make([]int32, len(prefix), len(prefix)+len(optionPath))
copy(fullPath, prefix)
if optionPath[0] == -1 {
// used by "default" and "json_name" field pseudo-options
// to attribute path to parent element (since those are
// stored directly on the descriptor, not its options)
optionPath = optionPath[1:]
fullPath = fullPath[:len(prefix)-1]
}
return append(fullPath, optionPath...)
}
func generateSourceInfoForOptionChildren(sci *sourceCodeInfo, n ast.ValueNode, pathPrefix, path []int32, childInfo OptionChildrenSourceInfo) {
switch childInfo := childInfo.(type) {
case *ArrayLiteralSourceInfo:
if arrayLiteral, ok := n.(*ast.ArrayLiteralNode); ok {
for i, val := range arrayLiteral.Elements {
elementInfo := childInfo.Elements[i]
fullPath := combinePathsForOption(pathPrefix, elementInfo.Path)
sci.newLoc(val, fullPath)
generateSourceInfoForOptionChildren(sci, val, pathPrefix, fullPath, elementInfo.Children)
}
}
case *MessageLiteralSourceInfo:
if msgLiteral, ok := n.(*ast.MessageLiteralNode); ok {
for _, fieldNode := range msgLiteral.Elements {
fieldInfo, ok := childInfo.Fields[fieldNode]
if !ok {
continue
}
fullPath := combinePathsForOption(pathPrefix, fieldInfo.Path)
locationNode := ast.Node(fieldNode)
if fieldNode.Name.IsAnyTypeReference() && fullPath[len(fullPath)-1] == internal.AnyValueTag {
// This is a special expanded Any. So also insert a location
// for the type URL field.
typeURLPath := make([]int32, len(fullPath))
copy(typeURLPath, fullPath)
typeURLPath[len(typeURLPath)-1] = internal.AnyTypeURLTag
sci.newLoc(fieldNode.Name, fullPath)
// And create the next location so it's just the value,
// not the full field definition.
locationNode = fieldNode.Val
}
_, isArrayLiteral := fieldNode.Val.(*ast.ArrayLiteralNode)
if !isArrayLiteral {
// We don't include this with an array literal since the path
// is to the first element of the array. If we added it here,
// it would be redundant with the child info we add next, and
// it wouldn't be entirely correct since it only indicates the
// index of the first element in the array (and not the others).
sci.newLoc(locationNode, fullPath)
}
generateSourceInfoForOptionChildren(sci, fieldNode.Val, pathPrefix, fullPath, fieldInfo.Children)
}
}
case nil:
if arrayLiteral, ok := n.(*ast.ArrayLiteralNode); ok {
// an array literal without child source info is an array of scalars
for i, val := range arrayLiteral.Elements {
// last element of path is starting index for array literal
elementPath := append(([]int32)(nil), path...)
elementPath[len(elementPath)-1] += int32(i)
sci.newLoc(val, elementPath)
}
}
}
}
func generateSourceCodeInfoForMessage(opts OptionIndex, sci *sourceCodeInfo, n ast.MessageDeclNode, fieldPath []int32, path []int32) {
var openBrace ast.Node
var decls []ast.MessageElement
switch n := n.(type) {
case *ast.MessageNode:
openBrace = n.OpenBrace
decls = n.Decls
case *ast.SyntheticGroupMessageNode:
openBrace = n.OpenBrace
decls = n.Decls
case *ast.SyntheticMapEntryNode:
sci.newLoc(n, path)
// map entry so nothing else to do
return
}
sci.newBlockLocWithComments(n, openBrace, path)
sci.newLoc(n.MessageName(), append(path, internal.MessageNameTag))
// matching protoc, which emits the corresponding field type name (for group fields)
// right after the source location for the group message name
if fieldPath != nil {
sci.newLoc(n.MessageName(), append(fieldPath, internal.FieldTypeNameTag))
}
var optIndex, fieldIndex, oneofIndex, extendIndex, nestedMsgIndex int32
var nestedEnumIndex, extRangeIndex, reservedRangeIndex, reservedNameIndex int32
for _, child := range decls {
switch child := child.(type) {
case *ast.OptionNode:
generateSourceCodeInfoForOption(opts, sci, child, false, &optIndex, append(path, internal.MessageOptionsTag))
case *ast.FieldNode:
generateSourceCodeInfoForField(opts, sci, child, append(path, internal.MessageFieldsTag, fieldIndex))
fieldIndex++
case *ast.GroupNode:
fldPath := append(path, internal.MessageFieldsTag, fieldIndex) //nolint:gocritic // intentionally creating new slice var
generateSourceCodeInfoForField(opts, sci, child, fldPath)
fieldIndex++
// we clone the path here so that append can't mutate fldPath, since they may share storage
msgPath := append(internal.ClonePath(path), internal.MessageNestedMessagesTag, nestedMsgIndex)
generateSourceCodeInfoForMessage(opts, sci, child.AsMessage(), fldPath, msgPath)
nestedMsgIndex++
case *ast.MapFieldNode:
generateSourceCodeInfoForField(opts, sci, child, append(path, internal.MessageFieldsTag, fieldIndex))
fieldIndex++
nestedMsgIndex++
case *ast.OneofNode:
fldsPath := append(path, internal.MessageFieldsTag) //nolint:gocritic // intentionally creating new slice var
// we clone the path here and below so that append ops can't mutate
// fldPath or msgsPath, since they may otherwise share storage
msgsPath := append(internal.ClonePath(path), internal.MessageNestedMessagesTag)
ooPath := append(internal.ClonePath(path), internal.MessageOneofsTag, oneofIndex)
generateSourceCodeInfoForOneof(opts, sci, child, &fieldIndex, &nestedMsgIndex, fldsPath, msgsPath, ooPath)
oneofIndex++
case *ast.MessageNode:
generateSourceCodeInfoForMessage(opts, sci, child, nil, append(path, internal.MessageNestedMessagesTag, nestedMsgIndex))
nestedMsgIndex++
case *ast.EnumNode:
generateSourceCodeInfoForEnum(opts, sci, child, append(path, internal.MessageEnumsTag, nestedEnumIndex))
nestedEnumIndex++
case *ast.ExtendNode:
extsPath := append(path, internal.MessageExtensionsTag) //nolint:gocritic // intentionally creating new slice var
// we clone the path here so that append can't mutate extsPath, since they may share storage
msgsPath := append(internal.ClonePath(path), internal.MessageNestedMessagesTag)
generateSourceCodeInfoForExtensions(opts, sci, child, &extendIndex, &nestedMsgIndex, extsPath, msgsPath)
case *ast.ExtensionRangeNode:
generateSourceCodeInfoForExtensionRanges(opts, sci, child, &extRangeIndex, append(path, internal.MessageExtensionRangesTag))
case *ast.ReservedNode:
if len(child.Names) > 0 {
resPath := path
resPath = append(resPath, internal.MessageReservedNamesTag)
sci.newLocWithComments(child, resPath)
for _, rn := range child.Names {
sci.newLoc(rn, append(resPath, reservedNameIndex))
reservedNameIndex++
}
}
// For editions, reserved names are identifiers.
if len(child.Identifiers) > 0 {
resPath := path
resPath = append(resPath, internal.MessageReservedNamesTag)
sci.newLocWithComments(child, resPath)
for _, rn := range child.Identifiers {
sci.newLoc(rn, append(resPath, reservedNameIndex))
reservedNameIndex++
}
}
if len(child.Ranges) > 0 {
resPath := path
resPath = append(resPath, internal.MessageReservedRangesTag)
sci.newLocWithComments(child, resPath)
for _, rr := range child.Ranges {
generateSourceCodeInfoForReservedRange(sci, rr, append(resPath, reservedRangeIndex))
reservedRangeIndex++
}
}
}
}
}
func generateSourceCodeInfoForEnum(opts OptionIndex, sci *sourceCodeInfo, n *ast.EnumNode, path []int32) {
sci.newBlockLocWithComments(n, n.OpenBrace, path)
sci.newLoc(n.Name, append(path, internal.EnumNameTag))
var optIndex, valIndex, reservedNameIndex, reservedRangeIndex int32
for _, child := range n.Decls {
switch child := child.(type) {
case *ast.OptionNode:
generateSourceCodeInfoForOption(opts, sci, child, false, &optIndex, append(path, internal.EnumOptionsTag))
case *ast.EnumValueNode:
generateSourceCodeInfoForEnumValue(opts, sci, child, append(path, internal.EnumValuesTag, valIndex))
valIndex++
case *ast.ReservedNode:
if len(child.Names) > 0 {
resPath := path
resPath = append(resPath, internal.EnumReservedNamesTag)
sci.newLocWithComments(child, resPath)
for _, rn := range child.Names {
sci.newLoc(rn, append(resPath, reservedNameIndex))
reservedNameIndex++
}
}
if len(child.Ranges) > 0 {
resPath := path
resPath = append(resPath, internal.EnumReservedRangesTag)
sci.newLocWithComments(child, resPath)
for _, rr := range child.Ranges {
generateSourceCodeInfoForReservedRange(sci, rr, append(resPath, reservedRangeIndex))
reservedRangeIndex++
}
}
}
}
}
func generateSourceCodeInfoForEnumValue(opts OptionIndex, sci *sourceCodeInfo, n *ast.EnumValueNode, path []int32) {
sci.newLocWithComments(n, path)
sci.newLoc(n.Name, append(path, internal.EnumValNameTag))
sci.newLoc(n.Number, append(path, internal.EnumValNumberTag))
// enum value options
if n.Options != nil {
optsPath := path
optsPath = append(optsPath, internal.EnumValOptionsTag)
sci.newLoc(n.Options, optsPath)
var optIndex int32
for _, opt := range n.Options.GetElements() {
generateSourceCodeInfoForOption(opts, sci, opt, true, &optIndex, optsPath)
}
}
}
func generateSourceCodeInfoForReservedRange(sci *sourceCodeInfo, n *ast.RangeNode, path []int32) {
sci.newLoc(n, path)
sci.newLoc(n.StartVal, append(path, internal.ReservedRangeStartTag))
switch {
case n.EndVal != nil:
sci.newLoc(n.EndVal, append(path, internal.ReservedRangeEndTag))
case n.Max != nil:
sci.newLoc(n.Max, append(path, internal.ReservedRangeEndTag))
default:
sci.newLoc(n.StartVal, append(path, internal.ReservedRangeEndTag))
}
}
func generateSourceCodeInfoForExtensions(opts OptionIndex, sci *sourceCodeInfo, n *ast.ExtendNode, extendIndex, msgIndex *int32, extendPath, msgPath []int32) {
sci.newBlockLocWithComments(n, n.OpenBrace, extendPath)
for _, decl := range n.Decls {
switch decl := decl.(type) {
case *ast.FieldNode:
generateSourceCodeInfoForField(opts, sci, decl, append(extendPath, *extendIndex))
*extendIndex++
case *ast.GroupNode:
fldPath := extendPath
fldPath = append(fldPath, *extendIndex)
generateSourceCodeInfoForField(opts, sci, decl, fldPath)
*extendIndex++
generateSourceCodeInfoForMessage(opts, sci, decl.AsMessage(), fldPath, append(msgPath, *msgIndex))
*msgIndex++
}
}
}
func generateSourceCodeInfoForOneof(opts OptionIndex, sci *sourceCodeInfo, n *ast.OneofNode, fieldIndex, nestedMsgIndex *int32, fieldPath, nestedMsgPath, oneofPath []int32) {
sci.newBlockLocWithComments(n, n.OpenBrace, oneofPath)
sci.newLoc(n.Name, append(oneofPath, internal.OneofNameTag))
var optIndex int32
for _, child := range n.Decls {
switch child := child.(type) {
case *ast.OptionNode:
generateSourceCodeInfoForOption(opts, sci, child, false, &optIndex, append(oneofPath, internal.OneofOptionsTag))
case *ast.FieldNode:
generateSourceCodeInfoForField(opts, sci, child, append(fieldPath, *fieldIndex))
*fieldIndex++
case *ast.GroupNode:
fldPath := fieldPath
fldPath = append(fldPath, *fieldIndex)
generateSourceCodeInfoForField(opts, sci, child, fldPath)
*fieldIndex++
generateSourceCodeInfoForMessage(opts, sci, child.AsMessage(), fldPath, append(nestedMsgPath, *nestedMsgIndex))
*nestedMsgIndex++
}
}
}
func generateSourceCodeInfoForField(opts OptionIndex, sci *sourceCodeInfo, n ast.FieldDeclNode, path []int32) {
var fieldType string
if f, ok := n.(*ast.FieldNode); ok {
fieldType = string(f.FldType.AsIdentifier())
}
if n.GetGroupKeyword() != nil {
// comments will appear on group message
sci.newLocWithoutComments(n, path)
if n.FieldExtendee() != nil {
sci.newLoc(n.FieldExtendee(), append(path, internal.FieldExtendeeTag))
}
if n.FieldLabel() != nil {
// no comments here either (label is first token for group, so we want
// to leave the comments to be associated with the group message instead)
sci.newLocWithoutComments(n.FieldLabel(), append(path, internal.FieldLabelTag))
}
sci.newLoc(n.FieldType(), append(path, internal.FieldTypeTag))
// let the name comments be attributed to the group name
sci.newLocWithoutComments(n.FieldName(), append(path, internal.FieldNameTag))
} else {
sci.newLocWithComments(n, path)
if n.FieldExtendee() != nil {
sci.newLoc(n.FieldExtendee(), append(path, internal.FieldExtendeeTag))
}
if n.FieldLabel() != nil {
sci.newLoc(n.FieldLabel(), append(path, internal.FieldLabelTag))
}
var tag int32
if _, isScalar := internal.FieldTypes[fieldType]; isScalar {
tag = internal.FieldTypeTag
} else {
// this is a message or an enum, so attribute type location
// to the type name field
tag = internal.FieldTypeNameTag
}
sci.newLoc(n.FieldType(), append(path, tag))
sci.newLoc(n.FieldName(), append(path, internal.FieldNameTag))
}
sci.newLoc(n.FieldTag(), append(path, internal.FieldNumberTag))
if n.GetOptions() != nil {
optsPath := path
optsPath = append(optsPath, internal.FieldOptionsTag)
sci.newLoc(n.GetOptions(), optsPath)
var optIndex int32
for _, opt := range n.GetOptions().GetElements() {
generateSourceCodeInfoForOption(opts, sci, opt, true, &optIndex, optsPath)
}
}
}
func generateSourceCodeInfoForExtensionRanges(opts OptionIndex, sci *sourceCodeInfo, n *ast.ExtensionRangeNode, extRangeIndex *int32, path []int32) {
sci.newLocWithComments(n, path)
startExtRangeIndex := *extRangeIndex
for _, child := range n.Ranges {
path := append(path, *extRangeIndex)
*extRangeIndex++
sci.newLoc(child, path)
sci.newLoc(child.StartVal, append(path, internal.ExtensionRangeStartTag))
switch {
case child.EndVal != nil:
sci.newLoc(child.EndVal, append(path, internal.ExtensionRangeEndTag))
case child.Max != nil:
sci.newLoc(child.Max, append(path, internal.ExtensionRangeEndTag))
default:
sci.newLoc(child.StartVal, append(path, internal.ExtensionRangeEndTag))
}
}
// options for all ranges go after the start+end values
for range n.Ranges {
path := append(path, startExtRangeIndex)
startExtRangeIndex++
if n.Options != nil {
optsPath := path
optsPath = append(optsPath, internal.ExtensionRangeOptionsTag)
sci.newLoc(n.Options, optsPath)
var optIndex int32
for _, opt := range n.Options.GetElements() {
generateSourceCodeInfoForOption(opts, sci, opt, true, &optIndex, optsPath)
}
}
}
}
func generateSourceCodeInfoForService(opts OptionIndex, sci *sourceCodeInfo, n *ast.ServiceNode, path []int32) {
sci.newBlockLocWithComments(n, n.OpenBrace, path)
sci.newLoc(n.Name, append(path, internal.ServiceNameTag))
var optIndex, rpcIndex int32
for _, child := range n.Decls {
switch child := child.(type) {
case *ast.OptionNode:
generateSourceCodeInfoForOption(opts, sci, child, false, &optIndex, append(path, internal.ServiceOptionsTag))
case *ast.RPCNode:
generateSourceCodeInfoForMethod(opts, sci, child, append(path, internal.ServiceMethodsTag, rpcIndex))
rpcIndex++
}
}
}
func generateSourceCodeInfoForMethod(opts OptionIndex, sci *sourceCodeInfo, n *ast.RPCNode, path []int32) {
if n.OpenBrace != nil {
sci.newBlockLocWithComments(n, n.OpenBrace, path)
} else {
sci.newLocWithComments(n, path)
}
sci.newLoc(n.Name, append(path, internal.MethodNameTag))
if n.Input.Stream != nil {
sci.newLoc(n.Input.Stream, append(path, internal.MethodInputStreamTag))
}
sci.newLoc(n.Input.MessageType, append(path, internal.MethodInputTag))
if n.Output.Stream != nil {
sci.newLoc(n.Output.Stream, append(path, internal.MethodOutputStreamTag))
}
sci.newLoc(n.Output.MessageType, append(path, internal.MethodOutputTag))
optsPath := path
optsPath = append(optsPath, internal.MethodOptionsTag)
var optIndex int32
for _, decl := range n.Decls {
if opt, ok := decl.(*ast.OptionNode); ok {
generateSourceCodeInfoForOption(opts, sci, opt, false, &optIndex, optsPath)
}
}
}
type sourceCodeInfo struct {
file *ast.FileNode
extraComments bool
extraOptionLocs bool
locs []*descriptorpb.SourceCodeInfo_Location
commentsUsed map[ast.SourcePos]struct{}
}
func (sci *sourceCodeInfo) newLocWithoutComments(n ast.Node, path []int32) {
var start, end ast.SourcePos
if n == sci.file {
// For files, we don't want to consider trailing EOF token
// as part of the span. We want the span to only include
// actual lexical elements in the file (which also excludes
// whitespace and comments).
children := sci.file.Children()
if len(children) > 0 && isEOF(children[len(children)-1]) {
children = children[:len(children)-1]
}
if len(children) == 0 {
start = ast.SourcePos{Filename: sci.file.Name(), Line: 1, Col: 1}
end = start
} else {
start = sci.file.TokenInfo(n.Start()).Start()
end = sci.file.TokenInfo(children[len(children)-1].End()).End()
}
} else {
info := sci.file.NodeInfo(n)
start, end = info.Start(), info.End()
}
sci.locs = append(sci.locs, &descriptorpb.SourceCodeInfo_Location{
Path: internal.ClonePath(path),
Span: makeSpan(start, end),
})
}
func (sci *sourceCodeInfo) newLoc(n ast.Node, path []int32) {
info := sci.file.NodeInfo(n)
if !sci.extraComments {
start, end := info.Start(), info.End()
sci.locs = append(sci.locs, &descriptorpb.SourceCodeInfo_Location{
Path: internal.ClonePath(path),
Span: makeSpan(start, end),
})
} else {
detachedComments, leadingComments := sci.getLeadingComments(n)
trailingComments := sci.getTrailingComments(n)
sci.newLocWithGivenComments(info, detachedComments, leadingComments, trailingComments, path)
}
}
func isEOF(n ast.Node) bool {
r, ok := n.(*ast.RuneNode)
return ok && r.Rune == 0
}
func (sci *sourceCodeInfo) newBlockLocWithComments(n, openBrace ast.Node, path []int32) {
// Block definitions use trailing comments after the open brace "{" as the
// element's trailing comments. For example:
//
// message Foo { // this is a trailing comment for a message
//
// } // not this
//
nodeInfo := sci.file.NodeInfo(n)
detachedComments, leadingComments := sci.getLeadingComments(n)
trailingComments := sci.getTrailingComments(openBrace)
sci.newLocWithGivenComments(nodeInfo, detachedComments, leadingComments, trailingComments, path)
}
func (sci *sourceCodeInfo) newLocWithComments(n ast.Node, path []int32) {
nodeInfo := sci.file.NodeInfo(n)
detachedComments, leadingComments := sci.getLeadingComments(n)
trailingComments := sci.getTrailingComments(n)
sci.newLocWithGivenComments(nodeInfo, detachedComments, leadingComments, trailingComments, path)
}
func (sci *sourceCodeInfo) newLocWithGivenComments(nodeInfo ast.NodeInfo, detachedComments []comments, leadingComments comments, trailingComments comments, path []int32) {
if (len(detachedComments) > 0 && sci.commentUsed(detachedComments[0])) ||
(len(detachedComments) == 0 && sci.commentUsed(leadingComments)) {
detachedComments = nil
leadingComments = ast.EmptyComments
}
if sci.commentUsed(trailingComments) {
trailingComments = ast.EmptyComments
}
var trail *string
if trailingComments.Len() > 0 {
trail = proto.String(sci.combineComments(trailingComments))
}
var lead *string
if leadingComments.Len() > 0 {
lead = proto.String(sci.combineComments(leadingComments))
}
detached := make([]string, len(detachedComments))
for i, cmts := range detachedComments {
detached[i] = sci.combineComments(cmts)
}
sci.locs = append(sci.locs, &descriptorpb.SourceCodeInfo_Location{
LeadingDetachedComments: detached,
LeadingComments: lead,
TrailingComments: trail,
Path: internal.ClonePath(path),
Span: makeSpan(nodeInfo.Start(), nodeInfo.End()),
})
}
type comments interface {
Len() int
Index(int) ast.Comment
}
type subComments struct {
offs, n int
c ast.Comments
}
func (s subComments) Len() int {
return s.n
}
func (s subComments) Index(i int) ast.Comment {
if i < 0 || i >= s.n {
panic(fmt.Errorf("runtime error: index out of range [%d] with length %d", i, s.n))
}
return s.c.Index(i + s.offs)
}
func (sci *sourceCodeInfo) getLeadingComments(n ast.Node) ([]comments, comments) {
s := n.Start()
info := sci.file.TokenInfo(s)
var prevInfo ast.NodeInfo
if prev, ok := sci.file.Tokens().Previous(s); ok {
prevInfo = sci.file.TokenInfo(prev)
}
_, d, l := sci.attributeComments(prevInfo, info)
return d, l
}
func (sci *sourceCodeInfo) getTrailingComments(n ast.Node) comments {
e := n.End()
next, ok := sci.file.Tokens().Next(e)
if !ok {
return ast.EmptyComments
}
info := sci.file.TokenInfo(e)
nextInfo := sci.file.TokenInfo(next)
t, _, _ := sci.attributeComments(info, nextInfo)
return t
}
func (sci *sourceCodeInfo) attributeComments(prevInfo, info ast.NodeInfo) (t comments, d []comments, l comments) {
detached := groupComments(info.LeadingComments())
var trail comments
if prevInfo.IsValid() {
trail = comments(prevInfo.TrailingComments())
if trail.Len() == 0 {
trail, detached = sci.maybeDonate(prevInfo, info, detached)
}
} else {
trail = ast.EmptyComments
}
detached, lead := sci.maybeAttach(prevInfo, info, trail.Len() > 0, detached)
return trail, detached, lead
}
func (sci *sourceCodeInfo) maybeDonate(prevInfo ast.NodeInfo, info ast.NodeInfo, lead []comments) (t comments, l []comments) {
if len(lead) == 0 {
// nothing to donate
return ast.EmptyComments, nil
}
firstCommentPos := lead[0].Index(0)
if firstCommentPos.Start().Line > prevInfo.End().Line+1 {
// first comment is detached from previous token, so can't be a trailing comment
return ast.EmptyComments, lead
}
if len(lead) > 1 {
// multiple groups? then donate first comment to previous token
return lead[0], lead[1:]
}
// there is only one element in lead
comment := lead[0]
lastCommentPos := comment.Index(comment.Len() - 1)
if lastCommentPos.End().Line < info.Start().Line-1 {
// there is a blank line between the comments and subsequent token, so
// we can donate the comment to previous token
return comment, nil
}
if txt := info.RawText(); txt == "" || (len(txt) == 1 && strings.ContainsAny(txt, "}]),;")) {
// token is a symbol for the end of a scope or EOF, which doesn't need a leading comment
if !sci.extraComments && txt != "" &&
firstCommentPos.Start().Line == prevInfo.End().Line &&
lastCommentPos.End().Line == info.Start().Line {
// protoc does not donate if prev and next token are on the same line since it's
// ambiguous which one should get the comment; so we mirror that here
return ast.EmptyComments, lead
}
// But with extra comments, we always donate in this situation in order to capture
// more comments. Because otherwise, these comments are lost since these symbols
// don't map to a location in source code info.
return comment, nil
}
// cannot donate
return ast.EmptyComments, lead
}
func (sci *sourceCodeInfo) maybeAttach(prevInfo ast.NodeInfo, info ast.NodeInfo, hasTrail bool, lead []comments) (d []comments, l comments) {
if len(lead) == 0 {
return nil, ast.EmptyComments
}
if len(lead) == 1 && !hasTrail && prevInfo.IsValid() {
// If the one comment appears attached to both previous and next tokens,
// don't attach to either.
comment := lead[0]
attachedToPrevious := comment.Index(0).Start().Line == prevInfo.End().Line
attachedToNext := comment.Index(comment.Len()-1).End().Line == info.Start().Line
if attachedToPrevious && attachedToNext {
// Since attachment is ambiguous, leave it detached.
return lead, ast.EmptyComments
}
}
lastComment := lead[len(lead)-1]
if lastComment.Index(lastComment.Len()-1).End().Line >= info.Start().Line-1 {
return lead[:len(lead)-1], lastComment
}
return lead, ast.EmptyComments
}
func makeSpan(start, end ast.SourcePos) []int32 {
if start.Line == end.Line {
return []int32{int32(start.Line) - 1, int32(start.Col) - 1, int32(end.Col) - 1}
}
return []int32{int32(start.Line) - 1, int32(start.Col) - 1, int32(end.Line) - 1, int32(end.Col) - 1}
}
func (sci *sourceCodeInfo) commentUsed(c comments) bool {
if c.Len() == 0 {
return false
}
pos := c.Index(0).Start()
if _, ok := sci.commentsUsed[pos]; ok {
return true
}
sci.commentsUsed[pos] = struct{}{}
return false
}
func groupComments(cmts ast.Comments) []comments {
if cmts.Len() == 0 {
return nil
}
var groups []comments
singleLineStyle := cmts.Index(0).RawText()[:2] == "//"
line := cmts.Index(0).End().Line
start := 0
for i := 1; i < cmts.Len(); i++ {
c := cmts.Index(i)
prevSingleLine := singleLineStyle
singleLineStyle = strings.HasPrefix(c.RawText(), "//")
if !singleLineStyle || prevSingleLine != singleLineStyle || c.Start().Line > line+1 {
// new group!
groups = append(groups, subComments{offs: start, n: i - start, c: cmts})
start = i
}
line = c.End().Line
}
// don't forget last group
groups = append(groups, subComments{offs: start, n: cmts.Len() - start, c: cmts})
return groups
}
func (sci *sourceCodeInfo) combineComments(comments comments) string {
if comments.Len() == 0 {
return ""
}
var buf bytes.Buffer
for i, l := 0, comments.Len(); i < l; i++ {
c := comments.Index(i)
txt := c.RawText()
if txt[:2] == "//" {
buf.WriteString(txt[2:])
// protoc includes trailing newline for line comments,
// but it's not present in the AST comment. So we need
// to add it if present.
if i, ok := sci.file.Items().Next(c.AsItem()); ok {
info := sci.file.ItemInfo(i)
if strings.HasPrefix(info.LeadingWhitespace(), "\n") {
buf.WriteRune('\n')
}
}
} else {
lines := strings.Split(txt[2:len(txt)-2], "\n")
first := true
for _, l := range lines {
if first {
first = false
buf.WriteString(l)
continue
}
buf.WriteByte('\n')
// strip a prefix of whitespace followed by '*'
j := 0
for j < len(l) {
if l[j] != ' ' && l[j] != '\t' {
break
}
j++
}
switch {
case j == len(l):
l = ""
case l[j] == '*':
l = l[j+1:]
case j > 0:
l = l[j:]
}
buf.WriteString(l)
}
}
}
return buf.String()
}
// 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 protocompile
import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
_ "google.golang.org/protobuf/types/gofeaturespb" // link in packages that include the standard protos included with protoc.
_ "google.golang.org/protobuf/types/known/anypb"
_ "google.golang.org/protobuf/types/known/apipb"
_ "google.golang.org/protobuf/types/known/durationpb"
_ "google.golang.org/protobuf/types/known/emptypb"
_ "google.golang.org/protobuf/types/known/fieldmaskpb"
_ "google.golang.org/protobuf/types/known/sourcecontextpb"
_ "google.golang.org/protobuf/types/known/structpb"
_ "google.golang.org/protobuf/types/known/timestamppb"
_ "google.golang.org/protobuf/types/known/typepb"
_ "google.golang.org/protobuf/types/known/wrapperspb"
_ "google.golang.org/protobuf/types/pluginpb"
"github.com/bufbuild/protocompile/internal/featuresext"
)
// All files that are included with protoc are also included with this package
// so that clients do not need to explicitly supply a copy of these protos (just
// like callers of protoc do not need to supply them).
var standardImports map[string]protoreflect.FileDescriptor
func init() {
standardFilenames := []string{
"google/protobuf/any.proto",
"google/protobuf/api.proto",
"google/protobuf/compiler/plugin.proto",
"google/protobuf/descriptor.proto",
"google/protobuf/duration.proto",
"google/protobuf/empty.proto",
"google/protobuf/field_mask.proto",
"google/protobuf/go_features.proto",
"google/protobuf/source_context.proto",
"google/protobuf/struct.proto",
"google/protobuf/timestamp.proto",
"google/protobuf/type.proto",
"google/protobuf/wrappers.proto",
}
standardImports = map[string]protoreflect.FileDescriptor{}
for _, fn := range standardFilenames {
fd, err := protoregistry.GlobalFiles.FindFileByPath(fn)
if err != nil {
panic(err.Error())
}
standardImports[fn] = fd
}
otherFeatures := []struct {
Name string
GetDescriptor func() (protoreflect.FileDescriptor, error)
}{
{
Name: "google/protobuf/cpp_features.proto",
GetDescriptor: featuresext.CppFeaturesDescriptor,
},
{
Name: "google/protobuf/java_features.proto",
GetDescriptor: featuresext.JavaFeaturesDescriptor,
},
}
for _, feature := range otherFeatures {
// First see if the program has generated Go code for this
// file linked in:
fd, err := protoregistry.GlobalFiles.FindFileByPath(feature.Name)
if err == nil {
standardImports[feature.Name] = fd
continue
}
fd, err = feature.GetDescriptor()
if err != nil {
// For these extensions to FeatureSet, we are lenient. If
// we can't load them, just ignore them.
continue
}
standardImports[feature.Name] = fd
}
}
// 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 protocompile
import (
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/internal/editions"
)
// IsEditionSupported returns true if this module can compile sources for
// the given edition. This returns true for the special EDITION_PROTO2 and
// EDITION_PROTO3 as well as all actual editions supported.
func IsEditionSupported(edition descriptorpb.Edition) bool {
return edition == descriptorpb.Edition_EDITION_PROTO2 ||
edition == descriptorpb.Edition_EDITION_PROTO3 ||
(edition >= editions.MinSupportedEdition && edition <= editions.MaxSupportedEdition)
}
// 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 walk provides helper functions for traversing all elements in a
// protobuf file descriptor. There are versions both for traversing "rich"
// descriptors (protoreflect.Descriptor) and for traversing the underlying
// "raw" descriptor protos.
//
// # Enter And Exit
//
// This package includes variants of the functions that accept two callback
// functions. These variants have names ending with "EnterAndExit". One function
// is called as each element is visited ("enter") and the other is called after
// the element and all of its descendants have been visited ("exit"). This
// can be useful when you need to track state that is scoped to the visitation
// of a single element.
//
// # Source Path
//
// When traversing raw descriptor protos, this package include variants whose
// callback accepts a protoreflect.SourcePath. These variants have names that
// include "WithPath". This path can be used to locate corresponding data in the
// file's source code info (if present).
package walk
import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/bufbuild/protocompile/internal"
)
// Descriptors walks all descriptors in the given file using a depth-first
// traversal, calling the given function for each descriptor in the hierarchy.
// The walk ends when traversal is complete or when the function returns an
// error. If the function returns an error, that is returned as the result of the
// walk operation.
//
// Descriptors are visited using a pre-order traversal, where the function is
// called for a descriptor before it is called for any of its descendants.
func Descriptors(file protoreflect.FileDescriptor, fn func(protoreflect.Descriptor) error) error {
return DescriptorsEnterAndExit(file, fn, nil)
}
// DescriptorsEnterAndExit walks all descriptors in the given file using a
// depth-first traversal, calling the given functions on entry and on exit
// for each descriptor in the hierarchy. The walk ends when traversal is
// complete or when a function returns an error. If a function returns an error,
// that is returned as the result of the walk operation.
//
// The enter function is called using a pre-order traversal, where the function
// is called for a descriptor before it is called for any of its descendants.
// The exit function is called using a post-order traversal, where the function
// is called for a descriptor only after it is called for any descendants.
func DescriptorsEnterAndExit(file protoreflect.FileDescriptor, enter, exit func(protoreflect.Descriptor) error) error {
if err := walkContainer(file, enter, exit); err != nil {
return err
}
services := file.Services()
for i, length := 0, services.Len(); i < length; i++ {
svc := services.Get(i)
if err := enter(svc); err != nil {
return err
}
methods := svc.Methods()
for i, length := 0, methods.Len(); i < length; i++ {
mtd := methods.Get(i)
if err := enter(mtd); err != nil {
return err
}
if exit != nil {
if err := exit(mtd); err != nil {
return err
}
}
}
if exit != nil {
if err := exit(svc); err != nil {
return err
}
}
}
return nil
}
type container interface {
Messages() protoreflect.MessageDescriptors
Enums() protoreflect.EnumDescriptors
Extensions() protoreflect.ExtensionDescriptors
}
func walkContainer(container container, enter, exit func(protoreflect.Descriptor) error) error {
messages := container.Messages()
for i, length := 0, messages.Len(); i < length; i++ {
msg := messages.Get(i)
if err := messageDescriptor(msg, enter, exit); err != nil {
return err
}
}
enums := container.Enums()
for i, length := 0, enums.Len(); i < length; i++ {
en := enums.Get(i)
if err := enumDescriptor(en, enter, exit); err != nil {
return err
}
}
exts := container.Extensions()
for i, length := 0, exts.Len(); i < length; i++ {
ext := exts.Get(i)
if err := enter(ext); err != nil {
return err
}
if exit != nil {
if err := exit(ext); err != nil {
return err
}
}
}
return nil
}
func messageDescriptor(msg protoreflect.MessageDescriptor, enter, exit func(protoreflect.Descriptor) error) error {
if err := enter(msg); err != nil {
return err
}
fields := msg.Fields()
for i, length := 0, fields.Len(); i < length; i++ {
fld := fields.Get(i)
if err := enter(fld); err != nil {
return err
}
if exit != nil {
if err := exit(fld); err != nil {
return err
}
}
}
oneofs := msg.Oneofs()
for i, length := 0, oneofs.Len(); i < length; i++ {
oo := oneofs.Get(i)
if err := enter(oo); err != nil {
return err
}
if exit != nil {
if err := exit(oo); err != nil {
return err
}
}
}
if err := walkContainer(msg, enter, exit); err != nil {
return err
}
if exit != nil {
if err := exit(msg); err != nil {
return err
}
}
return nil
}
func enumDescriptor(en protoreflect.EnumDescriptor, enter, exit func(protoreflect.Descriptor) error) error {
if err := enter(en); err != nil {
return err
}
vals := en.Values()
for i, length := 0, vals.Len(); i < length; i++ {
enVal := vals.Get(i)
if err := enter(enVal); err != nil {
return err
}
if exit != nil {
if err := exit(enVal); err != nil {
return err
}
}
}
if exit != nil {
if err := exit(en); err != nil {
return err
}
}
return nil
}
// DescriptorProtosWithPath walks all descriptor protos in the given file using
// a depth-first traversal. This is the same as DescriptorProtos except that the
// callback function, fn, receives a protoreflect.SourcePath, that indicates the
// path for the element in the file's source code info.
func DescriptorProtosWithPath(file *descriptorpb.FileDescriptorProto, fn func(protoreflect.FullName, protoreflect.SourcePath, proto.Message) error) error {
return DescriptorProtosWithPathEnterAndExit(file, fn, nil)
}
// DescriptorProtosWithPathEnterAndExit walks all descriptor protos in the given
// file using a depth-first traversal. This is the same as
// DescriptorProtosEnterAndExit except that the callback function, fn, receives
// a protoreflect.SourcePath, that indicates the path for the element in the
// file's source code info.
func DescriptorProtosWithPathEnterAndExit(file *descriptorpb.FileDescriptorProto, enter, exit func(protoreflect.FullName, protoreflect.SourcePath, proto.Message) error) error {
w := &protoWalker{usePath: true, enter: enter, exit: exit}
return w.walkDescriptorProtos(file)
}
// DescriptorProtos walks all descriptor protos in the given file using a
// depth-first traversal, calling the given function for each descriptor proto
// in the hierarchy. The walk ends when traversal is complete or when the
// function returns an error. If the function returns an error, that is
// returned as the result of the walk operation.
//
// Descriptor protos are visited using a pre-order traversal, where the function
// is called for a descriptor before it is called for any of its descendants.
func DescriptorProtos(file *descriptorpb.FileDescriptorProto, fn func(protoreflect.FullName, proto.Message) error) error {
return DescriptorProtosEnterAndExit(file, fn, nil)
}
// DescriptorProtosEnterAndExit walks all descriptor protos in the given file
// using a depth-first traversal, calling the given functions on entry and on
// exit for each descriptor in the hierarchy. The walk ends when traversal is
// complete or when a function returns an error. If a function returns an error,
// that is returned as the result of the walk operation.
//
// The enter function is called using a pre-order traversal, where the function
// is called for a descriptor proto before it is called for any of its
// descendants. The exit function is called using a post-order traversal, where
// the function is called for a descriptor proto only after it is called for any
// descendants.
func DescriptorProtosEnterAndExit(file *descriptorpb.FileDescriptorProto, enter, exit func(protoreflect.FullName, proto.Message) error) error {
enterWithPath := func(n protoreflect.FullName, _ protoreflect.SourcePath, m proto.Message) error {
return enter(n, m)
}
var exitWithPath func(protoreflect.FullName, protoreflect.SourcePath, proto.Message) error
if exit != nil {
exitWithPath = func(n protoreflect.FullName, _ protoreflect.SourcePath, m proto.Message) error {
return exit(n, m)
}
}
w := &protoWalker{
enter: enterWithPath,
exit: exitWithPath,
}
return w.walkDescriptorProtos(file)
}
type protoWalker struct {
usePath bool
enter, exit func(protoreflect.FullName, protoreflect.SourcePath, proto.Message) error
}
func (w *protoWalker) walkDescriptorProtos(file *descriptorpb.FileDescriptorProto) error {
prefix := file.GetPackage()
if prefix != "" {
prefix += "."
}
var path protoreflect.SourcePath
for i, msg := range file.MessageType {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.FileMessagesTag, int32(i))
}
if err := w.walkDescriptorProto(prefix, p, msg); err != nil {
return err
}
}
for i, en := range file.EnumType {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.FileEnumsTag, int32(i))
}
if err := w.walkEnumDescriptorProto(prefix, p, en); err != nil {
return err
}
}
for i, ext := range file.Extension {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.FileExtensionsTag, int32(i))
}
fqn := prefix + ext.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, ext); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, ext); err != nil {
return err
}
}
}
for i, svc := range file.Service {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.FileServicesTag, int32(i))
}
fqn := prefix + svc.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, svc); err != nil {
return err
}
for j, mtd := range svc.Method {
var mp protoreflect.SourcePath
if w.usePath {
mp = p
mp = append(mp, internal.ServiceMethodsTag, int32(j))
}
mtdFqn := fqn + "." + mtd.GetName()
if err := w.enter(protoreflect.FullName(mtdFqn), mp, mtd); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(mtdFqn), mp, mtd); err != nil {
return err
}
}
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, svc); err != nil {
return err
}
}
}
return nil
}
func (w *protoWalker) walkDescriptorProto(prefix string, path protoreflect.SourcePath, msg *descriptorpb.DescriptorProto) error {
fqn := prefix + msg.GetName()
if err := w.enter(protoreflect.FullName(fqn), path, msg); err != nil {
return err
}
prefix = fqn + "."
for i, fld := range msg.Field {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.MessageFieldsTag, int32(i))
}
fqn := prefix + fld.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, fld); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, fld); err != nil {
return err
}
}
}
for i, oo := range msg.OneofDecl {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.MessageOneofsTag, int32(i))
}
fqn := prefix + oo.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, oo); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, oo); err != nil {
return err
}
}
}
for i, nested := range msg.NestedType {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.MessageNestedMessagesTag, int32(i))
}
if err := w.walkDescriptorProto(prefix, p, nested); err != nil {
return err
}
}
for i, en := range msg.EnumType {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.MessageEnumsTag, int32(i))
}
if err := w.walkEnumDescriptorProto(prefix, p, en); err != nil {
return err
}
}
for i, ext := range msg.Extension {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.MessageExtensionsTag, int32(i))
}
fqn := prefix + ext.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, ext); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, ext); err != nil {
return err
}
}
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), path, msg); err != nil {
return err
}
}
return nil
}
func (w *protoWalker) walkEnumDescriptorProto(prefix string, path protoreflect.SourcePath, en *descriptorpb.EnumDescriptorProto) error {
fqn := prefix + en.GetName()
if err := w.enter(protoreflect.FullName(fqn), path, en); err != nil {
return err
}
for i, val := range en.Value {
var p protoreflect.SourcePath
if w.usePath {
p = path
p = append(p, internal.EnumValuesTag, int32(i))
}
fqn := prefix + val.GetName()
if err := w.enter(protoreflect.FullName(fqn), p, val); err != nil {
return err
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), p, val); err != nil {
return err
}
}
}
if w.exit != nil {
if err := w.exit(protoreflect.FullName(fqn), path, en); err != nil {
return err
}
}
return nil
}
// 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 wellknownimports provides source code for the well-known import
// files for use with a protocompile.Compiler.
package wellknownimports
import (
"embed"
"io"
"io/fs"
"github.com/bufbuild/protocompile"
)
//go:embed google/protobuf/*.proto google/protobuf/*/*.proto
var files embed.FS
// FS returns a filesystem over the built-in well-known imports.
func FS() fs.FS {
return files
}
// WithStandardImports returns a new resolver that can provide the source code for the
// standard imports that are included with protoc. This differs from
// protocompile.WithStandardImports, which uses descriptors embedded in generated
// code in the Protobuf Go module. That function is lighter weight, and does not need
// to bring in additional embedded data outside the Protobuf Go runtime. This version
// includes its own embedded versions of the source files.
//
// Unlike protocompile.WithStandardImports, this resolver does not provide results for
// "google/protobuf/go_features.proto" file. This resolver is backed by source files
// that are shipped with the Protobuf installation, which does not include that file.
//
// It is possible that the source code provided by this resolver differs from the
// source code used to create the descriptors provided by protocompile.WithStandardImports.
// That is because that other function depends on the Protobuf Go module, which could
// resolve in user programs to a different version than was used to build this package.
func WithStandardImports(resolver protocompile.Resolver) protocompile.Resolver {
return protocompile.CompositeResolver{
resolver,
&protocompile.SourceResolver{
Accessor: func(path string) (io.ReadCloser, error) {
return files.Open(path)
},
},
}
}