From b2f2446b46bf02821d90ebedadae2bf7ae0e880e Mon Sep 17 00:00:00 2001 From: Taras Madan Date: Mon, 5 Sep 2022 14:27:54 +0200 Subject: go.mod, vendor: update (#3358) * go.mod, vendor: remove unnecessary dependencies Commands: 1. go mod tidy 2. go mod vendor * go.mod, vendor: update cloud.google.com/go Commands: 1. go get -u cloud.google.com/go 2. go mod tidy 3. go mod vendor * go.mod, vendor: update cloud.google.com/* Commands: 1. go get -u cloud.google.com/storage cloud.google.com/logging 2. go mod tidy 3. go mod vendor * go.mod, .golangci.yml, vendor: update *lint* Commands: 1. go get -u golang.org/x/tools github.com/golangci/golangci-lint@v1.47.0 2. go mod tidy 3. go mod vendor 4. edit .golangci.yml to suppress new errors (resolved in the same PR later) * all: fix lint errors hash.go: copy() recommended by gosimple parse.go: ent is never nil verifier.go: signal.Notify() with unbuffered channel is bad. Have no idea why. * .golangci.yml: adjust godot rules check-all is deprecated, but still work if you're hesitating too - I'll remove this commit --- vendor/github.com/nishanths/exhaustive/.gitignore | 3 + vendor/github.com/nishanths/exhaustive/.travis.yml | 12 - vendor/github.com/nishanths/exhaustive/Makefile | 28 + vendor/github.com/nishanths/exhaustive/README.md | 81 ++- vendor/github.com/nishanths/exhaustive/comment.go | 74 +++ vendor/github.com/nishanths/exhaustive/enum.go | 249 +++++---- .../github.com/nishanths/exhaustive/exhaustive.go | 405 ++++++++------ vendor/github.com/nishanths/exhaustive/fact.go | 28 + .../github.com/nishanths/exhaustive/generated.go | 34 -- vendor/github.com/nishanths/exhaustive/go.mod | 2 +- vendor/github.com/nishanths/exhaustive/go.sum | 47 +- vendor/github.com/nishanths/exhaustive/switch.go | 601 +++++++++------------ 12 files changed, 834 insertions(+), 730 deletions(-) delete mode 100644 vendor/github.com/nishanths/exhaustive/.travis.yml create mode 100644 vendor/github.com/nishanths/exhaustive/Makefile create mode 100644 vendor/github.com/nishanths/exhaustive/comment.go create mode 100644 vendor/github.com/nishanths/exhaustive/fact.go delete mode 100644 vendor/github.com/nishanths/exhaustive/generated.go (limited to 'vendor/github.com/nishanths') diff --git a/vendor/github.com/nishanths/exhaustive/.gitignore b/vendor/github.com/nishanths/exhaustive/.gitignore index 24bde5301..10acec6e1 100644 --- a/vendor/github.com/nishanths/exhaustive/.gitignore +++ b/vendor/github.com/nishanths/exhaustive/.gitignore @@ -5,3 +5,6 @@ tags # binary cmd/exhaustive/exhaustive exhaustive + +# testing artifacts +coverage.out diff --git a/vendor/github.com/nishanths/exhaustive/.travis.yml b/vendor/github.com/nishanths/exhaustive/.travis.yml deleted file mode 100644 index bd342f558..000000000 --- a/vendor/github.com/nishanths/exhaustive/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: go - -go: - - 1.x - - master - -# Only clone the most recent commit. -git: - depth: 1 - -notifications: - email: false diff --git a/vendor/github.com/nishanths/exhaustive/Makefile b/vendor/github.com/nishanths/exhaustive/Makefile new file mode 100644 index 000000000..981a7ebe9 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/Makefile @@ -0,0 +1,28 @@ +.PHONY: default +default: build + +.PHONY: build +build: + go build ./... + +.PHONY: test +test: + go test -cover ./... + +.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 + +.PHONY: vet +vet: + go vet ./... + exhaustive ./... + ineffassign ./... + errcheck ./... + +.PHONY: upgrade-deps +upgrade-deps: + go get golang.org/x/tools + go mod tidy diff --git a/vendor/github.com/nishanths/exhaustive/README.md b/vendor/github.com/nishanths/exhaustive/README.md index 90afc87fe..3992704a5 100644 --- a/vendor/github.com/nishanths/exhaustive/README.md +++ b/vendor/github.com/nishanths/exhaustive/README.md @@ -1,54 +1,31 @@ -# exhaustive +## exhaustive [![Godoc][2]][1] -[![Godoc](https://godoc.org/github.com/nishanths/exhaustive?status.svg)](https://godoc.org/github.com/nishanths/exhaustive) - -[![Build Status](https://travis-ci.org/nishanths/exhaustive.svg?branch=master)](https://travis-ci.org/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 +Check exhaustiveness of enum switch statements in Go source code. ``` -go get github.com/nishanths/exhaustive/... +go install github.com/nishanths/exhaustive/cmd/exhaustive@latest ``` -## Docs +For docs on the flags, the definition of enum, and the definition of +exhaustiveness, see [godocs.io][4]. -https://godoc.org/github.com/nishanths/exhaustive +For the changelog, see [CHANGELOG][changelog] in the wiki. -## Usage +The package provides an `Analyzer` that follows the guidelines in the +[`go/analysis`][3] package; this should make it possible to integrate +exhaustive with your own analysis driver program. -The command line usage is: +## Bugs -``` -Usage: exhaustive [-flags] [packages...] - -Flags: - -check-generated - check switch statements in generated files also - -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 github.com/foo/bar/... - exhaustive github.com/a/b github.com/x/y -``` +`exhaustive` does not report missing cases if the switch statement +switches on a type parameterized type. See [this +issue](https://github.com/nishanths/exhaustive/issues/31) for details. ## Example -Given the code: +Given the enum -```diff +```go package token type Token int @@ -57,35 +34,41 @@ const ( Add Token = iota Subtract Multiply -+ Quotient -+ Remainder + Quotient + Remainder ) ``` -``` + +and the switch statement + +```go package calc import "token" -func processToken(t token.Token) { +func f(t token.Token) { switch t { case token.Add: - ... case token.Subtract: - ... case token.Multiply: - ... + default: } } ``` -Running the `exhaustive` command will print: +running exhaustive will print ``` calc.go:6:2: missing cases in switch of type token.Token: Quotient, Remainder ``` -Enums can also be defined using explicit constant values instead of `iota`. +## Contributing -## License +Issues and pull requests are welcome. Before making a substantial +change, please discuss it in an issue. -BSD 2-Clause +[1]: https://godocs.io/github.com/nishanths/exhaustive +[2]: https://godocs.io/github.com/nishanths/exhaustive?status.svg +[3]: https://pkg.go.dev/golang.org/x/tools/go/analysis +[4]: https://godocs.io/github.com/nishanths/exhaustive +[changelog]: https://github.com/nishanths/exhaustive/wiki/CHANGELOG diff --git a/vendor/github.com/nishanths/exhaustive/comment.go b/vendor/github.com/nishanths/exhaustive/comment.go new file mode 100644 index 000000000..cae8c64d1 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/comment.go @@ -0,0 +1,74 @@ +package exhaustive + +import ( + "go/ast" + "regexp" + "strings" +) + +// Generated file definition +// http://golang.org/s/generatedcode +// +// To convey to humans and machine tools that code is generated, generated +// source should have a line that matches the following regular expression (in +// Go syntax): +// +// ^// Code generated .* DO NOT EDIT\.$ +// +// This line must appear before the first non-comment, non-blank +// text in the file. + +func isGeneratedFile(file *ast.File) bool { + // NOTE: file.Comments includes file.Doc as well, so no need + // to separately check file.Doc. + + for _, c := range file.Comments { + for _, cc := range c.List { + // This check is intended to handle "must appear before the + // first non-comment, non-blank text in the file". + // TODO: Is this check fully correct? Seems correct based + // on https://golang.org/ref/spec#Source_file_organization. + if c.Pos() >= file.Package { + return false + } + // According to the docs: + // '\r' has been removed. + // '\n' has been removed for //-style comments, which is what we care about. + // Also manually verified. + if isGeneratedFileComment(cc.Text) { + return true + } + } + } + + return false +} + +var generatedCodeRe = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`) + +func isGeneratedFileComment(s string) bool { + return generatedCodeRe.MatchString(s) +} + +// ignoreDirective is used to exclude checking of specific switch statements. +const ignoreDirective = "//exhaustive:ignore" +const enforceDirective = "//exhaustive:enforce" + +func containsDirective(comments []*ast.CommentGroup, directive string) bool { + for _, c := range comments { + for _, cc := range c.List { + if strings.HasPrefix(cc.Text, directive) { + return true + } + } + } + return false +} + +func containsEnforceDirective(comments []*ast.CommentGroup) bool { + return containsDirective(comments, enforceDirective) +} + +func containsIgnoreDirective(comments []*ast.CommentGroup) bool { + return containsDirective(comments, ignoreDirective) +} diff --git a/vendor/github.com/nishanths/exhaustive/enum.go b/vendor/github.com/nishanths/exhaustive/enum.go index ed0df642b..2b287e39a 100644 --- a/vendor/github.com/nishanths/exhaustive/enum.go +++ b/vendor/github.com/nishanths/exhaustive/enum.go @@ -1,146 +1,171 @@ package exhaustive import ( + "fmt" "go/ast" "go/token" "go/types" + "strings" - "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" ) -type enums map[string]*enumMembers // enum type name -> enum members +// 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. +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. +// The zero value is ready to use. type enumMembers struct { - // Names in the order encountered in the AST. - OrderedNames []string - - // Maps name -> (constant.Value).ExactString(). - // If a name is missing in the map, it means that it does not have a - // corresponding constant.Value defined in the AST. - NameToValue map[string]string - - // Maps (constant.Value).ExactString() -> names. - // Names that don't have a constant.Value defined in the AST (e.g., some - // iota constants) will not have a corresponding entry in this map. - ValueToNames map[string][]string + Names []string // enum member names, AST order + NameToValue map[string]constantValue // enum member name -> constant value + ValueToNames map[constantValue][]string // constant value -> enum member names } -func (em *enumMembers) add(name string, constVal *string) { - em.OrderedNames = append(em.OrderedNames, name) +func (em *enumMembers) add(name string, val constantValue) { + if em.NameToValue == nil { + em.NameToValue = make(map[string]constantValue) + } + if em.ValueToNames == nil { + em.ValueToNames = make(map[constantValue][]string) + } - if constVal != nil { - if em.NameToValue == nil { - em.NameToValue = make(map[string]string) - } - em.NameToValue[name] = *constVal + em.Names = append(em.Names, name) + em.NameToValue[name] = val + em.ValueToNames[val] = append(em.ValueToNames[val], name) +} - if em.ValueToNames == nil { - em.ValueToNames = make(map[string][]string) +func (em enumMembers) String() string { return em.factString() } // for debugging + +func (em enumMembers) factString() string { + var buf strings.Builder + for j, vv := range em.Names { + buf.WriteString(vv) + // add comma separator between each enum member + if j != len(em.Names)-1 { + buf.WriteString(",") } - em.ValueToNames[*constVal] = append(em.ValueToNames[*constVal], name) } + return buf.String() } -func (em *enumMembers) numMembers() int { - return len(em.OrderedNames) -} - -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 - } +func findEnums(pkgScopeOnly bool, pkg *types.Package, inspect *inspector.Inspector, info *types.Info) map[enumType]enumMembers { + result := make(map[enumType]enumMembers) - named, ok := obj.Type().(*types.Named) + inspect.Preorder([]ast.Node{&ast.GenDecl{}}, func(n ast.Node) { + gen := n.(*ast.GenDecl) + if gen.Tok != token.CONST { + return + } + for _, s := range gen.Specs { + for _, name := range s.(*ast.ValueSpec).Names { + enumTyp, memberName, val, ok := possibleEnumMember(name, info) if !ok { continue } - basic, ok := named.Underlying().(*types.Basic) - if !ok { + if pkgScopeOnly && enumTyp.scope() != pkg.Scope() { continue } - - switch i := basic.Info(); { - case i&types.IsInteger != 0: - pkgEnums[named.Obj().Name()] = &enumMembers{} - case i&types.IsFloat != 0: - pkgEnums[named.Obj().Name()] = &enumMembers{} - case i&types.IsString != 0: - pkgEnums[named.Obj().Name()] = &enumMembers{} - } + v := result[enumTyp] + v.add(memberName, val) + result[enumTyp] = v } } + }) + + return result +} + +func possibleEnumMember(constName *ast.Ident, info *types.Info) (et enumType, name string, val constantValue, ok bool) { + obj := info.Defs[constName] + if obj == nil { + panic(fmt.Sprintf("info.Defs[%s] == nil", constName)) + } + if _, ok = obj.(*types.Const); !ok { + panic(fmt.Sprintf("obj must be *types.Const, got %T", obj)) + } + if isBlankIdentifier(obj) { + // These objects have a nil parent scope. + // Also, we have no real purpose to record them. + return enumType{}, "", "", false } - // 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 i, name := range v.Names { - obj := pass.TypesInfo.Defs[name] - if obj == nil { - continue - } - - named, ok := obj.Type().(*types.Named) - if !ok { - continue - } - - // Get the constant.Value representation, if any. - var constVal *string - if len(v.Values) > i { - value := v.Values[i] - if con, ok := pass.TypesInfo.Types[value]; ok && con.Value != nil { - str := con.Value.ExactString() // temp var to be able to take address - constVal = &str - } - } - - em, ok := pkgEnums[named.Obj().Name()] - if !ok { - continue - } - em.add(obj.Name(), constVal) - pkgEnums[named.Obj().Name()] = em - } - } - } + /* + NOTE: + + type T int + const A T = iota // obj.Type() is T + + type R T + const B R = iota // obj.Type() is R + + type T2 int + type T1 = T2 + const C T1 = iota // obj.Type() is T2 + + type T3 = T4 + type T4 int + type T5 = T3 + const D T5 = iota // obj.Type() is T4 + + // And, in all these cases, validNamedBasic(obj.Type()) == true. + */ + + if !validNamedBasic(obj.Type()) { + return enumType{}, "", "", false } - // 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 v.numMembers() == 0 { - delete(pkgEnums, k) - } + named := obj.Type().(*types.Named) // guaranteed by validNamedBasic() + tn := named.Obj() + + // Enum type's scope and enum member's scope must be the same. If they're + // not, don't consider the const a member. Additionally, the enum type and + // the enum member must be in the same package (the scope check accounts for + // this, too). + if tn.Parent() != obj.Parent() { + return enumType{}, "", "", false } - return pkgEnums + return enumType{tn}, obj.Name(), determineConstVal(constName, info), true +} + +func determineConstVal(name *ast.Ident, info *types.Info) constantValue { + c := info.ObjectOf(name).(*types.Const) + return constantValue(c.Val().ExactString()) +} + +func isBlankIdentifier(obj types.Object) bool { + return obj.Name() == "_" // NOTE: go/types/decl.go does a direct comparison like this +} + +func validBasic(basic *types.Basic) bool { + switch i := basic.Info(); { + case i&types.IsInteger != 0, i&types.IsFloat != 0, i&types.IsString != 0: + return true + } + return false +} + +// validNamedBasic returns whether the type t is a named type whose underlying +// type is a valid basic type to form an enum. +// A type that passes this check meets the definition of an enum type. +// Note that +// validNamedBasic(t) == true => t.(*types.Named) +func validNamedBasic(t types.Type) bool { + named, ok := t.(*types.Named) + if !ok { + return false + } + basic, ok := named.Underlying().(*types.Basic) + if !ok || !validBasic(basic) { + return false + } + return true } diff --git a/vendor/github.com/nishanths/exhaustive/exhaustive.go b/vendor/github.com/nishanths/exhaustive/exhaustive.go index 73815a626..e852df757 100644 --- a/vendor/github.com/nishanths/exhaustive/exhaustive.go +++ b/vendor/github.com/nishanths/exhaustive/exhaustive.go @@ -1,113 +1,259 @@ -// Package exhaustive provides an analyzer that checks exhaustiveness of enum -// switch statements. 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 Go 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. (You may -// also use iota instead of explicitly specifying values.) -// -// type Biome int -// -// const ( -// Tundra Biome = 1 -// Savanna Biome = 2 -// Desert Biome = 3 -// ) -// -// 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 enabled, 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 -check-generated boolean flag, disabled by default, indicates whether -// to check switch statements in generated Go source files. -// -// The other relevant flag is the -fix flag; its behavior is described -// in the next section. -// -// 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. -// -// Skipping analysis -// -// 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. -// -// Additionally, no diagnostics are reported for switch statements in -// generated files (see https://golang.org/s/generatedcode for definition of -// generated file), unless the -check-generated flag is enabled. +/* +Package exhaustive provides an analyzer that checks exhaustiveness of enum +switch statements in Go source code. + +Definition of enum + +The Go language spec does not provide an explicit definition for an enum. For +the purpose of this analyzer, an enum type is any named type (a.k.a. defined +type) whose underlying type is an integer (includes byte and rune), a float, or +a string type. An enum type has associated with it constants of this named type; +these constants constitute the enum members. + +In the example below, Biome is an enum type with 3 members. + + type Biome int + + const ( + Tundra Biome = 1 + Savanna Biome = 2 + Desert Biome = 3 + ) + +For a constant to be an enum member for an enum type, the constant must be +declared in the same scope as the enum type. Note that the scope requirement +implies that only constants declared in the same package as the enum type's +package can constitute the enum members for the enum type. + +Enum member constants for a given enum type don't necessarily have to all be +declared in the same const block. Constant values may be specified using iota, +using explicit values, or by any means of declaring a valid Go const. It is +allowed for multiple enum member constants for a given 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 type's members are listed in the switch statement's cases. If +multiple enum member constants 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 exhaustiveness. +For an enum type defined in an external package, it is sufficient that only +exported enum members are listed. + +Only identifiers denoting constants (e.g. Tundra) and qualified identifiers +denoting constants (e.g. somepkg.Grassland) listed in a switch statement's cases +can contribute towards satisfying exhaustiveness. Literal values, struct fields, +re-assignable variables, etc. will not. + +The analyzer will produce a diagnostic about unhandled enum members if the +required memebers are not listed in a switch statement's cases (this applies +even if the switch statement has a 'default' case). + +Type aliases + +The analyzer handles type aliases for an enum type in the following manner. +Consider the example below. T2 is a enum type, and T1 is an alias for T2. Note +that we don't term T1 itself an enum type; it is only an alias for an enum +type. + + package pkg + type T1 = newpkg.T2 + const ( + A = newpkg.A + B = newpkg.B + ) + + package newpkg + type T2 int + const ( + A T2 = 1 + B T2 = 2 + ) + +Then 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. The same conditions +described in the previous section for same-valued enum members and for +exported/unexported enum members apply here too. + +It is worth noting that, though T1 and T2 are identical types, only constants +declared in the same scope as type T2's scope can be T2's enum members. In the +example, newpkg.A and newpkg.B are T2's enum members. + +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 new RHS type (newpkg.T2) is a +subset of the set of enum member constant values of the old LHS type (T1). + +Advanced notes + +Non-enum member constants in a switch statement's cases: Recall from an earlier +section that a constant must be declared in the same scope as the enum type to +be an enum member. It is valid, however, both to the Go type checker and to this +analyzer, for any constant of the right type to be listed in the cases of an +enum switch statement (it does not necessarily have to be an enum member +constant declared in the same scope/package as the enum type's scope/package). +This is particularly useful when a type alias is involved: A forwarding constant +declaration (such as pkg.A, in type T1's package) can take the place of the +actual enum member constant (newpkg.A, in type T2's package) in the switch +statement's cases to satisfy exhaustiveness. + + var v pkg.T1 = pkg.ReturnsT1() // v is effectively of type newpkg.T2 due to alias + switch v { + case pkg.A: // valid substitute for newpkg.A (same constant value) + case pkg.B: // valid substitute for newpkg.B (same constant value) + } + +Flags + +Notable flags supported by the analyzer are described below. +All of these flags are optional. + + flag type default value + + -explicit-exhaustive-switch bool false + -check-generated bool false + -default-signifies-exhaustive bool false + -ignore-enum-members string (none) + -package-scope-only bool false + + +If the -explicit-exhaustive-switch flag is enabled, the analyzer only runs on +switch statements explicitly marked with the comment text +("exhaustive:enforce"). Otherwise, it runs on every enum switch statement not +marked with the comment text ("exhaustive:ignore"). + +If the -check-generated flag is enabled, switch statements in generated Go +source files are also checked. Otherwise, by default, switch statements in +generated files are not checked. See https://golang.org/s/generatedcode for the +definition of generated file. + +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 not recommended that you enable this +flag; enabling it generally defeats the purpose of exhaustiveness checking. + +The -ignore-enum-members flag specifies a regular expression in Go syntax. Enum +members matching the regular expression don't 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/pkg" and the member +name is "Tundra", the specified regular expression will be matched against the +string "example.com/pkg.Tundra". + +If the -package-scope-only flag is enabled, the analyzer only finds enums +defined in package scopes, and consequently only switch statements that switch +on package-scoped 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. + +Skip analysis + +In implicitly exhaustive switch mode (-explicit-exhaustive-switch=false), skip +checking of a specific switch statement by associating the comment shown in +the example below with the switch statement. Note the lack of whitespace +between the comment marker ("//") and the comment text ("exhaustive:ignore"). + + //exhaustive:ignore + switch v { ... } + +In explicitly exhaustive switch mode (-explicit-exhaustive-switch=true), run +exhaustiveness checks on a specific switch statement by associating the +comment shown in the example below with the switch statement. + + //exhaustive:enforce + switch v { ... } + +To ignore specific enum members, see the -ignore-enum-members flag. + +Switch statements in generated Go source files are not checked by default. +Use the -check-generated flag to change this behavior. +*/ package exhaustive import ( - "go/ast" - "go/types" - "sort" - "strings" + "flag" + "regexp" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" ) +var _ flag.Value = (*regexpFlag)(nil) + +// regexpFlag implements the flag.Value interface for parsing +// regular expression flag values. +type regexpFlag struct{ r *regexp.Regexp } + +func (v *regexpFlag) String() string { + if v == nil || v.r == nil { + return "" + } + return v.r.String() +} + +func (v *regexpFlag) Set(expr string) error { + if expr == "" { + v.r = nil + return nil + } + + r, err := regexp.Compile(expr) + if err != nil { + return err + } + + v.r = r + return nil +} + +func (v *regexpFlag) value() *regexp.Regexp { return v.r } + +func init() { + Analyzer.Flags.BoolVar(&fExplicitExhaustiveSwitch, ExplicitExhaustiveSwitchFlag, false, "only run exhaustive check on switches with \"//exhaustive:enforce\" comment") + Analyzer.Flags.BoolVar(&fCheckGenerated, CheckGeneratedFlag, false, "check switch statements in generated files") + Analyzer.Flags.BoolVar(&fDefaultSignifiesExhaustive, DefaultSignifiesExhaustiveFlag, false, "presence of \"default\" case in switch statements satisfies exhaustiveness, even if all enum members are not listed") + Analyzer.Flags.Var(&fIgnoreEnumMembers, IgnoreEnumMembersFlag, "enum members matching `regex` do not have to be listed in switch statements to satisfy exhaustiveness") + Analyzer.Flags.BoolVar(&fPackageScopeOnly, PackageScopeOnlyFlag, false, "consider enums only in package scopes, not in inner scopes") + + var unused string + Analyzer.Flags.StringVar(&unused, IgnorePatternFlag, "", "no effect (deprecated); see -"+IgnoreEnumMembersFlag+" instead") + Analyzer.Flags.StringVar(&unused, CheckingStrategyFlag, "", "no effect (deprecated)") +} + // Flag names used by the analyzer. They are exported for use by analyzer // driver programs. const ( - DefaultSignifiesExhaustiveFlag = "default-signifies-exhaustive" + ExplicitExhaustiveSwitchFlag = "explicit-exhaustive-switch" CheckGeneratedFlag = "check-generated" + DefaultSignifiesExhaustiveFlag = "default-signifies-exhaustive" + IgnoreEnumMembersFlag = "ignore-enum-members" + PackageScopeOnlyFlag = "package-scope-only" + + IgnorePatternFlag = "ignore-pattern" // Deprecated: see IgnoreEnumMembersFlag instead. + CheckingStrategyFlag = "checking-strategy" // Deprecated. ) var ( + fExplicitExhaustiveSwitch bool + fCheckGenerated bool fDefaultSignifiesExhaustive bool - fCheckGeneratedFiles bool + fIgnoreEnumMembers regexpFlag + fPackageScopeOnly bool ) -func init() { - 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") - Analyzer.Flags.BoolVar(&fCheckGeneratedFiles, CheckGeneratedFlag, false, "check switch statements in generated files also") +// resetFlags resets the flag variables to their default values. +// Useful in tests. +func resetFlags() { + fExplicitExhaustiveSwitch = false + fCheckGenerated = false + fDefaultSignifiesExhaustive = false + fIgnoreEnumMembers = regexpFlag{} + fPackageScopeOnly = false } var Analyzer = &analysis.Analyzer{ @@ -115,76 +261,21 @@ var Analyzer = &analysis.Analyzer{ 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 package comment 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 { - Enums 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.Enums { - sortedKeys = append(sortedKeys, k) - } - sort.Strings(sortedKeys) - - var buf strings.Builder - for i, k := range sortedKeys { - v := e.Enums[k] - buf.WriteString(k) - buf.WriteString(":") - - for j, vv := range v.OrderedNames { - buf.WriteString(vv) - // add comma separator between each enum member in an enum type - if j != len(v.OrderedNames)-1 { - buf.WriteString(",") - } - } - // add semicolon separator between each enum type - if i != len(sortedKeys)-1 { - buf.WriteString("; ") - } - } - return buf.String() + FactTypes: []analysis.Fact{&enumMembersFact{}}, } func run(pass *analysis.Pass) (interface{}, error) { - e := findEnums(pass) - if len(e) != 0 { - pass.ExportPackageFact(&enumsFact{Enums: e}) - } - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - comments := make(map[*ast.File]ast.CommentMap) // CommentMap per package file, lazily populated by reference - generated := make(map[*ast.File]bool) - checkSwitchStatements(pass, inspect, comments, generated) - return nil, nil -} - -func enumTypeName(e *types.Named, samePkg bool) string { - if samePkg { - return e.Obj().Name() + for typ, members := range findEnums(fPackageScopeOnly, pass.Pkg, inspect, pass.TypesInfo) { + exportFact(pass, typ, members) } - return e.Obj().Pkg().Name() + "." + e.Obj().Name() + + checkSwitchStatements(pass, inspect, config{ + explicitExhaustiveSwitch: fExplicitExhaustiveSwitch, + defaultSignifiesExhaustive: fDefaultSignifiesExhaustive, + checkGeneratedFiles: fCheckGenerated, + ignoreEnumMembers: fIgnoreEnumMembers.value(), + }) + return nil, nil } diff --git a/vendor/github.com/nishanths/exhaustive/fact.go b/vendor/github.com/nishanths/exhaustive/fact.go new file mode 100644 index 000000000..ecf0907b2 --- /dev/null +++ b/vendor/github.com/nishanths/exhaustive/fact.go @@ -0,0 +1,28 @@ +package exhaustive + +import "golang.org/x/tools/go/analysis" + +// NOTE: Fact types must remain gob-coding compatible. +// See TestFactsGob. + +var _ analysis.Fact = (*enumMembersFact)(nil) + +type enumMembersFact struct{ Members enumMembers } + +func (f *enumMembersFact) AFact() {} +func (f *enumMembersFact) String() string { return f.Members.factString() } + +// exportFact exports the enum members for the given enum type. +func exportFact(pass *analysis.Pass, enumTyp enumType, members enumMembers) { + pass.ExportObjectFact(enumTyp.factObject(), &enumMembersFact{members}) +} + +// importFact imports the enum members for the given possible enum type. +// An (_, false) return indicates that the enum type is not a known one. +func importFact(pass *analysis.Pass, possibleEnumType enumType) (enumMembers, bool) { + var f enumMembersFact + if !pass.ImportObjectFact(possibleEnumType.factObject(), &f) { + return enumMembers{}, false + } + return f.Members, true +} diff --git a/vendor/github.com/nishanths/exhaustive/generated.go b/vendor/github.com/nishanths/exhaustive/generated.go deleted file mode 100644 index 19b4fb12b..000000000 --- a/vendor/github.com/nishanths/exhaustive/generated.go +++ /dev/null @@ -1,34 +0,0 @@ -package exhaustive - -import ( - "go/ast" - "strings" -) - -// Adapated from https://gotools.org/dmitri.shuralyov.com/go/generated - -func isGeneratedFile(file *ast.File) bool { - for _, c := range file.Comments { - for _, cc := range c.List { - s := cc.Text // "\n" already removed (see doc comment) - if len(s) >= 1 && s[len(s)-1] == '\r' { - s = s[:len(s)-1] // Trim "\r". - } - if containsGeneratedComment(s) { - return true - } - } - } - - return false -} - -func containsGeneratedComment(s string) bool { - return strings.HasPrefix(s, genCommentPrefix) && - strings.HasSuffix(s, genCommentSuffix) -} - -const ( - genCommentPrefix = "// Code generated " - genCommentSuffix = " DO NOT EDIT." -) diff --git a/vendor/github.com/nishanths/exhaustive/go.mod b/vendor/github.com/nishanths/exhaustive/go.mod index 9a75e5152..e50629727 100644 --- a/vendor/github.com/nishanths/exhaustive/go.mod +++ b/vendor/github.com/nishanths/exhaustive/go.mod @@ -2,4 +2,4 @@ module github.com/nishanths/exhaustive go 1.14 -require golang.org/x/tools v0.0.0-20201011145850-ed2f50202694 +require golang.org/x/tools v0.1.10 diff --git a/vendor/github.com/nishanths/exhaustive/go.sum b/vendor/github.com/nishanths/exhaustive/go.sum index 4f00a79cc..3f735d1c2 100644 --- a/vendor/github.com/nishanths/exhaustive/go.sum +++ b/vendor/github.com/nishanths/exhaustive/go.sum @@ -1,38 +1,29 @@ -github.com/yuin/goldmark v1.1.27 h1:nqDD4MMMQA0lmWq03Z2/myGPYLQoXtmi0rGVs95ntbo= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 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 h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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/tools v0.0.0-20201011145850-ed2f50202694 h1:BANdcOVw3KTuUiyfDp7wrzCpkCe8UP3lowugJngxBTg= -golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/nishanths/exhaustive/switch.go b/vendor/github.com/nishanths/exhaustive/switch.go index 2cec7f9cb..dcf3b6d0a 100644 --- a/vendor/github.com/nishanths/exhaustive/switch.go +++ b/vendor/github.com/nishanths/exhaustive/switch.go @@ -1,14 +1,11 @@ package exhaustive import ( - "bytes" "fmt" "go/ast" - "go/printer" - "go/token" "go/types" + "regexp" "sort" - "strconv" "strings" "golang.org/x/tools/go/analysis" @@ -16,416 +13,346 @@ import ( "golang.org/x/tools/go/ast/inspector" ) -func isDefaultCase(c *ast.CaseClause) bool { - return c.List == nil // see doc comment on field -} +// nodeVisitor is like the visitor function used by Inspector.WithStack, +// except that it returns an additional value: a short description of +// the result of this node visit. +// +// The result is typically useful in debugging or in unit tests to check +// that the nodeVisitor function took the expected code path. +type nodeVisitor func(n ast.Node, push bool, stack []ast.Node) (proceed bool, result string) + +// Result values returned by a node visitor constructed via switchStmtChecker. +const ( + resultNotPush = "not push" + resultGeneratedFile = "generated file" + resultNoSwitchTag = "no switch tag" + resultTagNotValue = "switch tag not value type" + resultTagNotNamed = "switch tag not named type" + resultTagNoPkg = "switch tag does not belong to regular package" + resultTagNotEnum = "switch tag not known enum type" + resultSwitchIgnoreComment = "switch statement has ignore comment" + resultSwitchNoEnforceComment = "switch statement has no enforce comment" + resultEnumMembersAccounted = "requisite enum members accounted for" + resultDefaultCaseSuffices = "default case presence satisfies exhaustiveness" + resultReportedDiagnostic = "reported diagnostic" +) -func checkSwitchStatements( - pass *analysis.Pass, - inspect *inspector.Inspector, - comments map[*ast.File]ast.CommentMap, - generated map[*ast.File]bool, -) { - inspect.WithStack([]ast.Node{&ast.SwitchStmt{}}, func(n ast.Node, push bool, stack []ast.Node) bool { +// switchStmtChecker returns a node visitor that checks exhaustiveness +// of enum switch statements for the supplied pass, and reports diagnostics for +// switch statements that are non-exhaustive. +// It expects to only see *ast.SwitchStmt nodes. +func switchStmtChecker(pass *analysis.Pass, cfg config) nodeVisitor { + generated := make(map[*ast.File]bool) // cached results + comments := make(map[*ast.File]ast.CommentMap) // cached results + + return func(n ast.Node, push bool, stack []ast.Node) (bool, string) { if !push { - return true + // 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) - // Determine if file is a generated file, based on https://golang.org/s/generatedcode. - // If generated, don't check this file. - var isGenerated bool - if gen, ok := generated[file]; ok { - isGenerated = gen - } else { - isGenerated = isGeneratedFile(file) - generated[file] = isGenerated + // Determine if the file is a generated file, and save the result. + // If it is a generated file, don't check the file. + if _, ok := generated[file]; !ok { + generated[file] = isGeneratedFile(file) } - if isGenerated && !fCheckGeneratedFiles { - // don't check - return true + if generated[file] && !cfg.checkGeneratedFiles { + // Don't check this file. + // Return false because the children nodes of node `n` don't have to be checked. + return false, resultGeneratedFile } sw := n.(*ast.SwitchStmt) + + if _, ok := comments[file]; !ok { + comments[file] = ast.NewCommentMap(pass.Fset, file, file.Comments) + } + switchComments := comments[file][sw] + if !cfg.explicitExhaustiveSwitch && containsIgnoreDirective(switchComments) { + // 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. + return true, resultSwitchIgnoreComment + } + if cfg.explicitExhaustiveSwitch && !containsEnforceDirective(switchComments) { + // Skip checking of this switch statement due to missing enforce directive comment. + return true, resultSwitchNoEnforceComment + } + if sw.Tag == nil { - return true + return true, resultNoSwitchTag } + t := pass.TypesInfo.Types[sw.Tag] if !t.IsValue() { - return true + return true, resultTagNotValue } + tagType, ok := t.Type.(*types.Named) if !ok { - return true + return true, resultTagNotNamed } tagPkg := tagType.Obj().Pkg() if tagPkg == nil { - // Doc comment: nil for labels and objects in the Universe scope. + // The Go documentation says: 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 + return true, resultTagNoPkg } - var enums enumsFact - if !pass.ImportPackageFact(tagPkg, &enums) { - // Can't do anything further. - return true + enumTyp := enumType{tagType.Obj()} + members, ok := importFact(pass, enumTyp) + if !ok { + // switch tag's type is not a known enum type. + return true, resultTagNotEnum } - em, isEnum := enums.Enums[tagType.Obj().Name()] - if !isEnum { - // Tag's type is not a known enum. - return true - } + 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) - // Get comment map. - var allComments ast.CommentMap - if cm, ok := comments[file]; ok { - allComments = cm - } else { - allComments = ast.NewCommentMap(pass.Fset, file, file.Comments) - comments[file] = allComments - } + hasDefaultCase := analyzeSwitchClauses(sw, pass.TypesInfo, func(val constantValue) { + checklist.found(val) + }) - specificComments := allComments.Filter(sw) - for _, group := range specificComments.Comments() { - if containsIgnoreDirective(group.List) { - return true // skip checking due to ignore directive - } + if len(checklist.remaining()) == 0 { + // All enum members accounted for. + // Nothing to report. + return true, resultEnumMembersAccounted } - - samePkg := tagPkg == pass.Pkg - checkUnexported := samePkg - - hitlist := hitlistFromEnumMembers(em, checkUnexported) - if len(hitlist) == 0 { - // can happen if external package and enum consists only of - // unexported members - return true + if hasDefaultCase && 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(makeDiagnostic(sw, samePkg, enumTyp, members, checklist.remaining())) + return true, resultReportedDiagnostic + } +} - 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 - } - updateHitlist(hitlist, em, 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 - } - - updateHitlist(hitlist, em, selExpr.Sel.Name) - } - } - } +// config is configuration for checkSwitchStatements. +type config struct { + explicitExhaustiveSwitch bool + defaultSignifiesExhaustive bool + checkGeneratedFiles bool + ignoreEnumMembers *regexp.Regexp // can be nil +} - defaultSuffices := fDefaultSignifiesExhaustive && defaultCaseExists - shouldReport := len(hitlist) > 0 && !defaultSuffices +// checkSwitchStatements checks exhaustiveness of enum switch statements for the supplied +// pass. It reports switch statements that are not exhaustive via pass.Report. +func checkSwitchStatements(pass *analysis.Pass, inspect *inspector.Inspector, cfg config) { + f := switchStmtChecker(pass, cfg) - if shouldReport { - reportSwitch(pass, sw, samePkg, tagType, em, hitlist, defaultCaseExists, file) - } - return true + inspect.WithStack([]ast.Node{&ast.SwitchStmt{}}, func(n ast.Node, push bool, stack []ast.Node) bool { + proceed, _ := f(n, push, stack) + return proceed }) } -func updateHitlist(hitlist map[string]struct{}, em *enumMembers, foundName string) { - constVal, ok := em.NameToValue[foundName] - if !ok { - // only delete the name alone from hitlist - delete(hitlist, foundName) - return - } - - // delete all of the same-valued names from hitlist - namesToDelete := em.ValueToNames[constVal] - for _, n := range namesToDelete { - delete(hitlist, n) - } +func isDefaultCase(c *ast.CaseClause) bool { + return c.List == nil // see doc comment on List field } -func isPackageNameIdentifier(pass *analysis.Pass, ident *ast.Ident) bool { - obj := pass.TypesInfo.ObjectOf(ident) +func denotesPackage(ident *ast.Ident, info *types.Info) (*types.Package, bool) { + obj := info.ObjectOf(ident) if obj == nil { - return false + return nil, false } - _, ok := obj.(*types.PkgName) - return ok + n, ok := obj.(*types.PkgName) + if !ok { + return nil, false + } + return n.Imported(), true } -func hitlistFromEnumMembers(em *enumMembers, checkUnexported bool) map[string]struct{} { - hitlist := make(map[string]struct{}) - for _, m := range em.OrderedNames { - if m == "_" { - // blank identifier is often used to skip entries in iota lists - continue +// analyzeSwitchClauses analyzes the clauses in the supplied switch statement. +// The info param should typically be pass.TypesInfo. The found 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) { + for _, stmt := range sw.Body.List { + caseCl := stmt.(*ast.CaseClause) + if isDefaultCase(caseCl) { + hasDefaultCase = true + continue // nothing more to do if it's the default case } - if !ast.IsExported(m) && !checkUnexported { - continue + for _, expr := range caseCl.List { + analyzeCaseClauseExpr(expr, info, found) } - hitlist[m] = struct{}{} } - return hitlist + return hasDefaultCase } -func determineMissingOutput(missingMembers map[string]struct{}, em *enumMembers) []string { - constValMembers := make(map[string][]string) // value -> names - var otherMembers []string // non-constant value names - - for m := range missingMembers { - if constVal, ok := em.NameToValue[m]; ok { - constValMembers[constVal] = append(constValMembers[constVal], m) - } else { - otherMembers = append(otherMembers, m) +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 } - } - missingOutput := make([]string, 0, len(constValMembers)+len(otherMembers)) - for _, names := range constValMembers { - sort.Strings(names) - missingOutput = append(missingOutput, strings.Join(names, "|")) + // There are two scenarios. + // See related test cases in typealias/quux/quux.go. + // + // ### Scenario 1 + // + // Tag package and constant package are the same. + // + // For example: + // var mode fs.FileMode + // switch mode { + // case fs.ModeDir: + // } + // + // This is simple: we just use fs.ModeDir's value. + // + // ### Scenario 2 + // + // Tag package and constant package are different. + // + // For example: + // var mode fs.FileMode + // switch mode { + // case os.ModeDir: + // } + // + // Or equivalently: + // var mode os.FileMode // in effect, fs.FileMode because of type alias in package os + // switch mode { + // case os.ModeDir: + // } + // + // 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. + // + found(determineConstVal(ident, info)) } - missingOutput = append(missingOutput, otherMembers...) - sort.Strings(missingOutput) - return missingOutput -} -func reportSwitch( - pass *analysis.Pass, - sw *ast.SwitchStmt, - samePkg bool, - enumType *types.Named, - em *enumMembers, - missingMembers map[string]struct{}, - defaultCaseExists bool, - f *ast.File, -) { - missingOutput := determineMissingOutput(missingMembers, em) - - var fixes []analysis.SuggestedFix - if !defaultCaseExists { - if fix, ok := computeFix(pass, pass.Fset, f, sw, enumType, samePkg, missingMembers); ok { - fixes = append(fixes, fix) + 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`. + // Check that X, which is everything except the rightmost *ast.Ident (or + // Sel), is also an *ast.Ident. + xIdent, ok := x.(*ast.Ident) + if !ok { + return + } + // Doesn't matter which package, just that it denotes a package. + if _, ok := denotesPackage(xIdent, info); !ok { + return } + handleIdent(e.Sel) } - - 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(missingOutput, ", ")), - 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)) +// 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 { + missingByConstVal := make(map[constantValue][]string) // missing members, keyed by constant value. for m := range missingMembers { - missing = append(missing, m) + val := em.NameToValue[m] + missingByConstVal[val] = append(missingByConstVal[val], 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 - } + var out []string + for _, names := range missingByConstVal { + sort.Strings(names) + out = append(out, strings.Join(names, "|")) } - return false + sort.Strings(out) + return out } -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 - } +// 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 nil + return enumType.Pkg().Name() + "." + enumType.Name() } -// 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] +// Makes a "missing cases in switch" diagnostic. +// samePkg should be true if the enum type and the switch statement are defined +// in the same package. +func makeDiagnostic(sw *ast.SwitchStmt, samePkg bool, enumTyp enumType, allMembers enumMembers, missingMembers map[string]struct{}) analysis.Diagnostic { + message := fmt.Sprintf("missing cases in switch of type %s: %s", + diagnosticEnumTypeName(enumTyp.TypeName, samePkg), + strings.Join(diagnosticMissingMembers(missingMembers, allMembers), ", ")) + + return analysis.Diagnostic{ + Pos: sw.Pos(), + End: sw.End(), + Message: message, } - 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 +// A checklist holds a set of enum member names that have to be +// accounted for to satisfy exhaustiveness in an enum switch statement. +// +// The found method checks off member names from the set, based on +// constant value, when a constant value is encoutered in the switch +// statement's cases. +// +// The remaining method returns the member names not accounted for. +// +type checklist struct { + em enumMembers + checkl map[string]struct{} } -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" - )`), - } - } +func makeChecklist(em enumMembers, enumPkg *types.Package, includeUnexported bool, ignore *regexp.Regexp) *checklist { + checkl := make(map[string]struct{}) - // 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 + 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{}{} } - // 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(), + for _, name := range em.Names { + add(name) } -} -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 - } - } + return &checklist{ + em: em, + checkl: checkl, } +} - 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) - } +func (c *checklist) found(val constantValue) { + // Delete all of the same-valued names. + for _, name := range c.em.ValueToNames[val] { + delete(c.checkl, name) } - 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), - } +func (c *checklist) remaining() map[string]struct{} { + return c.checkl } -- cgit mrf-deployment