github.com/googleapis/api-linter@v1.65.2/rules/aip0121/no_mutable_cycles.go (about) 1 // Copyright 2023 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 aip0121 16 17 import ( 18 "strings" 19 20 "bitbucket.org/creachadair/stringset" 21 "github.com/googleapis/api-linter/lint" 22 "github.com/googleapis/api-linter/locations" 23 "github.com/googleapis/api-linter/rules/internal/utils" 24 "github.com/jhump/protoreflect/desc" 25 ) 26 27 var noMutableCycles = &lint.MessageRule{ 28 Name: lint.NewRuleName(121, "no-mutable-cycles"), 29 OnlyIf: utils.IsResource, 30 LintMessage: func(m *desc.MessageDescriptor) []lint.Problem { 31 res := utils.GetResource(m) 32 33 return findCycles(res.GetType(), m, stringset.New(), nil) 34 }, 35 } 36 37 func findCycles(start string, node *desc.MessageDescriptor, seen stringset.Set, chain []string) []lint.Problem { 38 var problems []lint.Problem 39 nodeRes := utils.GetResource(node) 40 41 chain = append(chain, nodeRes.GetType()) 42 seen.Add(nodeRes.GetType()) 43 44 for _, f := range node.GetFields() { 45 if !isMutableReference(f) { 46 continue 47 } 48 ref := utils.GetResourceReference(f) 49 // Skip indirect references for now. 50 if ref.GetChildType() != "" { 51 continue 52 } 53 if ref.GetType() == start { 54 cycle := strings.Join(append(chain, start), " > ") 55 problems = append(problems, lint.Problem{ 56 Message: "mutable resource reference introduces a reference cycle:\n" + cycle, 57 Descriptor: f, 58 Location: locations.FieldResourceReference(f), 59 }) 60 } else if !seen.Contains(ref.GetType()) { 61 next := utils.FindResourceMessage(ref.GetType(), node.GetFile()) 62 // Skip unresolvable references. 63 if next == nil { 64 continue 65 } 66 if probs := findCycles(start, next, seen.Clone(), chain); len(probs) == 1 { 67 // A recursive call will only have one finding as it returns 68 // immediately. 69 p := probs[0] 70 p.Descriptor = f 71 p.Location = locations.FieldResourceReference(f) 72 73 problems = append(problems, p) 74 } 75 } 76 // Recursive calls should return immediately upon finding a cycle. 77 // The initial scan of the starting resource message should scan 78 // all of its own fields and not return immediately. 79 if len(problems) > 0 && len(chain) > 1 { 80 return problems 81 } 82 } 83 84 return problems 85 } 86 87 func isMutableReference(f *desc.FieldDescriptor) bool { 88 behaviors := utils.GetFieldBehavior(f) 89 return utils.HasResourceReference(f) && !behaviors.Contains("OUTPUT_ONLY") 90 }