307 lines
7.4 KiB
Go
307 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Config struct {
|
|
Root string
|
|
Catalog *Catalog // nil skips the semantic pass
|
|
Exceptions string // default: <Root>/tools/depcheck/exceptions.txt
|
|
}
|
|
|
|
type Violation struct {
|
|
Variants []string
|
|
Rule string
|
|
Where string
|
|
Detail string
|
|
}
|
|
|
|
func (v Violation) String() string {
|
|
return fmt.Sprintf("%s: [%s] %s (%s)", v.Where, v.Rule, v.Detail, strings.Join(v.Variants, ","))
|
|
}
|
|
|
|
type collector struct {
|
|
module string
|
|
exc []*exception
|
|
byKey map[string]*Violation
|
|
}
|
|
|
|
func (c *collector) add(variant, rule, where, detail string) {
|
|
key := rule + "\x00" + where + "\x00" + detail
|
|
if v, ok := c.byKey[key]; ok {
|
|
for _, x := range v.Variants {
|
|
if x == variant {
|
|
return
|
|
}
|
|
}
|
|
v.Variants = append(v.Variants, variant)
|
|
return
|
|
}
|
|
c.byKey[key] = &Violation{Variants: []string{variant}, Rule: rule, Where: where, Detail: detail}
|
|
}
|
|
|
|
func (c *collector) list() []Violation {
|
|
out := make([]Violation, 0, len(c.byKey))
|
|
for _, v := range c.byKey {
|
|
out = append(out, *v)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Where != out[j].Where {
|
|
return out[i].Where < out[j].Where
|
|
}
|
|
return out[i].Rule+out[i].Detail < out[j].Rule+out[j].Detail
|
|
})
|
|
return out
|
|
}
|
|
|
|
func Check(cfg Config) ([]Violation, error) {
|
|
root, err := filepath.Abs(cfg.Root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
module, err := modulePath(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
excFile := cfg.Exceptions
|
|
if excFile == "" {
|
|
excFile = filepath.Join(root, exceptionsFile)
|
|
}
|
|
exc, bad, err := loadExceptions(excFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &collector{module: module, exc: exc, byKey: map[string]*Violation{}}
|
|
for _, b := range bad {
|
|
c.add("exceptions", "exception-syntax", b, "")
|
|
}
|
|
|
|
def, err := loadGraph(root, module, variantSpec{name: "default", patterns: []string{"./..."}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
specs := []variantSpec{
|
|
{name: "editor", tags: "aego_editor", patterns: []string{"./..."}},
|
|
{name: "nonative", tags: "aego_nonative", patterns: []string{"./..."}},
|
|
{name: "runtime", tags: "aego_runtime", patterns: modulePaths(module, runtimeRoots(def))},
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "cmd", "aego-playground")); err == nil {
|
|
specs = append(specs, variantSpec{name: "wasm", env: []string{"GOOS=js", "GOARCH=wasm", "CGO_ENABLED=0"}, patterns: []string{"./cmd/aego-playground"}})
|
|
}
|
|
graphs := map[string]*graph{"default": def}
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
var firstErr error
|
|
for _, s := range specs {
|
|
if len(s.patterns) == 0 {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func(s variantSpec) {
|
|
defer wg.Done()
|
|
g, err := loadGraph(root, module, s)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
graphs[s.name] = g
|
|
}(s)
|
|
}
|
|
wg.Wait()
|
|
if firstErr != nil {
|
|
return nil, firstErr
|
|
}
|
|
|
|
for _, p := range def.order {
|
|
if _, ok := groupOf(p); !ok && p != "" {
|
|
c.add("default", "no-group", p, "package path has no group prefix (see tools/depcheck/groups.go)")
|
|
}
|
|
}
|
|
for _, name := range []string{"default", "editor", "nonative", "wasm"} {
|
|
if g := graphs[name]; g != nil {
|
|
c.checkEdges(name, g, false)
|
|
}
|
|
}
|
|
c.checkEdges("test", def, true)
|
|
if g := graphs["editor"]; g != nil {
|
|
c.checkEdges("test-editor", g, true)
|
|
}
|
|
if g := graphs["runtime"]; g != nil {
|
|
roots := runtimeRoots(def)
|
|
for _, p := range g.order {
|
|
if allowed(runtimeForbidden, p) {
|
|
c.reach("runtime", "runtime-reaches-dev", g, roots, p)
|
|
}
|
|
}
|
|
}
|
|
if g := graphs["wasm"]; g != nil {
|
|
for _, p := range g.order {
|
|
if !wasmOK(p) {
|
|
c.reach("wasm", "wasm-reaches-forbidden", g, []string{"cmd/aego-playground"}, p)
|
|
}
|
|
}
|
|
}
|
|
variants := map[string]bool{"test": true, "test-editor": true}
|
|
for name := range graphs {
|
|
variants[name] = true
|
|
}
|
|
if cfg.Catalog != nil {
|
|
variants["semantic"] = true
|
|
for i := range cfg.Catalog.Modules {
|
|
m := &cfg.Catalog.Modules[i]
|
|
if m.codes, err = diagPrefixes(root, m.Dir); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
hits, err := scanLiterals(root, def, cfg.Catalog)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, h := range hits {
|
|
if !excepted(exc, "semantic", h.file, h.lit) {
|
|
c.add("semantic", "module-literal", h.pos, h.lit+" names module "+h.module)
|
|
}
|
|
}
|
|
}
|
|
src, err := checkSources(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range src {
|
|
c.add("default", "sources", s, "")
|
|
}
|
|
for _, s := range staleExceptions(exc, variants) {
|
|
c.add("exceptions", "exception-unused", s, "")
|
|
}
|
|
return c.list(), nil
|
|
}
|
|
|
|
// checkEdges applies group rules, and layer rules for non-test edges, to every import in g.
|
|
func (c *collector) checkEdges(variant string, g *graph, tests bool) {
|
|
for _, p := range g.order {
|
|
from, ok := groupOf(p)
|
|
if !ok {
|
|
continue
|
|
}
|
|
info := g.pkgs[p]
|
|
imps, files := info.imports, info.files
|
|
if tests {
|
|
imps, files = info.testImps, info.testFiles
|
|
}
|
|
for _, imp := range imps {
|
|
to, ok := groupOf(imp)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rule := groupRule(from, to)
|
|
if rule == "" && !tests {
|
|
rule = layerRule(p, imp)
|
|
}
|
|
if rule == "" {
|
|
continue
|
|
}
|
|
for _, file := range importingFiles(info.dir, files, c.module+"/"+imp) {
|
|
where := p
|
|
if file != "" {
|
|
where = p + "/" + file
|
|
}
|
|
if !excepted(c.exc, variant, where, imp) {
|
|
c.add(variant, rule, where, "imports "+imp)
|
|
}
|
|
}
|
|
}
|
|
if !tests {
|
|
for _, ext := range info.external {
|
|
if rule := externalRule(p, ext); rule != "" && !excepted(c.exc, variant, p, ext) {
|
|
c.add(variant, rule, p, "imports external "+ext)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *collector) reach(variant, rule string, g *graph, roots []string, target string) {
|
|
chain := g.path(roots, target)
|
|
from := ""
|
|
if len(chain) >= 2 {
|
|
from = chain[len(chain)-2]
|
|
}
|
|
if excepted(c.exc, variant, from, target) {
|
|
return
|
|
}
|
|
c.add(variant, rule, chain[0], "reaches "+target+" via "+strings.Join(chain, " -> "))
|
|
}
|
|
|
|
// importingFiles returns the files of a package that import path.
|
|
func importingFiles(dir string, files []string, path string) []string {
|
|
var out []string
|
|
fset := token.NewFileSet()
|
|
for _, name := range files {
|
|
f, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.ImportsOnly)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, imp := range f.Imports {
|
|
if strings.Trim(imp.Path.Value, `"`) == path {
|
|
out = append(out, name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
out = []string{""}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func wasmOK(p string) bool {
|
|
if allowed(wasmDenied, p) {
|
|
return false
|
|
}
|
|
if g, ok := groupOf(p); ok && (g.kind == kLow || g.kind == kKernel) {
|
|
return true
|
|
}
|
|
return allowed(wasmExtra, p)
|
|
}
|
|
|
|
// runtimeRoots are the default graph's packages minus runtimeForbidden.
|
|
func runtimeRoots(def *graph) []string {
|
|
var out []string
|
|
for _, p := range def.order {
|
|
if p != "" && !allowed(runtimeForbidden, p) {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func modulePaths(module string, pkgs []string) []string {
|
|
out := make([]string, len(pkgs))
|
|
for i, p := range pkgs {
|
|
out[i] = module + "/" + p
|
|
}
|
|
return out
|
|
}
|
|
|
|
// RuntimePackages lists the import paths of the aego_runtime build.
|
|
func RuntimePackages(root string) ([]string, error) {
|
|
module, err := modulePath(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
def, err := loadGraph(root, module, variantSpec{name: "default", patterns: []string{"./..."}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return modulePaths(module, runtimeRoots(def)), nil
|
|
}
|