From 712de1c63d9db97c81af68cd0dc4372c53d2e57a Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Tue, 15 Sep 2020 18:05:35 +0200 Subject: vendor/github.com/golangci/golangci-lint: update to v1.31 --- vendor/github.com/nishanths/exhaustive/.gitignore | 5 + vendor/github.com/nishanths/exhaustive/LICENSE | 25 ++ vendor/github.com/nishanths/exhaustive/README.md | 85 +++++ vendor/github.com/nishanths/exhaustive/enum.go | 99 ++++++ .../github.com/nishanths/exhaustive/exhaustive.go | 182 ++++++++++ vendor/github.com/nishanths/exhaustive/go.mod | 5 + vendor/github.com/nishanths/exhaustive/go.sum | 21 ++ vendor/github.com/nishanths/exhaustive/map.go | 158 +++++++++ vendor/github.com/nishanths/exhaustive/switch.go | 367 +++++++++++++++++++++ 9 files changed, 947 insertions(+) create mode 100644 vendor/github.com/nishanths/exhaustive/.gitignore create mode 100644 vendor/github.com/nishanths/exhaustive/LICENSE create mode 100644 vendor/github.com/nishanths/exhaustive/README.md create mode 100644 vendor/github.com/nishanths/exhaustive/enum.go create mode 100644 vendor/github.com/nishanths/exhaustive/exhaustive.go create mode 100644 vendor/github.com/nishanths/exhaustive/go.mod create mode 100644 vendor/github.com/nishanths/exhaustive/go.sum create mode 100644 vendor/github.com/nishanths/exhaustive/map.go create mode 100644 vendor/github.com/nishanths/exhaustive/switch.go (limited to 'vendor/github.com/nishanths') diff --git a/vendor/github.com/nishanths/exhaustive/.gitignore b/vendor/github.com/nishanths/exhaustive/.gitignore new file mode 100644 index 000000000..a724b56a9 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/.gitignore @@ -0,0 +1,5 @@ +.DS_Store + +# binary +cmd/exhaustive/exhaustive +exhaustive diff --git a/vendor/github.com/nishanths/exhaustive/LICENSE b/vendor/github.com/nishanths/exhaustive/LICENSE new file mode 100644 index 000000000..32befa68f --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2020, Nishanth Shanmugham +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/nishanths/exhaustive/README.md b/vendor/github.com/nishanths/exhaustive/README.md new file mode 100644 index 000000000..ecc76c7c5 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/README.md @@ -0,0 +1,85 @@ +# exhaustive + +[![Godoc](https://godoc.org/github.com/nishanths/exhaustive?status.svg)](https://godoc.org/github.com/nishanths/exhaustive) + +The `exhaustive` package and command line program can be used to detect +enum switch statements that are not exhaustive. + +An enum switch statement is exhaustive if it has cases for each of the enum's members. See godoc for the definition of enum used by the program. + +The `exhaustive` package provides an `Analyzer` that follows the guidelines +described in the [go/analysis](https://godoc.org/golang.org/x/tools/go/analysis) package; this makes +it possible to integrate into existing analysis driver programs. + +## Install + +``` +go get github.com/nishanths/exhaustive/... +``` + +## Docs + +https://godoc.org/github.com/nishanths/exhaustive + +## Usage + +The command line usage is: + +``` +Usage: exhaustive [-flags] [packages...] + +Flags: + -default-signifies-exhaustive + indicates that switch statements are to be considered exhaustive if a 'default' case + is present, even if all enum members aren't listed in the switch (default false) + -fix + apply all suggested fixes (default false) + +Examples: + exhaustive code.org/proj/... + exhaustive -fix example.org/foo/pkg example.org/foo/bar +``` + +## Example + +Given the code: + +```diff +package token + +type Token int + +const ( + Add Token = iota + Subtract + Multiply ++ Quotient ++ Remainder +) +``` +``` +package calc + +import "token" + +func processToken(t token.Token) { + switch t { + case token.Add: + // ... + case token.Subtract: + // ... + case token.Multiply: + // ... + } +} +``` + +Running the `exhaustive` command will print: + +``` +calc.go:6:2: missing cases in switch of type token.Token: Quotient, Remainder +``` + +## License + +BSD 2-Clause diff --git a/vendor/github.com/nishanths/exhaustive/enum.go b/vendor/github.com/nishanths/exhaustive/enum.go new file mode 100644 index 000000000..98b5656b6 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/enum.go @@ -0,0 +1,99 @@ +package exhaustive + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +type enums map[string][]string // enum type name -> enum member names + +func findEnums(pass *analysis.Pass) enums { + pkgEnums := make(enums) + + // Gather enum types. + for _, f := range pass.Files { + for _, decl := range f.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + if gen.Tok != token.TYPE { + continue + } + for _, s := range gen.Specs { + // Must be TypeSpec since we've filtered on token.TYPE. + t, ok := s.(*ast.TypeSpec) + obj := pass.TypesInfo.Defs[t.Name] + if obj == nil { + continue + } + + named, ok := obj.Type().(*types.Named) + if !ok { + continue + } + basic, ok := named.Underlying().(*types.Basic) + if !ok { + continue + } + switch i := basic.Info(); { + case i&types.IsInteger != 0: + pkgEnums[named.Obj().Name()] = nil + case i&types.IsFloat != 0: + pkgEnums[named.Obj().Name()] = nil + case i&types.IsString != 0: + pkgEnums[named.Obj().Name()] = nil + } + } + } + } + + // Gather enum members. + for _, f := range pass.Files { + for _, decl := range f.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + if gen.Tok != token.CONST && gen.Tok != token.VAR { + continue + } + for _, s := range gen.Specs { + // Must be ValueSpec since we've filtered on token.CONST, token.VAR. + v := s.(*ast.ValueSpec) + for _, name := range v.Names { + obj := pass.TypesInfo.Defs[name] + if obj == nil { + continue + } + named, ok := obj.Type().(*types.Named) + if !ok { + continue + } + + members, ok := pkgEnums[named.Obj().Name()] + if !ok { + continue + } + members = append(members, obj.Name()) + pkgEnums[named.Obj().Name()] = members + } + } + } + } + + // Delete member-less enum types. + // We can't call these enums, since we can't be sure without + // the existence of members. (The type may just be a named type, + // for instance.) + for k, v := range pkgEnums { + if len(v) == 0 { + delete(pkgEnums, k) + } + } + + return pkgEnums +} diff --git a/vendor/github.com/nishanths/exhaustive/exhaustive.go b/vendor/github.com/nishanths/exhaustive/exhaustive.go new file mode 100644 index 000000000..ef869f268 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/exhaustive.go @@ -0,0 +1,182 @@ +// Package exhaustive provides an analyzer that helps ensure enum switch statements +// are exhaustive. The analyzer also provides fixes to make the offending switch +// statements exhaustive (see "Fixes" section). +// +// See "cmd/exhaustive" subpackage for the related command line program. +// +// Definition of enum +// +// The language spec does not provide an explicit definition for enums. +// For the purpose of this program, an enum type is a package-level named type +// whose underlying type is an integer (includes byte and rune), a float, or +// a string type. An enum type must have associated with it one or more +// package-level variables of the named type in the package. These variables +// constitute the enum's members. +// +// In the code snippet below, Biome is an enum type with 3 members. +// +// type Biome int +// +// const ( +// Tundra Biome = iota +// Savanna +// Desert +// ) +// +// Switch statement exhaustiveness +// +// An enum switch statement is exhaustive if it has cases for each of the enum's members. +// +// For an enum type defined in the same package as the switch statement, both +// exported and unexported enum members must be present in order to consider +// the switch exhaustive. On the other hand, for an enum type defined +// in an external package it is sufficient for just exported enum members +// to be present in order to consider the switch exhaustive. +// +// Flags +// +// The analyzer accepts a boolean flag: -default-signifies-exhaustive. +// The flag, if set, indicates to the analyzer that switch statements +// are to be considered exhaustive as long as a 'default' case is present, even +// if all enum members aren't listed in the switch statements cases. +// +// The other relevant flag is the -fix flag. +// +// Fixes +// +// The analyzer suggests fixes for a switch statement if it is not exhaustive +// and does not have a 'default' case. The suggested fix always adds a single +// case clause for the missing enum members. +// +// case missingA, missingB, missingC: +// panic(fmt.Sprintf("unhandled value: %v", v)) +// +// where v is the expression in the switch statement's tag (in other words, the +// value being switched upon). If the switch statement's tag is a function or a +// method call the analyzer does not suggest a fix, as reusing the call expression +// in the panic/fmt.Sprintf call could be mutative. +// +// The rationale for the fix using panic is that it might be better to fail loudly on +// existing unhandled or impossible cases than to let them slip by quietly unnoticed. +// An even better fix may, of course, be to manually inspect the sites reported +// by the package and handle the missing cases if necessary. +// +// Imports will be adjusted automatically to account for the "fmt" dependency. +// +// Skip analysis of specific switch statements +// +// If the following directive comment: +// +// //exhaustive:ignore +// +// is associated with a switch statement, the analyzer skips +// checking of the switch statement and no diagnostics are reported. +package exhaustive + +import ( + "go/ast" + "go/types" + "sort" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +const ( + // DefaultSignifiesExhaustiveFlag is a flag name used by the analyzer. It + // is exported for use by analyzer driver programs. + DefaultSignifiesExhaustiveFlag = "default-signifies-exhaustive" +) + +var ( + fCheckMaps bool + fDefaultSignifiesExhaustive bool +) + +func init() { + Analyzer.Flags.BoolVar(&fCheckMaps, "maps", false, "check key exhaustiveness for map literals of enum key type, in addition to checking switch statements") + Analyzer.Flags.BoolVar(&fDefaultSignifiesExhaustive, DefaultSignifiesExhaustiveFlag, false, "indicates that switch statements are to be considered exhaustive if a 'default' case is present, even if all enum members aren't listed in the switch") +} + +var Analyzer = &analysis.Analyzer{ + Name: "exhaustive", + Doc: "check exhaustiveness of enum switch statements", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + FactTypes: []analysis.Fact{&enumsFact{}}, +} + +// IgnoreDirectivePrefix is used to exclude checking of specific switch statements. +// See https://godoc.org/github.com/nishanths/exhaustive#hdr-Skip_analysis_of_specific_switch_statements +// for details. +const IgnoreDirectivePrefix = "//exhaustive:ignore" + +func containsIgnoreDirective(comments []*ast.Comment) bool { + for _, c := range comments { + if strings.HasPrefix(c.Text, IgnoreDirectivePrefix) { + return true + } + } + return false +} + +type enumsFact struct { + Entries enums +} + +var _ analysis.Fact = (*enumsFact)(nil) + +func (e *enumsFact) AFact() {} + +func (e *enumsFact) String() string { + // sort for stability (required for testing) + var sortedKeys []string + for k := range e.Entries { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + + var buf strings.Builder + for i, k := range sortedKeys { + v := e.Entries[k] + buf.WriteString(k) + buf.WriteString(":") + for j, vv := range v { + buf.WriteString(vv) + // add comma separator between each enum member in an enum type + if j != len(v)-1 { + buf.WriteString(",") + } + } + // add semicolon separator between each enum type + if i != len(sortedKeys)-1 { + buf.WriteString("; ") + } + } + return buf.String() +} + +func run(pass *analysis.Pass) (interface{}, error) { + e := findEnums(pass) + if len(e) != 0 { + pass.ExportPackageFact(&enumsFact{Entries: e}) + } + + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + comments := make(map[*ast.File]ast.CommentMap) // CommentMap per package file, lazily populated by reference + + checkSwitchStatements(pass, inspect, comments) + if fCheckMaps { + checkMapLiterals(pass, inspect, comments) + } + return nil, nil +} + +func enumTypeName(e *types.Named, samePkg bool) string { + if samePkg { + return e.Obj().Name() + } + return e.Obj().Pkg().Name() + "." + e.Obj().Name() +} diff --git a/vendor/github.com/nishanths/exhaustive/go.mod b/vendor/github.com/nishanths/exhaustive/go.mod new file mode 100644 index 000000000..b15048eab --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/go.mod @@ -0,0 +1,5 @@ +module github.com/nishanths/exhaustive + +go 1.14 + +require golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a diff --git a/vendor/github.com/nishanths/exhaustive/go.sum b/vendor/github.com/nishanths/exhaustive/go.sum new file mode 100644 index 000000000..01ba99965 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/go.sum @@ -0,0 +1,21 @@ +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a h1:gILuVKC+ZPD6g/tj6zBOdnOH1ZHI0zZ86+KLMogc6/s= +golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200519142718-10921354bc51 h1:GtYAC9y+dpwWCXBwbcZgxcFfiqW4SI93yvQqpF+9+P8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/nishanths/exhaustive/map.go b/vendor/github.com/nishanths/exhaustive/map.go new file mode 100644 index 000000000..6d875e32a --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/map.go @@ -0,0 +1,158 @@ +package exhaustive + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/ast/inspector" +) + +func checkMapLiterals(pass *analysis.Pass, inspect *inspector.Inspector, comments map[*ast.File]ast.CommentMap) { + for _, f := range pass.Files { + for _, d := range f.Decls { + gen, ok := d.(*ast.GenDecl) + if !ok { + continue + } + if gen.Tok != token.VAR { + continue // map literals have to be declared as "var" + } + for _, s := range gen.Specs { + valueSpec := s.(*ast.ValueSpec) + for idx, name := range valueSpec.Names { + obj := pass.TypesInfo.Defs[name] + if obj == nil { + continue + } + + mapType, ok := obj.Type().(*types.Map) + if !ok { + continue + } + + keyType, ok := mapType.Key().(*types.Named) + if !ok { + continue + } + keyPkg := keyType.Obj().Pkg() + if keyPkg == nil { + // Doc comment: nil for labels and objects in the Universe scope. + // This happens for the `error` type, for example. + // Continuing would mean that ImportPackageFact panics. + continue + } + + var enums enumsFact + if !pass.ImportPackageFact(keyPkg, &enums) { + // Can't do anything further. + continue + } + + enumMembers, ok := enums.Entries[keyType.Obj().Name()] + if !ok { + // Key type is not a known enum. + continue + } + + // Check comments for the ignore directive. + + var allComments ast.CommentMap + if cm, ok := comments[f]; ok { + allComments = cm + } else { + allComments = ast.NewCommentMap(pass.Fset, f, f.Comments) + comments[f] = allComments + } + + genDeclComments := allComments.Filter(gen) + genDeclIgnore := false + for _, group := range genDeclComments.Comments() { + if containsIgnoreDirective(group.List) && gen.Lparen == token.NoPos && len(gen.Specs) == 1 { + genDeclIgnore = true + break + } + } + if genDeclIgnore { + continue + } + + if (valueSpec.Doc != nil && containsIgnoreDirective(valueSpec.Doc.List)) || + (valueSpec.Comment != nil && containsIgnoreDirective(valueSpec.Comment.List)) { + continue + } + + samePkg := keyPkg == pass.Pkg + checkUnexported := samePkg + + hitlist := hitlistFromEnumMembers(enumMembers, checkUnexported) + if len(hitlist) == 0 { + // can happen if external package and enum consists only of + // unexported members + continue + } + + if !(len(valueSpec.Values) > idx) { + continue // no value for name + } + comp, ok := valueSpec.Values[idx].(*ast.CompositeLit) + if !ok { + continue + } + for _, el := range comp.Elts { + kvExpr, ok := el.(*ast.KeyValueExpr) + if !ok { + continue + } + e := astutil.Unparen(kvExpr.Key) + if samePkg { + ident, ok := e.(*ast.Ident) + if !ok { + continue + } + delete(hitlist, ident.Name) + } else { + selExpr, ok := e.(*ast.SelectorExpr) + if !ok { + continue + } + + // ensure X is package identifier + ident, ok := selExpr.X.(*ast.Ident) + if !ok { + continue + } + if !isPackageNameIdentifier(pass, ident) { + continue + } + + delete(hitlist, selExpr.Sel.Name) + } + } + + if len(hitlist) > 0 { + reportMapLiteral(pass, name, samePkg, keyType, hitlist) + } + } + } + } + } +} + +func reportMapLiteral(pass *analysis.Pass, mapVarIdent *ast.Ident, samePkg bool, enumType *types.Named, missingMembers map[string]struct{}) { + missing := make([]string, 0, len(missingMembers)) + for m := range missingMembers { + missing = append(missing, m) + } + sort.Strings(missing) + + pass.Report(analysis.Diagnostic{ + Pos: mapVarIdent.Pos(), + Message: fmt.Sprintf("missing keys in map %s of key type %s: %s", mapVarIdent.Name, enumTypeName(enumType, samePkg), strings.Join(missing, ", ")), + }) +} diff --git a/vendor/github.com/nishanths/exhaustive/switch.go b/vendor/github.com/nishanths/exhaustive/switch.go new file mode 100644 index 000000000..5889c2934 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/switch.go @@ -0,0 +1,367 @@ +package exhaustive + +import ( + "bytes" + "fmt" + "go/ast" + "go/printer" + "go/token" + "go/types" + "sort" + "strconv" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/ast/inspector" +) + +func isDefaultCase(c *ast.CaseClause) bool { + return c.List == nil // see doc comment on field +} + +func checkSwitchStatements(pass *analysis.Pass, inspect *inspector.Inspector, comments map[*ast.File]ast.CommentMap) { + inspect.WithStack([]ast.Node{&ast.SwitchStmt{}}, func(n ast.Node, push bool, stack []ast.Node) bool { + if !push { + return true + } + sw := n.(*ast.SwitchStmt) + if sw.Tag == nil { + return true + } + t := pass.TypesInfo.Types[sw.Tag] + if !t.IsValue() { + return true + } + tagType, ok := t.Type.(*types.Named) + if !ok { + return true + } + + tagPkg := tagType.Obj().Pkg() + if tagPkg == nil { + // Doc comment: nil for labels and objects in the Universe scope. + // This happens for the `error` type, for example. + // Continuing would mean that ImportPackageFact panics. + return true + } + + var enums enumsFact + if !pass.ImportPackageFact(tagPkg, &enums) { + // Can't do anything further. + return true + } + + enumMembers, isEnum := enums.Entries[tagType.Obj().Name()] + if !isEnum { + // Tag's type is not a known enum. + return true + } + + // Get comment map. + file := stack[0].(*ast.File) + var allComments ast.CommentMap + if cm, ok := comments[file]; ok { + allComments = cm + } else { + allComments = ast.NewCommentMap(pass.Fset, file, file.Comments) + comments[file] = allComments + } + + specificComments := allComments.Filter(sw) + for _, group := range specificComments.Comments() { + if containsIgnoreDirective(group.List) { + return true // skip checking due to ignore directive + } + } + + samePkg := tagPkg == pass.Pkg + checkUnexported := samePkg + + hitlist := hitlistFromEnumMembers(enumMembers, checkUnexported) + if len(hitlist) == 0 { + // can happen if external package and enum consists only of + // unexported members + return true + } + + defaultCaseExists := false + for _, stmt := range sw.Body.List { + caseCl := stmt.(*ast.CaseClause) + if isDefaultCase(caseCl) { + defaultCaseExists = true + continue // nothing more to do if it's the default case + } + for _, e := range caseCl.List { + e = astutil.Unparen(e) + if samePkg { + ident, ok := e.(*ast.Ident) + if !ok { + continue + } + delete(hitlist, ident.Name) + } else { + selExpr, ok := e.(*ast.SelectorExpr) + if !ok { + continue + } + + // ensure X is package identifier + ident, ok := selExpr.X.(*ast.Ident) + if !ok { + continue + } + if !isPackageNameIdentifier(pass, ident) { + continue + } + + delete(hitlist, selExpr.Sel.Name) + } + } + } + + defaultSuffices := fDefaultSignifiesExhaustive && defaultCaseExists + shouldReport := len(hitlist) > 0 && !defaultSuffices + + if shouldReport { + reportSwitch(pass, sw, samePkg, tagType, hitlist, defaultCaseExists, file) + } + return true + }) +} + +func isPackageNameIdentifier(pass *analysis.Pass, ident *ast.Ident) bool { + obj := pass.TypesInfo.ObjectOf(ident) + if obj == nil { + return false + } + _, ok := obj.(*types.PkgName) + return ok +} + +func hitlistFromEnumMembers(enumMembers []string, checkUnexported bool) map[string]struct{} { + hitlist := make(map[string]struct{}) + for _, m := range enumMembers { + if m == "_" { + // blank identifier is often used to skip entries in iota lists + continue + } + if ast.IsExported(m) || checkUnexported { + hitlist[m] = struct{}{} + } + } + return hitlist +} + +func reportSwitch(pass *analysis.Pass, sw *ast.SwitchStmt, samePkg bool, enumType *types.Named, missingMembers map[string]struct{}, defaultCaseExists bool, f *ast.File) { + missing := make([]string, 0, len(missingMembers)) + for m := range missingMembers { + missing = append(missing, m) + } + sort.Strings(missing) + + var fixes []analysis.SuggestedFix + if !defaultCaseExists { + if fix, ok := computeFix(pass, pass.Fset, f, sw, enumType, samePkg, missingMembers); ok { + fixes = append(fixes, fix) + } + } + + pass.Report(analysis.Diagnostic{ + Pos: sw.Pos(), + End: sw.End(), + Message: fmt.Sprintf("missing cases in switch of type %s: %s", enumTypeName(enumType, samePkg), strings.Join(missing, ", ")), + SuggestedFixes: fixes, + }) +} + +func computeFix(pass *analysis.Pass, fset *token.FileSet, f *ast.File, sw *ast.SwitchStmt, enumType *types.Named, samePkg bool, missingMembers map[string]struct{}) (analysis.SuggestedFix, bool) { + // Function and method calls may be mutative, so we don't want to reuse the + // call expression in the about-to-be-inserted case clause body. So we just + // don't suggest a fix in such situations. + // + // However, we need to make an exception for type conversions, which are + // also call expressions in the AST. + // + // We'll need to lookup type information for this, and can't rely solely + // on the AST. + if containsFuncCall(pass, sw.Tag) { + return analysis.SuggestedFix{}, false + } + + textEdits := []analysis.TextEdit{ + missingCasesTextEdit(fset, f, samePkg, sw, enumType, missingMembers), + } + + // need to add "fmt" import if "fmt" import doesn't already exist + if !hasImportWithPath(fset, f, `"fmt"`) { + textEdits = append(textEdits, fmtImportTextEdit(fset, f)) + } + + missing := make([]string, 0, len(missingMembers)) + for m := range missingMembers { + missing = append(missing, m) + } + sort.Strings(missing) + + return analysis.SuggestedFix{ + Message: fmt.Sprintf("add case clause for: %s?", strings.Join(missing, ", ")), + TextEdits: textEdits, + }, true +} + +func containsFuncCall(pass *analysis.Pass, e ast.Expr) bool { + e = astutil.Unparen(e) + c, ok := e.(*ast.CallExpr) + if !ok { + return false + } + if _, isFunc := pass.TypesInfo.TypeOf(c.Fun).Underlying().(*types.Signature); isFunc { + return true + } + for _, a := range c.Args { + if containsFuncCall(pass, a) { + return true + } + } + return false +} + +func firstImportDecl(fset *token.FileSet, f *ast.File) *ast.GenDecl { + for _, decl := range f.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if ok && genDecl.Tok == token.IMPORT { + // first IMPORT GenDecl + return genDecl + } + } + return nil +} + +// copies an GenDecl in a manner such that appending to the returned GenDecl's Specs field +// doesn't mutate the original GenDecl +func copyGenDecl(im *ast.GenDecl) *ast.GenDecl { + imCopy := *im + imCopy.Specs = make([]ast.Spec, len(im.Specs)) + for i := range im.Specs { + imCopy.Specs[i] = im.Specs[i] + } + return &imCopy +} + +func hasImportWithPath(fset *token.FileSet, f *ast.File, pathLiteral string) bool { + igroups := astutil.Imports(fset, f) + for _, igroup := range igroups { + for _, importSpec := range igroup { + if importSpec.Path.Value == pathLiteral { + return true + } + } + } + return false +} + +func fmtImportTextEdit(fset *token.FileSet, f *ast.File) analysis.TextEdit { + firstDecl := firstImportDecl(fset, f) + + if firstDecl == nil { + // file has no import declarations + // insert "fmt" import spec after package statement + return analysis.TextEdit{ + Pos: f.Name.End() + 1, // end of package name + 1 + End: f.Name.End() + 1, + NewText: []byte(`import ( + "fmt" + )`), + } + } + + // copy because we'll be mutating its Specs field + firstDeclCopy := copyGenDecl(firstDecl) + + // find insertion index for "fmt" import spec + var i int + for ; i < len(firstDeclCopy.Specs); i++ { + im := firstDeclCopy.Specs[i].(*ast.ImportSpec) + if v, _ := strconv.Unquote(im.Path.Value); v > "fmt" { + break + } + } + + // insert "fmt" import spec at the index + fmtSpec := &ast.ImportSpec{ + Path: &ast.BasicLit{ + // NOTE: Pos field doesn't seem to be required for our + // purposes here. + Kind: token.STRING, + Value: `"fmt"`, + }, + } + s := firstDeclCopy.Specs // local var for easier comprehension of next line + s = append(s[:i], append([]ast.Spec{fmtSpec}, s[i:]...)...) + firstDeclCopy.Specs = s + + // create the text edit + var buf bytes.Buffer + printer.Fprint(&buf, fset, firstDeclCopy) + + return analysis.TextEdit{ + Pos: firstDecl.Pos(), + End: firstDecl.End(), + NewText: buf.Bytes(), + } +} + +func missingCasesTextEdit(fset *token.FileSet, f *ast.File, samePkg bool, sw *ast.SwitchStmt, enumType *types.Named, missingMembers map[string]struct{}) analysis.TextEdit { + // ... Construct insertion text for case clause and its body ... + + var tag bytes.Buffer + printer.Fprint(&tag, fset, sw.Tag) + + // If possible and if necessary, determine the package identifier based on the AST of other `case` clauses. + var pkgIdent *ast.Ident + if !samePkg { + for _, stmt := range sw.Body.List { + caseCl := stmt.(*ast.CaseClause) + // At least one expression must exist in List at this point. + // List cannot be nil because we only arrive here if the "default" clause + // does not exist. Additionally, a syntactically valid case clause must + // have at least one expression. + if sel, ok := caseCl.List[0].(*ast.SelectorExpr); ok { + pkgIdent = sel.X.(*ast.Ident) + break + } + } + } + + missing := make([]string, 0, len(missingMembers)) + for m := range missingMembers { + if !samePkg { + if pkgIdent != nil { + // we were able to determine package identifier + missing = append(missing, pkgIdent.Name+"."+m) + } else { + // use the package name (may not be correct always) + // + // TODO: May need to also add import if the package isn't imported + // elsewhere. This (ie, a switch with zero case clauses) should + // happen rarely, so don't implement this for now. + missing = append(missing, enumType.Obj().Pkg().Name()+"."+m) + } + } else { + missing = append(missing, m) + } + } + sort.Strings(missing) + + insert := `case ` + strings.Join(missing, ", ") + `: + panic(fmt.Sprintf("unhandled value: %v",` + tag.String() + `))` + + // ... Create the text edit ... + + return analysis.TextEdit{ + Pos: sw.Body.Rbrace - 1, + End: sw.Body.Rbrace - 1, + NewText: []byte(insert), + } +} -- cgit mrf-deployment