Skip to content

Commit 3a6ba38

Browse files
committed
Check for JSON decoder; use gotypesalias; skip generated files
Signed-off-by: Oliver Eikemeier <[email protected]>
1 parent 3a9780e commit 3a6ba38

19 files changed

+199
-86
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
- name: 🧸 golangci-lint
3333
uses: golangci/golangci-lint-action@v6
3434
with:
35-
version: v1.60.3
35+
version: v1.61.0
3636
- name: 🔨 Test
3737
run: |
3838
go get -C ./pkg/analyzer/testdata golang.org/x/exp/errors

.golangci.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ issues:
4848
linters:
4949
- govet
5050
text: "lostcancel"
51+
- path: ^main\.go$
52+
linters:
53+
- gocheckcompilerdirectives
54+
text: "go:debug"

go.mod

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
module fillmore-labs.com/zerolint
22

3-
go 1.22
3+
go 1.22.7
44

5-
toolchain go1.23.0
5+
toolchain go1.23.1
66

7-
require golang.org/x/tools v0.24.0
7+
require golang.org/x/tools v0.25.0
88

99
require (
10-
golang.org/x/mod v0.20.0 // indirect
10+
golang.org/x/mod v0.21.0 // indirect
1111
golang.org/x/sync v0.8.0 // indirect
1212
)

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
22
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3-
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
4-
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
3+
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
4+
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
55
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
66
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
7-
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
8-
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
7+
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
8+
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
//
1515
// SPDX-License-Identifier: Apache-2.0
1616

17+
//go:debug gotypesalias=1
18+
1719
// This is the main program for the zerolint linter.
1820
package main
1921

pkg/analyzer/analyzer.go

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func init() { //nolint:gochecknoinits
4141
Analyzer.Flags.StringVar(&Excludes, "excluded", "", "read excluded types from this file")
4242
Analyzer.Flags.BoolVar(&ZeroTrace, "zerotrace", false, "trace found zero-sized types")
4343
Analyzer.Flags.BoolVar(&Basic, "basic", false, "basic analysis only")
44+
Analyzer.Flags.BoolVar(&Generated, "generated", false, "check generated files")
4445
}
4546

4647
var (
@@ -52,6 +53,9 @@ var (
5253

5354
// Basic enables basic analysis only.
5455
Basic bool //nolint:gochecknoglobals
56+
57+
// Generated enables checking generated files.
58+
Generated bool //nolint:gochecknoglobals
5559
)
5660

5761
// Run applies the analyzer to a package.
@@ -69,6 +73,7 @@ func run(pass *analysis.Pass) (any, error) {
6973
},
7074
ZeroTrace: ZeroTrace,
7175
Basic: Basic,
76+
Generated: Generated,
7277
}
7378
v.Run()
7479

