summaryrefslogtreecommitdiff
path: root/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/composite/composite.go
blob: 64e184d343927fb0b450ab3ef44f41ef586da190 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 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,
	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
}