1package main
2
3import (
4	"go/ast"
5	"go/types"
6	"strings"
7
8	"golang.org/x/tools/go/analysis"
9	"golang.org/x/tools/go/analysis/passes/inspect"
10	"golang.org/x/tools/go/analysis/singlechecker"
11)
12
13func main() {
14	singlechecker.Main(Analyzer)
15}
16
17var Analyzer = &analysis.Analyzer{
18	Name:     "hclogvet",
19	Doc:      "check hclog invocations",
20	Requires: []*analysis.Analyzer{inspect.Analyzer},
21	Run:      run,
22}
23
24var checkHCLogFunc = map[string]bool{
25	"Trace": true,
26	"Debug": true,
27	"Info":  true,
28	"Warn":  true,
29	"Error": true,
30}
31
32func run(pass *analysis.Pass) (interface{}, error) {
33	for _, f := range pass.Files {
34		ast.Inspect(f, func(n ast.Node) bool {
35			call, ok := n.(*ast.CallExpr)
36			if !ok {
37				return true
38			}
39
40			fun, ok := call.Fun.(*ast.SelectorExpr)
41			if !ok {
42				return true
43			}
44
45			typ := pass.TypesInfo.Types[fun]
46			sig, ok := typ.Type.(*types.Signature)
47			if !ok {
48				return true
49			} else if sig == nil {
50				return true // the call is not on of the form x.f()
51			}
52
53			recv := pass.TypesInfo.Types[fun.X]
54			if recv.Type == nil {
55				return true
56			}
57
58			if !isNamedType(recv.Type, "github.com/hashicorp/go-hclog", "Logger") &&
59				!isNamedType(recv.Type, "github.com/hashicorp/go-hclog", "InterceptLogger") {
60				return true
61			}
62
63			if _, ok := checkHCLogFunc[fun.Sel.Name]; !ok {
64				return true
65			}
66
67			// arity should be odd, with the log message being first and then followed by K/V pairs
68			if numArgs := len(call.Args); numArgs%2 != 1 {
69				pairs := numArgs / 2
70				noun := "pairs"
71				if pairs == 1 {
72					noun = "pair"
73				}
74				pass.Reportf(call.Lparen, "invalid number of log arguments to %s (%d valid %s only)", fun.Sel.Name, pairs, noun)
75			}
76
77			return true
78		})
79	}
80
81	return nil, nil
82}
83
84// isNamedType reports whether t is the named type path.name.
85func isNamedType(t types.Type, path, name string) bool {
86	n, ok := t.(*types.Named)
87	if !ok {
88		return false
89	}
90	obj := n.Obj()
91	return obj.Name() == name && isPackage(obj.Pkg(), path)
92}
93
94// isPackage reports whether pkg has path as the canonical path,
95// taking into account vendoring effects
96func isPackage(pkg *types.Package, path string) bool {
97	if pkg == nil {
98		return false
99	}
100
101	return pkg.Path() == path ||
102		strings.HasSuffix(pkg.Path(), "/vendor/"+path)
103}
104