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 }