Skip to content

Commit

Permalink
lint: add new deferunlocktest pass
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Santamaura committed Sep 5, 2023
1 parent 2b71df6 commit f8cf42e
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 0 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ nogo(
"@org_golang_x_tools//go/analysis/passes/unreachable:go_default_library",
"@org_golang_x_tools//go/analysis/passes/unsafeptr:go_default_library",
"@org_golang_x_tools//go/analysis/passes/unusedresult:go_default_library",
"//pkg/testutils/lint/passes/deferunlockcheck",
"//pkg/testutils/lint/passes/descriptormarshal",
"//pkg/testutils/lint/passes/errcheck",
"//pkg/testutils/lint/passes/errcmp",
Expand Down
6 changes: 6 additions & 0 deletions build/bazelutil/nogo_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
"_test\\.go$": "tests"
}
},
"deferunlockcheck": {
"only_files": {
"cockroach/pkg/.*$": "first-party code",
"cockroach/bazel-out/.*/bin/pkg/.*$": "first-party code"
}
},
"errcheck": {
"exclude_files": {
"pkg/.*\\.eg\\.go$": "generated code",
Expand Down
3 changes: 3 additions & 0 deletions pkg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ ALL_TESTS = [
"//pkg/testutils/fingerprintutils:fingerprintutils_test",
"//pkg/testutils/floatcmp:floatcmp_test",
"//pkg/testutils/keysutils:keysutils_test",
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck_test",
"//pkg/testutils/lint/passes/errcmp:errcmp_test",
"//pkg/testutils/lint/passes/errwrap:errwrap_test",
"//pkg/testutils/lint/passes/fmtsafe:fmtsafe_test",
Expand Down Expand Up @@ -2167,6 +2168,8 @@ GO_TARGETS = [
"//pkg/testutils/keysutils:keysutils",
"//pkg/testutils/keysutils:keysutils_test",
"//pkg/testutils/kvclientutils:kvclientutils",
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck",
"//pkg/testutils/lint/passes/deferunlockcheck:deferunlockcheck_test",
"//pkg/testutils/lint/passes/descriptormarshal:descriptormarshal",
"//pkg/testutils/lint/passes/errcheck:errcheck",
"//pkg/testutils/lint/passes/errcmp:errcmp",
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/roachvet/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go_library(
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/roachvet",
visibility = ["//visibility:private"],
deps = [
"//pkg/testutils/lint/passes/deferunlockcheck",
"//pkg/testutils/lint/passes/errcmp",
"//pkg/testutils/lint/passes/errwrap",
"//pkg/testutils/lint/passes/fmtsafe",
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/roachvet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package main

import (
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck"
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/errcmp"
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/errwrap"
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/fmtsafe"
Expand Down Expand Up @@ -70,6 +71,7 @@ func main() {
nilness.Analyzer,
errwrap.Analyzer,
loopvarcapture.Analyzer,
deferunlockcheck.Analyzer,
)

// Standard go vet analyzers:
Expand Down
30 changes: 30 additions & 0 deletions pkg/testutils/lint/passes/deferunlockcheck/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "deferunlockcheck",
srcs = ["deferunlockcheck.go"],
importpath = "github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck",
visibility = ["//visibility:public"],
deps = [
"//pkg/testutils/lint/passes/passesutil",
"@org_golang_x_tools//go/analysis",
"@org_golang_x_tools//go/analysis/passes/inspect",
"@org_golang_x_tools//go/ast/inspector",
],
)

go_test(
name = "deferunlockcheck_test",
srcs = ["deferunlockcheck_test.go"],
args = ["-test.timeout=295s"],
data = glob(["testdata/**"]) + [
"@go_sdk//:files",
],
deps = [
":deferunlockcheck",
"//pkg/build/bazel",
"//pkg/testutils/datapathutils",
"//pkg/testutils/skip",
"@org_golang_x_tools//go/analysis/analysistest",
],
)
59 changes: 59 additions & 0 deletions pkg/testutils/lint/passes/deferunlockcheck/deferunlockcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package deferunlockcheck

import (
"go/ast"

"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/passesutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)

// Doc documents this pass.
const Doc = "checks that usages of mutex Unlock() are deferred."

const noLintName = "deferunlock"

// Analyzer is an analysis pass that checks for mutex unlocks which
// aren't deferred.
var Analyzer = &analysis.Analyzer{
Name: "deferunlockcheck",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}

func run(pass *analysis.Pass) (interface{}, error) {
astInspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Note: defer ...Unlock() expressions are captured as ast.DeferStmt nodes so we don't have to worry about those results returning
// for ast.ExprStmt nodes.
filter := []ast.Node{
(*ast.ExprStmt)(nil),
}
astInspector.Preorder(filter, func(n ast.Node) {
stmt := n.(*ast.ExprStmt)
expr, ok := stmt.X.(*ast.CallExpr)
if !ok {
return
}
sel, ok := expr.Fun.(*ast.SelectorExpr)
if !ok {
return
}
if sel.Sel != nil && (sel.Sel.Name == "Unlock" || sel.Sel.Name == "RUnlock") && !passesutil.HasNolintComment(pass, n, noLintName) {
pass.Reportf(sel.Pos(), "Mutex %s not deferred", sel.Sel.Name)
}
})

return nil, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package deferunlockcheck_test

import (
"testing"

"github.com/cockroachdb/cockroach/pkg/build/bazel"
"github.com/cockroachdb/cockroach/pkg/testutils/datapathutils"
"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferunlockcheck"
"github.com/cockroachdb/cockroach/pkg/testutils/skip"
"golang.org/x/tools/go/analysis/analysistest"
)

func init() {
if bazel.BuiltWithBazel() {
bazel.SetGoEnv()
}
}

func Test(t *testing.T) {
skip.UnderStress(t)
testdata := datapathutils.TestDataPath(t)
analysistest.TestData = func() string { return testdata }
analysistest.Run(t, testdata, deferunlockcheck.Analyzer, "a")
}
58 changes: 58 additions & 0 deletions pkg/testutils/lint/passes/deferunlockcheck/testdata/src/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package a

import (
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
)

type TestUnlockLint struct {
mu struct {
syncutil.Mutex
}
}

func init() {
var mut syncutil.Mutex
var rwmut syncutil.RWMutex
testUnlock := &TestUnlockLint{}

// Test the main use case.
// Should only capture Unlock()
testUnlock.mu.Lock()
testUnlock.mu.Unlock() // want `Mutex Unlock not deferred`
// This should pass.
defer testUnlock.mu.Unlock()

// Test within a function.
okFn := func() {
testUnlock.mu.Lock()
defer testUnlock.mu.Unlock()
}
failFn := func() {
testUnlock.mu.Lock()
testUnlock.mu.Unlock() // want `Mutex Unlock not deferred`
}
okFn()
failFn()

// Test mut variation.
defer mut.Unlock()
mut.Unlock() // want `Mutex Unlock not deferred`

// Test RUnlock
defer rwmut.RUnlock()
rwmut.RUnlock() // want `Mutex RUnlock not deferred`

// Test the no lint rule.
// nolint:deferunlock
testUnlock.mu.Unlock()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package syncutil

import "sync"

type Mutex struct {
sync.Mutex
}

type RWMutex struct {
sync.RWMutex
}

0 comments on commit f8cf42e

Please sign in to comment.