Update error test case

There were several issues with the error test case that have been
addressed in this commit.

- It is possible to specify a whitelist of calls that error handling
  should be ignored for.
- Additional support for ast.ExprStmt for cases where the error is
  implicitly ignored.

There were several other additions to the helpers and call list in order
to support this type of functionality.

Fixes #54
This commit is contained in:
Grant Murphy 2016-11-18 14:09:10 -08:00
parent 5242a2c1df
commit 129be1561b
5 changed files with 187 additions and 69 deletions

View file

@ -13,7 +13,9 @@
package core
import "go/ast"
import (
"go/ast"
)
type set map[string]bool
@ -26,14 +28,11 @@ func NewCallList() CallList {
return make(CallList)
}
/// NewCallListFor createse a call list using the package path
func NewCallListFor(selector string, idents ...string) CallList {
c := NewCallList()
c[selector] = make(set)
/// AddAll will add several calls to the call list at once
func (c CallList) AddAll(selector string, idents ...string) {
for _, ident := range idents {
c.Add(selector, ident)
}
return c
}
/// Add a selector and call to the call list
@ -61,5 +60,14 @@ func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) bool {
if err != nil {
return false
}
return c.Contains(selector, ident)
// Try direct resolution
if c.Contains(selector, ident) {
return true
}
// Also support explicit path
if path, ok := GetImportPath(selector, ctx); ok {
return c.Contains(path, ident)
}
return false
}

View file

