Skip to content

Commit 176c4b3

Browse files
author
Nishanth Shanmugham
authored
ability to ignore specific types through new -ignore-enum-types flag
This commit adds an -ignore-enum-types flag to the analyzer. The value is a regexp pattern that specifies types that should be ignored in exhaustiveness checks. The new flag is similar to the existing -ignore-enum-members flag, except that the new flag is used to ignore types, not constants. Also: update and clarify related documentation. Re-add the "Skip analysis" section that was removed in earlier commits. Related test files are added to testdata/src/ignore-pattern/, which is a rename of the previously named ignore-enum-member directory.
1 parent a40fd81 commit 176c4b3

File tree

12 files changed

+288
-146
lines changed

12 files changed

+288
-146
lines changed

common.go

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package exhaustive
22

33
import (
4+
"fmt"
45
"go/ast"
56
"go/token"
67
"go/types"
@@ -140,13 +141,25 @@ type typeAndMembers struct {
140141
}
141142

142143
type checklist struct {
143-
info map[enumType]enumMembers
144-
checkl map[member]struct{}
145-
ignoreRx *regexp.Regexp
144+
info map[enumType]enumMembers
145+
checkl map[member]struct{}
146+
ignoreConstantRe *regexp.Regexp
147+
ignoreTypeRe *regexp.Regexp
146148
}
147149

148-
func (c *checklist) ignore(pattern *regexp.Regexp) {
149-
c.ignoreRx = pattern
150+
func (c *checklist) ignoreConstant(pattern *regexp.Regexp) {
151+
c.ignoreConstantRe = pattern
152+
}
153+
154+
func (c *checklist) ignoreType(pattern *regexp.Regexp) {
155+
c.ignoreTypeRe = pattern
156+
}
157+
158+
func (*checklist) reMatch(re *regexp.Regexp, s string) bool {
159+
if re == nil {
160+
return false
161+
}
162+
return re.MatchString(s)
150163
}
151164

152165
func (c *checklist) add(et enumType, em enumMembers, includeUnexported bool) {
@@ -162,7 +175,10 @@ func (c *checklist) add(et enumType, em enumMembers, includeUnexported bool) {
162175
if !ast.IsExported(name) && !includeUnexported {
163176
return
164177
}
165-
if c.ignoreRx != nil && c.ignoreRx.MatchString(et.Pkg().Path()+"."+name) {
178+
if c.reMatch(c.ignoreConstantRe, fmt.Sprintf("%s.%s", et.Pkg().Path(), name)) {
179+
return
180+
}
181+
if c.reMatch(c.ignoreTypeRe, fmt.Sprintf("%s.%s", et.Pkg().Path(), et.TypeName.Name())) {
166182
return
167183
}
168184
mem := member{
@@ -262,22 +278,6 @@ func diagnosticEnumType(enumType *types.TypeName) string {
262278
return enumType.Pkg().Name() + "." + enumType.Name()
263279
}
264280

265-
func dedupEnumTypes(types []enumType) []enumType {
266-
// TODO(nishanths) this function is a candidate for type parameterization
267-
268-
m := make(map[enumType]struct{})
269-
var ret []enumType
270-
for _, t := range types {
271-
_, ok := m[t]
272-
if ok {
273-
continue
274-
}
275-
m[t] = struct{}{}
276-
ret = append(ret, t)
277-
}
278-
return ret
279-
}
280-
281281
func diagnosticEnumTypes(types []enumType) string {
282282
var buf strings.Builder
283283
for i := range types {
@@ -308,6 +308,30 @@ func diagnosticGroups(gs []group) string {
308308
return strings.Join(out, ", ")
309309
}
310310

311+
func toEnumTypes(es []typeAndMembers) []enumType {
312+
out := make([]enumType, len(es))
313+
for i := range es {
314+
out[i] = es[i].et
315+
}
316+
return out
317+
}
318+
319+
func dedupEnumTypes(types []enumType) []enumType {
320+
// TODO(nishanths) this function is a candidate for type parameterization
321+
322+
m := make(map[enumType]struct{})
323+
var ret []enumType
324+
for _, t := range types {
325+
_, ok := m[t]
326+
if ok {
327+
continue
328+
}
329+
m[t] = struct{}{}
330+
ret = append(ret, t)
331+
}
332+
return ret
333+
}
334+
311335
// TODO(nishanths) If dropping pre-go1.18 support, the following
312336
// types and functions are candidates to be type parameterized.
313337

common_test.go

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ func TestChecklist(t *testing.T) {
115115
})
116116
})
117117

118-
t.Run("ignore regexp", func(t *testing.T) {
119-
t.Run("no filtering", func(t *testing.T) {
118+
t.Run("ignore pattern", func(t *testing.T) {
119+
t.Run("none", func(t *testing.T) {
120120
var c checklist
121121
c.add(et, em, false)
122122
checkRemaining(t, c, map[string]struct{}{
@@ -130,39 +130,65 @@ func TestChecklist(t *testing.T) {
130130
})
131131
})
132132

133-
t.Run("basic", func(t *testing.T) {
134-
var c checklist
135-
c.ignore(regexp.MustCompile(`^github.com/example/bar-go.G$`))
136-
c.add(et, em, false)
137-
checkRemaining(t, c, map[string]struct{}{
138-
"A": {},
139-
"B": {},
140-
"C": {},
141-
"D": {},
142-
"E": {},
143-
"F": {},
133+
t.Run("constant", func(t *testing.T) {
134+
t.Run("basic", func(t *testing.T) {
135+
var c checklist
136+
c.ignoreConstant(regexp.MustCompile(`^github.com/example/bar-go.G$`))
137+
c.add(et, em, false)
138+
checkRemaining(t, c, map[string]struct{}{
139+
"A": {},
140+
"B": {},
141+
"C": {},
142+
"D": {},
143+
"E": {},
144+
"F": {},
145+
})
144146
})
145-
})
146147

147-
t.Run("matches multiple", func(t *testing.T) {
148-
var c checklist
149-
c.ignore(regexp.MustCompile(`^github.com/example/bar-go`))
150-
c.add(et, em, false)
151-
checkRemaining(t, c, map[string]struct{}{})
148+
t.Run("matches multiple", func(t *testing.T) {
149+
var c checklist
150+
c.ignoreConstant(regexp.MustCompile(`^github.com/example/bar-go`))
151+
c.add(et, em, false)
152+
checkRemaining(t, c, map[string]struct{}{})
153+
})
154+
155+
t.Run("uses package path, not package name", func(t *testing.T) {
156+
var c checklist
157+
c.ignoreConstant(regexp.MustCompile(`bar.G`)) // this should not cause anything to be ignored.
158+
c.add(et, em, false)
159+
checkRemaining(t, c, map[string]struct{}{
160+
"A": {},
161+
"B": {},
162+
"C": {},
163+
"D": {},
164+
"E": {},
165+
"F": {},
166+
"G": {},
167+
})
168+
})
152169
})
153170

154-
t.Run("uses package path, not package name", func(t *testing.T) {
155-
var c checklist
156-
c.ignore(regexp.MustCompile(`bar.G`))
157-
c.add(et, em, false)
158-
checkRemaining(t, c, map[string]struct{}{
159-
"A": {},
160-
"B": {},
161-
"C": {},
162-
"D": {},
163-
"E": {},
164-
"F": {},
165-
"G": {},
171+
t.Run("type", func(t *testing.T) {
172+
t.Run("basic", func(t *testing.T) {
173+
var c checklist
174+
c.ignoreType(regexp.MustCompile(`^github.com/example/bar-go.T$`))
175+
c.add(et, em, false)
176+
checkRemaining(t, c, map[string]struct{}{})
177+
})
178+
179+
t.Run("uses package path, not package name", func(t *testing.T) {
180+
var c checklist
181+
c.ignoreType(regexp.MustCompile(`bar.T`)) // this should not cause anything to be ignored.
182+
c.add(et, em, false)
183+
checkRemaining(t, c, map[string]struct{}{
184+
"A": {},
185+
"B": {},
186+
"C": {},
187+
"D": {},
188+
"E": {},
189+
"F": {},
190+
"G": {},
191+
})
166192
})
167193
})
168194
})

exhaustive.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ optional.
134134
-check-generated bool false
135135
-default-signifies-exhaustive bool false
136136
-ignore-enum-members regexp pattern (none)
137+
-ignore-enum-types regexp pattern (none)
137138
-package-scope-only bool false
138139
139140
The -check flag specifies is a comma-separated list of program elements
@@ -145,14 +146,7 @@ If the -explicit-exhaustive-switch flag is enabled, the analyzer checks a
145146
switch statement only if it associated with a comment beginning with
146147
"//exhaustive:enforce". By default the flag is disabled, which means that the
147148
analyzer checks every enum switch statement not associated with a comment
148-
beginning with "//exhaustive:ignore". As an example, the following switch
149-
statement will be ignored.
150-
151-
//exhaustive:ignore
152-
switch v {
153-
case A:
154-
case B:
155-
}
149+
beginning with "//exhaustive:ignore".
156150
157151
The -explicit-exhaustive-map flag is the map literal counterpart of the
158152
-explicit-exhaustive-switch flag.
@@ -169,21 +163,39 @@ If the -default-signifies-exhaustive flag is enabled, the presence of a
169163
to counter the purpose of exhaustiveness checking, so it is not recommended
170164
that you do so.
171165
172-
The -ignore-enum-members flag specifies a regular expression in Go
173-
package regexp syntax. Enum members matching the regular expression do
174-
not have to be listed in switch statement cases or map literals to
175-
satisfy exhaustiveness. The specified regular expression is matched
176-
against an enum member name inclusive of the enum package import path.
177-
For example, if the enum package import path is "example.com/eco" and
178-
the member name is "Tundra", the specified regular expression will be
179-
matched against the string "example.com/eco.Tundra".
166+
The -ignore-enum-members flag specifies a regular expression in Go package
167+
regexp syntax. Constants matching the regular expression do not have to be
168+
listed in switch statement cases or map literals in order to satisfy
169+
exhaustiveness. The specified regular expression is matched against an the
170+
constant name inclusive of the enum package import path. For example, if the
171+
package import path of the constant is "example.com/eco" and the constant name
172+
is "Tundra", the specified regular expression will be matched against the
173+
string "example.com/eco.Tundra".
174+
175+
The -ignore-enum-types flag is similar to the -ignore-enum-members flag,
176+
except that it applies to types.
180177
181178
If the -package-scope-only flag is enabled, the analyzer only finds enums
182179
defined in package scope, but not in inner scopes such as functions.
183180
Consequently only switch statements and map literals that use these enums will
184181
be checked for exhaustiveness. By default, the analyzer finds enums defined in
185182
all scopes, including in inner scopes such as functions.
186183
184+
# Skip analysis
185+
186+
To skip analysis of a switch statement or map literal, associate it with a
187+
comment that begins with "//exhaustive:ignore". For example:
188+
189+
//exhaustive:ignore
190+
switch v {
191+
case A:
192+
case B:
193+
}
194+
195+
To ignore specific constants for exhaustiveness checks, use the
196+
-ignore-enum-members flag. To ignore specific types, use the
197+
-ignore-enum-types flag.
198+
187199
[Go language spec]: https://golang.org/ref/spec
188200
*/
189201
package exhaustive
@@ -203,7 +215,8 @@ func init() {
203215
Analyzer.Flags.BoolVar(&fExplicitExhaustiveMap, ExplicitExhaustiveMapFlag, false, `check map literal only if associated with "//exhaustive:enforce" comment`)
204216
Analyzer.Flags.BoolVar(&fCheckGenerated, CheckGeneratedFlag, false, "check generated files")
205217
Analyzer.Flags.BoolVar(&fDefaultSignifiesExhaustive, DefaultSignifiesExhaustiveFlag, false, "presence of 'default' case in switch statement unconditionally satisfies exhaustiveness")
206-
Analyzer.Flags.Var(&fIgnoreEnumMembers, IgnoreEnumMembersFlag, "enum members matching `regexp` do not have to be listed to satisfy exhaustiveness")
218+
Analyzer.Flags.Var(&fIgnoreEnumMembers, IgnoreEnumMembersFlag, "constants matching `regexp` are ignored for exhaustiveness checks")
219+
Analyzer.Flags.Var(&fIgnoreEnumTypes, IgnoreEnumTypesFlag, "types matching `regexp` are ignored for exhaustiveness checks")
207220
Analyzer.Flags.BoolVar(&fPackageScopeOnly, PackageScopeOnlyFlag, false, "find enums only in package scopes, not inner scopes")
208221

209222
var unused string
@@ -220,6 +233,7 @@ const (
220233
CheckGeneratedFlag = "check-generated"
221234
DefaultSignifiesExhaustiveFlag = "default-signifies-exhaustive"
222235
IgnoreEnumMembersFlag = "ignore-enum-members"
236+
IgnoreEnumTypesFlag = "ignore-enum-types"
223237
PackageScopeOnlyFlag = "package-scope-only"
224238

225239
IgnorePatternFlag = "ignore-pattern" // Deprecated: use IgnoreEnumMembersFlag.
@@ -257,6 +271,7 @@ var (
257271
fCheckGenerated bool
258272
fDefaultSignifiesExhaustive bool
259273
fIgnoreEnumMembers regexpFlag
274+
fIgnoreEnumTypes regexpFlag
260275
fPackageScopeOnly bool
261276
)
262277

@@ -269,6 +284,7 @@ func resetFlags() {
269284
fCheckGenerated = false
270285
fDefaultSignifiesExhaustive = false
271286
fIgnoreEnumMembers = regexpFlag{}
287+
fIgnoreEnumTypes = regexpFlag{}
272288
fPackageScopeOnly = false
273289
}
274290

@@ -298,12 +314,14 @@ func run(pass *analysis.Pass) (interface{}, error) {
298314
explicit: fExplicitExhaustiveSwitch,
299315
defaultSignifiesExhaustive: fDefaultSignifiesExhaustive,
300316
checkGenerated: fCheckGenerated,
301-
ignoreEnumMembers: fIgnoreEnumMembers.rx,
317+
ignoreConstant: fIgnoreEnumMembers.re,
318+
ignoreType: fIgnoreEnumTypes.re,
302319
}
303320
mapConf := mapConfig{
304-
explicit: fExplicitExhaustiveMap,
305-
checkGenerated: fCheckGenerated,
306-
ignoreEnumMembers: fIgnoreEnumMembers.rx,
321+
explicit: fExplicitExhaustiveMap,
322+
checkGenerated: fCheckGenerated,
323+
ignoreConstant: fIgnoreEnumMembers.re,
324+
ignoreType: fIgnoreEnumTypes.re,
307325
}
308326
swChecker := switchChecker(pass, swConf, generated, comments)
309327
mapChecker := mapChecker(pass, mapConf, generated, comments)

exhaustive_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@ func TestExhaustive(t *testing.T) {
4040
runTest(t, "default-signifies-exhaustive/default-absent/...", func() { fDefaultSignifiesExhaustive = true })
4141
runTest(t, "default-signifies-exhaustive/default-present/...", func() { fDefaultSignifiesExhaustive = true })
4242

43-
// Tests for the -ignore-enum-member flag.
44-
runTest(t, "ignore-enum-member/...", func() {
45-
re := regexp.MustCompile(`_UNSPECIFIED$|^general/y\.Echinodermata$|^ignore-enum-member.User$`)
46-
fIgnoreEnumMembers = regexpFlag{re}
43+
// Tests for -ignore-enum-members and -ignore-enum-types flags.
44+
runTest(t, "ignore-pattern/...", func() {
45+
fIgnoreEnumMembers = regexpFlag{
46+
regexp.MustCompile(`_UNSPECIFIED$|^general/y\.Echinodermata$|^ignore-pattern.User$`),
47+
}
48+
fIgnoreEnumTypes = regexpFlag{
49+
regexp.MustCompile(`label|^reflect.Kind$|^time.Duration$`),
50+
}
4751
})
4852

4953
// Tests for -package-scope-only flag.

flag.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,27 @@ var _ flag.Value = (*stringsFlag)(nil)
1111

1212
// regexpFlag implements flag.Value for parsing
1313
// regular expression flag inputs.
14-
type regexpFlag struct{ rx *regexp.Regexp }
14+
type regexpFlag struct{ re *regexp.Regexp }
1515

1616
func (f *regexpFlag) String() string {
17-
if f == nil || f.rx == nil {
17+
if f == nil || f.re == nil {
1818
return ""
1919
}
20-
return f.rx.String()
20+
return f.re.String()
2121
}
2222

2323
func (f *regexpFlag) Set(expr string) error {
2424
if expr == "" {
25-
f.rx = nil
25+
f.re = nil
2626
return nil
2727
}
2828

29-
rx, err := regexp.Compile(expr)
29+
re, err := regexp.Compile(expr)
3030
if err != nil {
3131
return err
3232
}
3333

34-
f.rx = rx
34+
f.re = re
3535
return nil
3636
}
3737

0 commit comments

Comments
 (0)