| // 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 *typeparams.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 *typeparams.TypeParam: |
| return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") |
| } |
| return false |
| } |