Documentation
¶
Overview ¶
Package ast reads Go source as text: the helpers a code generator or an analysis tool needs to walk a repository's files and learn what is declared in them.
It is the compile-time counterpart to the parent reflection package, and the distinction is which artifact each one inspects. reflection works on values and types in a running program, so it can only see code that is linked into the binary asking. This package works on parsed source, so it can describe a package the tool using it does not import and does not compile — which is the case a generator is always in.
Nothing here runs on a request path. These are build-time tools: they touch the filesystem, and ModuleDirs runs `go list` when there is no vendor directory to read instead.
What it answers ¶
Where source lives: GetModulePath reads a go.mod to answer what a directory's module is called, which is what makes an import path classifiable as module-internal or third-party. ModuleDirs finds the source of everything that module depends on. WalkModule parses the files that belong to one module and nothing else — the exclusions it encodes (vendor, testdata, nested go.mod) are the part that is easy to get subtly wrong.
What a file declares: BuildImportMap and ResolveImports turn a file's imports into the lookup a type reference has to be resolved through. GetStructFields reports a struct's fields, ParseTypeRef and InlineStruct read what a field's type expression names, and UnionTerms enumerates the members of a type-union constraint — which is how a generator can key on a constraint rather than on a hand-kept list and be complete by construction.
Names, not resolved types ¶
The type of a struct field is reported as the Go source that spells it — "*pkg.T", "map[string]int", "Foo[T]" — and a type reference keeps the local name of the package it was written under, rather than either being resolved. Resolution needs a type checker and a build, and a generator reading one file has neither. Turning a reference into something canonical needs the declaring file's imports, which is why that step is the caller's: only the caller knows what it wants to key on.
An embedded field is keyed under the name Go gives it: the base identifier, with any pointer and type arguments stripped.
Struct tags ¶
Struct tags are read with reflect.StructTag rather than by splitting on spaces. A tag is not a space-separated list — a value may contain spaces, and this repository writes several that do — so the conventional grammar, quoting included, is the only parse that agrees with what the compiler and every reflection-based decoder see. LookupTag is that parse; GetTagValue is the same lookup narrowed to the value before the first comma.
Index ¶
- func BuildImportMap(file *goast.File) map[string]string
- func EmbeddedFieldName(expr goast.Expr) string
- func FilterModuleImports(imports map[string]string, modulePath string) map[string]string
- func GetModulePath(dir string) (string, error)
- func GetStructFields(structType *goast.StructType) map[string]string
- func GetTagValue(tag, key string) string
- func InlineStruct(expr goast.Expr) (*goast.StructType, bool)
- func LookupTag(tag, key string) (string, bool)
- func ModuleDirs(ctx context.Context, dir string) (map[string]string, error)
- func ResolveImports(file *goast.File, modulePath string) map[string]string
- func WalkModule(dir string, fn func(file *goast.File, relDir string) error) error
- type TypeRef
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildImportMap ¶
BuildImportMap returns a map from each import's local name (explicit alias or inferred last path segment) to its full import path. Blank ("_") and dot (".") imports are excluded.
A dot import is excluded because it puts names into the file's scope under no qualifier at all, so a reference that resolves through one is indistinguishable from a reference to a type declared in the file's own package. There is nothing to map it to.
func EmbeddedFieldName ¶
EmbeddedFieldName derives the field name Go assigns to an embedded field from its type expression: the base type identifier, ignoring any leading pointer and any generic type arguments (e.g. "*pkg.Base[T]" is embedded as field "Base"). It returns "" for an expression that names no type.
It is ParseTypeRef with the package qualifier dropped, which is what makes it the field's *name*: an embedded pkg.Base is reached as .Base, not as .pkg.Base.
func FilterModuleImports ¶
FilterModuleImports filters an import map to only include module-internal imports and converts the values from full import paths to module-relative directory paths.
Note that an import of the module root itself is dropped rather than mapped, since it has no relative directory below the root. ResolveImports is the variant that keeps everything and maps that case to ".".
func GetModulePath ¶
GetModulePath reads the module path from the go.mod file in the given directory.
func GetStructFields ¶
func GetStructFields(structType *goast.StructType) map[string]string
GetStructFields returns a map of field names to their type representation from an *ast.StructType. Fields named "_" are excluded.
The type representation is the field type rendered as Go source, so all field kinds are handled: "TypeName" (local), "pkg.TypeName" (imported), "*T" (pointer), "[]byte" (slice/array), "map[string]int" (map), "Foo[T]" (generic), and so on. Embedded (anonymous) fields are keyed by the embedded type's base name — e.g. an embedded "pkg.Base" or "*pkg.Base" is keyed "Base". An embedded field whose name cannot be derived (rare, e.g. an anonymous instantiated type with no resolvable base ident) is skipped.
func GetTagValue ¶
GetTagValue extracts the value of a specific tag key from a raw struct field tag string (with or without surrounding backticks). It returns the value before any comma (i.e., omitting options like "omitempty"). Returns empty string if the key is not found.
It is LookupTag narrowed to the conventional "value,option,option" grammar; see there for why the underlying parse is reflect.StructTag's.
func InlineStruct ¶
func InlineStruct(expr goast.Expr) (*goast.StructType, bool)
InlineStruct returns the struct literal a type expression declares inline, seeing through the same parenthesis and pointer wrappers ParseTypeRef does.
An inline struct is the case that names no type and still has fields worth walking, so a caller that recurses into struct-typed fields has to ask this as well as ParseTypeRef.
func LookupTag ¶
LookupTag reads a struct tag value whole, reporting whether the key was declared at all.
The lookup is reflect.StructTag's rather than a scan of this package's own, because a struct tag is not a space-separated list: a value may itself contain spaces, and this repo writes several that do — `validate:"required,min=1"` is fine either way, but `env:"X" envDefault:"a b"` is not. Splitting on spaces read "a" as the whole default and then treated the orphaned `b"` as a further key, so the field after it in the tag went missing too. reflect.StructTag.Lookup implements the conventional grammar, quoting included, and is the definition the compiler and every reflection-based decoder already agree on.
The value is returned uncut, and the reported bool distinguishes a declared empty value from an absent one. Both matter for tags whose grammar is not the conventional "value,option,option": an `envDefault` for a slice field is comma-separated all the way down, and declaring one empty is different from declaring none, since a default that exists always wins over a value some other layer supplied. GetTagValue is the narrower reading, for the tags that do follow the convention.
func ModuleDirs ¶
ModuleDirs locates the source of every module the module rooted at dir depends on, mapping each module's path to the directory holding it.
A module in the graph whose source has not been downloaded has no directory and is omitted rather than reported: whether a particular absence matters is the caller's question, not this one's.
Unlike the rest of this package it runs a subprocess — `go list -m all` — for the non-vendored case. That is a build-time cost in a build-time package, but it is a real one, and it is why a vendor directory is preferred when there is one rather than merely consulted.
func ResolveImports ¶
ResolveImports maps each of a file's imports from its local name to the path it should be keyed under, given the module the file belongs to: a module-relative directory for one of that module's own packages ("." for the module root), and the unchanged import path for anything else.
The two cannot collide, which is what makes the result usable as a single keyspace: a module-relative directory never begins with a domain name.
Use FilterModuleImports instead when the external imports are genuinely not wanted; this keeps them, so a type reference into a dependency resolves rather than silently going missing.
func WalkModule ¶
WalkModule parses every Go source file belonging to the module rooted at dir and calls fn for each, with the directory it was found in relative to dir, in slash form ("." for the root package).
What "belonging to" excludes is the part worth having one copy of. A vendor directory holds someone else's source. A testdata directory holds source that is deliberately not the module's — often deliberately broken. A directory beginning with "." or "_" is ignored by the go tool and so is ignored here. A _test.go file declares types the module does not export to anyone.
The subtle one is a nested module: a subdirectory with its own go.mod is a different module, and its types belong to whatever import path that go.mod names rather than to a directory under this one. Walking into it silently files those types under the wrong path, which is the kind of thing that surfaces much later as a type that cannot be found.
Files are parsed with parser.SkipObjectResolution, since nothing here needs the deprecated object graph.
Types ¶
type TypeRef ¶
type TypeRef struct {
// Package is the local name the qualifying package was imported under, as
// written. It is empty for a reference to a type in the same package.
Package string
// Name is the type's own name, unqualified.
Name string
}
TypeRef is a named type as source writes it: a type name, qualified by the local name of the package it was imported under when it comes from elsewhere.
It is deliberately the *written* reference rather than a resolved type. A generator reading one file has no type checker and no build, so the local name is all it has; turning that into something canonical needs the file's imports, which is the caller's to supply because only the caller knows what it wants to key on.
func ParseTypeRef ¶
ParseTypeRef reads the named type out of a type expression, seeing through the wrappers that do not change which type is named: parentheses, a pointer, and generic type arguments.
It reports false for anything that does not name a single type — a slice, a map, a function type, an inline struct — because there is no name in it to resolve. Use InlineStruct for the struct-literal case, which is the one that has fields to walk despite naming nothing.
func UnionTerms ¶
func UnionTerms(iface *goast.InterfaceType) ([]TypeRef, bool)
UnionTerms returns the types named by an interface that is a pure type union — `interface{ A | ~B | pkg.C }` — in declaration order.
It reports false for anything else: an interface carrying a method, one whose terms include a type this cannot name (a slice, a map), and the empty interface, which constrains nothing and so has no members to enumerate.
The tilde is discarded. `~B` and `B` name the same type for the purpose of asking what a constraint's members are; the difference is whether types *defined* as B also satisfy it, which is not a question the source can answer without resolving every type in the module.