blob: d4717ee5ee978ef5fb0a33d1c899c57ccb12bcd3 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT 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 bufbreakingcheck
import (
"fmt"
"sort"
"strings"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufanalysis"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufcheck/internal"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/protosource"
"google.golang.org/protobuf/types/descriptorpb"
)
var (
// https://developers.google.com/protocol-buffers/docs/proto3#updating
fieldDescriptorProtoTypeToWireCompatiblityGroup = map[descriptorpb.FieldDescriptorProto_Type]int{
descriptorpb.FieldDescriptorProto_TYPE_INT32: 1,
descriptorpb.FieldDescriptorProto_TYPE_INT64: 1,
descriptorpb.FieldDescriptorProto_TYPE_UINT32: 1,
descriptorpb.FieldDescriptorProto_TYPE_UINT64: 1,
descriptorpb.FieldDescriptorProto_TYPE_BOOL: 1,
descriptorpb.FieldDescriptorProto_TYPE_SINT32: 2,
descriptorpb.FieldDescriptorProto_TYPE_SINT64: 2,
// While string and bytes are compatible if the bytes are valid UTF-8, we cannot
// determine if a field will actually be valid UTF-8, as we are concerned with the
// definitions and not individual messages, so we have these in different
// compatibility groups. We allow string to evolve to bytes, but not bytes to
// string, but we need them to be in different compatibility groups so that
// we have to manually detect this.
descriptorpb.FieldDescriptorProto_TYPE_STRING: 3,
descriptorpb.FieldDescriptorProto_TYPE_BYTES: 4,
descriptorpb.FieldDescriptorProto_TYPE_FIXED32: 5,
descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: 5,
descriptorpb.FieldDescriptorProto_TYPE_FIXED64: 6,
descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: 6,
descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: 7,
descriptorpb.FieldDescriptorProto_TYPE_FLOAT: 8,
descriptorpb.FieldDescriptorProto_TYPE_GROUP: 9,
// Embedded messages are compatible with bytes if the bytes are serialized versions
// of the message, but we have no way of verifying this.
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: 10,
// Enum is compatible with int32, uint32, int64, uint64 if the values match
// an enum value, but we have no way of verifying this.
descriptorpb.FieldDescriptorProto_TYPE_ENUM: 11,
}
// https://developers.google.com/protocol-buffers/docs/proto3#json
// this is not just JSON-compatible, but also wire-compatible, i.e. the intersection
fieldDescriptorProtoTypeToWireJSONCompatiblityGroup = map[descriptorpb.FieldDescriptorProto_Type]int{
// fixed32 not compatible for wire so not included
descriptorpb.FieldDescriptorProto_TYPE_INT32: 1,
descriptorpb.FieldDescriptorProto_TYPE_UINT32: 1,
// fixed64 not compatible for wire so not included
descriptorpb.FieldDescriptorProto_TYPE_INT64: 2,
descriptorpb.FieldDescriptorProto_TYPE_UINT64: 2,
descriptorpb.FieldDescriptorProto_TYPE_FIXED32: 3,
descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: 3,
descriptorpb.FieldDescriptorProto_TYPE_FIXED64: 4,
descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: 4,
descriptorpb.FieldDescriptorProto_TYPE_BOOL: 5,
descriptorpb.FieldDescriptorProto_TYPE_SINT32: 6,
descriptorpb.FieldDescriptorProto_TYPE_SINT64: 7,
descriptorpb.FieldDescriptorProto_TYPE_STRING: 8,
descriptorpb.FieldDescriptorProto_TYPE_BYTES: 9,
descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: 10,
descriptorpb.FieldDescriptorProto_TYPE_FLOAT: 11,
descriptorpb.FieldDescriptorProto_TYPE_GROUP: 12,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: 14,
descriptorpb.FieldDescriptorProto_TYPE_ENUM: 15,
}
)
// addFunc adds a FileAnnotation.
//
// Both the Descriptor and Location can be nil.
type addFunc func(protosource.Descriptor, []protosource.Descriptor, protosource.Location, string, ...interface{})
// corpus is a store of the previous files and files given to a check function.
//
// this is passed down so that pair functions have access to the original inputs.
type corpus struct {
previousFiles []protosource.File
files []protosource.File
}
func newCorpus(
previousFiles []protosource.File,
files []protosource.File,
) *corpus {
return &corpus{
previousFiles: previousFiles,
files: files,
}
}
func newFilesCheckFunc(
f func(addFunc, *corpus) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return func(id string, ignoreFunc internal.IgnoreFunc, previousFiles []protosource.File, files []protosource.File) ([]bufanalysis.FileAnnotation, error) {
helper := internal.NewHelper(id, ignoreFunc)
if err := f(helper.AddFileAnnotationWithExtraIgnoreDescriptorsf, newCorpus(previousFiles, files)); err != nil {
return nil, err
}
return helper.FileAnnotations(), nil
}
}
func newFilePairCheckFunc(
f func(addFunc, *corpus, protosource.File, protosource.File) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newFilesCheckFunc(
func(add addFunc, corpus *corpus) error {
previousFilePathToFile, err := protosource.FilePathToFile(corpus.previousFiles...)
if err != nil {
return err
}
filePathToFile, err := protosource.FilePathToFile(corpus.files...)
if err != nil {
return err
}
for previousFilePath, previousFile := range previousFilePathToFile {
if file, ok := filePathToFile[previousFilePath]; ok {
if err := f(add, corpus, previousFile, file); err != nil {
return err
}
}
}
return nil
},
)
}
func newEnumPairCheckFunc(
f func(addFunc, *corpus, protosource.Enum, protosource.Enum) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newFilesCheckFunc(
func(add addFunc, corpus *corpus) error {
previousFullNameToEnum, err := protosource.FullNameToEnum(corpus.previousFiles...)
if err != nil {
return err
}
fullNameToEnum, err := protosource.FullNameToEnum(corpus.files...)
if err != nil {
return err
}
for previousFullName, previousEnum := range previousFullNameToEnum {
if enum, ok := fullNameToEnum[previousFullName]; ok {
if err := f(add, corpus, previousEnum, enum); err != nil {
return err
}
}
}
return nil
},
)
}
// compares all the enums that are of the same number
// map is from name to EnumValue for the given number
func newEnumValuePairCheckFunc(
f func(addFunc, *corpus, map[string]protosource.EnumValue, map[string]protosource.EnumValue) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newEnumPairCheckFunc(
func(add addFunc, corpus *corpus, previousEnum protosource.Enum, enum protosource.Enum) error {
previousNumberToNameToEnumValue, err := protosource.NumberToNameToEnumValue(previousEnum)
if err != nil {
return err
}
numberToNameToEnumValue, err := protosource.NumberToNameToEnumValue(enum)
if err != nil {
return err
}
for previousNumber, previousNameToEnumValue := range previousNumberToNameToEnumValue {
if nameToEnumValue, ok := numberToNameToEnumValue[previousNumber]; ok {
if err := f(add, corpus, previousNameToEnumValue, nameToEnumValue); err != nil {
return err
}
}
}
return nil
},
)
}
func newMessagePairCheckFunc(
f func(addFunc, *corpus, protosource.Message, protosource.Message) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newFilesCheckFunc(
func(add addFunc, corpus *corpus) error {
previousFullNameToMessage, err := protosource.FullNameToMessage(corpus.previousFiles...)
if err != nil {
return err
}
fullNameToMessage, err := protosource.FullNameToMessage(corpus.files...)
if err != nil {
return err
}
for previousFullName, previousMessage := range previousFullNameToMessage {
if message, ok := fullNameToMessage[previousFullName]; ok {
if err := f(add, corpus, previousMessage, message); err != nil {
return err
}
}
}
return nil
},
)
}
func newFieldPairCheckFunc(
f func(addFunc, *corpus, protosource.Field, protosource.Field) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newMessagePairCheckFunc(
func(add addFunc, corpus *corpus, previousMessage protosource.Message, message protosource.Message) error {
previousNumberToField, err := protosource.NumberToMessageField(previousMessage)
if err != nil {
return err
}
numberToField, err := protosource.NumberToMessageField(message)
if err != nil {
return err
}
for previousNumber, previousField := range previousNumberToField {
if field, ok := numberToField[previousNumber]; ok {
if err := f(add, corpus, previousField, field); err != nil {
return err
}
}
}
return nil
},
)
}
func newServicePairCheckFunc(
f func(addFunc, *corpus, protosource.Service, protosource.Service) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newFilesCheckFunc(
func(add addFunc, corpus *corpus) error {
previousFullNameToService, err := protosource.FullNameToService(corpus.previousFiles...)
if err != nil {
return err
}
fullNameToService, err := protosource.FullNameToService(corpus.files...)
if err != nil {
return err
}
for previousFullName, previousService := range previousFullNameToService {
if service, ok := fullNameToService[previousFullName]; ok {
if err := f(add, corpus, previousService, service); err != nil {
return err
}
}
}
return nil
},
)
}
func newMethodPairCheckFunc(
f func(addFunc, *corpus, protosource.Method, protosource.Method) error,
) func(string, internal.IgnoreFunc, []protosource.File, []protosource.File) ([]bufanalysis.FileAnnotation, error) {
return newServicePairCheckFunc(
func(add addFunc, corpus *corpus, previousService protosource.Service, service protosource.Service) error {
previousNameToMethod, err := protosource.NameToMethod(previousService)
if err != nil {
return err
}
nameToMethod, err := protosource.NameToMethod(service)
if err != nil {
return err
}
for previousName, previousMethod := range previousNameToMethod {
if method, ok := nameToMethod[previousName]; ok {
if err := f(add, corpus, previousMethod, method); err != nil {
return err
}
}
}
return nil
},
)
}
func getDescriptorAndLocationForDeletedEnum(file protosource.File, previousNestedName string) (protosource.Descriptor, protosource.Location, error) {
if strings.Contains(previousNestedName, ".") {
nestedNameToMessage, err := protosource.NestedNameToMessage(file)
if err != nil {
return nil, nil, err
}
split := strings.Split(previousNestedName, ".")
for i := len(split) - 1; i > 0; i-- {
if message, ok := nestedNameToMessage[strings.Join(split[0:i], ".")]; ok {
return message, message.Location(), nil
}
}
}
return file, nil, nil
}
func getDescriptorAndLocationForDeletedMessage(file protosource.File, nestedNameToMessage map[string]protosource.Message, previousNestedName string) (protosource.Descriptor, protosource.Location) {
if strings.Contains(previousNestedName, ".") {
split := strings.Split(previousNestedName, ".")
for i := len(split) - 1; i > 0; i-- {
if message, ok := nestedNameToMessage[strings.Join(split[0:i], ".")]; ok {
return message, message.Location()
}
}
}
return file, nil
}
func getSortedEnumValueNames(nameToEnumValue map[string]protosource.EnumValue) []string {
names := make([]string, 0, len(nameToEnumValue))
for name := range nameToEnumValue {
names = append(names, name)
}
sort.Strings(names)
return names
}
func getEnumByFullName(files []protosource.File, enumFullName string) (protosource.Enum, error) {
fullNameToEnum, err := protosource.FullNameToEnum(files...)
if err != nil {
return nil, err
}
enum, ok := fullNameToEnum[enumFullName]
if !ok {
return nil, fmt.Errorf("expected enum %q to exist but was not found", enumFullName)
}
return enum, nil
}
func withBackupLocation(primary protosource.Location, secondary protosource.Location) protosource.Location {
if primary != nil {
return primary
}
return secondary
}