Skip to content

Commit f8cf42e

Browse files
committed
lint: add new deferunlocktest pass
This commit adds a new pass which checks for `...Unlock()` expressions without `defer` which could result in a lock being held indefinitely. Resolves #105366 Release note: None
1 parent 2b71df6 commit f8cf42e

File tree

10 files changed

+215
-0
lines changed

10 files changed

+215
-0
lines changed

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ nogo(
186186
"@org_golang_x_tools//go/analysis/passes/unreachable:go_default_library",
187187
"@org_golang_x_tools//go/analysis/passes/unsafeptr:go_default_library",
188188
"@org_golang_x_tools//go/analysis/passes/unusedresult:go_default_library",
189+
"//pkg/testutils/lint/passes/deferunlockcheck",
189190
"//pkg/testutils/lint/passes/descriptormarshal",
190191
"//pkg/testutils/lint/passes/errcheck",
191192
"//pkg/testutils/lint/passes/errcmp",

build/bazelutil/nogo_config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
"_test\\.go$": "tests"
2929
}
3030
},
31+
"deferunlockcheck": {
32+
"only_files": {
33+
"cockroach/pkg/.*$": "first-party code",
34+
"cockroach/bazel-out/.*/bin/pkg/.*$": "first-party code"
35+
}
36+
},
3137
"errcheck": {
3238
"exclude_files": {
3339
"pkg/.*\\.eg\\.go$": "generated code",

pkg/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ ALL_TESTS = [
593593
"//pkg/testutils/fingerprintutils:fingerprintutils_test",
594594
"//pkg/testutils/floatcmp:floatcmp_test",
595595
"//pkg/testutils/keysutils:keysutils_test",
596+
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck_test",
596597
"//pkg/testutils/lint/passes/errcmp:errcmp_test",
597598
"//pkg/testutils/lint/passes/errwrap:errwrap_test",
598599
"//pkg/testutils/lint/passes/fmtsafe:fmtsafe_test",
@@ -2167,6 +2168,8 @@ GO_TARGETS = [
21672168
"//pkg/testutils/keysutils:keysutils",
21682169
"//pkg/testutils/keysutils:keysutils_test",
21692170
"//pkg/testutils/kvclientutils:kvclientutils",
2171+
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck",
2172+
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck_test",
21702173
"//pkg/testutils/lint/passes/descriptormarshal:descriptormarshal",
21712174
"//pkg/testutils/lint/passes/errcheck:errcheck",
21722175
"//pkg/testutils/lint/passes/errcmp:errcmp",

pkg/cmd/roachvet/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ go_library(
66
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/roachvet",
77
visibility = ["//visibility:private"],
88
deps = [
9+
"//pkg/testutils/lint/passes/deferunlockcheck",
910
"//pkg/testutils/lint/passes/errcmp",
1011
"//pkg/testutils/lint/passes/errwrap",
1112
"//pkg/testutils/lint/passes/fmtsafe",

pkg/cmd/roachvet/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package main
1515

1616
import (
17+
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck"
1718
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/errcmp"
1819
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/errwrap"
1920
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/fmtsafe"
@@ -70,6 +71,7 @@ func main() {
7071
nilness.Analyzer,
7172
errwrap.Analyzer,
7273
loopvarcapture.Analyzer,
74+
deferunlockcheck.Analyzer,
7375
)
7476

7577
// Standard go vet analyzers:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "deferunlockcheck",
5+
srcs = ["deferunlockcheck.go"],
6+
importpath = "github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//pkg/testutils/lint/passes/passesutil",
10+
"@org_golang_x_tools//go/analysis",
11+
"@org_golang_x_tools//go/analysis/passes/inspect",
12+
"@org_golang_x_tools//go/ast/inspector",
13+
],
14+
)
15+
16+
go_test(
17+
name = "deferunlockcheck_test",
18+
srcs = ["deferunlockcheck_test.go"],
19+
args = ["-test.timeout=295s"],
20+
data = glob(["testdata/**"]) + [
21+
"@go_sdk//:files",
22+
],
23+
deps = [
24+
":deferunlockcheck",
25+
"//pkg/build/bazel",
26+
"//pkg/testutils/datapathutils",
27+
"//pkg/testutils/skip",
28+
"@org_golang_x_tools//go/analysis/analysistest",
29+
],
30+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package deferunlockcheck
12+
13+
import (
14+
"go/ast"
15+
16+
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/passesutil"
17+
"golang.org/x/tools/go/analysis"
18+
"golang.org/x/tools/go/analysis/passes/inspect"
19+
"golang.org/x/tools/go/ast/inspector"
20+
)
21+
22+
// Doc documents this pass.
23+
const Doc = "checks that usages of mutex Unlock() are deferred."
24+
25+
const noLintName = "deferunlock"
26+
27+
// Analyzer is an analysis pass that checks for mutex unlocks which
28+
// aren't deferred.
29+
var Analyzer = &analysis.Analyzer{
30+
Name: "deferunlockcheck",
31+
Doc: Doc,
32+
Requires: []*analysis.Analyzer{inspect.Analyzer},
33+
Run: run,
34+
}
35+
36+
func run(pass *analysis.Pass) (interface{}, error) {
37+
astInspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
38+
// Note: defer ...Unlock() expressions are captured as ast.DeferStmt nodes so we don't have to worry about those results returning
39+
// for ast.ExprStmt nodes.
40+
filter := []ast.Node{
41+
(*ast.ExprStmt)(nil),
42+
}
43+
astInspector.Preorder(filter, func(n ast.Node) {
44+
stmt := n.(*ast.ExprStmt)
45+
expr, ok := stmt.X.(*ast.CallExpr)
46+
if !ok {
47+
return
48+
}
49+
sel, ok := expr.Fun.(*ast.SelectorExpr)
50+
if !ok {
51+
return
52+
}
53+
if sel.Sel != nil && (sel.Sel.Name == "Unlock" || sel.Sel.Name == "RUnlock") && !passesutil.HasNolintComment(pass, n, noLintName) {
54+
pass.Reportf(sel.Pos(), "Mutex %s not deferred", sel.Sel.Name)
55+
}
56+
})
57+
58+
return nil, nil
59+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package deferunlockcheck_test
12+
13+
import (
14+
"testing"
15+
16+
"github.com/cockroachdb/cockroach/pkg/build/bazel"
17+
"github.com/cockroachdb/cockroach/pkg/testutils/datapathutils"
18+
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck"
19+
"github.com/cockroachdb/cockroach/pkg/testutils/skip"
20+
"golang.org/x/tools/go/analysis/analysistest"
21+
)
22+
23+
func init() {
24+
if bazel.BuiltWithBazel() {
25+
bazel.SetGoEnv()
26+
}
27+
}
28+
29+
func Test(t *testing.T) {
30+
skip.UnderStress(t)
31+
testdata := datapathutils.TestDataPath(t)
32+
analysistest.TestData = func() string { return testdata }
33+
analysistest.Run(t, testdata, deferunlockcheck.Analyzer, "a")
34+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package a
12+
13+
import (
14+
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
15+
)
16+
17+
type TestUnlockLint struct {
18+
mu struct {
19+
syncutil.Mutex
20+
}
21+
}
22+
23+
func init() {
24+
var mut syncutil.Mutex
25+
var rwmut syncutil.RWMutex
26+
testUnlock := &TestUnlockLint{}
27+
28+
// Test the main use case.
29+
// Should only capture Unlock()
30+
testUnlock.mu.Lock()
31+
testUnlock.mu.Unlock() // want `Mutex Unlock not deferred`
32+
// This should pass.
33+
defer testUnlock.mu.Unlock()
34+
35+
// Test within a function.
36+
okFn := func() {
37+
testUnlock.mu.Lock()
38+
defer testUnlock.mu.Unlock()
39+
}
40+
failFn := func() {
41+
testUnlock.mu.Lock()
42+
testUnlock.mu.Unlock() // want `Mutex Unlock not deferred`
43+
}
44+
okFn()
45+
failFn()
46+
47+
// Test mut variation.
48+
defer mut.Unlock()
49+
mut.Unlock() // want `Mutex Unlock not deferred`
50+
51+
// Test RUnlock
52+
defer rwmut.RUnlock()
53+
rwmut.RUnlock() // want `Mutex RUnlock not deferred`
54+
55+
// Test the no lint rule.
56+
// nolint:deferunlock
57+
testUnlock.mu.Unlock()
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package syncutil
12+
13+
import "sync"
14+
15+
type Mutex struct {
16+
sync.Mutex
17+
}
18+
19+
type RWMutex struct {
20+
sync.RWMutex
21+
}

0 commit comments

Comments
 (0)