Go
This page curates a list of example ast-grep rules to check and to rewrite Go code.
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:
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
ast-grep \
--lang go \
--pattern '{ defer $A.$B(t, failpoint.$M($$$)) } \
--selector defer_statement'
Example
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:
defer func() {
require.NoError(t, failpoint.Disable("some/path"))
}()
Contributed by
Inspired by YangKeao's tweet about this common pitfall in TiDB codebase.
Find function declarations with names of certain pattern
Description
ast-grep can find function declarations by their names. But not all names can be matched by a meta variable pattern. For instance, you cannot use a meta variable pattern to find function declarations whose names start with a specific prefix, e.g. TestAbs
with the prefix Test
. Attempting Test$_
will fail because it is not a valid syntax.
Instead, you can use a YAML rule to use the regex
atomic rule.
YAML
id: test-functions
language: go
rule:
kind: function_declaration
has:
field: name
regex: Test.*
Example
package abs
import "testing"
func TestAbs(t *testing.T) {
got := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %d; want 1", got)
}
}
Contributed by
kevinkjt2000 on Discord.
Match Function Call in Golang
Description
One of the common questions of ast-grep is to match function calls in Golang.
A plain pattern like fmt.Println($A)
will not work. This is because Golang syntax also allows type conversions, e.g. int(3.14)
, that look like function calls. Tree-sitter, ast-grep's parser, will prefer parsing func_call(arg)
as a type conversion instead of a call expression.
To avoid this ambiguity, ast-grep lets us write a contextual pattern, which is a pattern inside a larger code snippet. We can use context
to write a pattern like this: func t() { fmt.Println($A) }
. Then, we can use the selector call_expression
to match only function calls.
Please also read the deep dive on ambiguous pattern.
YAML
id: match-function-call
language: go
rule:
pattern:
context: 'func t() { fmt.Println($A) }'
selector: call_expression
Example
func main() {
fmt.Println("OK")
}
Contributed by
Inspired by QuantumGhost from ast-grep/ast-grep#646
Match package import in Golang
Description
A generic rule template for detecting imports of specific packages in Go source code. This rule can be customized to match any package by modifying the regex pattern, making it useful for security auditing, dependency management, and compliance checking.
This rule identifies Go import statements based on the configured regex pattern, including:
Direct imports: import "package/name"
Versioned imports: import "package/name/v4"
Subpackage imports: import "package/name/subpkg"
Grouped imports within import () blocks
YAML
id: match-package-import
language: go
rule:
kind: import_spec
has:
regex: PACKAGE_PATTERN_HERE
Example
JWT Library Detection
package main
import (
"fmt"
"github.com/golang-jwt/jwt" // This matches the AST rule
)
func main() {
token := jwt.New(jwt.SigningMethodHS256) // Create a new token
// Add some claims
token.Claims = jwt.MapClaims{"user": "alice", "role": "admin"}
tokenString, err := token.SignedString([]byte("my-secret")) // Sign the token
if err != nil {
fmt.Printf("Error signing token: %v\n", err)
return
}
fmt.Printf("Generated token: %s\n", tokenString)
}
Contributed by
Detect problematic JSON tags with dash prefix
Description
This rule detects a security vulnerability in Go's JSON unmarshaling. When a struct field has a JSON tag that starts with -,
, it can be unexpectedly unmarshaled with the -
key.
According to the Go documentation, if the field tag is -
, the field should be omitted. However, a field with name -
can still be unmarshaled using the tag -,
.
This creates a security issue where developers think they are preventing a field from being unmarshaled (like IsAdmin
in authentication), but attackers can still set that field by providing the -
key in JSON input.
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-,omitempty"` // Intended to prevent marshaling
}
// This still works and sets IsAdmin to true!
json.Unmarshal([]byte(`{"-": true}`), &user)
// Result: main.User{Username:"", Password:"", IsAdmin:true}
YAML
id: unmarshal-tag-is-dash
severity: error
message: Struct field can be decoded with the `-` key because the JSON tag
starts with a `-` but is followed by a comma.
rule:
pattern: '`$TAG`'
inside:
kind: field_declaration
constraints:
TAG:
regex: json:"-,.*"
Example
package main
type TestStruct1 struct {
A string `json:"id"` // ok
}
type TestStruct2 struct {
B string `json:"-,omitempty"` // wrong
}
type TestStruct3 struct {
C string `json:"-,123"` // wrong
}
type TestStruct4 struct {
D string `json:"-,"` // wrong
}
Fix
To properly omit a field from JSON marshaling/unmarshaling, use just -
without a comma:
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-"` // Correctly prevents marshaling/unmarshaling
}
Contributed by
Inspired by Trail of Bits blog post and their public Semgrep rule.