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

import (
	"bufio"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

type site struct {
	owner string
	name  string
	class string
	kind  string
	at    string
}

func (c *checker) scanSites() []site {
	var out []site
	for _, top := range []string{"internal", "cmd"} {
		_ = filepath.WalkDir(filepath.Join(c.root, top), func(p string, d fs.DirEntry, err error) error {
			if err != nil || d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
				return nil
			}
			fset := token.NewFileSet()
			f, err := parser.ParseFile(fset, p, nil, 0)
			if err != nil {
				return nil
			}
			rel, _ := filepath.Rel(c.root, p)
			owner, base := filepath.Dir(rel), filepath.Base(rel)
			for _, decl := range f.Decls {
				name := declName(decl)
				ast.Inspect(decl, func(n ast.Node) bool {
					kind := siteKind(n)
					if kind == "" {
						return true
					}
					class := "requeue"
					if strings.HasPrefix(kind, "time.") {
						class = "ticker"
					}
					out = append(out, site{owner: owner, name: base + ":" + name, class: class, kind: kind,
						at: fmt.Sprintf("%s:%d", rel, fset.Position(n.Pos()).Line)})
					return true
				})
			}
			return nil
		})
	}
	return out
}

func siteKind(n ast.Node) string {
	switch x := n.(type) {
	case *ast.CallExpr:
		if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
			if id, ok := sel.X.(*ast.Ident); ok && id.Name == "time" && (sel.Sel.Name == "NewTicker" || sel.Sel.Name == "Tick") {
				return "time." + sel.Sel.Name
			}
			if sel.Sel.Name == "RefreshAfter" {
				return "RefreshAfter"
			}
		}
	case *ast.KeyValueExpr:
		if k, ok := x.Key.(*ast.Ident); ok && k.Name == "RequeueAfter" && periodic(x.Value) {
			return "RequeueAfter"
		}
	case *ast.AssignStmt:
		for i, l := range x.Lhs {
			if sel, ok := l.(*ast.SelectorExpr); ok && sel.Sel.Name == "RequeueAfter" && i < len(x.Rhs) && periodic(x.Rhs[i]) {
				return "RequeueAfter"
			}
		}
	}
	return ""
}

func periodic(e ast.Expr) bool {
	switch v := e.(type) {
	case *ast.BasicLit:
		return v.Value != "0"
	case *ast.SelectorExpr:
		return v.Sel.Name != "RequeueAfter"
	}
	return true
}

func declName(d ast.Decl) string {
	switch x := d.(type) {
	case *ast.FuncDecl:
		if x.Recv != nil && len(x.Recv.List) > 0 {
			t := x.Recv.List[0].Type
			if s, ok := t.(*ast.StarExpr); ok {
				t = s.X
			}
			if ix, ok := t.(*ast.IndexExpr); ok {
				t = ix.X
			}
			if ix, ok := t.(*ast.IndexListExpr); ok {
				t = ix.X
			}
			if id, ok := t.(*ast.Ident); ok {
				return id.Name + "." + x.Name.Name
			}
		}
		return x.Name.Name
	case *ast.GenDecl:
		for _, s := range x.Specs {
			if v, ok := s.(*ast.ValueSpec); ok && len(v.Names) > 0 {
				return v.Names[0].Name
			}
		}
	}
	return "_"
}

var listedClass = map[string]string{"ticker": "ticker", "requeue": "requeue", "deadline": "requeue"}

const obligationsFile = "deploy/obligations.txt"

func (c *checker) obligations() {
	f, err := os.Open(filepath.Join(c.root, obligationsFile))
	if err != nil {
		c.fail("obligations", obligationsFile, "%v", err)
		return
	}
	defer f.Close()
	type key struct{ owner, name, class string }
	listed := map[key][]int{}
	scanner := bufio.NewScanner(f)
	n := 0
	for scanner.Scan() {
		n++
		line := scanner.Text()
		if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" || strings.HasPrefix(line, "owner\t") {
			continue
		}
		cols := strings.Split(line, "\t")
		if len(cols) < 5 {
			c.fail("obligations", fmt.Sprintf("%s:%d", obligationsFile, n), "a line has owner, site, kind, period and reason")
			continue
		}
		class, scanned := listedClass[cols[2]]
		if !scanned {
			continue
		}
		k := key{cols[0], cols[1], class}
		listed[k] = append(listed[k], n)
	}
	found := map[key][]site{}
	for _, s := range c.scanSites() {
		k := key{s.owner, s.name, s.class}
		found[k] = append(found[k], s)
	}
	for k, sites := range found {
		for i := len(listed[k]); i < len(sites); i++ {
			s := sites[i]
			c.fail("obligations", s.at, "%s site %s\t%s is not listed in %s as %s", s.kind, s.owner, s.name, obligationsFile, kindsOf(k.class))
		}
	}
	for k, lines := range listed {
		for i := len(found[k]); i < len(lines); i++ {
			c.fail("obligations", fmt.Sprintf("%s:%d", obligationsFile, lines[i]), "lists %s\t%s (%s) but the tree has no such site", k.owner, k.name, kindsOf(k.class))
		}
	}
}

func kindsOf(class string) string {
	var out []string
	for k, v := range listedClass {
		if v == class {
			out = append(out, k)
		}
	}
	sort.Strings(out)
	return strings.Join(out, " or ")
}