diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml new file mode 100644 index 0000000..c6fb23a --- /dev/null +++ b/.github/workflows/all.yml @@ -0,0 +1,37 @@ +on: [push, pull_request] +name: all +jobs: + main: + strategy: + matrix: + go-version: [1.14, 1.16, 1.18, 1.x] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: install go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: checkout code + uses: actions/checkout@v2 + + - name: build + run: make build + + # for earlier go versions, staticcheck build fails due to + # ../../../go/pkg/mod/honnef.co/go/tools@v0.3.3/go/ir/builder.go:36:2: //go:build comment without // +build comment + - name: install vet tools (>= go1.17) + if: ${{ matrix.go-version >= '1.17' }} + run: | + go install github.com/nishanths/exhaustive/cmd/exhaustive@latest + go install github.com/gordonklaus/ineffassign@latest + go install github.com/kisielk/errcheck@latest + go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: vet (>= go1.17) + if: ${{ matrix.go-version >= '1.17' }} + run: make vet + + - name: test + run: make test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 1c82551..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,29 +0,0 @@ -on: [push, pull_request] -name: build, test, and vet -jobs: - main: - strategy: - matrix: - go-version: [1.14, 1.x] - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - name: install go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - - name: checkout code - uses: actions/checkout@v2 - - name: build - # NOTE: Must build with go1.14. - # Do not change this requirement unless you're sure. - run: make build - - name: test - if: matrix.go-version >= '1.16' # some tests use io/fs which is available only >= go1.16 - run: make test - - name: install vet tools - if: matrix.go-version >= '1.16' # required for 'go install cmd@v1.2.3' syntax - run: make install-vet - - name: vet - if: matrix.go-version >= '1.16' - run: make vet diff --git a/Makefile b/Makefile index 981a7eb..bd305c3 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,29 @@ .PHONY: default default: build +.PHONY: all +all: build vet test + .PHONY: build build: go build ./... + go build ./cmd/exhaustive .PHONY: test test: - go test -cover ./... + go test ./... + +.PHONY: cover +cover: + go test -cover -coverprofile=coverage.out ./... + go tool cover -html=coverage.out .PHONY: install-vet install-vet: go install github.com/nishanths/exhaustive/cmd/exhaustive@latest go install github.com/gordonklaus/ineffassign@latest go install github.com/kisielk/errcheck@latest + go install honnef.co/go/tools/cmd/staticcheck@latest .PHONY: vet vet: @@ -21,6 +31,7 @@ vet: exhaustive ./... ineffassign ./... errcheck ./... + staticcheck -checks="inherit,-S1034" ./... .PHONY: upgrade-deps upgrade-deps: diff --git a/README.md b/README.md index 30757e0..3c9042c 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,35 @@ # exhaustive [![Godoc][godoc-svg]][repo] -Checks exhaustiveness of enum switch statements in Go source code. +Package exhaustive defines an analyzer that checks exhaustiveness of switch +statements of enum-like constants in Go source code. The analyzer can be +configured to additionally check exhaustiveness of map keys for map literals +whose key type is enum-like. -``` -go install github.com/nishanths/exhaustive/cmd/exhaustive@latest -``` +For documentation on the flags, the definition of enum, and the definition of +exhaustiveness, see [pkg.go.dev][godoc-doc]. For a changelog, see +[CHANGELOG][changelog] in the GitHub wiki. -For documentation on flags, the definition of enum, and the definition -of exhaustiveness, see [pkg.go.dev][godoc-doc]. For a changelog, see -[CHANGELOG][changelog] in the wiki. +The exported `analysis.Analyzer` uses the +[`golang.org/x/tools/go/analysis`][xanalysis] API. This should make it +possible to integrate `exhaustive` in your own analysis driver program. -The program may additionally be configured to check for exhaustiveness -of map literals with enum key types. See examples below. +## Install -The package provides an `analysis.Analyzer` value that follows the -guidelines in the [`golang.org/x/tools/go/analysis`][xanalysis] package. -This should make it possible to integrate `exhaustive` with your own -analysis driver programs. +Install the command line program: -## Bugs +``` +go install github.com/nishanths/exhaustive/cmd/exhaustive@latest +``` + +## Usage -`exhaustive` does not report missing cases in a switch statement that -switches on a type-parameterized type. See [this issue][issue-typeparam] -for details. +``` +exhaustive [flags] [packages] +``` -## Examples +## Example -Given the enum +Given the enum: ```go package token @@ -42,7 +45,7 @@ const ( ) ``` -and code that switches on the enum +and code that switches on the enum: ```go package calc @@ -65,19 +68,19 @@ var m = map[token.Token]string{ } ``` -running `exhaustive` with default options will print +running `exhaustive` with default options will print: ``` -% exhaustive path/to/pkg/calc +% exhaustive calc.go:6:2: missing cases in switch of type token.Token: Quotient, Remainder % ``` -To additionally check exhaustiveness of map literals, use -`-check=switch,map`. +To additionally check exhaustiveness of map literal keys, use +`-check=switch,map`: ``` -% exhaustive -check=switch,map path/to/pkg/calc +% exhaustive -check=switch,map calc.go:6:2: missing cases in switch of type token.Token: Quotient, Remainder calc.go:14:9: missing keys in map of key type token.Token: Quotient, Remainder % @@ -85,8 +88,8 @@ calc.go:14:9: missing keys in map of key type token.Token: Quotient, Remainder ## Contributing -Issues and pull requests are welcome. Before making a substantial -change please discuss it in an issue. +Issues and changes are welcome. Please discuss substantial changes +in an issue first. [repo]: https://pkg.go.dev/github.com/nishanths/exhaustive [godoc-svg]: https://pkg.go.dev/badge/github.com/nishanths/exhaustive.svg diff --git a/checklist.go b/checklist.go deleted file mode 100644 index c7c77f9..0000000 --- a/checklist.go +++ /dev/null @@ -1,58 +0,0 @@ -package exhaustive - -import ( - "go/ast" - "go/types" - "regexp" -) - -// A checklist holds a set of enum member names that have to be -// accounted for in order to satisfy exhaustiveness. -// -// The found method checks off member names from the set, based on -// constant value. The remaining method returns the member names not -// accounted for. -type checklist struct { - em enumMembers - checkl map[string]struct{} -} - -func makeChecklist(em enumMembers, enumPkg *types.Package, includeUnexported bool, ignore *regexp.Regexp) *checklist { - checkl := make(map[string]struct{}) - add := func(memberName string) { - if memberName == "_" { - // Blank identifier is often used to skip entries in iota lists. - // Also, it can't be referenced anywhere (including in a switch - // statement's cases), so it doesn't make sense to include it - // as required member to satisfy exhaustiveness. - return - } - if !ast.IsExported(memberName) && !includeUnexported { - return - } - if ignore != nil && ignore.MatchString(enumPkg.Path()+"."+memberName) { - return - } - checkl[memberName] = struct{}{} - } - - for _, name := range em.Names { - add(name) - } - - return &checklist{ - em: em, - checkl: checkl, - } -} - -func (c *checklist) found(val constantValue) { - // Delete all of the same-valued names. - for _, name := range c.em.ValueToNames[val] { - delete(c.checkl, name) - } -} - -func (c *checklist) remaining() map[string]struct{} { - return c.checkl -} diff --git a/cmd/exhaustive/exhaustive.go b/cmd/exhaustive/exhaustive.go index 932a2e0..4510a73 100644 --- a/cmd/exhaustive/exhaustive.go +++ b/cmd/exhaustive/exhaustive.go @@ -1,20 +1,9 @@ -// Command exhaustive checks exhaustiveness of enum switch statements. +// Command exhaustive is a command line interface for the exhaustive +// package at github.com/nishanths/exhaustive. // -// Usage +// # Usage // -// The command line usage is: -// -// exhaustive [flags] [packages] -// -// The program checks exhaustiveness of enum switch statements found in the -// specified packages. The enums required for the analysis don't necessarily -// have to be declared in the specified packages. -// -// For more about specifying packages, see 'go help packages'. -// -// For help, run 'exhaustive -h'. -// -// For more documentation, see https://godocs.io/github.com/nishanths/exhaustive. +// exhaustive [flags] [packages] package main import ( diff --git a/common.go b/common.go new file mode 100644 index 0000000..079b4c6 --- /dev/null +++ b/common.go @@ -0,0 +1,342 @@ +package exhaustive + +import ( + "go/ast" + "go/token" + "go/types" + "regexp" + "sort" + "strings" + + "golang.org/x/tools/go/ast/astutil" +) + +func denotesPackage(ident *ast.Ident, info *types.Info) bool { + obj := info.ObjectOf(ident) + if obj == nil { + return false + } + _, ok := obj.(*types.PkgName) + return ok +} + +// exprConstVal returns the constantValue for an expression if the +// expression is a constant value and if the expression is considered +// valid to satisfy exhaustiveness as defined by this program. +// Otherwise it returns (_, false). +func exprConstVal(e ast.Expr, info *types.Info) (constantValue, bool) { + handleIdent := func(ident *ast.Ident) (constantValue, bool) { + obj := info.Uses[ident] + if obj == nil { + return "", false + } + if _, ok := obj.(*types.Const); !ok { + return "", false + } + // There are two scenarios. + // See related test cases in typealias/quux/quux.go. + // + // Scenario 1 + // + // Tag package and constant package are the same. This is + // simple; we just use fs.ModeDir's value. + // + // Example: + // + // var mode fs.FileMode + // switch mode { + // case fs.ModeDir: + // } + // + // Scenario 2 + // + // Tag package and constant package are different. In this + // scenario, too, we accept the case clause expr constant value, + // as is. If the Go type checker is okay with the name being + // listed in the case clause, we don't care much further. + // + // Example: + // + // var mode fs.FileMode + // switch mode { + // case os.ModeDir: + // } + // + // Or equivalently: + // + // // The type of mode is effectively fs.FileMode, + // // due to type alias. + // var mode os.FileMode + // switch mode { + // case os.ModeDir: + // } + return determineConstVal(ident, info), true + } + + e = stripTypeConversions(astutil.Unparen(e), info) + + switch e := e.(type) { + case *ast.Ident: + return handleIdent(e) + + case *ast.SelectorExpr: + x := astutil.Unparen(e.X) + // Ensure we only see the form pkg.Const, and not e.g. + // structVal.f or structVal.inner.f. + // + // For this purpose, first we check that X, which is everything + // except the rightmost field selector *ast.Ident (the Sel + // field), is also an *ast.Ident. + xIdent, ok := x.(*ast.Ident) + if !ok { + return "", false + } + // Second, check that it's a package. It doesn't matter which + // package, just that it denotes some package. + if !denotesPackage(xIdent, info) { + return "", false + } + return handleIdent(e.Sel) + + default: + // e.g. literal + // these aren't considered towards satisfying exhaustiveness. + return "", false + } +} + +func stripTypeConversions(e ast.Expr, info *types.Info) ast.Expr { + c, ok := e.(*ast.CallExpr) + if !ok { + return e + } + if len(c.Args) != 1 { + return e + } + typ := info.TypeOf(c.Fun) + if typ == nil { + // can never happen for a valid Go program? + return e + } + // must not allow function calls. + if _, ok := typ.Underlying().(*types.Signature); ok { + return e + } + return stripTypeConversions(astutil.Unparen(c.Args[0]), info) +} + +// member is a single member of an enum type. +type member struct { + pos token.Pos + typ enumType + name string + val constantValue +} + +// typeAndMembers combines an enumType and its members set. +type typeAndMembers struct { + et enumType + em enumMembers +} + +type checklist struct { + info map[enumType]enumMembers + checkl map[member]struct{} + ignoreRx *regexp.Regexp +} + +func (c *checklist) ignore(pattern *regexp.Regexp) { + c.ignoreRx = pattern +} + +func (c *checklist) add(et enumType, em enumMembers, includeUnexported bool) { + addOne := func(name string) { + if name == "_" { + // Blank identifier is often used to skip entries in iota + // lists. Also, it can't be referenced anywhere (e.g. can't + // be referenced in switch statement cases) It doesn't make + // sense to include it as required member to satisfy + // exhaustiveness. + return + } + if !ast.IsExported(name) && !includeUnexported { + return + } + if c.ignoreRx != nil && c.ignoreRx.MatchString(et.Pkg().Path()+"."+name) { + return + } + mem := member{ + em.NameToPos[name], + et, + name, + em.NameToValue[name], + } + if c.checkl == nil { + c.checkl = make(map[member]struct{}) + } + c.checkl[mem] = struct{}{} + } + + if c.info == nil { + c.info = make(map[enumType]enumMembers) + } + c.info[et] = em + + for _, name := range em.Names { + addOne(name) + } +} + +func (c *checklist) found(val constantValue) { + // delete all same-valued items. + for et, em := range c.info { + for _, name := range em.ValueToNames[val] { + delete(c.checkl, member{ + em.NameToPos[name], + et, + name, + em.NameToValue[name], + }) + } + } +} + +func (c *checklist) remaining() map[member]struct{} { + return c.checkl +} + +// group is a collection of same-valued members, possibly from +// different enum types. +type group []member + +func groupMissing(missing map[member]struct{}, types []enumType) []group { + // indices maps each element in the input slice to its index. + indices := func(vs []enumType) map[enumType]int { + ret := make(map[enumType]int, len(vs)) + for i, v := range vs { + ret[v] = i + } + return ret + } + + typesOrder := indices(types) // for quick lookup + astBefore := func(x, y member) bool { + if typesOrder[x.typ] < typesOrder[y.typ] { + return true + } + if typesOrder[x.typ] > typesOrder[y.typ] { + return false + } + return x.pos < y.pos + } + + // byConstVal groups member names by constant value. + byConstVal := func(members map[member]struct{}) map[constantValue][]member { + ret := make(map[constantValue][]member) + for m := range members { + ret[m.val] = append(ret[m.val], m) + } + return ret + } + + var groups []group + for _, members := range byConstVal(missing) { + groups = append(groups, group(members)) + } + + // sort members within each group in AST order. + for i := range groups { + g := groups[i] + sort.Slice(g, func(i, j int) bool { return astBefore(g[i], g[j]) }) + groups[i] = g + } + // sort groups themselves in AST order. + // the index [0] access is safe, because there will be at least one + // element per group. + sort.Slice(groups, func(i, j int) bool { return astBefore(groups[i][0], groups[j][0]) }) + + return groups +} + +func diagnosticEnumType(enumType *types.TypeName) string { + return enumType.Pkg().Name() + "." + enumType.Name() +} + +func dedupEnumTypes(types []enumType) []enumType { + // TODO(nishanths) this function is a candidate for type parameterization + + m := make(map[enumType]struct{}) + var ret []enumType + for _, t := range types { + _, ok := m[t] + if ok { + continue + } + m[t] = struct{}{} + ret = append(ret, t) + } + return ret +} + +func diagnosticEnumTypes(types []enumType) string { + var buf strings.Builder + for i := range types { + buf.WriteString(diagnosticEnumType(types[i].TypeName)) + if i != len(types)-1 { + buf.WriteByte('|') + } + } + return buf.String() +} + +func diagnosticMember(m member) string { + return m.typ.Pkg().Name() + "." + m.name +} + +func diagnosticGroups(gs []group) string { + out := make([]string, len(gs)) + for i := range gs { + var buf strings.Builder + for j := range gs[i] { + buf.WriteString(diagnosticMember(gs[i][j])) + if j != len(gs[i])-1 { + buf.WriteByte('|') + } + } + out[i] = buf.String() + } + return strings.Join(out, ", ") +} + +// TODO(nishanths) If dropping pre-go1.18 support, the following +// types and functions are candidates to be type parameterized. + +type boolCache struct { + m map[*ast.File]bool + value func(*ast.File) bool +} + +func (c boolCache) get(file *ast.File) bool { + if c.m == nil { + c.m = make(map[*ast.File]bool) + } + if _, ok := c.m[file]; !ok { + c.m[file] = c.value(file) + } + return c.m[file] +} + +type commentCache struct { + m map[*ast.File]ast.CommentMap + value func(*token.FileSet, *ast.File) ast.CommentMap +} + +func (c commentCache) get(fset *token.FileSet, file *ast.File) ast.CommentMap { + if c.m == nil { + c.m = make(map[*ast.File]ast.CommentMap) + } + if _, ok := c.m[file]; !ok { + c.m[file] = c.value(fset, file) + } + return c.m[file] +} diff --git a/common_go118.go b/common_go118.go new file mode 100644 index 0000000..6c46fb9 --- /dev/null +++ b/common_go118.go @@ -0,0 +1,114 @@ +//go:build go1.18 +// +build go1.18 + +package exhaustive + +import ( + "go/types" + + "golang.org/x/tools/go/analysis" +) + +func fromNamed(pass *analysis.Pass, t *types.Named, typeparam bool) (result []typeAndMembers, ok bool) { + if tpkg := t.Obj().Pkg(); tpkg == nil { + // go/types documentation says: nil for labels and + // objects in the Universe scope. This happens for the built-in + // error type for example. + return nil, false // not a valid enum type, so ok == false + } + + et := enumType{t.Obj()} + if em, ok := importFact(pass, et); ok { + return []typeAndMembers{{et, em}}, true + } + + if typeparam { + if intf, ok := t.Underlying().(*types.Interface); ok { + return fromInterface(pass, intf, typeparam) + } + } + + return nil, false // not a valid enum type, so ok == false +} + +func fromInterface(pass *analysis.Pass, intf *types.Interface, typeparam bool) (result []typeAndMembers, all bool) { + var kind types.BasicKind + var kindSet bool + all = true + + // sameKind reports whether each type t that the function is called with + // has the same underlying basic kind as the rest. + sameBasicKind := func(t types.Type) (ok bool) { + basic, ok := t.Underlying().(*types.Basic) + if !ok { + return false + } + if kindSet && kind != basic.Kind() { + return false + } + kind = basic.Kind() + kindSet = true + return true + } + + for i := 0; i < intf.NumEmbeddeds(); i++ { + embed := intf.EmbeddedType(i) + + switch embed.(type) { + case *types.Union: + u := embed.(*types.Union) + // gather from each term in the union. + for i := 0; i < u.Len(); i++ { + r, a := fromType(pass, u.Term(i).Type(), typeparam) + for _, rr := range r { + if !sameBasicKind(rr.et.TypeName.Type()) { + all = false + break + } + } + result = append(result, r...) + all = all && a + } + + case *types.Named: + r, a := fromNamed(pass, embed.(*types.Named), typeparam) + for _, rr := range r { + if !sameBasicKind(rr.et.TypeName.Type()) { + all = false + break + } + } + result = append(result, r...) + all = all && a + + default: + // don't care about these. + // e.g. basic type + } + } + + return +} + +func fromType(pass *analysis.Pass, t types.Type, typeparam bool) (result []typeAndMembers, ok bool) { + switch t := t.(type) { + case *types.Named: + return fromNamed(pass, t, typeparam) + + case *types.TypeParam: + // does not appear to be explicitly documented, but based on + // spec (see section Type constraints) and source code, we can + // expect constraints to have underlying type *types.Interface. + intf := t.Constraint().Underlying().(*types.Interface) + return fromInterface(pass, intf, typeparam) + + default: + // ignore these. + return nil, true + } +} + +func composingEnumTypes(pass *analysis.Pass, t types.Type) (result []typeAndMembers, ok bool) { + _, typeparam := t.(*types.TypeParam) + return fromType(pass, t, typeparam) +} diff --git a/common_pre_go118.go b/common_pre_go118.go new file mode 100644 index 0000000..f916c17 --- /dev/null +++ b/common_pre_go118.go @@ -0,0 +1,37 @@ +//go:build !go1.18 +// +build !go1.18 + +package exhaustive + +import ( + "go/types" + + "golang.org/x/tools/go/analysis" +) + +func fromNamed(pass *analysis.Pass, t *types.Named) (result typeAndMembers, ok bool) { + if tpkg := t.Obj().Pkg(); tpkg == nil { + return typeAndMembers{}, false + } + + et := enumType{t.Obj()} + em, ok := importFact(pass, et) + if !ok { + return typeAndMembers{}, false + } + + return typeAndMembers{et, em}, true +} + +func composingEnumTypes(pass *analysis.Pass, t types.Type) (result []typeAndMembers, ok bool) { + switch t := t.(type) { + case *types.Named: + e, ok := fromNamed(pass, t) + if !ok { + return nil, false + } + return []typeAndMembers{e}, true + default: + return nil, false + } +} diff --git a/common_test.go b/common_test.go new file mode 100644 index 0000000..2a1214a --- /dev/null +++ b/common_test.go @@ -0,0 +1,381 @@ +package exhaustive + +import ( + "go/token" + "go/types" + "reflect" + "regexp" + "testing" +) + +func TestChecklist(t *testing.T) { + et := enumType{types.NewTypeName(50, types.NewPackage("github.com/example/bar-go", "bar"), "T", nil)} + em := enumMembers{ + Names: []string{"A", "B", "C", "D", "E", "F", "G"}, + NameToPos: map[string]token.Pos{ + "A": 0, + "B": 0, + "C": 0, + "D": 0, + "E": 0, + "F": 0, + "G": 0, + }, + NameToValue: map[string]constantValue{ + "A": "1", + "B": "2", + "C": "5", + "D": "2", + "E": "3", + "F": "2", + "G": "4", + }, + ValueToNames: map[constantValue][]string{ + "1": {"A"}, + "2": {"B", "D", "F"}, + "3": {"E"}, + "4": {"G"}, + "5": {"C"}, + }, + } + checkEnumMembersLiteral("TestChecklist", em) + + checkRemaining := func(t *testing.T, h checklist, want map[string]struct{}) { + t.Helper() + rem := make(map[string]struct{}) + for k := range h.remaining() { + rem[k.name] = struct{}{} + } + if !reflect.DeepEqual(want, rem) { + t.Errorf("want %+v, got %+v", want, rem) + } + } + + t.Run("main operations", func(t *testing.T) { + var c checklist + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + + c.found(`1`) + checkRemaining(t, c, map[string]struct{}{ + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + + c.found(`2`) + checkRemaining(t, c, map[string]struct{}{ + "C": {}, + "E": {}, + "G": {}, + }) + + // repeated call should be a no-op. + c.found(`2`) + checkRemaining(t, c, map[string]struct{}{ + "C": {}, + "E": {}, + "G": {}, + }) + + c.found(`2`) + checkRemaining(t, c, map[string]struct{}{ + "C": {}, + "E": {}, + "G": {}, + }) + + c.found(`5`) + checkRemaining(t, c, map[string]struct{}{ + "E": {}, + "G": {}, + }) + + // unknown value + c.found(`100000`) + checkRemaining(t, c, map[string]struct{}{ + "E": {}, + "G": {}, + }) + + c.found(`3`) + checkRemaining(t, c, map[string]struct{}{ + "G": {}, + }) + }) + + t.Run("ignore regexp", func(t *testing.T) { + t.Run("no filtering", func(t *testing.T) { + var c checklist + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + }) + + t.Run("basic", func(t *testing.T) { + var c checklist + c.ignore(regexp.MustCompile(`^github.com/example/bar-go.G$`)) + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + }) + }) + + t.Run("matches multiple", func(t *testing.T) { + var c checklist + c.ignore(regexp.MustCompile(`^github.com/example/bar-go`)) + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{}) + }) + + t.Run("uses package path, not package name", func(t *testing.T) { + var c checklist + c.ignore(regexp.MustCompile(`bar.G`)) + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + }) + }) + + t.Run("blank identifier", func(t *testing.T) { + em := enumMembers{ + Names: []string{"A", "B", "C", "D", "E", "F", "G", "_"}, + NameToPos: map[string]token.Pos{ + "A": 0, + "B": 0, + "C": 0, + "D": 0, + "E": 0, + "F": 0, + "G": 0, + "_": 0, + }, + NameToValue: map[string]constantValue{ + "A": "1", + "B": "2", + "C": "5", + "D": "2", + "E": "3", + "F": "2", + "G": "4", + "_": "0", + }, + ValueToNames: map[constantValue][]string{ + "0": {"_"}, + "1": {"A"}, + "2": {"B", "D", "F"}, + "3": {"E"}, + "4": {"G"}, + "5": {"C"}, + }, + } + checkEnumMembersLiteral("TestChecklist blank identifier", em) + + var c checklist + c.add(et, em, true) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + }) + + t.Run("unexported", func(t *testing.T) { + em := enumMembers{ + Names: []string{"A", "B", "C", "D", "E", "F", "G", "lowercase"}, + NameToPos: map[string]token.Pos{ + "A": 0, + "B": 0, + "C": 0, + "D": 0, + "E": 0, + "F": 0, + "G": 0, + "lowercase": 0, + }, + NameToValue: map[string]constantValue{ + "A": "1", + "B": "2", + "C": "5", + "D": "2", + "E": "3", + "F": "2", + "G": "4", + "lowercase": "42", + }, + ValueToNames: map[constantValue][]string{ + "1": {"A"}, + "2": {"B", "D", "F"}, + "3": {"E"}, + "4": {"G"}, + "5": {"C"}, + "42": {"lowercase"}, + }, + } + checkEnumMembersLiteral("TestChecklist lowercase", em) + + t.Run("include", func(t *testing.T) { + var c checklist + c.add(et, em, true) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + "lowercase": {}, + }) + }) + + t.Run("don't include", func(t *testing.T) { + var c checklist + c.add(et, em, false) + checkRemaining(t, c, map[string]struct{}{ + "A": {}, + "B": {}, + "C": {}, + "D": {}, + "E": {}, + "F": {}, + "G": {}, + }) + }) + }) +} + +func TestDiagnosticEnumType(t *testing.T) { + tn := types.NewTypeName(50, types.NewPackage("example.org/enumpkg-go", "enumpkg"), "Biome", nil) + got := diagnosticEnumType(tn) + want := "enumpkg.Biome" + if got != want { + t.Errorf("want %q, got %q", want, got) + } +} + +func TestGroupMissing(t *testing.T) { + groupStrings := func(groups []group) [][]string { + var out [][]string + for i := range groups { + var x []string + for j := range groups[i] { + x = append(x, diagnosticMember(groups[i][j])) + } + out = append(out, x) + } + return out + } + + // f adapts groupMissing for easy use in the test. + f := func(missing map[member]struct{}, types []enumType) [][]string { + return groupStrings(groupMissing(missing, types)) + } + + tn := types.NewTypeName(50, types.NewPackage("example.org/enumpkg-go", "enumpkg"), "River", nil) + et := enumType{tn} + + members := []member{ + 0: {10, et, "Ganga", "0"}, + 1: {20, et, "Yamuna", "2"}, + 2: {30, et, "Kaveri", "1"}, + 3: {60, et, "Unspecified", "0"}, + } + + t.Run("missing some: same-valued", func(t *testing.T) { + got := f(map[member]struct{}{ + members[0]: {}, + members[3]: {}, + members[2]: {}, + }, []enumType{et}) + want := [][]string{{"enumpkg.Ganga", "enumpkg.Unspecified"}, {"enumpkg.Kaveri"}} + if !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + }) + + t.Run("missing some: unique or unknown values", func(t *testing.T) { + got := f(map[member]struct{}{ + members[1]: {}, + members[2]: {}, + }, []enumType{et}) + want := [][]string{{"enumpkg.Yamuna"}, {"enumpkg.Kaveri"}} + if !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + }) + + t.Run("missing none", func(t *testing.T) { + got := f(nil, []enumType{et}) + if len(got) != 0 { + t.Errorf("want zero elements, got %d", len(got)) + } + }) + + t.Run("missing all", func(t *testing.T) { + got := f(map[member]struct{}{ + members[0]: {}, + members[2]: {}, + members[1]: {}, + members[3]: {}, + }, []enumType{et}) + want := [][]string{{"enumpkg.Ganga", "enumpkg.Unspecified"}, {"enumpkg.Yamuna"}, {"enumpkg.Kaveri"}} + if !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + }) + + tn = types.NewTypeName(50, types.NewPackage("example.org/xkcd-go", "xkcd"), "T", nil) + et = enumType{tn} + members = []member{ + 0: {12, et, "X", "0"}, + 1: {13, et, "A", "1"}, + 2: {14, et, "Unspecified", "0"}, + } + + t.Run("AST order", func(t *testing.T) { + got := f(map[member]struct{}{ + members[2]: {}, + members[0]: {}, + members[1]: {}, + }, []enumType{et}) + want := [][]string{{"xkcd.X", "xkcd.Unspecified"}, {"xkcd.A"}} + if !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + }) +} diff --git a/diagnostic.go b/diagnostic.go deleted file mode 100644 index 7d3f1ab..0000000 --- a/diagnostic.go +++ /dev/null @@ -1,62 +0,0 @@ -package exhaustive - -import ( - "go/types" - "sort" - "strings" -) - -// diagnosticMissingMembers constructs the list of missing enum members, -// suitable for use in a reported diagnostic message. -func diagnosticMissingMembers(missingMembers map[string]struct{}, em enumMembers) []string { - // inASTOrder sorts the given names in AST order. The AST position - // of each name is determined using the astPositions map. Names with - // smaller position values appear in the AST before names with large - // position values. - // - // The slice is sorted in place. It is also returned for - // convenience. - inASTOrder := func(names []string, astPositions map[string]int) []string { - sort.Slice(names, func(i, j int) bool { - return astPositions[names[i]] < astPositions[names[j]] - }) - return names - } - - // byConstVal groups member names by constant value. - byConstVal := func(names map[string]struct{}, nameToValue map[string]constantValue) map[constantValue][]string { - ret := make(map[constantValue][]string) - for name := range names { - val := nameToValue[name] - ret[val] = append(ret[val], name) - } - return ret - } - - // indices maps each string in the input slice to its index. - indices := func(names []string) map[string]int { - ret := make(map[string]int, len(names)) - for i, name := range names { - ret[name] = i - } - return ret - } - - astPositions := indices(em.Names) - - var groups []string - for _, names := range byConstVal(missingMembers, em.NameToValue) { - group := strings.Join(inASTOrder(names, astPositions), "|") - groups = append(groups, group) - } - return inASTOrder(groups, astPositions) -} - -// diagnosticEnumTypeName returns a string representation of an enum -// type for use in reported diagnostics. -func diagnosticEnumTypeName(enumType *types.TypeName, samePkg bool) string { - if samePkg { - return enumType.Name() - } - return enumType.Pkg().Name() + "." + enumType.Name() -} diff --git a/enum.go b/enum.go index e7e2957..b0643f7 100644 --- a/enum.go +++ b/enum.go @@ -13,36 +13,43 @@ import ( // constantValue is a constant.Value.ExactString(). type constantValue string -// Represents an enum type (or a potential enum type). -// It is a defined (named) type's name. +// enumType represents an enum type as defined by this program, which +// effectively is a defined (named) type. type enumType struct{ *types.TypeName } func (et enumType) String() string { return et.TypeName.String() } // for debugging func (et enumType) scope() *types.Scope { return et.TypeName.Parent() } // scope that the type is declared in func (et enumType) factObject() types.Object { return et.TypeName } // types.Object for fact export -// enumMembers is the members for a single enum type. +// enumMembers is set of enum members for a single enum type. // The zero value is ready to use. type enumMembers struct { - Names []string // enum member names, AST order + Names []string // enum member names + NameToPos map[string]token.Pos // member name -> AST position NameToValue map[string]constantValue // enum member name -> constant value ValueToNames map[constantValue][]string // constant value -> enum member names } -func (em *enumMembers) add(name string, val constantValue) { +// add adds an enum member to the set. +func (em *enumMembers) add(name string, val constantValue, pos token.Pos) { + if em.NameToPos == nil { + em.NameToPos = make(map[string]token.Pos) + } if em.NameToValue == nil { em.NameToValue = make(map[string]constantValue) } if em.ValueToNames == nil { em.ValueToNames = make(map[constantValue][]string) } - em.Names = append(em.Names, name) + em.NameToPos[name] = pos em.NameToValue[name] = val em.ValueToNames[val] = append(em.ValueToNames[val], name) } -func (em enumMembers) String() string { return em.factString() } // for debugging +func (em enumMembers) String() string { + return em.factString() +} func (em enumMembers) factString() string { var buf strings.Builder @@ -74,7 +81,7 @@ func findEnums(pkgScopeOnly bool, pkg *types.Package, inspect *inspector.Inspect continue } v := result[enumTyp] - v.add(memberName, val) + v.add(memberName, val, name.Pos()) result[enumTyp] = v } } diff --git a/enum_test.go b/enum_test.go index feecc3d..a855f72 100644 --- a/enum_test.go +++ b/enum_test.go @@ -2,6 +2,7 @@ package exhaustive import ( "fmt" + "go/token" "reflect" "sort" "strconv" @@ -13,14 +14,16 @@ import ( // checks that an enumMembers literal is correctly defined in tests. func checkEnumMembersLiteral(id string, v enumMembers) { - if len(v.Names) != len(v.NameToValue) { - panic(fmt.Sprintf("%s: wrong lengths: %d != %d (test definition bug)", id, len(v.Names), len(v.NameToValue))) - } - var count int for _, names := range v.ValueToNames { count += len(names) } + if len(v.Names) != len(v.NameToPos) { + panic(fmt.Sprintf("%s: wrong lengths: %d != %d (test definition bug)", id, len(v.Names), len(v.NameToPos))) + } + if len(v.Names) != len(v.NameToValue) { + panic(fmt.Sprintf("%s: wrong lengths: %d != %d (test definition bug)", id, len(v.Names), len(v.NameToValue))) + } if len(v.Names) != count { panic(fmt.Sprintf("%s: wrong lengths: %d != %d (test definition bug)", id, len(v.Names), count)) } @@ -29,15 +32,26 @@ func checkEnumMembersLiteral(id string, v enumMembers) { func TestEnumMembers_add(t *testing.T) { t.Run("basic", func(t *testing.T) { var v enumMembers - v.add("foo", "\"A\"") - v.add("z", "X") - v.add("bar", "\"B\"") - v.add("y", "Y") - v.add("x", "X") + v.add("foo", "\"A\"", 10) + v.add("z", "X", 20) + v.add("bar", "\"B\"", 30) + v.add("y", "Y", 40) + v.add("x", "X", 50) if want, got := []string{"foo", "z", "bar", "y", "x"}, v.Names; !reflect.DeepEqual(want, got) { t.Errorf("want %v, got %v", want, got) } + + if want, got := map[string]token.Pos{ + "foo": 10, + "z": 20, + "bar": 30, + "y": 40, + "x": 50, + }, v.NameToPos; !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + if want, got := map[string]constantValue{ "foo": "\"A\"", "z": "X", @@ -95,11 +109,12 @@ type checkEnum struct { } func equalCheckEnum(t *testing.T, want, got checkEnum) { + t.Helper() if want.typeName != got.typeName { - t.Errorf("want type name %s, got %s", want.typeName, got.typeName) + t.Errorf("type name: want %s, got %s", want.typeName, got.typeName) } if !reflect.DeepEqual(want.members, got.members) { - t.Errorf("type name %s: want members %+v, got %+v", want.typeName, want.members, got.members) + t.Errorf("type name %s: members: want %+v, got %+v", want.typeName, want.members, got.members) } } @@ -120,6 +135,9 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { wantPkg := []checkEnum{ {"VarConstMixed", enumMembers{ []string{"VCMixedB"}, + map[string]token.Pos{ + "VCMixedB": 0, + }, map[string]constantValue{ "VCMixedB": `1`, }, @@ -129,6 +147,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"IotaEnum", enumMembers{ []string{"IotaA", "IotaB"}, + map[string]token.Pos{ + "IotaA": 0, + "IotaB": 0, + }, map[string]constantValue{ "IotaA": `0`, "IotaB": `2`, @@ -140,6 +162,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"RepeatedValue", enumMembers{ []string{"RepeatedValueA", "RepeatedValueB"}, + map[string]token.Pos{ + "RepeatedValueA": 0, + "RepeatedValueB": 0, + }, map[string]constantValue{ "RepeatedValueA": `1`, "RepeatedValueB": `1`, @@ -150,6 +176,11 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"AcrossBlocksDeclsFiles", enumMembers{ []string{"Here", "Separate", "There"}, + map[string]token.Pos{ + "Here": 0, + "Separate": 0, + "There": 0, + }, map[string]constantValue{ "Here": `0`, "Separate": `1`, @@ -163,6 +194,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"UnexportedMembers", enumMembers{ []string{"unexportedMembersA", "unexportedMembersB"}, + map[string]token.Pos{ + "unexportedMembersA": 0, + "unexportedMembersB": 0, + }, map[string]constantValue{ "unexportedMembersA": `1`, "unexportedMembersB": `2`, @@ -174,6 +209,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"ParenVal", enumMembers{ []string{"ParenVal0", "ParenVal1"}, + map[string]token.Pos{ + "ParenVal0": 0, + "ParenVal1": 0, + }, map[string]constantValue{ "ParenVal0": `0`, "ParenVal1": `1`, @@ -185,6 +224,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"EnumRHS", enumMembers{ []string{"EnumRHS_A", "EnumRHS_B"}, + map[string]token.Pos{ + "EnumRHS_A": 0, + "EnumRHS_B": 0, + }, map[string]constantValue{ "EnumRHS_A": `0`, "EnumRHS_B": `1`, @@ -196,6 +239,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"T", enumMembers{ []string{"A", "B"}, + map[string]token.Pos{ + "A": 0, + "B": 0, + }, map[string]constantValue{ "A": `0`, "B": `1`, @@ -207,6 +254,9 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"PkgRequireSameLevel", enumMembers{ []string{"PA"}, + map[string]token.Pos{ + "PA": 0, + }, map[string]constantValue{ "PA": `200`, }, @@ -216,6 +266,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"UIntEnum", enumMembers{ []string{"UIntA", "UIntB"}, + map[string]token.Pos{ + "UIntA": 0, + "UIntB": 0, + }, map[string]constantValue{ "UIntA": "0", "UIntB": "1", @@ -227,6 +281,11 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"StringEnum", enumMembers{ []string{"StringA", "StringB", "StringC"}, + map[string]token.Pos{ + "StringA": 0, + "StringB": 0, + "StringC": 0, + }, map[string]constantValue{ "StringA": `"stringa"`, "StringB": `"stringb"`, @@ -240,6 +299,9 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"RuneEnum", enumMembers{ []string{"RuneA"}, + map[string]token.Pos{ + "RuneA": 0, + }, map[string]constantValue{ "RuneA": `97`, }, @@ -249,6 +311,9 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"ByteEnum", enumMembers{ []string{"ByteA"}, + map[string]token.Pos{ + "ByteA": 0, + }, map[string]constantValue{ "ByteA": `97`, }, @@ -258,6 +323,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"Int32Enum", enumMembers{ []string{"Int32A", "Int32B"}, + map[string]token.Pos{ + "Int32A": 0, + "Int32B": 0, + }, map[string]constantValue{ "Int32A": "0", "Int32B": "1", @@ -269,6 +338,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"Float64Enum", enumMembers{ []string{"Float64A", "Float64B"}, + map[string]token.Pos{ + "Float64A": 0, + "Float64B": 0, + }, map[string]constantValue{ "Float64A": `0`, "Float64B": `1`, @@ -287,6 +360,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { wantInner := []checkEnum{ {"InnerRequireSameLevel", enumMembers{ []string{"IX", "IY"}, + map[string]token.Pos{ + "IX": 0, + "IY": 0, + }, map[string]constantValue{ "IX": `200`, "IY": `200`, @@ -297,6 +374,12 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"T", enumMembers{ []string{"C", "D", "E", "F"}, + map[string]token.Pos{ + "C": 0, + "D": 0, + "E": 0, + "F": 0, + }, map[string]constantValue{ "C": `0`, "D": `1`, @@ -312,6 +395,10 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { }}, {"T", enumMembers{ []string{"A", "B"}, + map[string]token.Pos{ + "A": 0, + "B": 0, + }, map[string]constantValue{ "A": `0`, "B": `1`, @@ -348,6 +435,11 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { } for i := range want { + // don't bother with checking ast positions. + // zero out the values. + for k := range got[i].members.NameToPos { + got[i].members.NameToPos[k] = 0 + } equalCheckEnum(t, want[i], got[i]) } } diff --git a/exhaustive.go b/exhaustive.go index a0077b4..14f9a35 100644 --- a/exhaustive.go +++ b/exhaustive.go @@ -1,18 +1,18 @@ /* -Package exhaustive defines an analyzer that checks exhaustiveness of -enum switch statements in Go source code. It can be configured to -additionally check exhaustiveness of map literals that have enum key -types. +Package exhaustive defines an analyzer that checks exhaustiveness of switch +statements of enum-like constants in Go source code. The analyzer can be +configured to additionally check exhaustiveness of map keys for map literals +whose key type is enum-like. -# Definition of enum +# Definition of enum types and enum members -The Go language spec does not provide an explicit definition for an -enum. By convention, and for the purpose of this analyzer, an enum type -is any named type that meets these requirements: +The [Go programming language spec] does not provide an explicit definition for +an enum. For the purpose of this analyzer, and by convention, an enum type is +any named type that: 1. has underlying type float, string, or integer (includes byte and - rune); and - 2. has at least one constant of its type defined in the same scope. + rune, which are aliases for uint8 and int32, respectively); and + 2. has at least one constant of the type defined in the same scope. In the example below, Biome is an enum type. The 3 constants are its enum members. @@ -22,24 +22,23 @@ enum members. type Biome int const ( - Tundra Biome = 1 - Savanna Biome = 2 - Desert Biome = 3 + Tundra Biome = iota + Savanna + Desert ) Enum member constants for a particular enum type do not necessarily all have to be declared in the same const block. The constant values may be specified using iota, using literal values, or using any valid means for -declaring a Go constant. It is valid for multiple enum member constants -for a particular enum type to have the same constant value. +declaring a Go constant. It is allowed for multiple enum member +constants for a particular enum type to have the same constant value. # Definition of exhaustiveness -A switch statement that switches on a value of an enum type is -exhaustive if all of the enum members are listed in the switch -statement's cases. If multiple enum members have the same constant -value, it is sufficient for any one of these same-valued members to be -listed. +A switch statement that switches on a value of an enum type is exhaustive if +all of the enum members are listed in the switch statement's cases. If +multiple members have the same constant value, it is sufficient for any one of +these same-valued members to be listed. For an enum type defined in the same package as the switch statement, both exported and unexported enum members must be listed to satisfy @@ -52,84 +51,96 @@ cases may contribute towards satisfying exhaustiveness; literal values or variables will not. When using the default analyzer configuration, the existence of a -'default' case in a switch statement, on its own, does not automatically +'default' case in a switch statement, on its own, does not immediately make a switch statement exhaustive. See the -default-signifies-exhaustive flag to adjust this behavior. -A similar definition of exhaustiveness applies to a map literal whose -key type is an enum type. To be exhaustive, the map literal must specify -keys corresponding to all of the enum members. Empty map literals never -checked. Note that the -check flag must include "map" for map literals +A similar definition of exhaustiveness applies to a map literal whose key type +is an enum type. To be exhaustive, the map literal must specify keys +corresponding to all of the enum members. Empty map literals are +never checked. Note that the -check flag must include "map" for map literals to be checked. # Type aliases -The analyzer handles type aliases in the following manner. In the -example, T2 is a enum type, and T1 is an alias for T2. Note that we -don't call T1 itself an enum type; T1 is only an alias for an enum type. +The analyzer handles type aliases as shown in the following example. T2 +is a enum type, and T1 is an alias for T2. Note that we don't call T1 +itself an enum type; T1 is only an alias for an enum type. package pkg type T1 = newpkg.T2 const ( - A = newpkg.A - B = newpkg.B + A = newpkg.A + B = newpkg.B ) package newpkg type T2 int const ( - A T2 = 1 - B T2 = 2 + A T2 = 1 + B T2 = 2 ) -A switch statement that switches on a value of type T1 (which, in -reality, is just an alternate spelling for type T2) is exhaustive if all -of T2's enum members are listed in the switch statement's cases. Recall -that only constants declared in the same scope as type T2's scope -can be T2's enum members. +A switch statement that switches on a value of type T1 (which, in reality, is +just an alternate spelling for type T2) is exhaustive if all of T2's enum +members are listed in the switch statement's cases. Recall that +only constants declared in the same scope as type T2's scope can be T2's enum +members. The following switch statements are valid Go code and and are +exhaustive. + + // Note: the type of v is effectively newpkg.T2 due to alias. + func f(v pkg.T1) { + switch v { + case newpkg.A: + case newpkg.B: + } + switch v { + case pkg.A: + case pkg.B: + } + } The analyzer guarantees that introducing a type alias (such as type T1 = newpkg.T2) will never result in new diagnostics from the analyzer, as long as the set of enum member constant values of the RHS type is a subset of the set of enum member constant values of the old LHS type. -On a more advanced note, note that both of the following switch -statements are equally valid and exhaustive. - - // The type of v is effectively newpkg.T2 due to alias. - var v pkg.T1 +# Type parameters - // pkg.A is a valid substitute for newpkg.A (same constant value). - // Similarly for pkg.B. - switch v { - case pkg.A: - case pkg.B: - } +A switch statement that switches on a value whose type is a type parameter is +checked for exhaustiveness iff each type element in the type constraint is +an enum type and shares the same underlying basic kind. For example, the +following switch statement will be checked, assuming M, N, and O are enum +types with the same underlying basic kind. To satisfy exhaustiveness, all enum +members for each of the types M, N, and O must be listed in the switch +statement's cases. - switch v { - case newpkg.A: - case newpkg.B: + func bar[T M | I](v T) { + switch v { + } } + type I interface{ N | J } + type J interface{ O } # Flags Flags supported by the analyzer are described below. All flags are optional. - flag type default value - ---- ---- ------------- - -check string switch - -explicit-exhaustive-switch bool false - -explicit-exhaustive-map bool false - -check-generated bool false - -default-signifies-exhaustive bool false - -ignore-enum-members string (empty) - -package-scope-only bool false + flag type default value + ---- ---- ------------- + -check comma-separated strings "switch" + -explicit-exhaustive-switch bool false + -explicit-exhaustive-map bool false + -check-generated bool false + -default-signifies-exhaustive bool false + -ignore-enum-members regexp pattern (none) + -package-scope-only bool false The -check flag specifies is a comma-separated list of program elements that should be checked for exhaustiveness. Supported program elements are "switch" and "map". By default, only switch statements are checked. -Specify -check=switch,map to additionally check map literals. +Specify -check=switch,map to additionally check map literal keys. If the -explicit-exhaustive-switch flag is enabled, the analyzer only checks enum switch statements associated with a comment beginning with @@ -143,42 +154,43 @@ a comment beginning with "//exhaustive:ignore". case B: } -The -explicit-exhaustive-map flag is the map literal counterpart of the +The -explicit-exhaustive-map flag is the map literals counterpart of the -explicit-exhaustive-switch flag. -If the -check-generated flag is enabled, switch statements or map -literals in generated Go source files are also checked. Otherwise, by -default, generated files are not checked. Refer to +If the -check-generated flag is enabled, switch statements and map +literals in generated Go source files are checked. Otherwise, by +default, generated files are ignored. Refer to https://golang.org/s/generatedcode for the definition of generated files. If the -default-signifies-exhaustive flag is enabled, the presence of a -'default' case in a switch statement always satisfies exhaustiveness, -even if all enum members are not listed. It is recommended that you do -not enable this flag. Enabling it usually defeats the purpose of -exhaustiveness checking. +'default' case in a switch statement unconditionally satisfies exhaustiveness +(all enum members do not have to be listed). Enabling this flag usually tends +to counter the purpose of exhaustiveness checking, so it is not recommended +that you do so. The -ignore-enum-members flag specifies a regular expression in Go -package regexp syntax. Enum members matching the regular expression -do not have to be listed in switch statement cases to satisfy -exhaustiveness. The specified regular expression is matched against an -enum member name inclusive of the enum package import path: for example, -if the enum package import path is "example.com/eco" and the member name -is "Tundra", the specified regular expression will be matched against -the string "example.com/eco.Tundra". +package regexp syntax. Enum members matching the regular expression do +not have to be listed in switch statement cases or map literals to +satisfy exhaustiveness. The specified regular expression is matched +against an enum member name inclusive of the enum package import path: +for example, if the enum package import path is "example.com/eco" and +the member name is "Tundra", the specified regular expression will be +matched against the string "example.com/eco.Tundra". If the -package-scope-only flag is enabled, the analyzer only finds -enums defined in package-level scopes, and consequently only switch -statements and map literals that use package-level enums will be checked -for exhaustiveness. By default, the analyzer finds enums defined in all -scopes, and checks switch statements that switch on all these enums. +enums defined in package scope, and consequently only switch statements +and map literals that use package-scoped enums will be checked for +exhaustiveness. By default, the analyzer finds enums defined in all +scopes (e.g. in function bodies). + +[Go language spec]: https://golang.org/ref/spec */ package exhaustive import ( "fmt" "go/ast" - "go/token" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -186,16 +198,16 @@ import ( ) func init() { - Analyzer.Flags.Var(&fCheck, CheckFlag, "comma-separated list of program elements to check for exhaustiveness; supported elements are: switch, map") - Analyzer.Flags.BoolVar(&fExplicitExhaustiveSwitch, ExplicitExhaustiveSwitchFlag, false, `run exhaustive check on switch statements with "//exhaustive:enforce" comment`) - Analyzer.Flags.BoolVar(&fExplicitExhaustiveMap, ExplicitExhaustiveMapFlag, false, `run exhaustive check on map literals with "//exhaustive:enforce" comment`) + Analyzer.Flags.Var(&fCheck, CheckFlag, "comma-separated list of program `elements` that should be checked for exhaustiveness; supported elements are: switch, map") + Analyzer.Flags.BoolVar(&fExplicitExhaustiveSwitch, ExplicitExhaustiveSwitchFlag, false, `only check exhaustivess of switch statements associated with "//exhaustive:enforce" comment`) + Analyzer.Flags.BoolVar(&fExplicitExhaustiveMap, ExplicitExhaustiveMapFlag, false, `only check exhaustiveness of map literals associated with "//exhaustive:enforce" comment`) Analyzer.Flags.BoolVar(&fCheckGenerated, CheckGeneratedFlag, false, "check generated files") - Analyzer.Flags.BoolVar(&fDefaultSignifiesExhaustive, DefaultSignifiesExhaustiveFlag, false, `presence of "default" case in a switch statement unconditionally satisfies exhaustiveness`) + Analyzer.Flags.BoolVar(&fDefaultSignifiesExhaustive, DefaultSignifiesExhaustiveFlag, false, "presence of 'default' case in switch statement unconditionally satisfies exhaustiveness") Analyzer.Flags.Var(&fIgnoreEnumMembers, IgnoreEnumMembersFlag, "enum members matching `regexp` do not have to be listed to satisfy exhaustiveness") - Analyzer.Flags.BoolVar(&fPackageScopeOnly, PackageScopeOnlyFlag, false, "find enums only in package-level scopes, not in inner scopes") + Analyzer.Flags.BoolVar(&fPackageScopeOnly, PackageScopeOnlyFlag, false, "find enums only in package scopes, not inner scopes") var unused string - Analyzer.Flags.StringVar(&unused, IgnorePatternFlag, "", "no effect (deprecated); see -"+IgnoreEnumMembersFlag+" instead") + Analyzer.Flags.StringVar(&unused, IgnorePatternFlag, "", "no effect (deprecated); use -"+IgnoreEnumMembersFlag) Analyzer.Flags.StringVar(&unused, CheckingStrategyFlag, "", "no effect (deprecated)") } @@ -210,7 +222,7 @@ const ( IgnoreEnumMembersFlag = "ignore-enum-members" PackageScopeOnlyFlag = "package-scope-only" - IgnorePatternFlag = "ignore-pattern" // Deprecated: see IgnoreEnumMembersFlag instead. + IgnorePatternFlag = "ignore-pattern" // Deprecated: use IgnoreEnumMembersFlag. CheckingStrategyFlag = "checking-strategy" // Deprecated. ) @@ -223,8 +235,7 @@ const ( ) func validCheckElement(s string) error { - e := checkElement(s) // temporarily for check - switch e { + switch checkElement(s) { case elementSwitch: return nil case elementMap: @@ -287,82 +298,29 @@ func run(pass *analysis.Pass) (interface{}, error) { explicit: fExplicitExhaustiveSwitch, defaultSignifiesExhaustive: fDefaultSignifiesExhaustive, checkGenerated: fCheckGenerated, - ignoreEnumMembers: fIgnoreEnumMembers.regexp(), + ignoreEnumMembers: fIgnoreEnumMembers.rx, } mapConf := mapConfig{ explicit: fExplicitExhaustiveMap, checkGenerated: fCheckGenerated, - ignoreEnumMembers: fIgnoreEnumMembers.regexp(), + ignoreEnumMembers: fIgnoreEnumMembers.rx, } - swChecker := switchChecker(pass, swConf, generated, comments) mapChecker := mapChecker(pass, mapConf, generated, comments) - nodeType := func(e checkElement) ast.Node { - switch e { + // NOTE: should not share the same inspect.WithStack call for different + // program elements: the visitor function for a program element may + // exit traversal early, but this shouldn't affect traversal for + // other program elements. + for _, e := range fCheck.elements { + switch checkElement(e) { case elementSwitch: - return &ast.SwitchStmt{} + inspect.WithStack([]ast.Node{&ast.SwitchStmt{}}, toVisitor(swChecker)) case elementMap: - return &ast.CompositeLit{} + inspect.WithStack([]ast.Node{&ast.CompositeLit{}}, toVisitor(mapChecker)) default: panic(fmt.Sprintf("unknown checkElement %v", e)) } } - - nodeTypes := func() []ast.Node { - var out []ast.Node - for _, e := range fCheck.elements { - out = append(out, nodeType(checkElement(e))) - } - return out - } - - visitor := func(n ast.Node, push bool, stack []ast.Node) bool { - var proceed bool - switch n.(type) { - case *ast.SwitchStmt: - proceed, _ = swChecker(n, push, stack) - case *ast.CompositeLit: - proceed, _ = mapChecker(n, push, stack) - default: - panic(fmt.Sprintf("unexpected node type %T", n)) - } - return proceed - } - - inspect.WithStack(nodeTypes(), visitor) return nil, nil } - -// TODO(nishanths): When dropping pre go1.19 support, the following -// types and functions are candidates to be type parameterized. - -type boolCache struct { - m map[*ast.File]bool - value func(*ast.File) bool -} - -func (c boolCache) get(file *ast.File) bool { - if c.m == nil { - c.m = make(map[*ast.File]bool) - } - if _, ok := c.m[file]; !ok { - c.m[file] = c.value(file) - } - return c.m[file] -} - -type commentCache struct { - m map[*ast.File]ast.CommentMap - value func(*token.FileSet, *ast.File) ast.CommentMap -} - -func (c commentCache) get(fset *token.FileSet, file *ast.File) ast.CommentMap { - if c.m == nil { - c.m = make(map[*ast.File]ast.CommentMap) - } - if _, ok := c.m[file]; !ok { - c.m[file] = c.value(fset, file) - } - return c.m[file] -} diff --git a/exhaustive_go118_test.go b/exhaustive_go118_test.go new file mode 100644 index 0000000..b9e2312 --- /dev/null +++ b/exhaustive_go118_test.go @@ -0,0 +1,12 @@ +//go:build go1.18 +// +build go1.18 + +package exhaustive + +import ( + "testing" +) + +func TestExhaustiveGo118(t *testing.T) { + runTest(t, "typeparam/...") +} diff --git a/exhaustive_test.go b/exhaustive_test.go index 65a6599..1f7ff3d 100644 --- a/exhaustive_test.go +++ b/exhaustive_test.go @@ -7,130 +7,68 @@ import ( "golang.org/x/tools/go/analysis/analysistest" ) -func TestRegexpFlag(t *testing.T) { - t.Run("not set", func(t *testing.T) { - var v regexpFlag - if got := v.regexp(); got != nil { - t.Errorf("want nil, got %+v", got) - } - if got := v.String(); got != "" { - t.Errorf("expected empty string, got %q", got) - } - }) - - t.Run("empty input", func(t *testing.T) { - var v regexpFlag - if err := v.Set(""); err != nil { - t.Errorf("error unexpectedly non-nil: %v", err) - } - if got := v.regexp(); got != nil { - t.Errorf("want nil, got %+v", got) - } - if got := v.String(); got != "" { - t.Errorf("expected empty string, got %q", got) - } - }) - - t.Run("bad input", func(t *testing.T) { - var v regexpFlag - if err := v.Set("("); err == nil { - t.Errorf("error unexpectedly nil") - } - if got := v.regexp(); got != nil { - t.Errorf("want nil, got %+v", got) - } - if got := v.String(); got != "" { - t.Errorf("expected empty string, got %q", got) - } - }) - - t.Run("good input", func(t *testing.T) { - var v regexpFlag - if err := v.Set("^foo$"); err != nil { - t.Errorf("error unexpectedly non-nil: %v", err) - } - if v.regexp() == nil { - t.Errorf("unexpectedly nil") - } - if !v.regexp().MatchString("foo") { - t.Errorf("did not match") - } - if got, want := v.String(), regexp.MustCompile("^foo$").String(); got != want { - t.Errorf("want %q, got %q", got, want) +func runTest(t *testing.T, pattern string, setup ...func()) { + t.Helper() + t.Run(pattern, func(t *testing.T) { + resetFlags() + // default to checking switch and map for test. + fCheck = stringsFlag{ + []string{ + string(elementSwitch), + string(elementMap), + }, + nil, } - }) - - // The flag.Value interface doc says: "The flag package may call the - // String method with a zero-valued receiver, such as a nil pointer." - t.Run("String() nil receiver", func(t *testing.T) { - var v *regexpFlag - // expect no panic, and ... - if got := v.String(); got != "" { - t.Errorf("expected empty string, got %q", got) + for _, f := range setup { + f() } + analysistest.Run(t, analysistest.TestData(), Analyzer, pattern) }) } func TestExhaustive(t *testing.T) { - run := func(t *testing.T, pattern string, setup ...func()) { - t.Helper() - t.Run(pattern, func(t *testing.T) { - resetFlags() - // default to checking switch and map for test. - fCheck = stringsFlag{ - []string{string(elementSwitch), string(elementMap)}, - nil, - } - for _, f := range setup { - f() - } - analysistest.Run(t, analysistest.TestData(), Analyzer, pattern) - }) - } - // Enum discovery, enum types. - run(t, "enum/...") + runTest(t, "enum/...") // Tests for the -check-generated flag. - run(t, "generated-file/check-generated-off/...") - run(t, "generated-file/check-generated-on/...", func() { fCheckGenerated = true }) + runTest(t, "generated-file/check-generated-off/...") + runTest(t, "generated-file/check-generated-on/...", func() { fCheckGenerated = true }) // Tests for the -default-signifies-exhaustive flag. // (For tests with this flag off, see other testdata packages // such as "general/...".) - run(t, "default-signifies-exhaustive/default-absent/...", func() { fDefaultSignifiesExhaustive = true }) - run(t, "default-signifies-exhaustive/default-present/...", func() { fDefaultSignifiesExhaustive = true }) + runTest(t, "default-signifies-exhaustive/default-absent/...", func() { fDefaultSignifiesExhaustive = true }) + runTest(t, "default-signifies-exhaustive/default-present/...", func() { fDefaultSignifiesExhaustive = true }) // Tests for the -ignore-enum-member flag. - run(t, "ignore-enum-member/...", func() { + runTest(t, "ignore-enum-member/...", func() { re := regexp.MustCompile(`_UNSPECIFIED$|^general/y\.Echinodermata$|^ignore-enum-member.User$`) fIgnoreEnumMembers = regexpFlag{re} }) // Tests for -package-scope-only flag. - run(t, "scope/allscope/...") - run(t, "scope/pkgscope/...", func() { fPackageScopeOnly = true }) + runTest(t, "scope/allscope/...") + runTest(t, "scope/pkgscope/...", func() { fPackageScopeOnly = true }) - // Switch statements with ignore directive comment should not be checked during implicitly exhaustive switch - // mode - run(t, "ignore-comment/...") + // Program elements with ignore comment should not be + // checked during implicitly exhaustive mode. + runTest(t, "ignore-comment/...") - // Switch statements without enforce directive comment should not be checked during explicitly exhaustive - // switch mode - run(t, "enforce-comment/...", func() { + // Program elements without enforce comment should not be + // checked in explicitly exhaustive mode. + runTest(t, "enforce-comment/...", func() { fExplicitExhaustiveSwitch = true fExplicitExhaustiveMap = true }) // To satisfy exhaustiveness, it is sufficient for each unique constant // value of the members to be listed, not each member by name. - run(t, "duplicate-enum-value/...") + runTest(t, "duplicate-enum-value/...") - // Type alias switch statements. - run(t, "typealias/...") + runTest(t, "typealias/...") - // General tests (a mixture). - run(t, "general/...") + // mixture of general tests. + runTest(t, "general/...") } func assertNoError(t *testing.T, err error) { diff --git a/fact_test.go b/fact_test.go index 8cbe844..161bb17 100644 --- a/fact_test.go +++ b/fact_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/gob" "go/ast" + "go/token" "reflect" "testing" @@ -15,6 +16,11 @@ func TestEnumMembersFact(t *testing.T) { e := enumMembersFact{ Members: enumMembers{ Names: []string{"Tundra", "Savanna", "Desert"}, + NameToPos: map[string]token.Pos{ + "Tundra": 100, + "Savanna": 200, + "Desert": 300, + }, NameToValue: map[string]constantValue{ "Tundra": "1", "Savanna": "2", @@ -35,6 +41,14 @@ func TestEnumMembersFact(t *testing.T) { e = enumMembersFact{ Members: enumMembers{ Names: []string{"_", "add", "sub", "mul", "quotient", "remainder"}, + NameToPos: map[string]token.Pos{ + "_": 1, + "add": 11, + "sub": 12, + "mul": 33, + "quotient": 34, + "remainder": 35, + }, NameToValue: map[string]constantValue{ "_": "0", "add": "1", @@ -61,8 +75,8 @@ func TestEnumMembersFact(t *testing.T) { // This test exists to prevent regressions where changes made to a fact type used // by the Analyzer makes the type fail to gob-encode/decode. Particuarly: // -// * gob values cannot seem to have nil pointers. -// * fields must be exported to survive the encode/decode. +// - gob values cannot seem to have nil pointers. +// - fields must be exported to survive the encode/decode. // // The test likely doesn't cover everything that could go wrong during gob // encoding/decoding. @@ -103,8 +117,9 @@ func checkOneFactType(t *testing.T, fact analysis.Fact) { } }) - // Fields should all be exported. And no pointer types should be present - // unless you're absolutely sure, since nil pointers don't work with gob. + // Ensure that all all fields all exported, and there are no pointer + // types. Nil pointer values don't work with gob. We can't guarantee + // non-nil values here, so we just disallow all pointer types. t.Run("fields", func(t *testing.T) { switch v := fact.(type) { // NOTE: if there are more fact types, add them here. @@ -137,21 +152,32 @@ func checkTypeEnumMembers(t *testing.T, enumMembersType reflect.Type) { assertTypeFields(t, enumMembersType, []wantField{ {"Names", "[]string"}, + {"NameToPos", "map[string]token.Pos"}, {"NameToValue", "map[string]exhaustive.constantValue"}, {"ValueToNames", "map[exhaustive.constantValue][]string"}, }) - field, ok := enumMembersType.FieldByName("NameToValue") + // Check that types such as token.Pos and constantValue have basic + // underlying types (e.g. int, string). + + // check token.Pos. + field, ok := enumMembersType.FieldByName("NameToPos") if !ok { t.Errorf("failed to find field") return } cvType := field.Type.Elem() - checkTypeConstantValue(t, cvType) -} + if cvType.Kind() != reflect.Int { + t.Errorf("unexpected kind %v", cvType.Kind()) + } -func checkTypeConstantValue(t *testing.T, cvType reflect.Type) { - t.Helper() + // check constantValue. + field, ok = enumMembersType.FieldByName("NameToValue") + if !ok { + t.Errorf("failed to find field") + return + } + cvType = field.Type.Elem() if cvType.Kind() != reflect.String { t.Errorf("unexpected kind %v", cvType.Kind()) } diff --git a/flag.go b/flag.go index ac89c52..c5ab8db 100644 --- a/flag.go +++ b/flag.go @@ -11,62 +11,65 @@ var _ flag.Value = (*stringsFlag)(nil) // regexpFlag implements flag.Value for parsing // regular expression flag inputs. -type regexpFlag struct{ r *regexp.Regexp } +type regexpFlag struct{ rx *regexp.Regexp } -func (v *regexpFlag) String() string { - if v == nil || v.r == nil { +func (f *regexpFlag) String() string { + if f == nil || f.rx == nil { return "" } - return v.r.String() + return f.rx.String() } -func (v *regexpFlag) Set(expr string) error { +func (f *regexpFlag) Set(expr string) error { if expr == "" { - v.r = nil + f.rx = nil return nil } - r, err := regexp.Compile(expr) + rx, err := regexp.Compile(expr) if err != nil { return err } - v.r = r + f.rx = rx return nil } -func (v *regexpFlag) regexp() *regexp.Regexp { return v.r } - -// stringsFlag implements flag.Value for parsing a comma-separated -// string list. Surrounding space is stripped from each element of the -// list. If filter is non-nil it is called for each element in the -// input. +// stringsFlag implements flag.Value for parsing a comma-separated string +// list. Surrounding whitespace is stripped from the input and from each +// element. If filter is non-nil it is called for each element in the input. type stringsFlag struct { elements []string filter func(string) error } -func (v *stringsFlag) String() string { - if v == nil { +func (f *stringsFlag) String() string { + if f == nil { return "" } - return strings.Join(v.elements, ",") + return strings.Join(f.elements, ",") } -func (v *stringsFlag) filterFunc() func(string) error { - if v.filter != nil { - return v.filter +func (f *stringsFlag) filterFunc() func(string) error { + if f.filter != nil { + return f.filter } return func(_ string) error { return nil } } -func (v *stringsFlag) Set(input string) error { +func (f *stringsFlag) Set(input string) error { + input = strings.TrimSpace(input) + if input == "" { + f.elements = nil + return nil + } + for _, el := range strings.Split(input, ",") { el = strings.TrimSpace(el) - if err := v.filter(el); err != nil { + if err := f.filterFunc()(el); err != nil { return err } - v.elements = append(v.elements, el) + f.elements = append(f.elements, el) } return nil } diff --git a/flag_test.go b/flag_test.go new file mode 100644 index 0000000..a7431b7 --- /dev/null +++ b/flag_test.go @@ -0,0 +1,134 @@ +package exhaustive + +import ( + "errors" + "reflect" + "regexp" + "testing" +) + +func TestRegexpFlag(t *testing.T) { + t.Run("not set", func(t *testing.T) { + var v regexpFlag + if got := v.rx; got != nil { + t.Errorf("want nil, got %+v", got) + } + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) + + t.Run("empty input", func(t *testing.T) { + var v regexpFlag + if err := v.Set(""); err != nil { + t.Errorf("error unexpectedly non-nil: %v", err) + } + if got := v.rx; got != nil { + t.Errorf("want nil, got %+v", got) + } + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) + + t.Run("bad input", func(t *testing.T) { + var v regexpFlag + if err := v.Set("("); err == nil { + t.Errorf("error unexpectedly nil") + } + if got := v.rx; got != nil { + t.Errorf("want nil, got %+v", got) + } + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) + + t.Run("good input", func(t *testing.T) { + var v regexpFlag + if err := v.Set("^foo$"); err != nil { + t.Errorf("error unexpectedly non-nil: %v", err) + } + if v.rx == nil { + t.Errorf("unexpectedly nil") + } + if !v.rx.MatchString("foo") { + t.Errorf("did not match") + } + if got, want := v.String(), regexp.MustCompile("^foo$").String(); got != want { + t.Errorf("want %q, got %q", got, want) + } + }) + + // The flag.Value interface doc says: "The flag package may call the + // String method with a zero-valued receiver, such as a nil pointer." + t.Run("String() nil receiver", func(t *testing.T) { + var v *regexpFlag + // expect no panic, and ... + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) +} + +func TestStringsFlag(t *testing.T) { + t.Run("empty", func(t *testing.T) { + var v stringsFlag + if err := v.Set(""); err != nil { + t.Errorf("error unexpectedly non-nil: %v", err) + } + if got := len(v.elements); got != 0 { + t.Errorf("want zero length, got %d", got) + } + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) + + t.Run("happy path", func(t *testing.T) { + var v stringsFlag + + if err := v.Set("a, b,bb, c ,d "); err != nil { + t.Errorf("error unexpectedly non-nil: %v", err) + } + want := []string{"a", "b", "bb", "c", "d"} + got := v.elements + if !reflect.DeepEqual(want, got) { + t.Errorf("want %v, got %v", want, got) + } + + if want, got := "a,b,bb,c,d", v.String(); want != got { + t.Errorf("want %q, got %q", want, got) + } + }) + + t.Run("filter error", func(t *testing.T) { + errBoom := errors.New("boom") + + var v stringsFlag + v.filter = func(e string) error { + if e == "bb" { + return errBoom + } + return nil + } + + err := v.Set("a, b,bb, c ,d ") + if err == nil { + t.Errorf("error unexpectedly nil: %v", err) + } + if errBoom != err { + t.Errorf("want %v, got %v", errBoom, err) + } + }) + + // The flag.Value interface doc says: "The flag package may call the + // String method with a zero-valued receiver, such as a nil pointer." + t.Run("String() nil receiver", func(t *testing.T) { + var v *stringsFlag + // expect no panic, and ... + if got := v.String(); got != "" { + t.Errorf("expected empty string, got %q", got) + } + }) +} diff --git a/map.go b/map.go index 76da76f..7ec844f 100644 --- a/map.go +++ b/map.go @@ -5,7 +5,6 @@ import ( "go/ast" "go/types" "regexp" - "strings" "golang.org/x/tools/go/analysis" ) @@ -23,17 +22,12 @@ type mapConfig struct { func mapChecker(pass *analysis.Pass, cfg mapConfig, 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 } @@ -55,19 +49,16 @@ func mapChecker(pass *analysis.Pass, cfg mapConfig, generated boolCache, comment return false, resultEmptyMapLiteral } - keyType, ok := mapType.Key().(*types.Named) - if !ok { - return true, resultKeyNotNamed - } - fileComments := comments.get(pass.Fset, file) var relatedComments []*ast.CommentGroup for i := range stack { - // iterate over stack in the reverse order (from bottom to top) + // iterate over stack in the reverse order (from inner + // node to outer node) node := stack[len(stack)-1-i] switch node.(type) { // need to check comments associated with following nodes, - // because logic of ast package doesn't allow to associate comment with *ast.CompositeLit + // because logic of ast package doesn't associate comment + // with *ast.CompositeLit as required. case *ast.CompositeLit, // stack[len(stack)-1] *ast.ReturnStmt, // return ... *ast.IndexExpr, // map[enum]...{...}[key] @@ -79,64 +70,63 @@ func mapChecker(pass *analysis.Pass, cfg mapConfig, generated boolCache, comment *ast.ValueSpec: // var declaration relatedComments = append(relatedComments, fileComments[node]...) continue + default: + // stop iteration on the first inappropriate node + break } - // stop iteration on the first inappropriate node - break } if !cfg.explicit && hasComment(relatedComments, ignoreComment) { - // Skip checking of this map literal due to ignore directive comment. - // Still return true because there may be nested map literals - // that are not to be ignored. + // Skip checking of this map literal due to ignore + // comment. Still return true because there may be nested + // map literals that are not to be ignored. return true, resultIgnoreComment } if cfg.explicit && !hasComment(relatedComments, enforceComment) { - // Skip checking of this map literal due to missing enforce directive comment. return true, resultNoEnforceComment } - keyPkg := keyType.Obj().Pkg() - if keyPkg == nil { - // The Go documentation says: nil for labels and objects in the Universe scope. - // This happens for the `error` type, for example. - return true, resultKeyNilPkg - } - - enumTyp := enumType{keyType.Obj()} - members, ok := importFact(pass, enumTyp) - if !ok { - return true, resultKeyNotEnum + es, ok := composingEnumTypes(pass, mapType.Key()) + if !ok || len(es) == 0 { + return true, resultEnumTypes } - samePkg := keyPkg == pass.Pkg // do the map literal and the map key type (i.e. enum type) live in the same package? - checkUnexported := samePkg // we want to include unexported members in the exhaustiveness check only if we're in the same package - checklist := makeChecklist(members, keyPkg, checkUnexported, cfg.ignoreEnumMembers) + var checkl checklist + checkl.ignore(cfg.ignoreEnumMembers) - for _, e := range lit.Elts { - expr, ok := e.(*ast.KeyValueExpr) - if !ok { - continue // is it possible for valid map literal? - } - analyzeCaseClauseExpr(expr.Key, pass.TypesInfo, checklist.found) + for _, e := range es { + checkl.add(e.et, e.em, pass.Pkg == e.et.Pkg()) } - if len(checklist.remaining()) == 0 { - // All enum members accounted for. - // Nothing to report. + analyzeMapLiteral(lit, pass.TypesInfo, checkl.found) + if len(checkl.remaining()) == 0 { return true, resultEnumMembersAccounted } - - pass.Report(makeMapDiagnostic(lit, samePkg, enumTyp, members, checklist.remaining())) + pass.Report(makeMapDiagnostic(lit, dedupEnumTypes(toEnumTypes(es)), checkl.remaining())) return true, resultReportedDiagnostic } } -func makeMapDiagnostic(lit *ast.CompositeLit, samePkg bool, enumTyp enumType, all enumMembers, missing map[string]struct{}) analysis.Diagnostic { - typeName := diagnosticEnumTypeName(enumTyp.TypeName, samePkg) - members := strings.Join(diagnosticMissingMembers(missing, all), ", ") +func analyzeMapLiteral(lit *ast.CompositeLit, info *types.Info, each func(constantValue)) { + for _, e := range lit.Elts { + expr, ok := e.(*ast.KeyValueExpr) + if !ok { + continue + } + if val, ok := exprConstVal(expr.Key, info); ok { + each(val) + } + } +} + +func makeMapDiagnostic(lit *ast.CompositeLit, enumTypes []enumType, missing map[member]struct{}) analysis.Diagnostic { return analysis.Diagnostic{ - Pos: lit.Pos(), - End: lit.End(), - Message: fmt.Sprintf("missing keys in map of key type %s: %s", typeName, members), + Pos: lit.Pos(), + End: lit.End(), + Message: fmt.Sprintf( + "missing keys in map of key type %s: %s", + diagnosticEnumTypes(enumTypes), + diagnosticGroups(groupMissing(missing, enumTypes)), + ), } } diff --git a/switch.go b/switch.go index 2441f69..dea9288 100644 --- a/switch.go +++ b/switch.go @@ -5,10 +5,8 @@ import ( "go/ast" "go/types" "regexp" - "strings" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/astutil" ) // nodeVisitor is like the visitor function used by Inspector.WithStack, @@ -19,27 +17,35 @@ import ( // that the nodeVisitor function took the expected code path. type nodeVisitor func(n ast.Node, push bool, stack []ast.Node) (proceed bool, result string) -// Result values returned by a node visitors. +// toVisitor converts the 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" - resultKeyNotNamed = "map key not named type" resultKeyNilPkg = "nil map key package" - resultKeyNotEnum = "map key not known enum type" + resultKeyNotEnum = "not all map key type terms are known enum types" resultNoSwitchTag = "no switch tag" resultTagNotValue = "switch tag not value type" - resultTagNotNamed = "switch tag not named type" resultTagNilPkg = "nil switch tag package" - resultTagNotEnum = "switch tag not known enum type" + 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 presence satisfies exhaustiveness" + resultDefaultCaseSuffices = "default case satisfies exhaustiveness" resultReportedDiagnostic = "reported diagnostic" + resultEnumTypes = "invalid or empty composing enum types" ) // switchChecker returns a node visitor that checks exhaustiveness of @@ -66,13 +72,14 @@ func switchChecker(pass *analysis.Pass, cfg switchConfig, generated boolCache, c switchComments := comments.get(pass.Fset, file)[sw] if !cfg.explicit && hasComment(switchComments, ignoreComment) { - // Skip checking of this switch statement due to ignore directive comment. - // Still return true because there may be nested switch statements - // that are not to be ignored. + // 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 && !hasComment(switchComments, enforceComment) { - // Skip checking of this switch statement due to missing enforce directive comment. + // Skip checking of this switch statement due to missing + // enforce comment. return true, resultNoEnforceComment } @@ -85,47 +92,43 @@ func switchChecker(pass *analysis.Pass, cfg switchConfig, generated boolCache, c return true, resultTagNotValue } - tagType, ok := t.Type.(*types.Named) - if !ok { - return true, resultTagNotNamed + es, ok := composingEnumTypes(pass, t.Type) + if !ok || len(es) == 0 { + return true, resultEnumTypes } - tagPkg := tagType.Obj().Pkg() - if tagPkg == nil { - // The Go documentation says: nil for labels and objects in the Universe scope. - // This happens for the `error` type, for example. - return true, resultTagNilPkg - } + var checkl checklist + checkl.ignore(cfg.ignoreEnumMembers) - enumTyp := enumType{tagType.Obj()} - members, ok := importFact(pass, enumTyp) - if !ok { - // switch tag's type is not a known enum type. - return true, resultTagNotEnum + for _, e := range es { + checkl.add(e.et, e.em, pass.Pkg == e.et.Pkg()) } - samePkg := tagPkg == pass.Pkg // do the switch statement and the switch tag type (i.e. enum type) live in the same package? - checkUnexported := samePkg // we want to include unexported members in the exhaustiveness check only if we're in the same package - checklist := makeChecklist(members, tagPkg, checkUnexported, cfg.ignoreEnumMembers) - - hasDefaultCase := analyzeSwitchClauses(sw, pass.TypesInfo, checklist.found) - - if len(checklist.remaining()) == 0 { + def := analyzeSwitchClauses(sw, pass.TypesInfo, checkl.found) + if len(checkl.remaining()) == 0 { // All enum members accounted for. // Nothing to report. return true, resultEnumMembersAccounted } - if hasDefaultCase && cfg.defaultSignifiesExhaustive { - // Though enum members are not accounted for, - // the existence of the default case signifies exhaustiveness. - // So don't report. + if def && 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, samePkg, enumTyp, members, checklist.remaining())) + pass.Report(makeSwitchDiagnostic(sw, dedupEnumTypes(toEnumTypes(es)), checkl.remaining())) return true, resultReportedDiagnostic } } +func toEnumTypes(es []typeAndMembers) []enumType { + out := make([]enumType, len(es)) + for i := range es { + out[i] = es[i].et + } + return out +} + // switchConfig is configuration for switchChecker. type switchConfig struct { explicit bool @@ -138,24 +141,12 @@ func isDefaultCase(c *ast.CaseClause) bool { return c.List == nil // see doc comment on List field } -func denotesPackage(ident *ast.Ident, info *types.Info) (*types.Package, bool) { - obj := info.ObjectOf(ident) - if obj == nil { - return nil, false - } - n, ok := obj.(*types.PkgName) - if !ok { - return nil, false - } - return n.Imported(), true -} - // analyzeSwitchClauses analyzes the clauses in the supplied switch -// statement. The info param typically is pass.TypesInfo. The found +// 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, found func(val constantValue)) (hasDefaultCase bool) { +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) { @@ -163,98 +154,22 @@ func analyzeSwitchClauses(sw *ast.SwitchStmt, info *types.Info, found func(val c continue } for _, expr := range caseCl.List { - analyzeCaseClauseExpr(expr, info, found) + if val, ok := exprConstVal(expr, info); ok { + each(val) + } } } return hasDefaultCase } -func analyzeCaseClauseExpr(e ast.Expr, info *types.Info, found func(val constantValue)) { - handleIdent := func(ident *ast.Ident) { - obj := info.Uses[ident] - if obj == nil { - return - } - if _, ok := obj.(*types.Const); !ok { - return - } - - // There are two scenarios. - // See related test cases in typealias/quux/quux.go. - // - // ## Scenario 1 - // - // Tag package and constant package are the same. This is - // simple; we just use fs.ModeDir's value. - // - // Example: - // - // var mode fs.FileMode - // switch mode { - // case fs.ModeDir: - // } - // - // ## Scenario 2 - // - // Tag package and constant package are different. In this - // scenario, too, we accept the case clause expr constant value, - // as is. If the Go type checker is okay with the name being - // listed in the case clause, we don't care much further. - // - // Example: - // - // var mode fs.FileMode - // switch mode { - // case os.ModeDir: - // } - // - // Or equivalently: - // - // // The type of mode is effectively fs.FileMode, - // // due to type alias. - // var mode os.FileMode - // switch mode { - // case os.ModeDir: - // } - found(determineConstVal(ident, info)) - } - - e = astutil.Unparen(e) - switch e := e.(type) { - case *ast.Ident: - handleIdent(e) - - case *ast.SelectorExpr: - x := astutil.Unparen(e.X) - // Ensure we only see the form pkg.Const, and not e.g. - // structVal.f or structVal.inner.f. - // - // For this purpose, first we check that X, which is everything - // except the rightmost field selector *ast.Ident (the Sel - // field), is also an *ast.Ident. - xIdent, ok := x.(*ast.Ident) - if !ok { - return - } - // Second , check that it's a package. It doesn't matter which - // package, just that it denotes some package. - if _, ok := denotesPackage(xIdent, info); !ok { - return - } - handleIdent(e.Sel) - } -} - -// Makes a diagnostic for a non-exhaustive switch statement. samePkg -// should be true if the enum type and the switch statement are defined -// in the same package. -func makeSwitchDiagnostic(sw *ast.SwitchStmt, samePkg bool, enumTyp enumType, all enumMembers, missing map[string]struct{}) analysis.Diagnostic { - typeName := diagnosticEnumTypeName(enumTyp.TypeName, samePkg) - members := strings.Join(diagnosticMissingMembers(missing, all), ", ") - +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", typeName, members), + Pos: sw.Pos(), + End: sw.End(), + Message: fmt.Sprintf( + "missing cases in switch of type %s: %s", + diagnosticEnumTypes(dedupEnumTypes(enumTypes)), + diagnosticGroups(groupMissing(missing, enumTypes)), + ), } } diff --git a/switch_test.go b/switch_test.go index 7b0aa6e..b978893 100644 --- a/switch_test.go +++ b/switch_test.go @@ -4,7 +4,6 @@ import ( "go/ast" "go/types" "reflect" - "regexp" "testing" "golang.org/x/tools/go/analysis" @@ -14,98 +13,8 @@ import ( // TODO(testing): write tests that assert on the "result" returned by // switchStmtChecker. -func TestDiagnosticEnumTypeName(t *testing.T) { - t.Run("same package", func(t *testing.T) { - tn := types.NewTypeName(50, types.NewPackage("example.org/enumpkg-go", "enumpkg"), "Biome", nil) - got := diagnosticEnumTypeName(tn, true) - want := "Biome" - if got != want { - t.Errorf("want %q, got %q", want, got) - } - }) - - t.Run("different package", func(t *testing.T) { - tn := types.NewTypeName(50, types.NewPackage("example.org/enumpkg-go", "enumpkg"), "Biome", nil) - got := diagnosticEnumTypeName(tn, false) - want := "enumpkg.Biome" - if got != want { - t.Errorf("want %q, got %q", want, got) - } - }) -} - -func TestDiagnosticMissingMembers(t *testing.T) { - em := enumMembers{ - Names: []string{"Ganga", "Yamuna", "Kaveri", "Unspecified"}, - NameToValue: map[string]constantValue{ - "Unspecified": "0", - "Ganga": "0", - "Kaveri": "1", - "Yamuna": "2", - }, - ValueToNames: map[constantValue][]string{ - "0": {"Unspecified", "Ganga"}, - "1": {"Kaveri"}, - "2": {"Yamuna"}, - }, - } - checkEnumMembersLiteral("River", em) - - t.Run("missing some: same-valued", func(t *testing.T) { - got := diagnosticMissingMembers(map[string]struct{}{"Ganga": {}, "Unspecified": {}, "Kaveri": {}}, em) - want := []string{"Ganga|Unspecified", "Kaveri"} - if !reflect.DeepEqual(want, got) { - t.Errorf("want %v, got %v", want, got) - } - }) - - t.Run("missing some: unique or unknown values", func(t *testing.T) { - got := diagnosticMissingMembers(map[string]struct{}{"Yamuna": {}, "Kaveri": {}}, em) - want := []string{"Yamuna", "Kaveri"} - if !reflect.DeepEqual(want, got) { - t.Errorf("want %v, got %v", want, got) - } - }) - - t.Run("missing none", func(t *testing.T) { - got := diagnosticMissingMembers(nil, em) - if len(got) != 0 { - t.Errorf("want zero elements, got %d", len(got)) - } - }) - - t.Run("missing all", func(t *testing.T) { - got := diagnosticMissingMembers(map[string]struct{}{"Ganga": {}, "Kaveri": {}, "Yamuna": {}, "Unspecified": {}}, em) - want := []string{"Ganga|Unspecified", "Yamuna", "Kaveri"} - if !reflect.DeepEqual(want, got) { - t.Errorf("want %v, got %v", want, got) - } - }) - - em = enumMembers{ - Names: []string{"X", "A", "Unspecified"}, - NameToValue: map[string]constantValue{ - "Unspecified": "0", - "X": "0", - "A": "1", - }, - ValueToNames: map[constantValue][]string{ - "0": {"Unspecified", "X"}, - "1": {"A"}, - }, - } - checkEnumMembersLiteral("whatever", em) - - t.Run("AST order", func(t *testing.T) { - got := diagnosticMissingMembers(map[string]struct{}{"Unspecified": {}, "X": {}, "A": {}}, em) - want := []string{"X|Unspecified", "A"} - if !reflect.DeepEqual(want, got) { - t.Errorf("want %v, got %v", want, got) - } - }) -} - -// This test mainly exists to ensure stability of the diagnostic message format. +// This test mainly exists to ensure stability of the diagnostic message +// format. func TestMakeSwitchDiagnostic(t *testing.T) { sw := &ast.SwitchStmt{ Switch: 1, @@ -114,30 +23,18 @@ func TestMakeSwitchDiagnostic(t *testing.T) { }, // other fields shouldn't matter } - samePkg := false tn := types.NewTypeName(50, types.NewPackage("example.org/enumpkg", "enumpkg"), "Biome", nil) - enumTyp := enumType{tn} - allMembers := enumMembers{ - Names: []string{"Tundra", "Savanna", "Desert"}, - NameToValue: map[string]constantValue{ - "Tundra": "1", - "Savanna": "2", - "Desert": "3", - }, - ValueToNames: map[constantValue][]string{ - "1": {"Tundra"}, - "2": {"Savanna"}, - "3": {"Desert"}, - }, + et := enumType{tn} + missing := map[member]struct{}{ + {102, et, "Savanna", "2"}: {}, + {109, et, "Desert", "3"}: {}, } - checkEnumMembersLiteral("Biome", allMembers) - missingMembers := map[string]struct{}{"Savanna": {}, "Desert": {}} - got := makeSwitchDiagnostic(sw, samePkg, enumTyp, allMembers, missingMembers) + got := makeSwitchDiagnostic(sw, []enumType{et}, missing) want := analysis.Diagnostic{ Pos: 1, End: 11, - Message: "missing cases in switch of type enumpkg.Biome: Savanna, Desert", + Message: "missing cases in switch of type enumpkg.Biome: enumpkg.Savanna, enumpkg.Desert", } if !reflect.DeepEqual(want, got) { t.Errorf("want %+v, got %+v", want, got) @@ -222,232 +119,3 @@ func TestAnalyzeSwitchClauses(t *testing.T) { }) } } - -func TestChecklist(t *testing.T) { - enumPkg := types.NewPackage("github.com/example/bar-go", "bar") - - em := enumMembers{ - Names: []string{"A", "B", "C", "D", "E", "F", "G"}, - NameToValue: map[string]constantValue{ - "A": "1", - "B": "2", - "C": "5", - "D": "2", - "E": "3", - "F": "2", - "G": "4", - }, - ValueToNames: map[constantValue][]string{ - "1": {"A"}, - "2": {"B", "D", "F"}, - "3": {"E"}, - "4": {"G"}, - "5": {"C"}, - }, - } - checkEnumMembersLiteral("TestChecklist", em) - - checkRemaining := func(t *testing.T, h *checklist, want map[string]struct{}) { - t.Helper() - rem := h.remaining() - if !reflect.DeepEqual(want, rem) { - t.Errorf("want %+v, got %+v", want, rem) - } - } - - t.Run("main operations", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, nil) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - - checklist.found(`1`) - checkRemaining(t, checklist, map[string]struct{}{ - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - - checklist.found(`2`) - checkRemaining(t, checklist, map[string]struct{}{ - "C": {}, - "E": {}, - "G": {}, - }) - - // repeated call should be a no-op. - checklist.found(`2`) - checkRemaining(t, checklist, map[string]struct{}{ - "C": {}, - "E": {}, - "G": {}, - }) - - checklist.found(`2`) - checkRemaining(t, checklist, map[string]struct{}{ - "C": {}, - "E": {}, - "G": {}, - }) - - checklist.found(`5`) - checkRemaining(t, checklist, map[string]struct{}{ - "E": {}, - "G": {}, - }) - - // unknown value - checklist.found(`100000`) - checkRemaining(t, checklist, map[string]struct{}{ - "E": {}, - "G": {}, - }) - - checklist.found(`3`) - checkRemaining(t, checklist, map[string]struct{}{ - "G": {}, - }) - }) - - t.Run("ignore regexp", func(t *testing.T) { - t.Run("nil means no filtering", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, nil) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - }) - - t.Run("basic", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, regexp.MustCompile(`^github.com/example/bar-go.G$`)) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - }) - }) - - t.Run("matches multiple", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, regexp.MustCompile(`^github.com/example/bar-go`)) - checkRemaining(t, checklist, map[string]struct{}{}) - }) - - t.Run("uses package path, not package name", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, regexp.MustCompile(`bar.G`)) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - }) - }) - - t.Run("blank identifier", func(t *testing.T) { - em := enumMembers{ - Names: []string{"A", "B", "C", "D", "E", "F", "G", "_"}, - NameToValue: map[string]constantValue{ - "A": "1", - "B": "2", - "C": "5", - "D": "2", - "E": "3", - "F": "2", - "G": "4", - "_": "0", - }, - ValueToNames: map[constantValue][]string{ - "0": {"_"}, - "1": {"A"}, - "2": {"B", "D", "F"}, - "3": {"E"}, - "4": {"G"}, - "5": {"C"}, - }, - } - checkEnumMembersLiteral("TestChecklist blank identifier", em) - - checklist := makeChecklist(em, enumPkg, true, nil) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - }) - - t.Run("unexported", func(t *testing.T) { - em := enumMembers{ - Names: []string{"A", "B", "C", "D", "E", "F", "G", "lowercase"}, - NameToValue: map[string]constantValue{ - "A": "1", - "B": "2", - "C": "5", - "D": "2", - "E": "3", - "F": "2", - "G": "4", - "lowercase": "42", - }, - ValueToNames: map[constantValue][]string{ - "1": {"A"}, - "2": {"B", "D", "F"}, - "3": {"E"}, - "4": {"G"}, - "5": {"C"}, - "42": {"lowercase"}, - }, - } - checkEnumMembersLiteral("TestChecklist lowercase", em) - - t.Run("include", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, true, nil) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - "lowercase": {}, - }) - }) - - t.Run("don't include", func(t *testing.T) { - checklist := makeChecklist(em, enumPkg, false, nil) - checkRemaining(t, checklist, map[string]struct{}{ - "A": {}, - "B": {}, - "C": {}, - "D": {}, - "E": {}, - "F": {}, - "G": {}, - }) - }) - }) -} diff --git a/testdata/src/default-signifies-exhaustive/default-absent/default_absent.go b/testdata/src/default-signifies-exhaustive/default-absent/default_absent.go index 0c4169e..8884d7d 100644 --- a/testdata/src/default-signifies-exhaustive/default-absent/default_absent.go +++ b/testdata/src/default-signifies-exhaustive/default-absent/default_absent.go @@ -1,9 +1,9 @@ package absent -import dse "default-signifies-exhaustive" +import "default-signifies-exhaustive" func _a(t dse.T) { - switch t { // want "^missing cases in switch of type dse.T: B$" + switch t { // want "^missing cases in switch of type dse.T: dse.B$" case dse.A: } } diff --git a/testdata/src/default-signifies-exhaustive/default-present/default_present.go b/testdata/src/default-signifies-exhaustive/default-present/default_present.go index cd0b27c..749f377 100644 --- a/testdata/src/default-signifies-exhaustive/default-present/default_present.go +++ b/testdata/src/default-signifies-exhaustive/default-present/default_present.go @@ -1,6 +1,6 @@ package present -import dse "default-signifies-exhaustive" +import "default-signifies-exhaustive" func _a(t dse.T) { // expect no diagnostics, since default case is present, diff --git a/testdata/src/direction.go b/testdata/src/direction.go deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/src/duplicate-enum-value/duplicate_enum_value.go b/testdata/src/duplicate-enum-value/duplicate_enum_value.go index 22f607f..baf48da 100644 --- a/testdata/src/duplicate-enum-value/duplicate_enum_value.go +++ b/testdata/src/duplicate-enum-value/duplicate_enum_value.go @@ -31,13 +31,13 @@ const ( ) func _s(c Chart) { - switch c { // want "^missing cases in switch of type Chart: Pie\\|circle$" + switch c { // want `^missing cases in switch of type duplicateenumvalue.Chart: duplicateenumvalue.Pie\|duplicateenumvalue.circle$` case Line: case Sunburst: case Area: } - _ = map[Chart]int{ // want "^missing keys in map of key type Chart: Pie\\|circle$" + _ = map[Chart]int{ // want `^missing keys in map of key type duplicateenumvalue.Chart: duplicateenumvalue.Pie\|duplicateenumvalue.circle$` Line: 1, Sunburst: 2, Area: 3, diff --git a/testdata/src/duplicate-enum-value/otherpkg/otherpkg.go b/testdata/src/duplicate-enum-value/otherpkg/otherpkg.go index d532d1a..392b24f 100644 --- a/testdata/src/duplicate-enum-value/otherpkg/otherpkg.go +++ b/testdata/src/duplicate-enum-value/otherpkg/otherpkg.go @@ -53,32 +53,32 @@ func _r() { // missing. var r d.River - switch r { // want "^missing cases in switch of type duplicateenumvalue.River: DefaultRiver\\|Ganga, Kaveri$" + switch r { // want `^missing cases in switch of type duplicateenumvalue.River: duplicateenumvalue.DefaultRiver\|duplicateenumvalue.Ganga, duplicateenumvalue.Kaveri$` case d.Yamuna: } var s d.State - switch s { // want "^missing cases in switch of type duplicateenumvalue.State: TamilNadu\\|DefaultState, Kerala$" + switch s { // want `^missing cases in switch of type duplicateenumvalue.State: duplicateenumvalue.TamilNadu\|duplicateenumvalue.DefaultState, duplicateenumvalue.Kerala$` case d.Karnataka: } - _ = map[d.River]int{ // want "^missing keys in map of key type duplicateenumvalue.River: DefaultRiver\\|Ganga, Kaveri$" + _ = map[d.River]int{ // want `^missing keys in map of key type duplicateenumvalue.River: duplicateenumvalue.DefaultRiver\|duplicateenumvalue.Ganga, duplicateenumvalue.Kaveri$` d.Yamuna: 1, } - _ = map[d.State]int{ // want "^missing keys in map of key type duplicateenumvalue.State: TamilNadu\\|DefaultState, Kerala$" + _ = map[d.State]int{ // want `^missing keys in map of key type duplicateenumvalue.State: duplicateenumvalue.TamilNadu\|duplicateenumvalue.DefaultState, duplicateenumvalue.Kerala$` d.Karnataka: 1, } } func _s(c d.Chart) { - switch c { // want "^missing cases in switch of type duplicateenumvalue.Chart: Pie$" + switch c { // want "^missing cases in switch of type duplicateenumvalue.Chart: duplicateenumvalue.Pie$" case d.Line: case d.Sunburst: case d.Area: } - _ = map[d.Chart]int{ // want "^missing keys in map of key type duplicateenumvalue.Chart: Pie$" + _ = map[d.Chart]int{ // want "^missing keys in map of key type duplicateenumvalue.Chart: duplicateenumvalue.Pie$" d.Line: 1, d.Sunburst: 2, d.Area: 3, diff --git a/testdata/src/enforce-comment/enforce_comment_map.go b/testdata/src/enforce-comment/enforce_comment_map.go index c24b002..a443577 100644 --- a/testdata/src/enforce-comment/enforce_comment_map.go +++ b/testdata/src/enforce-comment/enforce_comment_map.go @@ -7,29 +7,29 @@ var _ = map[Direction]int{ } //exhaustive:enforce -var _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" +var _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } //exhaustive:enforce var ( - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } - _ = &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = &map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[N] - _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }) ) var ( //exhaustive:enforce - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } _ = &map[Direction]int{ @@ -46,13 +46,13 @@ func returnMap() map[Direction]int { // some other comment //exhaustive:enforce // some other comment - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } case 2: //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } @@ -85,13 +85,13 @@ func returnValueFromMap(d Direction) int { // some other comment //exhaustive:enforce // some other comment - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[d] case 2: //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[d] @@ -124,13 +124,13 @@ func returnFuncCallWithMap() error { // some other comment //exhaustive:enforce // some other comment - return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }) case 2: //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }) @@ -163,13 +163,13 @@ func returnPointerToMap() *map[Direction]int { // some other comment //exhaustive:enforce // some other comment - return &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return &map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } case 2: //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - return &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return &map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } @@ -200,22 +200,22 @@ func assignMapLiteral() { // some other comment //exhaustive:enforce // some other comment - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } //exhaustive:enforce - a := map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + a := map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } //exhaustive:enforce - b, ok := map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + b, ok := map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }, 10 @@ -243,22 +243,22 @@ func assignValueFromMapLiteral(d Direction) { // some other comment //exhaustive:enforce // some other comment - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[d] //exhaustive:enforce ... more arbitrary comment content (e.g. an explanation) ... - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[d] //exhaustive:enforce - a := map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + a := map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[N] //exhaustive:enforce - b, ok := map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + b, ok := map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[N] @@ -289,29 +289,29 @@ func localVarDeclaration() { } //exhaustive:enforce - var _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + var _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } //exhaustive:enforce var ( - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } - _ = &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = &map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }[N] - _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, }) ) var ( //exhaustive:enforce - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type enforcecomment.Direction: enforcecomment.E, enforcecomment.S, enforcecomment.W, enforcecomment.directionInvalid$" N: 1, } _ = &map[Direction]int{ diff --git a/testdata/src/enforce-comment/enforce_comment_switch.go b/testdata/src/enforce-comment/enforce_comment_switch.go index e444d1c..5360c7f 100644 --- a/testdata/src/enforce-comment/enforce_comment_switch.go +++ b/testdata/src/enforce-comment/enforce_comment_switch.go @@ -14,7 +14,7 @@ func _a() { // some other comment //exhaustive:enforce // some other comment - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type enforcecomment.Direction: enforcecomment.E, enforcecomment.directionInvalid$" case N: case S: case W: @@ -35,7 +35,7 @@ func _b() { // this should report //exhaustive:enforce - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type enforcecomment.Direction: enforcecomment.E, enforcecomment.directionInvalid$" case N: case S: case W: @@ -54,7 +54,7 @@ func _nested() { default: // this should report. //exhaustive:enforce - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type enforcecomment.Direction: enforcecomment.E, enforcecomment.directionInvalid$" case N: case S: case W: @@ -68,7 +68,7 @@ func _reverse_nested() { // this should report. //exhaustive:enforce - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type enforcecomment.Direction: enforcecomment.E, enforcecomment.directionInvalid$" case N: case S: case W: diff --git a/testdata/src/general/dotimport/dotimport.go b/testdata/src/general/dotimport/dotimport.go index 418c5df..7f1ffdc 100644 --- a/testdata/src/general/dotimport/dotimport.go +++ b/testdata/src/general/dotimport/dotimport.go @@ -8,23 +8,23 @@ import ( func _dot() { var p Phylum - switch p { // want "^missing cases in switch of type bar.Phylum: Chordata, Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Chordata, bar.Mollusca$" case Echinodermata: } - _ = map[Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Chordata, Mollusca$" + _ = map[Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Chordata, bar.Mollusca$" Echinodermata: 1, } } func _mixed() { var p bar.Phylum - switch p { // want "^missing cases in switch of type bar.Phylum: Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Mollusca$" case Echinodermata: case barpkg.Chordata: } - _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Mollusca$" + _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Mollusca$" Echinodermata: 1, barpkg.Chordata: 2, } diff --git a/testdata/src/general/x/general.go b/testdata/src/general/x/general.go index fa7292d..70c7571 100644 --- a/testdata/src/general/x/general.go +++ b/testdata/src/general/x/general.go @@ -8,7 +8,6 @@ import ( "fmt" bar "general/y" barpkg "general/y" - "io/fs" "net/http" "os" "reflect" @@ -43,14 +42,14 @@ func _a() { // check since enum is in same package. var d Direction - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type x.Direction: x.E, x.directionInvalid$" case N: case S: case W: default: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type x.Direction: x.E, x.directionInvalid$" N: 1, S: 2, W: 3, @@ -64,12 +63,12 @@ func _b() { // check since enum is in external package. var p bar.Phylum - switch p { // want "^missing cases in switch of type bar.Phylum: Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Mollusca$" case bar.Chordata: case bar.Echinodermata: } - _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Mollusca$" + _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Mollusca$" bar.Chordata: 1, bar.Echinodermata: 2, } @@ -79,12 +78,12 @@ func _j() { // Named imports still report real package name. var p barpkg.Phylum - switch p { // want "^missing cases in switch of type bar.Phylum: Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Mollusca$" case barpkg.Chordata: case barpkg.Echinodermata: } - _ = map[barpkg.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Mollusca$" + _ = map[barpkg.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Mollusca$" barpkg.Chordata: 1, barpkg.Echinodermata: 2, } @@ -94,7 +93,7 @@ func _f() { // Multiple values in single case. var d Direction - switch d { // want "^missing cases in switch of type Direction: W$" + switch d { // want "^missing cases in switch of type x.Direction: x.W$" case E, directionInvalid, S: default: case N: @@ -106,16 +105,16 @@ func _g() { var d Direction if true { - switch d { // want "^missing cases in switch of type Direction: S, directionInvalid$" + switch d { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" case (N): case (E): case (W): } } - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type x.Direction: x.E, x.S, x.W, x.directionInvalid$" case N: - switch d { // want "^missing cases in switch of type Direction: N, S, W$" + switch d { // want "^missing cases in switch of type x.Direction: x.N, x.S, x.W$" case E, directionInvalid: } } @@ -147,13 +146,13 @@ func _o() { var p bar.Phylum var h holdsPhylum - switch p { // want "^missing cases in switch of type bar.Phylum: Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Mollusca$" case bar.Chordata: case bar.Echinodermata: case h.Mollusca: } - _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Mollusca$" + _ = map[bar.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Mollusca$" bar.Chordata: 1, bar.Echinodermata: 2, h.Mollusca: 3, @@ -179,47 +178,11 @@ func _p() { } } -func _q() { - // Type alias: - // type os.FileMode = fs.FileMode - // - // Of interest, note that e.g. listing os.ModeSocket in a case clause is - // equivalent to listing fs.ModeSocket (both have the same constant value). - - fi, err := os.Lstat(".") - fmt.Println(err) - - switch fi.Mode() { // want "^missing cases in switch of type fs.FileMode: ModeDevice, ModeSetuid, ModeSetgid, ModeType, ModePerm$" - case os.ModeDir: - case os.ModeAppend: - case os.ModeExclusive: - case fs.ModeTemporary: - case fs.ModeSymlink: - case fs.ModeNamedPipe, os.ModeSocket: - case fs.ModeCharDevice: - case fs.ModeSticky: - case fs.ModeIrregular: - } - - _ = map[fs.FileMode]int{ // want "^missing keys in map of key type fs.FileMode: ModeDevice, ModeSetuid, ModeSetgid, ModeType, ModePerm$" - os.ModeDir: 1, - os.ModeAppend: 2, - os.ModeExclusive: 3, - fs.ModeTemporary: 4, - fs.ModeSymlink: 5, - fs.ModeNamedPipe: 6, - os.ModeSocket: 7, - fs.ModeCharDevice: 8, - fs.ModeSticky: 9, - fs.ModeIrregular: 10, - } -} - func _r(d Direction) { // Raw constants (i.e. not identifier or sel expr) // in case clauses do not count. - switch d { // want "^missing cases in switch of type Direction: S, directionInvalid$" + switch d { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" case N: case E: case 3: @@ -227,7 +190,7 @@ func _r(d Direction) { case 5: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: S, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type x.Direction: x.S, x.directionInvalid$" N: 1, E: 2, 3: 3, @@ -246,10 +209,23 @@ func _s(u bar.Uppercase) { } } +func _ufunc0() Direction { return directionInvalid } +func _ufunc1(d Direction) Direction { return d } + +func _u() { + switch _ufunc0() { // want "^missing cases in switch of type x.Direction: x.W, x.directionInvalid$" + case N, E, S: + } + + switch _ufunc1(Direction(0)) { // want "^missing cases in switch of type x.Direction: x.W, x.directionInvalid$" + case N, E, S: + } +} + func mapTypeAlias() { type myMapAlias = map[Direction]int - _ = myMapAlias{ // want "^missing keys in map of key type Direction: S, directionInvalid$" + _ = myMapAlias{ // want "^missing keys in map of key type x.Direction: x.S, x.directionInvalid$" N: 1, E: 2, W: 4, @@ -259,9 +235,17 @@ func mapTypeAlias() { func customMapType() { type myMap map[Direction]int - _ = myMap{ // want "^missing keys in map of key type Direction: S, directionInvalid$" + _ = myMap{ // want "^missing keys in map of key type x.Direction: x.S, x.directionInvalid$" N: 1, E: 2, W: 4, } } + +func mapKeyFuncCall() { + _ = map[Direction]int{ // want "^missing keys in map of key type x.Direction: x.N, x.E, x.W, x.directionInvalid$" + _ufunc0(): 1, + _ufunc1(Direction(100)): 1, + S: 1, + } +} diff --git a/testdata/src/general/x/general_go116.go b/testdata/src/general/x/general_go116.go new file mode 100644 index 0000000..2424c1f --- /dev/null +++ b/testdata/src/general/x/general_go116.go @@ -0,0 +1,46 @@ +//go:build go1.16 +// +build go1.16 + +package x + +import ( + "fmt" + "io/fs" + "os" +) + +func _q() { + // Type alias: + // type os.FileMode = fs.FileMode + // + // Of interest, note that e.g. listing os.ModeSocket in a case clause is + // equivalent to listing fs.ModeSocket (both have the same constant value). + + fi, err := os.Lstat(".") + fmt.Println(err) + + switch fi.Mode() { // want "^missing cases in switch of type fs.FileMode: fs.ModeDevice, fs.ModeSetuid, fs.ModeSetgid, fs.ModeType, fs.ModePerm$" + case os.ModeDir: + case os.ModeAppend: + case os.ModeExclusive: + case fs.ModeTemporary: + case fs.ModeSymlink: + case fs.ModeNamedPipe, os.ModeSocket: + case fs.ModeCharDevice: + case fs.ModeSticky: + case fs.ModeIrregular: + } + + _ = map[fs.FileMode]int{ // want "^missing keys in map of key type fs.FileMode: fs.ModeDevice, fs.ModeSetuid, fs.ModeSetgid, fs.ModeType, fs.ModePerm$" + os.ModeDir: 1, + os.ModeAppend: 2, + os.ModeExclusive: 3, + fs.ModeTemporary: 4, + fs.ModeSymlink: 5, + fs.ModeNamedPipe: 6, + os.ModeSocket: 7, + fs.ModeCharDevice: 8, + fs.ModeSticky: 9, + fs.ModeIrregular: 10, + } +} diff --git a/testdata/src/general/x/irrelevant.go b/testdata/src/general/x/irrelevant.go index c8317f3..15cb937 100644 --- a/testdata/src/general/x/irrelevant.go +++ b/testdata/src/general/x/irrelevant.go @@ -47,11 +47,5 @@ func _e() { } func emptyMapShouldBeIgnored() { - // because it can be used instead of make(...) - _ = map[barpkg.Phylum]int{} - - _ = map[barpkg.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Echinodermata, Mollusca$" - barpkg.Chordata: 1, - } } diff --git a/testdata/src/general/x/is_exhaustive.go b/testdata/src/general/x/is_exhaustive.go index bed604e..d6b0cd1 100644 --- a/testdata/src/general/x/is_exhaustive.go +++ b/testdata/src/general/x/is_exhaustive.go @@ -2,7 +2,7 @@ package x import bar "general/y" -// These switches are exhaustive, expect no diagnostics. +// These are exhaustive, expect no diagnostics. func _l() { var d Direction diff --git a/testdata/src/general/x/paren.go b/testdata/src/general/x/paren.go index 8306558..b7ffdc5 100644 --- a/testdata/src/general/x/paren.go +++ b/testdata/src/general/x/paren.go @@ -2,20 +2,20 @@ package x func _k(d Direction) { // Parenthesized values in case statements. - switch d { // want "^missing cases in switch of type Direction: S, directionInvalid$" + switch d { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" case (N): case (E): case (((W))): } // Parenthesized values in switch tag. - switch ((d)) { // want "^missing cases in switch of type Direction: S, directionInvalid$" + switch ((d)) { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" case N: case E: case W: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: S, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type x.Direction: x.S, x.directionInvalid$" (N): 1, (E): 2, (((W))): 3, diff --git a/testdata/src/general/x/typeconv.go b/testdata/src/general/x/typeconv.go new file mode 100644 index 0000000..74cfa75 --- /dev/null +++ b/testdata/src/general/x/typeconv.go @@ -0,0 +1,33 @@ +package x + +func _t(d Direction) { + switch d { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" + case Direction(N): + case Direction(int(E)): + case W: + } + + switch Direction(int(d)) { // want "^missing cases in switch of type x.Direction: x.S, x.directionInvalid$" + case N: + case Direction(Direction(E)): + case Direction(W): + } + + switch Direction(int(d)) { // want "^missing cases in switch of type x.Direction: x.N, x.S, x.W, x.directionInvalid$" + case (_tt{}).methodCallMe(N): + case Direction(E): + case callMe(W): + } + + var _ = map[Direction]struct{}{ // want "^missing keys in map of key type x.Direction: x.N, x.S, x.W, x.directionInvalid$" + (_tt{}).methodCallMe(N): struct{}{}, + Direction(E): struct{}{}, + callMe(W): struct{}{}, + } +} + +func callMe(d Direction) Direction { return d } + +type _tt struct{} + +func (_tt) methodCallMe(d Direction) Direction { return d } diff --git a/testdata/src/general/y/y.go b/testdata/src/general/y/y.go index 93ce213..ff004a7 100644 --- a/testdata/src/general/y/y.go +++ b/testdata/src/general/y/y.go @@ -1,6 +1,6 @@ package bar -type Phylum int // want Phylum:"^Chordata,Echinodermata,Mollusca,platyhelminthes$" +type Phylum uint8 // want Phylum:"^Chordata,Echinodermata,Mollusca,platyhelminthes$" const ( Chordata Phylum = iota diff --git a/testdata/src/generated-file/check-generated-off/not_generated_1.go b/testdata/src/generated-file/check-generated-off/not_generated_1.go index b285dd0..21916a9 100644 --- a/testdata/src/generated-file/check-generated-off/not_generated_1.go +++ b/testdata/src/generated-file/check-generated-off/not_generated_1.go @@ -4,11 +4,11 @@ package generated func _1() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-off/not_generated_2.go b/testdata/src/generated-file/check-generated-off/not_generated_2.go index 6ba9e4a..df21fdb 100644 --- a/testdata/src/generated-file/check-generated-off/not_generated_2.go +++ b/testdata/src/generated-file/check-generated-off/not_generated_2.go @@ -4,11 +4,11 @@ package generated func _2() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-off/not_generated_doc_2.go b/testdata/src/generated-file/check-generated-off/not_generated_doc_2.go index 2c33dfc..860a15e 100644 --- a/testdata/src/generated-file/check-generated-off/not_generated_doc_2.go +++ b/testdata/src/generated-file/check-generated-off/not_generated_doc_2.go @@ -3,11 +3,11 @@ package generated func _doc_2() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/generated_0.go b/testdata/src/generated-file/check-generated-on/generated_0.go index 35b99c0..1f1a5c9 100644 --- a/testdata/src/generated-file/check-generated-on/generated_0.go +++ b/testdata/src/generated-file/check-generated-on/generated_0.go @@ -4,11 +4,11 @@ package generated func _0() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/generated_3.go b/testdata/src/generated-file/check-generated-on/generated_3.go index c30a571..9d463de 100644 --- a/testdata/src/generated-file/check-generated-on/generated_3.go +++ b/testdata/src/generated-file/check-generated-on/generated_3.go @@ -6,11 +6,11 @@ package generated func _3() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/generated_4.go b/testdata/src/generated-file/check-generated-on/generated_4.go index ef14145..b91b8f8 100644 --- a/testdata/src/generated-file/check-generated-on/generated_4.go +++ b/testdata/src/generated-file/check-generated-on/generated_4.go @@ -8,11 +8,11 @@ package generated func _4() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/generated_doc_0.go b/testdata/src/generated-file/check-generated-on/generated_doc_0.go index c125d0d..ea3a47b 100644 --- a/testdata/src/generated-file/check-generated-on/generated_doc_0.go +++ b/testdata/src/generated-file/check-generated-on/generated_doc_0.go @@ -7,11 +7,11 @@ package generated func _doc_0() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/generated_doc_1.go b/testdata/src/generated-file/check-generated-on/generated_doc_1.go index 0532f37..de3ca68 100644 --- a/testdata/src/generated-file/check-generated-on/generated_doc_1.go +++ b/testdata/src/generated-file/check-generated-on/generated_doc_1.go @@ -3,11 +3,11 @@ package generated func _doc_1() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/not_generated_1.go b/testdata/src/generated-file/check-generated-on/not_generated_1.go index b285dd0..21916a9 100644 --- a/testdata/src/generated-file/check-generated-on/not_generated_1.go +++ b/testdata/src/generated-file/check-generated-on/not_generated_1.go @@ -4,11 +4,11 @@ package generated func _1() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/not_generated_2.go b/testdata/src/generated-file/check-generated-on/not_generated_2.go index 6ba9e4a..df21fdb 100644 --- a/testdata/src/generated-file/check-generated-on/not_generated_2.go +++ b/testdata/src/generated-file/check-generated-on/not_generated_2.go @@ -4,11 +4,11 @@ package generated func _2() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/generated-file/check-generated-on/not_generated_doc_2.go b/testdata/src/generated-file/check-generated-on/not_generated_doc_2.go index 2c33dfc..860a15e 100644 --- a/testdata/src/generated-file/check-generated-on/not_generated_doc_2.go +++ b/testdata/src/generated-file/check-generated-on/not_generated_doc_2.go @@ -3,11 +3,11 @@ package generated func _doc_2() { var d Direction - switch d { // want "^missing cases in switch of type Direction: E, S, W, directionInvalid$" + switch d { // want "^missing cases in switch of type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" case N: } - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type generated.Direction: generated.E, generated.S, generated.W, generated.directionInvalid$" N: 1, } } diff --git a/testdata/src/ignore-comment/ignore_comment_map.go b/testdata/src/ignore-comment/ignore_comment_map.go index 40340f1..2fb14a9 100644 --- a/testdata/src/ignore-comment/ignore_comment_map.go +++ b/testdata/src/ignore-comment/ignore_comment_map.go @@ -2,7 +2,7 @@ package ignorecomment import "fmt" -var _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" +var _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } @@ -32,10 +32,10 @@ var ( _ = map[Direction]int{ N: 1, } - _ = &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = &map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } - _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }) ) @@ -57,21 +57,21 @@ func returnMap() map[Direction]int { } case 3: - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } case 4: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } case 5: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, } @@ -96,21 +96,21 @@ func returnValueFromMap(d Direction) int { }[d] case 3: - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }[d] case 4: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }[d] case 5: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, }[d] @@ -135,21 +135,21 @@ func returnFuncCallWithMap() error { }) case 3: - return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }) case 4: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return fmt.Errorf("%v", map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return fmt.Errorf("%v", map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }) case 5: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, }) @@ -174,21 +174,21 @@ func returnPointerToMap() *map[Direction]int { } case 3: - return &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return &map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } case 4: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return &map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return &map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } case 5: // this should report: according to go/ast, the comment is not considered to // be associated with the return node. - return &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + return &map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, } @@ -221,19 +221,19 @@ func assignMapLiteral() { _, _, _ = a, b, ok - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } // this should report: according to go/ast, the comment is not considered to // be associated with the assign node. - _ = map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } // this should report: according to go/ast, the comment is not considered to // be associated with the assign node. - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, } @@ -265,26 +265,26 @@ func assignValueFromMapLiteral(d Direction) { _, _, _ = a, b, ok // this should report. - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }[d] // this should report: according to go/ast, the comment is not considered to // be associated with the assign node. - _ = map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ //exhaustive:ignore // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }[d] // this should report: according to go/ast, the comment is not considered to // be associated with the assign node. - _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" //exhaustive:ignore N: 1, }[d] } func localVarDeclaration() { - var _ = map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + var _ = map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } @@ -314,10 +314,10 @@ func localVarDeclaration() { _ = map[Direction]int{ N: 1, } - _ = &map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = &map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, } - _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type Direction: E, S, W, directionInvalid$" + _ = fmt.Errorf("%v", map[Direction]int{ // want "^missing keys in map of key type ignorecomment.Direction: ignorecomment.E, ignorecomment.S, ignorecomment.W, ignorecomment.directionInvalid$" N: 1, }) ) diff --git a/testdata/src/ignore-comment/ignore_comment_switch.go b/testdata/src/ignore-comment/ignore_comment_switch.go index f0c2f73..02c2a87 100644 --- a/testdata/src/ignore-comment/ignore_comment_switch.go +++ b/testdata/src/ignore-comment/ignore_comment_switch.go @@ -15,7 +15,7 @@ func _a() { } // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -36,7 +36,7 @@ func _b() { } // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -49,7 +49,7 @@ func _c0() { // this should report: according to go/ast, the comment is not considered to // be associated with the switch statement node. - switch d { //exhaustive:ignore // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { //exhaustive:ignore // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -57,7 +57,7 @@ func _c0() { } // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -70,7 +70,7 @@ func _c1() { // this should report: according to go/ast, the comment is not considered to // be associated with the switch statement node. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" //exhaustive:ignore case N: case S: @@ -79,7 +79,7 @@ func _c1() { } // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -89,7 +89,7 @@ func _c1() { func _d() { // this should report. - switch (func() Direction { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch (func() Direction { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" // this should not report. var x Direction //exhaustive:ignore @@ -107,7 +107,7 @@ func _d() { var d Direction // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -126,7 +126,7 @@ func _nested() { case W: default: // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: @@ -139,7 +139,7 @@ func _reverse_nested() { var d Direction // this should report. - switch d { // want "^missing cases in switch of type Direction: E, directionInvalid$" + switch d { // want "^missing cases in switch of type ignorecomment.Direction: ignorecomment.E, ignorecomment.directionInvalid$" case N: case S: case W: diff --git a/testdata/src/ignore-enum-member/ignore_enum_member.go b/testdata/src/ignore-enum-member/ignore_enum_member.go index 30501e5..7571dd0 100644 --- a/testdata/src/ignore-enum-member/ignore_enum_member.go +++ b/testdata/src/ignore-enum-member/ignore_enum_member.go @@ -12,22 +12,22 @@ const ( func _a() { var e Exchange - switch e { // want "^missing cases in switch of type Exchange: Exchange_EXCHANGE_BINANCE$" + switch e { // want "^missing cases in switch of type ignoreenummember.Exchange: ignoreenummember.Exchange_EXCHANGE_BINANCE$" case Exchange_EXCHANGE_BITMEX: } - _ = map[Exchange]int{ // want "^missing keys in map of key type Exchange: Exchange_EXCHANGE_BINANCE$" + _ = map[Exchange]int{ // want "^missing keys in map of key type ignoreenummember.Exchange: ignoreenummember.Exchange_EXCHANGE_BINANCE$" Exchange_EXCHANGE_BITMEX: 1, } } func _b() { var p barpkg.Phylum - switch p { // want "^missing cases in switch of type bar.Phylum: Mollusca$" + switch p { // want "^missing cases in switch of type bar.Phylum: bar.Mollusca$" case barpkg.Chordata: } - _ = map[barpkg.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: Mollusca$" + _ = map[barpkg.Phylum]int{ // want "^missing keys in map of key type bar.Phylum: bar.Mollusca$" barpkg.Chordata: 1, } } diff --git a/testdata/src/ignore-enum-member/same_value.go b/testdata/src/ignore-enum-member/same_value.go index 6a4d4ba..3669e09 100644 --- a/testdata/src/ignore-enum-member/same_value.go +++ b/testdata/src/ignore-enum-member/same_value.go @@ -13,10 +13,10 @@ const ( // The member Standard, though it has the same constant value as User, must // still be reported in the diagnostic. func _c(a Access) { - switch a { // want "^missing cases in switch of type Access: Standard, Group$" + switch a { // want "^missing cases in switch of type ignoreenummember.Access: ignoreenummember.Standard, ignoreenummember.Group$" } - _ = map[Access]int{ // want "^missing keys in map of key type Access: Standard, Group$" + _ = map[Access]int{ // want "^missing keys in map of key type ignoreenummember.Access: ignoreenummember.Standard, ignoreenummember.Group$" 0: 0, } } diff --git a/testdata/src/scope/allscope/allscope.go b/testdata/src/scope/allscope/allscope.go index 6c2b9da..6397271 100644 --- a/testdata/src/scope/allscope/allscope.go +++ b/testdata/src/scope/allscope/allscope.go @@ -28,7 +28,7 @@ func _a() { case C, D: } - switch t { // want "^missing cases in switch of type T: D$" + switch t { // want "^missing cases in switch of type allscope.T: allscope.D$" case C: } @@ -45,7 +45,7 @@ func _a() { case X, Y: } - switch q { // want "^missing cases in switch of type Q: Y$" + switch q { // want "^missing cases in switch of type allscope.Q: allscope.Y$" case X: } } @@ -64,7 +64,7 @@ func _b() { D: 2, } - _ = map[T]int{ // want "^missing keys in map of key type T: D$" + _ = map[T]int{ // want "^missing keys in map of key type allscope.T: allscope.D$" C: 1, } @@ -81,7 +81,7 @@ func _b() { Y: 2, } - _ = map[Q]int{ // want "^missing keys in map of key type Q: Y$" + _ = map[Q]int{ // want "^missing keys in map of key type allscope.Q: allscope.Y$" X: 1, } } diff --git a/testdata/src/typealias/quux/quux.go b/testdata/src/typealias/quux/quux.go index bfcfd57..9575956 100644 --- a/testdata/src/typealias/quux/quux.go +++ b/testdata/src/typealias/quux/quux.go @@ -7,7 +7,7 @@ import ( func x() { var v foo.T1 = foo.ReturnsT1() - switch v { // want "^missing cases in switch of type bar.T2: D, E$" + switch v { // want "^missing cases in switch of type bar.T2: bar.D, bar.E$" case foo.A: case bar.B: case foo.C: @@ -17,7 +17,7 @@ func x() { } var w bar.T2 = foo.ReturnsT1() - switch w { // want "^missing cases in switch of type bar.T2: D, E$" + switch w { // want "^missing cases in switch of type bar.T2: bar.D, bar.E$" case foo.A: case bar.B: case foo.C: @@ -26,7 +26,7 @@ func x() { case foo.H: } - _ = map[foo.T1]int{ // want "^missing keys in map of key type bar.T2: D, E$" + _ = map[foo.T1]int{ // want "^missing keys in map of key type bar.T2: bar.D, bar.E$" foo.A: 1, bar.B: 2, foo.C: 3, @@ -35,7 +35,7 @@ func x() { foo.H: 6, } - _ = map[bar.T2]int{ // want "^missing keys in map of key type bar.T2: D, E$" + _ = map[bar.T2]int{ // want "^missing keys in map of key type bar.T2: bar.D, bar.E$" foo.A: 1, bar.B: 2, foo.C: 3, diff --git a/testdata/src/typealias/typealias.go b/testdata/src/typealias/typealias.go index 05515f9..99fa997 100644 --- a/testdata/src/typealias/typealias.go +++ b/testdata/src/typealias/typealias.go @@ -89,10 +89,10 @@ func _d() { } } func _e() { - switch t10() { // want "^missing cases in switch of type typealias.T11: T11_A, T11_B$" + switch t10() { // want "^missing cases in switch of type typealias.T11: typealias.T11_A, typealias.T11_B$" } - _ = map[typealias.T10]int{ // want "^missing keys in map of key type typealias.T11: T11_A, T11_B$" + _ = map[typealias.T10]int{ // want "^missing keys in map of key type typealias.T11: typealias.T11_A, typealias.T11_B$" 0: 0, } } @@ -105,10 +105,10 @@ func _f() { } } func _g() { - switch t15() { // want "^missing cases in switch of type typealias.T11: T11_A, T11_B$" + switch t15() { // want "^missing cases in switch of type typealias.T11: typealias.T11_A, typealias.T11_B$" } - _ = map[typealias.T15]int{ // want "^missing keys in map of key type typealias.T11: T11_A, T11_B$" + _ = map[typealias.T15]int{ // want "^missing keys in map of key type typealias.T11: typealias.T11_A, typealias.T11_B$" 0: 0, } } @@ -146,10 +146,10 @@ func _h() { } } func _i() { - switch t13() { // want "^missing cases in switch of type otherpkg.T11: T11_A, T11_B, T11_C$" + switch t13() { // want "^missing cases in switch of type otherpkg.T11: otherpkg.T11_A, otherpkg.T11_B, otherpkg.T11_C$" } - _ = map[typealias.T13]int{ // want "^missing keys in map of key type otherpkg.T11: T11_A, T11_B, T11_C$" + _ = map[typealias.T13]int{ // want "^missing keys in map of key type otherpkg.T11: otherpkg.T11_A, otherpkg.T11_B, otherpkg.T11_C$" 0: 0, } } @@ -162,10 +162,10 @@ func _j() { } } func _k() { - switch t17() { // want "^missing cases in switch of type otherpkg.T11: T11_A, T11_B, T11_C$" + switch t17() { // want "^missing cases in switch of type otherpkg.T11: otherpkg.T11_A, otherpkg.T11_B, otherpkg.T11_C$" } - _ = map[typealias.T17]int{ // want "^missing keys in map of key type otherpkg.T11: T11_A, T11_B, T11_C$" + _ = map[typealias.T17]int{ // want "^missing keys in map of key type otherpkg.T11: otherpkg.T11_A, otherpkg.T11_B, otherpkg.T11_C$" 0: 0, } } @@ -182,10 +182,10 @@ const ( func _l() { v := t18() - switch v { // want "^missing cases in switch of type anotherpkg.T1: T1_A$" + switch v { // want "^missing cases in switch of type anotherpkg.T1: anotherpkg.T1_A$" } - _ = map[typealias.T18]int{ // want "^missing keys in map of key type anotherpkg.T1: T1_A$" + _ = map[typealias.T18]int{ // want "^missing keys in map of key type anotherpkg.T1: anotherpkg.T1_A$" 0: 0, } } diff --git a/testdata/src/typeparam/typeparam.go b/testdata/src/typeparam/typeparam.go new file mode 100644 index 0000000..a585e96 --- /dev/null +++ b/testdata/src/typeparam/typeparam.go @@ -0,0 +1,184 @@ +//go:build go1.18 +// +build go1.18 + +package typeparam + +import ( + "fmt" + y "general/y" +) + +type M uint8 // want M:"^A,B$" +const ( + _ M = iota * 100 + A + B +) + +func (M) String() string { return "" } + +type N uint8 // want N:"^C,D$" +const ( + _ N = iota * 100 + C + D +) + +type O byte // want O:"^E1,E2$" +const ( + E1 O = 'h' + E2 O = 'e' +) + +type P float32 // want P:"^F$" +const ( + F P = 1.1234 +) + +type Q string // want Q:"^G$" +const ( + G Q = "world" +) + +type NotEnumType uint8 + +type II interface{ N | JJ } +type JJ interface{ O } +type KK interface { + M + fmt.Stringer + comparable +} +type LL interface { + M | NotEnumType + fmt.Stringer +} +type MM interface { + M +} +type QQ interface { + Q +} + +func _a[T y.Phylum | M](v T) { + switch v { // want `^missing cases in switch of type bar.Phylum\|typeparam.M: bar.Chordata, bar.Mollusca, typeparam.B$` + case T(A): + case T(y.Echinodermata): + } + + switch M(v) { // want "^missing cases in switch of type typeparam.M: typeparam.A, typeparam.B$" + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type bar.Phylum\|typeparam.M: bar.Chordata, bar.Mollusca, typeparam.B$` + T(A): struct{}{}, + T(y.Echinodermata): struct{}{}, + } +} + +func _b[T N | MM](v T) { + switch v { // want `^missing cases in switch of type typeparam.N\|typeparam.M: typeparam.D\|typeparam.B$` + case T(A): + } + + switch M(v) { // want "^missing cases in switch of type typeparam.M: typeparam.A, typeparam.B$" + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type typeparam.N\|typeparam.M: typeparam.D\|typeparam.B$` + T(A): struct{}{}, + } +} + +func _c[T O | M | N](v T) { + switch v { // want `^missing cases in switch of type typeparam.O\|typeparam.M\|typeparam.N: typeparam.E1, typeparam.E2, typeparam.A\|typeparam.C$` + case T(B): + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type typeparam.O\|typeparam.M\|typeparam.N: typeparam.E1, typeparam.E2, typeparam.A\|typeparam.C$` + T(B): struct{}{}, + } +} + +func _d[T y.Phylum | II | M](v T) { + switch v { // want `^missing cases in switch of type bar.Phylum\|typeparam.N\|typeparam.O\|typeparam.M: bar.Chordata, bar.Mollusca, typeparam.D\|typeparam.B, typeparam.E1, typeparam.E2$` + case T(A): + case T(y.Echinodermata): + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type bar.Phylum\|typeparam.N\|typeparam.O\|typeparam.M: bar.Chordata, bar.Mollusca, typeparam.D\|typeparam.B, typeparam.E1, typeparam.E2$` + T(A): struct{}{}, + T(y.Echinodermata): struct{}{}, + } +} + +func _e[T M](v T) { + switch v { // want `^missing cases in switch of type typeparam.M: typeparam.A$` + case T(M(B)): + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type typeparam.M: typeparam.A$` + T(M(B)): struct{}{}, + } +} + +func repeat0[T II | O](v T) { + switch v { // want `^missing cases in switch of type typeparam.N\|typeparam.O: typeparam.C, typeparam.D, typeparam.E2$` + case T(E1): + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type typeparam.N\|typeparam.O: typeparam.C, typeparam.D, typeparam.E2$` + T(E1): struct{}{}, + } +} + +func repeat1[T MM | M](v T) { + switch v { // want `^missing cases in switch of type typeparam.M: typeparam.A$` + case T(B): + } + + _ = map[T]struct{}{ // want `^missing keys in map of key type typeparam.M: typeparam.A$` + T(B): struct{}{}, + } +} + +func _mixedTypes0[T M | QQ](v T) { + // expect no diagnostic because underlying basic kinds are not same: + // uint8 vs. string + switch v { + case T(A): + } + + _ = map[T]struct{}{ + T(A): struct{}{}, + } +} + +func _mixedTypes1[T MM | QQ](v T) { + switch v { + case T(A): + } + + _ = map[T]struct{}{ + T(A): struct{}{}, + } +} + +func _notEnumType0[T M | NotEnumType](v T) { + // expect no diagnostic because not type elements are enum types. + switch v { + case T(B): + } + + _ = map[T]struct{}{ + T(B): struct{}{}, + } +} + +func _notEnumType1[T LL](v T) { + switch v { + case T(A): + } + + _ = map[T]struct{}{ + T(A): struct{}{}, + } +} diff --git a/testdata/xxx/app/app.go b/testdata/xxx/app/app.go deleted file mode 100644 index 023bd49..0000000 --- a/testdata/xxx/app/app.go +++ /dev/null @@ -1,12 +0,0 @@ -package app - -import "github.com/nishanths/exhaustive/testdata/playground/env" - -func readFile(path string) ([]byte, error) { - switch env.Current() { - case env.Production: - case env.Dev: - default: - } - panic("") -} diff --git a/testdata/xxx/calc/calc.go b/testdata/xxx/calc/calc.go index 07ba531..bf251f5 100644 --- a/testdata/xxx/calc/calc.go +++ b/testdata/xxx/calc/calc.go @@ -2,10 +2,23 @@ package calc import "github.com/nishanths/exhaustive/testdata/playground/token" -func processToken(t token.Token) { +func f(t token.Token) { switch t { case token.Add: case token.Subtract: case token.Multiply: + default: } } + +var m = map[token.Token]string{ + token.Add: "add", + token.Subtract: "subtract", + token.Multiply: "multiply", +} + +// Testing instructions +// +// % go build ./cmd/exhaustive +// % ./exhaustive ./testdata/xxx/calc +// % diff --git a/testdata/xxx/env/env.go b/testdata/xxx/env/env.go deleted file mode 100644 index ecf579e..0000000 --- a/testdata/xxx/env/env.go +++ /dev/null @@ -1,11 +0,0 @@ -package env - -type Environment string - -const ( - Production Environment = "production" - Staging Environment = "staging" - Dev Environment = "dev" -) - -func Current() Environment { return Dev } diff --git a/testdata/xxx/typeparam/main.go b/testdata/xxx/typeparam/main.go deleted file mode 100644 index 6efc2da..0000000 --- a/testdata/xxx/typeparam/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package typeparam - -// Testing instructions: -// $ go build ./cmd/exhaustive -// $ ./exhaustive ./testdata/xxx/typeparam - -type M int - -const ( - A M = iota - B -) - -type N uint - -const ( - C N = iota - D -) - -func foo[T M](v T) { - switch v { - case T(A): - } -} - -func bar[T M | N](v T) { - switch v { - case T(A): - case T(D): - } -}