test: freeze direct instance-state consumers

Add an AST ratchet over production code that fails when a new file names
the daemon's instance-state bucket, record keys, local-file API, the raw
vm.InstanceRecord or its conversion, or a JetStreamManager instance-state
method. Today's 73 uses form a shrink-only allowlist keyed to the slice 0
inventory; stale entries fail, and a synthetic-source self-test proves the
matcher detects each kind of use and ignores declarations.
This commit is contained in:
joshsiv-mulga
2026-10-10 13:11:10 +10:00
parent 38fbd14993
commit 6fa09d336c
3 changed files with 599 additions and 0 deletions
@@ -0,0 +1,101 @@
package instancestate
// allowance is one recorded production consumer of raw instance state.
type allowance struct {
File, Symbol string
Inv string // inventory item in docs/PACKAGE_BOUNDARY_MIGRATION.md
}
// allowlist mirrors the ADR-0007 slice 0 inventory in
// docs/PACKAGE_BOUNDARY_MIGRATION.md. It only shrinks: delete an entry as soon
// as its use is gone, and never add one to make a new consumer pass.
var allowlist = []allowance{
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.LocalStatePath", Inv: "INV-01"},
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.ReadLocalState", Inv: "INV-01"},
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.MarshalLocalState", Inv: "INV-01"},
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.WriteLocalStateBytes", Inv: "INV-01"},
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.JetStreamManager", Inv: "INV-02"},
{File: "spinifex/daemon/daemon.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-02"},
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-02"},
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.InitTerminatedInstanceBucket", Inv: "INV-02"},
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteNodeMarkerBestEffort", Inv: "INV-03"},
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
{File: "spinifex/daemon/instance_record_repair.go", Symbol: "JetStreamManager.WriteRunningSet", Inv: "INV-03"},
{File: "spinifex/daemon/daemon.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-04"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "daemon.JetStreamManager", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadState", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-05"},
{File: "spinifex/daemon/vm_adapters.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-05"},
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/migrate.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/shutdown.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/orphan_qemu_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.UpdateTerminatedInstance", Inv: "INV-06"},
{File: "spinifex/runtime/compute/vm/teardown_reaper.go", Symbol: "JetStreamManager.DeleteTerminatedInstance", Inv: "INV-06"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ListStoppedInstances", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ListTerminatedInstances", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.WriteStoppedInstance", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.ClaimStoppedInstance", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.WriteTerminatedInstance", Inv: "INV-07"},
{File: "spinifex/domains/ec2/instance/service_impl.go", Symbol: "JetStreamManager.DeleteStoppedInstance", Inv: "INV-07"},
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.LoadStoppedInstance", Inv: "INV-08"},
{File: "spinifex/daemon/daemon_handlers_image.go", Symbol: "JetStreamManager.UpdateStoppedInstance", Inv: "INV-08"},
{File: "spinifex/daemon/daemon_mgmt_ip.go", Symbol: "daemon.JetStreamManager", Inv: "INV-09"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "vm.InstanceRecord", Inv: "INV-10"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-10"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ClaimRecoverableInstance", Inv: "INV-10"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.ReleaseRecoveredInstance", Inv: "INV-10"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.AbandonRecovery", Inv: "INV-10"},
{File: "spinifex/daemon/instance_recovery.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-10"},
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-11"},
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-11"},
{File: "spinifex/daemon/dns_reconcile.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-11"},
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "vm.InstanceRecord", Inv: "INV-12"},
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListInstanceRecords", Inv: "INV-12"},
{File: "spinifex/daemon/eni_orphan_reaper.go", Symbol: "JetStreamManager.ListTerminatedInstanceRecords", Inv: "INV-12"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.LocalState", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.LocalStatePath", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.ReadLocalState", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "JetStreamManager.InitKVBucket", Inv: "INV-13"},
{File: "spinifex/vpcd/imds_instance_state.go", Symbol: "vm.InstanceRecord", Inv: "INV-13"},
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "vm.InstanceRecord", Inv: "INV-14"},
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "vm.VMFromRecord", Inv: "INV-14"},
{File: "spinifex/domains/ec2/guestmetadata/instance_lookup.go", Symbol: "JetStreamManager.LoadInstanceRecord", Inv: "INV-14"},
{File: "spinifex/domains/network/reconcile/intent.go", Symbol: `literal "spinifex-instance-state"`, Inv: "INV-15"},
{File: "spinifex/domains/network/reconcile/intent.go", Symbol: "vm.InstanceRecord", Inv: "INV-15"},
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "daemon.InstanceStateBucket", Inv: "INV-16"},
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "daemon.InstanceRecordPrefix", Inv: "INV-16"},
{File: "spinifex/runtime/roles/awsgw/awsgw.go", Symbol: "vm.InstanceRecord", Inv: "INV-16"},
{File: "spinifex/runtime/compute/cache/cache.go", Symbol: "vm.InstanceRecord", Inv: "INV-17"},
{File: "spinifex/runtime/compute/cache/cache.go", Symbol: "vm.VMFromRecord", Inv: "INV-17"},
{File: "spinifex/domains/admission/quota/records.go", Symbol: "vm.InstanceRecord", Inv: "INV-18"},
{File: "spinifex/operator/cli/cluster.go", Symbol: "daemon.NewJetStreamManager", Inv: "INV-19"},
}
@@ -0,0 +1,196 @@
package instancestate
import (
"fmt"
"go/parser"
"go/token"
"os"
"path/filepath"
"testing"
)
type report struct {
unrecorded []use
stale []allowance
malformed []string
}
// check compares the scanned uses with allow.
func check(s scan, allow []allowance) report {
var rep report
allowed := map[use]bool{}
for _, a := range allow {
u := use{File: a.File, Symbol: a.Symbol}
switch {
case allowed[u]:
rep.malformed = append(rep.malformed, "duplicate entry "+u.String())
case a.Inv == "":
rep.malformed = append(rep.malformed, "entry "+u.String()+" cites no inventory item")
}
allowed[u] = true
}
for _, u := range s.sorted() {
if !allowed[u] {
rep.unrecorded = append(rep.unrecorded, u)
}
}
for _, a := range allow {
if _, ok := s[use{File: a.File, Symbol: a.Symbol}]; !ok {
rep.stale = append(rep.stale, a)
}
}
return rep
}
// TestADR0007_S6_NoNewRawInstanceStateConsumers is the blocking ratchet: no
// production file may start naming the daemon's instance-state bucket, keys,
// local file, raw record or JetStreamManager instance methods.
func TestADR0007_S6_NoNewRawInstanceStateConsumers(t *testing.T) {
root, err := moduleRoot()
if err != nil {
t.Fatal(err)
}
s, files, err := scanModule(root)
if err != nil {
t.Fatal(err)
}
// Guards against a walk that silently finds nothing and passes vacuously.
if files < 500 || len(s) < 20 {
t.Fatalf("scanned %d files and found %d uses under %s; the walk has lost the tree", files, len(s), root)
}
if len(allowlist) == 0 {
t.Fatal("the allowlist is empty, so the stale check would pass vacuously")
}
rep := check(s, allowlist)
t.Run("NoUnrecordedConsumer", func(t *testing.T) {
for _, u := range rep.unrecorded {
t.Errorf("ADR-0007 S6 (consumers receive narrow state capabilities rather than raw stores): %s at %s; "+
"consume an instance-domain capability instead of the daemon's raw instance state", u, s[u])
}
})
t.Run("Allowlist_NoStaleEntries", func(t *testing.T) {
for _, a := range rep.stale {
t.Errorf("allowlist entry %s uses %s (%s) no longer matches; delete it and update the inventory",
a.File, a.Symbol, a.Inv)
}
})
t.Run("Allowlist_WellFormed", func(t *testing.T) {
for _, m := range rep.malformed {
t.Errorf("allowlist_test.go: %s", m)
}
t.Logf("%d allow-listed uses", len(allowlist))
})
t.Run("OwnerFilesExist", func(t *testing.T) {
for _, f := range ownerFiles {
if _, err := os.Stat(filepath.Join(root, f)); err != nil {
t.Errorf("owner file %s is gone; remove it from ownerFiles and re-scan its consumers: %v", f, err)
}
}
})
}
const faultSource = `package %s
import (
"encoding/json"
spxd "github.com/mulgadc/spinifex/spinifex/daemon"
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
)
type holder struct {
LoadInstanceRecord func() // a field name, not a use
m *spxd.JetStreamManager
}
func (h holder) WriteRunningSet() {} // a declaration, not a use
func use(h holder, raw []byte, jsManager *spxd.JetStreamManager, other interface{ LoadState() }) {
_ = spxd.InstanceStateBucket
_ = spxd.LocalStatePath("")
var r vm.InstanceRecord
_ = json.Unmarshal(raw, &r)
_ = vm.VMFromRecord(&r)
_ = (&vm.VM{}).Record()
_, _ = h.m.LoadInstanceRecord("i-1")
_, _, _ = jsManager.LoadState("node")
other.LoadState()
_ = "spinifex-instance-state"
}
`
func parseFault(t *testing.T, rel, pkg string) scan {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, rel, fmt.Sprintf(faultSource, pkg), parser.SkipObjectResolution)
if err != nil {
t.Fatal(err)
}
out := scan{}
scanFile(fset, rel, f, out)
return out
}
// TestADR0007_S6_RatchetDetectsFaults injects a new consumer, a stale entry
// and a malformed entry, and checks the matcher's declaration exclusions.
func TestADR0007_S6_RatchetDetectsFaults(t *testing.T) {
const rel = "spinifex/domains/widget/w.go"
s := parseFault(t, rel, "widget")
want := []string{
"daemon.InstanceStateBucket", "daemon.LocalStatePath", "daemon.JetStreamManager",
"vm.InstanceRecord", "vm.VMFromRecord", "VM.Record()",
"JetStreamManager.LoadInstanceRecord", "JetStreamManager.LoadState",
`literal "spinifex-instance-state"`,
}
for _, sym := range want {
if _, ok := s[use{File: rel, Symbol: sym}]; !ok {
t.Errorf("injected use of %s was not detected; got %v", sym, s.sorted())
}
}
if len(s) != len(want) {
t.Errorf("want exactly %d uses (field names, method declarations and an unrelated LoadState excluded), got %v",
len(want), s.sorted())
}
stale := allowance{File: "spinifex/domains/gone/g.go", Symbol: "vm.InstanceRecord", Inv: "INV-00"}
recorded := allowance{File: rel, Symbol: "vm.InstanceRecord", Inv: "INV-00"}
rep := check(s, []allowance{recorded, stale, {File: rel, Symbol: "vm.VMFromRecord"}, recorded})
if len(rep.unrecorded) != len(want)-2 {
t.Errorf("want %d unrecorded uses, got %v", len(want)-2, rep.unrecorded)
}
if len(rep.stale) != 1 || rep.stale[0] != stale {
t.Errorf("want stale entry %+v, got %+v", stale, rep.stale)
}
if len(rep.malformed) != 2 {
t.Errorf("want a missing-inventory and a duplicate entry reported, got %v", rep.malformed)
}
}
// Inside the owning packages the symbols are bare identifiers, and the scan
// must see those too.
func TestADR0007_S6_RatchetSeesBareIdentifiersInOwnerPackages(t *testing.T) {
src := map[string]string{
"spinifex/daemon/new_consumer.go": "package daemon\n\nfunc f() string { return InstanceStateBucket + LocalStatePath(\"\") }\n",
"spinifex/runtime/compute/vm/new_reader.go": "package vm\n\nfunc g(r *InstanceRecord) *VM { return VMFromRecord(r) }\n",
}
want := map[string][]string{
"spinifex/daemon/new_consumer.go": {"daemon.InstanceStateBucket", "daemon.LocalStatePath"},
"spinifex/runtime/compute/vm/new_reader.go": {"vm.InstanceRecord", "vm.VMFromRecord"},
}
for rel, code := range src {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, rel, code, parser.SkipObjectResolution)
if err != nil {
t.Fatal(err)
}
out := scan{}
scanFile(fset, rel, f, out)
for _, sym := range want[rel] {
if _, ok := out[use{File: rel, Symbol: sym}]; !ok {
t.Errorf("%s: bare use of %s not detected; got %v", rel, sym, out.sorted())
}
}
}
}
+302
View File
@@ -0,0 +1,302 @@
// Package instancestate freezes the production consumers of the daemon's raw
// instance-state persistence, so ADR-0007 S6 and S7 start from a known set
// that can only shrink.
package instancestate
import (
"errors"
"fmt"
"go/ast"
"go/build/constraint"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
)
const (
daemonPath = "github.com/mulgadc/spinifex/spinifex/daemon"
vmPath = "github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
daemonDir = "spinifex/daemon"
vmDir = "spinifex/runtime/compute/vm"
)
// daemonSymbols are the daemon's instance-state bucket, key and local-file
// identifiers. A consumer naming one knows where instance state lives.
var daemonSymbols = set(
"InstanceStateBucket", "InstanceStateBucketVersion",
"TerminatedInstanceBucket", "TerminatedInstanceBucketVersion",
"InstanceRecordPrefix", "InstanceStatePrefix", "NodePresencePrefix",
"StoppedInstancePrefix", "TerminatedInstancePrefix",
"LocalState", "LocalStateSchemaVersion", "LocalStatePath", "ReadLocalState",
"MarshalLocalState", "WriteLocalStateBytes", "LocalStateFileName",
"LocalStateFileMode", "DefaultLocalStateDir",
"JetStreamManager", "NewJetStreamManager",
)
// vmSymbols are the raw persisted record and its conversion.
var vmSymbols = set("InstanceRecord", "InstanceSpec", "InstanceStatus", "VMFromRecord")
// jsmMethods are JetStreamManager's instance-state methods. Matched by name, so
// a call through an interface that mirrors them counts as a consumer too.
var jsmMethods = set(
"WriteInstanceRecord", "LoadInstanceRecord", "UpdateInstanceRecord",
"DeleteInstanceRecord", "ListInstanceRecords",
"WriteTerminatedInstanceRecord", "LoadTerminatedInstanceRecord",
"UpdateTerminatedInstanceRecord", "DeleteTerminatedInstanceRecord",
"ListTerminatedInstanceRecords",
"WriteNodeMarker", "WriteNodeMarkerBestEffort", "WriteRunningSet",
"WriteStoppedInstance", "LoadStoppedInstance", "DeleteStoppedInstance",
"ClaimStoppedInstance", "ClaimRecoverableInstance", "ReleaseRecoveredInstance",
"AbandonRecovery", "UpdateStoppedInstance", "ListStoppedInstances",
"WriteTerminatedInstance", "UpdateTerminatedInstance", "ListTerminatedInstances",
"DeleteTerminatedInstance", "LoadTerminatedInstance",
"InitKVBucket", "InitTerminatedInstanceBucket",
)
// ambiguousJSMMethods share a name with unrelated methods, so they count only
// when called on a field or variable named like a JetStreamManager handle.
var ambiguousJSMMethods = set("LoadState", "DeleteState")
// literals duplicate a bucket or file name instead of naming the owner's symbol.
var literals = set(`"spinifex-instance-state"`, `"spinifex-terminated-instances"`, `"instance-state.json"`)
// ownerFiles define the symbols above. They are the implementation being
// migrated, not consumers of it, and are listed so a deleted one is noticed.
var ownerFiles = []string{
"spinifex/daemon/instance_membership.go",
"spinifex/daemon/instance_records.go",
"spinifex/daemon/instance_records_migrate.go",
"spinifex/daemon/instance_running_set.go",
"spinifex/daemon/jetstream.go",
"spinifex/daemon/local_state.go",
"spinifex/runtime/compute/vm/record.go",
}
func set(names ...string) map[string]bool {
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
return m
}
// use is one consumer of one symbol, keyed by file so a new file is a new use.
type use struct {
File, Symbol string
}
func (u use) String() string { return u.File + " uses " + u.Symbol }
// scan maps each use to the first file:line it appears at.
type scan map[use]string
func moduleRoot() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", errors.New("cannot locate test source")
}
return filepath.Abs(filepath.Join(filepath.Dir(file), "..", ".."))
}
var excludedTopDirs = map[string]bool{"tests": true, "docs": true, "architecture": true}
// scanModule parses every non-test production Go file under root.
func scanModule(root string) (scan, int, error) {
out := scan{}
files := 0
fset := token.NewFileSet()
owners := set(ownerFiles...)
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if d.IsDir() {
if rel == "." {
return nil
}
name := d.Name()
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") ||
name == "testdata" || name == "vendor" || name == "node_modules" || excludedTopDirs[rel] {
return filepath.SkipDir
}
if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
src, err := os.ReadFile(path)
if err != nil {
return err
}
f, err := parser.ParseFile(fset, path, src, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
return err
}
if ignoredByBuildTag(f.Comments, f.Package) {
return nil
}
files++
if owners[rel] {
return nil
}
scanFile(fset, rel, f, out)
return nil
})
return out, files, err
}
// scanFile records every use in f, which lives at module-relative path rel.
func scanFile(fset *token.FileSet, rel string, f *ast.File, out scan) {
dir := filepath.ToSlash(filepath.Dir(rel))
daemonName, vmName := importName(f, daemonPath, "daemon"), importName(f, vmPath, "vm")
inDaemon, inVM := dir == daemonDir, dir == vmDir
seesVM := inDaemon || inVM || daemonName != "" || vmName != ""
record := func(symbol string, pos token.Pos) {
u := use{File: rel, Symbol: symbol}
if _, ok := out[u]; !ok {
out[u] = fmt.Sprintf("%s:%d", rel, fset.Position(pos).Line)
}
}
var visit func(n ast.Node) bool
visit = func(n ast.Node) bool {
switch n := n.(type) {
case *ast.ImportSpec:
return false
case *ast.BasicLit:
if n.Kind == token.STRING && literals[n.Value] {
record("literal "+n.Value, n.Pos())
}
case *ast.CallExpr:
if sel, ok := n.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Record" && len(n.Args) == 0 && seesVM {
record("VM.Record()", sel.Sel.Pos())
}
case *ast.SelectorExpr:
if x, ok := n.X.(*ast.Ident); ok {
switch {
case daemonName != "" && x.Name == daemonName && daemonSymbols[n.Sel.Name]:
record("daemon."+n.Sel.Name, n.Sel.Pos())
case vmName != "" && x.Name == vmName && vmSymbols[n.Sel.Name]:
record("vm."+n.Sel.Name, n.Sel.Pos())
}
}
if jsmMethods[n.Sel.Name] || (ambiguousJSMMethods[n.Sel.Name] && jsmReceivers[receiverName(n.X)]) {
record("JetStreamManager."+n.Sel.Name, n.Sel.Pos())
}
// The selected name is a field or method, never a bare package symbol.
ast.Inspect(n.X, visit)
return false
case *ast.FuncDecl:
// A method declared with one of these names is an implementation,
// not a use; its body is still scanned.
if n.Recv != nil {
ast.Inspect(n.Recv, visit)
}
if n.Body != nil {
ast.Inspect(n.Body, visit)
}
if n.Type != nil {
ast.Inspect(n.Type, visit)
}
return false
case *ast.Field:
// Interface method and struct field names are declarations.
if n.Type != nil {
ast.Inspect(n.Type, visit)
}
if n.Tag != nil {
ast.Inspect(n.Tag, visit)
}
return false
case *ast.Ident:
if inDaemon && daemonSymbols[n.Name] {
record("daemon."+n.Name, n.Pos())
}
if inVM && vmSymbols[n.Name] {
record("vm."+n.Name, n.Pos())
}
}
return true
}
ast.Inspect(f, visit)
}
var jsmReceivers = set("jsManager", "jsm", "js")
// receiverName is the last name in x: "js" for both js and a.js.
func receiverName(x ast.Expr) string {
switch x := x.(type) {
case *ast.Ident:
return x.Name
case *ast.SelectorExpr:
return x.Sel.Name
}
return ""
}
// importName returns the name f refers to path by, or "" if f does not import it.
func importName(f *ast.File, path, def string) string {
for _, imp := range f.Imports {
p, err := strconv.Unquote(imp.Path.Value)
if err != nil || p != path {
continue
}
if imp.Name != nil {
if imp.Name.Name == "_" || imp.Name.Name == "." {
return ""
}
return imp.Name.Name
}
return def
}
return ""
}
// ignoredByBuildTag reports a `//go:build ignore` file, which is a standalone
// program rather than part of its directory's package.
func ignoredByBuildTag(groups []*ast.CommentGroup, pkgPos token.Pos) bool {
for _, cg := range groups {
if cg.Pos() >= pkgPos {
break
}
for _, c := range cg.List {
if !constraint.IsGoBuild(c.Text) {
continue
}
expr, err := constraint.Parse(c.Text)
if err == nil && expr.String() == "ignore" {
return true
}
}
}
return false
}
func (s scan) sorted() []use {
out := make([]use, 0, len(s))
for u := range s {
out = append(out, u)
}
sort.Slice(out, func(i, j int) bool {
if out[i].File != out[j].File {
return out[i].File < out[j].File
}
return out[i].Symbol < out[j].Symbol
})
return out
}