go/analysis/passes/modernize: unsafefuncs: ptr+int => unsafe.Add
This CL adds a new modernizer (only to gopls for now) that
replaces unsafe pointer arithmetic by calls to helper functions
such as go1.17's unsafe.Add.
We really need some kind of template matcher like the one
in staticcheck, ideally integrated with cursors.
+ tests, docs.
For golang/go#76648
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go
index 7469002..68cbc96 100644
--- a/go/analysis/passes/modernize/doc.go
+++ b/go/analysis/passes/modernize/doc.go
@@ -475,6 +475,33 @@
This change is only suggested if the `cancel` function is not used
for any other purpose.
+# Analyzer unsafefuncs
+
+unsafefuncs: replace unsafe pointer arithmetic with function calls
+
+The unsafefuncs analyzer simplifies pointer arithmetic expressions by
+replacing them will calls to helper functions such as unsafe.Add,
+added in Go 1.17.
+
+Example:
+
+ unsafe.Pointer(uintptr(ptr) + uintptr(n))
+
+where ptr is an unsafe.Pointer, is replaced by:
+
+ unsafe.Add(ptr, n)
+
+If the unsafe.Pointer was itself obtained from an ordinary *T pointer,
+and the result is converted to the same *T pointer type, the
+expression can be further simplified, as in this example wherfe ptr
+has type *T:
+
+ (*T)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(n)))
+
+=>
+
+ unsafe.Add(ptr, n)
+
# Analyzer waitgroup
waitgroup: replace wg.Add(1)/go/wg.Done() with wg.Go
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go
index 013ce79..066984f 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -54,6 +54,7 @@
StringsBuilderAnalyzer,
TestingContextAnalyzer,
WaitGroupAnalyzer,
+ UnsafeFuncsAnalyzer,
}
// -- helpers --
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go
index ce4341b..dc91ade 100644
--- a/go/analysis/passes/modernize/modernize_test.go
+++ b/go/analysis/passes/modernize/modernize_test.go
@@ -109,6 +109,10 @@
RunWithSuggestedFixes(t, TestData(), modernize.TestingContextAnalyzer, "testingcontext")
}
+func TestUnsafeFuncs(t *testing.T) {
+ RunWithSuggestedFixes(t, TestData(), modernize.UnsafeFuncsAnalyzer, "unsafefuncs")
+}
+
func TestWaitGroup(t *testing.T) {
RunWithSuggestedFixes(t, TestData(), modernize.WaitGroupAnalyzer, "waitgroup")
}
diff --git a/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go
new file mode 100644
index 0000000..3d5a67d
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go
@@ -0,0 +1,23 @@
+package unsafefuncs
+
+import "unsafe"
+
+func _(ptr unsafe.Pointer) unsafe.Pointer {
+ return unsafe.Pointer(uintptr(ptr) + 1) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+type uP = unsafe.Pointer
+
+func _(ptr uP) uP {
+ return uP(uintptr(ptr) + 1) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+func _(ptr unsafe.Pointer, n int) unsafe.Pointer {
+ return unsafe.Pointer(uintptr(ptr) + uintptr(n)) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+type namedUP unsafe.Pointer
+
+func _(ptr namedUP) namedUP {
+ return namedUP(uintptr(ptr) + 1) // nope: Add does not accept named unsafe.Pointer types
+}
diff --git a/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden
new file mode 100644
index 0000000..fbad7c2
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden
@@ -0,0 +1,23 @@
+package unsafefuncs
+
+import "unsafe"
+
+func _(ptr unsafe.Pointer) unsafe.Pointer {
+ return unsafe.Add(ptr, 1) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+type uP = unsafe.Pointer
+
+func _(ptr uP) uP {
+ return unsafe.Add(ptr, 1) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+func _(ptr unsafe.Pointer, n int) unsafe.Pointer {
+ return unsafe.Add(ptr, n) // want `pointer \+ integer can be simplified using unsafe.Add`
+}
+
+type namedUP unsafe.Pointer
+
+func _(ptr namedUP) namedUP {
+ return namedUP(uintptr(ptr) + 1) // nope: Add does not accept named unsafe.Pointer types
+}
diff --git a/go/analysis/passes/modernize/unsafefuncs.go b/go/analysis/passes/modernize/unsafefuncs.go
new file mode 100644
index 0000000..302156c
--- /dev/null
+++ b/go/analysis/passes/modernize/unsafefuncs.go
@@ -0,0 +1,221 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package modernize
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/edge"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/internal/analysis/analyzerutil"
+ "golang.org/x/tools/internal/astutil"
+ "golang.org/x/tools/internal/refactor"
+ "golang.org/x/tools/internal/typesinternal"
+ "golang.org/x/tools/internal/versions"
+)
+
+// TODO(adonovan): also support:
+//
+// func String(ptr *byte, len IntegerType) string
+// func StringData(str string) *byte
+// func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
+// func SliceData(slice []ArbitraryType) *ArbitraryType
+
+var UnsafeFuncsAnalyzer = &analysis.Analyzer{
+ Name: "unsafefuncs",
+ Doc: analyzerutil.MustExtractDoc(doc, "unsafefuncs"),
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: unsafefuncs,
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs",
+}
+
+func unsafefuncs(pass *analysis.Pass) (any, error) {
+ // Short circuit if the package doesn't use unsafe.
+ // (In theory one could use some imported alias of unsafe.Pointer,
+ // but let's ignore that.)
+ if !typesinternal.Imports(pass.Pkg, "unsafe") {
+ return nil, nil
+ }
+
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ info = pass.TypesInfo
+ tUnsafePointer = types.Typ[types.UnsafePointer]
+ )
+
+ isInteger := func(t types.Type) bool {
+ basic, ok := t.Underlying().(*types.Basic)
+ return ok && basic.Info()&types.IsInteger != 0
+ }
+
+ // isConversion reports whether e is a conversion T(x).
+ // If so, it returns T and x.
+ isConversion := func(curExpr inspector.Cursor) (t types.Type, x inspector.Cursor) {
+ e := curExpr.Node().(ast.Expr)
+ if conv, ok := ast.Unparen(e).(*ast.CallExpr); ok && len(conv.Args) == 1 {
+ if tv := pass.TypesInfo.Types[conv.Fun]; tv.IsType() {
+ return tv.Type, curExpr.ChildAt(edge.CallExpr_Args, 0)
+ }
+ }
+ return
+ }
+
+ // The general form is where ptr and the result are of type unsafe.Pointer:
+ //
+ // unsafe.Pointer(uintptr(ptr) + uintptr(n))
+ // =>
+ // unsafe.Add(ptr, n)
+ //
+ // Variant: ptr is itself converted from *T, and the sum is converted back to *T:
+ //
+ // (*T)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(n)))
+ // =>
+ // unsafe.Add(ptr, n)
+
+ // Search for 'unsafe.Pointer(uintptr + uintptr)'
+ // where the left operand was converted from a pointer.
+ //
+ // (Start from sum, not conversion, as it is not
+ // uncommon to use a local type alias for unsafe.Pointer.)
+ for curSum := range inspect.Root().Preorder((*ast.BinaryExpr)(nil)) {
+ if sum, ok := curSum.Node().(*ast.BinaryExpr); ok &&
+ sum.Op == token.ADD &&
+ types.Identical(info.TypeOf(sum.X), types.Typ[types.Uintptr]) &&
+ astutil.IsChildOf(curSum, edge.CallExpr_Args) {
+ // Have: sum ≡ T(x:...uintptr... + y:...uintptr...)
+ curX := curSum.ChildAt(edge.BinaryExpr_X, -1)
+ curY := curSum.ChildAt(edge.BinaryExpr_Y, -1)
+
+ // Is sum converted to unsafe.Pointer?
+ curResult := curSum.Parent()
+ if t, _ := isConversion(curResult); !(t != nil && types.Identical(t, tUnsafePointer)) {
+ continue
+ }
+ // Have: result ≡ unsafe.Pointer(x:...uintptr... + y:...uintptr...)
+
+ // Is sum.x converted from unsafe.Pointer?
+ _, curPtr := isConversion(curX)
+ ptr := curPtr.Node().(ast.Expr)
+ if !(astutil.CursorValid(curPtr) && types.Identical(info.TypeOf(ptr), tUnsafePointer)) {
+ continue
+ }
+ // Have: result ≡ unsafe.Pointer(x:uintptr(...unsafe.Pointer...) + y:...uintptr...)
+
+ file := astutil.EnclosingFile(curSum)
+ if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_17) {
+ continue // unsafe.Add not available in this file
+ }
+
+ // import "unsafe"
+ unsafedot, edits := refactor.AddImport(info, file, "unsafe", "unsafe", "Add", sum.Pos())
+
+ // unsafe.Pointer(x + y)
+ // --------------- -
+ // x + y
+ edits = append(edits, deleteConv(curResult)...)
+
+ // uintptr (ptr) + offset
+ // ----------- ---- -
+ // unsafe.Add(ptr, offset)
+ edits = append(edits, []analysis.TextEdit{
+ {
+ Pos: sum.Pos(),
+ End: ptr.Pos(),
+ NewText: fmt.Appendf(nil, "%sAdd(", unsafedot),
+ },
+ {
+ Pos: ptr.End(),
+ End: sum.Y.Pos(),
+ NewText: []byte(", "),
+ },
+ {
+ Pos: sum.Y.End(),
+ End: sum.Y.End(),
+ NewText: []byte(")"),
+ },
+ }...)
+
+ // Variant: ptr/result are converted from/to some pointer type *T.
+ // Discard the matched inner and outer conversions.
+ //
+ // (*T)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + ...uintptr...))
+ // ---- --------------- - -
+ // unsafe.Pointer(uintptr( ptr ) + ...uintptr...)
+ //
+ if tInner, _ := isConversion(curPtr); tInner != nil &&
+ is[*types.Pointer](tInner.Underlying()) &&
+ astutil.IsChildOf(curResult, edge.CallExpr_Args) {
+ curOuterConv := curResult.Parent()
+ if tOuter, _ := isConversion(curOuterConv); tOuter != nil &&
+ types.Identical(tOuter, tInner) {
+ edits = append(edits, deleteConv(curOuterConv)...)
+ edits = append(edits, deleteConv(curPtr)...)
+ }
+ }
+
+ // Variant: sum.y operand was converted from another integer type.
+ // Discard the conversion, as Add is generic over integers.
+ if t, _ := isConversion(curY); t != nil && isInteger(t) {
+ edits = append(edits, deleteConv(curY)...)
+ }
+
+ pass.Report(analysis.Diagnostic{
+ Pos: sum.Pos(),
+ End: sum.End(),
+ Message: "pointer + integer can be simplified using unsafe.Add",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Simplify pointer addition using unsafe.Add",
+ TextEdits: edits,
+ }},
+ })
+ }
+ }
+
+ return nil, nil
+}
+
+// deleteConv returns edits for changing T(x) to x, respecting precedence.
+func deleteConv(cur inspector.Cursor) []analysis.TextEdit {
+ conv := cur.Node().(*ast.CallExpr)
+
+ usesPrec := func(n ast.Node) bool {
+ switch n.(type) {
+ case *ast.BinaryExpr, *ast.UnaryExpr:
+ return true
+ }
+ return false
+ }
+
+ // Be careful not to change precedence of e.g. T(1+2) * 3.
+ // TODO(adonovan): refine this.
+ if usesPrec(cur.Parent().Node()) && usesPrec(conv.Args[0]) {
+ // T(x+y) * z
+ // -
+ // (x+y) * z
+ return []analysis.TextEdit{{
+ Pos: conv.Fun.Pos(),
+ End: conv.Fun.End(),
+ }}
+ }
+
+ // T(x)
+ // -- -
+ // x
+ return []analysis.TextEdit{
+ {
+ Pos: conv.Pos(),
+ End: conv.Args[0].Pos(),
+ },
+ {
+ Pos: conv.Args[0].End(),
+ End: conv.End(),
+ },
+ }
+}
diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go
index 5f10f56..3d24a8c 100644
--- a/go/types/typeutil/callee.go
+++ b/go/types/typeutil/callee.go
@@ -12,6 +12,7 @@
// Callee returns the named target of a function call, if any:
// a function, method, builtin, or variable.
+// It returns nil for a T(x) conversion.
//
// Functions and methods may potentially have type parameters.
//
diff --git a/internal/versions/features.go b/internal/versions/features.go
index a5f4e32..cdd36c3 100644
--- a/internal/versions/features.go
+++ b/internal/versions/features.go
@@ -9,6 +9,7 @@
// named constants, to avoid misspelling
const (
+ Go1_17 = "go1.17"
Go1_18 = "go1.18"
Go1_19 = "go1.19"
Go1_20 = "go1.20"
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Nice. I need to get you those other unsafe idioms, but it was helpful to read these to see how it's done. Not sure I am qualified to +2 it.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Sorry, I'm really not going to have time to review this thoroughly 😞
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +1 |
LGTM with a tiny nit
// Is sum converted to unsafe.Pointer?Because the type conversions can be a bit clunky, it's quite common to introduce temporary variables for readability.
```
y := uintptr(unsafe.Pointer(ptr)) + uintptr(someval)
return (*sometype)(unsafe.Pointer(y))
```
I suspect we could catch some more cases if you tested whether the expression is assigned to a variable that is used exactly once in a conversion to unsafe.Pointer, but that's probably out of scope for now. Maybe a TODO?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// Is sum converted to unsafe.Pointer?Because the type conversions can be a bit clunky, it's quite common to introduce temporary variables for readability.
```
y := uintptr(unsafe.Pointer(ptr)) + uintptr(someval)
return (*sometype)(unsafe.Pointer(y))
```I suspect we could catch some more cases if you tested whether the expression is assigned to a variable that is used exactly once in a conversion to unsafe.Pointer, but that's probably out of scope for now. Maybe a TODO?
That temporary variable is not actually valid. Grep https://pkg.go.dev/unsafe for "Note that both conversions must appear in the same expression"
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// Is sum converted to unsafe.Pointer?Dominik HonnefBecause the type conversions can be a bit clunky, it's quite common to introduce temporary variables for readability.
```
y := uintptr(unsafe.Pointer(ptr)) + uintptr(someval)
return (*sometype)(unsafe.Pointer(y))
```I suspect we could catch some more cases if you tested whether the expression is assigned to a variable that is used exactly once in a conversion to unsafe.Pointer, but that's probably out of scope for now. Maybe a TODO?
That temporary variable is not actually valid. Grep https://pkg.go.dev/unsafe for "Note that both conversions must appear in the same expression"
Well that's egg on my face. I've been using unsafe.Pointer so long, but it's been a while since I consulted the nitty gritty of the documentation. I have definitely seen (and written) that idiom around before, so I rescind my comment and present instead #76657
// Is sum converted to unsafe.Pointer?Dominik HonnefBecause the type conversions can be a bit clunky, it's quite common to introduce temporary variables for readability.
```
y := uintptr(unsafe.Pointer(ptr)) + uintptr(someval)
return (*sometype)(unsafe.Pointer(y))
```I suspect we could catch some more cases if you tested whether the expression is assigned to a variable that is used exactly once in a conversion to unsafe.Pointer, but that's probably out of scope for now. Maybe a TODO?
Daniel MorsingThat temporary variable is not actually valid. Grep https://pkg.go.dev/unsafe for "Note that both conversions must appear in the same expression"
Well that's egg on my face. I've been using unsafe.Pointer so long, but it's been a while since I consulted the nitty gritty of the documentation. I have definitely seen (and written) that idiom around before, so I rescind my comment and present instead #76657
Dominik makes a good point. Probably the right thing is for this mistake to be detected by the existing `unsafeptr` analyzer. (I'm surprised it doesn't already check for it.) I've filed https://github.com/golang/go/issues/76660.
| Code-Review | +2 |
// Variant: sum.y operand was converted from another integer type.Could you please add an example or a test case illustrating this variant?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Auto-Submit | +1 |
| Commit-Queue | +1 |
// Variant: sum.y operand was converted from another integer type.Could you please add an example or a test case illustrating this variant?
Sure; done. :)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
3 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: go/analysis/passes/modernize/unsafefuncs.go
Insertions: 4, Deletions: 0.
@@ -147,6 +147,10 @@
// Variant: sum.y operand was converted from another integer type.
// Discard the conversion, as Add is generic over integers.
+ //
+ // e.g. unsafe.Pointer(uintptr(ptr) + uintptr(len(s)))
+ // -------- -
+ // unsafe.Add ( ptr, len(s))
if t, _ := isConversion(curY); t != nil && isInteger(t) {
edits = append(edits, deleteConv(curY)...)
}
```
go/analysis/passes/modernize: unsafefuncs: ptr+int => unsafe.Add
This CL adds a new modernizer (only to gopls for now) that
replaces unsafe pointer arithmetic by calls to helper functions
such as go1.17's unsafe.Add.
We really need some kind of template matcher like the one
in staticcheck, ideally integrated with cursors.
+ tests, docs.
For golang/go#76648
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |