Skip to content

Detect problematic defer statements with function calls

Description

This rule detects a common anti-pattern in Go testing code where defer statements contain function calls with parameters that are evaluated immediately instead of when the defer executes.

In Go, defer schedules a function call to be executed when the surrounding function returns. However, the arguments to the deferred function are evaluated immediately when the defer statement is encountered, not when the defer executes.

This is particularly problematic when using assertion libraries in tests. For example:

go
defer require.NoError(t, failpoint.Disable("some/path"))

In this case, failpoint.Disable("some/path") is called immediately when the defer statement is reached, not when the function exits. This means the failpoint is disabled right after being enabled, making the test ineffective.

Pattern

shell
ast-grep \
  --lang go \
  --pattern '{ defer $A.$B(t, failpoint.$M($$$)) } \
  --selector defer_statement'

Example

go
func TestIssue16696(t *testing.T) {
	alarmRatio := vardef.MemoryUsageAlarmRatio.Load()
	vardef.MemoryUsageAlarmRatio.Store(0.0)
	defer vardef.MemoryUsageAlarmRatio.Store(alarmRatio)
	require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/executor/sortexec/testSortedRowContainerSpill", "return(true)"))
	defer require.NoError(t,
	   failpoint.Disable(
		"github.com/pingcap/tidb/pkg/executor/sortexec/testSortedRowContainerSpill"
	))
	require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/executor/join/testRowContainerSpill", "return(true)"))
	defer require.NoError(t,
		failpoint.Disable("github.com/pingcap/tidb/pkg/executor/join/testRowContainerSpill"))
}

Fix

The correct way to defer a function with parameters is to wrap it in an anonymous function:

go
defer func() {
    require.NoError(t, failpoint.Disable("some/path"))
}()

Contributed by

Inspired by YangKeao's tweet about this common pitfall in TiDB codebase.

Made with ❤️ with Rust