Documentation
¶
Overview ¶
Package typeseval provides go/types-backed helpers for archtest scanners.
Scope: archtest internal helper. Not exported beyond tools/archtest because kernel/governance enforces stdlib-only and runtime/cells/adapters have no reason to evaluate AST constants.
The helpers cover three patterns:
- EvaluateConstString — collapse BasicLit / Ident / SelectorExpr / BinaryExpr to their compile-time string constant value via go/types' built-in constant folding.
- LoadPackages / SharedResolver — load a module subtree with full type info once, then resolve any *ast.Expr to its constant via the owning packages.Package. Both accept a `tests` flag (true loads test variant packages, including *_test.go files) and a `tags` slice (joined into -tags=a,b,c BuildFlags).
- ResolveMethodCall / ResolvePackageRef / ResolveEnclosingFunc — given a SelectorExpr / Expr / Node, return the canonical *types.Func or (pkg, symbol) identity for callee-side and caller-side checks. ResolveEnclosingFunc is the caller-side helper: walk a file's top-level FuncDecls and return the one that lexically contains the node, mapped to its *types.Func (used as callsite identity for funnel allowlists).
ref: golang.org/x/tools/go/packages — NeedTypesInfo + constant folding ref: go/types TypesInfo.Types — maps ast.Expr to TypeAndValue (incl. const)
Index ¶
- func BuildContextPredicate(extraTags ...string) func(tag string) bool
- func EachFileInPackage(root string, pkg *packages.Package, skipTestFiles bool, ...)
- func EvaluateConstString(typesInfo *types.Info, expr ast.Expr) (string, bool)
- func FlatNonDefaultTags() []string
- func IsGeneratedRelPath(rel string) bool
- func KnownNonDefaultTags() [][]string
- func LoadPackages(modRoot string, tests bool, tags []string, patterns ...string) ([]*packages.Package, []packages.Error, error)
- func ParseBuildConstraint(filePath string) (constraint.Expr, error)
- func ResolveEnclosingFunc(typesInfo *types.Info, file *ast.File, node ast.Node) (*types.Func, bool)
- func ResolveMethodCall(typesInfo *types.Info, sel *ast.SelectorExpr) (*types.Func, bool)
- func ResolvePackageRef(typesInfo *types.Info, expr ast.Expr) (pkgPath, name string, ok bool)
- type ProductionResolver
- type Resolver
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildContextPredicate ¶
BuildContextPredicate returns a tag predicate suitable for constraint.Expr.Eval. It returns true for any tag the Go toolchain sets implicitly under a standard CI context plus any extraTags supplied by the caller.
Implicit defaults (union over all GOOS/GOARCH — fail-closed over-approximation: a constraint gated on "linux" is treated as satisfiable by some CI context):
- GOOS values (all known)
- GOARCH values (all known)
- "cgo" (CGO_ENABLED=1 is the toolchain default)
- "unix" (alias active for unix-family GOOS)
- "gc" (the standard compiler tag)
- go1.X release tags (sourced from build.Default.ReleaseTags so toolchain upgrades automatically refresh the set)
Repo-private skip markers (catalog_gen, never) are NOT implicit defaults and must not be returned true by this predicate. Knowledge that those tags exist lives in buildtags_test.go::repoSkipTagAllowlist for the coverage self-test only.
The implicit defaults map is INTENTIONALLY UNEXPORTED. Forcing every consumer through this constructor ensures that future additions to the default set (e.g. a new release tag after a go.mod floor bump, or a new implicit toolchain tag) automatically reach all archtest predicates without hand-edit drift. A caller that hand-rolled `expr.Eval(func(t string) bool { return myMap[t] })` would silently miss those additions; that error is unavailable here by API design.
ref: golang/go src/go/build/build.go Default.ReleaseTags ref: internal/syslist (canonical GOOS/GOARCH; mirrored here because the stdlib does not export the lists).
func EachFileInPackage ¶
func EachFileInPackage( root string, pkg *packages.Package, skipTestFiles bool, fn func(file *ast.File, relPath string, info *types.Info, fset *token.FileSet), )
INVARIANT: TYPESINFO-AST-SAME-SOURCE
EachFileInPackage is the single entry point for archtest checks that need go/types information. The callback receives the *ast.File, *types.Info, and *token.FileSet from the same packages.Load result, so info.Types[node] / info.Uses[node] / info.Selections[node] are guaranteed to resolve for every node found inside file.
scanner.EachFile parses each source file with a fresh token.FileSet, producing AST nodes whose pointer identity differs from the nodes that pkg.TypesInfo was built from. Combining scanner.EachFile with a captured pkg.TypesInfo silently fails open: every Types/Uses/Selections lookup misses, and any "type-aware" check degrades to a name-only AST match.
Decision rule for archtest authors:
- need go/types info (receiver type, const identity, interface implementation, expr type) → EachFileInPackage
- pure AST shape / import path / filename pattern → scanner.EachFile
Mixing the two paths in one check is a bug — there is no scenario where scanner-parsed nodes and a packages-loaded TypesInfo can be combined meaningfully.
ref: golang.org/x/tools/go/analysis (Pass{Fset,Files,TypesInfo} single source); dominikh/go-tools staticcheck (analysis.Pass-based, no re-parse API); go-critic CheckerContext (single TypesInfo per ctx).
func EvaluateConstString ¶
EvaluateConstString returns the compile-time string constant value of expr, or ("", false) when expr is not a constant string.
func FlatNonDefaultTags ¶
func FlatNonDefaultTags() []string
FlatNonDefaultTags returns the union of all distinct non-empty tags appearing in KnownNonDefaultTags(), sorted. Suitable for callers that need a single LoadPackages call carrying every tag at once (e.g. test_time_literal_test.go's universal AST walk). Excludes nil.
func IsGeneratedRelPath ¶
IsGeneratedRelPath reports whether rel points to codegen output under the repo's generated/ tree.
Definition: rel begins with "generated/" (top-level only). The repo reserves exactly one generated/ directory at module root; sub-tree "generated/" inside a hand-written package would be a layout violation and is intentionally not matched.
Current users: the loader anchor test TestOutboxHandleResultFactoryPreferred_GeneratedLoadAnchor_Wave3 (which counts generated/ files loaded by raw SharedResolver to prove the funnel premise). Archtest rules that previously called this helper inline have migrated to typeseval.LoadProductionPackages, whose ProductionResolver pre-filters generated/ packages at the package-set level so per-file IsGeneratedRelPath skipping is no longer needed in the hot path.
Background: `go list ./...` includes generated/contracts/.../v1 packages despite the legacy comment block above TestOutboxHandleResultFactoryPreferred claiming the opposite — the original PR445-FU finding F4. The Soft fix (require IsGeneratedRelPath presence per file) was upgraded to the Hard typed funnel in PR-SH2:
- typeseval.LoadProductionPackages / ProductionResolver provides Production() (generated/-filtered) and All() (full set) accessors; callers iterating pkg.Syntax cannot reach codegen output unless they opt in via .All() — a named call-site signal.
- PRODUCTION-LOADER-FUNNEL-01 (tools/archtest/production_loader_funnel_test.go) bans the raw `typeseval.SharedResolver(root, _, _, "./...")` form in tools/archtest *_test.go files (named allowlist for the anchor test only), so authors of new archtest rules cannot accidentally bypass the funnel.
Closes PR445-FU finding F4. Invariant ID GENERATED-SKIP-CROSS-RULE-INVARIANT-01.
func KnownNonDefaultTags ¶
func KnownNonDefaultTags() [][]string
KnownNonDefaultTags returns the build tag combinations that gate test or production files in this repo. archtest rules that must scan every tag-set call SharedResolver once per group and dedupe diagnostics by (rel, line, message).
Single source: this list is the authoritative set. Whenever a new build tag is introduced anywhere under the module, add the corresponding combination here AND let TestKnownNonDefaultTagsCoverage in buildtags_test.go catch the gap (fail-closed: any //go:build directive referencing a tag not represented here causes the self-test to fail).
Each entry is a `tags` slice as accepted by LoadPackages / SharedResolver — empty (nil) means the default build tag set; {"e2e", "pg"} means both tags must be active for the targeted files to be loaded.
Closes PR445-FU finding F2 + the file-local testTimeLiteralBuildTags constant in test_time_literal_test.go (cross-rule single source).
func LoadPackages ¶
func LoadPackages(modRoot string, tests bool, tags []string, patterns ...string) ([]*packages.Package, []packages.Error, error)
LoadPackages loads patterns from modRoot with full type info in single-module mode (GOWORK=off): archtest analyzes the root module plus the isolated fixture modules under tools/archtest/testdata/*, none of which are in the repo go.work `use` set.
Parameters:
- tests: when true, load the test variant of each package (includes *_test.go and adds a synthetic xtest package for `package x_test`).
- tags: joined as `-tags=a,b,c` in BuildFlags; pass nil/empty to omit.
Returns the flat slice of packages.Errors collected from every package as the second value so callers can fail fast on type-check errors without re-walking.
This is the UNCACHED form, delegating to the shared satellite-aware loader packagesload.LoadWorkspace, so a satellite parent prefix ("./cmd/...", "./adapters/...", "./examples/...") is expanded to its go.work members and loaded in workspace mode — the single-module name refers to the GOWORK mode requested, not a guarantee that only one module loads. The signature is held stable so the pass / production funnel meta-archtests keep matching it.
Prefer SharedResolver (the cached counterpart, #2165) in new code; this uncached form exists only for that signature-stability requirement — a fresh caller that wants memoization must not reach for LoadPackages.
func ParseBuildConstraint ¶
func ParseBuildConstraint(filePath string) (constraint.Expr, error)
ParseBuildConstraint extracts the file's build constraint expression using go/parser.ParseFile so the CommentGroup boundary, leading-comment position rule, and Go-toolchain directive semantics match cmd/go's own reader.
Returns (nil, nil) when the file has no //go:build / // +build directive in its header (the comment block that precedes the package clause and is separated from it by a blank line — the only zone Go recognizes for build constraints). Returns (nil, err) when:
- the file cannot be opened or parsed, or
- a recognized directive line fails constraint.Parse (fail-closed: a constraint that cannot be evaluated must not be silently treated as "no constraint"), or
- the file contains multiple //go:build directives (errMultipleGoBuild per go/build/build.go:1660 — cmd/go rejects such files).
Directive precedence matches cmd/go (go/build/build.go parseFileHeader):
- If a //go:build line is present, it is authoritative; any // +build lines are ignored entirely (legacy syntax retained for old-toolchain compat — cmd/go's shouldBuild only scans +build when goBuild == nil).
- If only // +build lines are present (no //go:build), they are AND-merged into a single expression per go/build/constraint package doc.
Legacy plus-build recognition requires a blank line between the directive's CommentGroup and the package clause (cmd/go's parseFileHeader). The per-CG blank-line gate is equivalent because AST splits CGs at blank lines.
This helper replaces three independent bufio.Scanner+constraint.Parse duplicates that existed in build_constraint_test.go, ci_integration_discovery_invariants_test.go, and the (now removed) extractBuildTags in buildtags_test.go.
ref: golang/go src/go/build/constraint/expr.go ref: golang/go src/go/build/build.go::parseFileHeader (lines 1627-1662) ref: golang/go src/go/build/build.go::shouldBuild +build fallback
func ResolveEnclosingFunc ¶
ResolveEnclosingFunc returns the OUTERMOST top-level *ast.FuncDecl that encloses node in file, mapped to its *types.Func identity via typesInfo.Defs. The identity form (callers usually call fn.FullName(), which is the go/types canonical form — parentheses around the receiver type for both pointer and value methods):
- Top-level function: "pkg/path.FuncName" (e.g. "fixture.DoThing")
- Pointer method: "(*pkg/path.Recv).MethodName" (e.g. "(*fixture.Service).Run")
- Value method: "(pkg/path.Recv).MethodName" (e.g. "(fixture.Counter).Inc")
- init() function: "pkg/path.init" (Go reflection form)
Returns (nil, false) when node is NOT inside any top-level FuncDecl:
- Package-level var / const init expression (e.g. `var X = helper()`)
- Package-level var = FuncLit() invocation (e.g. `var _ = func(){...}()`)
- Import block / file-level CommentGroup
- nil typesInfo, nil file, or nil node
Callers funneling callsite-level allowlists treat (nil, false) as an automatic violation: a setter call at package scope has no enclosing FuncDecl to allowlist and would otherwise slip through.
FuncLit semantics (deliberate): a nested *ast.FuncLit inside a FuncDecl is NOT a distinct callsite — its identity is the OUTERMOST containing FuncDecl. Rationale: the FuncLit and the FuncDecl share the same author; allowlisting the outer FuncDecl implicitly trusts any FuncLit inside it. Multi-level FuncLit nesting collapses to the same outer FuncDecl.
Implementation: iterate file.Decls for *ast.FuncDecl whose Pos ≤ node.Pos() < End, then resolve fd.Name to *types.Func via typesInfo.Defs. O(F) lookup where F = FuncDecl count per file. No binary search: F is small (tens at most) and the scan is dominated by typesInfo.Defs map lookup.
ref: golang/tools go/types: Info.Defs[*ast.FuncDecl.Name] is the canonical way to obtain *types.Func for a FuncDecl. fn.FullName() returns the same canonical form Go reflection uses for method names.
func ResolveMethodCall ¶
ResolveMethodCall returns the *types.Func that a method-call SelectorExpr `recv.Method()` or method-expression `T.Method(recv, ...)` resolves to, using info.Selections to recover the actual method object regardless of how the call site reaches it. Handles:
- Direct interface receiver: `var x fs.ReadDirFS; x.ReadDir(...)`
- Pointer / value method: `f := os.Open(...); f.ReadDir(-1)`
- Promoted via struct embed: `type W struct{ fs.ReadDirFS }; w.ReadDir(...)`
- Named type definition: `type MyFS fs.ReadDirFS; var x MyFS; x.ReadDir(...)`
- Type alias: `type MyFS = fs.ReadDirFS; x.ReadDir(...)`
- Generic type parameter: `func [F fs.ReadDirFS](x F) { x.ReadDir(...) }`
- Method expression (qualified): `fs.ReadDirFS.ReadDir(fsys, ".")`
- Method expression (pointer): `(*os.File).ReadDir(f, -1)`
Callers filter by the resolved method's owning package and name:
fn, ok := typeseval.ResolveMethodCall(info, sel)
if !ok { return }
if banned[fn.Pkg().Path()] && contains(banned[fn.Pkg().Path()], fn.Name()) {
// forbidden method call
}
Returns (nil, false) for:
- non-method selectors (qualified `pkg.Func` is in info.Uses, not Selections; use ResolvePackageRef for that shape)
- field-position selectors (info.Selections[sel].Kind() == FieldVal)
- methods whose owning *types.Package is nil (universe pseudo-types)
- nil typesInfo or nil sel
ref: golang/tools go/types/typeutil.Callee (same info.Selections lookup pattern) ref: dominikh/go-tools analysis/code.IsCallTo (Selections.Obj() typed filter)
func ResolvePackageRef ¶
ResolvePackageRef returns the (pkgPath, name) tuple for a reference to a package-level symbol, covering two AST shapes:
Qualified selector `pkg.Name` — requires info.Uses[sel.X] to resolve to *types.PkgName. Sel.Name is taken syntactically; the symbol itself does NOT need to resolve through types.Info. This tolerates partial type info, e.g. fixtures that type-check with importer.Default() against packages it cannot load (the import alias still resolves to *types.PkgName even when the imported package's symbols don't).
Bare identifier `Name` — requires info.Uses[id] to resolve to *types.Func or *types.TypeName with a non-nil owning *types.Package. Covers both dot-imported function references (e.g. `Sleep` after `import . "time"`) and dot-imported type references (e.g. `ImportBan{}` after `import . ".../scanner"`). *types.TypeName is the object kind for struct types, interfaces, type aliases, and named types — all forms that appear at the bare-Ident position when a type is referenced from a dot-import.
Returns ("", "", false) for:
- vars, consts, builtins, and packages at the bare-Ident position (*types.Var / *types.Const / *types.Builtin / *types.PkgName are not handled)
- method-position selectors (`receiver.Method` where sel.X is a value)
- identifiers whose owning *types.Package is nil (universe builtins)
- nil typesInfo or nil expr
- any expression kind other than *ast.Ident or *ast.SelectorExpr — wrappers like *ast.ParenExpr, *ast.IndexExpr (generic instantiation `Func[T]`), and *ast.IndexListExpr (`Func[T, U]`) are NOT unwrapped; callers iterating via scanner.EachInSubtree pick up the underlying Ident/SelectorExpr nodes directly, but a caller that passes a wrapper gets ("", "", false)
Callers are responsible for filtering by pkgPath / name. In particular, bare-Ident matches for a locally-defined func or type return the current package's path; matchers that only care about cross-package references must check pkgPath explicitly.
ref: golang/tools go/analysis/passes/copylock/copylock.go (qualified identifier resolution via info.Uses[id].(*types.PkgName)) ref: dominikh/go-tools analysis/code/code.go (TypesInfo lookup pattern with explicit nil guards)
Types ¶
type ProductionResolver ¶
type ProductionResolver struct {
// contains filtered or unexported fields
}
ProductionResolver partitions a real-repo "./..." load into production and full sets. The fields are unexported so callers cannot reach for a raw []*packages.Package outside of the two named accessors.
func LoadProductionPackages ¶
func LoadProductionPackages(workspaceRoot string, modules []workspace.Module, tests bool, tags []string) (*ProductionResolver, error)
LoadProductionPackages loads the production package set of an entire workspace and partitions packages by whether their PkgPath begins with any member module's "<importPath>/generated/" prefix. It returns a *ProductionResolver whose Production() accessor exposes only the non-generated subset, eliminating the per-callsite IsGeneratedRelPath skip discipline.
workspaceRoot is the go.work directory; modules is the set of workspace members (from tools/workspace.Modules — the authoritative go.work `use` set), each carrying its on-disk Dir and Go ImportPath. The load uses ModeWorkspace with one RELATIVE-DIR "./<dir>/..." pattern per member, so EVERY workspace module — including a nested module extracted into go.work — is scanned. Relative-dir patterns (not "<importPath>/..." module-path patterns) are required: a module-path pattern makes `go` resolve that path as an external dependency at its required version (network fetch), whereas a directory pattern resolves the local workspace member. This is the keystone that keeps archtest's production coverage from silently dropping an extracted module: the scan set is derived from go.work, not a hand-maintained list. For a single-module workspace (today's `use .`, Dir ".") the pattern reduces to "./..." — byte-identical to the former single-module load.
This is the Hard-grade replacement for archtest tests that previously called SharedResolver(modRoot, _, _, "./..."). The PRODUCTION-LOADER-FUNNEL-01 archtest bans the raw form in tools/archtest/*_test.go (named allowlist for the loader anchor test only); together with this typed accessor, a caller iterating pkg.Syntax cannot reach codegen output unless they explicitly opt in via All() — which names the trade-off at the call site.
AI-robust grade: Hard for the iteration path (violation not expressible without renaming `Production` → `All` at every call site), Medium for the load API (archtest gating with named allowlist). The combination closes the file-level grep loophole described in `docs/plans/202605112000-036-archtest-governance-rollout-plan.md` §3.3.
ref: charter §1 "violation not expressible" / type system funnel ref: golang.org/x/tools/go/analysis Pass.Files driver-controlled scope
func (*ProductionResolver) All ¶
func (r *ProductionResolver) All() []*packages.Package
All returns the full loaded package set including generated/. Use only when generated/ packages are required for type resolution (e.g., depgraph import-edge construction, cross-package type scope walks). pkg.Syntax iteration over All() WILL reach codegen output — the name forces callers to acknowledge that semantics at the call site.
func (*ProductionResolver) Production ¶
func (r *ProductionResolver) Production() []*packages.Package
Production returns packages whose PkgPath is NOT under <module>/generated/. pkg.Syntax iteration over Production() cannot reach codegen output, so rules that reason over hand-written source can omit the per-file IsGeneratedRelPath skip entirely.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver wraps a loaded set of packages for repeated constant evaluation.
func SharedResolver ¶
func SharedResolver(modRoot string, tests bool, tags []string, patterns ...string) (*Resolver, error)
SharedResolver returns a Resolver backed by the process-wide packagesload cache (#2165), keyed on (modRoot, tests, tags, patterns). Successive callers with the same key reuse the satellite-aware cached load instead of re-running packages.Load. Errors are not cached, so a transient failure does not poison subsequent calls. The returned *Resolver is a thin wrapper minted per call; two calls share the same underlying packages on a cache hit (the property that matters: no re-load), not the same wrapper.
The cache lives in tools/packagesload (the single sanctioned package-load cache); typeseval holds no cache state of its own. SharedResolver wraps the raw cached load packagesload.LoadWorkspaceCached, mapping any packages.Error into a fail-fast scan error via [resolverFrom]. ref: ADR docs/architecture/202605190000-adr-archtest-in-process-warmup.md