Skip to content

Commit cf5d9bf

Browse files
authored
Merge pull request #198 from dtcaciuc/analyzer
Provide go/analysis analyzer instance
2 parents 98b1bd1 + 2cfce26 commit cf5d9bf

File tree

15 files changed

+438
-130
lines changed

15 files changed

+438
-130
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ takes no arguments.
3636
The `-blank` flag enables checking for assignments of errors to the
3737
blank identifier. It takes no arguments.
3838

39+
### go/analysis
40+
41+
The package provides `Analyzer` instance that can be used with
42+
[go/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) API.
43+
44+
Currently supported flags are `blank`, `assert`, `exclude`, and `excludeonly`.
45+
Just as the API itself, the analyzer is exprimental and may change in the
46+
future.
3947

4048
## Excluding functions
4149

errcheck/analyzer.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package errcheck
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/token"
7+
"reflect"
8+
"regexp"
9+
10+
"golang.org/x/tools/go/analysis"
11+
)
12+
13+
var Analyzer = &analysis.Analyzer{
14+
Name: "errcheck",
15+
Doc: "check for unchecked errors",
16+
Run: runAnalyzer,
17+
ResultType: reflect.TypeOf(Result{}),
18+
}
19+
20+
var (
21+
argBlank bool
22+
argAsserts bool
23+
argExcludeFile string
24+
argExcludeOnly bool
25+
)
26+
27+
func init() {
28+
Analyzer.Flags.BoolVar(&argBlank, "blank", false, "if true, check for errors assigned to blank identifier")
29+
Analyzer.Flags.BoolVar(&argAsserts, "assert", false, "if true, check for ignored type assertion results")
30+
Analyzer.Flags.StringVar(&argExcludeFile, "exclude", "", "Path to a file containing a list of functions to exclude from checking")
31+
Analyzer.Flags.BoolVar(&argExcludeOnly, "excludeonly", false, "Use only excludes from exclude file")
32+
}
33+
34+
func runAnalyzer(pass *analysis.Pass) (interface{}, error) {
35+
36+
exclude := map[string]bool{}
37+
if !argExcludeOnly {
38+
for _, name := range DefaultExcludedSymbols {
39+
exclude[name] = true
40+
}
41+
}
42+
if argExcludeFile != "" {
43+
excludes, err := ReadExcludes(argExcludeFile)
44+
if err != nil {
45+
return nil, fmt.Errorf("Could not read exclude file: %v\n", err)
46+
}
47+
for _, name := range excludes {
48+
exclude[name] = true
49+
}
50+
}
51+
52+
var allErrors []UncheckedError
53+
for _, f := range pass.Files {
54+
v := &visitor{
55+
typesInfo: pass.TypesInfo,
56+
fset: pass.Fset,
57+
blank: argBlank,
58+
asserts: argAsserts,
59+
exclude: exclude,
60+
ignore: map[string]*regexp.Regexp{}, // deprecated & not used
61+
lines: make(map[string][]string),
62+
errors: nil,
63+
}
64+
65+
ast.Walk(v, f)
66+
67+
for _, err := range v.errors {
68+
pass.Report(analysis.Diagnostic{
69+
Pos: token.Pos(int(f.Pos()) + err.Pos.Offset),
70+
Message: "unchecked error",
71+
})
72+
}
73+
74+
allErrors = append(allErrors, v.errors...)
75+
}
76+
77+
return Result{UncheckedErrors: allErrors}, nil
78+
}

errcheck/errcheck.go

