Skip to content

Commit 624a3b4

Browse files
authored
feat: add config to ignore interface (#25)
* feature/ignore-interface-regex added ignoreInterfaceRegexps option to ignore errors returned from a function call defined on a interface * Added ReadMe description. Fixed comments and fixed typos * refactored regexs and globs, changed readme description, plus minor nit picks
1 parent fab0c41 commit 624a3b4

File tree

4 files changed

+105
-30
lines changed

4 files changed

+105
-30
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ ignoreSigRegexps:
5252
ignorePackageGlobs:
5353
- encoding/*
5454
- github.com/pkg/*
55+
56+
# ignoreInterfaceRegexps defines a list of regular expressions which, if matched
57+
# to a underlying interface name, will ignore unwrapped errors returned from a
58+
# function whose call is defined on the given interface.
59+
ignoreInterfaceRegexps:
60+
- ^(?i)c(?-i)ach(ing|e)
5561
```
5662
5763
## Usage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignoreInterfaceRegexps:
2+
- (errorer|error)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
)
7+
8+
type errorer interface {
9+
Decode(v interface{}) error
10+
}
11+
12+
func main() {
13+
d := json.NewDecoder(strings.NewReader("hello world"))
14+
do(d)
15+
}
16+
17+
func do(fn errorer) error {
18+
var str string
19+
err := fn.Decode(&str)
20+
if err != nil {
21+
return err // errorer interface ignored as per `ignoreInterfaceRegexps`
22+
}
23+
24+
return nil
25+
}

wrapcheck/wrapcheck.go

+72-30
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"go/ast"
66
"go/token"
77
"go/types"
8-
"log"
9-
"os"
108
"regexp"
119
"strings"
1210

@@ -51,11 +49,11 @@ type WrapcheckConfig struct {
5149
// unwrapped.
5250
//
5351
// For example, an ignoreSigRegexp of `[]string{"\.New.*Err\("}`` will ignore errors
54-
// returned from any signture whose method name starts with "New" and ends with "Err"
52+
// returned from any signature whose method name starts with "New" and ends with "Err"
5553
// due to the signature matching the regular expression `\.New.*Err\(`.
5654
//
5755
// Note that this is similar to the ignoreSigs configuration, but provides
58-
// slightly more flexibility in defining rules by which signtures will be
56+
// slightly more flexibility in defining rules by which signatures will be
5957
// ignored.
6058
IgnoreSigRegexps []string `mapstructure:"ignoreSigRegexps" yaml:"ignoreSigRegexps"`
6159

@@ -71,13 +69,23 @@ type WrapcheckConfig struct {
7169
// ignorePackageGlobs:
7270
// - encoding/*
7371
IgnorePackageGlobs []string `mapstructure:"ignorePackageGlobs" yaml:"ignorePackageGlobs"`
72+
73+
// IgnoreInterfaceRegexps defines a list of regular expressions which, if matched
74+
// to a underlying interface name, will ignore unwrapped errors returned from a
75+
// function whose call is defined on the given interface.
76+
//
77+
// For example, an ignoreInterfaceRegexps of `[]string{"Transac(tor|tion)"}`` will ignore errors
78+
// returned from any function whose call is defined on a interface named 'Transactor'
79+
// or 'Transaction' due to the name matching the regular expression `Transac(tor|tion)`.
80+
IgnoreInterfaceRegexps []string `mapstructure:"ignoreInterfaceRegexps" yaml:"ignoreInterfaceRegexps"`
7481
}
7582

7683
func NewDefaultConfig() WrapcheckConfig {
7784
return WrapcheckConfig{
78-
IgnoreSigs: DefaultIgnoreSigs,
79-
IgnoreSigRegexps: []string{},
80-
IgnorePackageGlobs: []string{},
85+
IgnoreSigs: DefaultIgnoreSigs,
86+
IgnoreSigRegexps: []string{},
87+
IgnorePackageGlobs: []string{},
88+
IgnoreInterfaceRegexps: []string{},
8189
}
8290
}
8391

@@ -91,7 +99,21 @@ func NewAnalyzer(cfg WrapcheckConfig) *analysis.Analyzer {
9199

92100
func run(cfg WrapcheckConfig) func(*analysis.Pass) (interface{}, error) {
93101
// Precompile the regexps, report the error
94-
ignoreSigRegexp, err := compileRegexps(cfg.IgnoreSigRegexps)
102+
var (
103+
ignoreSigRegexp []*regexp.Regexp
104+
ignoreInterfaceRegexps []*regexp.Regexp
105+
ignorePackageGlobs []glob.Glob
106+
err error
107+
)
108+
109+
ignoreSigRegexp, err = compileRegexps(cfg.IgnoreSigRegexps)
110+
if err == nil {
111+
ignoreInterfaceRegexps, err = compileRegexps(cfg.IgnoreInterfaceRegexps)
112+
}
113+
if err == nil {
114+
ignorePackageGlobs, err = compileGlobs(cfg.IgnorePackageGlobs)
115+
116+
}
95117

96118
return func(pass *analysis.Pass) (interface{}, error) {
97119
if err != nil {
@@ -120,7 +142,7 @@ func run(cfg WrapcheckConfig) func(*analysis.Pass) (interface{}, error) {
120142
// tuple check is required.
121143

122144
if isError(pass.TypesInfo.TypeOf(expr)) {
123-
reportUnwrapped(pass, retFn, retFn.Pos(), cfg, ignoreSigRegexp)
145+
reportUnwrapped(pass, retFn, retFn.Pos(), cfg, ignoreSigRegexp, ignoreInterfaceRegexps, ignorePackageGlobs)
124146
return true
125147
}
126148

@@ -138,7 +160,7 @@ func run(cfg WrapcheckConfig) func(*analysis.Pass) (interface{}, error) {
138160
return true
139161
}
140162
if isError(v.Type()) {
141-
reportUnwrapped(pass, retFn, expr.Pos(), cfg, ignoreSigRegexp)
163+
reportUnwrapped(pass, retFn, expr.Pos(), cfg, ignoreSigRegexp, ignoreInterfaceRegexps, ignorePackageGlobs)
142164
return true
143165
}
144166
}
@@ -200,7 +222,7 @@ func run(cfg WrapcheckConfig) func(*analysis.Pass) (interface{}, error) {
200222
return true
201223
}
202224

203-
reportUnwrapped(pass, call, ident.NamePos, cfg, ignoreSigRegexp)
225+
reportUnwrapped(pass, call, ident.NamePos, cfg, ignoreSigRegexp, ignoreInterfaceRegexps, ignorePackageGlobs)
204226
}
205227

206228
return true
@@ -213,7 +235,7 @@ func run(cfg WrapcheckConfig) func(*analysis.Pass) (interface{}, error) {
213235

214236
// Report unwrapped takes a call expression and an identifier and reports
215237
// if the call is unwrapped.
216-
func reportUnwrapped(pass *analysis.Pass, call *ast.CallExpr, tokenPos token.Pos, cfg WrapcheckConfig, regexps []*regexp.Regexp) {
238+
func reportUnwrapped(pass *analysis.Pass, call *ast.CallExpr, tokenPos token.Pos, cfg WrapcheckConfig, regexpsSig []*regexp.Regexp, regexpsInter []*regexp.Regexp, pkgGlobs []glob.Glob) {
217239
sel, ok := call.Fun.(*ast.SelectorExpr)
218240
if !ok {
219241
return
@@ -224,21 +246,26 @@ func reportUnwrapped(pass *analysis.Pass, call *ast.CallExpr, tokenPos token.Pos
224246

225247
if contains(cfg.IgnoreSigs, fnSig) {
226248
return
227-
} else if containsMatch(regexps, fnSig) {
249+
} else if containsMatch(regexpsSig, fnSig) {
228250
return
229251
}
230252

231253
// Check if the underlying type of the "x" in x.y.z is an interface, as
232-
// errors returned from interface types should be wrapped.
254+
// errors returned from interface types should be wrapped, unless ignored
255+
// as per `ignoreInterfaceRegexps`
233256
if isInterface(pass, sel) {
234-
pass.Reportf(tokenPos, "error returned from interface method should be wrapped: sig: %s", fnSig)
235-
return
257+
name := types.TypeString(pass.TypesInfo.TypeOf(sel.X), func(p *types.Package) string { return p.Name() })
258+
if containsMatch(regexpsInter, name) {
259+
} else {
260+
pass.Reportf(tokenPos, "error returned from interface method should be wrapped: sig: %s", fnSig)
261+
return
262+
}
236263
}
237264

238265
// Check whether the function being called comes from another package,
239266
// as functions called across package boundaries which returns errors
240267
// should be wrapped
241-
if isFromOtherPkg(pass, sel, cfg) {
268+
if isFromOtherPkg(pass, sel, pkgGlobs) {
242269
pass.Reportf(tokenPos, "error returned from external package is unwrapped: sig: %s", fnSig)
243270
return
244271
}
@@ -251,23 +278,14 @@ func isInterface(pass *analysis.Pass, sel *ast.SelectorExpr) bool {
251278
return ok
252279
}
253280

254-
// isFromotherPkg returns whether the function is defined in the pacakge
281+
// isFromotherPkg returns whether the function is defined in the package
255282
// currently under analysis or is considered external. It will ignore packages
256283
// defined in config.IgnorePackageGlobs.
257-
func isFromOtherPkg(pass *analysis.Pass, sel *ast.SelectorExpr, config WrapcheckConfig) bool {
284+
func isFromOtherPkg(pass *analysis.Pass, sel *ast.SelectorExpr, pkgGlobs []glob.Glob) bool {
258285
// The package of the function that we are calling which returns the error
259286
fn := pass.TypesInfo.ObjectOf(sel.Sel)
260-
261-
for _, globString := range config.IgnorePackageGlobs {
262-
g, err := glob.Compile(globString)
263-
if err != nil {
264-
log.Printf("unable to parse glob: %s\n", globString)
265-
os.Exit(1)
266-
}
267-
268-
if g.Match(fn.Pkg().Path()) {
269-
return false
270-
}
287+
if containsMatchGlob(pkgGlobs, fn.Pkg().Path()) {
288+
return false
271289
}
272290

273291
// If it's not a package name, then we should check the selector to make sure
@@ -350,6 +368,15 @@ func containsMatch(regexps []*regexp.Regexp, el string) bool {
350368
return false
351369
}
352370

371+
func containsMatchGlob(globs []glob.Glob, el string) bool {
372+
for _, g := range globs {
373+
if g.Match(el) {
374+
return true
375+
}
376+
}
377+
return false
378+
}
379+
353380
// isError returns whether or not the provided type interface is an error
354381
func isError(typ types.Type) bool {
355382
if typ == nil {
@@ -384,3 +411,18 @@ func compileRegexps(regexps []string) ([]*regexp.Regexp, error) {
384411

385412
return compiledRegexps, nil
386413
}
414+
415+
// compileGlobs compiles a set of globs, returning them for use,
416+
// or the first encountered error due to an invalid expression.
417+
func compileGlobs(globs []string) ([]glob.Glob, error) {
418+
var compiledGlobs []glob.Glob
419+
for _, globString := range globs {
420+
glob, err := glob.Compile(globString)
421+
if err != nil {
422+
return nil, fmt.Errorf("unable to compile globs %s: %v\n", glob, err)
423+
}
424+
425+
compiledGlobs = append(compiledGlobs, glob)
426+
}
427+
return compiledGlobs, nil
428+
}

0 commit comments

Comments
 (0)