mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-12 03:26:30 +00:00
c28f7cb29f
Initial part of #435
207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
// 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 4 flags.
|
|
//
|
|
// The -default-signifies-exhaustive boolean flag indicates to the analyzer
|
|
// whether 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 default value is false.
|
|
//
|
|
// The -check-generated boolean flag indicates whether to check switch
|
|
// statements in generated Go source files. The default value is false.
|
|
//
|
|
// The -ignore-pattern flag specifies a regular expression. Member names
|
|
// in enum definitions that match the regular expression do not require a case
|
|
// clause to satisfy exhaustiveness. The regular expression is matched against
|
|
// enum member names inclusive of the import path, e.g. of the
|
|
// form: github.com/foo/bar.Tundra, where the import path is github.com/foo/bar
|
|
// and the enum member name is Tundra.
|
|
//
|
|
// The behavior of the -fix flag is described in the next section.
|
|
//
|
|
// Fixes
|
|
//
|
|
// The analyzer suggests fixes for a switch statement if it is not exhaustive.
|
|
// 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.
|
|
//
|
|
// 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.
|
|
//
|
|
// Additionally, see the -ignore-pattern flag.
|
|
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"
|
|
)
|
|
|
|
// Flag names used by the analyzer. They are exported for use by analyzer
|
|
// driver programs.
|
|
const (
|
|
DefaultSignifiesExhaustiveFlag = "default-signifies-exhaustive"
|
|
CheckGeneratedFlag = "check-generated"
|
|
IgnorePatternFlag = "ignore-pattern"
|
|
)
|
|
|
|
var (
|
|
fDefaultSignifiesExhaustive bool
|
|
fCheckGeneratedFiles bool
|
|
fIgnorePattern regexpFlag
|
|
)
|
|
|
|
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")
|
|
Analyzer.Flags.Var(&fIgnorePattern, IgnorePatternFlag, "do not require a case clause to satisfy exhaustiveness for enum member names that match the provided regular expression pattern")
|
|
}
|
|
|
|
// resetFlags resets the flag variables to their default values.
|
|
// Useful in tests.
|
|
func resetFlags() {
|
|
fDefaultSignifiesExhaustive = false
|
|
fCheckGeneratedFiles = false
|
|
fIgnorePattern = regexpFlag{}
|
|
}
|
|
|
|
var Analyzer = &analysis.Analyzer{
|
|
Name: "exhaustive",
|
|
Doc: "check exhaustiveness of enum switch statements",
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
|
FactTypes: []analysis.Fact{&enumsFact{}},
|
|
}
|
|
|
|
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)
|
|
err := checkSwitchStatements(pass, inspect)
|
|
return nil, err
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
func enumTypeName(e *types.Named, samePkg bool) string {
|
|
if samePkg {
|
|
return e.Obj().Name()
|
|
}
|
|
return e.Obj().Pkg().Name() + "." + e.Obj().Name()
|
|
}
|