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  }