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.
The cross-module workspace scan (LoadProductionPackages) uses the ModeWorkspace variant ([loadPackagesMode]); this single-module form is the one the Typed / Fixture / StandaloneModule scopes route through, and its signature is held stable so the pass / production funnel meta-archtests keep matching it.
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 process-wide cached Resolver keyed on (modRoot, tests, tags, patterns). Successive callers with the same key reuse the loaded packages. Errors are not cached — a transient failure does not poison subsequent calls.
Cache keys are formed by joining modRoot, the tests flag, the tag list, and each pattern with NUL bytes. NUL is illegal in POSIX paths and Go import patterns, so collisions are impossible even when patterns themselves contain "|" or ",".
Concurrency: the cache is read and written under sharedMu, but the expensive LoadPackages call runs without the lock. singleflight deduplicates concurrent loads of the same key so only one packages.Load is in flight per key, while loads for different keys run in parallel.
为什么 cacheKey 保留 patterns 维度而不剥离:archtest 调用方实测分布显示 主模块 subpath patterns(./cells/.../, ./cmd/.../, ./runtime/.../ 等) 占 83.5%,"./..." 仅占 16.5%。每个 subpath patterns 加载不同 package 集 合,必须区分 cacheKey。"./..." 形态的 cacheKey 合并已由 LoadProductionPackages typed wrapper 完成(固定 patterns="./...")。 ref: ADR docs/architecture/202605190000-adr-archtest-in-process-warmup.md