Skip to content

Commit fea724b

Browse files
Add custom Linter to check 'setupLog.Error(nil, ...)'
New custom linter under hack/ci project was created to allow us to define custom linters for the project
1 parent 46cec30 commit fea724b

File tree

5 files changed

+130
-1
lines changed

5 files changed

+130
-1
lines changed

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,13 @@ help-extended: #HELP Display extended help.
9595
#SECTION Development
9696

9797
.PHONY: lint
98-
lint: $(GOLANGCI_LINT) #HELP Run golangci linter.
98+
lint: lint-custom $(GOLANGCI_LINT) #HELP Run golangci linter.
9999
$(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS)
100100

101+
.PHONY: lint-custom
102+
lint-custom: custom-linter-build
103+
go vet -vettool=./bin/custom-linter ./...
104+
101105
.PHONY: tidy
102106
tidy: #HELP Update dependencies.
103107
# Force tidy to use the version already in go.mod
@@ -290,6 +294,11 @@ BINARIES=operator-controller
290294
$(BINARIES):
291295
go build $(GO_BUILD_FLAGS) -tags '$(GO_BUILD_TAGS)' -ldflags '$(GO_BUILD_LDFLAGS)' -gcflags '$(GO_BUILD_GCFLAGS)' -asmflags '$(GO_BUILD_ASMFLAGS)' -o $(BUILDBIN)/$@ ./cmd/$@
292296

297+
.PHONY: custom-linter-build
298+
custom-linter-build:
299+
cd ./hack/ci/custom-linters/cmd/ && go build -o ../../../../bin/custom-linter
300+
301+
293302
.PHONY: build-deps
294303
build-deps: manifests generate fmt vet
295304

hack/ci/custom-linters/cmd/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
import (
4+
"github.com/github.com/operator-framework/operator-controller/custom-linters/setuplognilerrorcheck"
5+
"golang.org/x/tools/go/analysis"
6+
"golang.org/x/tools/go/analysis/unitchecker"
7+
)
8+
9+
// Define the custom Linters implemented in the project
10+
var customLinters = []*analysis.Analyzer{
11+
setuplognilerrorcheck.SetupLogNilErrorCheck,
12+
}
13+
14+
func main() {
15+
unitchecker.Main(customLinters...)
16+
}

hack/ci/custom-linters/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/github.com/operator-framework/operator-controller/custom-linters
2+
3+
go 1.23.4
4+
5+
require golang.org/x/tools v0.29.0
6+
7+
require (
8+
golang.org/x/mod v0.22.0 // indirect
9+
golang.org/x/sync v0.10.0 // indirect
10+
)

hack/ci/custom-linters/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
4+
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
5+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
6+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
7+
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
8+
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package setuplognilerrorcheck
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/ast"
7+
"go/format"
8+
"go/token"
9+
"go/types"
10+
11+
"golang.org/x/tools/go/analysis"
12+
)
13+
14+
var SetupLogNilErrorCheck = &analysis.Analyzer{
15+
Name: "setuplognilerrorcheck",
16+
Doc: "Detects improper usage of logger.Error() calls, ensuring the first argument is a non-nil error.",
17+
Run: runSetupLogNilErrorCheck,
18+
}
19+
20+
func runSetupLogNilErrorCheck(pass *analysis.Pass) (interface{}, error) {
21+
for _, f := range pass.Files {
22+
ast.Inspect(f, func(n ast.Node) bool {
23+
callExpr, ok := n.(*ast.CallExpr)
24+
if !ok {
25+
return true
26+
}
27+
28+
// Ensure function being called is logger.Error
29+
selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
30+
if !ok || selectorExpr.Sel.Name != "Error" {
31+
return true
32+
}
33+
34+
// Ensure receiver (logger) is identified
35+
ident, ok := selectorExpr.X.(*ast.Ident)
36+
if !ok {
37+
return true
38+
}
39+
40+
// Verify if the receiver is logr.Logger
41+
obj := pass.TypesInfo.ObjectOf(ident)
42+
if obj == nil {
43+
return true
44+
}
45+
46+
if named, ok := obj.Type().(*types.Named); ok {
47+
if named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == "github.com/go-logr/logr" && named.Obj().Name() == "Logger" {
48+
if len(callExpr.Args) > 0 {
49+
firstArg := callExpr.Args[0]
50+
51+
// Default suggested fix (kind error instead of using log message as error)
52+
suggestedError := "errors.New(\"kind error (i.e. configuration error)\")"
53+
suggestedMessage := "\"error message describing the failed operation\""
54+
55+
// Extract log message (second argument) for proper logging
56+
if len(callExpr.Args) > 1 {
57+
if msgArg, ok := callExpr.Args[1].(*ast.BasicLit); ok && msgArg.Kind == token.STRING {
58+
suggestedMessage = msgArg.Value // Use the actual log message correctly
59+
}
60+
}
61+
62+
// Get the actual source code line where the issue occurs
63+
var srcBuffer bytes.Buffer
64+
if err := format.Node(&srcBuffer, pass.Fset, callExpr); err == nil {
65+
sourceLine := srcBuffer.String()
66+
67+
// Case 1: First argument is nil
68+
if arg, ok := firstArg.(*ast.Ident); ok && arg.Name == "nil" {
69+
pass.Reportf(callExpr.Pos(),
70+
"Incorrect usage of 'logger.Error(nil, ...)'. The first argument must be a non-nil 'error'. "+
71+
"Passing 'nil' results in silent failures, making debugging harder.\n\n"+
72+
"\U0001F41B **What is wrong?**\n"+
73+
fmt.Sprintf(" %s\n\n", sourceLine)+
74+
"\U0001F4A1 **How to solve? Return the error, i.e.:**\n"+
75+
fmt.Sprintf(" logger.Error(%s, %s, \"key\", value)\n\n", suggestedError, suggestedMessage))
76+
}
77+
}
78+
}
79+
}
80+
}
81+
82+
return true
83+
})
84+
}
85+
return nil, nil
86+
}

0 commit comments

Comments
 (0)