knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/depcheck/depcheck.go (about) 1 /* 2 Copyright 2020 The Knative Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package depcheck defines a test utility for ensuring certain packages don't 18 // take on heavy dependencies. 19 package depcheck 20 21 import ( 22 "fmt" 23 "sort" 24 "strings" 25 "testing" 26 27 "golang.org/x/tools/go/packages" 28 ) 29 30 type node struct { 31 importpath string 32 consumers map[string]struct{} 33 } 34 35 type graph map[string]node 36 37 func (g graph) contains(name string) bool { 38 _, ok := g[name] 39 return ok 40 } 41 42 func (g graph) order() []string { 43 order := make(sort.StringSlice, 0, len(g)) 44 for k := range g { 45 order = append(order, k) 46 } 47 order.Sort() 48 return order 49 } 50 51 // path constructs an examplary path that looks something like: 52 // 53 // knative.dev/pkg/apis/duck 54 // knative.dev/pkg/apis # Also: [knative.dev/pkg/kmeta knative.dev/pkg/tracker] 55 // k8s.io/api/core/v1 56 // 57 // See the failing example in the test file. 58 func (g graph) path(name string) []string { 59 n := g[name] 60 // Base case. 61 if len(n.consumers) == 0 { 62 return []string{name} 63 } 64 // Inductive step. 65 consumers := make(sort.StringSlice, 0, len(n.consumers)) 66 for k := range n.consumers { 67 consumers = append(consumers, k) 68 } 69 consumers.Sort() 70 base := g.path(consumers[0]) 71 if len(base) > 1 { // Don't decorate the first entry, which is always an entrypoint. 72 if len(consumers) > 1 { 73 // Attach other consumers to the last entry in base. 74 base = append(base[:len(base)-1], fmt.Sprintf("%s # Also: %v", consumers[0], consumers[1:])) 75 } 76 } 77 return append(base, name) 78 } 79 80 func buildGraph(importpath string, buildFlags ...string) (graph, error) { 81 g := make(graph, 1) 82 pkgs, err := packages.Load(&packages.Config{ 83 Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedModule, 84 BuildFlags: buildFlags, 85 }, importpath) 86 if err != nil { 87 return nil, err 88 } 89 packages.Visit(pkgs, func(pkg *packages.Package) bool { 90 g[pkg.PkgPath] = node{ 91 importpath: pkg.PkgPath, 92 consumers: make(map[string]struct{}), 93 } 94 return pkg.Module != nil 95 }, func(pkg *packages.Package) { 96 for _, imp := range pkg.Imports { 97 if _, ok := g[imp.PkgPath]; ok { 98 g[imp.PkgPath].consumers[pkg.PkgPath] = struct{}{} 99 } 100 } 101 }) 102 return g, nil 103 } 104 105 // CheckNoDependency checks that the given import paths (ip) does not 106 // depend (transitively) on certain banned imports. 107 func CheckNoDependency(ip string, banned []string, buildFlags ...string) error { 108 g, err := buildGraph(ip, buildFlags...) 109 if err != nil { 110 return fmt.Errorf("buildGraph(%q) = %w", ip, err) 111 } 112 for _, dip := range banned { 113 if g.contains(dip) { 114 return fmt.Errorf("%s depends on banned dependency %s\n%s", ip, dip, 115 strings.Join(g.path(dip), "\n")) 116 } 117 } 118 return nil 119 } 120 121 // AssertNoDependency checks that the given import paths (the keys) do not 122 // depend (transitively) on certain banned imports (the values) 123 func AssertNoDependency(t *testing.T, banned map[string][]string, buildFlags ...string) { 124 t.Helper() 125 for ip, banned := range banned { 126 t.Run(ip, func(t *testing.T) { 127 if err := CheckNoDependency(ip, banned, buildFlags...); err != nil { 128 t.Error("CheckNoDependency() =", err) 129 } 130 }) 131 } 132 } 133 134 // CheckOnlyDependencies checks that the given import path only 135 // depends (transitively) on certain allowed imports. 136 // Note: while perhaps counterintuitive we allow the value to be a superset 137 // of the actual imports to that folks can use a constant that holds blessed 138 // import paths. 139 func CheckOnlyDependencies(ip string, allowed map[string]struct{}, buildFlags ...string) error { 140 g, err := buildGraph(ip, buildFlags...) 141 if err != nil { 142 return fmt.Errorf("buildGraph(%q) = %w", ip, err) 143 } 144 for _, name := range g.order() { 145 if _, ok := allowed[name]; !ok { 146 return fmt.Errorf("dependency %s of %s is not explicitly allowed\n%s", name, ip, 147 strings.Join(g.path(name), "\n")) 148 } 149 } 150 return nil 151 } 152 153 // AssertOnlyDependencies checks that the given import paths (the keys) only 154 // depend (transitively) on certain allowed imports (the values). 155 // Note: while perhaps counterintuitive we allow the value to be a superset 156 // of the actual imports to that folks can use a constant that holds blessed 157 // import paths. 158 func AssertOnlyDependencies(t *testing.T, allowed map[string][]string, buildFlags ...string) { 159 t.Helper() 160 for ip, allow := range allowed { 161 // Always include our own package in the set of allowed dependencies. 162 allowed := make(map[string]struct{}, len(allow)+1) 163 for _, x := range append(allow, ip) { 164 allowed[x] = struct{}{} 165 } 166 t.Run(ip, func(t *testing.T) { 167 if err := CheckOnlyDependencies(ip, allowed, buildFlags...); err != nil { 168 t.Error("CheckOnlyDependencies() =", err) 169 } 170 }) 171 } 172 }