summaryrefslogtreecommitdiffstats
path: root/internal/depcheck/depcheck.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/depcheck/depcheck.go')
-rw-r--r--internal/depcheck/depcheck.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/depcheck/depcheck.go b/internal/depcheck/depcheck.go
new file mode 100644
index 0000000..ba24665
--- /dev/null
+++ b/internal/depcheck/depcheck.go
@@ -0,0 +1,186 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package depcheck defines a test utility for ensuring certain packages don't
+// take on heavy dependencies.
+//
+// This is forked from https://pkg.go.dev/knative.dev/pkg/depcheck
+package depcheck
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+)
+
+type node struct {
+ importpath string
+ consumers map[string]struct{}
+}
+
+type graph map[string]node
+
+func (g graph) contains(name string) bool {
+ _, ok := g[name]
+ return ok
+}
+
+func (g graph) order() []string {
+ order := make(sort.StringSlice, 0, len(g))
+ for k := range g {
+ order = append(order, k)
+ }
+ order.Sort()
+ return order
+}
+
+// path constructs an examplary path that looks something like:
+//
+// knative.dev/pkg/apis/duck
+// knative.dev/pkg/apis # Also: [knative.dev/pkg/kmeta knative.dev/pkg/tracker]
+// k8s.io/api/core/v1
+func (g graph) path(name string) []string {
+ n := g[name]
+ // Base case.
+ if len(n.consumers) == 0 {
+ return []string{name}
+ }
+ // Inductive step.
+ consumers := make(sort.StringSlice, 0, len(n.consumers))
+ for k := range n.consumers {
+ consumers = append(consumers, k)
+ }
+ consumers.Sort()
+ base := g.path(consumers[0])
+ if len(base) > 1 { // Don't decorate the first entry, which is always an entrypoint.
+ if len(consumers) > 1 {
+ // Attach other consumers to the last entry in base.
+ base = append(base[:len(base)-1], fmt.Sprintf("%s # Also: %v", consumers[0], consumers[1:]))
+ }
+ }
+ return append(base, name)
+}
+
+func buildGraph(importpath string, buildFlags ...string) (graph, error) {
+ g := make(graph, 1)
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedModule,
+ BuildFlags: buildFlags,
+ }, importpath)
+ if err != nil {
+ return nil, err
+ }
+ packages.Visit(pkgs, func(pkg *packages.Package) bool {
+ g[pkg.PkgPath] = node{
+ importpath: pkg.PkgPath,
+ consumers: make(map[string]struct{}),
+ }
+ return pkg.Module != nil
+ }, func(pkg *packages.Package) {
+ for _, imp := range pkg.Imports {
+ if _, ok := g[imp.PkgPath]; ok {
+ g[imp.PkgPath].consumers[pkg.PkgPath] = struct{}{}
+ }
+ }
+ })
+ return g, nil
+}
+
+// StdlibPackages returns the list of all standard library packages, including
+// some golang.org/x/ dependencies.
+func StdlibPackages() []string {
+ // pkg/registry is allowed to depend on any stdlib package, so collect
+ // all of those -- this also includes golang.org/x/ packages.
+ pkgs, err := packages.Load(nil, "std")
+ if err != nil {
+ panic(fmt.Sprintf("Loading stdlib packages: %v", err))
+ }
+ pkgnames := make([]string, len(pkgs))
+ for idx, p := range pkgs {
+ pkgnames[idx] = p.PkgPath
+ }
+ return pkgnames
+}
+
+// CheckNoDependency checks that the given import paths (ip) does not
+// depend (transitively) on certain banned imports.
+func CheckNoDependency(ip string, banned []string, buildFlags ...string) error {
+ g, err := buildGraph(ip, buildFlags...)
+ if err != nil {
+ return fmt.Errorf("buildGraph(%q) = %w", ip, err)
+ }
+ for _, dip := range banned {
+ if g.contains(dip) {
+ return fmt.Errorf("%s depends on banned dependency %s\n%s", ip, dip,
+ strings.Join(g.path(dip), "\n"))
+ }
+ }
+ return nil
+}
+
+// AssertNoDependency checks that the given import paths (the keys) do not
+// depend (transitively) on certain banned imports (the values)
+func AssertNoDependency(t *testing.T, banned map[string][]string, buildFlags ...string) {
+ t.Helper()
+ for ip, banned := range banned {
+ t.Run(ip, func(t *testing.T) {
+ if err := CheckNoDependency(ip, banned, buildFlags...); err != nil {
+ t.Error("CheckNoDependency() =", err)
+ }
+ })
+ }
+}
+
+// AssertOnlyDependencies checks that the given import paths (the keys) only
+// depend (transitively) on certain allowed imports (the values).
+// Note: while perhaps counterintuitive we allow the value to be a superset
+// of the actual imports to that folks can use a constant that holds blessed
+// import paths.
+func AssertOnlyDependencies(t *testing.T, allowed map[string][]string, buildFlags ...string) {
+ t.Helper()
+ for ip, allow := range allowed {
+ // Always include our own package in the set of allowed dependencies.
+ allowed := make(map[string]struct{}, len(allow)+1)
+ for _, x := range append(allow, ip) {
+ allowed[x] = struct{}{}
+ }
+ t.Run(ip, func(t *testing.T) {
+ if err := CheckOnlyDependencies(ip, allowed, buildFlags...); err != nil {
+ t.Error("CheckOnlyDependencies() =", err)
+ }
+ })
+ }
+}
+
+// CheckOnlyDependencies checks that the given import path only
+// depends (transitively) on certain allowed imports.
+// Note: while perhaps counterintuitive we allow the value to be a superset
+// of the actual imports to that folks can use a constant that holds blessed
+// import paths.
+func CheckOnlyDependencies(ip string, allowed map[string]struct{}, buildFlags ...string) error {
+ g, err := buildGraph(ip, buildFlags...)
+ if err != nil {
+ return fmt.Errorf("buildGraph(%q) = %w", ip, err)
+ }
+ for _, name := range g.order() {
+ if _, ok := allowed[name]; !ok {
+ return fmt.Errorf("dependency %s of %s is not explicitly allowed\n%s", name, ip,
+ strings.Join(g.path(name), "\n"))
+ }
+ }
+ return nil
+}