From 80ad0aabbb0d4b0d1c208b871587f527124bd774 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Fri, 3 May 2024 23:27:17 -1000 Subject: [PATCH] Allow `//exhaustive:ignore` on potential enums (#78) Fixes https://github.com/nishanths/exhaustive/issues/76 --- comment.go | 3 ++ enum.go | 57 +++++++++++++++++++++++++++++++++-- enum_test.go | 62 ++++++++++++++++++++++++++++++++++++++- exhaustive.go | 2 +- testdata/src/enum/enum.go | 53 +++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 4 deletions(-) diff --git a/comment.go b/comment.go index caf7649..e3347d9 100644 --- a/comment.go +++ b/comment.go @@ -29,6 +29,9 @@ type directiveSet int64 func parseDirectives(commentGroups []*ast.CommentGroup) (directiveSet, error) { var out directiveSet for _, commentGroup := range commentGroups { + if commentGroup == nil { + continue + } for _, comment := range commentGroup.List { commentLine := comment.Text if !strings.HasPrefix(commentLine, exhaustiveComment) { diff --git a/enum.go b/enum.go index cabf1d8..e95e78e 100644 --- a/enum.go +++ b/enum.go @@ -7,6 +7,7 @@ import ( "go/types" "strings" + "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/inspector" ) @@ -63,16 +64,33 @@ func (em *enumMembers) factString() string { return buf.String() } -func findEnums(pkgScopeOnly bool, pkg *types.Package, inspect *inspector.Inspector, info *types.Info) map[enumType]enumMembers { +func findEnums(pass *analysis.Pass, pkgScopeOnly bool, pkg *types.Package, inspect *inspector.Inspector, info *types.Info) map[enumType]enumMembers { result := make(map[enumType]enumMembers) + ignoredTypes := findIgnoredTypes(pass, inspect, info) + inspect.Preorder([]ast.Node{&ast.GenDecl{}}, func(n ast.Node) { gen := n.(*ast.GenDecl) if gen.Tok != token.CONST { return } + + if hasIgnoreDecl(pass, gen.Doc) { + return + } + for _, s := range gen.Specs { - for _, name := range s.(*ast.ValueSpec).Names { + s := s.(*ast.ValueSpec) + if hasIgnoreDecl(pass, s.Doc) { + continue + } + + for _, name := range s.Names { + + if _, ignored := ignoredTypes[info.Defs[name].Type()]; ignored { + continue + } + enumTyp, memberName, val, ok := possibleEnumMember(name, info) if !ok { continue @@ -140,6 +158,32 @@ func possibleEnumMember(constName *ast.Ident, info *types.Info) (et enumType, na return enumType{tn}, obj.Name(), determineConstVal(constName, info), true } +func findIgnoredTypes(pass *analysis.Pass, inspect *inspector.Inspector, info *types.Info) map[types.Type]struct{} { + ignoredTypes := map[types.Type]struct{}{} + + inspect.Preorder([]ast.Node{&ast.GenDecl{}}, func(n ast.Node) { + gen := n.(*ast.GenDecl) + if gen.Tok != token.TYPE { + return + } + + doIgnoreDecl := hasIgnoreDecl(pass, gen.Doc) + + for _, s := range gen.Specs { + t := s.(*ast.TypeSpec) + + doIgnoreSpec := doIgnoreDecl || hasIgnoreDecl(pass, t.Doc) + if !doIgnoreSpec { + continue + } + + ignoredTypes[info.Defs[t.Name].Type()] = struct{}{} + } + }) + + return ignoredTypes +} + func determineConstVal(name *ast.Ident, info *types.Info) constantValue { c := info.ObjectOf(name).(*types.Const) return constantValue(c.Val().ExactString()) @@ -157,6 +201,15 @@ func validBasic(basic *types.Basic) bool { return false } +func hasIgnoreDecl(pass *analysis.Pass, doc *ast.CommentGroup) bool { + dirs, err := parseDirectives([]*ast.CommentGroup{doc}) + if err != nil { + pass.Report(makeInvalidDirectiveDiagnostic(doc, err)) + return false + } + return dirs.has(ignoreDirective) +} + // 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. diff --git a/enum_test.go b/enum_test.go index bac3f08..8ee6772 100644 --- a/enum_test.go +++ b/enum_test.go @@ -96,7 +96,7 @@ func TestFindEnums(t *testing.T) { for _, pkgOnly := range [...]bool{false, true} { t.Run(fmt.Sprint("pkgOnly", pkgOnly), func(t *testing.T) { - result := findEnums(pkgOnly, testdataEnumPkg.Types, inspect, testdataEnumPkg.TypesInfo) + result := findEnums(nil, pkgOnly, testdataEnumPkg.Types, inspect, testdataEnumPkg.TypesInfo) checkEnums(t, transform(result), pkgOnly) }) } @@ -366,6 +366,66 @@ func checkEnums(t *testing.T, got []checkEnum, pkgOnly bool) { `1`: {"Float64B"}, }, }}, + {"DeclGroupIgnoredEnum", enumMembers{ + []string{"DeclGroupIgnoredMemberC"}, + map[string]token.Pos{ + "DeclGroupIgnoredMemberC": 0, + }, + map[string]constantValue{ + "DeclGroupIgnoredMemberC": `3`, + }, + map[constantValue][]string{ + `3`: {"DeclGroupIgnoredMemberC"}, + }, + }}, + {"DeclIgnoredEnum", enumMembers{ + []string{"DeclIgnoredMemberB"}, + map[string]token.Pos{ + "DeclIgnoredMemberB": 0, + }, + map[string]constantValue{ + "DeclIgnoredMemberB": `2`, + }, + map[constantValue][]string{ + `2`: {"DeclIgnoredMemberB"}, + }, + }}, + {"DeclTypeInnerNotIgnore", enumMembers{ + []string{"DeclTypeInnerNotIgnoreMember"}, + map[string]token.Pos{ + "DeclTypeInnerNotIgnoreMember": 0, + }, + map[string]constantValue{ + "DeclTypeInnerNotIgnoreMember": `5`, + }, + map[constantValue][]string{ + `5`: {"DeclTypeInnerNotIgnoreMember"}, + }, + }}, + {"DeclTypeIgnoredValue", enumMembers{ + []string{"DeclTypeNotIgnoredValue"}, + map[string]token.Pos{ + "DeclTypeNotIgnoredValue": 0, + }, + map[string]constantValue{ + "DeclTypeNotIgnoredValue": `1`, + }, + map[constantValue][]string{ + `1`: {"DeclTypeNotIgnoredValue"}, + }, + }}, + {"DeclTypePartialIgnore", enumMembers{ + []string{"DeclTypePartialIgnoreNotIgnored"}, + map[string]token.Pos{ + "DeclTypePartialIgnoreNotIgnored": 0, + }, + map[string]constantValue{ + "DeclTypePartialIgnoreNotIgnored": `2`, + }, + map[constantValue][]string{ + `2`: {"DeclTypePartialIgnoreNotIgnored"}, + }, + }}, } for _, c := range wantPkg { diff --git a/exhaustive.go b/exhaustive.go index 013ac47..470b56d 100644 --- a/exhaustive.go +++ b/exhaustive.go @@ -108,7 +108,7 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for typ, members := range findEnums(fPackageScopeOnly, pass.Pkg, inspect, pass.TypesInfo) { + for typ, members := range findEnums(pass, fPackageScopeOnly, pass.Pkg, inspect, pass.TypesInfo) { exportFact(pass, typ, members) } diff --git a/testdata/src/enum/enum.go b/testdata/src/enum/enum.go index 4135243..37961be 100644 --- a/testdata/src/enum/enum.go +++ b/testdata/src/enum/enum.go @@ -86,3 +86,56 @@ const ( ) func (WithMethod) String() string { return "whatever" } + +type DeclGroupIgnoredEnum int // want DeclGroupIgnoredEnum:"^DeclGroupIgnoredMemberC$" + +//exhaustive:ignore +const ( + DeclGroupIgnoredMemberA DeclGroupIgnoredEnum = 1 + DeclGroupIgnoredMemberB DeclGroupIgnoredEnum = 2 +) + +const DeclGroupIgnoredMemberC DeclGroupIgnoredEnum = 3 + +type DeclIgnoredEnum int // want DeclIgnoredEnum:"^DeclIgnoredMemberB$" + +//exhaustive:ignore +const DeclIgnoredMemberA DeclIgnoredEnum = 1 + +const DeclIgnoredMemberB DeclIgnoredEnum = 2 + +//exhaustive:ignore +type DeclTypeIgnoredEnum int + +const ( + DeclTypeIgnoredMemberA DeclTypeIgnoredEnum = 1 + DeclTypeIgnoredMemberB DeclTypeIgnoredEnum = 2 +) + +type ( + //exhaustive:ignore + DeclTypeInnerIgnore int + DeclTypeInnerNotIgnore int // want DeclTypeInnerNotIgnore:"^DeclTypeInnerNotIgnoreMember$" +) + +const ( + DeclTypeInnerIgnoreMemberA DeclTypeInnerIgnore = 3 + DeclTypeInnerIgnoreMemberB DeclTypeInnerIgnore = 4 + DeclTypeInnerNotIgnoreMember DeclTypeInnerNotIgnore = 5 +) + +type DeclTypeIgnoredValue int // want DeclTypeIgnoredValue:"^DeclTypeNotIgnoredValue$" + +const ( + DeclTypeNotIgnoredValue DeclTypeIgnoredValue = 1 + //exhaustive:ignore + DeclTypeIsIgnoredValue DeclTypeIgnoredValue = 2 +) + +type DeclTypePartialIgnore int // want DeclTypePartialIgnore:"^DeclTypePartialIgnoreNotIgnored$" + +const ( + //exhaustive:ignore + DeclTypePartialIgnoreIgnored DeclTypePartialIgnore = 1 + DeclTypePartialIgnoreNotIgnored DeclTypePartialIgnore = 2 +)