github.com/googleapis/api-linter@v1.65.2/lint/lint.go (about)

     1  // Copyright 2019 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  // 		https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package lint provides lint functions for Google APIs that register rules and user configurations,
    16  // apply those rules to a lint request, and produce lint results.
    17  package lint
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"runtime/debug"
    23  	"strings"
    24  
    25  	"github.com/jhump/protoreflect/desc"
    26  )
    27  
    28  // Linter checks API files and returns a list of detected problems.
    29  type Linter struct {
    30  	rules                 RuleRegistry
    31  	configs               Configs
    32  	debug                 bool
    33  	ignoreCommentDisables bool
    34  }
    35  
    36  // LinterOption prvoides the ability to configure the Linter.
    37  type LinterOption func(l *Linter)
    38  
    39  // Debug is a LinterOption for setting if debug mode is on.
    40  func Debug(debug bool) LinterOption {
    41  	return func(l *Linter) {
    42  		l.debug = debug
    43  	}
    44  }
    45  
    46  // IgnoreCommentDisables sets the flag for ignoring comments which disable rules.
    47  func IgnoreCommentDisables(ignoreCommentDisables bool) LinterOption {
    48  	return func(l *Linter) {
    49  		l.ignoreCommentDisables = ignoreCommentDisables
    50  	}
    51  }
    52  
    53  // New creates and returns a linter with the given rules and configs.
    54  func New(rules RuleRegistry, configs Configs, opts ...LinterOption) *Linter {
    55  	l := &Linter{
    56  		rules:   rules,
    57  		configs: configs,
    58  	}
    59  
    60  	for _, opt := range opts {
    61  		opt(l)
    62  	}
    63  
    64  	return l
    65  }
    66  
    67  // LintProtos checks protobuf files and returns a list of problems or an error.
    68  func (l *Linter) LintProtos(files ...*desc.FileDescriptor) ([]Response, error) {
    69  	var responses []Response
    70  	for _, proto := range files {
    71  		resp, err := l.lintFileDescriptor(proto)
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  		responses = append(responses, resp)
    76  	}
    77  	return responses, nil
    78  }
    79  
    80  // run executes rules on the request.
    81  //
    82  // It uses the proto file path to determine which rules will
    83  // be applied to the request, according to the list of Linter
    84  // configs.
    85  func (l *Linter) lintFileDescriptor(fd *desc.FileDescriptor) (Response, error) {
    86  	resp := Response{
    87  		FilePath: fd.GetName(),
    88  		Problems: []Problem{},
    89  	}
    90  	var errMessages []string
    91  
    92  	for name, rule := range l.rules {
    93  		// Run the linter rule against this file, and throw away any problems
    94  		// which should have been disabled.
    95  		if l.configs.IsRuleEnabled(string(name), fd.GetName()) {
    96  			if problems, err := l.runAndRecoverFromPanics(rule, fd); err == nil {
    97  				for _, p := range problems {
    98  					if p.Descriptor == nil {
    99  						errMessages = append(errMessages, fmt.Sprintf("rule %q missing required Descriptor in returned Problem", rule.GetName()))
   100  						continue
   101  					}
   102  					if ruleIsEnabled(rule, p.Descriptor, p.Location, aliasMap, l.ignoreCommentDisables) {
   103  						p.RuleID = rule.GetName()
   104  						resp.Problems = append(resp.Problems, p)
   105  					}
   106  				}
   107  			} else {
   108  				errMessages = append(errMessages, err.Error())
   109  			}
   110  		}
   111  	}
   112  
   113  	var err error
   114  	if len(errMessages) != 0 {
   115  		err = errors.New(strings.Join(errMessages, "; "))
   116  	}
   117  
   118  	return resp, err
   119  }
   120  
   121  func (l *Linter) runAndRecoverFromPanics(rule ProtoRule, fd *desc.FileDescriptor) (probs []Problem, err error) {
   122  	defer func() {
   123  		if r := recover(); r != nil {
   124  			if l.debug {
   125  				debug.PrintStack()
   126  			}
   127  			if rerr, ok := r.(error); ok {
   128  				err = rerr
   129  			} else {
   130  				err = fmt.Errorf("panic occurred during rule execution: %v", r)
   131  			}
   132  		}
   133  	}()
   134  
   135  	return rule.Lint(fd), nil
   136  }