Documentation
¶
Overview ¶
Package scanner provides shared walk + parse + report primitives for tools/archtest scanners. All operations are fail-closed by construction:
- Scope zero value is rejected; must be created via ModuleScope or DirsScope.
- EachFile treats any parse error as a fatal test failure (no silent fallback).
- Scope.Files returns an error on any walk failure.
- Report deduplicates and sorts diagnostics before emitting t.Errorf calls.
Choosing between ImportBan, EachFile, EachInSubtree, and EachInChildren ¶
Use ImportBan when the entire invariant is "file must not import package X".
Use EachFile with Report for custom AST patterns: combine with EachInSubtree / EachInChildren for typed node iteration, or FindFirstInSubtree / FindFirstChild for typed find-first with implicit early-stop, so the per-node-kind handler is statically constrained to the right *ast.<NodeKind> type.
Use EachInSubtree / EachInChildren / FindFirstInSubtree / FindFirstChild inside any rule that iterates AST nodes. Bare go/ast.Inspect, go/ast.Walk, go/ast.Preorder, and golang.org/x/tools/go/ast/inspector APIs are forbidden in tools/archtest/*_test.go (enforced by SCANNER-FRAMEWORK-USAGE-01); the generic typed funnels make "wrong node kind" a compile error rather than a silent runtime miss — critical for AI-robust archtest authoring (see .claude/rules/gocell/ai-robust.md AI-robust 三档分级).
Choosing walk depth (iteration and find-first) ¶
Walk depth is a compile-time choice — picking the wrong API shows at the call site, not in runtime AST drift. Six APIs forming a 2×3 matrix {EachIn*, FindFirst*} × {Children, Subtree, SubtreeStopAt}:
- EachInSubtree / FindFirstInSubtree: recursive over the full sub-tree (root + every descendant). For "any FuncDecl in the file" / "any IfStmt anywhere in fn.Body" style rules — and the find-first sibling when the rule needs existence/first-match with implicit early-stop (closure-sentinel idiom is forbidden by SCANNER-FRAMEWORK-USAGE-02, allowlist 0, covering both EachInSubtree and the depth-1 EachInChildren axis).
- EachInChildren / FindFirstChild: depth-1 only. For "container's direct elements" — KeyValueExpr of CompositeLit, CaseClause of SwitchStmt.Body, CommClause of SelectStmt.Body, top-level Decl of *ast.File.
- EachInSubtreeStopAt / FindFirstInSubtreeStopAt: recursive with a boundary predicate that prunes descent into matching non-root nodes (typical use: skip nested *ast.FuncLit so dead-closure call positions are excluded). Third depth-semantic member of the typed-function-choice Hard 范本 (see ai-robust.md §"Hard 范本目录" #1) alongside EachInSubtree / EachInChildren. The FindFirst variant completes the matrix, giving rule authors boundary-aware find-first without falling back to the banned EachInSubtreeStopAt+sentinel idiom (also covered by SCANNER-FRAMEWORK-USAGE-02).
All silently no-op on nil root; callers need not guard. FindFirst* returns (zero, false) on nil root.
Subpackage scan exemption ¶
SCANNER-FRAMEWORK-USAGE-01 only scans top-level archtest test files (tools/archtest/<file>_test.go). Files under tools/archtest/internal/... (this package, typeseval, etc.) are exempt by design — they ARE the framework and may legitimately use bare go/ast walks. AI authors must NOT extend "subpackage exempt" reasoning to other tools/archtest/internal/ helpers: only the framework implementation files (this package) and the stdlib type-checker plumbing (typeseval) qualify.
Why not go/analysis ¶
GoCell archtests have no inter-rule dependencies (no Requires/FactType DAG), so the lighter AST scanner API is preferred over go/analysis.Analyzer.
Typical usage ¶
root, err := scanner.FindModuleRoot(...) // or use findModuleRoot testing helper
scanner.ImportBan{
RuleID: "MY-RULE-01",
Forbidden: []string{"forbidden/path"},
}.Run(t, scanner.ModuleScope(root))
scanner.EachFile(t, scope, parser.SkipObjectResolution, func(t *testing.T, fc scanner.FileContext) {
scanner.EachInSubtree[ast.CallExpr](fc.File, func(call *ast.CallExpr) {
// call is *ast.CallExpr — typed by Go's generic constraint
})
})
ref: go/ast.Preorder@go1.23 — stdlib typed iteration for EachInSubtree ref: go/ast.Walk — stdlib Visitor pattern for EachInChildren ref: golang.org/x/tools/go/analysis analysis.go — Analyzer.RunDespiteErrors=false default (fail-closed) ref: kubernetes/kubernetes test/typecheck/main.go ignoredPaths — driver-level skip set ref: golangci-lint pkg/golinters/depguard — high-level import-ban encapsulation ref: golang.org/x/tools/go/packages packages.go — NeedSyntax + Errors collection
Index ¶
- func EachContentFile(t *testing.T, s Scope, suffixes []string, fn func(*testing.T, ContentContext))
- func EachFile(t *testing.T, s Scope, mode parser.Mode, fn func(*testing.T, FileContext))
- func EachInChildren[S any, N interface{ ... }](root ast.Node, fn func(N))
- func EachInSubtree[S any, N interface{ ... }](root ast.Node, fn func(N))
- func EachInSubtreeStopAt[S any, N interface{ ... }](root ast.Node, stopAt func(ast.Node) bool, fn func(N))
- func FindFirstChild[S any, N interface{ ... }](root ast.Node, predicate func(N) bool) (N, bool)
- func FindFirstInSubtree[S any, N interface{ ... }](root ast.Node, predicate func(N) bool) (N, bool)
- func FindFirstInSubtreeStopAt[S any, N interface{ ... }](root ast.Node, stopAt func(ast.Node) bool, predicate func(N) bool) (N, bool)
- func ReceiverTypeName(expr ast.Expr) string
- func Report(t *testing.T, ruleID string, diags []Diagnostic)
- func StringLitValue(lit *ast.BasicLit) (string, bool)
- func StructTagJSONKey(tag *ast.BasicLit, jsonKey string) bool
- type ContentContext
- type Diagnostic
- type DirsScopeEscapeError
- type FileContext
- type ImportBan
- type Option
- type Scope
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EachContentFile ¶
EachContentFile iterates over every file in scope whose path ends in any of suffixes (case-sensitive, must include the dot — e.g. ".yaml"). Validation failures, walk errors, and per-file read errors fail-loud via t.Fatalf. fn is invoked for each successfully read file with the file's bytes; calling t.Errorf inside fn does not stop iteration (collect-all-violations semantics, mirroring EachFile).
Suffixes must be non-empty and each must start with ".". This is the only way archtest tests should iterate non-Go files; raw os.ReadDir / fs.WalkDir in tools/archtest/*_test.go is forbidden by SCANNER-FRAMEWORK-USAGE-01.
Implementation: thin wrapper over LoadContentFiles (the pure testing-free counterpart). Failure-mode coverage lives in TestLoadContentFiles_Errors.
func EachFile ¶
EachFile iterates over every file in scope, parsing each with the given mode. Any parse error causes t.Fatalf immediately, stopping the entire test (fail-loud by construction; no silent fallback). fn is invoked for each successfully parsed file; calling t.Errorf inside fn does not stop iteration (collect-all-violations semantics — the loop continues to accumulate further findings before the test ultimately fails).
func EachInChildren ¶
EachInChildren iterates only the DIRECT children of root (depth = 1), calling fn for any child whose type matches N. The root itself is never passed to fn; grandchildren are never visited.
Implementation uses stdlib ast.Walk with a depth-1 visitor: the first Visit receives root and returns the visitor (recurse into root's children); each subsequent Visit processes any typed child and returns nil (halt at depth 1).
Use for "container's direct elements" semantics: KeyValueExpr from a CompositeLit, CaseClause from SwitchStmt.Body / TypeSwitchStmt.Body, CommClause from SelectStmt.Body, top-level FuncDecl/GenDecl from a *ast.File (each concrete decl kind passed as N, since ast.Decl is an interface and would fail the `*S` constraint), top-level Stmt from a *ast.BlockStmt (e.g. fn.Body's direct statements).
See EachInSubtree for the recursive variant and for the depth-choice decision criteria.
Nil root ¶
Returns silently (no-op).
ref: go/ast.Walk — Go stdlib Visitor pattern (depth control via returning nil from Visit).
func EachInSubtree ¶
EachInSubtree iterates every node of kind N in the sub-tree rooted at root (preorder, recursive), calling fn with the already-typed node. Backed by Go 1.23 stdlib ast.Preorder — single backend, no cache, no external dependency.
N is constrained via `interface { *S; ast.Node }` so it must be a concrete pointer type to a node struct. Callers write
scanner.EachInSubtree[ast.CallExpr](fc.File, func(call *ast.CallExpr) { ... })
where Go's type inference fills in N=*ast.CallExpr from S=ast.CallExpr. Wrong N is a compile error, not a runtime panic. For example, `scanner.EachInSubtree[ast.Expr](file, ...)` fails to compile because ast.Expr is an interface and does not satisfy `*S` (must be a concrete pointer).
Choosing between EachInSubtree and [EachInChildren] ¶
Walk depth is a compile-time choice: different API names express different traversal semantics, so picking the wrong depth is statically visible at the call site rather than hidden in runtime AST behavior.
- EachInSubtree: recursive over the full sub-tree (root + every descendant). Use when the rule reasons over all positions in a function body, file, or expression — e.g. "any FuncDecl in the file", "any IfStmt nested anywhere in fn.Body".
- EachInChildren: depth-1 only (root's direct children). Use when the rule reasons over a container's immediate elements — KeyValueExpr elements of a CompositeLit, CaseClause elements of a SwitchStmt.Body, CommClause elements of a SelectStmt.Body, top-level Decl of a File.
The dual-semantic fixtures in eachnode_test.go (T1+T2 share a nested CompositeLit fixture; subtree finds nested results, children does not) anchor the difference with RED tests, so the depth contract is not just godoc convention.
Pruning ¶
By node-kind selection only; there is no "skip subtree" callback. To constrain scope, pass a narrower sub-root or call EachInSubtree again on a chosen descendant.
Nil root ¶
Returns silently (no-op).
ref: go/ast.Preorder — Go 1.23 stdlib typed iteration No *testing.T parameter: EachInSubtree is pure (no I/O, no parse errors). Callers call t.Errorf inside fn. The pure form also allows use from non-test paths (e.g. SCANNER-FRAMEWORK-USAGE-01 self-test walkers).
func EachInSubtreeStopAt ¶
func EachInSubtreeStopAt[S any, N interface { *S ast.Node }](root ast.Node, stopAt func(ast.Node) bool, fn func(N))
EachInSubtreeStopAt traverses root's subtree like EachInSubtree, invoking fn for each *N encountered, but stops descending into any non-root node for which stopAt returns true. The boundary node itself is NOT visited as N even if its type matches (it is excluded along with its subtree).
Use this for archtest rules whose presence-check must IGNORE syntactic scopes that do not execute at the rule's site of interest — e.g. a funnel-call-presence check on a test body that must not credit funnel calls inside a nested *ast.FuncLit dead closure (the closure body runs only if the closure is invoked, which a static AST scan cannot prove).
Picking EachInSubtreeStopAt over EachInSubtree is the third member of the typed-function-choice-for-walk-depth Hard template (ai-robust.md §"Hard 范本" #1, alongside EachInChildren depth=1 and EachInSubtree full-recursive): boundary-aware recursion is its own depth semantic, so picking the wrong walker = picking the wrong API name and fails archtest at the call site.
Root handling ¶
root is ALWAYS entered regardless of stopAt — stopAt only applies to non-root nodes, so a stopAt predicate may simply check the node's type (`_, isFL := n.(*ast.FuncLit); return isFL`) without manually excluding root. If root itself is of type N it is passed to fn before descent begins.
Nil root ¶
Returns silently (no-op).
Nil stopAt ¶
A nil stopAt is treated as the zero predicate (always false), making EachInSubtreeStopAt[N](root, nil, fn) equivalent to EachInSubtree[N](root, fn). Callers should prefer the simpler EachInSubtree in that case for clarity.
ref: go/ast.Walk — Go stdlib Visitor pattern (descent control via returning nil from Visit).
func FindFirstChild ¶
func FindFirstChild[S any, N interface { *S ast.Node }](root ast.Node, predicate func(N) bool) (N, bool)
FindFirstChild walks root's direct children (depth = 1, identical semantics to EachInChildren) and returns the first child whose concrete type is N and for which predicate returns true, together with ok = true. If no direct child matches, it returns the zero value and ok = false.
FindFirstChild is the typed funnel that replaces the hand-written closure+done/found sentinel idiom:
// before — Soft: hand-rolled sentinel, scoping/guard easy to get wrong
found := false
scanner.EachInChildren[ast.KeyValueExpr](lit, func(kv *ast.KeyValueExpr) {
if found { return }
if match(kv) { found = true }
})
// after — early-return implicit, no caller-held flag
_, found := scanner.FindFirstChild[ast.KeyValueExpr](lit,
func(kv *ast.KeyValueExpr) bool { return match(kv) })
The early-return is implicit (predicate is not invoked again after the first match), there is no caller-exposed done flag, and picking the wrong N (an interface such as ast.Expr instead of a concrete *S) is a compile error via the same `interface { *S; ast.Node }` constraint as EachInChildren. Enforced against regression by archtest SCANNER-FRAMEWORK-USAGE-02 (allowlist = 0).
Depth ¶
depth = 1 only. Grandchildren are never inspected and root itself is never passed to predicate (see EachInChildren).
Subtree variant ¶
See FindFirstInSubtree for the recursive (full-subtree) twin. Picking the wrong depth is a different API name, not a runtime parameter.
Nil root ¶
Returns (zero, false) silently (no-op), matching EachInChildren.
Implementation reuses EachInChildren wholesale; the single sentinel the codebase-wide ban removes from business code is internalized here exactly once, under central governance.
func FindFirstInSubtree ¶
func FindFirstInSubtree[S any, N interface { *S ast.Node }](root ast.Node, predicate func(N) bool) (N, bool)
FindFirstInSubtree walks root's entire subtree (root + every descendant, preorder, identical depth semantics to EachInSubtree) and returns the first node whose concrete type is N and for which predicate returns true, together with ok = true. If no node matches, it returns the zero value and ok = false.
FindFirstInSubtree is the subtree-depth twin of FindFirstChild: same predicate-driven find-first contract, different depth — picking depth is a typed function choice (different API name, different semantics, AI-robust Hard 范本 #1 "typed function choice for walk depth", alongside EachInSubtree vs EachInChildren and EachInSubtreeStopAt).
The closure-sentinel idiom over EachInSubtree (the recursive walker) — `found := false; EachInSubtree(...){ if found { return }; ... found = true }` — is banned by archtest SCANNER-FRAMEWORK-USAGE-02 (allowlist = 0), alongside its depth-1 sibling over EachInChildren. Use FindFirstInSubtree instead: the early-return is implicit, no caller-held flag, and picking the wrong N is a compile error via the same `interface{*S; ast.Node}` constraint as EachInSubtree.
Implementation ¶
Uses ast.Inspect whose visitor's bool return halts descent into the current subtree — the natural early-stop primitive for find-first. After the first match, the visitor sets a closure flag and returns false to prune descent into the matched node; the flag also short-circuits every subsequent sibling visit, so the predicate is invoked exactly once after a match (anchored by TestFindFirstInSubtree_StopsAfterFirstMatch and TestFindFirstInSubtree_StopsBeforeDescendingMatchedSubtree).
ast.Inspect signals "children done" by invoking the visitor with a nil node after each subtree (mirrors ast.Walk's Visit(nil) convention; see go/ast.Inspect's documented "followed by a call of f(nil)" contract). The implementation short-circuits on nil before the typed assertion — same explicit nil handling as [childrenVisitor] and [subtreeStopAtVisitor] elsewhere in this file.
Root handling ¶
Root IS visited (mirrors EachInSubtree, which includes root via ast.Preorder). Contrast FindFirstChild which EXCLUDES root by design (depth=1; root is the iteration start, not a candidate). The divergence is intentional — each find-first API mirrors the root semantics of its matching iteration walker — and is anchored by TestFindFirstInSubtree_RootSelfMatched / TestFindFirstChild_RootSelfNotMatched.
Nil root ¶
Returns (zero, false) silently (no-op).
Nil predicate ¶
Panics on the first visited node — the predicate is invoked unconditionally per candidate. Matches the contract of FindFirstChild; callers that need "always-true find-first" should pass `func(N) bool { return true }` explicitly. The implementation is fail-fast rather than nil-tolerant because there is no sensible semantic interpretation of a nil predicate in a find-first contract (unlike EachInSubtreeStopAt where nil stopAt has a natural "no boundary" meaning).
ref: go/ast.Inspect — Go stdlib early-stop preorder traversal.
func FindFirstInSubtreeStopAt ¶
func FindFirstInSubtreeStopAt[S any, N interface { *S ast.Node }](root ast.Node, stopAt func(ast.Node) bool, predicate func(N) bool) (N, bool)
FindFirstInSubtreeStopAt walks root's subtree like FindFirstInSubtree, returning the first node of kind N satisfying predicate, but stops descending into any non-root node for which stopAt returns true. The boundary node itself is NOT considered as a candidate even if its type matches N (it is excluded along with its subtree, same semantics as EachInSubtreeStopAt).
FindFirstInSubtreeStopAt completes the typed function choice matrix — {EachIn*, FindFirst*} × {Children, Subtree, SubtreeStopAt} — closing the gap where boundary-aware find-first was previously unavailable. Picking the wrong combination is a typed function name selection per ai-robust.md AI-robust Hard 范本 #1 "typed function choice for walk depth"; archtest authors needing "find-first respecting closure / scope boundary" should reach for this API rather than falling back to manual sentinel idioms over EachInSubtreeStopAt (which are banned by SCANNER-FRAMEWORK-USAGE-02).
Root handling ¶
root is ALWAYS evaluated regardless of stopAt — stopAt only applies to non-root nodes. Same root-included semantics as FindFirstInSubtree and EachInSubtreeStopAt.
Nil root ¶
Returns (zero, false) silently (no-op).
Nil stopAt ¶
A nil stopAt is treated as the zero predicate (always false), making FindFirstInSubtreeStopAt[N](root, nil, predicate) equivalent to FindFirstInSubtree[N](root, predicate). Callers should prefer the simpler FindFirstInSubtree in that case for clarity.
Nil predicate ¶
Panics on the first visited candidate — matches FindFirstInSubtree fail-fast contract.
ref: go/ast.Inspect — Go stdlib early-stop preorder traversal.
func ReceiverTypeName ¶
ReceiverTypeName extracts the base type name from a receiver type expression. It handles the following forms:
- *T (*ast.StarExpr wrapping *ast.Ident)
- T (*ast.Ident)
- T[P] (*ast.IndexExpr — single type parameter)
- T[P,Q] (*ast.IndexListExpr — multiple type parameters)
Any other form (e.g. anonymous struct receivers) returns the empty string.
func Report ¶
func Report(t *testing.T, ruleID string, diags []Diagnostic)
Report formats and emits each diagnostic as a t.Errorf call, sorted and deduplicated. An empty diags slice is a no-op.
func StringLitValue ¶
StringLitValue returns the unquoted value of a string literal AST node. Both interpreted strings (`"foo"`) and raw strings (“ `foo` “) are normalized through strconv.Unquote, so escape sequences such as `\x61`, `a`, and `\n` are decoded. The returned ok is false in any of the following cases:
- lit is nil
- lit.Kind is not token.STRING (e.g. rune literals like `'a'`, numeric literals, etc.)
- lit.Value is malformed and strconv.Unquote returns an error
Callers should treat ok=false as "not a usable string literal" and either surface a diagnostic or skip the node — never fall back to the raw lit.Value because that would defeat the normalization contract.
Note: this helper does not handle BinaryExpr-based concatenation (`"ad" + "min"`). Such forms intentionally fall through to ok=false; rules that need to detect them must walk BinaryExpr explicitly.
ref: golang/go src/strconv/quote.go — Unquote handles both `"..."` and “ `...` “ forms; rune literals require a different code path which we reject here so STRING-only contracts stay strict.
func StructTagJSONKey ¶
StructTagJSONKey extracts the JSON key from an *ast.Field's tag literal and reports whether it equals the given key.
In the Go AST a struct tag is stored as *ast.BasicLit with Kind=STRING and Value a raw-string literal `json:"authz_epoch"`. A bare literal scan for "authz_epoch" would NOT match because the full tag value is `json:"authz_epoch"` — the outer backticks are not stripped by a simple StartsWith/Contains check, and the inner quotes are escaped. This helper normalises the tag by:
- Stripping the outer backtick delimiters (raw string literal) — since struct tags in Go are always raw string literals enclosed in backticks.
- Parsing the result as reflect.StructTag (Go's canonical tag format).
- Extracting the "json" key and stripping any comma-separated options (e.g. `json:"authz_epoch,omitempty"` → `authz_epoch`).
Returns false if tag is nil, Kind != STRING, or the tag value does not use backtick delimiters (malformed tag AST).
Blind-spot: the helper does not handle dynamically-constructed struct tags (reflect.StructTag set at runtime), which would be invisible to any AST scanner. Dynamic tags are an unusual pattern; archtest consumer tests assert their absence.
ref: reflect.StructTag.Get — Go stdlib struct tag parsing. ref: golang/go src/go/ast/ast.go Field.Tag — *ast.BasicLit with Kind=STRING.
Types ¶
type ContentContext ¶
type ContentContext struct {
// AbsPath is the absolute path to the file.
AbsPath string
// Rel is the module-relative slash path (e.g. "cells/auth/cell.yaml").
Rel string
// Bytes is the file's raw content. Caller decodes with the lib of choice.
Bytes []byte
}
ContentContext is the non-Go counterpart of FileContext: raw bytes only, no AST and no token.FileSet. Use it for YAML / JSON / Markdown / SQL or any other format the scanner framework should funnel.
func LoadContentFiles ¶
func LoadContentFiles(s Scope, suffixes []string) ([]ContentContext, error)
LoadContentFiles is the pure (testing-free) side of EachContentFile: validates suffixes, walks the scope, reads bytes, returns ContentContexts. Returns an error on any of: empty suffixes, suffix without leading dot, invalid scope (zero value, setup error, walk error), or per-file read error.
Exposed for fixture testing — direct callers should prefer EachContentFile which fail-loud t.Fatalf's on error and invokes fn per file.
type Diagnostic ¶
type Diagnostic struct {
// Rel is the file path relative to the module root.
Rel string
// Line is the 1-based line number of the violation.
Line int
// Message describes the violation.
Message string
}
Diagnostic represents a single rule violation found during a scan.
func Canonical ¶
func Canonical(diags []Diagnostic) []Diagnostic
Canonical deduplicates diags and returns them sorted by (Rel, Line, Message). It is the single source of diagnostic ordering shared by Report and the archtest golden harness (archtest.AssertGolden) — so a golden file and a Report failure always present the same diagnostic set in the same order. An empty or nil input returns nil.
type DirsScopeEscapeError ¶
type DirsScopeEscapeError struct {
// Dirs are the offending input directories, preserved in their original
// (caller-supplied, pre-clean) form so error messages stay meaningful.
Dirs []string
}
DirsScopeEscapeError is the structured error returned via Scope.Files when one or more directories supplied to DirsScope would resolve outside the module root. Callers verify this condition with errors.As and inspect Dirs; keep the field exported so tests can assert on the offending paths without resorting to substring matching on the message.
func (*DirsScopeEscapeError) Error ¶
func (e *DirsScopeEscapeError) Error() string
type FileContext ¶
type FileContext struct {
// AbsPath is the absolute path to the file.
AbsPath string
// Rel is the path relative to the scope's module root.
Rel string
// Fset is the file set used during parsing.
Fset *token.FileSet
// File is the parsed AST.
File *ast.File
}
FileContext holds the parsed AST and metadata for a single Go source file.
Pair with EachInSubtree or EachInChildren for typed AST iteration: callers commonly write
scanner.EachFile(t, scope, parser.SkipObjectResolution, func(t *testing.T, fc scanner.FileContext) {
scanner.EachInSubtree[ast.CallExpr](fc.File, func(call *ast.CallExpr) { ... })
})
type ImportBan ¶
type ImportBan struct {
// RuleID is the invariant identifier, e.g. "KERNEL-NO-RUNTIME-01".
RuleID string
// Forbidden is the list of import paths that are disallowed.
Forbidden []string
// AllowRels lists relative file paths (from module root) that are exempt
// from the ban. Useful for adapter bridges that must import the forbidden
// package by design.
AllowRels []string
// Hint is an optional message appended to each violation describing the
// preferred alternative.
Hint string
}
ImportBan describes a rule that forbids importing specific packages.
Use ImportBan when the entire check is "file imports forbidden package X". For custom AST patterns (e.g., struct literals, argument values), use EachFile with Report instead.
type Option ¶
type Option func(*scopeConfig)
Option is the functional-option type accepted by ModuleScope and DirsScope. The underlying [scopeConfig] is unexported, so external callers can only obtain Options via the exported IncludeTests / ExcludeRels / MatchRels / IncludeTestdata / IncludeGenerated constructors — they cannot author new Option values. This matches the Go standard-library pattern for sealed option sets.
func ExcludeRels ¶
ExcludeRels returns an option that excludes specific file paths (relative to the module root) from the file set returned by Scope.Files. Paths are matched after filepath.Clean normalization; use slash-separated paths on all platforms (e.g., "runtime/auth/roles.go"). Directory exclusion is not supported. To add custom skip directories, extend the option set in the scanner package; callers cannot define new options.
func IncludeGenerated ¶
func IncludeGenerated() Option
IncludeGenerated returns an option that allows the walk to descend into directories named "generated" (which are otherwise excluded by the default skip set). Use for "anywhere in the module" semantics where generated code must also be subject to the rule — e.g. anti-regression rules whose invariant would be defeated if codegen reintroduced the forbidden symbol.
Unlike IncludeTestdata, no path-segment validation is required: any rule that legitimately wants module-wide coverage including codegen output is the use case. Combine with ModuleScope for repo-wide "anywhere" rules.
func IncludeTestdata ¶
func IncludeTestdata() Option
IncludeTestdata returns an option that allows the walk to descend into directories named "testdata" (which are otherwise excluded by the default skip set). Legal only when the scope's roots include at least one path with a "testdata" segment relative to the module root; otherwise Scope.Files returns an error. ModuleScope + IncludeTestdata always errors — there is no legitimate use case for module-wide testdata scanning, and that is precisely the regression the default skip prevents.
func IncludeTests ¶
func IncludeTests() Option
IncludeTests returns an option that instructs ModuleScope and DirsScope to include *_test.go files in the file set returned by Scope.Files.
func MatchRels ¶
MatchRels returns an option that retains only files whose module-relative slash path satisfies pred. Applied AFTER default skip + ExcludeRels (composes AND with both — file must satisfy MatchRels AND not be in ExcludeRels).
Use for glob-style patterns that filename-suffix alone cannot express:
MatchRels(func(rel string) bool { return filepath.Base(rel) == "cell.yaml" })
MatchRels(func(rel string) bool { return strings.HasPrefix(filepath.Base(rel), "relay") })
rel is in slash form on all platforms. Multiple MatchRels options are chained (all predicates must return true). A nil predicate is silently ignored.
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope is an opaque file-set descriptor. Obtain a value via ModuleScope or DirsScope; the zero value is invalid and Scope.Files will return an error.
func DirsScope ¶
DirsScope creates a Scope limited to dirs (relative to modRoot). Missing directories are silently skipped — Scope.Files returns an empty slice with no error for a scope whose roots do not exist. Dirs that would escape modRoot via ".." path traversal are rejected at construction time; Scope.Files returns an error listing every out-of-bound path.
Prefer DirsScope when the rule applies to specific layers (e.g., runtime/, cells/); use ModuleScope when the rule must cover the entire repository.
func ModuleScope ¶
ModuleScope creates a Scope rooted at modRoot that walks the entire module, skipping the default directory set: vendor, testdata, worktrees, generated, .git, .venv, node_modules.
func (Scope) Files ¶
Files returns the sorted, deduplicated list of absolute file paths in the scope. It returns an error if the scope was not constructed via a constructor or if any walk operation fails.
func (Scope) ModRoot ¶
ModRoot returns the module root (absolute, OS-native) the scope was constructed against. Returns the empty string for a zero-value Scope. Callers use this to derive module-relative paths for files discovered by Scope.Files without re-running the walk.