+27-65
Original file line numberDiff line numberDiff line change
@@ -25,49 +25,6 @@ func init() {
2525
var (
2626
// ErrNoGoFiles is returned when CheckPackage is run on a package with no Go source files
2727
ErrNoGoFiles = errors.New("package contains no go source files")
28-
29-
// DefaultExcludedSymbols is a list of symbol names that are usually excluded from checks by default.
30-
//
31-
// Note, that they still need to be explicitly copied to Checker.Exclusions.Symbols
32-
DefaultExcludedSymbols = []string{
33-
// bytes
34-
"(*bytes.Buffer).Write",
35-
"(*bytes.Buffer).WriteByte",
36-
"(*bytes.Buffer).WriteRune",
37-
"(*bytes.Buffer).WriteString",
38-
39-
// fmt
40-
"fmt.Errorf",
41-
"fmt.Print",
42-
"fmt.Printf",
43-
"fmt.Println",
44-
"fmt.Fprint(*bytes.Buffer)",
45-
"fmt.Fprintf(*bytes.Buffer)",
46-
"fmt.Fprintln(*bytes.Buffer)",
47-
"fmt.Fprint(*strings.Builder)",
48-
"fmt.Fprintf(*strings.Builder)",
49-
"fmt.Fprintln(*strings.Builder)",
50-
"fmt.Fprint(os.Stderr)",
51-
"fmt.Fprintf(os.Stderr)",
52-
"fmt.Fprintln(os.Stderr)",
53-
54-
// io
55-
"(*io.PipeReader).CloseWithError",
56-
"(*io.PipeWriter).CloseWithError",
57-
58-
// math/rand
59-
"math/rand.Read",
60-
"(*math/rand.Rand).Read",
61-
62-
// strings
63-
"(*strings.Builder).Write",
64-
"(*strings.Builder).WriteByte",
65-
"(*strings.Builder).WriteRune",
66-
"(*strings.Builder).WriteString",
67-
68-
// hash
69-
"(hash.Hash).Write",
70-
}
7128
)
7229

7330
// UncheckedError indicates the position of an unchecked error return.
@@ -261,16 +218,17 @@ func (c *Checker) CheckPackage(pkg *packages.Package) Result {
261218
}
262219

263220
v := &visitor{
264-
pkg: pkg,
265-
ignore: ignore,
266-
blank: !c.Exclusions.BlankAssignments,
267-
asserts: !c.Exclusions.TypeAssertions,
268-
lines: make(map[string][]string),
269-
exclude: excludedSymbols,
270-
errors: []UncheckedError{},
221+
typesInfo: pkg.TypesInfo,
222+
fset: pkg.Fset,
223+
ignore: ignore,
224+
blank: !c.Exclusions.BlankAssignments,
225+
asserts: !c.Exclusions.TypeAssertions,
226+
lines: make(map[string][]string),
227+
exclude: excludedSymbols,
228+
errors: []UncheckedError{},
271229
}
272230

273-
for _, astFile := range v.pkg.Syntax {
231+
for _, astFile := range pkg.Syntax {
274232
if c.shouldSkipFile(astFile) {
275233
continue
276234
}
@@ -281,12 +239,13 @@ func (c *Checker) CheckPackage(pkg *packages.Package) Result {
281239

282240
// visitor implements the errcheck algorithm
283241
type visitor struct {
284-
pkg *packages.Package
285-
ignore map[string]*regexp.Regexp
286-
blank bool
287-
asserts bool
288-
lines map[string][]string
289-
exclude map[string]bool
242+
typesInfo *types.Info
243+
fset *token.FileSet
244+
ignore map[string]*regexp.Regexp
245+
blank bool
246+
asserts bool
247+
lines map[string][]string
248+
exclude map[string]bool
290249

291250
errors []UncheckedError
292251
}
@@ -306,7 +265,7 @@ func (v *visitor) selectorAndFunc(call *ast.CallExpr) (*ast.SelectorExpr, *types
306265
return nil, nil, false
307266
}
308267

309-
fn, ok := v.pkg.TypesInfo.ObjectOf(sel.Sel).(*types.Func)
268+
fn, ok := v.typesInfo.ObjectOf(sel.Sel).(*types.Func)
310269
if !ok {
311270
// Shouldn't happen, but be paranoid
312271
return nil, nil, false
@@ -393,7 +352,7 @@ func (v *visitor) namesForExcludeCheck(call *ast.CallExpr) []string {
393352

394353
// This will be missing for functions without a receiver (like fmt.Printf),
395354
// so just fall back to the the function's fullName in that case.
396-
selection, ok := v.pkg.TypesInfo.Selections[sel]
355+
selection, ok := v.typesInfo.Selections[sel]
397356
if !ok {
398357
return []string{name}
399358
}
@@ -420,14 +379,14 @@ func (v *visitor) namesForExcludeCheck(call *ast.CallExpr) []string {
420379
func (v *visitor) argName(expr ast.Expr) string {
421380
// Special-case literal "os.Stdout" and "os.Stderr"
422381
if sel, ok := expr.(*ast.SelectorExpr); ok {
423-
if obj := v.pkg.TypesInfo.ObjectOf(sel.Sel); obj != nil {
382+
if obj := v.typesInfo.ObjectOf(sel.Sel); obj != nil {
424383
vr, ok := obj.(*types.Var)
425384
if ok && vr.Pkg() != nil && vr.Pkg().Name() == "os" && (vr.Name() == "Stderr" || vr.Name() == "Stdout") {
426385
return "os." + vr.Name()
427386
}
428387
}
429388
}
430-
t := v.pkg.TypesInfo.TypeOf(expr)
389+
t := v.typesInfo.TypeOf(expr)
431390
if t == nil {
432391
return ""
433392
}
@@ -478,7 +437,7 @@ func (v *visitor) ignoreCall(call *ast.CallExpr) bool {
478437
return true
479438
}
480439

481-
if obj := v.pkg.TypesInfo.Uses[id]; obj != nil {
440+
if obj := v.typesInfo.Uses[id]; obj != nil {
482441
if pkg := obj.Pkg(); pkg != nil {
483442
if re, ok := v.ignore[nonVendoredPkgPath(pkg.Path())]; ok {
484443
return re.MatchString(id.Name)
@@ -504,7 +463,7 @@ func nonVendoredPkgPath(pkgPath string) string {
504463
// len(s) == number of return types of call
505464
// s[i] == true iff return type at position i from left is an error type
506465
func (v *visitor) errorsByArg(call *ast.CallExpr) []bool {
507-
switch t := v.pkg.TypesInfo.Types[call].Type.(type) {
466+
switch t := v.typesInfo.Types[call].Type.(type) {
508467
case *types.Named:
509468
// Single return
510469
return []bool{isErrorType(t)}
@@ -546,15 +505,18 @@ func (v *visitor) callReturnsError(call *ast.CallExpr) bool {
546505
// isRecover returns true if the given CallExpr is a call to the built-in recover() function.
547506
func (v *visitor) isRecover(call *ast.CallExpr) bool {
548507
if fun, ok := call.Fun.(*ast.Ident); ok {
549-
if _, ok := v.pkg.TypesInfo.Uses[fun].(*types.Builtin); ok {
508+
if _, ok := v.typesInfo.Uses[fun].(*types.Builtin); ok {
550509
return fun.Name == "recover"
551510
}
552511
}
553512
return false
554513
}
555514

515+
// TODO (dtcaciuc) collect token.Pos and then convert them to UncheckedErrors
516+
// after visitor is done running. This will allow to integrate more cleanly
517+
// with analyzer so that we don't have to convert Position back to Pos.
556518
func (v *visitor) addErrorAtPosition(position token.Pos, call *ast.CallExpr) {
557-
pos := v.pkg.Fset.Position(position)
519+
pos := v.fset.Position(position)
558520
lines, ok := v.lines[pos.Filename]
559521
if !ok {
560522
lines = readfile(pos.Filename)

errcheck/excludes.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package errcheck
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"io/ioutil"
7+
"strings"
8+
)
9+
10+
var (
11+
// DefaultExcludedSymbols is a list of symbol names that are usually excluded from checks by default.
12+
//
13+
// Note, that they still need to be explicitly copied to Checker.Exclusions.Symbols
14+
DefaultExcludedSymbols = []string{
15+
// bytes
16+
"(*bytes.Buffer).Write",
17+
"(*bytes.Buffer).WriteByte",
18+
"(*bytes.Buffer).WriteRune",
19+
"(*bytes.Buffer).WriteString",
20+
21+
// fmt
22+
"fmt.Errorf",
23+
"fmt.Print",
24+
"fmt.Printf",
25+
"fmt.Println",
26+
"fmt.Fprint(*bytes.Buffer)",
27+
"fmt.Fprintf(*bytes.Buffer)",
28+
"fmt.Fprintln(*bytes.Buffer)",
29+
"fmt.Fprint(*strings.Builder)",
30+
"fmt.Fprintf(*strings.Builder)",
31+
"fmt.Fprintln(*strings.Builder)",
32+
"fmt.Fprint(os.Stderr)",
33+
"fmt.Fprintf(os.Stderr)",
34+
"fmt.Fprintln(os.Stderr)",
35+
36+
// io
37+
"(*io.PipeReader).CloseWithError",
38+
"(*io.PipeWriter).CloseWithError",
39+
40+
// math/rand
41+
"math/rand.Read",
42+
"(*math/rand.Rand).Read",
43+
44+
// strings
45+
"(*strings.Builder).Write",
46+
"(*strings.Builder).WriteByte",
47+
"(*strings.Builder).WriteRune",
48+
"(*strings.Builder).WriteString",
49+
50+
// hash
51+
"(hash.Hash).Write",
52+
}
53+
)
54+
55+
// ReadExcludes reads an excludes file, a newline delimited file that lists
56+
// patterns for which to allow unchecked errors.
57+
//
58+
// Lines that start with two forward slashes are considered comments and are ignored.
59+
//
60+
func ReadExcludes(path string) ([]string, error) {
61+
var excludes []string
62+
63+
buf, err := ioutil.ReadFile(path)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
scanner := bufio.NewScanner(bytes.NewReader(buf))
69+
70+
for scanner.Scan() {
71+
name := scanner.Text()
72+
// Skip comments and empty lines.
73+
if strings.HasPrefix(name, "//") || name == "" {
74+
continue
75+
}
76+
excludes = append(excludes, name)
77+
}
78+
if err := scanner.Err(); err != nil {
79+
return nil, err
80+
}
81+
82+
return excludes, nil
83+
}

errcheck/excludes_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package errcheck
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestReadExcludes(t *testing.T) {
9+
expectedExcludes := []string{
10+
"hello()",
11+
"world()",
12+
}
13+
t.Logf("expectedExcludes: %#v", expectedExcludes)
14+
excludes, err := ReadExcludes("testdata/excludes.txt")
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
t.Logf("excludes: %#v", excludes)
19+
if !reflect.DeepEqual(expectedExcludes, excludes) {
20+
t.Fatal("excludes did not match expectedExcludes")
21+
}
22+
}
23+
24+
func TestReadEmptyExcludes(t *testing.T) {
25+
excludes, err := ReadExcludes("testdata/empty_excludes.txt")
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
if len(excludes) != 0 {
30+
t.Fatalf("expected empty excludes, got %#v", excludes)
31+
}
32+
}
33+
34+
func TestReadExcludesMissingFile(t *testing.T) {
35+
_, err := ReadExcludes("testdata/missing_file")
36+
if err == nil {
37+
t.Fatal("expected non-nil err, got nil")
38+
}
39+
}

errcheck/testdata/custom_exclude.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(custom_exclude.ErrorMakerInterfaceWrapper).MakeNilError
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)