Refactoring and code transformation tool for Go.
gopatch is a tool to match and transform Go code. It is meant to aid in refactoring and restyling.
gopatch operates like the Unix patch tool: given a patch file and another
file as input, it applies the changes specified in the patch to the provided
file.
.-------. .-------.
/_| |. /_| |.
| ||. +---------+ | ||.
| .go |||>-->| gopatch |>-->| .go |||
| ||| +---------+ | |||
'--------'|| ^ '--------'||
'--------'| | '--------'|
'--------' | '--------'
.-------. |
/_| | |
| +----'
| .patch |
| |
'--------'What specifically differentiates it from patch is that unlike plain text
transformations, it can be smarter because it understands Go syntax.
Download a pre-built binary of gopatch from the Releases page or by
running the following command in your terminal and place it on your $PATH.
VERSION=0.4.0
URL="https://github.com/uber-go/gopatch/releases/download/v$VERSION/gopatch_${VERSION}_$(uname -s)_$(uname -m).tar.gz"
curl -L "$URL" | tar xzv gopatchAlternatively, if you have Go installed, build it from source and install it with the following command.
go install github.com/uber-go/gopatch@latestNote: If you're using Go ~/s1028.patch
@@ @@ -import "errors"
-errors.New(fmt.Sprintf(...)) +fmt.Errorf(...)
This patch is a fix for staticcheck [S1028]. It searches for uses of
[`fmt.Sprintf`] with [`errors.New`], and simplifies them by replacing them
with [`fmt.Errorf`].
[S1028]: https://staticcheck.io/docs/checks#S1028
[`fmt.Sprintf`]: https://golang.org/pkg/fmt/#Sprintf
[`errors.New`]: https://golang.org/pkg/errors/#New
[`fmt.Errorf`]: https://golang.org/pkg/fmt/#Errorf
For example,
```go
return errors.New(fmt.Sprintf("invalid port: %v", err))
// becomes
return fmt.Errorf("invalid port: %v", err)cd to your Go project's directory.$ cd ~/go/src/example.com/myproject Run gopatch on the project, supplying the previously written patch with the
-p flag.
$ gopatch -p ~/s1028.patch ./...This will apply the patch on all Go code in your project.
Check if there were any instances of this issue in your code by running
git diff.
Instead, cd to your Go project's directory.
$ cd ~/go/src/example.com/myprojectRun gopatch on the project, supplying the previously written patch with the
-p flag along with '-d' flag.
$ gopatch -d -p ~/s1028.patch ./...This will turn on diff mode and will write the diff to stdout instead of modifying all the Go code in your project. To provide more context on what the patch does, if there were description comments in the patch, they will also get displayed at the top. To learn more about description comments jump to section here
For example if we applied patch ~/s1028 to our testfile error.go
$ gopatch -d -p ~/s1028.patch ./testdata/test_files/diff_example/Output would be :
gopatch/testdata/test_files/diff_example/error.go:Replace redundant fmt.Sprintf with fmt.Errorf
--- gopatch/testdata/test_files/diff_example/error.go
+++ gopatch/testdata/test_files/diff_example/error.go
@@ -7,7 +7,7 @@
func foo() error {
err := errors.New("test")
- return errors.New(fmt.Sprintf("error: %v", err))
+ return fmt.Errorf("error: %v", err)
}
func main() {
Note: Only the description comments of patches that actually apply are displayed.
To learn how to write your own patches, move on to the Patches section. To dive deeper into patches, check out Patches in depth.
To experiment with other sample patches, check out the Examples section.
To use the gopatch command line tool, provide the following arguments.
gopatch [options] pattern ...Where pattern specifies one or more Go files, or directories containing Go files. For directories, all Go code inside them and their descendants will be considered by gopatch.
gopatch supports the following command line options.
-p file, --patch=file
Path to a patch file specifying a transformation. Read more about the patch file format in Patches.
Provide this flag multiple times to apply multiple patches in-order.
$ gopatch -p foo.patch -p bar.patch path/to/my/projectIf this flag is omitted, a patch is expected on stdin.
$ gopatch path/to/my/project **What is a Go expression?**Expressions usually refer to code that has value. You can pass these as arguments to functions. These include
x,foo(),user.Name, etc.Check the Identifiers vs expressions vs statements section of the appendix for more.
So the following patch will search for invocations of foo with a single
argument---any argument---and replace them with invocations of bar with the
same argument.
@@
var x expression
@@
-foo(x)
+bar(x)| Input | Output |
|---|---|
foo(42) |
bar(42) |
foo(answer) |
bar(answer) |
foo(getAnswer()) |
bar(getAnswer()) |
Metavariables hold the entire matched value, so we can add code around them without risk of breaking anything.
@@
var x expression
@@
-foo(x)
+bar(x + 3, true)| Input | Output |
|---|---|
foo(42) |
bar(42 + 3, true) |
foo(answer) |
bar(answer + 3, true) |
foo(getAnswer()) |
bar(getAnswer() + 3, true) |
For more on metavariables see Patches in depth/Metavariables.
gopatch patches are not limited to transforming basic expressions. You can also transform statements.
What is a Go statements?
Statements are instructions to do things, and do not have value. They cannot be passed as parameters to other functions. These include assignments (
foo := bar()), if statements (if foo { bar() }), variable declarations (var foo Bar), and so on.Check the Identifiers vs expressions vs statements section of the appendix for more.
For example, consider the following patch.
@@
var f expression
var err identifier
@@
-err = f
-if err != nil {
+if err := f; err != nil {
return err
}The patch declares two metavariables:
f: This represents an operation that possibly returns an errorerr: This represents the name of the error variableThe patch will search for code that assigns to an error variable immediately
before returning it, and inlines the assignment into the if statement. This
effectively reduces the scope of the variable to just the if statement.
InputOutput
err = foo(bar, baz)
if err != nil {
return err
}if err := foo(bar, baz); err != nil {
return err
}err = comment.Submit(ctx)
if err != nil {
return err
}if err := comment.Submit(ctx); err != nil {
return err
}For more on transforming statements, see Patches In Depth/Statements.
Matching a single argument is still too selective and we may want to match a wider criteria.
For this, gopatch supports elision of code by adding ... in many places.
For example,
@@
@@
-foo(...)
+bar(...)The patch above looks for all calls to the function foo and replaces them
with calls to the function bar, regardless of the number of arguments they
have.
| Input | Output |
|---|---|
foo(42) |
bar(42) |
foo(42, true, 1) |
bar(42, true, 1) |
foo(getAnswer(), x(y())) |
bar(getAnswer(), x(y())) |
Going back to the patch from Statements, we can instead write the following patch.
@@
var f expression
var err identifier
@@
-err = f
-if err != nil {
+if err := f; err != nil {
return ..., err
}This patch is almost exactly the same as before except the return statement
was changed to return ..., err. This will allow the patch to operate even on
functions that return multiple values.
InputOutput
err = foo()
if err != nil {
return false, err
}if err := foo(); err != nil {
return false, err
}For more on elision, see Patches in depth/Elision.
Patches come with comments to give more context about what they do.
Comments are prefixed by '#'
For example:
# Replace time.Now().Sub(x) with time.Since(x)
@@
# var x is in the metavariable section
var x identifier
@@
-time.Now().Sub(x)
+time.Since(x)
# We replace time.Now().Sub(x)
# with time.Since(x)Description comments are comments that appear directly above a patch's first
@@ line.
gopatch will record these descriptions and display them to users with use of
the --diff or --print-only flags.
For example,
# Replace time.Now().Sub(x) with time.Since(x)
@@
# Not a description comment
var x identifier
@@
-time.Now().Sub(x)
+time.Since(x)
# Not a description comment
# Not a description commentPatch files with multiple patches can have a separate description for each patch.
# Replace redundant fmt.Sprintf with fmt.Errorf
@@
@@
-import "errors"
-errors.New(fmt.Sprintf(...))
+fmt.Errorf(...)
# Replace time.Now().Sub(x) with time.Since(x)
@@
var x identifier
@@
-time.Now().Sub(x)
+time.Since(x)
# Not a description commentAs these are messages that will be printed to users of the patch, we recommend the following best practices for description comments.
--diffWhen diff mode is turned on by the -d/--diff flag, gopatch will print
description comments for patches that matched different files to stderr.
$ gopatch -d -p ~/s1028.patch testdata/test_files/diff_example/error.go
error.go:Replace redundant fmt.Sprintf with fmt.Errorf
--- error.go
+++ error.go
@@ -7,7 +7,7 @@
func foo() error {
err := errors.New("test")
-No open issues yet, or sync has not completed.