@ -21,13 +21,15 @@ func (r *callListRule) Match(n ast.Node, c *Context) (gi *Issue, err error) {
func TestCallListContainsCallExpr(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := NewAnalyzer(config, nil)
calls := NewCallList()
calls.AddAll("bytes.Buffer", "Write", "WriteTo")
rule := &callListRule{
MetaData: MetaData{
Severity: Low,
Confidence: Low,
What: "A dummy rule",
},
callList: NewCallListFor("bytes.Buffer", "Write", "WriteTo"),
callList: calls,
matched: 0,
}
analyzer.AddRule(rule, []ast.Node{(*ast.CallExpr)(nil)})

View file

@ -56,25 +56,17 @@ func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr {
//
func MatchCallByPackage(n ast.Node, c *Context, pkg string, names ...string) (*ast.CallExpr, bool) {
importName, imported := c.Imports.Imported[pkg]
if !imported {
importedName, found := GetImportedName(pkg, c)
if !found {
return nil, false
}
if _, initonly := c.Imports.InitOnly[pkg]; initonly {
return nil, false
}
if alias, ok := c.Imports.Aliased[pkg]; ok {
importName = alias
}
if callExpr, ok := n.(*ast.CallExpr); ok {
packageName, callName, err := GetCallInfo(callExpr, c)
if err != nil {
return nil, false
}
if packageName == importName {
if packageName == importedName {
for _, name := range names {
if callName == name {
return callExpr, true
@ -185,7 +177,38 @@ func GetCallInfo(n ast.Node, ctx *Context) (string, string, error) {
return expr.Name, fn.Sel.Name, nil
}
}
case *ast.Ident:
return ctx.Pkg.Name(), fn.Name, nil
}
}
return "", "", fmt.Errorf("unable to determine call info")
}
// GetImportedName returns the name used for the package within the
// code. It will resolve aliases and ignores initalization only imports.
func GetImportedName(path string, ctx *Context) (string, bool) {
importName, imported := ctx.Imports.Imported[path]
if !imported {
return "", false
}
if _, initonly := ctx.Imports.InitOnly[path]; initonly {
return "", false
}
if alias, ok := ctx.Imports.Aliased[path]; ok {
importName = alias
}
return importName, true
}
// GetImportPath resolves the full import path of an identifer based on
// the imports in the current context.
func GetImportPath(name string, ctx *Context) (string, bool) {
for path, _ := range ctx.Imports.Imported {
if imported, ok := GetImportedName(path, ctx); ok && imported == name {
return path, true
}
}
return "", false
}

View file

@ -15,47 +15,82 @@
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"go/ast"
"go/types"
"reflect"
gas "github.com/GoASTScanner/gas/core"
)
type NoErrorCheck struct {
gas.MetaData
whitelist gas.CallList
}
func (r *NoErrorCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node, ok := n.(*ast.AssignStmt); ok {
sel := reflect.TypeOf(&ast.CallExpr{})
if call, ok := gas.SimpleSelect(node.Rhs[0], sel).(*ast.CallExpr); ok {
if t := c.Info.Types[call].Type; t != nil {
if typeVal, typeErr := t.(*types.Tuple); typeErr {
for i := 0; i < typeVal.Len(); i++ {
if typeVal.At(i).Type().String() == "error" { // TODO(tkelsey): is there a better way?
if id, ok := node.Lhs[i].(*ast.Ident); ok && id.Name == "_" {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
func returnsError(callExpr *ast.CallExpr, ctx *gas.Context) int {
if tv := ctx.Info.TypeOf(callExpr); tv != nil {
switch t := tv.(type) {
case *types.Tuple:
for pos := 0; pos < t.Len(); pos += 1 {
variable := t.At(pos)
if variable != nil && variable.Type().String() == "error" {
return pos
}
}
case *types.Named:
if t.String() == "error" {
return 0
}
}
}
} else if t.String() == "error" { // TODO(tkelsey): is there a better way?
if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "_" {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
return -1
}
func (r *NoErrorCheck) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
switch stmt := n.(type) {
case *ast.AssignStmt:
for _, expr := range stmt.Rhs {
if callExpr, ok := expr.(*ast.CallExpr); ok && !r.whitelist.ContainsCallExpr(callExpr, ctx) {
pos := returnsError(callExpr, ctx)
if pos < 0 || pos >= len(stmt.Lhs) {
return nil, nil
}
if id, ok := stmt.Lhs[pos].(*ast.Ident); ok && id.Name == "_" {
return gas.NewIssue(ctx, n, r.What, r.Severity, r.Confidence), nil
}
}
}
case *ast.ExprStmt:
if callExpr, ok := stmt.X.(*ast.CallExpr); ok && !r.whitelist.ContainsCallExpr(callExpr, ctx) {
pos := returnsError(callExpr, ctx)
if pos >= 0 {
return gas.NewIssue(ctx, n, r.What, r.Severity, r.Confidence), nil
}
}
}
return nil, nil
}
func NewNoErrorCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// TODO(gm) Come up with sensible defaults here. Or flip it to use a
// black list instead.
whitelist := gas.NewCallList()
whitelist.AddAll("bytes.Buffer", "Write", "WriteByte", "WriteRune", "WriteString")
whitelist.AddAll("fmt", "Print", "Printf", "Println")
whitelist.Add("io.PipeWriter", "CloseWithError")
if configured, ok := conf["G104"]; ok {
if whitelisted, ok := configured.(map[string][]string); ok {
for key, val := range whitelisted {
whitelist.AddAll(key, val...)
}
}
}
return &NoErrorCheck{
MetaData: gas.MetaData{
Severity: gas.Low,
Confidence: gas.High,
What: "Errors unhandled.",
},
}, []ast.Node{(*ast.AssignStmt)(nil)}
whitelist: whitelist,
}, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)}
}

View file

@ -55,15 +55,26 @@ func TestErrorsSingle(t *testing.T) {
"fmt"
)
func test() (err error) {
return nil
func a() error {
return fmt.Errorf("This is an error")
}
func b() {
fmt.Println("b")
}
func c() string {
return fmt.Sprintf("This isn't anything")
}
func main() {
_ := test()
_ = a()
a()
b()
_ = c()
c()
}`, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
checkTestResults(t, issues, 2, "Errors unhandled")
}
func TestErrorsGood(t *testing.T) {
@ -88,3 +99,42 @@ func TestErrorsGood(t *testing.T) {
checkTestResults(t, issues, 0, "")
}
func TestErrorsWhitelisted(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G104": map[string][]string{
"compress/zlib": []string{"NewReader"},
"io": []string{"Copy"},
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
source := `package main
import (
"io"
"os"
"fmt"
"bytes"
"compress/zlib"
)
func a() error {
return fmt.Errorf("This is an error ok")
}
func main() {
// Expect at least one failure
_ = a()
var b bytes.Buffer
// Default whitelist
nbytes, _ := b.Write([]byte("Hello "))
// Whitelisted via configuration
r, _ := zlib.NewReader(&b)
io.Copy(os.Stdout, r)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
}