Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
BurntSushi committed Mar 19, 2017
0 parents commit d2b4005
Show file tree
Hide file tree
Showing 12 changed files with 978 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: go
3 changes: 3 additions & 0 deletions COPYING
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This project is dual-licensed under the Unlicense and MIT licenses.

You may use this code under the terms of either license.
21 changes: 21 additions & 0 deletions LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 Andrew Gallant

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
go-sumtype
==========
A simple utility for running exhaustiveness checks on type switch statements.
Exhaustiveness checks are only run on interfaces that are declared to be
sum types.

[![Linux build status](https://api.travis-ci.org/BurntSushi/go-sumtype.png)](https://travis-ci.org/BurntSushi/go-sumtype)

Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).

### Installation

```go
$ go get github.com/BurntSushi/go-sumtype
```

For usage info, just run the command:

```
$ go-sumtype
```

### Usage

go-sumtype takes a list of Go package paths or files and looks for sum type
declarations in each package/file provided. Exhaustiveness checks are then
performed for each use of a declared sum type in a type switch statement.
Namely, `go-sumtype` will report an error for any type switch statement that
either lacks a `default` clause or does not account for all possible variants.

Declarations are provided in comments like so:

```
//go-sumtype:decl MySumType
```

`MySumType` must satisfy the following:

1. It is a type defined in the same package.
2. It is an interface.
3. It is *sealed*. That is, part of its interface definition contains an
unexported method.

`go-sumtype` will produce an error if any of the above is not true.

For valid declarations, `go-sumtype` will look for all occurrences in which a
value of type `MySumType` participates in a type switch statement. In those
occurrences, it will attempt to detect whether the type switch is exhaustive
or not. If it's not, `go-sumtype` will report an error. For example:

```
$ cat mysumtype.go
package main
//go-sumtype:decl MySumType
type MySumType interface {
sealed()
}
type VariantA struct{}
func (a *VariantA) sealed() {}
type VariantB struct{}
func (b *VariantB) sealed() {}
func main() {
switch MySumType(nil).(type) {
case *VariantA:
}
}
$ go-sumtype mysumtype.go
mysumtype.go:18:2: exhaustiveness check failed for sum type 'MySumType': missing cases for VariantB
```

Adding either a `default` clause or a clause to handle `*VariantB` will cause
exhaustive checks to pass.

As a special case, if the type switch statement contains a `default` clause
that always panics, then exhaustiveness checks are still performed.
24 changes: 24 additions & 0 deletions UNLICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>
185 changes: 185 additions & 0 deletions check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package main

import (
"fmt"
"go/ast"
"go/token"
"go/types"
"sort"
"strings"

"golang.org/x/tools/go/loader"
)

// inexhaustiveError is returned from check for each occurrence of inexhaustive
// case analysis in a Go type switch statement.
type inexhaustiveError struct {
Pos token.Position
Def sumTypeDef
Missing []types.Object
}

func (e inexhaustiveError) Error() string {
return fmt.Sprintf(
"%s: exhaustiveness check failed for sum type '%s': missing cases for %s",
e.Pos, e.Def.Decl.TypeName, strings.Join(e.Names(), ", "))
}

// Names returns a sorted list of names corresponding to the missing variant
// cases.
func (e inexhaustiveError) Names() []string {
var list []string
for _, o := range e.Missing {
list = append(list, o.Name())
}
sort.Sort(sort.StringSlice(list))
return list
}

// check does exhaustiveness checking for the given sum type definitions in the
// given package. Every instance of inexhaustive case analysis is returned.
func check(prog *loader.Program, defs []sumTypeDef, pkg *loader.PackageInfo) []error {
var errs []error
for _, astfile := range pkg.Files {
ast.Inspect(astfile, func(n ast.Node) bool {
swtch, ok := n.(*ast.TypeSwitchStmt)
if !ok {
return true
}
if err := checkSwitch(prog, pkg, defs, swtch); err != nil {
errs = append(errs, err)
}
return true
})
}
return errs
}

// checkSwitch performs an exhaustiveness check on the given type switch
// statement. If the type switch is used on a sum type and does not cover
// all variants of that sum type, then an error is returned indicating which
// variants were missed.
//
// Note that if the type switch contains a non-panicing default case, then
// exhaustiveness checks are disabled.
func checkSwitch(
prog *loader.Program,
pkg *loader.PackageInfo,
defs []sumTypeDef,
swtch *ast.TypeSwitchStmt,
) error {
def, missing := missingVariantsInSwitch(prog, pkg, defs, swtch)
if len(missing) > 0 {
return inexhaustiveError{
Pos: prog.Fset.Position(swtch.Pos()),
Def: *def,
Missing: missing,
}
}
return nil
}

// missingVariantsInSwitch returns a list of missing variants corresponding to
// the given switch statement. The corresponding sum type definition is also
// returned. (If no sum type definition could be found, then no exhaustiveness
// checks are performed, and therefore, no missing variants are returned.)
func missingVariantsInSwitch(
prog *loader.Program,
pkg *loader.PackageInfo,
defs []sumTypeDef,
swtch *ast.TypeSwitchStmt,
) (*sumTypeDef, []types.Object) {
asserted := findTypeAssertExpr(swtch)
ty := pkg.TypeOf(asserted)
def := findDef(defs, ty)
if def == nil {
// We couldn't find a corresponding sum type, so there's
// nothing we can do to check it.
return nil, nil
}
variantExprs, hasDefault := switchVariants(swtch)
if hasDefault && !defaultClauseAlwaysPanics(swtch) {
// A catch-all case defeats all exhaustiveness checks.
return def, nil
}
var variantTypes []types.Type
for _, expr := range variantExprs {
variantTypes = append(variantTypes, pkg.TypeOf(expr))
}
return def, def.missing(variantTypes)
}

// switchVariants returns all case expressions found in a type switch. This
// includes expressions from cases that have a list of expressions.
func switchVariants(swtch *ast.TypeSwitchStmt) (exprs []ast.Expr, hasDefault bool) {
for _, stmt := range swtch.Body.List {
clause := stmt.(*ast.CaseClause)
if clause.List == nil {
hasDefault = true
} else {
exprs = append(exprs, clause.List...)
}
}
return
}

// defaultClauseAlwaysPanics returns true if the given switch statement has a
// default clause that always panics. Note that this is done on a best-effort
// basis. While there will never be any false positives, there may be false
// negatives.
//
// If the given switch statement has no default clause, then this function
// panics.
func defaultClauseAlwaysPanics(swtch *ast.TypeSwitchStmt) bool {
var clause *ast.CaseClause
for _, stmt := range swtch.Body.List {
c := stmt.(*ast.CaseClause)
if c.List == nil {
clause = c
break
}
}
if clause == nil {
panic("switch statement has no default clause")
}
if len(clause.Body) != 1 {
return false
}
exprStmt, ok := clause.Body[0].(*ast.ExprStmt)
if !ok {
return false
}
callExpr, ok := exprStmt.X.(*ast.CallExpr)
if !ok {
return false
}
fun, ok := callExpr.Fun.(*ast.Ident)
if !ok {
return false
}
return fun.Name == "panic"
}

// findTypeAssertExpr extracts the expression that is being type asserted from a
// type swtich statement.
func findTypeAssertExpr(swtch *ast.TypeSwitchStmt) ast.Expr {
var expr ast.Expr
if assign, ok := swtch.Assign.(*ast.AssignStmt); ok {
expr = assign.Rhs[0]
} else {
expr = swtch.Assign.(*ast.ExprStmt).X
}
return expr.(*ast.TypeAssertExpr).X
}

// findDef returns the sum type definition corresponding to the given type. If
// no such sum type definition exists, then nil is returned.
func findDef(defs []sumTypeDef, needle types.Type) *sumTypeDef {
for i := range defs {
def := &defs[i]
if types.Identical(needle.Underlying(), def.Ty) {
return def
}
}
return nil
}
Loading

0 comments on commit d2b4005

Please sign in to comment.