// Package discover is plumb's first phase: it scans loaded packages for // //plumb:<name> directives and turns each tagged declaration into a Provider, // the surface-independent description the solve phase wires together. It reads // syntax and type information but makes no wiring decisions.
package discover import ( ) // directiveNames extracts the set names from a comment group attached to a // declaration. It returns the recognized plumb set names in order, plus any // located error for a recognized-but-invalid name (e.g. //plumb:123). // // Recognition uses go/ast.ParseDirective, the same rule gofmt applies: a comment // gofmt would demote by inserting a space after "//" (and block comments) is not // a directive and is silently ignored. A //plumb: directive whose name is not a // Go identifier, or that carries arguments, is rejected. func ( *ast.CommentGroup, *token.FileSet) ([]string, *diag.Error) { return appendDirectiveNames(nil, , ) } // appendDirectiveNames appends the set names from doc to prior. prior holds the // sets the declaration already joins (an enclosing group's directives, or an // earlier comment in the same doc), so a directive naming one of them (whether // the repeat is within doc or across the group/spec boundary) is a duplicate and // is rejected identically. The returned slice reuses prior's backing array; the // caller must not retain prior separately. func ( []string, *ast.CommentGroup, *token.FileSet) ([]string, *diag.Error) { if == nil { return , nil } := for , := range .List { , := ast.ParseDirective(.Slash, .Text) if ! || .Tool != "plumb" { continue // not a //plumb: directive (gofmt-demoted, block comment, or another tool) } // Check the name before the trailing-argument case: //plumb:123 extra has // both faults, and the invalid name is the more fundamental one to report. // ParseDirective already requires the name to start with [a-z0-9], so it // never yields the blank identifier; token.IsIdentifier is the whole check. if !token.IsIdentifier(.Name) { return nil, diag.Errorf(diag.PosIn(, .Pos()), diag.ErrInvalidSetName, "//%s; the set name must be a valid Go identifier", strings.TrimPrefix(.Text, "//")) } if .Args != "" { return nil, diag.Errorf(diag.PosIn(, .Pos()), diag.ErrInvalidSetName, "//%s; a plumb directive names only a set, with no trailing arguments", strings.TrimPrefix(.Text, "//")) } if slices.Contains(, .Name) { // A declaration joins a set at most once. Rather than silently deduping // (or producing a confusing "X collides with X" ambiguity), reject the // repeated directive with a clear, dedicated diagnostic. return nil, diag.Errorf(diag.PosIn(, .Pos()), diag.ErrDuplicateDirective, "//%s; the declaration already joins set %q: remove the duplicate directive", strings.TrimPrefix(.Text, "//"), .Name) } = append(, .Name) } return , nil } // allBlank reports whether every declared name is the blank identifier, so the // spec declares nothing that could be wired. func ( []*ast.Ident) bool { for , := range { if .Name != "_" { return false } } return len() > 0 } // Analyze scans every package for directives and builds the provider list. It // returns a located error for any malformed provider. // // Packages are scanned in import-path order and files in name order, and scanFile // returns the source-earliest fault within a file, so when the input has more than // one fault the diagnostic returned is a fixed choice: the source-earliest fault // of the first file (in that order) to have one. This choice does not depend on the order // the loader presented packages and files (which it does not promise); every phase // anchors ordering to source position. The sorts run over copies, leaving the // loaded packages untouched. Provider order is irrelevant downstream (solve // re-sorts each set by position), so only diagnostic selection is affected. // // The file being overwritten (the prior generated output, identified by the // base name of -output within the destination package) is skipped, so plumb // never feeds its own previous output back as input. (plumb does not emit // directives, so this only matters for a hand-edited or stray directive in that // file.) func ( []*Package, , string) ([]*Provider, *diag.Error) { var []*Provider for , := range slices.SortedFunc(slices.Values(), func(, *Package) int { return cmp.Compare(.PkgPath, .PkgPath) }) { for , := range slices.SortedFunc(slices.Values(.Syntax), func(, *ast.File) int { return cmp.Compare(FileBase(, ), FileBase(, )) }) { if != "" && .PkgPath == && FileBase(, ) == { continue // the file plumb is about to overwrite } , := scanFile(, ) if != nil { return nil, } = append(, ...) } } return , nil } // FileBase returns the base name of the file containing the given syntax tree. func ( *Package, *ast.File) string { := .Fset.File(.Pos()) if == nil { return "" } return filepath.Base(.Name()) } // scanFile walks one file's top-level declarations (locals are never providers) // and the type members nested in them, returning the source-earliest fault it // finds. A stray directive is caught only by the whole-file sweep, which must run // after the declaration scan, so the two can surface out of source order: a stray // in an early function body precedes a duplicate directive on a later declaration, // yet the declaration scan produces its fault first. Comparing the two by position // makes the reported diagnostic the one a reader would fix first. Within each phase // the earliest already comes first (declarations are visited in source order, so // the first declaration fault is the earliest of them, and the sweep walks comments // in position order), so only the cross-phase pair needs comparing. func ( *Package, *ast.File) ([]*Provider, *diag.Error) { var []*Provider var *diag.Error for , := range .Decls { var []*Provider var *diag.Error switch d := .(type) { case *ast.FuncDecl: , = scanFunc(, ) case *ast.GenDecl: , = scanGenDecl(, ) default: continue } if != nil { = break } = append(, ...) } if := diag.Earlier(, reportStrayDirectives(, )); != nil { return nil, } return , nil } // reportStrayDirectives flags any //plumb: directive that sits in a position the // targeted scan above never reads, so it would otherwise be silently ignored: a // directive in a function body, on an import, or floating free between // declarations. The scan only consults the doc comments of declarations and // members it recognizes, so a directive anywhere else is a mistake worth a // located error rather than a no-op. // // The sweep walks file.Comments (every comment group in the file) because // comments inside bodies and free-floating ones live only there and are never // attached to a node's Doc; an AST node walk would miss exactly the positions we // want to catch. collectProviderDocs marks the groups the scan does read, and // anything else carrying a plumb directive is reported. func ( *Package, *ast.File) *diag.Error { := collectProviderDocs() for , := range .Comments { if [] { continue } for , := range .List { , := ast.ParseDirective(.Slash, .Text) if ! || .Tool != "plumb" { continue } return diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrMisplacedDirective, "//%s; a directive attaches to a package-level function, method, variable, constant, conversion, struct field, struct type, or interface method", strings.TrimPrefix(.Text, "//")) } } return nil } // collectProviderDocs returns the set of comment groups the targeted scan reads // for directives: the doc of each top-level func, of each non-import GenDecl and // its value/type specs, and of each struct field and interface method. A plumb // directive in any other group is in an unsupported position. func ( *ast.File) map[*ast.CommentGroup]bool { := make(map[*ast.CommentGroup]bool) := func( *ast.CommentGroup) { if != nil { [] = true } } for , := range .Decls { switch d := .(type) { case *ast.FuncDecl: (.Doc) case *ast.GenDecl: if .Tok == token.IMPORT { continue // import specs are never providers; directives on them are stray } (.Doc) for , := range .Specs { switch s := .(type) { case *ast.ValueSpec: (.Doc) case *ast.TypeSpec: (.Doc) switch t := .Type.(type) { case *ast.StructType: for , := range .Fields.List { (.Doc) } case *ast.InterfaceType: for , := range .Methods.List { (.Doc) } } } } } } return } // specDirectiveNames returns the sets a spec joins: the union of the directives on // the enclosing GenDecl (which apply to every spec in the group) and the spec's // own directives. A group directive that precedes the keyword attaches to // GenDecl.Doc for both a single declaration and a parenthesized group; a directive // inside the group attaches to the spec's own doc. Unioning them (rather than // letting the spec's doc shadow the group's) means a spec that joins an extra set // does not silently drop out of the group's set. A set named at both levels is a // duplicate (the group already joins the spec to it), so it is rejected like any // other repeated directive rather than silently deduped. func ( *ast.GenDecl, *ast.CommentGroup, *token.FileSet) ([]string, *diag.Error) { , := directiveNames(.Doc, ) if != nil { return nil, } return appendDirectiveNames(, , ) } func ( *Package, *ast.FuncDecl) ([]*Provider, *diag.Error) { , := directiveNames(.Doc, .Fset) if != nil || len() == 0 { return nil, } , := .Info.Defs[.Name].(*types.Func) if == nil { // A declared name with no object did not type-check: a redeclaration // loses its Defs entry. The loader already reports the real cause; a // recognized directive must still fail loudly, never silently vanish. return nil, diag.Errorf(diag.PosIn(.Fset, .Name.Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name.Name) } := .Type().(*types.Signature) := diag.PosIn(.Fset, .Name.Pos()) // go/types records an object for a blank name too, so a directive on func _() // or a blank method reaches here. A blank identifier cannot be referenced (and // a blank method is absent from its type's method set, so it would later panic), // so reject it rather than emit an unusable "_" call. if .Name() == "_" { := "function" if .Recv() != nil { = "method" } return nil, diag.Errorf(, diag.ErrBlankProvider, "give the %s a name; a blank identifier declares nothing to wire", ) } // init is the same trap with a different spelling: Go forbids referring to it // from anywhere, so an emitted init() call can never compile, and a package // may declare several. A method named init is an ordinary selector and stays // allowed. (A set named init is unrelated and supported.) if .Name() == "init" && .Recv() == nil { return nil, diag.Errorf(, diag.ErrInitProvider, "init cannot be referenced, so it cannot provide; rename the function") } var []*Provider for , := range { := &Provider{ SetName: , Pos: , Name: .Name(), Pkg: .Types, Fn: , } if := .Recv(); != nil { // A method. Resolve the receiver's named type and its type params. A // concrete and an interface receiver share one kind; solve resolves the // receiver input type from the owner at instantiation time. := receiverNamed(.Type()) if == nil { // The receiver type did not type-check: a typo'd or undefined // receiver in tolerated-invalid input. The loader already reports the // real cause, so surface a located error rather than crashing. return nil, diag.Errorf(, diag.ErrInvalidType, "method %s has an unresolvable receiver type", .Name()) } .Kind = KindMethod .Owner = .Tparams = .TypeParams() // Diagnostics and the report name a method as Owner.Method, matching // the interface-method path: two same-named methods on different // receivers stay distinguishable without leaning on positions. .Name = .Obj().Name() + "." + .Name() } else { .Kind = KindFunc .Tparams = .TypeParams() } = append(, ) } return , nil } // receiverNamed unwraps a method receiver type to its origin named type, or nil // if the receiver is not a defined (named) type. func ( types.Type) *types.Named { if , := .(*types.Pointer); { = .Elem() } , := types.Unalias().(*types.Named) if ! { return nil } return .Origin() } func ( *Package, *ast.GenDecl) ([]*Provider, *diag.Error) { // An empty group such as var (), const (), or type () has no spec a directive could // attach to, and collectProviderDocs marks the group doc as read, so the // stray-directive sweep never sees it. Judge the directive here: an invalid // name surfaces as such, and a well-formed one is rejected as misplaced // rather than silently ignored. Import groups stay with the sweep, which // already flags them. if len(.Specs) == 0 && .Tok != token.IMPORT { , := directiveNames(.Doc, .Fset) if != nil { return nil, } if len() > 0 { for , := range .Doc.List { if , := ast.ParseDirective(.Slash, .Text); && .Tool == "plumb" { return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrMisplacedDirective, "//%s; the declaration group is empty and declares nothing to wire: add a declaration or remove the directive", strings.TrimPrefix(.Text, "//")) } } } } var []*Provider for , := range .Specs { switch s := .(type) { case *ast.ValueSpec: , := scanValueSpec(, , ) if != nil { return nil, } = append(, ...) case *ast.TypeSpec: , := scanTypeSpec(, , ) if != nil { return nil, } = append(, ...) } } return , nil } func ( *Package, *ast.GenDecl, *ast.ValueSpec) ([]*Provider, *diag.Error) { , := specDirectiveNames(, .Doc, .Fset) if != nil || len() == 0 { return nil, } switch .Tok { case token.VAR: return scanVar(, , ) case token.CONST: return scanConst(, , ) } return nil, nil } func ( *Package, *ast.ValueSpec, []string) ([]*Provider, *diag.Error) { if len(.Names) == 0 { return nil, nil } // Conversion provider: a single blank variable with an explicit target type. if len(.Names) == 1 && .Names[0].Name == "_" { return scanConversion(, , .Names[0], ) } // Past the conversion case, an all-blank spec (var _, _ = ...) declares nothing // to wire; the directive is inert, so reject it rather than silently dropping it. if allBlank(.Names) { return nil, diag.Errorf(diag.PosIn(.Fset, .Names[0].Pos()), diag.ErrBlankProvider, "give the variable a name; a blank identifier declares nothing to wire") } // The directive applies to the spec; every named variable it declares becomes // a value provider. (Two of the same type then collide as an ambiguity, which // is correct.) var []*Provider for , := range .Names { if .Name == "_" { continue } , := .Info.Defs[].(*types.Var) if == nil { // Redeclared names lose their Defs entry; fail loudly, as scanFunc does. return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name) } := diag.PosIn(.Fset, .Pos()) for , := range { = append(, &Provider{ SetName: , Kind: KindSymbol, Pos: , Name: .Name(), Pkg: .Types, Sym: , }) } } return , nil } func ( *Package, *ast.ValueSpec, *ast.Ident, []string) ([]*Provider, *diag.Error) { := diag.PosIn(.Fset, .Pos()) if .Type == nil { return nil, diag.Errorf(, diag.ErrInvalidConversion, "must declare an explicit target type") } := .Info.TypeOf(.Type) if == nil { // Unreachable even for tolerated-invalid input: go/types records a // (possibly Invalid) type for every type expression, never nothing. panic(fmt.Sprintf("plumb: conversion target type at %s has no recorded type", )) } if len(.Values) == 0 { return nil, diag.Errorf(, diag.ErrInvalidConversion, "needs a source like (*T)(nil); write the source type explicitly") } // A conversion provider goes from the source type to the declared target type: // it consumes typeof(Expr) and produces T, emitting T(src). Any conversion the // blank-var assignment type-checks is allowed (Go makes assignability itself a // conversion rule), so the target may be an interface the source implements, a // directional channel, a pointer with an identical base, and so on. The only // rejected form is an untyped nil, which names no source type at all. := .Info.TypeOf(.Values[0]) if == nil { // An undefined identifier in value position gets no recorded type at all // (unlike a type position, which records Invalid). The loader already // reports the real cause; the directive must still fail loudly. return nil, diag.Errorf(, diag.ErrInvalidType, "conversion source did not type-check; cannot wire it") } if gotypes.IsUntypedNil() { return nil, diag.Errorf(, diag.ErrInvalidConversion, "bare nil source; write (*Concrete)(nil) or name the source type") } // An identity conversion (source and target the same type) consumes and // produces the same type, which would otherwise surface as a self-referential // dependency cycle; reject it with the real reason instead. if types.Identical(, ) { // A constant source that is not itself written as a conversion (var _ // MyInt = 5) already carries the target's type (go/types converts the // literal implicitly), so "both MyInt" would baffle someone who wrote 5. // Name the real problem instead. if := .Info.Types[.Values[0]]; .Value != nil && !isConversionExpr(, .Values[0]) { return nil, diag.Errorf(, diag.ErrInvalidConversion, "the source must name a type, e.g. (*T)(nil); a constant takes the target's own type") } return nil, diag.Errorf(, diag.ErrInvalidConversion, "identity conversion: source and target are both %s", gotypes.TypeName()) } // On valid input the blank-var assignment type-checks, so the source is // assignable (hence convertible) to the target. When it does not (tolerated // type-error input), a non-convertible pair would emit an uncompilable T(src); // report it here instead. Skip the check if either side did not type-check; that // invalid type is caught downstream as ErrInvalidType. if !gotypes.ContainsInvalid() && !gotypes.ContainsInvalid() && !types.ConvertibleTo(, ) { return nil, diag.Errorf(, diag.ErrInvalidConversion, "cannot convert %s to %s", gotypes.TypeName(), gotypes.TypeName()) } var []*Provider for , := range { = append(, &Provider{ SetName: , Kind: KindConvert, Pos: , Name: gotypes.TypeName(), Pkg: .Types, ConvertTo: , ConvertFrom: , }) } return , nil } func ( *Package, *ast.ValueSpec, []string) ([]*Provider, *diag.Error) { if allBlank(.Names) { return nil, diag.Errorf(diag.PosIn(.Fset, .Names[0].Pos()), diag.ErrBlankProvider, "give the constant a name; a blank identifier declares nothing to wire") } var []*Provider for , := range .Names { if .Name == "_" { continue } , := .Info.Defs[].(*types.Const) if == nil { // Redeclared names lose their Defs entry; fail loudly, as scanFunc does. return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name) } := diag.PosIn(.Fset, .Pos()) if gotypes.IsUntyped(.Type()) { return nil, diag.Errorf(, diag.ErrUntypedConstant, "constant %s must have an explicit type", .Name()) } for , := range { = append(, &Provider{ SetName: , Kind: KindSymbol, Pos: , Name: .Name(), Pkg: .Types, Sym: , }) } } return , nil } func ( *Package, *ast.GenDecl, *ast.TypeSpec) ([]*Provider, *diag.Error) { var []*Provider // 1. A directive on the type declaration itself: a struct provider. , := specDirectiveNames(, .Doc, .Fset) if != nil { return nil, } , := .Info.Defs[.Name].(*types.TypeName) := .Name.Name == "_" if len() > 0 { if { return nil, diag.Errorf(diag.PosIn(.Fset, .Name.Pos()), diag.ErrBlankProvider, "give the type a name; a blank identifier declares nothing to wire") } if == nil { // A declared name with no object did not type-check: a redeclaration // loses its Defs entry. Fail loudly rather than drop the directive. return nil, diag.Errorf(diag.PosIn(.Fset, .Name.Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name.Name) } , := structProviders(, , , ) if != nil { return nil, } = append(, ...) } // 2. Directives on the type's members: struct fields or interface methods. The // owner is the declared type: a *types.Named for a defined type, or a // *types.Alias when the directive is on a member of an alias to an anonymous // composite (type C = struct{...} / interface{...}). This member scan only fires // when s.Type is a struct or interface literal, so an alias to a *named* // composite (which has an ast.Ident type) never reaches here. The gotypes.* // helpers read the origin, type parameters, and declaring object off either // form uniformly, so a field or method binds to a value of the alias just as it // does to one of a defined type. if != nil { := .Type() var []*Provider switch st := .Type.(type) { case *ast.StructType: , = scanStructFields(, , ) case *ast.InterfaceType: , = scanInterfaceMethods(, , ) } if != nil { return nil, } // A blank-named type cannot be referenced, so a member provider bound to it // would emit an unusable receiver; reject a member directive on one. if len() > 0 && { return nil, diag.Errorf(diag.PosIn(.Fset, .Name.Pos()), diag.ErrBlankProvider, "give the type a name; its members cannot be reached through a blank identifier") } = append(, ...) } else if := memberDirective(.Type); != nil { // The owner's declaration did not type-check (a redeclaration loses its // Defs entry), so the member scan cannot run. A member directive // must still fail loudly, never silently vanish with its owner. return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire its members", .Name.Name) } return , nil } // memberDirective returns the first //plumb: directive comment attached to a // member of a struct or interface literal, or nil. func ( ast.Expr) *ast.Comment { var []*ast.Field switch t := .(type) { case *ast.StructType: = .Fields.List case *ast.InterfaceType: = .Methods.List default: return nil } for , := range { if .Doc == nil { continue } for , := range .Doc.List { if , := ast.ParseDirective(.Slash, .Text); && .Tool == "plumb" { return } } } return nil } func ( *Package, *ast.TypeSpec, *types.TypeName, []string) ([]*Provider, *diag.Error) { := diag.PosIn(.Fset, .Name.Pos()) // The declared type may be a defined type or an alias; either way it must be a // struct at its core. An alias to an anonymous struct (type S = struct{...}) has // no *types.Named of its own but is still a struct, so gate on the underlying // type rather than requiring a defined type. := types.Unalias(.Type()).Underlying() if , := .(*types.Struct); ! { return nil, diag.Errorf(, diag.ErrStructProvider, "%s is %s; struct-type providers are only supported on struct types", .Name(), gotypes.KindOfType()) } // Keep the declared type as written (a *Named, or a *Alias when the directive // is on an alias) so the generated composite literal names it rather than the // underlying type; the type parameters and origin come from that declared type. := .Type() var []*Provider for , := range { = append(, &Provider{ SetName: , Kind: KindStruct, Pos: , Name: .Name(), Pkg: .Types, Declared: , Tparams: gotypes.TypeParamsOf(), }) } return , nil } // scanStructFields scans the fields of a struct declaration for member // directives. owner is the declared type (a *types.Named or, for an alias to an // anonymous struct, a *types.Alias), which the field provider binds to as its // receiver. func ( *Package, *ast.StructType, types.Type) ([]*Provider, *diag.Error) { var []*Provider for , := range .Fields.List { , := directiveNames(.Doc, .Fset) if != nil { return nil, } if len() == 0 { continue } := diag.PosIn(.Fset, .Pos()) if len(.Names) == 0 { return nil, diag.Errorf(, diag.ErrEmbeddedField, "give the field a name to use it") } // An all-blank directed field group (//plumb + _ int) declares nothing to // wire; reject it rather than silently drop the directive, as scanVar does. if allBlank(.Names) { return nil, diag.Errorf(, diag.ErrBlankProvider, "give the field a name; a blank identifier declares nothing to wire") } // Every name in a multi-name field group (A, B int) is its own provider, // mirroring multi-name var/const handling; same-type names then collide as // an ambiguity rather than being silently narrowed to the first. A blank // among them is skipped, the same way scanVar skips one. for , := range .Names { if .Name == "_" { continue } , := .Info.Defs[].(*types.Var) if == nil { // Redeclared fields lose their Defs entry; fail loudly, as scanFunc // does, rather than silently drop the directive on the losing field. return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name) } for , := range { = append(, &Provider{ SetName: , Kind: KindField, Pos: , Name: gotypes.TypeNameOf().Name() + "." + .Name(), Pkg: .Types, Sym: , Owner: gotypes.GenericOrigin(), Tparams: gotypes.TypeParamsOf(), }) } } } return , nil } // scanInterfaceMethods scans the methods of an interface declaration for member // directives. owner is the declared type (a *types.Named or, for an alias to an // anonymous interface, a *types.Alias), which the method provider binds to as its // receiver. func ( *Package, *ast.InterfaceType, types.Type) ([]*Provider, *diag.Error) { var []*Provider for , := range .Methods.List { , := directiveNames(.Doc, .Fset) if != nil { return nil, } if len() == 0 { continue } := diag.PosIn(.Fset, .Pos()) if len(.Names) == 0 { return nil, diag.Errorf(, diag.ErrEmbeddedInterface, "put the directive on a method instead") } // A non-basic (constraint) interface cannot be the type of a value. if , := .Underlying().(*types.Interface); && !.IsMethodSet() { return nil, diag.Errorf(, diag.ErrConstraintInterfaceMethod, "%s: a constraint type cannot be the type of a value, so it cannot be a receiver", gotypes.TypeNameOf().Name()) } := .Names[0] , := .Info.Defs[].(*types.Func) if == nil { // No reachable trigger while the interface type-checks, but mirror the // sibling scanners rather than silently drop a recognized directive. return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrInvalidType, "declaration of %s did not type-check; cannot wire it", .Name) } for , := range { = append(, &Provider{ SetName: , Kind: KindMethod, Pos: , Name: gotypes.TypeNameOf().Name() + "." + .Name(), Pkg: .Types, Fn: , Owner: gotypes.GenericOrigin(), Tparams: gotypes.TypeParamsOf(), }) } } return , nil } // isConversionExpr reports whether e is written as a conversion (a call whose // operand names a type), so MyInt(5) keeps the identity-conversion wording // while a bare constant gets the constant-specific one. func ( *Package, ast.Expr) bool { , := ast.Unparen().(*ast.CallExpr) if ! { return false } return .Info.Types[ast.Unparen(.Fun)].IsType() }