add linter to check for missing finishers (#85)
This commit is contained in:
parent
9cd6f6eef2
commit
372015deb4
|
@ -0,0 +1,175 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/loader"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
recursivelyIgnoredPkgs arrayFlag
|
||||||
|
ignoredPkgs arrayFlag
|
||||||
|
ignoredFiles arrayFlag
|
||||||
|
allowedFinishers arrayFlag = []string{"Msg", "Msgf"}
|
||||||
|
rootPkg string
|
||||||
|
)
|
||||||
|
|
||||||
|
// parse input flags and args
|
||||||
|
func init() {
|
||||||
|
flag.Var(&recursivelyIgnoredPkgs, "ignorePkgRecursively", "ignore the specified package and all subpackages recursively")
|
||||||
|
flag.Var(&ignoredPkgs, "ignorePkg", "ignore the specified package")
|
||||||
|
flag.Var(&ignoredFiles, "ignoreFile", "ignore the specified file by its path and/or go path (package/file.go)")
|
||||||
|
flag.Var(&allowedFinishers, "finisher", "allowed finisher for the event chain")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// add zerolog to recursively ignored packages
|
||||||
|
recursivelyIgnoredPkgs = append(recursivelyIgnoredPkgs, "github.com/rs/zerolog")
|
||||||
|
args := flag.Args()
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "you must provide exactly one package path")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
rootPkg = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// load the package and all its dependencies
|
||||||
|
conf := loader.Config{}
|
||||||
|
conf.Import(rootPkg)
|
||||||
|
p, err := conf.Load()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: unable to load the root package. %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the github.com/rs/zerolog.Event type
|
||||||
|
event := getEvent(p)
|
||||||
|
if event == nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: github.com/rs/zerolog.Event declaration not found, maybe zerolog is not imported in the scanned package?")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all selections (function calls) with the github.com/rs/zerolog.Event (or pointer) receiver
|
||||||
|
selections := getSelectionsWithReceiverType(p, event)
|
||||||
|
|
||||||
|
// print the violations (if any)
|
||||||
|
hasViolations := false
|
||||||
|
for _, s := range selections {
|
||||||
|
if hasBadFinisher(p, s) {
|
||||||
|
hasViolations = true
|
||||||
|
fmt.Printf("Error: missing or bad finisher for log chain, last call: %q at: %s:%v\n", s.fn.Name(), p.Fset.File(s.Pos()).Name(), p.Fset.Position(s.Pos()).Line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no violations detected, return normally
|
||||||
|
if !hasViolations {
|
||||||
|
fmt.Println("No violations found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if violations were detected, return error code
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEvent(p *loader.Program) types.Type {
|
||||||
|
for _, pkg := range p.AllPackages {
|
||||||
|
if strings.HasSuffix(pkg.Pkg.Path(), "github.com/rs/zerolog") {
|
||||||
|
for _, d := range pkg.Defs {
|
||||||
|
if d != nil && d.Name() == "Event" {
|
||||||
|
return d.Type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectionsWithReceiverType(p *loader.Program, targetType types.Type) map[token.Pos]selection {
|
||||||
|
selections := map[token.Pos]selection{}
|
||||||
|
|
||||||
|
for _, z := range p.AllPackages {
|
||||||
|
for i, t := range z.Selections {
|
||||||
|
switch o := t.Obj().(type) {
|
||||||
|
case *types.Func:
|
||||||
|
// this is not a bug, o.Type() is always *types.Signature, see docs
|
||||||
|
if vt := o.Type().(*types.Signature).Recv(); vt != nil {
|
||||||
|
typ := vt.Type()
|
||||||
|
if pointer, ok := typ.(*types.Pointer); ok {
|
||||||
|
typ = pointer.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ == targetType {
|
||||||
|
if s, ok := selections[i.Pos()]; !ok || i.End() > s.End() {
|
||||||
|
selections[i.Pos()] = selection{i, o, z.Pkg}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selections
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasBadFinisher(p *loader.Program, s selection) bool {
|
||||||
|
pkgPath := strings.TrimPrefix(s.pkg.Path(), rootPkg+"/vendor/")
|
||||||
|
absoluteFilePath := strings.TrimPrefix(p.Fset.File(s.Pos()).Name(), rootPkg+"/vendor/")
|
||||||
|
goFilePath := pkgPath + "/" + filepath.Base(p.Fset.Position(s.Pos()).Filename)
|
||||||
|
|
||||||
|
for _, f := range allowedFinishers {
|
||||||
|
if f == s.fn.Name() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ignoredPkg := range recursivelyIgnoredPkgs {
|
||||||
|
if strings.HasPrefix(pkgPath, ignoredPkg) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ignoredPkg := range ignoredPkgs {
|
||||||
|
if pkgPath == ignoredPkg {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ignoredFile := range ignoredFiles {
|
||||||
|
if absoluteFilePath == ignoredFile {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if goFilePath == ignoredFile {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type arrayFlag []string
|
||||||
|
|
||||||
|
func (i *arrayFlag) String() string {
|
||||||
|
return fmt.Sprintf("%v", []string(*i))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *arrayFlag) Set(value string) error {
|
||||||
|
*i = append(*i, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type selection struct {
|
||||||
|
*ast.SelectorExpr
|
||||||
|
fn *types.Func
|
||||||
|
pkg *types.Package
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Zerolog Lint
|
||||||
|
|
||||||
|
This is a basic linter that checks for missing log event finishers. Finds errors like: `log.Error().Int64("userID": 5)` - missing the `Msg`/`Msgf` finishers.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
When using zerolog it's easy to forget to finish the log event chain by calling a finisher - the `Msg` or `Msgf` function that will schedule the event for writing. The problem with this is that it doesn't warn/panic during compilation and it's not easily found by grep or other general tools. It's even prominently mentioned in the project's readme, that:
|
||||||
|
|
||||||
|
> It is very important to note that when using the **zerolog** chaining API, as shown above (`log.Info().Msg("hello world"`), the chain must have either the `Msg` or `Msgf` method call. If you forget to add either of these, the log will not occur and there is no compile time error to alert you of this.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
A basic linter like this one here that looks for method invocations on `zerolog.Event` can examine the last call in a method call chain and check if it is a finisher, thus pointing out these errors.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Just compile this and then run it. Or just run it via `go run` command via something like `go run cmd/lint/lint.go`.
|
||||||
|
|
||||||
|
The command accepts only one argument - the package to be inspected - and 4 optional flags, all of which can occur multiple times. The standard synopsis of the command is:
|
||||||
|
|
||||||
|
`lint [-finisher value] [-ignoreFile value] [-ignorePkg value] [-ignorePkgRecursively value] package`
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
- finisher
|
||||||
|
- specify which finishers to accept, defaults to `Msg` and `Msgf`
|
||||||
|
- ignoreFile
|
||||||
|
- which files to ignore, either by full path or by go path (package/file.go)
|
||||||
|
- ignorePkg
|
||||||
|
- do not inspect the specified package if found in the dependecy tree
|
||||||
|
- ignorePkgRecursively
|
||||||
|
- do not inspect the specified package or its subpackages if found in the dependency tree
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
|
||||||
|
As it is, linter can generate a false positives in a specific case. These false positives come from the fact that if you have a method that returns a `zerolog.Event` the linter will flag it because you are obviously not finishing the event. This will be solved in later release.
|
||||||
|
|
Loading…
Reference in New Issue