In a custom go vet analyzer, what does pass.TypesInfo give you that the syntax tree alone cannot?
answer
- the AST only knows what was typed
- go/types already resolved every name
- an identifier maps to an object
- compare package path, not spelling
- import aliases defeat text matching
basics
~20 spass.TypesInfo is the package's *types.Info: it maps each identifier to the declaration it resolves to and each expression to its type. That is how a check recognises context.TODO whatever the import is aliased to, and ignores an unrelated TODO.
solid answer
~50 sThe syntax tree only tells you how the code was written. `pass.TypesInfo` is the `*types.Info` the driver produced when it type-checked the package, and it answers what the code *means*: `TypesInfo.Uses[ident]` gives the `types.Object` an identifier at a use site resolves to, `Defs[ident]` the object it declares, and `TypesInfo.TypeOf(expr)` the type of any expression. So instead of comparing a selector's text to `context.TODO` — which breaks the moment someone writes `import ctx "context"` and matches a local variable coincidentally called `context` — you resolve the selector's `Sel` identifier to an object and compare `obj.Pkg().Path()` and `obj.Name()`. Type information also lets you ask structural questions the AST cannot: does this argument's type implement `error`, is this receiver a pointer, is this a conversion or a call. It is also the reason each package must type-check before its analyzers run.
code
go · 11 linesfunc isContextTODO(pass *analysis.Pass, call *ast.CallExpr) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
obj, ok := pass.TypesInfo.Uses[sel.Sel]
if !ok || obj.Pkg() == nil {
return false
}
return obj.Pkg().Path() == "context" && obj.Name() == "TODO"
}go deeper
Know that go vet checks get resolved type information alongside the syntax, and that comparing identifiers as text is not how you recognise a call to a specific function.
Explain the Uses, Defs and Types maps, why obj.Pkg().Path() is the reliable identity test, and where the type information comes from before Run is called.
Demonstrate the cheap-syntax-filter-then-type-predicate structure, and be able to state the precise predicate your check fires on when a team disputes a hit.
Judge whether a proposed rule is even expressible as a type predicate; rules that can only be approximated by name matching should not become mandatory checks.
## Syntax says how it was written; types say what it means A custom `go vet` check that only walks syntax is a fancy grep. It sees an `*ast.SelectorExpr` whose `X` is the identifier `context` and whose `Sel` is `TODO`, and that is all it can honestly conclude — that somebody typed those two words. It does not know which package (if any) `context` refers to here, whether the file imported the real `context` under a different local name, or whether `context` is a parameter shadowing the import. `pass.TypesInfo` closes that gap. It is a `*types.Info` filled in by `go/types` when the driver type-checked this package, and it is the single most important field on `analysis.Pass` for writing a check with a low false-positive *and* low false-negative rate. ## The maps you will actually use - **`Uses map[*ast.Ident]types.Object`** — for an identifier at a *use* site, the object it refers to: a `*types.Func`, `*types.Var`, `*types.Const`, `*types.TypeName`, `*types.PkgName`. This is the workhorse. - **`Defs map[*ast.Ident]types.Object`** — for an identifier at a *declaration* site, the object it declares. `Defs` for the blank identifier and for package clauses can be nil, so check. - **`Types map[ast.Expr]types.TypeAndValue`** — the type of every expression, plus its constant value if it has one and whether it is a value, a type, or a conversion. - **`Selections`** — for a selector, which field or method was selected and whether it required indirection. - **`Scopes`**, **`Implicits`**, **`InitOrder`**, **`Instances`** — for scope questions, implicit objects in type switches and imports, initialisation order, and generic instantiations. `TypesInfo.TypeOf(expr)` and `TypesInfo.ObjectOf(ident)` are convenience methods over those maps and are usually what you reach for. ## Identity, not spelling Once you have a `types.Object`, identity is exact: ```go obj := pass.TypesInfo.Uses[sel.Sel] if obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "context" && obj.Name() == "TODO" { ... } ``` That matches an aliased import, a dot-import, and a call written through a variable of function type holding the same object, and it *stops* matching a method called `TODO` on somebody's own type. `obj.Pkg()` is nil for the predeclared builtins and for labels, so the nil check is not decoration. Because objects come from `go/types`, you also get the whole type system for free. `types.Implements(t, errorInterface)`, `types.AssignableTo`, `types.Identical` and the `*types.Signature` of a function let you ask questions such as: does this call's second argument implement `io.Closer`, is this method's receiver a pointer, is this a variadic call, is this constant expression zero. None of those are answerable from an AST node. ## Where the type information comes from The driver, not your check, does the work. It parses the package's files, loads the type information for its dependencies (as export data, under the build-integrated driver), type-checks the package, and only then calls `Run`. Two consequences follow. First, **if a package does not type-check, your analyzer does not run on it** — unless the analyzer sets `RunDespiteErrors`, in which case it may run with partial information and `pass.TypeErrors` populated. A repository where one generated file is broken can therefore go silently unchecked. Second, **types stop at the package boundary in shape but not in reach**: you can inspect the types and objects of imported declarations, because those come from the dependency's export data, but you cannot see the *bodies* of functions in another package. Whole-program reasoning has to be assembled package by package using `analysis.Fact` values. ## Practical shape of a typed check The usual structure is: narrow by syntax cheaply (only `*ast.CallExpr`, only selectors), then confirm by type. Syntax is the fast filter; types are the decision. Doing it the other way round — asking type questions about every node — makes the check slow, and skipping the type step altogether is what produces the check that quietly matches nothing in the repositories that matter, because the one place the pattern occurs uses an import alias or a wrapper. ## A note on what this buys you at review time When someone challenges your check with "it will flag legitimate code", the answer is almost always a type predicate: it fires only for this object from this package path, with this signature. A check argued in terms of identifiers and string matching cannot make that promise, and should not be mandatory.
- What happens to your analyzer on a package that fails to type-check?The driver skips it, because there is no usable `pass.TypesInfo` to run against. Setting `RunDespiteErrors` on the analyzer opts in to running anyway, with whatever partial information exists and the errors available in `pass.TypeErrors`. Most checks should not set it: acting on half-resolved types produces confident nonsense, and the build was going to fail anyway.
- How would you check that an argument's type satisfies a particular interface?Get the argument's type with `pass.TypesInfo.TypeOf(arg)`, get the interface's `*types.Interface` (looking the object up from the package's imports, or from a well-known type such as the predeclared `error`), and call `types.Implements`. Use `types.AssignableTo` when you care about assignment rather than method sets, and remember pointer versus value receivers change the answer.
- Why isn't Uses enough on its own for a check about a method call?`Uses` gives you the `*types.Func` selected, but a method's meaning often depends on the receiver: `pass.TypesInfo.Selections[sel]` tells you whether it was a field or a method, whether an implicit address-of or dereference happened, and the index path through embedded fields. For interface method calls the object is the interface's method, not any concrete implementation.
saying these in an interview costs you the question
- Matching calls by comparing selector text to a package name
- Assuming the import name always equals the package path
- Ignoring that obj.Pkg() is nil for builtins
- Believing an analyzer can read another package's function bodies
- Setting RunDespiteErrors so the check runs on broken packages