150 lines
3.5 KiB
Go
150 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// An exception lets one edge through; one that matches nothing is a violation.
|
|
type exception struct {
|
|
line int
|
|
from string
|
|
to string
|
|
variant string
|
|
reason string
|
|
used bool
|
|
}
|
|
|
|
const exceptionsFile = "tools/depcheck/exceptions.txt"
|
|
|
|
var knownVariants = map[string]bool{
|
|
"*": true, "default": true, "editor": true, "nonative": true, "runtime": true, "wasm": true,
|
|
"test": true, "test-editor": true, "semantic": true,
|
|
}
|
|
|
|
func loadExceptions(file string) ([]*exception, []string, error) {
|
|
f, err := os.Open(file)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer f.Close()
|
|
var out []*exception
|
|
var bad []string
|
|
sc := bufio.NewScanner(f)
|
|
n := 0
|
|
for sc.Scan() {
|
|
n++
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
e, err := parseException(line)
|
|
if err != nil {
|
|
bad = append(bad, fmt.Sprintf("%s:%d: [exception-syntax] %v", path.Base(file), n, err))
|
|
continue
|
|
}
|
|
e.line = n
|
|
out = append(out, e)
|
|
}
|
|
return out, bad, sc.Err()
|
|
}
|
|
|
|
func parseException(line string) (*exception, error) {
|
|
from, rest, ok := strings.Cut(line, "->")
|
|
if !ok {
|
|
return nil, fmt.Errorf("want `from -> to, variant, reason`")
|
|
}
|
|
parts := strings.SplitN(rest, ",", 3)
|
|
if len(parts) < 3 || strings.TrimSpace(parts[2]) == "" {
|
|
return nil, fmt.Errorf("exception %q has no reason", strings.TrimSpace(line))
|
|
}
|
|
e := &exception{
|
|
from: strings.TrimSpace(from),
|
|
to: strings.TrimSpace(parts[0]),
|
|
variant: strings.TrimSpace(parts[1]),
|
|
reason: strings.TrimSpace(parts[2]),
|
|
}
|
|
if e.from == "" || e.to == "" || e.variant == "" {
|
|
return nil, fmt.Errorf("exception %q has an empty field", strings.TrimSpace(line))
|
|
}
|
|
for _, v := range strings.Split(e.variant, "|") {
|
|
if !knownVariants[v] {
|
|
return nil, fmt.Errorf("exception %q names the unknown variant %q", strings.TrimSpace(line), v)
|
|
}
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// excepted reports whether an exception allows the edge and marks it used.
|
|
func excepted(list []*exception, variant, from, to string) bool {
|
|
hit := false
|
|
for _, e := range list {
|
|
if !e.inVariant(variant) {
|
|
continue
|
|
}
|
|
if !matchFrom(e.from, from) || !matchTo(e.to, to) {
|
|
continue
|
|
}
|
|
e.used = true
|
|
hit = true
|
|
}
|
|
return hit
|
|
}
|
|
|
|
func matchFrom(pattern, from string) bool {
|
|
if strings.HasSuffix(pattern, ".go") {
|
|
return pattern == from
|
|
}
|
|
if strings.HasSuffix(from, ".go") {
|
|
from = path.Dir(from)
|
|
}
|
|
return match(pattern, from)
|
|
}
|
|
|
|
func matchTo(pattern, to string) bool {
|
|
if strings.HasPrefix(pattern, `"`) {
|
|
if !strings.HasPrefix(to, `"`) {
|
|
return false
|
|
}
|
|
ok, _ := path.Match(strings.Trim(pattern, `"`), strings.Trim(to, `"`))
|
|
return ok
|
|
}
|
|
return match(pattern, to)
|
|
}
|
|
|
|
func staleExceptions(list []*exception, variants map[string]bool) []string {
|
|
var out []string
|
|
for _, e := range list {
|
|
if e.used || !e.checked(variants) {
|
|
continue
|
|
}
|
|
out = append(out, fmt.Sprintf("exceptions.txt:%d: [exception-unused] %s -> %s (%s) matches nothing, remove it", e.line, e.from, e.to, e.variant))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (e *exception) inVariant(v string) bool {
|
|
for _, x := range strings.Split(e.variant, "|") {
|
|
if x == "*" || x == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checked reports whether any variant of the exception ran.
|
|
func (e *exception) checked(ran map[string]bool) bool {
|
|
for _, x := range strings.Split(e.variant, "|") {
|
|
if x == "*" || ran[x] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|