// Copyright 2020-2024 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package linker import ( "fmt" "math" "strings" "unicode" "unicode/utf8" "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/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 } } else if descriptorpb.Edition(r.Edition()) == descriptorpb.Edition_EDITION_2023 { if fld.Kind() != protoreflect.StringKind && fld.Kind() != protoreflect.BytesKind { span := r.findOptionSpan(fd, internal.FieldOptionsCTypeTag) if err := handler.HandleErrorf(span, "ctype option can only be used on string and bytes fields"); err != nil { return err } } if fd.proto.Options.GetCtype() == descriptorpb.FieldOptions_CORD && fd.IsExtension() { span := r.findOptionSpan(fd, internal.FieldOptionsCTypeTag) if err := handler.HandleErrorf(span, "ctype option cannot be CORD for extension fields"); 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() { // 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 tbe 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 { return enumValCamelCase(removePrefix(enumValueName, enumName)) } // removePrefix 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 removePrefix(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 } // enumValCamelCase converts the given string to upper-camel-case. // // The algorithm is adapted from the protoc source: // // https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L887 func enumValCamelCase(name string) string { var js []rune nextUpper := true for _, r := range name { if r == '_' { nextUpper = true continue } if nextUpper { nextUpper = false js = append(js, unicode.ToUpper(r)) } else { js = append(js, unicode.ToLower(r)) } } return string(js) } 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 := 0; i < limit; i++ { 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 }