github.com/googleapis/api-linter@v1.65.2/lint/rule.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
    16  
    17  import (
    18  	"regexp"
    19  	"strings"
    20  
    21  	"github.com/jhump/protoreflect/desc"
    22  	dpb "google.golang.org/protobuf/types/descriptorpb"
    23  )
    24  
    25  // ProtoRule defines a lint rule that checks Google Protobuf APIs.
    26  //
    27  // Anything that satisfies this interface can be used as a rule,
    28  // but most rule authors will want to use the implementations provided.
    29  //
    30  // Rules must only report errors in the file under which they are being run
    31  // (not imported files).
    32  type ProtoRule interface {
    33  	// GetName returns the name of the rule.
    34  	GetName() RuleName
    35  
    36  	// Lint accepts a FileDescriptor and lints it,
    37  	// returning a slice of Problem objects it finds.
    38  	Lint(*desc.FileDescriptor) []Problem
    39  }
    40  
    41  // FileRule defines a lint rule that checks a file as a whole.
    42  type FileRule struct {
    43  	Name RuleName
    44  
    45  	// LintFile accepts a FileDescriptor and lints it, returning a slice of
    46  	// Problems it finds.
    47  	LintFile func(*desc.FileDescriptor) []Problem
    48  
    49  	// OnlyIf accepts a FileDescriptor and determines whether this rule
    50  	// is applicable.
    51  	OnlyIf func(*desc.FileDescriptor) bool
    52  
    53  	//lint:ignore U1000 ignored via golint previously
    54  	noPositional struct{}
    55  }
    56  
    57  // GetName returns the name of the rule.
    58  func (r *FileRule) GetName() RuleName {
    59  	return r.Name
    60  }
    61  
    62  // Lint forwards the FileDescriptor to the LintFile method defined on the
    63  // FileRule.
    64  func (r *FileRule) Lint(fd *desc.FileDescriptor) []Problem {
    65  	if r.OnlyIf == nil || r.OnlyIf(fd) {
    66  		return r.LintFile(fd)
    67  	}
    68  	return nil
    69  }
    70  
    71  // MessageRule defines a lint rule that is run on each message in the file.
    72  //
    73  // Both top-level messages and nested messages are visited.
    74  type MessageRule struct {
    75  	Name RuleName
    76  
    77  	// LintMessage accepts a MessageDescriptor and lints it, returning a slice
    78  	// of Problems it finds.
    79  	LintMessage func(*desc.MessageDescriptor) []Problem
    80  
    81  	// OnlyIf accepts a MessageDescriptor and determines whether this rule
    82  	// is applicable.
    83  	OnlyIf func(*desc.MessageDescriptor) bool
    84  
    85  	//lint:ignore U1000 ignored via golint previously
    86  	noPositional struct{}
    87  }
    88  
    89  // GetName returns the name of the rule.
    90  func (r *MessageRule) GetName() RuleName {
    91  	return r.Name
    92  }
    93  
    94  // Lint visits every message in the file, and runs `LintMessage`.
    95  //
    96  // If an `OnlyIf` function is provided on the rule, it is run against each
    97  // message, and if it returns false, the `LintMessage` function is not called.
    98  func (r *MessageRule) Lint(fd *desc.FileDescriptor) []Problem {
    99  	problems := []Problem{}
   100  
   101  	// Iterate over each message and process rules for each message.
   102  	for _, message := range GetAllMessages(fd) {
   103  		if r.OnlyIf == nil || r.OnlyIf(message) {
   104  			problems = append(problems, r.LintMessage(message)...)
   105  		}
   106  	}
   107  	return problems
   108  }
   109  
   110  // FieldRule defines a lint rule that is run on each field within a file.
   111  type FieldRule struct {
   112  	Name RuleName
   113  
   114  	// LintField accepts a FieldDescriptor and lints it, returning a slice of
   115  	// Problems it finds.
   116  	LintField func(*desc.FieldDescriptor) []Problem
   117  
   118  	// OnlyIf accepts a FieldDescriptor and determines whether this rule
   119  	// is applicable.
   120  	OnlyIf func(*desc.FieldDescriptor) bool
   121  
   122  	//lint:ignore U1000 ignored via golint previously
   123  	noPositional struct{}
   124  }
   125  
   126  // GetName returns the name of the rule.
   127  func (r *FieldRule) GetName() RuleName {
   128  	return r.Name
   129  }
   130  
   131  // Lint visits every field in the file and runs `LintField`.
   132  //
   133  // If an `OnlyIf` function is provided on the rule, it is run against each
   134  // field, and if it returns false, the `LintField` function is not called.
   135  func (r *FieldRule) Lint(fd *desc.FileDescriptor) []Problem {
   136  	problems := []Problem{}
   137  
   138  	// Iterate over each message and process rules for each field in that
   139  	// message.
   140  	for _, message := range GetAllMessages(fd) {
   141  		for _, field := range message.GetFields() {
   142  			if r.OnlyIf == nil || r.OnlyIf(field) {
   143  				problems = append(problems, r.LintField(field)...)
   144  			}
   145  		}
   146  	}
   147  	return problems
   148  }
   149  
   150  // ServiceRule defines a lint rule that is run on each service.
   151  type ServiceRule struct {
   152  	Name RuleName
   153  
   154  	// LintService accepts a ServiceDescriptor and lints it.
   155  	LintService func(*desc.ServiceDescriptor) []Problem
   156  
   157  	// OnlyIf accepts a ServiceDescriptor and determines whether this rule
   158  	// is applicable.
   159  	OnlyIf func(*desc.ServiceDescriptor) bool
   160  
   161  	//lint:ignore U1000 ignored via golint previously
   162  	noPositional struct{}
   163  }
   164  
   165  // GetName returns the name of the rule.
   166  func (r *ServiceRule) GetName() RuleName {
   167  	return r.Name
   168  }
   169  
   170  // Lint visits every service in the file and runs `LintService`.
   171  //
   172  // If an `OnlyIf` function is provided on the rule, it is run against each
   173  // service, and if it returns false, the `LintService` function is not called.
   174  func (r *ServiceRule) Lint(fd *desc.FileDescriptor) []Problem {
   175  	problems := []Problem{}
   176  	for _, service := range fd.GetServices() {
   177  		if r.OnlyIf == nil || r.OnlyIf(service) {
   178  			problems = append(problems, r.LintService(service)...)
   179  		}
   180  	}
   181  	return problems
   182  }
   183  
   184  // MethodRule defines a lint rule that is run on each method.
   185  type MethodRule struct {
   186  	Name RuleName
   187  
   188  	// LintMethod accepts a MethodDescriptor and lints it.
   189  	LintMethod func(*desc.MethodDescriptor) []Problem
   190  
   191  	// OnlyIf accepts a MethodDescriptor and determines whether this rule
   192  	// is applicable.
   193  	OnlyIf func(*desc.MethodDescriptor) bool
   194  
   195  	//lint:ignore U1000 ignored via golint previously
   196  	noPositional struct{}
   197  }
   198  
   199  // GetName returns the name of the rule.
   200  func (r *MethodRule) GetName() RuleName {
   201  	return r.Name
   202  }
   203  
   204  // Lint visits every method in the file and runs `LintMethod`.
   205  //
   206  // If an `OnlyIf` function is provided on the rule, it is run against each
   207  // method, and if it returns false, the `LintMethod` function is not called.
   208  func (r *MethodRule) Lint(fd *desc.FileDescriptor) []Problem {
   209  	problems := []Problem{}
   210  	for _, service := range fd.GetServices() {
   211  		for _, method := range service.GetMethods() {
   212  			if r.OnlyIf == nil || r.OnlyIf(method) {
   213  				problems = append(problems, r.LintMethod(method)...)
   214  			}
   215  		}
   216  	}
   217  	return problems
   218  }
   219  
   220  // EnumRule defines a lint rule that is run on each enum.
   221  type EnumRule struct {
   222  	Name RuleName
   223  
   224  	// LintEnum accepts a EnumDescriptor and lints it.
   225  	LintEnum func(*desc.EnumDescriptor) []Problem
   226  
   227  	// OnlyIf accepts an EnumDescriptor and determines whether this rule
   228  	// is applicable.
   229  	OnlyIf func(*desc.EnumDescriptor) bool
   230  
   231  	//lint:ignore U1000 ignored via golint previously
   232  	noPositional struct{}
   233  }
   234  
   235  // GetName returns the name of the rule.
   236  func (r *EnumRule) GetName() RuleName {
   237  	return r.Name
   238  }
   239  
   240  // Lint visits every enum in the file and runs `LintEnum`.
   241  //
   242  // If an `OnlyIf` function is provided on the rule, it is run against each
   243  // enum, and if it returns false, the `LintEnum` function is not called.
   244  func (r *EnumRule) Lint(fd *desc.FileDescriptor) []Problem {
   245  	problems := []Problem{}
   246  
   247  	// Lint all enums, either at the top of the file, or nested within messages.
   248  	for _, enum := range getAllEnums(fd) {
   249  		if r.OnlyIf == nil || r.OnlyIf(enum) {
   250  			problems = append(problems, r.LintEnum(enum)...)
   251  		}
   252  	}
   253  	return problems
   254  }
   255  
   256  // EnumValueRule defines a lint rule that is run on each enum value.
   257  type EnumValueRule struct {
   258  	Name RuleName
   259  
   260  	// LintEnumValue accepts a EnumValueDescriptor and lints it.
   261  	LintEnumValue func(*desc.EnumValueDescriptor) []Problem
   262  
   263  	// OnlyIf accepts an EnumValueDescriptor and determines whether this rule
   264  	// is applicable.
   265  	OnlyIf func(*desc.EnumValueDescriptor) bool
   266  
   267  	//lint:ignore U1000 ignored via golint previously
   268  	noPositional struct{}
   269  }
   270  
   271  // GetName returns the name of the rule.
   272  func (r *EnumValueRule) GetName() RuleName {
   273  	return r.Name
   274  }
   275  
   276  // Lint visits every enum value in the file and runs `LintEnum`.
   277  //
   278  // If an `OnlyIf` function is provided on the rule, it is run against each
   279  // enum value, and if it returns false, the `LintEnum` function is not called.
   280  func (r *EnumValueRule) Lint(fd *desc.FileDescriptor) []Problem {
   281  	problems := []Problem{}
   282  
   283  	// Lint all enums, either at the top of the file, or nested within messages.
   284  	for _, enum := range getAllEnums(fd) {
   285  		for _, value := range enum.GetValues() {
   286  			if r.OnlyIf == nil || r.OnlyIf(value) {
   287  				problems = append(problems, r.LintEnumValue(value)...)
   288  			}
   289  		}
   290  	}
   291  	return problems
   292  }
   293  
   294  // DescriptorRule defines a lint rule that is run on every descriptor
   295  // in the file (but not the file itself).
   296  type DescriptorRule struct {
   297  	Name RuleName
   298  
   299  	// LintDescriptor accepts a generic descriptor and lints it.
   300  	//
   301  	// Note: Unless the descriptor is typecast to a more specific type,
   302  	// only a subset of methods are available to it.
   303  	LintDescriptor func(desc.Descriptor) []Problem
   304  
   305  	// OnlyIf accepts a Descriptor and determines whether this rule
   306  	// is applicable.
   307  	OnlyIf func(desc.Descriptor) bool
   308  
   309  	//lint:ignore U1000 ignored via golint previously
   310  	noPositional struct{}
   311  }
   312  
   313  // GetName returns the name of the rule.
   314  func (r *DescriptorRule) GetName() RuleName {
   315  	return r.Name
   316  }
   317  
   318  // Lint visits every descriptor in the file and runs `LintDescriptor`.
   319  //
   320  // It visits every service, method, message, field, enum, and enum value.
   321  // This order is not guaranteed. It does NOT visit the file itself.
   322  func (r *DescriptorRule) Lint(fd *desc.FileDescriptor) []Problem {
   323  	problems := []Problem{}
   324  
   325  	// Iterate over all services and methods.
   326  	for _, service := range fd.GetServices() {
   327  		if r.OnlyIf == nil || r.OnlyIf(service) {
   328  			problems = append(problems, r.LintDescriptor(service)...)
   329  		}
   330  		for _, method := range service.GetMethods() {
   331  			if r.OnlyIf == nil || r.OnlyIf(method) {
   332  				problems = append(problems, r.LintDescriptor(method)...)
   333  			}
   334  		}
   335  	}
   336  
   337  	// Iterate over all messages, and all fields within each message.
   338  	for _, message := range GetAllMessages(fd) {
   339  		if r.OnlyIf == nil || r.OnlyIf(message) {
   340  			problems = append(problems, r.LintDescriptor(message)...)
   341  		}
   342  		for _, field := range message.GetFields() {
   343  			if r.OnlyIf == nil || r.OnlyIf(field) {
   344  				problems = append(problems, r.LintDescriptor(field)...)
   345  			}
   346  		}
   347  	}
   348  
   349  	// Iterate over all enums and enum values.
   350  	for _, enum := range getAllEnums(fd) {
   351  		if r.OnlyIf == nil || r.OnlyIf(enum) {
   352  			problems = append(problems, r.LintDescriptor(enum)...)
   353  		}
   354  		for _, value := range enum.GetValues() {
   355  			if r.OnlyIf == nil || r.OnlyIf(value) {
   356  				problems = append(problems, r.LintDescriptor(value)...)
   357  			}
   358  		}
   359  	}
   360  
   361  	// Done; return the full set of problems.
   362  	return problems
   363  }
   364  
   365  var disableRuleNameRegex = regexp.MustCompile(`api-linter:\s*(.+)\s*=\s*disabled`)
   366  
   367  func extractDisabledRuleName(commentLine string) string {
   368  	match := disableRuleNameRegex.FindStringSubmatch(commentLine)
   369  	if len(match) > 0 {
   370  		return match[1]
   371  	}
   372  	return ""
   373  }
   374  
   375  func getLeadingComments(d desc.Descriptor) string {
   376  	if sourceInfo := d.GetSourceInfo(); sourceInfo != nil {
   377  		return sourceInfo.GetLeadingComments()
   378  	}
   379  	return ""
   380  }
   381  
   382  // GetAllMessages returns a slice with every message (not just top-level
   383  // messages) in the file.
   384  func GetAllMessages(f *desc.FileDescriptor) (messages []*desc.MessageDescriptor) {
   385  	messages = append(messages, f.GetMessageTypes()...)
   386  	for _, message := range f.GetMessageTypes() {
   387  		messages = append(messages, getAllNestedMessages(message)...)
   388  	}
   389  	return messages
   390  }
   391  
   392  // getAllNestedMessages returns a slice with the given message descriptor as well
   393  // as all nested message descriptors, traversing to arbitrary depth.
   394  func getAllNestedMessages(m *desc.MessageDescriptor) (messages []*desc.MessageDescriptor) {
   395  	for _, nested := range m.GetNestedMessageTypes() {
   396  		if !nested.IsMapEntry() { // Don't include the synthetic message type that represents an entry in a map field.
   397  			messages = append(messages, nested)
   398  		}
   399  		messages = append(messages, getAllNestedMessages(nested)...)
   400  	}
   401  	return messages
   402  }
   403  
   404  // getAllEnums returns a slice with every enum (not just top-level enums)
   405  // in the file.
   406  func getAllEnums(f *desc.FileDescriptor) (enums []*desc.EnumDescriptor) {
   407  	// Append all enums at the top level.
   408  	enums = append(enums, f.GetEnumTypes()...)
   409  
   410  	// Append all enums nested within messages.
   411  	for _, m := range GetAllMessages(f) {
   412  		enums = append(enums, m.GetNestedEnumTypes()...)
   413  	}
   414  
   415  	return
   416  }
   417  
   418  // fileHeader attempts to get the comment at the top of the file, but it
   419  // is on a best effort basis because protobuf is inconsistent.
   420  //
   421  // Taken from https://github.com/jhump/protoreflect/issues/215
   422  func fileHeader(fd *desc.FileDescriptor) string {
   423  	var firstLoc *dpb.SourceCodeInfo_Location
   424  	var firstSpan int64
   425  
   426  	// File level comments should only be comments identified on either
   427  	// syntax (12), package (2), option (8), or import (3) statements.
   428  	allowedPaths := map[int32]struct{}{2: {}, 3: {}, 8: {}, 12: {}}
   429  
   430  	// Iterate over locations in the file descriptor looking for
   431  	// what we think is a file-level comment.
   432  	for _, curr := range fd.AsFileDescriptorProto().GetSourceCodeInfo().GetLocation() {
   433  		// Skip locations that have no comments.
   434  		if curr.LeadingComments == nil && len(curr.LeadingDetachedComments) == 0 {
   435  			continue
   436  		}
   437  		// Skip locations that are not allowed because they should never be
   438  		// mistaken for file-level comments.
   439  		if _, ok := allowedPaths[curr.GetPath()[0]]; !ok {
   440  			continue
   441  		}
   442  		currSpan := asPos(curr.Span)
   443  		if firstLoc == nil || currSpan < firstSpan {
   444  			firstLoc = curr
   445  			firstSpan = currSpan
   446  		}
   447  	}
   448  	if firstLoc == nil {
   449  		return ""
   450  	}
   451  	if len(firstLoc.LeadingDetachedComments) > 0 {
   452  		return strings.Join(firstLoc.LeadingDetachedComments, "\n")
   453  	}
   454  	return firstLoc.GetLeadingComments()
   455  }
   456  
   457  func asPos(span []int32) int64 {
   458  	return (int64(span[0]) << 32) + int64(span[1])
   459  }