diff --git a/architecture/instancestate/allowlist_test.go b/architecture/instancestate/allowlist_test.go new file mode 100644 index 000000000..b8984d58f --- /dev/null +++ b/architecture/instancestate/allowlist_test.go @@ -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"}, +} diff --git a/architecture/instancestate/instancestate_test.go b/architecture/instancestate/instancestate_test.go new file mode 100644 index 000000000..5676cbdf7 --- /dev/null +++ b/architecture/instancestate/instancestate_test.go @@ -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()) + } + } + } +} diff --git a/architecture/instancestate/scan_test.go b/architecture/instancestate/scan_test.go new file mode 100644 index 000000000..5b93b2358 --- /dev/null +++ b/architecture/instancestate/scan_test.go @@ -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 +}