~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
142 lines · 3.4 KB Raw History
package main

import (
	"flag"
	"fmt"
	"go/token"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"golang.org/x/tools/go/packages"
)

type finding struct {
	rule string
	at   string
	msg  string
}

type checker struct {
	root     string
	module   string
	fset     *token.FileSet
	pkgs     []*packages.Package
	byPath   map[string]*packages.Package
	findings []finding
}

func (c *checker) fail(rule, at, format string, args ...any) {
	c.findings = append(c.findings, finding{rule: rule, at: at, msg: fmt.Sprintf(format, args...)})
}

func (c *checker) rel(path string) string {
	if path == c.module {
		return "."
	}
	return strings.TrimPrefix(path, c.module+"/")
}

func (c *checker) local(path string) bool {
	return path == c.module || strings.HasPrefix(path, c.module+"/")
}

func (c *checker) pos(p token.Pos) string {
	at := c.fset.Position(p)
	if r, err := filepath.Rel(c.root, at.Filename); err == nil {
		at.Filename = r
	}
	return fmt.Sprintf("%s:%d", at.Filename, at.Line)
}

func under(rel string, prefixes ...string) bool {
	for _, p := range prefixes {
		if rel == p || strings.HasPrefix(rel, p+"/") {
			return true
		}
	}
	return false
}

var rules = []struct {
	name string
	run  func(*checker)
}{
	{"layers", (*checker).layers},
	{"backend", (*checker).backends},
	{"obligations", (*checker).obligations},
	{"system-caller", (*checker).systemCallers},
	{"dynamic", (*checker).dynamicClients},
	{"gvr", (*checker).gvrLiterals},
	{"annotation", (*checker).annotationReads},
	{"endpoint", (*checker).endpoints},
	{"assertion", (*checker).assertions},
}

func main() {
	root := flag.String("root", ".", "the repository root")
	only := flag.String("rules", "", "comma-separated rules to run; empty runs every rule")
	flag.Parse()
	abs, err := filepath.Abs(*root)
	if err != nil {
		fmt.Fprintf(os.Stderr, "check-architecture: %v\n", err)
		os.Exit(2)
	}
	c := &checker{root: abs, fset: token.NewFileSet(), byPath: map[string]*packages.Package{}}
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedSyntax |
			packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule,
		Dir:  abs,
		Fset: c.fset,
	}
	pkgs, err := packages.Load(cfg, "./...")
	if err != nil {
		fmt.Fprintf(os.Stderr, "check-architecture: loading the packages: %v\n", err)
		os.Exit(2)
	}
	broken := 0
	packages.Visit(pkgs, nil, func(p *packages.Package) {
		c.byPath[p.PkgPath] = p
		if p.Module != nil && p.Module.Main && c.module == "" {
			c.module = p.Module.Path
		}
	})
	for _, p := range pkgs {
		for _, e := range p.Errors {
			fmt.Fprintf(os.Stderr, "check-architecture: %s: %v\n", p.PkgPath, e)
			broken++
		}
	}
	if broken > 0 {
		os.Exit(2)
	}
	c.pkgs = pkgs
	selected := map[string]bool{}
	for _, r := range strings.Split(*only, ",") {
		if r = strings.TrimSpace(r); r != "" {
			selected[r] = true
		}
	}
	ran := 0
	for _, r := range rules {
		if len(selected) > 0 && !selected[r.name] {
			continue
		}
		r.run(c)
		ran++
	}
	sort.SliceStable(c.findings, func(i, j int) bool {
		if c.findings[i].rule != c.findings[j].rule {
			return c.findings[i].rule < c.findings[j].rule
		}
		return c.findings[i].at < c.findings[j].at
	})
	for _, f := range c.findings {
		fmt.Fprintf(os.Stderr, "check-architecture: FAIL [%s] %s: %s\n", f.rule, f.at, f.msg)
	}
	fmt.Printf("check-architecture: %d rules, %d errors\n", ran, len(c.findings))
	if len(c.findings) > 0 {
		os.Exit(1)
	}
}