golang.org/x/tools@v0.21.0/go/analysis/passes/composite/composite.go (about)

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package composite defines an Analyzer that checks for unkeyed
     6  // composite literals.
     7  package composite
     8  
     9  import (
    10  	"fmt"
    11  	"go/ast"
    12  	"go/types"
    13  	"strings"
    14  
    15  	"golang.org/x/tools/go/analysis"
    16  	"golang.org/x/tools/go/analysis/passes/inspect"
    17  	"golang.org/x/tools/go/ast/inspector"
    18  	"golang.org/x/tools/internal/aliases"
    19  	"golang.org/x/tools/internal/typeparams"
    20  )
    21  
    22  const Doc = `check for unkeyed composite literals
    23  
    24  This analyzer reports a diagnostic for composite literals of struct
    25  types imported from another package that do not use the field-keyed
    26  syntax. Such literals are fragile because the addition of a new field
    27  (even if unexported) to the struct will cause compilation to fail.
    28  
    29  As an example,
    30  
    31  	err = &net.DNSConfigError{err}
    32  
    33  should be replaced by:
    34  
    35  	err = &net.DNSConfigError{Err: err}
    36  `
    37  
    38  var Analyzer = &analysis.Analyzer{
    39  	Name:             "composites",
    40  	Doc:              Doc,
    41  	URL:              "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite",
    42  	Requires:         []*analysis.Analyzer{inspect.Analyzer},
    43  	RunDespiteErrors: true,
    44  	Run:              run,
    45  }
    46  
    47  var whitelist = true
    48  
    49  func init() {
    50  	Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only")
    51  }
    52  
    53  // runUnkeyedLiteral checks if a composite literal is a struct literal with
    54  // unkeyed fields.
    55  func run(pass *analysis.Pass) (interface{}, error) {
    56  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    57  
    58  	nodeFilter := []ast.Node{
    59  		(*ast.CompositeLit)(nil),
    60  	}
    61  	inspect.Preorder(nodeFilter, func(n ast.Node) {
    62  		cl := n.(*ast.CompositeLit)
    63  
    64  		typ := pass.TypesInfo.Types[cl].Type
    65  		if typ == nil {
    66  			// cannot determine composite literals' type, skip it
    67  			return
    68  		}
    69  		typeName := typ.String()
    70  		if whitelist && unkeyedLiteral[typeName] {
    71  			// skip whitelisted types
    72  			return
    73  		}
    74  		var structuralTypes []types.Type
    75  		switch typ := aliases.Unalias(typ).(type) {
    76  		case *types.TypeParam:
    77  			terms, err := typeparams.StructuralTerms(typ)
    78  			if err != nil {
    79  				return // invalid type
    80  			}
    81  			for _, term := range terms {
    82  				structuralTypes = append(structuralTypes, term.Type())
    83  			}
    84  		default:
    85  			structuralTypes = append(structuralTypes, typ)
    86  		}
    87  
    88  		for _, typ := range structuralTypes {
    89  			strct, ok := typeparams.Deref(typ).Underlying().(*types.Struct)
    90  			if !ok {
    91  				// skip non-struct composite literals
    92  				continue
    93  			}
    94  			if isLocalType(pass, typ) {
    95  				// allow unkeyed locally defined composite literal
    96  				continue
    97  			}
    98  
    99  			// check if the struct contains an unkeyed field
   100  			allKeyValue := true
   101  			var suggestedFixAvailable = len(cl.Elts) == strct.NumFields()
   102  			var missingKeys []analysis.TextEdit
   103  			for i, e := range cl.Elts {
   104  				if _, ok := e.(*ast.KeyValueExpr); !ok {
   105  					allKeyValue = false
   106  					if i >= strct.NumFields() {
   107  						break
   108  					}
   109  					field := strct.Field(i)
   110  					if !field.Exported() {
   111  						// Adding unexported field names for structs not defined
   112  						// locally will not work.
   113  						suggestedFixAvailable = false
   114  						break
   115  					}
   116  					missingKeys = append(missingKeys, analysis.TextEdit{
   117  						Pos:     e.Pos(),
   118  						End:     e.Pos(),
   119  						NewText: []byte(fmt.Sprintf("%s: ", field.Name())),
   120  					})
   121  				}
   122  			}
   123  			if allKeyValue {
   124  				// all the struct fields are keyed
   125  				continue
   126  			}
   127  
   128  			diag := analysis.Diagnostic{
   129  				Pos:     cl.Pos(),
   130  				End:     cl.End(),
   131  				Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
   132  			}
   133  			if suggestedFixAvailable {
   134  				diag.SuggestedFixes = []analysis.SuggestedFix{{
   135  					Message:   "Add field names to struct literal",
   136  					TextEdits: missingKeys,
   137  				}}
   138  			}
   139  			pass.Report(diag)
   140  			return
   141  		}
   142  	})
   143  	return nil, nil
   144  }
   145  
   146  // isLocalType reports whether typ belongs to the same package as pass.
   147  // TODO(adonovan): local means "internal to a function"; rename to isSamePackageType.
   148  func isLocalType(pass *analysis.Pass, typ types.Type) bool {
   149  	switch x := aliases.Unalias(typ).(type) {
   150  	case *types.Struct:
   151  		// struct literals are local types
   152  		return true
   153  	case *types.Pointer:
   154  		return isLocalType(pass, x.Elem())
   155  	case interface{ Obj() *types.TypeName }: // *Named or *TypeParam (aliases were removed already)
   156  		// names in package foo are local to foo_test too
   157  		return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
   158  	}
   159  	return false
   160  }