// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package composite defines an Analyzer that checks for unkeyed // composite literals. package composite import ( "fmt" "go/ast" "go/types" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/typeparams" ) const Doc = `check for unkeyed composite literals This analyzer reports a diagnostic for composite literals of struct types imported from another package that do not use the field-keyed syntax. Such literals are fragile because the addition of a new field (even if unexported) to the struct will cause compilation to fail. As an example, err = &net.DNSConfigError{err} should be replaced by: err = &net.DNSConfigError{Err: err} ` var Analyzer = &analysis.Analyzer{ Name: "composites", Doc: Doc, URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite", Requires: []*analysis.Analyzer{inspect.Analyzer}, RunDespiteErrors: true, Run: run, } var whitelist = true func init() { Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only") } // runUnkeyedLiteral checks if a composite literal is a struct literal with // unkeyed fields. func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.CompositeLit)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { cl := n.(*ast.CompositeLit) typ := pass.TypesInfo.Types[cl].Type if typ == nil { // cannot determine composite literals' type, skip it return } typeName := typ.String() if whitelist && unkeyedLiteral[typeName] { // skip whitelisted types return } var structuralTypes []types.Type switch typ := typ.(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(typ) if err != nil { return // invalid type } for _, term := range terms { structuralTypes = append(structuralTypes, term.Type()) } default: structuralTypes = append(structuralTypes, typ) } for _, typ := range structuralTypes { under := deref(typ.Underlying()) strct, ok := under.(*types.Struct) if !ok { // skip non-struct composite literals continue } if isLocalType(pass, typ) { // allow unkeyed locally defined composite literal continue } // check if the struct contains an unkeyed field allKeyValue := true var suggestedFixAvailable = len(cl.Elts) == strct.NumFields() var missingKeys []analysis.TextEdit for i, e := range cl.Elts { if _, ok := e.(*ast.KeyValueExpr); !ok { allKeyValue = false if i >= strct.NumFields() { break } field := strct.Field(i) if !field.Exported() { // Adding unexported field names for structs not defined // locally will not work. suggestedFixAvailable = false break } missingKeys = append(missingKeys, analysis.TextEdit{ Pos: e.Pos(), End: e.Pos(), NewText: []byte(fmt.Sprintf("%s: ", field.Name())), }) } } if allKeyValue { // all the struct fields are keyed continue } diag := analysis.Diagnostic{ Pos: cl.Pos(), End: cl.End(), Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName), } if suggestedFixAvailable { diag.SuggestedFixes = []analysis.SuggestedFix{{ Message: "Add field names to struct literal", TextEdits: missingKeys, }} } pass.Report(diag) return } }) return nil, nil } func deref(typ types.Type) types.Type { for { ptr, ok := typ.(*types.Pointer) if !ok { break } typ = ptr.Elem().Underlying() } return typ } func isLocalType(pass *analysis.Pass, typ types.Type) bool { switch x := typ.(type) { case *types.Struct: // struct literals are local types return true case *types.Pointer: return isLocalType(pass, x.Elem()) case *types.Named: // names in package foo are local to foo_test too return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") case *types.TypeParam: return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") } return false }