-
Notifications
You must be signed in to change notification settings - Fork 11
/
switch.go
216 lines (190 loc) · 6.98 KB
/
switch.go
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package exhaustive
import (
"fmt"
"go/ast"
"go/types"
"regexp"
"golang.org/x/tools/go/analysis"
)
// nodeVisitor is like the visitor function used by inspector.WithStack,
// except that it returns an additional value: a short description of
// the result of this node visit.
//
// The result value is typically useful in debugging or in unit tests to check
// that the nodeVisitor function took the expected code path.
type nodeVisitor func(n ast.Node, push bool, stack []ast.Node) (proceed bool, result string)
// toVisitor converts a nodeVisitor to a function suitable for use
// with inspector.WithStack.
func toVisitor(v nodeVisitor) func(ast.Node, bool, []ast.Node) bool {
return func(node ast.Node, push bool, stack []ast.Node) bool {
proceed, _ := v(node, push, stack)
return proceed
}
}
// Result values returned by node visitors.
const (
resultEmptyMapLiteral = "empty map literal"
resultNotMapLiteral = "not map literal"
resultKeyNilPkg = "nil map key package"
resultKeyNotEnum = "not all map key type terms are known enum types"
resultNoSwitchTag = "no switch tag"
resultTagNotValue = "switch tag not value type"
resultTagNilPkg = "nil switch tag package"
resultTagNotEnum = "not all switch tag terms are known enum types"
resultNotPush = "not push"
resultGeneratedFile = "generated file"
resultIgnoreComment = "has ignore comment"
resultNoEnforceComment = "has no enforce comment"
resultEnumMembersAccounted = "required enum members accounted for"
resultDefaultCaseSuffices = "default case satisfies exhaustiveness"
resultMissingDefaultCase = "missing required default case"
resultReportedDiagnostic = "reported diagnostic"
resultEnumTypes = "invalid or empty composing enum types"
)
// switchConfig is configuration for switchChecker.
type switchConfig struct {
explicit bool
defaultSignifiesExhaustive bool
defaultCaseRequired bool
checkGenerated bool
ignoreConstant *regexp.Regexp // can be nil
ignoreType *regexp.Regexp // can be nil
}
// switchChecker returns a node visitor that checks exhaustiveness of
// enum switch statements for the supplied pass, and reports
// diagnostics. The node visitor expects only *ast.SwitchStmt nodes.
func switchChecker(pass *analysis.Pass, cfg switchConfig, generated boolCache, comments commentCache) nodeVisitor {
return func(n ast.Node, push bool, stack []ast.Node) (bool, string) {
if !push {
// The proceed return value should not matter; it is ignored by
// inspector package for pop calls.
// Nevertheless, return true to be on the safe side for the future.
return true, resultNotPush
}
file := stack[0].(*ast.File)
if !cfg.checkGenerated && generated.get(file) {
// Don't check this file.
// Return false because the children nodes of node `n` don't have to be checked.
return false, resultGeneratedFile
}
sw := n.(*ast.SwitchStmt)
switchComments := comments.get(pass.Fset, file)[sw]
uDirectives, err := parseDirectives(switchComments)
if err != nil {
pass.Report(makeInvalidDirectiveDiagnostic(sw, err))
}
if !cfg.explicit && uDirectives.has(ignoreDirective) {
// Skip checking of this switch statement due to ignore
// comment. Still return true because there may be nested
// switch statements that are not to be ignored.
return true, resultIgnoreComment
}
if cfg.explicit && !uDirectives.has(enforceDirective) {
// Skip checking of this switch statement due to missing
// enforce comment.
return true, resultNoEnforceComment
}
requireDefaultCase := cfg.defaultCaseRequired
if uDirectives.has(ignoreDefaultCaseRequiredDirective) {
requireDefaultCase = false
}
if uDirectives.has(enforceDefaultCaseRequiredDirective) {
// We have "if" instead of "else if" here in case of conflicting ignore/enforce directives.
// In that case, because this is second, we will default to enforcing.
requireDefaultCase = true
}
if sw.Tag == nil {
return true, resultNoSwitchTag
}
t := pass.TypesInfo.Types[sw.Tag]
if !t.IsValue() {
return true, resultTagNotValue
}
es, ok := composingEnumTypes(pass, t.Type)
if !ok || len(es) == 0 {
return true, resultEnumTypes
}
var checkl checklist
checkl.ignoreConstant(cfg.ignoreConstant)
checkl.ignoreType(cfg.ignoreType)
for _, e := range es {
checkl.add(e.typ, e.members, pass.Pkg == e.typ.Pkg())
}
defaultCaseExists := analyzeSwitchClauses(sw, pass.TypesInfo, checkl.found)
if !defaultCaseExists && requireDefaultCase {
// Even if the switch explicitly enumerates all the
// enum values, the user has still required all switches
// to have a default case. We check this first to avoid
// early-outs
pass.Report(makeMissingDefaultDiagnostic(sw, dedupEnumTypes(toEnumTypes(es))))
return true, resultMissingDefaultCase
}
if len(checkl.remaining()) == 0 {
// All enum members accounted for.
// Nothing to report.
return true, resultEnumMembersAccounted
}
if defaultCaseExists && cfg.defaultSignifiesExhaustive {
// Though enum members are not accounted for, the
// existence of the default case signifies
// exhaustiveness. So don't report.
return true, resultDefaultCaseSuffices
}
pass.Report(makeSwitchDiagnostic(sw, dedupEnumTypes(toEnumTypes(es)), checkl.remaining()))
return true, resultReportedDiagnostic
}
}
func isDefaultCase(c *ast.CaseClause) bool {
return c.List == nil // see doc comment on List field
}
// analyzeSwitchClauses analyzes the clauses in the supplied switch
// statement. The info param typically is pass.TypesInfo. The each
// function is called for each enum member name found in the switch
// statement. The hasDefaultCase return value indicates whether the
// switch statement has a default clause.
func analyzeSwitchClauses(sw *ast.SwitchStmt, info *types.Info, each func(val constantValue)) (hasDefaultCase bool) {
for _, stmt := range sw.Body.List {
caseCl := stmt.(*ast.CaseClause)
if isDefaultCase(caseCl) {
hasDefaultCase = true
continue
}
for _, expr := range caseCl.List {
if val, ok := exprConstVal(expr, info); ok {
each(val)
}
}
}
return hasDefaultCase
}
func makeSwitchDiagnostic(sw *ast.SwitchStmt, enumTypes []enumType, missing map[member]struct{}) analysis.Diagnostic {
return analysis.Diagnostic{
Pos: sw.Pos(),
End: sw.End(),
Message: fmt.Sprintf(
"missing cases in switch of type %s: %s",
diagnosticEnumTypes(enumTypes),
diagnosticGroups(groupify(missing, enumTypes)),
),
}
}
func makeMissingDefaultDiagnostic(sw *ast.SwitchStmt, enumTypes []enumType) analysis.Diagnostic {
return analysis.Diagnostic{
Pos: sw.Pos(),
End: sw.End(),
Message: fmt.Sprintf(
"missing default case in switch of type %s",
diagnosticEnumTypes(enumTypes),
),
}
}
func makeInvalidDirectiveDiagnostic(node ast.Node, err error) analysis.Diagnostic {
return analysis.Diagnostic{
Pos: node.Pos(),
End: node.End(),
Message: fmt.Sprintf(
"failed to parse directives: %s",
err,
),
}
}