blob: c3421412b3f4466c5587ec84a10950d724479a8b [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 internal
import (
"context"
"strings"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufanalysis"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/normalpath"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/protosource"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/protoversion"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/stringutil"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.uber.org/multierr"
"go.uber.org/zap"
)
const (
tracerName = "bufbuild/buf"
)
// Runner is a runner.
type Runner struct {
logger *zap.Logger
ignorePrefix string
tracer trace.Tracer
}
// NewRunner returns a new Runner.
func NewRunner(logger *zap.Logger, options ...RunnerOption) *Runner {
runner := &Runner{
logger: logger,
tracer: otel.GetTracerProvider().Tracer(tracerName),
}
for _, option := range options {
option(runner)
}
return runner
}
// RunnerOption is an option for a new Runner.
type RunnerOption func(*Runner)
// RunnerWithIgnorePrefix returns a new RunnerOption that sets the comment ignore prefix.
//
// This will result in failures where the location has "ignore_prefix id" in the leading
// comment being ignored.
//
// The default is to not enable comment ignores.
func RunnerWithIgnorePrefix(ignorePrefix string) RunnerOption {
return func(runner *Runner) {
runner.ignorePrefix = ignorePrefix
}
}
// Check runs the Rules.
func (r *Runner) Check(ctx context.Context, config *Config, previousFiles []protosource.File, files []protosource.File) ([]bufanalysis.FileAnnotation, error) {
rules := config.Rules
if len(rules) == 0 {
return nil, nil
}
ctx, span := r.tracer.Start(ctx, "check", trace.WithAttributes(
attribute.Key("num_files").Int(len(files)),
attribute.Key("num_rules").Int(len(rules)),
))
defer span.End()
ignoreFunc := r.newIgnoreFunc(config)
var fileAnnotations []bufanalysis.FileAnnotation
resultC := make(chan *result, len(rules))
for _, rule := range rules {
rule := rule
go func() {
iFileAnnotations, iErr := rule.check(ignoreFunc, previousFiles, files)
resultC <- newResult(iFileAnnotations, iErr)
}()
}
var err error
for i := 0; i < len(rules); i++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-resultC:
fileAnnotations = append(fileAnnotations, result.FileAnnotations...)
err = multierr.Append(err, result.Err)
}
}
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
bufanalysis.SortFileAnnotations(fileAnnotations)
return fileAnnotations, nil
}
func (r *Runner) newIgnoreFunc(config *Config) IgnoreFunc {
return func(id string, descriptors []protosource.Descriptor, locations []protosource.Location) bool {
if idIsIgnored(id, descriptors, config) {
return true
}
// if ignorePrefix is empty, comment ignores are not enabled for the runner
// this is the case with breaking changes
if r.ignorePrefix != "" && config.AllowCommentIgnores &&
locationsAreIgnored(id, r.ignorePrefix, locations, config) {
return true
}
if config.IgnoreUnstablePackages {
for _, descriptor := range descriptors {
if descriptorPackageIsUnstable(descriptor) {
return true
}
}
}
return false
}
}
func idIsIgnored(id string, descriptors []protosource.Descriptor, config *Config) bool {
for _, descriptor := range descriptors {
// OR of descriptors
if idIsIgnoredForDescriptor(id, descriptor, config) {
return true
}
}
return false
}
func idIsIgnoredForDescriptor(id string, descriptor protosource.Descriptor, config *Config) bool {
if descriptor == nil {
return false
}
path := descriptor.File().Path()
if normalpath.MapHasEqualOrContainingPath(config.IgnoreRootPaths, path, normalpath.Relative) {
return true
}
if id == "" {
return false
}
ignoreRootPaths, ok := config.IgnoreIDToRootPaths[id]
if !ok {
return false
}
return normalpath.MapHasEqualOrContainingPath(ignoreRootPaths, path, normalpath.Relative)
}
func locationsAreIgnored(id string, ignorePrefix string, locations []protosource.Location, config *Config) bool {
// we already check that ignorePrefix is non-empty, but just doing here for safety
if id == "" || ignorePrefix == "" {
return false
}
fullIgnorePrefix := ignorePrefix + " " + id
for _, location := range locations {
if location != nil {
if leadingComments := location.LeadingComments(); leadingComments != "" {
for _, line := range stringutil.SplitTrimLinesNoEmpty(leadingComments) {
if strings.HasPrefix(line, fullIgnorePrefix) {
return true
}
}
}
}
}
return false
}
func descriptorPackageIsUnstable(descriptor protosource.Descriptor) bool {
if descriptor == nil {
return false
}
packageVersion, ok := protoversion.NewPackageVersionForPackage(descriptor.File().Package())
if !ok {
return false
}
return packageVersion.StabilityLevel() != protoversion.StabilityLevelStable
}
type result struct {
FileAnnotations []bufanalysis.FileAnnotation
Err error
}
func newResult(fileAnnotations []bufanalysis.FileAnnotation, err error) *result {
return &result{
FileAnnotations: fileAnnotations,
Err: err,
}
}