pkg/analyzer/analyzer_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestAnalyzer(t *testing.T) { //nolint:paralleltest
2828
a := analyzer.Analyzer
2929

3030
analyzer.Basic = true
31-
analysistest.Run(t, dir, a, "go.test/basic")
31+
analysistest.RunWithSuggestedFixes(t, dir, a, "go.test/basic")
3232

3333
analyzer.Basic = false
3434
analyzer.Excludes = dir + "/excluded.txt"

pkg/analyzer/testdata/a/testdata.go

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package a
1818

1919
import (
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223

@@ -73,6 +74,10 @@ func Exported() {
7374
fmt.Println("equal")
7475
}
7576

77+
_ = json.Unmarshal(nil, &myErrs)
78+
_ = (*json.Decoder)(nil).Decode(&myErrs)
79+
_ = json.NewDecoder(nil).Decode(&myErrs)
80+
7681
var err *typedError[int] // want "pointer to zero-sized type"
7782
_ = errors.As(ErrOne, &err)
7883

pkg/analyzer/testdata/a/testdata.go.golden

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package a
1818

1919
import (
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223

@@ -73,6 +74,10 @@ func Exported() {
7374
fmt.Println("equal")
7475
}
7576

77+
_ = json.Unmarshal(nil, &myErrs)
78+
_ = (*json.Decoder)(nil).Decode(&myErrs)
79+
_ = json.NewDecoder(nil).Decode(&myErrs)
80+
7681
var err typedError[int] // want "pointer to zero-sized type"
7782
_ = errors.As(ErrOne, &err)
7883

pkg/analyzer/testdata/basic/testdata.go

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package basic
1818

1919
import (
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223
)
@@ -46,6 +47,12 @@ func Exported() {
4647
var err *myError
4748
_ = errors.As(ErrOne, &err)
4849

50+
var u myError
51+
_ = json.Unmarshal(nil, &u)
52+
d := json.NewDecoder(nil)
53+
_ = d.Decode(&u)
54+
_ = json.NewDecoder(nil).Decode(&u)
55+
4956
if ErrOne == ErrTwo { // want "comparison of pointers to zero-size variables"
5057
fmt.Println("equal")
5158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 Oliver Eikemeier. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package basic
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
)
24+
25+
type myError struct{}
26+
27+
func (myError) Error() string { // want "method receiver is pointer to zero-size variable"
28+
return "my error"
29+
}
30+
31+
var (
32+
ErrOne = &myError{}
33+
ErrTwo = new(myError)
34+
)
35+
36+
func Exported() {
37+
if errors.Is(nil, ErrOne) {
38+
fmt.Println("nil")
39+
}
40+
41+
if errors.Is(func() error { // want "comparison of pointer to zero-size variable"
42+
return ErrOne
43+
}(), ErrTwo) {
44+
fmt.Println("equal")
45+
}
46+
47+
var err *myError
48+
_ = errors.As(ErrOne, &err)
49+
50+
var u myError
51+
_ = json.Unmarshal(nil, &u)
52+
d := json.NewDecoder(nil)
53+
_ = d.Decode(&u)
54+
_ = json.NewDecoder(nil).Decode(&u)
55+
56+
if ErrOne == ErrTwo { // want "comparison of pointers to zero-size variables"
57+
fmt.Println("equal")
58+
}
59+
60+
if ErrOne != ErrTwo { // want "comparison of pointers to zero-size variables"
61+
fmt.Println("not equal")
62+
}
63+
}
64+
65+
type D struct{ _ int }
66+
67+
func (*D) String() string {
68+
return "hello, world"
69+
}
70+
71+
var _ fmt.Stringer = (*D)(nil)

pkg/visitor/call.go

+33-24
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,43 @@ import (
2121
"fmt"
2222
"go/ast"
2323
"go/format"
24-
"go/token"
2524
"go/types"
2625

2726
"golang.org/x/tools/go/analysis"
2827
)
2928

30-
// visitCallBasic checks for errors.Is(x, y) and errors.As(x, y).
31-
func (v Visitor) visitCallBasic(x *ast.CallExpr) bool {
32-
if len(x.Args) != 2 { //nolint:mnd
33-
return true
34-
}
29+
// visitCallValue checks for encoding/json#Decoder.Decode, json.Unmarshal, errors.Is and errors.As.
30+
func (v Visitor) visitCallBasic(x *ast.CallExpr) bool { //nolint:cyclop
3531
fun, ok := unwrap(x.Fun).(*ast.SelectorExpr)
36-
if !ok || !v.isErrors(fun.X) {
32+
if !ok {
3733
return true
3834
}
3935

40-
if fun.Sel.Name == "As" { // Do not report pointers in errors.As(..., ...).
41-
return false
36+
funType := v.TypesInfo.Types[fun.X]
37+
if funType.IsValue() && funType.Type.String() == "*encoding/json.Decoder" && fun.Sel.Name == "Decode" {
38+
return false // Do not report pointers in json.Decoder#Decode
4239
}
4340

44-
if fun.Sel.Name != "Is" {
41+
switch path, ok2 := v.pathOf(fun.X); {
42+
case !ok2:
43+
return true
44+
45+
case path == "encoding/json" && fun.Sel.Name == "Unmarshal":
46+
return false // Do not report pointers in json.Unmarshal(..., ...).
47+
48+
case len(x.Args) != 2 || path != "errors" && path != "golang.org/x/exp/errors":
4549
return true
46-
}
4750

48-
// Delegate errors.Is(..., ...) to visitCmp for further analysis of the comparison.
49-
return v.visitCmp(x, x.Args[0], x.Args[1])
51+
case fun.Sel.Name == "As":
52+
return false // Do not report pointers in errors.As(..., ...)
53+
54+
case fun.Sel.Name == "Is":
55+
// Delegate errors.Is(..., ...) to visitCmp for further analysis of the comparison.
56+
return v.visitCmp(x, x.Args[0], x.Args[1])
57+
58+
default:
59+
return true
60+
}
5061
}
5162

5263
// visitCall checks for type casts T(x), errors.Is(x, y), errors.As(x, y) and new(T).
@@ -67,20 +78,18 @@ func (v Visitor) visitCall(x *ast.CallExpr) bool {
6778
}
6879

6980
// isErrors checks whether the expression is a package specifier for errors or golang.org/x/exp/errors.
70-
func (v Visitor) isErrors(x ast.Expr) bool {
81+
func (v Visitor) pathOf(x ast.Expr) (string, bool) {
7182
id, ok := x.(*ast.Ident)
7283
if !ok {
73-
return false
84+
return "", false
7485
}
7586

7687
pkg, ok := v.TypesInfo.Uses[id].(*types.PkgName)
7788
if !ok {
78-
return false
89+
return "", false
7990
}
8091

81-
path := pkg.Imported().Path()
82-
83-
return path == "errors" || path == "golang.org/x/exp/errors"
92+
return pkg.Imported().Path(), true
8493
}
8594

8695
// visitCast is called for type casts T(nil).
@@ -97,7 +106,7 @@ func (v Visitor) visitCast(t types.Type, x *ast.CallExpr) bool {
97106
message := fmt.Sprintf("cast of nil to pointer to zero-size variable of type %q", elem)
98107
var fixes []analysis.SuggestedFix
99108
if s, ok2 := unwrap(x.Fun).(*ast.StarExpr); ok2 {
100-
fixes = makePure(x, s.X)
109+
fixes = v.makePure(x, s.X)
101110
}
102111

103112
v.report(x, message, fixes)
@@ -117,12 +126,12 @@ func (v Visitor) visitNew(x *ast.CallExpr) bool {
117126

118127
arg := x.Args[0] // new(arg).
119128
argType := v.TypesInfo.Types[arg].Type
120-
if !v.isZeroSizedType(argType) {
129+
if !v.zeroSizedType(argType) {
121130
return true
122131
}
123132

124133
message := fmt.Sprintf("new called on zero-sized type %q", argType)
125-
fixes := makePure(x, arg)
134+
fixes := v.makePure(x, arg)
126135
v.report(x, message, fixes)
127136

128137
return false
@@ -143,9 +152,9 @@ func unwrap(e ast.Expr) ast.Expr {
143152
}
144153

145154
// makePure adds a suggested fix from (*T)(nil) or new(T) to T{}.
146-
func makePure(n ast.Node, x ast.Expr) []analysis.SuggestedFix {
155+
func (v Visitor) makePure(n ast.Node, x ast.Expr) []analysis.SuggestedFix {
147156
var buf bytes.Buffer
148-
if err := format.Node(&buf, token.NewFileSet(), x); err != nil {
157+
if err := format.Node(&buf, v.Fset, x); err != nil {
149158
return nil
150159
}
151160
buf.WriteString("{}")

pkg/visitor/cmp.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import (
2424

2525
// comparisonInfo holds type information for comparison operands.
2626
type comparisonInfo struct {
27-
elem types.Type
28-
isZeroPointer bool
29-
isInterface bool
27+
elem types.Type
28+
zeroPointer bool
29+
iface bool
3030
}
3131

3232
// visitCmp checks comparisons like x == y, x != y and errors.Is(x, y).
@@ -42,14 +42,14 @@ func (v Visitor) visitCmp(n ast.Node, x, y ast.Expr) bool {
4242

4343
var message string
4444
switch {
45-
case p[0].isZeroPointer && p[1].isZeroPointer:
45+
case p[0].zeroPointer && p[1].zeroPointer:
4646
message = comparisonMessage(p[0].elem, p[1].elem)
4747

48-
case p[0].isZeroPointer:
49-
message = comparisonIMessage(p[0].elem, p[1].elem, p[1].isInterface)
48+
case p[0].zeroPointer:
49+
message = comparisonIMessage(p[0].elem, p[1].elem, p[1].iface)
5050

51-
case p[1].isZeroPointer:
52-
message = comparisonIMessage(p[1].elem, p[0].elem, p[0].isInterface)
51+
case p[1].zeroPointer:
52+
message = comparisonIMessage(p[1].elem, p[0].elem, p[0].iface)
5353

5454
default:
5555
return true
@@ -68,11 +68,11 @@ func (v Visitor) getComparisonInfo(t types.Type) comparisonInfo {
6868
switch underlying := t.Underlying().(type) {
6969
case *types.Pointer:
7070
info.elem = underlying.Elem()
71-
info.isZeroPointer = v.isZeroSizedType(info.elem)
71+
info.zeroPointer = v.zeroSizedType(info.elem)
7272

7373
case *types.Interface:
7474
info.elem = t
75-
info.isInterface = true
75+
info.iface = true
7676

7777
default:
7878
info.elem = t

0 commit comments

Comments
 (0)