typeseval

package
v0.0.0-...-043add5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: MIT Imports: 15 Imported by: 0

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:

  1. EvaluateConstString — collapse BasicLit / Ident / SelectorExpr / BinaryExpr to their compile-time string constant value via go/types' built-in constant folding.
  2. 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).
  3. 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildContextPredicate

func BuildContextPredicate(extraTags ...string) func(tag string) bool

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

func EvaluateConstString(typesInfo *types.Info, expr ast.Expr) (string, bool)

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

func IsGeneratedRelPath(rel string) bool

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):

  1. 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).
  2. 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

func ResolveEnclosingFunc(typesInfo *types.Info, file *ast.File, node ast.Node) (*types.Func, bool)

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

func ResolveMethodCall(typesInfo *types.Info, sel *ast.SelectorExpr) (*types.Func, bool)

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

func ResolvePackageRef(typesInfo *types.Info, expr ast.Expr) (pkgPath, name string, ok bool)

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

func (*Resolver) Packages

func (r *Resolver) Packages() []*packages.Package

Packages returns the loaded packages slice.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL