├── .github ├── dependabot.yml └── workflows │ ├── fossa.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Appendix.md ├── HACKING.md ├── PatchesInDepth.md └── RELEASE.md ├── e2e_test.go ├── examples ├── destutter.patch ├── gomock-v1.5.0.patch ├── s1012.patch ├── s1028.patch └── s1038.patch ├── go.mod ├── go.sum ├── internal ├── astdiff │ ├── diff.go │ └── snapshot.go ├── data │ ├── data.go │ └── data_test.go ├── diff │ └── diff.go ├── engine │ ├── change.go │ ├── changelog.go │ ├── compile.go │ ├── file.go │ ├── file_test.go │ ├── for_dots.go │ ├── for_dots_test.go │ ├── import.go │ ├── matcher.go │ ├── matcher_test.go │ ├── meta.go │ ├── meta_test.go │ ├── metavar.go │ ├── metavar_test.go │ ├── pos.go │ ├── reflect_match.go │ ├── reflect_match_test.go │ ├── reflect_replace.go │ ├── reflect_replace_test.go │ ├── replacer.go │ ├── search.go │ ├── slice_dots.go │ ├── stmt_list.go │ ├── stmt_list_test.go │ ├── util.go │ └── utils_for_test.go ├── goast │ ├── imports.go │ ├── imports_test.go │ ├── pos.go │ ├── pos_test.go │ └── types.go ├── parse │ ├── ast.go │ ├── change.go │ ├── doc.go │ ├── meta.go │ ├── meta_test.go │ ├── parse.go │ ├── patch.go │ ├── patch_test.go │ └── section │ │ ├── bytes.go │ │ ├── bytes_test.go │ │ ├── section.go │ │ └── section_test.go ├── pgo │ ├── ast.go │ ├── augment.go │ ├── augment │ │ ├── augment.go │ │ ├── augment_test.go │ │ ├── find.go │ │ └── rewrite.go │ ├── doc.go │ ├── parse.go │ └── parse_test.go └── text │ └── unlines.go ├── loader.go ├── loader_test.go ├── main.go ├── main_test.go ├── patch └── gopatch.go ├── testdata ├── README.md ├── add_ctx_param ├── add_error_param ├── add_iface_return ├── case_elision ├── const_to_var ├── dedupe_args ├── defer_return ├── delete_any_import ├── delete_dots ├── delete_field ├── delete_import_panic ├── delete_struct_field ├── delete_unnamed_import ├── destutter ├── dts_in_args ├── dts_in_receiver ├── dts_in_results ├── embed_to_field ├── embed_to_newtype ├── foo_call_to_bar ├── func_within_a_func ├── generic_instantiation ├── generics_in_src ├── gomock ├── httpclient_use_ctx ├── if_to_for ├── import_grouping ├── inline_err_assignments ├── inline_errors ├── match_named_import ├── matches_test_files ├── mismatched_dots ├── name_and_rewrite_unnamed_import ├── name_unnamed_import ├── nil_safe_string ├── noop_import ├── optimize_string_appends ├── patch │ ├── error.patch │ ├── replace_to_with_ptr.patch │ └── time.patch ├── range_value_elision ├── recognize_all_imports ├── redundant_fmt_errorf ├── redundant_fmt_sprintf ├── remove_context_field ├── rename_named_import ├── rename_package ├── rename_unnamed_import ├── replace_any_import_with_unnamed ├── replace_import_with_top_level_comment ├── replace_net_context ├── replace_with_time_since ├── return_err ├── rewrite_import_paths ├── s1012 ├── s1028 ├── s1038 ├── select_elision ├── slice_and_import ├── stmt_to_expr ├── string_builder_for_loop ├── string_repeat ├── struct_decl_field_rename ├── struct_field_list ├── struct_field_pair ├── struct_init_field_rename ├── switch_elision ├── test_files │ ├── diff_example │ │ └── error.go │ ├── lint_example │ │ └── time.go │ ├── skip_generated_files │ │ ├── simple_generated.go │ │ └── special_notation_generated.go │ └── skip_import_processing_example │ │ └── test1.go ├── type_to_alias ├── undo_expr_patch ├── unnamed_import_to_named ├── value_group ├── value_group_elision ├── value_list_elision └── writebytes_to_write ├── tools ├── cmd │ └── extract-changelog │ │ ├── main.go │ │ └── main_test.go ├── go.mod ├── go.sum └── tools.go └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "gomod" 9 | directory: "/tools" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: FOSSA Analysis 2 | on: push 3 | 4 | jobs: 5 | fossa: 6 | runs-on: ubuntu-latest 7 | if: github.repository_owner == 'uber-go' 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: FOSSA analysis 13 | uses: fossas/fossa-action@v1 14 | with: 15 | api-key: ${{ secrets.FOSSA_API_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.22.x 20 | 21 | - name: Load cached dependencies 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | 27 | - name: Download dependencies 28 | run: go mod download 29 | 30 | - name: Build 31 | run: make build 32 | 33 | - name: Lint 34 | run: make lint 35 | 36 | - name: Test 37 | run: make cover 38 | 39 | - name: Upload coverage to codecov.io 40 | uses: codecov/codecov-action@v4 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: ['v*'] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.22.x 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Load cached dependencies 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/go/pkg/mod 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | 33 | - name: Prepare release 34 | run: | 35 | set -eou pipefail 36 | make bin/extract-changelog 37 | 38 | # Extract target version number from the git tag and post it to 39 | # GITHUB_ENV to make it accessible via env.VERSION in other steps. 40 | VERSION=${{ github.ref }} 41 | VERSION="${VERSION#refs/tags/}" # refs/tags/v1.2.3 => v1.2.3 42 | echo "VERSION=$VERSION" >> $GITHUB_ENV 43 | 44 | echo "Releasing $VERSION" 45 | 46 | # Place the release notes in changes.v1.2.3.txt to grab them in the 47 | # release step. 48 | echo "Release notes:" 49 | echo "----" 50 | bin/extract-changelog $VERSION | tee changes.$VERSION.txt 51 | echo "----" 52 | 53 | - name: Release 54 | uses: goreleaser/goreleaser-action@v6 55 | with: 56 | distribution: goreleaser 57 | version: latest 58 | args: release --clean --release-notes changes.${{ env.VERSION }}.txt 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | GORELEASER_CURRENT_TAG: ${{ env.VERSION }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /gopatch 2 | /bin 3 | /cover.out 4 | /cover.html 5 | /dist 6 | /changes.*.txt 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | goos: 5 | - linux 6 | - windows 7 | - darwin 8 | # The default flags from goreleaser set a "version" variable instead of our 9 | # preferred "_version". The "-w" skips DWARF generation to reduce the 10 | # binary size (https://github.com/golang/go/issues/26074). This will not 11 | # affect usability of panics and profiling. 12 | ldflags: '-w -X main._version={{.Version}}' 13 | 14 | # List of replacements generated by goreleaser to match the output of `uname` 15 | # on various systems. This will make the binary more easily curl-able. 16 | archives: 17 | - name_template: >- 18 | {{ .ProjectName }}_ 19 | {{- title .Os }}_ 20 | {{- if eq .Arch "amd64" }}x86_64 21 | {{- else if eq .Arch "386" }}i386 22 | {{- else }}{{ .Arch }}{{ end }} 23 | 24 | checksum: 25 | name_template: 'checksums.txt' 26 | 27 | # Snapshot releases should have the -dev suffix. 28 | snapshot: 29 | name_template: "{{ incpatch .Tag }}-dev" 30 | 31 | changelog: 32 | # Skip commit log generation because we'll build ours from the CHANGELOG.md. 33 | skip: true 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.4.0 - 2024-04-03 9 | ### Added 10 | - ([#150]) `--skip-generated` flag to skip running on files containing 11 | generated code. 12 | 13 | [#150]: https://github.com/uber-go/gopatch/pull/150 14 | 15 | ## 0.3.0 - 2023-07-14 16 | ### Added 17 | - ([#112]) `--skip-import-processing` flag to skip import processing. 18 | 19 | [#112]: https://github.com/uber-go/gopatch/pull/112 20 | 21 | ## 0.2.0 - 2023-04-03 22 | ### Added 23 | - ([#64]) `--diff` flag reports a diff of the changes that would be made, but does not 24 | make them. 25 | - ([#70]) `--print-only` prints the modified contents of all matched files to stdout in 26 | their entirety without modifying files on disk. 27 | - ([#77]) `-P`/`--patches-file` reads a list of patch files from another file on-disk. 28 | 29 | [#64]: https://github.com/uber-go/gopatch/pull/64 30 | [#70]: https://github.com/uber-go/gopatch/pull/70 31 | [#77]: https://github.com/uber-go/gopatch/pull/77 32 | 33 | ## 0.1.1 - 2022-07-26 34 | ### Fixed 35 | - ([#54]) Preserve top-level comments in files when updating imports. 36 | - Parse generics syntax introduced in Go 1.18. 37 | 38 | [#54]: https://github.com/uber-go/gopatch/issues/54 39 | 40 | Thanks to @breml for their contribution to this release. 41 | 42 | ## 0.1.0 - 2021-08-19 43 | Starting this release, we will include pre-built binaries of gopatch for 44 | different systems. 45 | 46 | ### Added 47 | - ([#7]): Add support for verbose logging with a `-v` flag. 48 | - Add [introductory documentation] and [patch guide]. 49 | 50 | [introductory documentation]: https://github.com/uber-go/gopatch/blob/main/README.md 51 | [patch guide]: https://github.com/uber-go/gopatch/blob/main/docs/PatchesInDepth.md 52 | [#7]: https://github.com/uber-go/gopatch/issues/7 53 | 54 | ### Changed 55 | - Only the `--version` flag now prints the version number. The `-v` is used for 56 | verbose logging instead. 57 | 58 | ### Fixed 59 | - ([#2]): Patches with named imports now support matching and manipulating any 60 | import. 61 | - Fix issue where rewrites of unnamed imports would end up with duplicate 62 | entries. 63 | 64 | [#2]: https://github.com/uber-go/gopatch/issues/2 65 | 66 | ## 0.0.2 - 2020-11-04 67 | ### Fixed 68 | - Fixed unintended deletion of unchanged named imports. 69 | 70 | ## 0.0.1 - 2020-01-14 71 | 72 | - Initial alpha release. 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = bin 2 | 3 | export GO111MODULE=on 4 | export GOBIN ?= $(shell pwd)/$(BIN) 5 | 6 | GO_FILES = $(shell find . \ 7 | -path '*/.*' -prune -o \ 8 | '(' -type f -a -name '*.go' ')' -print) 9 | 10 | GOLINT = $(BIN)/golint 11 | STATICCHECK = $(BIN)/staticcheck 12 | EXTRACT_CHANGELOG = $(BIN)/extract-changelog 13 | TOOLS = $(GOLINT) $(STATICCHECK) $(EXTRACT_CHANGELOG) 14 | 15 | .PHONY: all 16 | all: build lint test 17 | 18 | .PHONY: build 19 | build: 20 | go build ./... 21 | 22 | .PHONY: test 23 | test: 24 | go test -v -race ./... 25 | 26 | .PHONY: cover 27 | cover: 28 | go test -race -coverprofile=cover.out -coverpkg=./... ./... 29 | go tool cover -html=cover.out -o cover.html 30 | 31 | .PHONY: lint 32 | lint: gofmt golint staticcheck 33 | 34 | .PHONY: gofmt 35 | gofmt: 36 | $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) 37 | @gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true 38 | @[ ! -s "$(FMT_LOG)" ] || \ 39 | (echo "gofmt failed. Please reformat the following files:" | \ 40 | cat - $(FMT_LOG) && false) 41 | 42 | .PHONY: golint 43 | golint: $(GOLINT) 44 | $(GOLINT) ./... 45 | 46 | .PHONY: staticcheck 47 | staticcheck: $(STATICCHECK) 48 | $(STATICCHECK) ./... 49 | 50 | .PHONY: tools 51 | tools: $(GOLINT) $(STATICCHECK) 52 | 53 | $(GOLINT): tools/go.mod 54 | cd tools && go install golang.org/x/lint/golint 55 | 56 | $(STATICCHECK): tools/go.mod 57 | cd tools && go install honnef.co/go/tools/cmd/staticcheck 58 | 59 | $(EXTRACT_CHANGELOG): tools/cmd/extract-changelog/main.go 60 | cd tools && go install github.com/uber-go/gopatch/tools/cmd/extract-changelog 61 | -------------------------------------------------------------------------------- /docs/Appendix.md: -------------------------------------------------------------------------------- 1 | # Appendix 2 | 3 | ## Identifiers vs expressions vs statements 4 | 5 | A simplified explanation of the difference between identifiers, expressions 6 | and statements is, 7 | 8 | - [**identifiers**] are names of things 9 | - [**expressions**] are things that have values (you can pass these into 10 | functions), or refer to types 11 | - [**statements**] are instructions to do things 12 | 13 | [**identifiers**]: https://golang.org/ref/spec#identifier 14 | [**expressions**]: https://golang.org/ref/spec#Expression 15 | [**statements**]: https://golang.org/ref/spec#Statement 16 | 17 | Consider the following snippet. 18 | 19 | ```go 20 | var bar Bar 21 | if err := foo(bar.Baz()); err != nil { 22 | return err 23 | } 24 | ``` 25 | 26 | It contains, 27 | 28 | - identifiers: `err`, `foo`, `bar`, `Bar`, `Baz` 29 | 30 | ``` 31 | var bar Bar 32 | '-' '-' 33 | if err := foo(bar.Baz()); err != nil { 34 | '-' '-' '-' '-' '-' 35 | return err 36 | '-' 37 | } 38 | ``` 39 | 40 | - expressions: `Bar`, `bar.Baz`, `bar.Baz()`, `foo(bar.Baz())`, `err`, `nil`, 41 | and `err != nil` 42 | 43 | ``` 44 | var bar Bar 45 | '-' 46 | .-------. 47 | .---|-------|. .---------. 48 | if err := foo(bar.Baz()); err != nil { 49 | '-' | '-' '-' 50 | '-----' 51 | return err 52 | '-' 53 | } 54 | ``` 55 | 56 | Note that `bar` in `var bar Bar` is not an expression. 57 | 58 | - statements: `var ...`, `err := ...`, `if ...`, and `return ...` 59 | 60 | ``` 61 | var bar Bar 62 | '---------' 63 | if err := foo(bar.Baz()); err != nil { -. 64 | '-------------------' | 65 | return err | 66 | '--------' | 67 | } -' 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | Take the following steps to release a new version of gopatch. 2 | 3 | 1. Ensure that the CHANGELOG.md has an entry for every **user-facing** change. 4 | Do not add entries for changes that are not user-facing. 5 | 6 | 2. Change the "Unreleased" header to the target version number *without* the 7 | `v` prefix and add today's date in YYYY-MM-DD format. For example, if the 8 | target version is `v1.2.3`, add: 9 | 10 | ```diff 11 | -## Unreleased 12 | +## 1.2.3 - 2021-08-18 13 | ``` 14 | 15 | 3. Update the `VERSION` constant in the Installation section of the README.md. 16 | 17 | ```diff 18 | -VERSION=1.2.2 19 | +VERSION=1.2.3 20 | ``` 21 | 22 | 4. Create a new PR with the change and the following title: 23 | 24 | ``` 25 | Preparing release v1.2.3 26 | ``` 27 | 28 | 5. After landing the PR, tag the release with an **annotated** git tag and push 29 | the tag. 30 | 31 | ``` 32 | $ git pull 33 | $ git tag -a v1.2.3 -m v1.2.3 34 | $ git push origin v1.2.3 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/destutter.patch: -------------------------------------------------------------------------------- 1 | @@ 2 | @@ 3 | package http 4 | 5 | -HTTPClient 6 | +Client 7 | 8 | @@ 9 | var http identifier 10 | @@ 11 | import http "example.com/http" 12 | 13 | -http.HTTPClient 14 | +http.Client 15 | -------------------------------------------------------------------------------- /examples/gomock-v1.5.0.patch: -------------------------------------------------------------------------------- 1 | # Delete redundant gomock.Controller.Finish() 2 | @@ 3 | # In gomock 1.50, the Controller.Finish method is called automatically when the 4 | # test finishes running. 5 | # We no longer need to call mockCtrl.Finish manually. 6 | var ctrl, gomock identifier 7 | var t expression 8 | @@ 9 | import gomock "github.com/golang/mock/gomock" 10 | 11 | ctrl := gomock.NewController(t) 12 | ... 13 | -defer ctrl.Finish() 14 | -------------------------------------------------------------------------------- /examples/s1012.patch: -------------------------------------------------------------------------------- 1 | @@ 2 | var x identifier 3 | @@ 4 | -time.Now().Sub(x) 5 | +time.Since(x) 6 | -------------------------------------------------------------------------------- /examples/s1028.patch: -------------------------------------------------------------------------------- 1 | @@ 2 | @@ 3 | -import "errors" 4 | 5 | -errors.New(fmt.Sprintf(...)) 6 | +fmt.Errorf(...) 7 | -------------------------------------------------------------------------------- /examples/s1038.patch: -------------------------------------------------------------------------------- 1 | @@ 2 | @@ 3 | -fmt.Print(fmt.Sprintf(...)) 4 | +fmt.Printf(...) 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/uber-go/gopatch 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/go-intervals v0.0.2 7 | github.com/jessevdk/go-flags v1.6.1 8 | github.com/kr/pretty v0.3.1 9 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e 10 | github.com/rogpeppe/go-internal v1.12.0 11 | github.com/stretchr/testify v1.9.0 12 | go.uber.org/multierr v1.11.0 13 | golang.org/x/tools v0.24.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | golang.org/x/mod v0.20.0 // indirect 21 | golang.org/x/sync v0.8.0 // indirect 22 | golang.org/x/sys v0.23.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= 5 | github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= 6 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 7 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= 13 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 17 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 18 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 22 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 23 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 24 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 25 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 26 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 27 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 28 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 29 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 30 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /internal/astdiff/snapshot.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package astdiff 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | 28 | "github.com/uber-go/gopatch/internal/goast" 29 | ) 30 | 31 | type value struct { 32 | t reflect.Type 33 | 34 | // Only one of the following three is set. 35 | isNil bool 36 | value any 37 | Elem *value 38 | Children []*value 39 | 40 | // Set only if this is a Node. 41 | IsNode bool 42 | pos, end token.Pos 43 | Comments []*ast.CommentGroup 44 | } 45 | 46 | func (v *value) Type() reflect.Type { return v.t } 47 | func (v *value) Kind() reflect.Kind { return v.t.Kind() } 48 | func (v *value) Pos() token.Pos { return v.pos } 49 | func (v *value) End() token.Pos { return v.end } 50 | func (v *value) Interface() any { return v.value } 51 | func (v *value) Len() int { return len(v.Children) } 52 | func (v *value) IsNil() bool { return v.isNil } 53 | 54 | func snapshot(v reflect.Value, cmap ast.CommentMap) (val *value) { 55 | t := v.Type() 56 | 57 | switch t { 58 | case goast.ObjectPtrType: 59 | // Ident.obj forms a cycle. 60 | return &value{t: t, isNil: true} 61 | case goast.CommentGroupPtrType, goast.ScopePtrType: 62 | // Snapshots don't care about comment groups. 63 | return &value{t: t, isNil: true} 64 | case goast.PosType: 65 | return &value{ 66 | t: t, 67 | value: v.Interface(), 68 | } 69 | } 70 | 71 | if t.Implements(goast.NodeType) && !v.IsNil() { 72 | defer func(n ast.Node) { 73 | val.IsNode = true 74 | val.pos = n.Pos() 75 | val.end = n.End() 76 | val.Comments = cmap[n] 77 | }(v.Interface().(ast.Node)) 78 | } 79 | 80 | switch v.Kind() { 81 | case reflect.Ptr, reflect.Interface: 82 | if v.IsNil() { 83 | return &value{t: t, isNil: true} 84 | } 85 | 86 | return &value{ 87 | t: t, 88 | Elem: snapshot(v.Elem(), cmap), 89 | } 90 | case reflect.Slice: 91 | children := make([]*value, v.Len()) 92 | for i := 0; i < v.Len(); i++ { 93 | children[i] = snapshot(v.Index(i), cmap) 94 | } 95 | return &value{ 96 | t: t, 97 | Children: children, 98 | } 99 | 100 | case reflect.Struct: 101 | children := make([]*value, v.NumField()) 102 | for i := 0; i < v.NumField(); i++ { 103 | children[i] = snapshot(v.Field(i), cmap) 104 | } 105 | return &value{ 106 | t: t, 107 | Children: children, 108 | } 109 | 110 | default: 111 | return &value{ 112 | t: t, 113 | value: v.Interface(), 114 | } 115 | } 116 | } 117 | 118 | func minPos(l, r token.Pos) token.Pos { 119 | if l < r { 120 | return l 121 | } 122 | return r 123 | } 124 | 125 | func maxPos(l, r token.Pos) token.Pos { 126 | if l > r { 127 | return l 128 | } 129 | return r 130 | } 131 | -------------------------------------------------------------------------------- /internal/data/data.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package data 22 | 23 | import ( 24 | "fmt" 25 | "reflect" 26 | ) 27 | 28 | // Data stores arbitrary data for Matchers and Replacers as they traverse or 29 | // build the Go AST. 30 | // 31 | // Data provides an interface very similar to context.Context. 32 | type Data interface { 33 | // Keys returns all known keys for this Data object in an unspecified 34 | // order. 35 | Keys() []any 36 | 37 | // Values retrives the value associated with the given key or nil. 38 | Value(k any) any 39 | } 40 | 41 | // New builds a new empty Data. 42 | func New() Data { 43 | return &emptyData{} 44 | } 45 | 46 | type emptyData struct{} 47 | 48 | func (d *emptyData) Keys() []any { return nil } 49 | func (d *emptyData) Value(any) any { return nil } 50 | 51 | // WithValue returns a new Data with the given key-value pair associated with 52 | // it. 53 | // 54 | // d = data.WithValue(d, x, 42) 55 | // d.Value(x) // == 42 56 | // 57 | // The original object is left unmodified so if the returned Data object is 58 | // discarded, its value will not be made available. 59 | // 60 | // Retreiving the value from this Data object is a linear time operation. To 61 | // optimize for read-heavy use cases, use the Index function. 62 | // 63 | // Panics if either the key or the value are nil. 64 | func WithValue(d Data, k, v any) Data { 65 | if k == nil || v == nil { 66 | panic("key or value may not be nil") 67 | } 68 | return &valueData{Data: d, k: k, v: v} 69 | } 70 | 71 | type valueData struct { 72 | Data 73 | 74 | k, v any 75 | } 76 | 77 | func (d *valueData) Keys() []any { 78 | return append(d.Data.Keys(), d.k) 79 | } 80 | 81 | func (d *valueData) Value(k any) any { 82 | if k == d.k { 83 | return d.v 84 | } 85 | return d.Data.Value(k) 86 | } 87 | 88 | // Lookup retrieves the value associated with the given key and stores it into 89 | // the pointer that vptr points to. 90 | // 91 | // Reports whether a value was found. 92 | // 93 | // d = data.WithValue(d, x, 42) 94 | // 95 | // var out int 96 | // data.Lookup(k, x, &out) // == true 97 | // 98 | // Panics if the type of the value for the pointer is not compatible with the 99 | // value associated with the key. 100 | func Lookup(d Data, k, vptr any) (ok bool) { 101 | dest := reflect.ValueOf(vptr) 102 | if t := dest.Type(); t.Kind() != reflect.Ptr { 103 | panic(fmt.Sprintf("Lookup target must be a pointer, not %v", t)) 104 | } 105 | 106 | v := d.Value(k) 107 | if v != nil { 108 | dest.Elem().Set(reflect.ValueOf(v)) 109 | } 110 | 111 | return v != nil 112 | } 113 | 114 | // Index returns a copy of the provided Data object where items are indexed 115 | // for fast lookup. 116 | // 117 | // Lookups with Value or the Lookup function are constant time in the returned 118 | // data object. 119 | // 120 | // Use this if your workload is divided between write-heavy and read-heavy 121 | // portions. 122 | func Index(d Data) Data { 123 | // Already indexed. 124 | if _, ok := d.(*indexedData); ok { 125 | return d 126 | } 127 | 128 | keys := d.Keys() 129 | items := make(map[any]any) 130 | for _, k := range keys { 131 | items[k] = d.Value(k) 132 | } 133 | 134 | return &indexedData{ 135 | items: items, 136 | keys: keys, 137 | } 138 | } 139 | 140 | type indexedData struct { 141 | keys []any 142 | items map[any]any 143 | } 144 | 145 | func (d *indexedData) Keys() []any { 146 | keys := make([]any, len(d.keys)) 147 | copy(keys, d.keys) 148 | return keys 149 | } 150 | 151 | func (d *indexedData) Value(k any) any { 152 | return d.items[k] 153 | } 154 | -------------------------------------------------------------------------------- /internal/data/data_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package data 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestNew(t *testing.T) { 30 | d := New() 31 | assert.Empty(t, d.Keys()) 32 | assert.Nil(t, d.Value(42)) 33 | } 34 | 35 | func TestValues(t *testing.T) { 36 | t.Run("indexed", func(t *testing.T) { 37 | testValues(t, Index) 38 | }) 39 | 40 | t.Run("no index", func(t *testing.T) { 41 | testValues(t, func(d Data) Data { return d }) 42 | }) 43 | 44 | t.Run("indexed twice", func(t *testing.T) { 45 | testValues(t, func(d Data) Data { 46 | return Index(Index(d)) 47 | }) 48 | }) 49 | } 50 | 51 | func testValues(t *testing.T, maybeIndex func(Data) Data) { 52 | type key string 53 | 54 | t.Run("adding a value", func(t *testing.T) { 55 | d := New() 56 | d = WithValue(d, key("foo"), 1) 57 | d = WithValue(d, key("bar"), 2) 58 | d = maybeIndex(d) 59 | 60 | t.Run("Keys", func(t *testing.T) { 61 | assert.ElementsMatch(t, []key{"foo", "bar"}, d.Keys()) 62 | }) 63 | 64 | t.Run("Value/match", func(t *testing.T) { 65 | assert.Equal(t, 1, d.Value(key("foo"))) 66 | assert.Equal(t, 2, d.Value(key("bar"))) 67 | }) 68 | 69 | t.Run("Value/wrong type", func(t *testing.T) { 70 | assert.Nil(t, d.Value("foo")) 71 | }) 72 | }) 73 | 74 | t.Run("adding and ignoring", func(t *testing.T) { 75 | d1 := WithValue(New(), key("foo"), 1) 76 | d2 := WithValue(maybeIndex(d1), key("bar"), 2) 77 | d3 := maybeIndex(WithValue(d1, key("baz"), 3)) 78 | 79 | t.Run("Keys", func(t *testing.T) { 80 | assert.ElementsMatch(t, []key{"foo"}, d1.Keys()) 81 | assert.ElementsMatch(t, []key{"foo", "bar"}, d2.Keys()) 82 | assert.ElementsMatch(t, []key{"foo", "baz"}, d3.Keys()) 83 | }) 84 | 85 | t.Run("Value/foo", func(t *testing.T) { 86 | assert.Equal(t, 1, d1.Value(key("foo"))) 87 | assert.Equal(t, 1, d2.Value(key("foo"))) 88 | assert.Equal(t, 1, d3.Value(key("foo"))) 89 | }) 90 | 91 | t.Run("Value/bar", func(t *testing.T) { 92 | assert.Nil(t, d1.Value(key("bar"))) 93 | assert.Equal(t, 2, d2.Value(key("bar"))) 94 | assert.Nil(t, d3.Value(key("bar"))) 95 | }) 96 | 97 | t.Run("Value/baz", func(t *testing.T) { 98 | assert.Nil(t, d1.Value(key("baz"))) 99 | assert.Nil(t, d2.Value(key("baz"))) 100 | assert.Equal(t, 3, d3.Value(key("baz"))) 101 | }) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /internal/engine/change.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "fmt" 25 | "go/ast" 26 | "go/token" 27 | "sort" 28 | 29 | "github.com/uber-go/gopatch/internal/data" 30 | "github.com/uber-go/gopatch/internal/parse" 31 | ) 32 | 33 | // Change is a single Change in a program. 34 | type Change struct { 35 | Name string 36 | Meta *Meta 37 | 38 | Comments []string 39 | fset *token.FileSet 40 | matcher FileMatcher 41 | replacer FileReplacer 42 | } 43 | 44 | func (c *compiler) compileChange(achange *parse.Change) *Change { 45 | meta := c.compileMeta(achange.Meta) 46 | 47 | mc := newMatcherCompiler(c.fset, meta, achange.Patch.Pos(), achange.Patch.End()) 48 | rc := newReplacerCompiler(c.fset, meta, achange.Patch.Pos(), achange.Patch.End()) 49 | 50 | matcher := mc.compileFile(achange.Patch.Minus) 51 | replacer := rc.compileFile(achange.Patch.Plus) 52 | 53 | ldots := mc.dots 54 | rdots := rc.dots 55 | connectDots(c.fset, ldots, rdots, rc.dotAssoc) 56 | 57 | return &Change{ 58 | Name: achange.Name, // TODO(abg): validate name 59 | Meta: meta, 60 | fset: c.fset, 61 | matcher: matcher, 62 | replacer: replacer, 63 | Comments: achange.Comments, 64 | } 65 | } 66 | 67 | // Match matches this change in the given Go AST and returns captured match 68 | // information it a data.Data object. 69 | func (c *Change) Match(f *ast.File) (d data.Data, ok bool) { 70 | return c.matcher.Match(f, data.New()) 71 | } 72 | 73 | // Replace generates a replacement File based on previously captured match 74 | // data. 75 | func (c *Change) Replace(d data.Data, cl Changelog) (*ast.File, error) { 76 | return c.replacer.Replace(d, cl) 77 | } 78 | 79 | func connectDots(fset *token.FileSet, lhs, rhs []token.Pos, conns map[token.Pos]token.Pos) error { 80 | cache := make(map[token.Pos]token.Position) 81 | getPosition := func(pos token.Pos) token.Position { 82 | p, ok := cache[pos] 83 | if !ok { 84 | p = fset.Position(pos) 85 | cache[pos] = p 86 | } 87 | return p 88 | } 89 | 90 | sort.Slice(lhs, func(i, j int) bool { 91 | pi := getPosition(lhs[i]) 92 | pj := getPosition(lhs[j]) 93 | 94 | // Descending order. 95 | return pi.Line > pj.Line || pi.Line == pj.Line && pi.Column > pj.Column 96 | }) 97 | 98 | sort.Slice(rhs, func(i, j int) bool { 99 | pi := getPosition(rhs[i]) 100 | pj := getPosition(rhs[j]) 101 | 102 | // Ascending order. 103 | return pi.Line < pj.Line || pi.Line == pj.Line && pi.Column < pj.Column 104 | }) 105 | 106 | for _, r := range rhs { 107 | rpos := getPosition(r) 108 | 109 | i := sort.Search(len(lhs), func(i int) bool { 110 | lpos := getPosition(lhs[i]) 111 | return lpos.Line < rpos.Line || lpos.Line == rpos.Line && lpos.Column <= rpos.Column 112 | }) 113 | 114 | if i == len(lhs) { 115 | return fmt.Errorf(`%v: "..." in "+" section does not have an associated "..." in "-" section`, rpos) 116 | } 117 | 118 | if other, conflict := conns[r]; conflict { 119 | lpos := getPosition(lhs[i]) 120 | conflictPos := getPosition(other) 121 | return fmt.Errorf( 122 | `cannot associate "..." at %v with %v: already associated with %v`, 123 | rpos, lpos, conflictPos, 124 | ) 125 | } 126 | 127 | conns[r] = lhs[i] 128 | } 129 | 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /internal/engine/compile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "go/token" 27 | 28 | "github.com/uber-go/gopatch/internal/parse" 29 | "go.uber.org/multierr" 30 | ) 31 | 32 | // Program is a collection of compiled changes. 33 | type Program struct { 34 | Changes []*Change 35 | } 36 | 37 | // Compile compiles a parsed gopatch Program. 38 | func Compile(fset *token.FileSet, p *parse.Program) (*Program, error) { 39 | c := newCompiler(fset) 40 | return c.compileProgram(p), c.Err() 41 | } 42 | 43 | type compiler struct { 44 | fset *token.FileSet 45 | errors []error 46 | } 47 | 48 | func newCompiler(fset *token.FileSet) *compiler { 49 | return &compiler{fset: fset} 50 | } 51 | 52 | // Convenience function to build error messages with positioning data. 53 | func (c *compiler) errf(pos token.Pos, msg string, args ...any) { 54 | if len(args) > 0 { 55 | msg = fmt.Sprintf(msg, args...) 56 | } 57 | if pos.IsValid() { 58 | msg = fmt.Sprintf("%v: %v", c.fset.Position(pos), msg) 59 | } 60 | c.errors = append(c.errors, errors.New(msg)) 61 | } 62 | 63 | // Err collates all the errors encountered during compilation and returns 64 | // them. 65 | func (c *compiler) Err() error { 66 | return multierr.Combine(c.errors...) 67 | } 68 | 69 | // Compiles a Program. 70 | func (c *compiler) compileProgram(aprogram *parse.Program) *Program { 71 | var p Program 72 | for _, achange := range aprogram.Changes { 73 | if change := c.compileChange(achange); change != nil { 74 | p.Changes = append(p.Changes, change) 75 | } 76 | } 77 | return &p 78 | } 79 | -------------------------------------------------------------------------------- /internal/engine/matcher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | 28 | "github.com/uber-go/gopatch/internal/data" 29 | "github.com/uber-go/gopatch/internal/goast" 30 | "github.com/uber-go/gopatch/internal/pgo" 31 | ) 32 | 33 | // Region denotes the portion of the code being matched, i.e. the start and end 34 | // position of the given Node. 35 | type Region struct{ Pos, End token.Pos } 36 | 37 | // nodeRegion returns the Region occupied by a given node. 38 | func nodeRegion(n ast.Node) Region { 39 | return Region{Pos: n.Pos(), End: n.End()} 40 | } 41 | 42 | // Matcher matches values in a Go AST. It is built from the "-" portion of a 43 | // patch. 44 | type Matcher interface { 45 | // Match is called with the a value from the AST being compared with the 46 | // match data captured so far. 47 | // 48 | // Match reports whether the match succeeded and if so, returns the 49 | // original or a different Data object containing additional match data. 50 | Match(reflect.Value, data.Data, Region) (_ data.Data, ok bool) 51 | } 52 | 53 | // matcherCompiler compiles the "-" portion of a patch into a Matcher which 54 | // will report whether another Go AST matches it. 55 | type matcherCompiler struct { 56 | fset *token.FileSet 57 | meta *Meta 58 | 59 | // All dots found during match compilation. 60 | dots []token.Pos 61 | 62 | patchStart, patchEnd token.Pos 63 | } 64 | 65 | func newMatcherCompiler(fset *token.FileSet, meta *Meta, patchStart, patchEnd token.Pos) *matcherCompiler { 66 | return &matcherCompiler{ 67 | fset: fset, 68 | meta: meta, 69 | patchStart: patchStart, 70 | patchEnd: patchEnd, 71 | } 72 | } 73 | 74 | func (c *matcherCompiler) compile(v reflect.Value) Matcher { 75 | switch v.Type() { 76 | case goast.IdentPtrType: 77 | return c.compileIdent(v) 78 | case goast.StmtSliceType: 79 | return c.compileSliceDots(v, func(n ast.Node) bool { 80 | es, ok := n.(*ast.ExprStmt) 81 | if ok { 82 | _, ok = es.X.(*pgo.Dots) 83 | } 84 | return ok 85 | }) 86 | case goast.ExprSliceType: 87 | return c.compileSliceDots(v, func(n ast.Node) bool { 88 | _, ok := n.(*pgo.Dots) 89 | return ok 90 | }) 91 | case goast.FieldPtrSliceType: 92 | // TODO(abg): pgo.Parse should probably replace this with a DotsField. 93 | return c.compileSliceDots(v, func(n ast.Node) bool { 94 | f, ok := n.(*ast.Field) 95 | if ok { 96 | _, ok = f.Type.(*pgo.Dots) 97 | } 98 | return ok 99 | }) 100 | case goast.ForStmtPtrType: 101 | return c.compileForStmt(v) 102 | 103 | // TODO: Dedupe 104 | case goast.CommentGroupPtrType: 105 | // Comments shouldn't affect match. 106 | return successMatcher 107 | case goast.ObjectPtrType: 108 | // Ident.Obj forms a cycle. We'll consider Object pointers to always 109 | // match because the entites they point to will be matched separately 110 | // anyway. 111 | return successMatcher 112 | case goast.PosType: 113 | return c.compilePosMatcher(v) 114 | } 115 | 116 | return c.compileGeneric(v) 117 | } 118 | 119 | type matcherFunc func(reflect.Value) bool 120 | 121 | func (f matcherFunc) Match(v reflect.Value, d data.Data, _ Region) (data.Data, bool) { 122 | return d, f(v) 123 | } 124 | 125 | var ( 126 | // nilMatcher is a Matcher that only matches nil values. 127 | nilMatcher Matcher = matcherFunc(func(got reflect.Value) bool { return got.IsNil() }) 128 | 129 | // successMatcher always return true. 130 | successMatcher Matcher = matcherFunc(func(reflect.Value) bool { return true }) 131 | ) 132 | -------------------------------------------------------------------------------- /internal/engine/matcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "reflect" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/gopatch/internal/data" 29 | ) 30 | 31 | type matchCase struct { 32 | desc string // name of the test case 33 | give reflect.Value // value to match 34 | ok bool // expected result 35 | } 36 | 37 | // Runs the given matcher on the provided test cases in-order, threading the 38 | // data.Data between them. Returns the final data.Data and whether all cases 39 | // matched or not. 40 | // 41 | // If all cases did not match, the test has already been marked as 42 | // unsuccessful. 43 | func assertMatchCases(t *testing.T, m Matcher, d data.Data, tests []matchCase) (data.Data, bool) { 44 | valid := true 45 | for _, tt := range tests { 46 | t.Run(tt.desc, func(t *testing.T) { 47 | newd, ok := m.Match(tt.give, d, Region{}) 48 | if ok { 49 | // Carry data over in case of successful matches. 50 | d = newd 51 | } 52 | valid = assert.Equal(t, tt.ok, ok) && valid 53 | }) 54 | } 55 | return d, valid 56 | } 57 | -------------------------------------------------------------------------------- /internal/engine/meta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "go/token" 25 | 26 | "github.com/uber-go/gopatch/internal/parse" 27 | ) 28 | 29 | // MetavarType defines the different types of metavariables accepted in a 30 | // '@@' section. 31 | type MetavarType int 32 | 33 | // Supported metavariable types. 34 | const ( 35 | ExprMetavarType MetavarType = iota + 1 // expression 36 | IdentMetavarType // identifier 37 | ) 38 | 39 | // Meta is the compiled representaton of a Meta section. 40 | type Meta struct { 41 | // Variables defined in this Meta section and their types. 42 | Vars map[string]MetavarType 43 | } 44 | 45 | // LookupVar returns the type of the given metavariable or zero value if it 46 | // wasn't found. 47 | func (m *Meta) LookupVar(name string) MetavarType { 48 | if m == nil { 49 | return 0 50 | } 51 | return m.Vars[name] 52 | } 53 | 54 | func (c *compiler) compileMeta(m *parse.Meta) *Meta { 55 | vars := make(map[string]MetavarType) 56 | declPos := make(map[string]token.Pos) 57 | 58 | for _, decl := range m.Vars { 59 | var t MetavarType 60 | switch decl.Type.Name { 61 | case "identifier": 62 | t = IdentMetavarType 63 | case "expression": 64 | t = ExprMetavarType 65 | default: 66 | c.errf(decl.Type.Pos(), "unknown metavariable type %q", decl.Type.Name) 67 | continue 68 | } 69 | 70 | for _, name := range decl.Names { 71 | if name.Name == "_" { 72 | // Underscore isn't a variable declaration. 73 | continue 74 | } 75 | 76 | if pos, conflict := declPos[name.Name]; conflict { 77 | c.errf(name.Pos(), "cannot define metavariable %q: "+ 78 | "name already taken by metavariable defined at %v", name.Name, 79 | c.fset.Position(pos)) 80 | continue 81 | } 82 | vars[name.Name] = t 83 | declPos[name.Name] = name.Pos() 84 | } 85 | } 86 | 87 | return &Meta{Vars: vars} 88 | } 89 | -------------------------------------------------------------------------------- /internal/engine/pos.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "go/token" 25 | "reflect" 26 | 27 | "github.com/uber-go/gopatch/internal/data" 28 | ) 29 | 30 | // PosMatcher matches token.Pos fields in an AST. 31 | // 32 | // token.Pos fields are not matched by value, but by validity. A token.Pos 33 | // in the patch matches a token.Pos in the source file if they're both 34 | // valid (non-zero) or they're both invalid (zero). 35 | // 36 | // PosMatcher records the matched position from the source file so that it 37 | // can be reproduced by the PosReplacer later. 38 | type PosMatcher struct { 39 | Fset *token.FileSet 40 | 41 | // Pos in the patch file. 42 | Pos token.Pos 43 | } 44 | 45 | func (c *matcherCompiler) compilePosMatcher(v reflect.Value) Matcher { 46 | return PosMatcher{ 47 | Fset: c.fset, 48 | Pos: v.Interface().(token.Pos), 49 | } 50 | } 51 | 52 | // Match matches a position in a file. 53 | // 54 | // If the position matches and is valid, this records this match in Data for 55 | // later retrieval. 56 | func (m PosMatcher) Match(v reflect.Value, d data.Data, _ Region) (data.Data, bool) { 57 | got := v.Interface().(token.Pos) 58 | ok := m.Pos.IsValid() == got.IsValid() 59 | if ok { 60 | d = pushPosMatch(m.Fset, d, m.Pos, got) 61 | } 62 | return d, ok 63 | } 64 | 65 | // PosReplacer replaces token.Pos fields. 66 | // 67 | // For valid positions, the replacer will use the previously recorded 68 | // position for the field, if any. This ensures that whitespace between 69 | // tokens from the original file are preserved as much as possible. 70 | type PosReplacer struct { 71 | Fset *token.FileSet 72 | Pos token.Pos 73 | } 74 | 75 | func (c *replacerCompiler) compilePosReplacer(v reflect.Value) Replacer { 76 | return PosReplacer{ 77 | Fset: c.fset, 78 | Pos: v.Interface().(token.Pos), 79 | } 80 | } 81 | 82 | // TODO DATA POSITION NEEDS TO BE MUTABLE. SEND POSITIONS UP FOR GENERATED 83 | // CODE AS WE PROGRESS THROUGH MORE OF THE ORIGINAL POSITIONS 84 | 85 | // Replace replaces position nodes. 86 | func (r PosReplacer) Replace(d data.Data, cl Changelog, pos token.Pos) (reflect.Value, error) { 87 | if !r.Pos.IsValid() { 88 | return reflect.ValueOf(r.Pos), nil 89 | } 90 | 91 | // For positions, use the position associated with this item in the match 92 | // data, falling back to the most recent position recorded in Data. 93 | if matchedPos := lookupPosMatch(r.Fset, d, r.Pos); matchedPos.IsValid() { 94 | pos = matchedPos 95 | } 96 | 97 | return reflect.ValueOf(pos), nil 98 | } 99 | 100 | type posMatchKey struct { 101 | Line int 102 | Column int 103 | } 104 | 105 | // Records a position match. Both positions MUST be valid. 106 | func pushPosMatch(fset *token.FileSet, d data.Data, patchPos, matchedPos token.Pos) data.Data { 107 | // TODO(abg): This can be cheaper if the positions in the PGo AST point 108 | // back to the patch file rather than the intermediate files. 109 | 110 | p := fset.Position(patchPos) 111 | return data.WithValue(d, posMatchKey{ 112 | Line: p.Line, 113 | Column: p.Column, 114 | }, matchedPos) 115 | } 116 | 117 | // Retrieves the position associated with the given patch position or an 118 | // invalid position if none. 119 | func lookupPosMatch(fset *token.FileSet, d data.Data, patchPos token.Pos) (pos token.Pos) { 120 | p := fset.Position(patchPos) 121 | _ = data.Lookup(d, posMatchKey{ 122 | Line: p.Line, 123 | Column: p.Column, 124 | }, &pos) // TODO(abg): Handle !ok 125 | return 126 | } 127 | -------------------------------------------------------------------------------- /internal/engine/reflect_replace_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | "github.com/stretchr/testify/require" 31 | "github.com/uber-go/gopatch/internal/data" 32 | "github.com/uber-go/gopatch/internal/goast" 33 | ) 34 | 35 | func TestGenericReplacer(t *testing.T) { 36 | tests := []struct { 37 | desc string 38 | value reflect.Value 39 | }{ 40 | { 41 | desc: "string pointer", 42 | value: refl(stringPtr("foo")), 43 | }, 44 | { 45 | desc: "nil pointer", 46 | value: refl((*string)(nil)), 47 | }, 48 | { 49 | desc: "slice", 50 | value: refl([]int{1, 2, 3}), 51 | }, 52 | { 53 | desc: "nil slice", 54 | value: refl([]string(nil)), 55 | }, 56 | { 57 | desc: "empty struct", 58 | value: refl(struct{}{}), 59 | }, 60 | { 61 | desc: "non-empty struct", 62 | value: refl(struct{ Foo string }{Foo: "bar"}), 63 | }, 64 | { 65 | desc: "ast.Node interface", 66 | value: refl(&ast.BasicLit{Kind: token.INT, Value: "42"}).Convert(goast.NodeType), 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.desc, func(t *testing.T) { 72 | r := newReplacerCompiler(token.NewFileSet(), nil, 0, 0).compileGeneric(tt.value) 73 | 74 | t.Run("equality", func(t *testing.T) { 75 | got, err := r.Replace(data.New(), NewChangelog(), token.NoPos) 76 | require.NoError(t, err) 77 | assert.Equal(t, tt.value.Interface(), got.Interface()) 78 | }) 79 | 80 | // A Matcher constructed from this value matches the output of a 81 | // replacer built with it. 82 | t.Run("matches self", func(t *testing.T) { 83 | m := newMatcherCompiler(token.NewFileSet(), nil, 0, 0).compileGeneric(tt.value) 84 | 85 | got, err := r.Replace(data.New(), NewChangelog(), token.NoPos) 86 | require.NoError(t, err) 87 | 88 | _, ok := m.Match(got, data.New(), Region{}) 89 | assert.True(t, ok) 90 | }) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/engine/replacer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | 28 | "github.com/uber-go/gopatch/internal/data" 29 | "github.com/uber-go/gopatch/internal/goast" 30 | "github.com/uber-go/gopatch/internal/pgo" 31 | ) 32 | 33 | // Replacer generates portions of the Go AST meant to replace sections matched 34 | // by a Matcher. A Replacer is built from the "+" portion of a patch. 35 | type Replacer interface { 36 | // Replace generates values for a Go AST provided prior match data. 37 | Replace(d data.Data, cl Changelog, pos token.Pos) (v reflect.Value, err error) 38 | } 39 | 40 | // replacerCompiler compiles the "+" portion of a patch into a Replacer which 41 | // will generate the portions to fill in the original AST. 42 | type replacerCompiler struct { 43 | fset *token.FileSet 44 | meta *Meta 45 | dots []token.Pos 46 | dotAssoc map[token.Pos]token.Pos 47 | 48 | patchStart, patchEnd token.Pos 49 | } 50 | 51 | func newReplacerCompiler(fset *token.FileSet, meta *Meta, patchStart, patchEnd token.Pos) *replacerCompiler { 52 | return &replacerCompiler{ 53 | fset: fset, 54 | meta: meta, 55 | dotAssoc: make(map[token.Pos]token.Pos), 56 | patchStart: patchStart, 57 | patchEnd: patchEnd, 58 | } 59 | } 60 | 61 | func (c *replacerCompiler) compile(v reflect.Value) Replacer { 62 | if v.Kind() == reflect.Ptr && v.IsNil() { 63 | return ZeroReplacer{Type: v.Type()} 64 | } 65 | 66 | switch v.Type() { 67 | case goast.IdentPtrType: 68 | return c.compileIdent(v) 69 | case goast.StmtSliceType: 70 | return c.compileSliceDots(v, func(n ast.Node) bool { 71 | es, ok := n.(*ast.ExprStmt) 72 | if ok { 73 | _, ok = es.X.(*pgo.Dots) 74 | } 75 | return ok 76 | }) 77 | case goast.ExprSliceType: 78 | return c.compileSliceDots(v, func(n ast.Node) bool { 79 | _, ok := n.(*pgo.Dots) 80 | return ok 81 | }) 82 | case goast.FieldPtrSliceType: 83 | // TODO(abg): pgo.Parse should probably replace this with a DotsField. 84 | return c.compileSliceDots(v, func(n ast.Node) bool { 85 | f, ok := n.(*ast.Field) 86 | if ok { 87 | _, ok = f.Type.(*pgo.Dots) 88 | } 89 | return ok 90 | }) 91 | case goast.ForStmtPtrType: 92 | return c.compileForStmt(v) 93 | case goast.CommentGroupPtrType: 94 | // TODO: We're currently ignoring comments in the replacement patch. 95 | // We should probably record them and report them in the top-level 96 | // file. 97 | return ValueReplacer{ 98 | Value: reflect.ValueOf((*ast.CommentGroup)(nil)), 99 | } 100 | 101 | case goast.ObjectPtrType: 102 | // Ident.Obj forms a cycle so we'll replace it with a nil pointer. 103 | return ValueReplacer{ 104 | Value: reflect.ValueOf((*ast.Object)(nil)), 105 | } 106 | 107 | case goast.PosType: 108 | return c.compilePosReplacer(v) 109 | } 110 | 111 | return c.compileGeneric(v) 112 | } 113 | 114 | // ZeroReplacer replaces with a zero value. 115 | type ZeroReplacer struct{ Type reflect.Type } 116 | 117 | // Replace replaces with a zero value. 118 | func (r ZeroReplacer) Replace(data.Data, Changelog, token.Pos) (reflect.Value, error) { 119 | return reflect.Zero(r.Type), nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/engine/search.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "fmt" 25 | "go/ast" 26 | "go/token" 27 | "reflect" 28 | 29 | "github.com/uber-go/gopatch/internal/data" 30 | "golang.org/x/tools/go/ast/astutil" 31 | ) 32 | 33 | // SearchResult contains information about search results found by a 34 | // SearchMatcher. 35 | type SearchResult struct { 36 | // Object containing the matched node. 37 | parent ast.Node 38 | 39 | // Name of the field of the parent referring to the matched node. 40 | name string 41 | 42 | // Non-negative if parent.name is a slice. The match node is at this 43 | // index in parent.name. 44 | index int 45 | 46 | data data.Data 47 | region Region 48 | } 49 | 50 | // SearchNode provides access to an AST node, its parent, and its positional 51 | // information during a traversal. 52 | type SearchNode interface { 53 | Node() ast.Node 54 | Parent() ast.Node 55 | Name() string 56 | Index() int 57 | } 58 | 59 | // Searcher inspects the given Node using the given Matcher and returns a 60 | // non-nil SearchResult if it matched. 61 | type Searcher func(SearchNode, Matcher, data.Data) *SearchResult 62 | 63 | // SearchMatcher runs a Matcher on descendants of an AST, producing 64 | // SearchResults into Data. 65 | // 66 | // The corresponding replacer applies a Replacer to these matched descendants. 67 | type SearchMatcher struct { 68 | Search Searcher 69 | Matcher Matcher 70 | } 71 | 72 | // Match runs the matcher on the provided ast.Node. 73 | func (m SearchMatcher) Match(got reflect.Value, d data.Data, _ Region) (data.Data, bool) { 74 | n, ok := got.Interface().(ast.Node) 75 | if !ok { 76 | return d, false 77 | } 78 | 79 | var results []*SearchResult 80 | astutil.Apply(n, func(cursor *astutil.Cursor) bool { 81 | n := cursor.Node() 82 | if n == nil { 83 | return false 84 | } 85 | 86 | if r := m.Search(cursor, m.Matcher, d); r != nil { 87 | results = append(results, r) 88 | return false 89 | } 90 | 91 | return true // keep looking 92 | }, nil /* post func */) 93 | 94 | return pushSearchResults(d, got, results), len(results) > 0 95 | } 96 | 97 | // SearchReplacer replaces nodes found by a SearchMatcher. 98 | type SearchReplacer struct { 99 | Replacer Replacer 100 | } 101 | 102 | // Replace replaces nodes found by a SearchMatcher. 103 | func (r SearchReplacer) Replace(d data.Data, cl Changelog, pos token.Pos) (reflect.Value, error) { 104 | root, results := lookupSearchResults(d) 105 | if len(results) == 0 { 106 | return root, nil 107 | } 108 | 109 | for _, m := range results { 110 | v := reflect.Indirect(reflect.ValueOf(m.parent)).FieldByName(m.name) 111 | if !v.IsValid() { 112 | // This is a bug in our code. 113 | panic(fmt.Sprintf("%q is not a field of %T", m.name, m.parent)) 114 | } 115 | 116 | if m.index >= 0 { 117 | v = v.Index(m.index) 118 | } 119 | 120 | give, err := r.Replacer.Replace(m.data, cl, m.region.Pos) 121 | if err != nil { 122 | return reflect.Value{}, err 123 | } 124 | 125 | // If the generated value isn't assignable to the target, the match 126 | // was too eager. For example, trying to place "foo.Bar" 127 | // (SelectorExpr) where only an identifier is allowed (in a variable 128 | // declaration name, for example). 129 | if give.Type().AssignableTo(v.Type()) { 130 | v.Set(give) 131 | } 132 | } 133 | 134 | return root, nil 135 | } 136 | 137 | type _searchResultKey struct{} 138 | 139 | var searchResultKey _searchResultKey 140 | 141 | type searchResultData struct { 142 | Root reflect.Value 143 | Results []*SearchResult 144 | } 145 | 146 | func pushSearchResults(d data.Data, root reflect.Value, results []*SearchResult) data.Data { 147 | return data.WithValue(d, searchResultKey, searchResultData{ 148 | Root: root, 149 | Results: results, 150 | }) 151 | } 152 | 153 | func lookupSearchResults(d data.Data) (root reflect.Value, results []*SearchResult) { 154 | var sr searchResultData 155 | _ = data.Lookup(d, searchResultKey, &sr) // TODO(abg): Handle !ok 156 | return sr.Root, sr.Results 157 | } 158 | -------------------------------------------------------------------------------- /internal/engine/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "go/ast" 27 | "go/printer" 28 | "go/token" 29 | "reflect" 30 | ) 31 | 32 | // Sprint is used primarily for debugging and prints a readable representation 33 | // of the provided value. 34 | func Sprint(x any) string { 35 | switch v := x.(type) { 36 | case Matcher: 37 | return fmt.Sprintf("%T", v) 38 | case reflect.Value: 39 | return Sprint(v.Interface()) 40 | case []reflect.Value: 41 | items := make([]string, len(v)) 42 | for i, item := range v { 43 | items[i] = Sprint(item) 44 | } 45 | return fmt.Sprint(items) 46 | case *ast.Field: 47 | return Sprint(&ast.StructType{Fields: &ast.FieldList{List: []*ast.Field{v}}}) 48 | case ast.Node: 49 | var out bytes.Buffer 50 | printer.Fprint(&out, token.NewFileSet(), v) 51 | return out.String() 52 | case []ast.Stmt: 53 | return Sprint(&ast.BlockStmt{List: v}) 54 | case []ast.Expr: 55 | return Sprint(&ast.CompositeLit{Elts: v}) 56 | default: 57 | return fmt.Sprint(x) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/engine/utils_for_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package engine 22 | 23 | import "reflect" 24 | 25 | func refl(v any) reflect.Value { 26 | return reflect.ValueOf(v) 27 | } 28 | 29 | func stringPtr(s string) *string { return &s } 30 | -------------------------------------------------------------------------------- /internal/goast/imports.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package goast 22 | 23 | import ( 24 | "fmt" 25 | "go/ast" 26 | "strconv" 27 | ) 28 | 29 | // ImportPath returns the import path of the provided Go import. 30 | func ImportPath(spec *ast.ImportSpec) string { 31 | if spec == nil || spec.Path == nil { 32 | panic("ImportSpec and its Path must be non-nil") 33 | } 34 | 35 | path, err := strconv.Unquote(spec.Path.Value) 36 | if err != nil { 37 | panic(fmt.Sprintf("invalid import path %q: %v", spec.Path.Value, err)) 38 | } 39 | return path 40 | } 41 | 42 | // ImportName returns the name of the provided import or an empty string if 43 | // it's not a named import. 44 | func ImportName(spec *ast.ImportSpec) string { 45 | if spec == nil { 46 | panic("ImportSpec must be non-nil") 47 | } 48 | 49 | var name string 50 | if spec.Name != nil { 51 | name = spec.Name.Name 52 | } 53 | return name 54 | } 55 | 56 | // FindImportSpec finds and returns the ImportSpec for an import with the 57 | // given import path, returning nil if a matching ImportSpec was not found. 58 | func FindImportSpec(f *ast.File, path string) *ast.ImportSpec { 59 | if f == nil { 60 | panic("File must be non-nil") 61 | } 62 | 63 | for _, got := range f.Imports { 64 | if ImportPath(got) == path { 65 | return got 66 | } 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/goast/imports_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package goast 22 | 23 | import ( 24 | "go/ast" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestImportPath(t *testing.T) { 31 | t.Run("nil", func(t *testing.T) { 32 | assert.Panics(t, func() { 33 | ImportPath(nil) 34 | }) 35 | }) 36 | 37 | t.Run("nil path", func(t *testing.T) { 38 | assert.Panics(t, func() { 39 | ImportPath(&ast.ImportSpec{}) 40 | }) 41 | }) 42 | 43 | t.Run("undecodable import path", func(t *testing.T) { 44 | assert.Panics(t, func() { 45 | ImportPath(&ast.ImportSpec{ 46 | Path: &ast.BasicLit{Value: "foo"}, 47 | }) 48 | }) 49 | }) 50 | 51 | t.Run("success", func(t *testing.T) { 52 | got := ImportPath(&ast.ImportSpec{Path: &ast.BasicLit{Value: `"foo"`}}) 53 | assert.Equal(t, "foo", got) 54 | }) 55 | } 56 | 57 | func TestImportName(t *testing.T) { 58 | t.Run("nil", func(t *testing.T) { 59 | assert.Panics(t, func() { 60 | ImportName(nil) 61 | }) 62 | }) 63 | 64 | t.Run("unnamed import", func(t *testing.T) { 65 | got := ImportName(&ast.ImportSpec{ 66 | Path: &ast.BasicLit{Value: `"foo"`}, 67 | }) 68 | assert.Empty(t, got) 69 | }) 70 | 71 | t.Run("named import", func(t *testing.T) { 72 | got := ImportName(&ast.ImportSpec{ 73 | Name: ast.NewIdent("bar"), 74 | Path: &ast.BasicLit{Value: `"foo"`}, 75 | }) 76 | assert.Equal(t, "bar", got) 77 | }) 78 | } 79 | 80 | func TestFindImportSpec(t *testing.T) { 81 | t.Run("nil", func(t *testing.T) { 82 | assert.Panics(t, func() { 83 | FindImportSpec(nil, "foo") 84 | }) 85 | }) 86 | 87 | foo := &ast.ImportSpec{Path: &ast.BasicLit{Value: `"foo"`}} 88 | fooBar := &ast.ImportSpec{Path: &ast.BasicLit{Value: `"foo/bar"`}} 89 | file := &ast.File{ 90 | Imports: []*ast.ImportSpec{foo, fooBar}, 91 | } 92 | 93 | t.Run("no match", func(t *testing.T) { 94 | assert.Nil(t, FindImportSpec(file, "foo/bar/baz")) 95 | }) 96 | 97 | t.Run("match foo", func(t *testing.T) { 98 | assert.Equal(t, foo, FindImportSpec(file, "foo")) 99 | }) 100 | 101 | // not using / in test name because that's how subtests are separated. 102 | t.Run("match foo-bar", func(t *testing.T) { 103 | assert.Equal(t, fooBar, FindImportSpec(file, "foo/bar")) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /internal/goast/pos.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package goast 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | ) 28 | 29 | // TransformPos runs the given function on all token.Pos values found in the 30 | // given object and its descendants, replacing them in-place with the value 31 | // produced by the provided function. 32 | // 33 | // Caveat: Free-floating comments on File objects are not handled by 34 | // TransformPos. 35 | func TransformPos(n any, transform func(token.Pos) token.Pos) { 36 | transformPos(reflect.ValueOf(n), transform) 37 | } 38 | 39 | // OffsetPos offsets all token.Pos values found in the given object and its 40 | // descendants in-place. 41 | // 42 | // Caveat: Free-floating comments on File objects are not handled by 43 | // OffsetPos. 44 | func OffsetPos(n any, offset int) { 45 | TransformPos(n, func(pos token.Pos) token.Pos { return pos + token.Pos(offset) }) 46 | } 47 | 48 | var ( 49 | posType = reflect.TypeOf(token.Pos(0)) 50 | objectPtrType = reflect.TypeOf((*ast.Object)(nil)) 51 | fileType = reflect.TypeOf(ast.File{}) 52 | ) 53 | 54 | func transformPos(v reflect.Value, transformFn func(token.Pos) token.Pos) { 55 | if !v.IsValid() { 56 | return 57 | } 58 | 59 | switch v.Type() { 60 | case fileType: 61 | // ast.File maintains a bunch of internal references. Only the 62 | // following fields are unique references. 63 | transformPos(v.FieldByName("Doc"), transformFn) 64 | transformPos(v.FieldByName("Package"), transformFn) 65 | transformPos(v.FieldByName("Name"), transformFn) 66 | transformPos(v.FieldByName("Decls"), transformFn) 67 | transformPos(v.FieldByName("FileStart"), transformFn) 68 | transformPos(v.FieldByName("FileEnd"), transformFn) 69 | 70 | // NOTE: File.Comments contains both, comments that document 71 | // objects (also referenced by those nodes' Doc fields), and 72 | // free-floating comments in the file. Rather than tracking 73 | // whether a comment has already been processed, we're just 74 | // not going to handle free-floating comments until it becomes 75 | // necessary. 76 | case objectPtrType: 77 | // ast.Object has a reference to the target object, causing 78 | // cyclic references. Since the underlying object isn't 79 | // changing, we don't need to do anything here. 80 | case posType: 81 | pos := token.Pos(v.Int()) 82 | if pos.IsValid() { 83 | // We want to change Pos only if it's valid, as in 84 | // non-zero. There are parts in the Go AST where the 85 | // presence of a valid Pos changes the generated 86 | // syntax significantly. One example is type aliases. 87 | // 88 | // type Foo = Bar 89 | // type Foo Bar 90 | // 91 | // The only difference between the parsed 92 | // representations for the two type declarations above 93 | // is whether the Equals field has a valid token.Pos 94 | // or not. If the Pos is invalid, we don't want to 95 | // change it. 96 | v.SetInt(int64(transformFn(pos))) 97 | } 98 | default: 99 | switch v.Kind() { 100 | case reflect.Array, reflect.Slice: 101 | for i := 0; i < v.Len(); i++ { 102 | transformPos(v.Index(i), transformFn) 103 | } 104 | case reflect.Interface, reflect.Ptr: 105 | transformPos(v.Elem(), transformFn) 106 | case reflect.Struct: 107 | for i := 0; i < v.NumField(); i++ { 108 | transformPos(v.Field(i), transformFn) 109 | } 110 | case reflect.Map: 111 | // go/ast does not use maps in the AST besides Scope objects 112 | // which are attached to File objects, which we handle 113 | // explicitly. 114 | panic("cannot use maps inside an AST node") 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/goast/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package goast 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | "reflect" 27 | ) 28 | 29 | // Reflected types of various AST nodes. 30 | var ( 31 | // Primitives 32 | PosType = reflect.TypeOf(token.Pos(0)) 33 | StringType = reflect.TypeOf("") 34 | 35 | // Structs 36 | BlockStmtType = reflect.TypeOf(ast.BlockStmt{}) 37 | CaseClauseType = reflect.TypeOf(ast.CaseClause{}) 38 | CommClauseType = reflect.TypeOf(ast.CommClause{}) 39 | CommentGroupType = reflect.TypeOf(ast.CommentGroup{}) 40 | FieldListType = reflect.TypeOf(ast.FieldList{}) 41 | FieldType = reflect.TypeOf(ast.Field{}) 42 | FileType = reflect.TypeOf(ast.File{}) 43 | ForStmtType = reflect.TypeOf(ast.ForStmt{}) 44 | FuncDeclType = reflect.TypeOf(ast.FuncDecl{}) 45 | GenDeclType = reflect.TypeOf(ast.GenDecl{}) 46 | IdentType = reflect.TypeOf(ast.Ident{}) 47 | ObjectType = reflect.TypeOf(ast.Object{}) 48 | RangeStmtType = reflect.TypeOf(ast.RangeStmt{}) 49 | ScopeType = reflect.TypeOf(ast.Scope{}) 50 | 51 | // Struct Pointers 52 | CommentGroupPtrType = reflect.PtrTo(CommentGroupType) 53 | FieldListPtrType = reflect.PtrTo(FieldListType) 54 | FieldPtrType = reflect.PtrTo(FieldType) 55 | FilePtrType = reflect.PtrTo(FileType) 56 | ForStmtPtrType = reflect.PtrTo(ForStmtType) 57 | FuncDeclPtrType = reflect.PtrTo(FuncDeclType) 58 | GenDeclPtrType = reflect.PtrTo(GenDeclType) 59 | IdentPtrType = reflect.PtrTo(IdentType) 60 | ObjectPtrType = reflect.PtrTo(ObjectType) 61 | RangeStmtPtrType = reflect.PtrTo(RangeStmtType) 62 | ScopePtrType = reflect.PtrTo(ScopeType) 63 | 64 | // Interfaces 65 | ExprType = reflect.TypeOf((*ast.Expr)(nil)).Elem() 66 | NodeType = reflect.TypeOf((*ast.Node)(nil)).Elem() 67 | StmtType = reflect.TypeOf((*ast.Stmt)(nil)).Elem() 68 | 69 | // Slices 70 | ExprSliceType = reflect.SliceOf(ExprType) 71 | FieldPtrSliceType = reflect.SliceOf(FieldPtrType) 72 | StmtSliceType = reflect.SliceOf(StmtType) 73 | ) 74 | -------------------------------------------------------------------------------- /internal/parse/ast.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package parse 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | 27 | "github.com/uber-go/gopatch/internal/pgo" 28 | ) 29 | 30 | // Program is a single gopatch program consisting of one or more changes. 31 | type Program struct { 32 | Changes []*Change 33 | } 34 | 35 | // Change is a single change in a patch. Changes are specified in the format, 36 | // 37 | // @@ 38 | // # metavariables go here 39 | // @@ 40 | // # patch goes here 41 | // 42 | // Optionally, a name may be specified for a change between the first two 43 | // "@@"s. 44 | // 45 | // @ mychange @ 46 | // # metavariables go here 47 | // @@ 48 | // # patch goes here 49 | type Change struct { 50 | // Name for the change, if any. 51 | // 52 | // Names must be valid Go identifiers. 53 | Name string 54 | 55 | // Metavariables defined for this change. 56 | Meta *Meta 57 | 58 | // Patch for this change. 59 | Patch *Patch 60 | 61 | // Comments for this change 62 | Comments []string 63 | } 64 | 65 | // Meta represents the metavariables section of a change. 66 | // 67 | // This consists of one or more declarations used in the patch. 68 | type Meta struct { 69 | // Variables declared in this section. 70 | Vars []*VarDecl 71 | } 72 | 73 | // VarDecl is a single var declaration in a metavariable block. 74 | // 75 | // var foo, bar identifier 76 | // var baz, qux expression 77 | type VarDecl struct { 78 | // Position at which the "var" keyword appears. 79 | VarPos token.Pos 80 | 81 | // We're re-using the "go/ast".Ident type to represent identifiers in the 82 | // code so that we can track positional data for when identifiers appear 83 | // in the patch file. 84 | 85 | // Names of the variables declared in this statement. 86 | Names []*ast.Ident 87 | 88 | // Type of the variables. 89 | Type *ast.Ident 90 | } 91 | 92 | var _ ast.Node = (*VarDecl)(nil) 93 | 94 | // Pos returns the position at which this declaration starts. 95 | func (d *VarDecl) Pos() token.Pos { return d.VarPos } 96 | 97 | // End returns the position of the next character after this declaration. 98 | func (d *VarDecl) End() token.Pos { 99 | if d.Type != nil { 100 | return d.Type.End() 101 | } 102 | return token.NoPos 103 | } 104 | 105 | // Patch is the patch portion of the change containing the unified diff of the 106 | // match/transformation. 107 | type Patch struct { 108 | // Positions at which the entire patch begins and ends. 109 | StartPos, EndPos token.Pos 110 | 111 | // The before and after versions of the Patch broken apart from the 112 | // unified diff. 113 | Minus, Plus *pgo.File 114 | } 115 | 116 | var _ ast.Node = (*Patch)(nil) 117 | 118 | // Pos returns the position at which this patch begins. 119 | func (p *Patch) Pos() token.Pos { return p.StartPos } 120 | 121 | // End returns the position immediately after this patch. 122 | func (p *Patch) End() token.Pos { return p.EndPos } 123 | -------------------------------------------------------------------------------- /internal/parse/change.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package parse 22 | 23 | import "github.com/uber-go/gopatch/internal/parse/section" 24 | 25 | // Parses the change at index i. 26 | func (p *parser) parseChange(i int, c *section.Change) (_ *Change, err error) { 27 | change := Change{Name: c.Name} 28 | 29 | change.Meta, err = p.parseMeta(i, c) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | change.Patch, err = p.parsePatch(i, c) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | change.Comments = c.Comments 40 | return &change, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/parse/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package parse provides the ability to parse a .patch file. 22 | package parse 23 | -------------------------------------------------------------------------------- /internal/parse/meta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package parse 22 | 23 | import ( 24 | "fmt" 25 | "go/ast" 26 | "go/scanner" 27 | "go/token" 28 | 29 | "github.com/uber-go/gopatch/internal/parse/section" 30 | "go.uber.org/multierr" 31 | ) 32 | 33 | // Parses the metavariables section of the change at index i. 34 | func (p *parser) parseMeta(i int, c *section.Change) (*Meta, error) { 35 | metaContents, metaLines := section.ToBytes(c.Meta) 36 | 37 | // We will create a new File with the contents of the metavariables 38 | // section and map positions in it back to the original file for error 39 | // messages. 40 | 41 | // Generate a fake name for the File. 42 | filename := p.fset.File(c.Pos()).Name() 43 | if len(c.Name) > 0 { 44 | filename += c.Name + ".meta" 45 | } else { 46 | filename += fmt.Sprintf("%d.meta", i) 47 | } 48 | 49 | file := p.fset.AddFile(filename, -1, len(metaContents)) 50 | for _, line := range metaLines { 51 | p := p.fset.Position(line.Pos) 52 | file.AddLineColumnInfo(line.Offset, p.Filename, p.Line, p.Column) 53 | } 54 | 55 | parser := metaParser{fset: p.fset} 56 | var scanner scanner.Scanner 57 | scanner.Init(file, metaContents, parser.onError, 0 /* mode */) 58 | parser.scanner = &scanner 59 | parser.next() // read the first token 60 | 61 | return parser.parse(), multierr.Combine(parser.errors...) 62 | } 63 | 64 | type metaParser struct { 65 | scanner *scanner.Scanner 66 | 67 | fset *token.FileSet 68 | pos token.Pos // current token position 69 | tok token.Token // current token 70 | text string // current token contents 71 | 72 | failed bool 73 | errors []error 74 | } 75 | 76 | // This function is called by go/scanner when errors are encountered. We 77 | // connect it in the Init call above. 78 | func (p *metaParser) onError(pos token.Position, msg string) { 79 | p.failed = true 80 | p.errors = append(p.errors, fmt.Errorf("%v: %v", pos, msg)) 81 | } 82 | 83 | // Posts a formatted error message to the parser. 84 | func (p *metaParser) errf(msg string, args ...any) { 85 | if len(args) > 0 { 86 | msg = fmt.Sprintf(msg, args...) 87 | } 88 | p.onError(p.fset.Position(p.pos), msg) 89 | } 90 | 91 | // Advances to the next token. 92 | func (p *metaParser) next() { 93 | p.pos, p.tok, p.text = p.scanner.Scan() 94 | } 95 | 96 | // Parses the metavariables section. 97 | func (p *metaParser) parse() *Meta { 98 | var m Meta 99 | for !p.failed && p.tok != token.EOF { 100 | m.Vars = append(m.Vars, p.parseDecl()) 101 | } 102 | return &m 103 | } 104 | 105 | // Parses and returns a VarDecl. 106 | // 107 | // var x, y, z Foo 108 | func (p *metaParser) parseDecl() *VarDecl { 109 | defer p.next() 110 | 111 | if p.tok != token.VAR { 112 | p.errf(`unexpected %q, expected "var"`, p.tok) 113 | return nil 114 | } 115 | 116 | d := VarDecl{VarPos: p.pos} 117 | for { 118 | p.next() // skip var/, 119 | name := p.parseIdent() 120 | if name == nil { 121 | return nil 122 | } 123 | d.Names = append(d.Names, name) 124 | 125 | if p.tok != token.COMMA { 126 | break 127 | } 128 | } 129 | 130 | // A type name is expected after list of variables. 131 | d.Type = p.parseIdent() 132 | if d.Type == nil { 133 | return nil 134 | } 135 | 136 | // go/scanner implicitly inserts SEMICOLON when a newline is found where a 137 | // semicolon would be accepted. So we expect a semicolon after every var 138 | // declaration. 139 | if p.tok != token.SEMICOLON { 140 | p.errf(`unexpected %q, expected ";" or a newline`, p.tok) 141 | return nil 142 | } 143 | 144 | return &d 145 | } 146 | 147 | // Reads and returns an identifier, advancing the parser to the next token. 148 | // Fails the parser and returns nil if an identifier was not found. 149 | func (p *metaParser) parseIdent() *ast.Ident { 150 | defer p.next() 151 | 152 | if p.tok != token.IDENT { 153 | p.errf("unexpected %q, expected an identifier", p.tok) 154 | return nil 155 | } 156 | 157 | return &ast.Ident{Name: p.text, NamePos: p.pos} 158 | } 159 | -------------------------------------------------------------------------------- /internal/parse/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package parse 22 | 23 | import ( 24 | "fmt" 25 | "go/token" 26 | 27 | "github.com/uber-go/gopatch/internal/parse/section" 28 | ) 29 | 30 | // Parse parses a Program. 31 | func Parse(fset *token.FileSet, filename string, contents []byte) (*Program, error) { 32 | return newParser(fset).parseProgram(filename, contents) 33 | } 34 | 35 | type parser struct { 36 | fset *token.FileSet 37 | } 38 | 39 | func newParser(fset *token.FileSet) *parser { 40 | return &parser{fset: fset} 41 | } 42 | 43 | func (p *parser) errf(pos token.Pos, msg string, args ...any) error { 44 | if len(args) > 0 { 45 | msg = fmt.Sprintf(msg, args...) 46 | } 47 | return fmt.Errorf("%v: %v", p.fset.Position(pos), msg) 48 | } 49 | 50 | func (p *parser) parseProgram(filename string, contents []byte) (*Program, error) { 51 | changes, err := section.Split(p.fset, filename, contents) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | prog := Program{Changes: make([]*Change, len(changes))} 57 | for i, c := range changes { 58 | change, err := p.parseChange(i, c) 59 | if err != nil { 60 | return nil, err 61 | } 62 | prog.Changes[i] = change 63 | } 64 | 65 | return &prog, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/parse/section/bytes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package section 22 | 23 | import ( 24 | "bytes" 25 | "go/token" 26 | "sort" 27 | ) 28 | 29 | // LinePos contains positional information about a line in a buffer. 30 | type LinePos struct { 31 | // Offset of the first character of this line. 32 | Offset int 33 | 34 | // Original position from which this line was extracted. 35 | Pos token.Pos 36 | } 37 | 38 | // ToBytes converts a Section to its raw byte contents. A sorted list mapping 39 | // offsets in the returned byte slice to original token.Pos values is included 40 | // in the response. 41 | func ToBytes(s Section) (src []byte, lines []LinePos) { 42 | var buff bytes.Buffer 43 | for _, line := range s { 44 | lines = append(lines, LinePos{ 45 | Offset: buff.Len(), 46 | Pos: line.Pos(), 47 | }) 48 | buff.Write(line.Text) 49 | buff.WriteByte('\n') 50 | } 51 | sort.Sort(byOffset(lines)) 52 | return buff.Bytes(), lines 53 | } 54 | 55 | type byOffset []LinePos 56 | 57 | func (ls byOffset) Len() int { return len(ls) } 58 | 59 | func (ls byOffset) Less(i int, j int) bool { 60 | return ls[i].Offset < ls[j].Offset 61 | } 62 | 63 | func (ls byOffset) Swap(i int, j int) { 64 | ls[i], ls[j] = ls[j], ls[i] 65 | } 66 | -------------------------------------------------------------------------------- /internal/parse/section/bytes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package section 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/uber-go/gopatch/internal/text" 28 | ) 29 | 30 | func TestToBytes(t *testing.T) { 31 | tests := []struct { 32 | desc string 33 | give Section 34 | wantSrc []byte 35 | wantLines []LinePos 36 | }{ 37 | { 38 | desc: "simple", 39 | give: Section{ 40 | {StartPos: 1, Text: []byte("foo")}, 41 | {StartPos: 5, Text: []byte("bar")}, 42 | }, 43 | wantSrc: text.Unlines( 44 | "foo", 45 | "bar", 46 | ), 47 | wantLines: []LinePos{ 48 | {Offset: 0, Pos: 1}, 49 | {Offset: 4, Pos: 5}, 50 | }, 51 | }, 52 | { 53 | desc: "missing portions", 54 | give: Section{ 55 | {StartPos: 1, Text: []byte("foo")}, 56 | {StartPos: 20, Text: []byte("bar")}, 57 | {StartPos: 30, Text: []byte("baz")}, 58 | }, 59 | wantSrc: text.Unlines( 60 | "foo", 61 | "bar", 62 | "baz", 63 | ), 64 | wantLines: []LinePos{ 65 | {Offset: 0, Pos: 1}, 66 | {Offset: 4, Pos: 20}, 67 | {Offset: 8, Pos: 30}, 68 | }, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.desc, func(t *testing.T) { 74 | gotSrc, gotLines := ToBytes(tt.give) 75 | assert.Equal(t, tt.wantSrc, gotSrc) 76 | assert.Equal(t, tt.wantLines, gotLines) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/pgo/ast.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package pgo 22 | 23 | import ( 24 | "go/ast" 25 | "go/token" 26 | ) 27 | 28 | // File is a single pgo file. This is analogous to go/ast's File. 29 | type File struct { 30 | // Package name specified in the file, if any. 31 | Package string 32 | 33 | // Imports made in the source. 34 | Imports []*ast.ImportSpec 35 | 36 | // Comments declared in the file. 37 | Comments []*ast.CommentGroup 38 | 39 | // Top-level node declared in the file. 40 | // 41 | // pgo allows only one node at the top-level. It must be either a standard 42 | // Go top-level declaration, an expression, or a list of statements. 43 | Node Node 44 | } 45 | 46 | // Node unifies the custom node types we introduce to the Go AST. 47 | type Node interface { 48 | ast.Node 49 | 50 | pgoNode() 51 | } 52 | 53 | var ( 54 | _ Node = (*Dots)(nil) 55 | _ Node = (*Expr)(nil) 56 | _ Node = (*FuncDecl)(nil) 57 | _ Node = (*GenDecl)(nil) 58 | _ Node = (*StmtList)(nil) 59 | ) 60 | 61 | // Expr is a Go expression at the top-level in pgo. 62 | type Expr struct{ ast.Expr } 63 | 64 | func (*Expr) pgoNode() {} 65 | 66 | // StmtList is a list of statements at the top-level of a pgo file. 67 | type StmtList struct { 68 | List []ast.Stmt // inv: len > 0 69 | } 70 | 71 | func (*StmtList) pgoNode() {} 72 | 73 | // Pos returns the start position of the statement list or NoPos if there are 74 | // no statements in it. 75 | func (l *StmtList) Pos() token.Pos { 76 | return l.List[0].Pos() 77 | } 78 | 79 | // End returns the position of the character immediately after this statement 80 | // list, or NoPos if there are no statements in this list. 81 | func (l *StmtList) End() token.Pos { 82 | return l.List[len(l.List)-1].End() 83 | } 84 | 85 | // FuncDecl is a Go function declaration at the top-level in pgo. 86 | type FuncDecl struct{ *ast.FuncDecl } 87 | 88 | func (*FuncDecl) pgoNode() {} 89 | 90 | // GenDecl is a Go general declaration at the top-level in pgo. 91 | type GenDecl struct{ *ast.GenDecl } 92 | 93 | func (*GenDecl) pgoNode() {} 94 | 95 | // Dots is a "..." used as an expression. 96 | // 97 | // If used as a statement, Dots will be inside an ExprStmt. 98 | type Dots struct { 99 | ast.Expr 100 | 101 | Dots token.Pos // position of dots 102 | } 103 | 104 | func (*Dots) pgoNode() {} 105 | 106 | // Pos returns the start position of "...". 107 | func (d *Dots) Pos() token.Pos { return d.Dots } 108 | 109 | // End returns the position after "...". 110 | func (d *Dots) End() token.Pos { return d.Dots + 3 } 111 | -------------------------------------------------------------------------------- /internal/pgo/augment/augment.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package augment facilitates adding syntax to the Go AST. 22 | // 23 | // It works by replacing invalid syntax with valid code, recording these 24 | // positions as it does so, allowing us to later transform the parsed AST for 25 | // affected nodes. 26 | package augment 27 | 28 | // Augmentation is an addition to the Go syntax for pgo. 29 | type Augmentation interface { 30 | augmentation() 31 | 32 | // Start returns the offset at which the augmentation was found. 33 | Start() int 34 | 35 | // End returns the offset of the first character immediately after this 36 | // augmentation. 37 | // 38 | // Given an augmentation a in text, text[a.Start():a.End()] fully 39 | // encapsulates the text of the augmentation. 40 | End() int 41 | } 42 | 43 | // Dots is a "..." operator appearing in the Go source outside of places 44 | // allowed by the Go syntax. 45 | type Dots struct { 46 | DotsStart, DotsEnd int 47 | 48 | // Named indicates whether the dots replace a named entity — such 49 | // as the named arguments or results of a function. 50 | Named bool 51 | } 52 | 53 | func (*Dots) augmentation() {} 54 | 55 | // Start offset of Dots. 56 | func (d *Dots) Start() int { return d.DotsStart } 57 | 58 | // End offset of Dots. 59 | func (d *Dots) End() int { return d.DotsEnd } 60 | 61 | // FakePackage is a fake package clause included in the code. This is needed 62 | // if the source didn't contain a package clause. 63 | type FakePackage struct { 64 | PackageStart int // position of "package" keyword 65 | } 66 | 67 | func (*FakePackage) augmentation() {} 68 | 69 | // Start offset for FakePackage. 70 | func (p *FakePackage) Start() int { return p.PackageStart } 71 | 72 | // End offset for FakePackage. This is meaningless for FakePackage. 73 | func (p *FakePackage) End() int { return p.PackageStart } 74 | 75 | // FakeFunc is a fake function block generated in the code. This is needed if 76 | // the source didn't open with a valid top-level declaration. 77 | type FakeFunc struct { 78 | FuncStart int // position of "func" keyword 79 | Braces bool // whether a { ... } was added 80 | } 81 | 82 | func (*FakeFunc) augmentation() {} 83 | 84 | // Start offset for FakeFunc. 85 | func (f *FakeFunc) Start() int { return f.FuncStart } 86 | 87 | // End offset for FakeFunc. This is meaningless for FakeFunc. 88 | func (f *FakeFunc) End() int { return f.FuncStart } 89 | 90 | // Augment takes the provided pgo syntax and returns valid Go syntax, a list 91 | // of augmentations made to it, and position adjustments required in the 92 | // parsed AST. 93 | func Augment(src []byte) ([]byte, []Augmentation, []PosAdjustment, error) { 94 | augs, err := find(src) 95 | if err != nil { 96 | return nil, nil, nil, err 97 | } 98 | src, adjs := rewrite(src, augs) 99 | return src, augs, adjs, nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/pgo/augment/rewrite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package augment 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "sort" 27 | ) 28 | 29 | const ( 30 | _fakePackage = "package _" 31 | _fakeFunc = "func _() " 32 | ) 33 | 34 | // PosAdjustment specifies how much token.Pos's have to be adjusted after 35 | // different offsets. This is required because to make the pgo source valid Go 36 | // code, we may have to geneate additional code around it. This will mess up 37 | // positioning information so we track how much we're affecting the positions 38 | // and starting at what offsets. 39 | type PosAdjustment struct { 40 | Offset int 41 | ReduceBy int 42 | } 43 | 44 | // rewrites the source to be valid Go syntax. Offsets in the augmentations are 45 | // updated to reflect offsets in new []byte. 46 | func rewrite(src []byte, augs []Augmentation) ([]byte, []PosAdjustment) { 47 | var ( 48 | pos int 49 | dst bytes.Buffer 50 | tail bytes.Buffer 51 | adjustments []PosAdjustment 52 | reduceBy int 53 | ) 54 | // augs will always be in the format, 55 | // 56 | // FakePackage?, FakeFunc?, Other* 57 | // 58 | // That is, FakePackage and/or FakeFunc MAY be present at the front of the 59 | // list in that order. This tells us to generate the fake package and func 60 | // clauses. Other augmentations MAY be present after that. 61 | // 62 | // Sort all augs by Start offset to retain the above ordering while ensuring 63 | // that augmentations get written to the `dst` Buffer in order. 64 | sort.Slice(augs, func(i, j int) bool { return augs[i].Start() < augs[j].Start() }) 65 | for _, aug := range augs { 66 | start, end := aug.Start(), aug.End() 67 | dst.Write(src[pos:start]) 68 | 69 | switch a := aug.(type) { 70 | case *FakePackage: 71 | a.PackageStart = dst.Len() 72 | fmt.Fprintln(&dst, _fakePackage) 73 | reduceBy += dst.Len() - a.PackageStart 74 | adjustments = append(adjustments, PosAdjustment{ 75 | Offset: a.PackageStart, 76 | ReduceBy: reduceBy, 77 | }) 78 | case *FakeFunc: 79 | a.FuncStart = dst.Len() 80 | dst.WriteString(_fakeFunc) 81 | if a.Braces { 82 | dst.WriteString("{\n") 83 | tail.WriteString("}\n") 84 | } 85 | reduceBy += dst.Len() - a.FuncStart 86 | adjustments = append(adjustments, PosAdjustment{ 87 | Offset: a.FuncStart, 88 | ReduceBy: reduceBy, 89 | }) 90 | case *Dots: 91 | a.DotsStart = dst.Len() 92 | if a.Named { 93 | // no PosAdjustment: len(_ d) == len(...) 94 | dst.WriteString("_ d") 95 | } else { 96 | // no PosAdjustment: len(dts) == len(...) 97 | dst.WriteString("dts") 98 | } 99 | a.DotsEnd = dst.Len() 100 | default: 101 | panic(fmt.Sprintf("unknown augmentation type %T", a)) 102 | } 103 | pos = end 104 | } 105 | dst.Write(src[pos:]) 106 | dst.Write(tail.Bytes()) 107 | return dst.Bytes(), adjustments 108 | } 109 | -------------------------------------------------------------------------------- /internal/pgo/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package pgo defines a superset of the Go syntax and provides the ability to 22 | // parse and manipulate it. pgo is used to parse versions of a gopatch change. 23 | // 24 | // Key features of pgo are: 25 | // - package names are optional 26 | // - expressions and statements may be written at the top level 27 | // - ... is supported in a number of places 28 | // 29 | // # Syntax 30 | // 31 | // The following documents the syntax for pgo, assuming the syntax for Go is 32 | // already provided under the "go." namespace. 33 | // 34 | // // Package names and imports are optional. Only a single top-level 35 | // // declaration is allowed. 36 | // file = package? imports decl; 37 | // package = "package" go.package_name; 38 | // 39 | // // import_decl is a standard Go import declarations. 40 | // imports = go.import_decl*; 41 | // 42 | // // For top-level declarations, type, function/method, and constant 43 | // // declarations are assumed to be standard Go declarations. In addition to 44 | // // them, statement lists and expressions are supported at the top-level. 45 | // decl 46 | // = go.type_decl 47 | // | go.func_decl 48 | // | go.const_decl 49 | // | stmt_list 50 | // | go.expr; 51 | // 52 | // // Statement lists can open with curly braces or as-is. The two cases are 53 | // // necessary to allow disambiguating between top-level type/const 54 | // // declarations and those inlined inside a code block. 55 | // stmt_list = '{' go.stmt* '}' | go.stmt+; 56 | // 57 | // # Dots 58 | // 59 | // "...", referred to as "dots" is accepted anywhere as a statement and an 60 | // expression. Note that pgo doesn't ascribe any meaning to these dots but 61 | // gopatch may. 62 | package pgo 63 | -------------------------------------------------------------------------------- /internal/text/unlines.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package text 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | ) 27 | 28 | // Unlines takes a list of strings and joins them with newlines between them, 29 | // including a trailing newline at the end. 30 | func Unlines(lines ...string) []byte { 31 | var out bytes.Buffer 32 | for _, l := range lines { 33 | fmt.Fprintln(&out, l) 34 | } 35 | return out.Bytes() 36 | } 37 | -------------------------------------------------------------------------------- /loader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "go/token" 7 | "io" 8 | "os" 9 | 10 | "github.com/uber-go/gopatch/internal/engine" 11 | "github.com/uber-go/gopatch/internal/parse" 12 | "go.uber.org/multierr" 13 | ) 14 | 15 | // patchLoader loads patches from varying sources 16 | // and compiles them into a series of programs. 17 | type patchLoader struct { 18 | fset *token.FileSet 19 | progs []*engine.Program 20 | 21 | // Pointer to parseAndCompile function, 22 | // which we can use to swap out this logic. 23 | parseAndCompile func(*token.FileSet, string, []byte) (*engine.Program, error) 24 | } 25 | 26 | func newPatchLoader(fset *token.FileSet) *patchLoader { 27 | return &patchLoader{ 28 | fset: fset, 29 | parseAndCompile: parseAndCompile, 30 | } 31 | } 32 | 33 | func (l *patchLoader) Programs() []*engine.Program { 34 | return l.progs 35 | } 36 | 37 | // LoadReader loads a patch from an io.Reader. 38 | func (l *patchLoader) LoadReader(name string, r io.Reader) error { 39 | src, err := io.ReadAll(r) 40 | if err != nil { 41 | return fmt.Errorf("read: %w", err) 42 | } 43 | 44 | prog, err := l.parseAndCompile(l.fset, name, src) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | l.progs = append(l.progs, prog) 50 | return nil 51 | } 52 | 53 | // LoadFile loads a patch from the given file. 54 | func (l *patchLoader) LoadFile(path string) (err error) { 55 | f, err := os.Open(path) 56 | if err != nil { 57 | return err 58 | } 59 | defer multierr.AppendInvoke(&err, multierr.Close(f)) 60 | 61 | if err := l.LoadReader(path, f); err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // LoadFileList loads patches specified in a file 69 | // that contains a list of file paths to other patches. 70 | func (l *patchLoader) LoadFileList(patchList string) (err error) { 71 | f, err := os.Open(patchList) 72 | if err != nil { 73 | return err 74 | } 75 | defer multierr.AppendInvoke(&err, multierr.Close(f)) 76 | 77 | scanner := bufio.NewScanner(f) 78 | for scanner.Scan() { 79 | path := scanner.Text() 80 | if len(path) == 0 { 81 | continue 82 | } 83 | 84 | if err := l.LoadFile(path); err != nil { 85 | return fmt.Errorf("load patch %q: %w", path, err) 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | // parseAndCompile parses the given patch contents, 92 | // and compiles them into a gopatch program. 93 | func parseAndCompile(fset *token.FileSet, name string, src []byte) (*engine.Program, error) { 94 | astProg, err := parse.Parse(fset, name, src) 95 | if err != nil { 96 | return nil, fmt.Errorf("parse: %w", err) 97 | } 98 | prog, err := engine.Compile(fset, astProg) 99 | if err != nil { 100 | return nil, fmt.Errorf("compile: %w", err) 101 | } 102 | return prog, nil 103 | } 104 | -------------------------------------------------------------------------------- /patch/gopatch.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/ast" 8 | "go/format" 9 | "go/parser" 10 | "go/token" 11 | "golang.org/x/tools/imports" 12 | "sort" 13 | 14 | "github.com/uber-go/gopatch/internal/astdiff" 15 | "github.com/uber-go/gopatch/internal/engine" 16 | "github.com/uber-go/gopatch/internal/parse" 17 | ) 18 | 19 | // File is a patch difference file that can be applied to Go file. 20 | type File struct { 21 | fset *token.FileSet 22 | prog *engine.Program 23 | } 24 | 25 | // Parse the patch file and creates data that can be applied to the Go file. 26 | func Parse(patchFileName string, src []byte) (*File, error) { 27 | fset := token.NewFileSet() 28 | 29 | astProg, err := parse.Parse(fset, patchFileName, src) 30 | if err != nil { 31 | return nil, fmt.Errorf("parse: %w", err) 32 | } 33 | prog, err := engine.Compile(fset, astProg) 34 | if err != nil { 35 | return nil, fmt.Errorf("compile: %w", err) 36 | } 37 | 38 | return &File{fset: fset, prog: prog}, nil 39 | } 40 | 41 | // Apply takes the Go file name and its contents and returns a Go file with the patch applied. 42 | func (f *File) Apply(filename string, src []byte) ([]byte, error) { 43 | base, err := parser.ParseFile(f.fset, filename, src, parser.AllErrors|parser.ParseComments) 44 | if err != nil { 45 | return nil, fmt.Errorf("could not parse %q: %w", filename, err) 46 | } 47 | 48 | snap := astdiff.Before(base, ast.NewCommentMap(f.fset, base, base.Comments)) 49 | 50 | var fout *ast.File 51 | var retErr error 52 | for _, c := range f.prog.Changes { 53 | d, ok := c.Match(base) 54 | if !ok { 55 | // This patch didn't modify the file. Try the next one. 56 | continue 57 | } 58 | 59 | cl := engine.NewChangelog() 60 | 61 | fout, err = c.Replace(d, cl) 62 | if err != nil { 63 | retErr = errors.Join(retErr, err) 64 | continue 65 | } 66 | 67 | snap = snap.Diff(fout, cl) 68 | cleanupFilePos(f.fset.File(fout.Pos()), cl, fout.Comments) 69 | } 70 | 71 | if retErr != nil { 72 | return nil, retErr 73 | } 74 | 75 | if fout == nil { 76 | return src, nil 77 | } 78 | 79 | var out bytes.Buffer 80 | err = format.Node(&out, f.fset, fout) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | bs := out.Bytes() 86 | bs, err = imports.Process(filename, bs, &imports.Options{ 87 | Comments: true, 88 | TabIndent: true, 89 | TabWidth: 8, 90 | FormatOnly: true, 91 | }) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return bs, nil 97 | } 98 | 99 | func cleanupFilePos(tfile *token.File, cl engine.Changelog, comments []*ast.CommentGroup) { 100 | linesToDelete := make(map[int]struct{}) 101 | for _, dr := range cl.ChangedIntervals() { 102 | if dr.Start == token.NoPos { 103 | continue 104 | } 105 | 106 | for i := tfile.Line(dr.Start); i < tfile.Line(dr.End); i++ { 107 | if i > 0 { 108 | linesToDelete[i] = struct{}{} 109 | } 110 | } 111 | 112 | // Remove comments in the changed sections of the code. 113 | for _, cg := range comments { 114 | var list []*ast.Comment 115 | for _, c := range cg.List { 116 | if c.Pos() >= dr.Start && c.End() <= dr.End { 117 | continue 118 | } 119 | list = append(list, c) 120 | } 121 | cg.List = list 122 | } 123 | } 124 | 125 | lines := make([]int, 0, len(linesToDelete)) 126 | for i := range linesToDelete { 127 | lines = append(lines, i) 128 | } 129 | sort.Ints(lines) 130 | for i := len(lines) - 1; i >= 0; i-- { 131 | tfile.MergeLine(lines[i]) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | This directory contains end-to-end integration tests for gopatch. Each test is 2 | specified as a [txtar] file. This is the same format used by Go Playground to 3 | support multiple files. 4 | 5 | [txtar]: https://godoc.org/github.com/rogpeppe/go-internal/txtar 6 | 7 | Tests contain a one or more patch files which are executed upon one or more Go 8 | file pairs, also specified in the patch file. 9 | 10 | - Patch files must be specified with the ".patch" suffix 11 | - Input files must be specified with the ".in.go" suffix 12 | - Output files must be specified with the ".out.go" suffix 13 | - Each input file must have an output file and vice versa 14 | 15 | Input files will be renamed from ".in.go" to ".go" and the patches will be 16 | executed on them in-order. Their new contents will be matched against the 17 | associated ".out.go" files, marking the test as failed if they don't. 18 | 19 | Test files will generally take the form, 20 | 21 | 22 | 23 | -- p1.patch -- 24 | @@ 25 | @@ 26 | -foo 27 | +bar 28 | 29 | 30 | 31 | -- foo.in.go -- 32 | package x 33 | 34 | // input source 35 | 36 | -- foo.out.go -- 37 | package x 38 | 39 | // output source 40 | 41 | 42 | 43 | Patch files referenced inside test cases may optionally take the following 44 | form instead. 45 | 46 | => path/to/another/file.patch 47 | 48 | Where the path is relative to the root of the directory. In this case, the 49 | patch will be read from the provided path. Use this to test examples in the 50 | examples/ directory. 51 | -------------------------------------------------------------------------------- /testdata/add_ctx_param: -------------------------------------------------------------------------------- 1 | Test adding a context parameter to a function. 2 | 3 | TODO: Collateral changes to callers of this function. 4 | TODO: Handle named and unnamed parameters automatically. 5 | 6 | -- add_ctx_param.patch -- 7 | # Add context parameter to the function 8 | @@ 9 | var f identifier 10 | @@ 11 | -func f(...) (*Response, error) { 12 | +func f(context.Context, ...) (*Response, error) { 13 | ... 14 | } 15 | 16 | -- send.in.go -- 17 | package sender 18 | 19 | // Send does stuff. 20 | func Send(string) (*Response, error) { 21 | fmt.Println("Sending request") 22 | // Here's a comment. 23 | return &Response{}, nil 24 | } 25 | 26 | -- send.out.go -- 27 | package sender 28 | 29 | // Send does stuff. 30 | func Send(context.Context, string) (*Response, error) { 31 | fmt.Println("Sending request") 32 | // Here's a comment. 33 | return &Response{}, nil 34 | } 35 | 36 | -- send.diff -- 37 | --- send.go 38 | +++ send.go 39 | @@ -1,7 +1,7 @@ 40 | package sender 41 | 42 | // Send does stuff. 43 | -func Send(string) (*Response, error) { 44 | +func Send(context.Context, string) (*Response, error) { 45 | fmt.Println("Sending request") 46 | // Here's a comment. 47 | return &Response{}, nil 48 | 49 | -- send.diff.stderr -- 50 | send.go:Add context parameter to the function 51 | -------------------------------------------------------------------------------- /testdata/add_error_param: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | -... := foo(...) 5 | +..., err := foo(...) 6 | 7 | -- a.in.go -- 8 | package x 9 | 10 | func do() { 11 | x, y, z := foo(1, 2, 3) 12 | } 13 | 14 | -- a.out.go -- 15 | package x 16 | 17 | func do() { 18 | x, y, z, err := foo(1, 2, 3) 19 | } 20 | -------------------------------------------------------------------------------- /testdata/add_iface_return: -------------------------------------------------------------------------------- 1 | TODO: Add "..." where applicable and update implementations. 2 | 3 | -- add_err.patch -- 4 | @@ 5 | @@ 6 | type Foo interface { 7 | ... 8 | - Do() 9 | + Do() error 10 | ... 11 | } 12 | 13 | @@ 14 | @@ 15 | -func (...) Do() { 16 | +func (...) Do() error { 17 | ... 18 | + return nil 19 | } 20 | 21 | -- a.in.go -- 22 | package a 23 | 24 | type Foo interface { 25 | Do() 26 | String() string 27 | } 28 | 29 | type bar struct{} 30 | 31 | func (*bar) Do() { 32 | fmt.Println("hello") 33 | } 34 | 35 | -- a.out.go -- 36 | package a 37 | 38 | type Foo interface { 39 | Do() error 40 | String() string 41 | } 42 | 43 | type bar struct{} 44 | 45 | func (*bar) Do() error { 46 | fmt.Println("hello") 47 | return nil 48 | } 49 | 50 | -- a.diff -- 51 | --- a.go 52 | +++ a.go 53 | @@ -1,12 +1,13 @@ 54 | package a 55 | 56 | type Foo interface { 57 | - Do() 58 | + Do() error 59 | String() string 60 | } 61 | 62 | type bar struct{} 63 | 64 | -func (*bar) Do() { 65 | +func (*bar) Do() error { 66 | fmt.Println("hello") 67 | + return nil 68 | } 69 | -------------------------------------------------------------------------------- /testdata/case_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | -case foo, ..., bar: 5 | +case foo, ..., baz: 6 | 7 | -- a.in.go -- 8 | package a 9 | 10 | func a() { 11 | switch s { 12 | case foo, bar: 13 | first() 14 | case foo, another, bar: 15 | second() 16 | } 17 | } 18 | 19 | -- a.out.go -- 20 | package a 21 | 22 | func a() { 23 | switch s { 24 | case foo, baz: 25 | first() 26 | case foo, another, baz: 27 | second() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /testdata/const_to_var: -------------------------------------------------------------------------------- 1 | -- apply.patch -- 2 | @@ 3 | var name identifier 4 | var value expression 5 | @@ 6 | -const ( 7 | +var ( 8 | name = value 9 | ) 10 | 11 | -- top_level.in.go -- 12 | package foo 13 | 14 | const ( 15 | Foo = "hello" 16 | ) 17 | 18 | -- top_level.out.go -- 19 | package foo 20 | 21 | var ( 22 | Foo = "hello" 23 | ) 24 | 25 | -- top_level.diff -- 26 | --- top_level.go 27 | +++ top_level.go 28 | @@ -1,5 +1,5 @@ 29 | package foo 30 | 31 | -const ( 32 | +var ( 33 | Foo = "hello" 34 | ) 35 | 36 | -- nested.in.go -- 37 | package foo 38 | 39 | func bar() { 40 | const ( 41 | Foo = "world" 42 | ) 43 | } 44 | 45 | -- nested.out.go -- 46 | package foo 47 | 48 | func bar() { 49 | var ( 50 | Foo = "world" 51 | ) 52 | } 53 | 54 | -- nested.diff -- 55 | --- nested.go 56 | +++ nested.go 57 | @@ -1,7 +1,7 @@ 58 | package foo 59 | 60 | func bar() { 61 | - const ( 62 | + var ( 63 | Foo = "world" 64 | ) 65 | } 66 | 67 | -- single_top_level.in.go -- 68 | package foo 69 | 70 | const Foo = "hello" 71 | 72 | -- single_top_level.out.go -- 73 | package foo 74 | 75 | var Foo = "hello" 76 | -------------------------------------------------------------------------------- /testdata/dedupe_args: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var x expression 4 | @@ 5 | -foo(x, x) 6 | +v := x 7 | +foo(v, v) 8 | 9 | -- foo.in.go -- 10 | package foo 11 | 12 | func bar() { 13 | foo(x + 1, x + 1) 14 | } 15 | 16 | -- foo.out.go -- 17 | package foo 18 | 19 | func bar() { 20 | v := x + 1 21 | foo(v, v) 22 | } 23 | 24 | -- foo.diff -- 25 | --- foo.go 26 | +++ foo.go 27 | @@ -1,5 +1,6 @@ 28 | package foo 29 | 30 | func bar() { 31 | - foo(x + 1, x + 1) 32 | + v := x + 1 33 | + foo(v, v) 34 | } 35 | 36 | -- bar.in.go -- 37 | package bar 38 | 39 | func baz() { 40 | foo(getValue(), getValue()) 41 | } 42 | 43 | -- bar.out.go -- 44 | package bar 45 | 46 | func baz() { 47 | v := getValue() 48 | foo(v, v) 49 | } 50 | 51 | -- bar.diff -- 52 | --- bar.go 53 | +++ bar.go 54 | @@ -1,5 +1,6 @@ 55 | package bar 56 | 57 | func baz() { 58 | - foo(getValue(), getValue()) 59 | + v := getValue() 60 | + foo(v, v) 61 | } 62 | -------------------------------------------------------------------------------- /testdata/defer_return: -------------------------------------------------------------------------------- 1 | Replaces "defer and return" with an explicit call to the deferred function 2 | before returning. 3 | 4 | This patch is less valuable than before as 5 | https://go-review.googlesource.com/c/go/+/171758/ has been merged. 6 | 7 | TODO: freshIdentifier to make r unique. 8 | 9 | -- defer_return.patch -- 10 | @@ 11 | var x, y expression 12 | @@ 13 | -defer x(...) 14 | -return y 15 | +r := y 16 | +x(...) 17 | +return r 18 | 19 | -- mutex.in.go -- 20 | package a 21 | 22 | func X() { 23 | mutex.Lock() 24 | defer mutex.Unlock() 25 | return calculate() 26 | } 27 | 28 | -- mutex.out.go -- 29 | package a 30 | 31 | func X() { 32 | mutex.Lock() 33 | r := calculate() 34 | mutex.Unlock() 35 | return r 36 | } 37 | 38 | -- mutex.diff -- 39 | --- mutex.go 40 | +++ mutex.go 41 | @@ -2,6 +2,7 @@ 42 | 43 | func X() { 44 | mutex.Lock() 45 | - defer mutex.Unlock() 46 | - return calculate() 47 | + r := calculate() 48 | + mutex.Unlock() 49 | + return r 50 | } 51 | -------------------------------------------------------------------------------- /testdata/delete_any_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var foo identifier 4 | @@ 5 | -import foo "foo" 6 | 7 | bar 8 | 9 | -- unnamed.in.go -- 10 | package x 11 | 12 | import "foo" 13 | 14 | func x() { 15 | bar() 16 | } 17 | 18 | -- unnamed.out.go -- 19 | package x 20 | 21 | func x() { 22 | bar() 23 | } 24 | 25 | -- unnamed.diff -- 26 | --- unnamed.go 27 | +++ unnamed.go 28 | @@ -1,7 +1,5 @@ 29 | package x 30 | 31 | -import "foo" 32 | - 33 | func x() { 34 | bar() 35 | } 36 | 37 | -- named.in.go -- 38 | package x 39 | 40 | import baz "foo" 41 | 42 | func x() { 43 | bar() 44 | } 45 | 46 | -- named.out.go -- 47 | package x 48 | 49 | func x() { 50 | bar() 51 | } 52 | 53 | -- named.diff -- 54 | --- named.go 55 | +++ named.go 56 | @@ -1,7 +1,5 @@ 57 | package x 58 | 59 | -import baz "foo" 60 | - 61 | func x() { 62 | bar() 63 | } 64 | -------------------------------------------------------------------------------- /testdata/delete_dots: -------------------------------------------------------------------------------- 1 | -- delete_dots.patch -- 2 | @@ 3 | @@ 4 | -foo() 5 | -... 6 | bar() 7 | ... 8 | baz() 9 | -... 10 | qux() 11 | 12 | -- stuff.in.go -- 13 | package stuff 14 | 15 | func zzz() { 16 | foo() 17 | 18 | if err := x(); err != nil { 19 | panic(err) 20 | } 21 | 22 | bar() 23 | 24 | if err := y(); err != nil { 25 | panic(err) 26 | } 27 | 28 | baz() 29 | 30 | if err := y(); err != nil { 31 | // This should be deleted. 32 | panic(err) 33 | } 34 | 35 | qux() 36 | } 37 | 38 | -- stuff.out.go -- 39 | package stuff 40 | 41 | func zzz() { 42 | bar() 43 | 44 | if err := y(); err != nil { 45 | panic(err) 46 | } 47 | 48 | baz() 49 | qux() 50 | } 51 | 52 | -- stuff.diff -- 53 | --- stuff.go 54 | +++ stuff.go 55 | @@ -1,12 +1,6 @@ 56 | package stuff 57 | 58 | func zzz() { 59 | - foo() 60 | - 61 | - if err := x(); err != nil { 62 | - panic(err) 63 | - } 64 | - 65 | bar() 66 | 67 | if err := y(); err != nil { 68 | @@ -14,11 +8,5 @@ 69 | } 70 | 71 | baz() 72 | - 73 | - if err := y(); err != nil { 74 | - // This should be deleted. 75 | - panic(err) 76 | - } 77 | - 78 | qux() 79 | } 80 | -------------------------------------------------------------------------------- /testdata/delete_field: -------------------------------------------------------------------------------- 1 | -- foo.patch -- 2 | @@ 3 | var foo identifier 4 | var y expression 5 | @@ 6 | import foo "example.com/foo.git" 7 | 8 | foo.Params{ 9 | ..., 10 | - Foo: y, 11 | ..., 12 | } 13 | 14 | -- named.in.go -- 15 | package a 16 | 17 | import foogit "example.com/foo.git" 18 | 19 | func bar() { 20 | foogit.Initialize(foogit.Params{ 21 | Name: "name", 22 | Foo: foogit.GetClient(), 23 | Value: "bar", 24 | }) 25 | } 26 | 27 | -- named.out.go -- 28 | package a 29 | 30 | import foogit "example.com/foo.git" 31 | 32 | func bar() { 33 | foogit.Initialize(foogit.Params{ 34 | Name: "name", 35 | 36 | Value: "bar", 37 | }) 38 | } 39 | 40 | -- named.diff -- 41 | --- named.go 42 | +++ named.go 43 | @@ -5,7 +5,7 @@ 44 | func bar() { 45 | foogit.Initialize(foogit.Params{ 46 | Name: "name", 47 | - Foo: foogit.GetClient(), 48 | + 49 | Value: "bar", 50 | }) 51 | } 52 | 53 | -- unnamed.in.go -- 54 | package a 55 | 56 | import "example.com/foo.git" 57 | 58 | func bar() { 59 | foo.Initialize(foo.Params{ 60 | Name: "name", 61 | Foo: foo.GetClient(), 62 | Value: "bar", 63 | }) 64 | } 65 | 66 | -- unnamed.out.go -- 67 | package a 68 | 69 | import "example.com/foo.git" 70 | 71 | func bar() { 72 | foo.Initialize(foo.Params{ 73 | Name: "name", 74 | 75 | Value: "bar", 76 | }) 77 | } 78 | 79 | -- unnamed.diff -- 80 | --- unnamed.go 81 | +++ unnamed.go 82 | @@ -5,7 +5,7 @@ 83 | func bar() { 84 | foo.Initialize(foo.Params{ 85 | Name: "name", 86 | - Foo: foo.GetClient(), 87 | + 88 | Value: "bar", 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /testdata/delete_import_panic: -------------------------------------------------------------------------------- 1 | Based on a user example of a panic. 2 | 3 | -- in.patch -- 4 | # Replace fooclient.SetParams with compat.SetParams 5 | @@ 6 | @@ 7 | -import fooclient "example.com/foo/client" 8 | +import "example.com/foo-client/compat" 9 | 10 | -fooclient.SetParams 11 | +compat.SetParams 12 | 13 | # Replace fooclient.getClient with compat.GetClient 14 | @@ 15 | @@ 16 | -import fooclient "example.com/foo/client" 17 | +import "example.com/foo-client/compat" 18 | 19 | -fooclient.GetClient() 20 | +compat.GetClient() 21 | 22 | -- a.in.go -- 23 | package main 24 | 25 | import fooclient "example.com/foo/client" 26 | 27 | func Example() { 28 | fooclient.SetParams("myservice") 29 | client, err := fooclient.GetClient() 30 | } 31 | 32 | -- a.out.go -- 33 | package main 34 | 35 | import "example.com/foo-client/compat" 36 | 37 | func Example() { 38 | compat.SetParams("myservice") 39 | client, err := compat.GetClient() 40 | } 41 | 42 | -- a.diff -- 43 | --- a.go 44 | +++ a.go 45 | @@ -1,8 +1,8 @@ 46 | package main 47 | 48 | -import fooclient "example.com/foo/client" 49 | +import "example.com/foo-client/compat" 50 | 51 | func Example() { 52 | - fooclient.SetParams("myservice") 53 | - client, err := fooclient.GetClient() 54 | + compat.SetParams("myservice") 55 | + client, err := compat.GetClient() 56 | } 57 | 58 | -- a.diff.stderr -- 59 | a.go:Replace fooclient.getClient with compat.GetClient 60 | -------------------------------------------------------------------------------- /testdata/delete_struct_field: -------------------------------------------------------------------------------- 1 | Deletes a deprecated field from a struct. 2 | 3 | -- delete_struct_field.patch -- 4 | @@ 5 | var T, D identifier 6 | @@ 7 | type T struct { 8 | ... 9 | - D *Deprecated 10 | ... 11 | } 12 | 13 | -- delete.in.go -- 14 | package something 15 | 16 | type Foo struct { 17 | A string 18 | B *Deprecated 19 | 20 | // C does stuff. 21 | C int 22 | } 23 | 24 | type Bar struct { 25 | d *Deprecated 26 | } 27 | 28 | -- delete.out.go -- 29 | package something 30 | 31 | type Foo struct { 32 | A string 33 | 34 | // C does stuff. 35 | C int 36 | } 37 | 38 | type Bar struct { 39 | } 40 | 41 | -- delete.diff -- 42 | --- delete.go 43 | +++ delete.go 44 | @@ -2,12 +2,10 @@ 45 | 46 | type Foo struct { 47 | A string 48 | - B *Deprecated 49 | 50 | // C does stuff. 51 | C int 52 | } 53 | 54 | type Bar struct { 55 | - d *Deprecated 56 | } 57 | -------------------------------------------------------------------------------- /testdata/delete_unnamed_import: -------------------------------------------------------------------------------- 1 | Delete an unnamed import verbatim. 2 | 3 | -- in.patch -- 4 | # Delete unnamed import 5 | @@ 6 | @@ 7 | -import "foo" 8 | 9 | bar 10 | 11 | -- unnamed.in.go -- 12 | package x 13 | 14 | import "foo" 15 | 16 | func x() { 17 | bar() 18 | } 19 | 20 | -- unnamed.out.go -- 21 | package x 22 | 23 | func x() { 24 | bar() 25 | } 26 | 27 | -- unnamed.diff -- 28 | --- unnamed.go 29 | +++ unnamed.go 30 | @@ -1,7 +1,5 @@ 31 | package x 32 | 33 | -import "foo" 34 | - 35 | func x() { 36 | bar() 37 | } 38 | 39 | -- unnamed.diff.stderr -- 40 | unnamed.go:Delete unnamed import 41 | 42 | -- named.in.go -- 43 | package x 44 | 45 | import foo "foo" 46 | 47 | func x() { 48 | bar() 49 | } 50 | 51 | -- named.out.go -- 52 | package x 53 | 54 | import foo "foo" 55 | 56 | func x() { 57 | bar() 58 | } 59 | 60 | -- named.diff -- 61 | -- named.diff.stderr -- 62 | -------------------------------------------------------------------------------- /testdata/destutter: -------------------------------------------------------------------------------- 1 | Removes stuttering in the name of a type. 2 | 3 | -- fix.patch -- 4 | => examples/destutter.patch 5 | 6 | -- http_client.in.go -- 7 | package http 8 | 9 | import "net/http" 10 | 11 | type HTTPClient struct { 12 | client *http.Client 13 | } 14 | 15 | func New(c *http.Client) *HTTPClient { 16 | return &HTTPClient{c} 17 | } 18 | 19 | func (c *HTTPClient) Do( 20 | ctx context.Context, 21 | r *http.Request, 22 | ) (*http.Response, error) { 23 | return c.client.Do(r.WithContext(ctx)) 24 | } 25 | 26 | -- http_client.out.go -- 27 | package http 28 | 29 | import "net/http" 30 | 31 | type Client struct { 32 | client *http.Client 33 | } 34 | 35 | func New(c *http.Client) *Client { 36 | return &Client{c} 37 | } 38 | 39 | func (c *Client) Do( 40 | ctx context.Context, 41 | r *http.Request, 42 | ) (*http.Response, error) { 43 | return c.client.Do(r.WithContext(ctx)) 44 | } 45 | 46 | -- http_client.diff -- 47 | --- http_client.go 48 | +++ http_client.go 49 | @@ -2,15 +2,15 @@ 50 | 51 | import "net/http" 52 | 53 | -type HTTPClient struct { 54 | +type Client struct { 55 | client *http.Client 56 | } 57 | 58 | -func New(c *http.Client) *HTTPClient { 59 | - return &HTTPClient{c} 60 | +func New(c *http.Client) *Client { 61 | + return &Client{c} 62 | } 63 | 64 | -func (c *HTTPClient) Do( 65 | +func (c *Client) Do( 66 | ctx context.Context, 67 | r *http.Request, 68 | ) (*http.Response, error) { 69 | 70 | -- unqualified_import.in.go -- 71 | package foo 72 | 73 | import "example.com/http" 74 | 75 | func doStuff(client *http.HTTPClient) err { 76 | res, err := client.Do(getRequest()) 77 | if err != nil { 78 | return err 79 | } 80 | return res.Body.Close() 81 | } 82 | 83 | -- unqualified_import.out.go -- 84 | package foo 85 | 86 | import "example.com/http" 87 | 88 | func doStuff(client *http.Client) err { 89 | res, err := client.Do(getRequest()) 90 | if err != nil { 91 | return err 92 | } 93 | return res.Body.Close() 94 | } 95 | 96 | -- unqualified_import.diff -- 97 | --- unqualified_import.go 98 | +++ unqualified_import.go 99 | @@ -2,7 +2,7 @@ 100 | 101 | import "example.com/http" 102 | 103 | -func doStuff(client *http.HTTPClient) err { 104 | +func doStuff(client *http.Client) err { 105 | res, err := client.Do(getRequest()) 106 | if err != nil { 107 | return err 108 | 109 | -- qualified_import.in.go -- 110 | package bar 111 | 112 | import ( 113 | "net/http" 114 | 115 | myhttp "example.com/http" 116 | ) 117 | 118 | func buildClient(c *http.Client) *myhttp.HTTPClient { 119 | return myhttp.New(c) 120 | } 121 | 122 | -- qualified_import.out.go -- 123 | package bar 124 | 125 | import ( 126 | "net/http" 127 | 128 | myhttp "example.com/http" 129 | ) 130 | 131 | func buildClient(c *http.Client) *myhttp.Client { 132 | return myhttp.New(c) 133 | } 134 | 135 | -- qualified_import.diff -- 136 | --- qualified_import.go 137 | +++ qualified_import.go 138 | @@ -6,6 +6,6 @@ 139 | myhttp "example.com/http" 140 | ) 141 | 142 | -func buildClient(c *http.Client) *myhttp.HTTPClient { 143 | +func buildClient(c *http.Client) *myhttp.Client { 144 | return myhttp.New(c) 145 | } 146 | -------------------------------------------------------------------------------- /testdata/dts_in_args: -------------------------------------------------------------------------------- 1 | -- dts_in_args.patch -- 2 | @@ 3 | @@ 4 | -func name(foo string, bar int, ...) string { 5 | +func name(foo string, ..., bar int) string { 6 | ... 7 | } 8 | 9 | -- dts_in_args.in.go -- 10 | package a 11 | 12 | func name(foo string, bar int, thingOne string, thingTwo func(...string) string) string { 13 | return "very valid go" 14 | } 15 | 16 | -- dts_in_args.out.go -- 17 | package a 18 | 19 | func name(foo string, thingOne string, thingTwo func(...string) string, bar int) string { 20 | return "very valid go" 21 | } 22 | 23 | -- dts_in_args.diff -- 24 | --- dts_in_args.go 25 | +++ dts_in_args.go 26 | @@ -1,5 +1,5 @@ 27 | package a 28 | 29 | -func name(foo string, bar int, thingOne string, thingTwo func(...string) string) string { 30 | +func name(foo string, thingOne string, thingTwo func(...string) string, bar int) string { 31 | return "very valid go" 32 | } 33 | -------------------------------------------------------------------------------- /testdata/dts_in_receiver: -------------------------------------------------------------------------------- 1 | -- dts_in_receiver.patch -- 2 | @@ 3 | @@ 4 | -func (...) name(foo string) string { 5 | +func (...) name(bar string) string { 6 | ... 7 | } 8 | 9 | -- has_receiver.in.go -- 10 | package a 11 | 12 | func (r *Receiver) name(foo string) string { 13 | return "very valid go" 14 | } 15 | 16 | -- has_receiver.out.go -- 17 | package a 18 | 19 | func (r *Receiver) name(bar string) string { 20 | return "very valid go" 21 | } 22 | 23 | -- has_receiver.diff -- 24 | --- has_receiver.go 25 | +++ has_receiver.go 26 | @@ -1,5 +1,5 @@ 27 | package a 28 | 29 | -func (r *Receiver) name(foo string) string { 30 | +func (r *Receiver) name(bar string) string { 31 | return "very valid go" 32 | } 33 | -------------------------------------------------------------------------------- /testdata/dts_in_results: -------------------------------------------------------------------------------- 1 | -- unnamed.patch -- 2 | @@ 3 | @@ 4 | -func name(foo string, bar int) (error, ...) { 5 | +func name(foo string, bar int) (..., error) { 6 | - return nil, ... 7 | + return ..., nil 8 | } 9 | 10 | -- unnamed.in.go -- 11 | package a 12 | 13 | func name(foo string, bar int) (error, string) { 14 | return nil, "very valid go" 15 | } 16 | 17 | -- unnamed.out.go -- 18 | package a 19 | 20 | func name(foo string, bar int) (string, error) { 21 | return "very valid go", nil 22 | } 23 | 24 | -- unnamed.diff -- 25 | --- unnamed.go 26 | +++ unnamed.go 27 | @@ -1,5 +1,5 @@ 28 | package a 29 | 30 | -func name(foo string, bar int) (error, string) { 31 | - return nil, "very valid go" 32 | +func name(foo string, bar int) (string, error) { 33 | + return "very valid go", nil 34 | } 35 | 36 | -- named.patch -- 37 | @@ 38 | @@ 39 | -func name(foo string, bar int) (err error, ...) { 40 | +func name(foo string, bar int) (..., err error) { 41 | - return nil, ... 42 | + return ..., nil 43 | } 44 | 45 | -- named.in.go -- 46 | package a 47 | 48 | func name(foo string, bar int) (err error, val string) { 49 | return nil, "very valid go" 50 | } 51 | 52 | -- named.out.go -- 53 | package a 54 | 55 | func name(foo string, bar int) (val string, err error) { 56 | return "very valid go", nil 57 | } 58 | 59 | -- named.diff -- 60 | --- named.go 61 | +++ named.go 62 | @@ -1,5 +1,5 @@ 63 | package a 64 | 65 | -func name(foo string, bar int) (err error, val string) { 66 | - return nil, "very valid go" 67 | +func name(foo string, bar int) (val string, err error) { 68 | + return "very valid go", nil 69 | } 70 | -------------------------------------------------------------------------------- /testdata/embed_to_field: -------------------------------------------------------------------------------- 1 | TODO: "..." for the field 2 | 3 | -- struct.patch -- 4 | @@ 5 | var Type, Embed identifier 6 | @@ 7 | type Type struct { 8 | - Embed 9 | + Embed Embed 10 | ... 11 | } 12 | 13 | -- a.in.go -- 14 | package a 15 | 16 | type User struct { 17 | Person 18 | 19 | Name string 20 | } 21 | 22 | -- a.out.go -- 23 | package a 24 | 25 | type User struct { 26 | Person Person 27 | 28 | Name string 29 | } 30 | 31 | -- a.diff -- 32 | --- a.go 33 | +++ a.go 34 | @@ -1,7 +1,7 @@ 35 | package a 36 | 37 | type User struct { 38 | - Person 39 | + Person Person 40 | 41 | Name string 42 | } 43 | -------------------------------------------------------------------------------- /testdata/embed_to_newtype: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | # Convert embed to newtype 3 | @@ 4 | var X, Y identifier 5 | @@ 6 | -type X struct{ Y } 7 | +type X Y 8 | 9 | -- foo.in.go -- 10 | package foo 11 | 12 | type A struct{ B } 13 | 14 | -- foo.out.go -- 15 | package foo 16 | 17 | type A B 18 | 19 | -- foo.diff -- 20 | --- foo.go 21 | +++ foo.go 22 | @@ -1,3 +1,3 @@ 23 | package foo 24 | 25 | -type A struct{ B } 26 | +type A B 27 | 28 | -- foo.diff.stderr -- 29 | foo.go:Convert embed to newtype 30 | -------------------------------------------------------------------------------- /testdata/foo_call_to_bar: -------------------------------------------------------------------------------- 1 | -- foo_bar.patch -- 2 | @@ 3 | var x expression 4 | @@ 5 | -foo(x) 6 | +bar(x) 7 | 8 | -- simple.in.go -- 9 | package main 10 | 11 | func main() { 12 | foo(42) 13 | } 14 | 15 | -- simple.out.go -- 16 | package main 17 | 18 | func main() { 19 | bar(42) 20 | } 21 | 22 | -- simple.diff -- 23 | --- simple.go 24 | +++ simple.go 25 | @@ -1,5 +1,5 @@ 26 | package main 27 | 28 | func main() { 29 | - foo(42) 30 | + bar(42) 31 | } 32 | -------------------------------------------------------------------------------- /testdata/func_within_a_func: -------------------------------------------------------------------------------- 1 | -- one_dots.patch -- 2 | @@ 3 | @@ 4 | -func foo(close func(...)) string { 5 | +func foo(close func(...) error) string { 6 | ... 7 | } 8 | 9 | -- one_dots.in.go -- 10 | package a 11 | 12 | import ( 13 | "log" 14 | "os" 15 | ) 16 | 17 | func foo(close func(*log.Logger)) string { 18 | logger := log.New(os.Stdout, name, 0) 19 | defer close(logger) 20 | return "very valid go" 21 | } 22 | 23 | -- one_dots.out.go -- 24 | package a 25 | 26 | import ( 27 | "log" 28 | "os" 29 | ) 30 | 31 | func foo(close func(*log.Logger) error) string { 32 | logger := log.New(os.Stdout, name, 0) 33 | defer close(logger) 34 | return "very valid go" 35 | } 36 | 37 | -- one_dots.diff -- 38 | --- one_dots.go 39 | +++ one_dots.go 40 | @@ -5,7 +5,7 @@ 41 | "os" 42 | ) 43 | 44 | -func foo(close func(*log.Logger)) string { 45 | +func foo(close func(*log.Logger) error) string { 46 | logger := log.New(os.Stdout, name, 0) 47 | defer close(logger) 48 | return "very valid go" 49 | 50 | -- two_dots.patch -- 51 | @@ 52 | @@ 53 | -func foo(..., close func(...)) string { 54 | +func foo(..., close func(...) error) string { 55 | ... 56 | } 57 | 58 | -- two_dots.in.go -- 59 | package a 60 | 61 | import ( 62 | "log" 63 | "os" 64 | ) 65 | 66 | func foo(name string, close func(*log.Logger)) string { 67 | logger := log.New(os.Stdout, name, 0) 68 | defer close(logger) 69 | return "very valid go" 70 | } 71 | 72 | -- two_dots.out.go -- 73 | package a 74 | 75 | import ( 76 | "log" 77 | "os" 78 | ) 79 | 80 | func foo(..., close func(*log.Logger) error) string { 81 | logger := log.New(os.Stdout, name, 0) 82 | defer close(logger) 83 | return "very valid go" 84 | } 85 | -------------------------------------------------------------------------------- /testdata/generic_instantiation: -------------------------------------------------------------------------------- 1 | Test that we can parse generic function instantiation in patches. 2 | 3 | -- foo_to_bar.patch -- 4 | @@ 5 | var T, v expression 6 | @@ 7 | -foo[T](v) 8 | +bar[T](v) 9 | 10 | -- a.in.go -- 11 | package a 12 | 13 | func baz() { 14 | foo[int](42) 15 | foo[[]byte]([]byte("hello")) 16 | foo[List[int]](nil) 17 | } 18 | 19 | -- a.out.go -- 20 | package a 21 | 22 | func baz() { 23 | bar[int](42) 24 | bar[[]byte]([]byte("hello")) 25 | bar[List[int]](nil) 26 | } 27 | 28 | -- a.diff -- 29 | --- a.go 30 | +++ a.go 31 | @@ -1,7 +1,7 @@ 32 | package a 33 | 34 | func baz() { 35 | - foo[int](42) 36 | - foo[[]byte]([]byte("hello")) 37 | - foo[List[int]](nil) 38 | + bar[int](42) 39 | + bar[[]byte]([]byte("hello")) 40 | + bar[List[int]](nil) 41 | } 42 | -------------------------------------------------------------------------------- /testdata/generics_in_src: -------------------------------------------------------------------------------- 1 | Test that we can parse generics syntax in source files. 2 | 3 | -- foo_to_bar.patch -- 4 | @@ 5 | @@ 6 | -foo 7 | +bar 8 | 9 | -- a.in.go -- 10 | package a 11 | 12 | import "fmt" 13 | 14 | func foo[T any](x T) { 15 | fmt.Println(x) 16 | } 17 | 18 | func baz() { 19 | foo[int](42) 20 | } 21 | 22 | -- a.out.go -- 23 | package a 24 | 25 | import "fmt" 26 | 27 | func bar[T any](x T) { 28 | fmt.Println(x) 29 | } 30 | 31 | func baz() { 32 | bar[int](42) 33 | } 34 | 35 | 36 | -- a.diff -- 37 | --- a.go 38 | +++ a.go 39 | @@ -2,10 +2,10 @@ 40 | 41 | import "fmt" 42 | 43 | -func foo[T any](x T) { 44 | +func bar[T any](x T) { 45 | fmt.Println(x) 46 | } 47 | 48 | func baz() { 49 | - foo[int](42) 50 | + bar[int](42) 51 | } 52 | -------------------------------------------------------------------------------- /testdata/gomock: -------------------------------------------------------------------------------- 1 | -- gomock.patch -- 2 | => examples/gomock-v1.5.0.patch 3 | 4 | -- foo_test.in.go -- 5 | package foo 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | ) 12 | 13 | func TestFoo(t *testing.T) { 14 | ctrl := gomock.NewController(t) 15 | defer ctrl.Finish() 16 | 17 | run(NewFooMock(ctrl)) 18 | } 19 | 20 | -- foo_test.out.go -- 21 | package foo 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/golang/mock/gomock" 27 | ) 28 | 29 | func TestFoo(t *testing.T) { 30 | ctrl := gomock.NewController(t) 31 | 32 | run(NewFooMock(ctrl)) 33 | } 34 | 35 | -- foo_test.diff -- 36 | --- foo_test.go 37 | +++ foo_test.go 38 | @@ -8,7 +8,6 @@ 39 | 40 | func TestFoo(t *testing.T) { 41 | ctrl := gomock.NewController(t) 42 | - defer ctrl.Finish() 43 | 44 | run(NewFooMock(ctrl)) 45 | } 46 | 47 | -- foo_test.diff.stderr -- 48 | foo_test.go:Delete redundant gomock.Controller.Finish() 49 | 50 | -- bar_test.in.go -- 51 | package bar 52 | 53 | import ( 54 | "testing" 55 | 56 | "github.com/golang/mock/gomock" 57 | ) 58 | 59 | func TestBar(t *testing.T) { 60 | ctrl := gomock.NewController(t) 61 | dir := t.TempDir() 62 | defer ctrl.Finish() 63 | 64 | run(NewBarMock(ctrl)) 65 | } 66 | 67 | -- bar_test.out.go -- 68 | package bar 69 | 70 | import ( 71 | "testing" 72 | 73 | "github.com/golang/mock/gomock" 74 | ) 75 | 76 | func TestBar(t *testing.T) { 77 | ctrl := gomock.NewController(t) 78 | dir := t.TempDir() 79 | 80 | run(NewBarMock(ctrl)) 81 | } 82 | 83 | -- bar_test.diff -- 84 | --- bar_test.go 85 | +++ bar_test.go 86 | @@ -9,7 +9,6 @@ 87 | func TestBar(t *testing.T) { 88 | ctrl := gomock.NewController(t) 89 | dir := t.TempDir() 90 | - defer ctrl.Finish() 91 | 92 | run(NewBarMock(ctrl)) 93 | } 94 | 95 | -- bar_test.diff.stderr -- 96 | bar_test.go:Delete redundant gomock.Controller.Finish() 97 | 98 | -- named_import.in.go -- 99 | package baz 100 | 101 | import ( 102 | "testing" 103 | 104 | mock "github.com/golang/mock/gomock" 105 | ) 106 | 107 | func TestBaz(t *testing.T) { 108 | mockCtrl := mock.NewController(t) 109 | defer mockCtrl.Finish() 110 | 111 | run(NewBazMock(mockCtrl)) 112 | } 113 | 114 | -- named_import.out.go -- 115 | package baz 116 | 117 | import ( 118 | "testing" 119 | 120 | mock "github.com/golang/mock/gomock" 121 | ) 122 | 123 | func TestBaz(t *testing.T) { 124 | mockCtrl := mock.NewController(t) 125 | 126 | run(NewBazMock(mockCtrl)) 127 | } 128 | 129 | -- named_import.diff -- 130 | --- named_import.go 131 | +++ named_import.go 132 | @@ -8,7 +8,6 @@ 133 | 134 | func TestBaz(t *testing.T) { 135 | mockCtrl := mock.NewController(t) 136 | - defer mockCtrl.Finish() 137 | 138 | run(NewBazMock(mockCtrl)) 139 | } 140 | 141 | -- named_import.diff.stderr -- 142 | named_import.go:Delete redundant gomock.Controller.Finish() 143 | -------------------------------------------------------------------------------- /testdata/httpclient_use_ctx: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var req, f identifier 4 | @@ 5 | import "net/http" 6 | +import "context" 7 | 8 | func f( 9 | + ctx context.Context, 10 | ..., 11 | req *http.Request, 12 | ..., 13 | ) (..., error) { 14 | + req = req.WithContext(ctx) 15 | ... 16 | } 17 | 18 | -- one_return.in.go -- 19 | package main 20 | 21 | import "net/http" 22 | 23 | func printResponse(r *http.Request) error { 24 | res, err := http.DefaultClient.Do(r) 25 | if err != nil { 26 | return err 27 | } 28 | defer res.Body.Close() 29 | 30 | if _, err := io.Copy(os.Stdout, res.Body); err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | 37 | -- one_return.out.go -- 38 | package main 39 | 40 | import ( 41 | "context" 42 | "net/http" 43 | ) 44 | 45 | func printResponse(ctx context.Context, r *http.Request) error { 46 | r = r.WithContext(ctx) 47 | res, err := http.DefaultClient.Do(r) 48 | if err != nil { 49 | return err 50 | } 51 | defer res.Body.Close() 52 | 53 | if _, err := io.Copy(os.Stdout, res.Body); err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | 60 | -- two_returns.in.go -- 61 | package main 62 | 63 | import "net/http" 64 | 65 | func getResponse(req *http.Request) (*http.Response, error) { 66 | res, err := http.DefaultClient.Do(req) 67 | return res, err 68 | } 69 | 70 | -- two_returns.out.go -- 71 | package main 72 | 73 | import ( 74 | "context" 75 | "net/http" 76 | ) 77 | 78 | func getResponse(ctx context.Context, req *http.Request) (*http.Response, error) { 79 | req = req.WithContext(ctx) 80 | res, err := http.DefaultClient.Do(req) 81 | return res, err 82 | } 83 | 84 | -- two_returns.diff -- 85 | --- two_returns.go 86 | +++ two_returns.go 87 | @@ -1,8 +1,12 @@ 88 | package main 89 | 90 | -import "net/http" 91 | +import ( 92 | + "context" 93 | + "net/http" 94 | +) 95 | 96 | -func getResponse(req *http.Request) (*http.Response, error) { 97 | +func getResponse(ctx context.Context, req *http.Request) (*http.Response, error) { 98 | + req = req.WithContext(ctx) 99 | res, err := http.DefaultClient.Do(req) 100 | return res, err 101 | } 102 | -------------------------------------------------------------------------------- /testdata/if_to_for: -------------------------------------------------------------------------------- 1 | -- if_to_for.patch -- 2 | @@ 3 | var c expression 4 | @@ 5 | -if c { 6 | +for c { 7 | ... 8 | } 9 | 10 | -- x.in.go -- 11 | package foo 12 | 13 | func x() { 14 | if true { 15 | foo() // baz 16 | bar() // qux 17 | } 18 | } 19 | 20 | -- x.out.go -- 21 | package foo 22 | 23 | func x() { 24 | for true { 25 | foo() // baz 26 | bar() // qux 27 | } 28 | } 29 | 30 | -- x.diff -- 31 | --- x.go 32 | +++ x.go 33 | @@ -1,7 +1,7 @@ 34 | package foo 35 | 36 | func x() { 37 | - if true { 38 | + for true { 39 | foo() // baz 40 | bar() // qux 41 | } 42 | -------------------------------------------------------------------------------- /testdata/import_grouping: -------------------------------------------------------------------------------- 1 | -- apply.patch -- 2 | @@ 3 | var s expression 4 | @@ 5 | -import "fmt" 6 | +import "errors" 7 | 8 | -fmt.Errorf(s) 9 | +errors.New(s) 10 | 11 | -- foo.in.go -- 12 | package foo 13 | 14 | import ( 15 | "context" 16 | "fmt" 17 | 18 | "code.mycompany.com/foo" 19 | ) 20 | 21 | // Foo does stuff. 22 | func Foo(context.Context) error { 23 | return fmt.Errorf("foo") 24 | } 25 | 26 | -- foo.out.go -- 27 | package foo 28 | 29 | import ( 30 | "context" 31 | "errors" 32 | 33 | "code.mycompany.com/foo" 34 | ) 35 | 36 | // Foo does stuff. 37 | func Foo(context.Context) error { 38 | return errors.New("foo") 39 | } 40 | 41 | -- foo.diff -- 42 | --- foo.go 43 | +++ foo.go 44 | @@ -2,12 +2,12 @@ 45 | 46 | import ( 47 | "context" 48 | - "fmt" 49 | + "errors" 50 | 51 | "code.mycompany.com/foo" 52 | ) 53 | 54 | // Foo does stuff. 55 | func Foo(context.Context) error { 56 | - return fmt.Errorf("foo") 57 | + return errors.New("foo") 58 | } 59 | -------------------------------------------------------------------------------- /testdata/inline_err_assignments: -------------------------------------------------------------------------------- 1 | -- inline.patch -- 2 | @@ 3 | var f expression 4 | var err identifier 5 | @@ 6 | -err = f 7 | -if err != nil { 8 | +if err := f; err != nil { 9 | return ..., err 10 | } 11 | 12 | -- multiple.in.go -- 13 | package foo 14 | 15 | func bar() (*Result, error) { 16 | // Test. 17 | err = baz() 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &Result{}, nil 22 | } 23 | 24 | -- multiple.out.go -- 25 | package foo 26 | 27 | func bar() (*Result, error) { 28 | // Test. 29 | if err := baz(); err != nil { 30 | return nil, err 31 | } 32 | return &Result{}, nil 33 | } 34 | 35 | 36 | -- multiple.diff -- 37 | --- multiple.go 38 | +++ multiple.go 39 | @@ -2,8 +2,7 @@ 40 | 41 | func bar() (*Result, error) { 42 | // Test. 43 | - err = baz() 44 | - if err != nil { 45 | + if err := baz(); err != nil { 46 | return nil, err 47 | } 48 | return &Result{}, nil 49 | 50 | -- single.in.go -- 51 | package foo 52 | 53 | func bar() error { 54 | // Test. 55 | err = baz() 56 | if err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | -- single.out.go -- 63 | package foo 64 | 65 | func bar() error { 66 | // Test. 67 | if err := baz(); err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | 73 | -- single.diff -- 74 | --- single.go 75 | +++ single.go 76 | @@ -2,8 +2,7 @@ 77 | 78 | func bar() error { 79 | // Test. 80 | - err = baz() 81 | - if err != nil { 82 | + if err := baz(); err != nil { 83 | return err 84 | } 85 | return nil 86 | -------------------------------------------------------------------------------- /testdata/inline_errors: -------------------------------------------------------------------------------- 1 | Inlines error variable declarations into if statements where possible. 2 | 3 | -- inline_errors.patch -- 4 | @@ 5 | var f expression 6 | var err identifier 7 | @@ 8 | - err := f 9 | - if err != nil { 10 | + if err := f; err != nil { 11 | ... 12 | return ... 13 | } 14 | 15 | @@ 16 | var f expression 17 | var err identifier 18 | @@ 19 | - err := f 20 | - if err != nil { 21 | + if err := f; err != nil { 22 | ... 23 | return ... 24 | } else { 25 | ... 26 | } 27 | 28 | -- match.in.go -- 29 | package foo 30 | 31 | func bar() (Result, error) { 32 | // Test. 33 | err := baz() 34 | if err != nil { 35 | // Hello. 36 | log() 37 | return nil, err 38 | } 39 | return Result{}, nil 40 | } 41 | 42 | -- match.out.go -- 43 | package foo 44 | 45 | func bar() (Result, error) { 46 | // Test. 47 | if err := baz(); err != nil { 48 | // Hello. 49 | log() 50 | return nil, err 51 | } 52 | return Result{}, nil 53 | } 54 | 55 | -- match.diff -- 56 | --- match.go 57 | +++ match.go 58 | @@ -2,8 +2,7 @@ 59 | 60 | func bar() (Result, error) { 61 | // Test. 62 | - err := baz() 63 | - if err != nil { 64 | + if err := baz(); err != nil { 65 | // Hello. 66 | log() 67 | return nil, err 68 | 69 | -- no_match.in.go -- 70 | package foo 71 | 72 | func baz() error { 73 | // This should remain unchanged. 74 | x, err := unchanged() 75 | if err != nil { 76 | return err 77 | } 78 | print(x) 79 | return nil 80 | } 81 | 82 | -- no_match.out.go -- 83 | package foo 84 | 85 | func baz() error { 86 | // This should remain unchanged. 87 | x, err := unchanged() 88 | if err != nil { 89 | return err 90 | } 91 | print(x) 92 | return nil 93 | } 94 | 95 | -- else.in.go -- 96 | package foo 97 | 98 | func qux() { 99 | err := foo() 100 | if err != nil { 101 | return 102 | } else { 103 | err2 := quux() 104 | if err2 != nil { 105 | return 106 | } 107 | } 108 | } 109 | 110 | -- else.out.go -- 111 | package foo 112 | 113 | func qux() { 114 | if err := foo(); err != nil { 115 | return 116 | } else { 117 | if err2 := quux(); err2 != nil { 118 | return 119 | } 120 | } 121 | } 122 | 123 | -- else.diff -- 124 | --- else.go 125 | +++ else.go 126 | @@ -1,12 +1,10 @@ 127 | package foo 128 | 129 | func qux() { 130 | - err := foo() 131 | - if err != nil { 132 | + if err := foo(); err != nil { 133 | return 134 | } else { 135 | - err2 := quux() 136 | - if err2 != nil { 137 | + if err2 := quux(); err2 != nil { 138 | return 139 | } 140 | } 141 | 142 | -- case.in.go -- 143 | package foo 144 | 145 | func foo() error { 146 | switch bar() { 147 | case "x": 148 | err := baz() 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | -- case.out.go -- 157 | package foo 158 | 159 | func foo() error { 160 | switch bar() { 161 | case "x": 162 | if err := baz(); err != nil { 163 | return err 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | -- case.diff -- 170 | --- case.go 171 | +++ case.go 172 | @@ -3,8 +3,7 @@ 173 | func foo() error { 174 | switch bar() { 175 | case "x": 176 | - err := baz() 177 | - if err != nil { 178 | + if err := baz(); err != nil { 179 | return err 180 | } 181 | } 182 | 183 | -- select.in.go -- 184 | package foo 185 | 186 | func foo(ctx context.Context) error { 187 | select { 188 | case <-ctx.Done(): 189 | err := ctx.Err() 190 | if err != nil { 191 | return err 192 | } 193 | } 194 | return nil 195 | } 196 | 197 | -- select.out.go -- 198 | package foo 199 | 200 | func foo(ctx context.Context) error { 201 | select { 202 | case <-ctx.Done(): 203 | if err := ctx.Err(); err != nil { 204 | return err 205 | } 206 | } 207 | return nil 208 | } 209 | 210 | -- select.diff -- 211 | --- select.go 212 | +++ select.go 213 | @@ -3,8 +3,7 @@ 214 | func foo(ctx context.Context) error { 215 | select { 216 | case <-ctx.Done(): 217 | - err := ctx.Err() 218 | - if err != nil { 219 | + if err := ctx.Err(); err != nil { 220 | return err 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /testdata/match_named_import: -------------------------------------------------------------------------------- 1 | Match a named import verbatim. 2 | 3 | -- in.patch -- 4 | @@ 5 | var X identifier 6 | @@ 7 | -import foo "example.com/bar" 8 | +import bar "example.com/bar" 9 | 10 | -foo.X 11 | +bar.X 12 | 13 | -- unnamed.in.go -- 14 | package whatever 15 | 16 | import "example.com/bar" 17 | 18 | // Should remain untouched. 19 | func stuff() { 20 | foo.stuff() 21 | } 22 | 23 | -- unnamed.out.go -- 24 | package whatever 25 | 26 | import "example.com/bar" 27 | 28 | // Should remain untouched. 29 | func stuff() { 30 | foo.stuff() 31 | } 32 | 33 | 34 | -- named.in.go -- 35 | package whatever 36 | 37 | import foo "example.com/bar" 38 | 39 | func stuff() { 40 | foo.stuff() 41 | } 42 | 43 | -- named.out.go -- 44 | package whatever 45 | 46 | import bar "example.com/bar" 47 | 48 | func stuff() { 49 | bar.stuff() 50 | } 51 | 52 | -- named.diff -- 53 | --- named.go 54 | +++ named.go 55 | @@ -1,7 +1,7 @@ 56 | package whatever 57 | 58 | -import foo "example.com/bar" 59 | +import bar "example.com/bar" 60 | 61 | func stuff() { 62 | - foo.stuff() 63 | + bar.stuff() 64 | } 65 | -------------------------------------------------------------------------------- /testdata/matches_test_files: -------------------------------------------------------------------------------- 1 | Verifies that we match on and transform _test files. 2 | 3 | -- simple.patch -- 4 | @@ 5 | @@ 6 | -foo 7 | +bar 8 | 9 | -- foo.in.go -- 10 | package x 11 | 12 | func y() { 13 | foo() 14 | } 15 | 16 | -- foo.out.go -- 17 | package x 18 | 19 | func y() { 20 | bar() 21 | } 22 | 23 | -- foo.diff -- 24 | --- foo.go 25 | +++ foo.go 26 | @@ -1,5 +1,5 @@ 27 | package x 28 | 29 | func y() { 30 | - foo() 31 | + bar() 32 | } 33 | 34 | -- foo_test.in.go -- 35 | package x 36 | 37 | import "testing" 38 | 39 | func TestThing(t *testing.T) { 40 | foo() 41 | } 42 | 43 | -- foo_test.out.go -- 44 | package x 45 | 46 | import "testing" 47 | 48 | func TestThing(t *testing.T) { 49 | bar() 50 | } 51 | 52 | -- foo_test.diff -- 53 | --- foo_test.go 54 | +++ foo_test.go 55 | @@ -3,5 +3,5 @@ 56 | import "testing" 57 | 58 | func TestThing(t *testing.T) { 59 | - foo() 60 | + bar() 61 | } 62 | -------------------------------------------------------------------------------- /testdata/mismatched_dots: -------------------------------------------------------------------------------- 1 | -- unnamed.patch -- 2 | @@ 3 | @@ 4 | -func name(...) (error, ...) { 5 | +func name(...) (..., error) { 6 | - return nil, ... 7 | + return ..., nil 8 | } 9 | 10 | -- unnamed.in.go -- 11 | package a 12 | 13 | func name(foo string, bar int) (error, string) { 14 | return nil, "very valid go" 15 | } 16 | 17 | -- unnamed.out.go -- 18 | package a 19 | 20 | func name(foo string, bar int) (string, error) { 21 | return "very valid go", nil 22 | } -------------------------------------------------------------------------------- /testdata/name_and_rewrite_unnamed_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var x identifier 4 | @@ 5 | -import "example.com/foo-go.git" 6 | +import foo "example.com/foo" 7 | 8 | foo.x 9 | 10 | -- user.in.go -- 11 | package user 12 | 13 | import "example.com/foo-go.git" 14 | 15 | func stuff() { 16 | foo.Do() 17 | } 18 | 19 | -- user.out.go -- 20 | package user 21 | 22 | import foo "example.com/foo" 23 | 24 | func stuff() { 25 | foo.Do() 26 | } 27 | 28 | 29 | -- user.diff -- 30 | --- user.go 31 | +++ user.go 32 | @@ -1,6 +1,6 @@ 33 | package user 34 | 35 | -import "example.com/foo-go.git" 36 | +import foo "example.com/foo" 37 | 38 | func stuff() { 39 | foo.Do() 40 | -------------------------------------------------------------------------------- /testdata/name_unnamed_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var x identifier 4 | @@ 5 | -import "example.com/foo-go.git" 6 | +import foo "example.com/foo-go.git" 7 | 8 | foo.x 9 | 10 | -- user.in.go -- 11 | package user 12 | 13 | import "example.com/foo-go.git" 14 | 15 | func stuff() { 16 | foo.Do() 17 | } 18 | 19 | -- user.out.go -- 20 | package user 21 | 22 | import foo "example.com/foo-go.git" 23 | 24 | func stuff() { 25 | foo.Do() 26 | } 27 | 28 | -- user.diff -- 29 | --- user.go 30 | +++ user.go 31 | @@ -1,6 +1,6 @@ 32 | package user 33 | 34 | -import "example.com/foo-go.git" 35 | +import foo "example.com/foo-go.git" 36 | 37 | func stuff() { 38 | foo.Do() 39 | -------------------------------------------------------------------------------- /testdata/nil_safe_string: -------------------------------------------------------------------------------- 1 | TODO: Support a means of verifying that there isn't already a nil check. 2 | 3 | -- in.patch -- 4 | @@ 5 | var t identifier 6 | var T expression 7 | @@ 8 | func (t *T) String() string { 9 | + if t == nil { 10 | + return "" 11 | + } 12 | ... 13 | } 14 | 15 | -- foo.in.go -- 16 | package foo 17 | 18 | type Bar struct { 19 | Name string 20 | } 21 | 22 | func (b *Bar) String() string { 23 | return b.Name 24 | } 25 | 26 | -- foo.out.go -- 27 | package foo 28 | 29 | type Bar struct { 30 | Name string 31 | } 32 | 33 | func (b *Bar) String() string { 34 | if b == nil { 35 | return "" 36 | } 37 | return b.Name 38 | } 39 | 40 | -- foo.diff -- 41 | --- foo.go 42 | +++ foo.go 43 | @@ -5,5 +5,8 @@ 44 | } 45 | 46 | func (b *Bar) String() string { 47 | + if b == nil { 48 | + return "" 49 | + } 50 | return b.Name 51 | } 52 | -------------------------------------------------------------------------------- /testdata/noop_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var to identifier 4 | @@ 5 | import( 6 | - to "conversion/to.git" 7 | + "go.uber.org/thriftrw/ptr" 8 | ) 9 | 10 | - to.StrPtr(...) 11 | + ptr.String(...) 12 | 13 | @@ 14 | var to identifier 15 | @@ 16 | import( 17 | - to "conversion/to.git" 18 | + "go.uber.org/thriftrw/ptr" 19 | ) 20 | 21 | - to.BoolPtr(...) 22 | + ptr.Bool(...) 23 | 24 | -- remove_all.in.go -- 25 | package main 26 | 27 | import ( 28 | "conversion/to.git" 29 | ) 30 | 31 | func foo(s string) *bool { 32 | if to.StrPtr(s) == nil { 33 | return to.BoolPtr(false) 34 | } 35 | 36 | return to.BoolPtr(true) 37 | } 38 | 39 | -- remove_all.out.go -- 40 | package main 41 | 42 | import "go.uber.org/thriftrw/ptr" 43 | 44 | func foo(s string) *bool { 45 | if ptr.String(s) == nil { 46 | return ptr.Bool(false) 47 | } 48 | 49 | return ptr.Bool(true) 50 | } 51 | 52 | -- remove_all.diff -- 53 | --- remove_all.go 54 | +++ remove_all.go 55 | @@ -1,13 +1,11 @@ 56 | package main 57 | 58 | -import ( 59 | - "conversion/to.git" 60 | -) 61 | +import "go.uber.org/thriftrw/ptr" 62 | 63 | func foo(s string) *bool { 64 | - if to.StrPtr(s) == nil { 65 | - return to.BoolPtr(false) 66 | + if ptr.String(s) == nil { 67 | + return ptr.Bool(false) 68 | } 69 | 70 | - return to.BoolPtr(true) 71 | + return ptr.Bool(true) 72 | } 73 | 74 | -- remove_some.in.go -- 75 | package main 76 | 77 | import ( 78 | "fmt" 79 | 80 | "conversion/to.git" 81 | ) 82 | 83 | func foo(s string) *bool { 84 | fmt.Println("Hello World") 85 | if to.StrPtr(s) == nil { 86 | return to.BoolPtr(false) 87 | } 88 | 89 | if to.DecimalPtr(s) == nil { 90 | return to.BoolPtr(false) 91 | } 92 | 93 | return to.BoolPtr(true) 94 | } 95 | 96 | -- remove_some.out.go -- 97 | package main 98 | 99 | import ( 100 | "fmt" 101 | 102 | "conversion/to.git" 103 | 104 | "go.uber.org/thriftrw/ptr" 105 | ) 106 | 107 | func foo(s string) *bool { 108 | fmt.Println("Hello World") 109 | if ptr.String(s) == nil { 110 | return ptr.Bool(false) 111 | } 112 | 113 | if to.DecimalPtr(s) == nil { 114 | return ptr.Bool(false) 115 | } 116 | 117 | return ptr.Bool(true) 118 | } 119 | 120 | -- remove_some.diff -- 121 | --- remove_some.go 122 | +++ remove_some.go 123 | @@ -4,17 +4,19 @@ 124 | "fmt" 125 | 126 | "conversion/to.git" 127 | + 128 | + "go.uber.org/thriftrw/ptr" 129 | ) 130 | 131 | func foo(s string) *bool { 132 | fmt.Println("Hello World") 133 | - if to.StrPtr(s) == nil { 134 | - return to.BoolPtr(false) 135 | + if ptr.String(s) == nil { 136 | + return ptr.Bool(false) 137 | } 138 | 139 | if to.DecimalPtr(s) == nil { 140 | - return to.BoolPtr(false) 141 | + return ptr.Bool(false) 142 | } 143 | 144 | - return to.BoolPtr(true) 145 | + return ptr.Bool(true) 146 | } 147 | 148 | -- remove_some.groupimports -- 149 | --- remove_some.go 150 | +++ remove_some.go 151 | @@ -4,17 +4,18 @@ 152 | "fmt" 153 | 154 | "conversion/to.git" 155 | + "go.uber.org/thriftrw/ptr" 156 | ) 157 | 158 | func foo(s string) *bool { 159 | fmt.Println("Hello World") 160 | - if to.StrPtr(s) == nil { 161 | - return to.BoolPtr(false) 162 | + if ptr.String(s) == nil { 163 | + return ptr.Bool(false) 164 | } 165 | 166 | if to.DecimalPtr(s) == nil { 167 | - return to.BoolPtr(false) 168 | + return ptr.Bool(false) 169 | } 170 | 171 | - return to.BoolPtr(true) 172 | + return ptr.Bool(true) 173 | } 174 | 175 | -------------------------------------------------------------------------------- /testdata/optimize_string_appends: -------------------------------------------------------------------------------- 1 | Optimizes string appends. 2 | 3 | TODO: Use freshIdentifier to make sb unique. 4 | TODO: Use <... ...> instead of just optimizing for loops. 5 | 6 | -- sb.patch -- 7 | @@ 8 | var str identifier 9 | var s expression 10 | @@ 11 | +import "strings" 12 | { 13 | - var str string 14 | + var sb strings.Builder 15 | ... 16 | for ... { 17 | ... 18 | - str += s 19 | + sb.WriteString(s) 20 | ... 21 | } 22 | + str := sb.String() 23 | } 24 | 25 | -- loop.in.go -- 26 | package foo 27 | 28 | func run() string { 29 | var s string 30 | for i := range foo(true /* bar */) { 31 | x := foo() 32 | s += x 33 | log(x) 34 | } 35 | return s 36 | } 37 | 38 | -- loop.out.go -- 39 | package foo 40 | 41 | import "strings" 42 | 43 | func run() string { 44 | var sb strings.Builder 45 | for i := range foo(true /* bar */) { 46 | x := foo() 47 | sb.WriteString(x) 48 | log(x) 49 | } 50 | s := sb.String() 51 | 52 | return s 53 | } 54 | 55 | -- loop.diff -- 56 | --- loop.go 57 | +++ loop.go 58 | @@ -1,11 +1,15 @@ 59 | package foo 60 | 61 | +import "strings" 62 | + 63 | func run() string { 64 | - var s string 65 | + var sb strings.Builder 66 | for i := range foo(true /* bar */) { 67 | x := foo() 68 | - s += x 69 | + sb.WriteString(x) 70 | log(x) 71 | } 72 | + s := sb.String() 73 | + 74 | return s 75 | } 76 | -------------------------------------------------------------------------------- /testdata/patch/error.patch: -------------------------------------------------------------------------------- 1 | # Replace unnecessary fmt.Sprintf with fmt.Errorf 2 | @@ 3 | # comments 5 - 4 | # comments 6 - 5 | @@ 6 | # comments 7 - 7 | -import "errors" 8 | -errors.New(fmt.Sprintf(...)) 9 | +fmt.Errorf(...) 10 | # comments 8 - 11 | -------------------------------------------------------------------------------- /testdata/patch/replace_to_with_ptr.patch: -------------------------------------------------------------------------------- 1 | @@ 2 | var to identifier 3 | @@ 4 | import( 5 | - to "conversion/to.git" 6 | + "go.uber.org/thriftrw/ptr" 7 | ) 8 | 9 | - to.StrPtr(...) 10 | + ptr.String(...) 11 | 12 | @@ 13 | var to identifier 14 | @@ 15 | import( 16 | - to "conversion/to.git" 17 | + "go.uber.org/thriftrw/ptr" 18 | ) 19 | 20 | - to.BoolPtr(...) 21 | + ptr.Bool(...) 22 | -------------------------------------------------------------------------------- /testdata/patch/time.patch: -------------------------------------------------------------------------------- 1 | # Replace time.Now().Sub() with time.Since() 2 | @@ 3 | # comments 11 - 4 | # comments 12 - 5 | var x identifier 6 | @@ 7 | -time.Now().Sub(x) 8 | +time.Since(x) 9 | # comments 11 - 10 | # comments 12 - 11 | -------------------------------------------------------------------------------- /testdata/range_value_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | for ... := range foo() { 5 | - x() 6 | + y() 7 | } 8 | 9 | -- no_params.in.go -- 10 | package foo 11 | 12 | func foo() { 13 | for range foo() { 14 | x() 15 | } 16 | } 17 | 18 | -- no_params.out.go -- 19 | package foo 20 | 21 | func foo() { 22 | for range foo() { 23 | y() 24 | } 25 | } 26 | 27 | -- one_param.in.go -- 28 | package foo 29 | 30 | func foo() { 31 | for i := range foo() { 32 | x() 33 | } 34 | } 35 | 36 | -- one_param.out.go -- 37 | package foo 38 | 39 | func foo() { 40 | for i := range foo() { 41 | y() 42 | } 43 | } 44 | 45 | -- two_params.in.go -- 46 | package foo 47 | 48 | func foo() { 49 | for i, a := range foo() { 50 | x() 51 | } 52 | } 53 | 54 | -- two_params.out.go -- 55 | package foo 56 | 57 | func foo() { 58 | for i, a := range foo() { 59 | y() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /testdata/recognize_all_imports: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var foo, x identifier 4 | @@ 5 | -import foo "example.com/foo-go.git" 6 | +import foo "example.com/foo.git" 7 | 8 | foo.x 9 | 10 | -- unnamed.in.go -- 11 | package whatever 12 | 13 | import "example.com/foo-go.git" 14 | 15 | func foo() { 16 | foo.X() 17 | } 18 | 19 | -- unnamed.out.go -- 20 | package whatever 21 | 22 | import "example.com/foo.git" 23 | 24 | func foo() { 25 | foo.X() 26 | } 27 | 28 | -- unnamed.diff -- 29 | --- unnamed.go 30 | +++ unnamed.go 31 | @@ -1,6 +1,6 @@ 32 | package whatever 33 | 34 | -import "example.com/foo-go.git" 35 | +import "example.com/foo.git" 36 | 37 | func foo() { 38 | foo.X() 39 | 40 | -- named.in.go -- 41 | package whatever 42 | 43 | import bar "example.com/foo-go.git" 44 | 45 | func foo() { 46 | bar.X() 47 | } 48 | 49 | -- named.out.go -- 50 | package whatever 51 | 52 | import bar "example.com/foo.git" 53 | 54 | func foo() { 55 | bar.X() 56 | } 57 | 58 | -- named.diff -- 59 | --- named.go 60 | +++ named.go 61 | @@ -1,6 +1,6 @@ 62 | package whatever 63 | 64 | -import bar "example.com/foo-go.git" 65 | +import bar "example.com/foo.git" 66 | 67 | func foo() { 68 | bar.X() 69 | -------------------------------------------------------------------------------- /testdata/redundant_fmt_errorf: -------------------------------------------------------------------------------- 1 | Replaces redundant use of fmt.Errorf with errors.New. 2 | 3 | // TODO(abg): Collapse Sprintf into Errorf. 4 | 5 | -- errorf.patch -- 6 | @@ 7 | var s expression 8 | @@ 9 | -import "fmt" 10 | +import "errors" 11 | 12 | -fmt.Errorf(s) 13 | +errors.New(s) 14 | 15 | -- replace.in.go -- 16 | package foo 17 | 18 | import "fmt" 19 | 20 | func Do() error { 21 | return fmt.Errorf("great sadness") 22 | } 23 | 24 | -- replace.out.go -- 25 | package foo 26 | 27 | import "errors" 28 | 29 | func Do() error { 30 | return errors.New("great sadness") 31 | } 32 | 33 | -- replace.diff -- 34 | --- replace.go 35 | +++ replace.go 36 | @@ -1,7 +1,7 @@ 37 | package foo 38 | 39 | -import "fmt" 40 | +import "errors" 41 | 42 | func Do() error { 43 | - return fmt.Errorf("great sadness") 44 | + return errors.New("great sadness") 45 | } 46 | 47 | -- leave_sprintf.in.go -- 48 | package bar 49 | 50 | import "fmt" 51 | 52 | const _thing = "thing" 53 | 54 | func Do() error { 55 | return fmt.Errorf(fmt.Sprintf("%v failed", _thing)) 56 | } 57 | 58 | -- leave_sprintf.out.go -- 59 | package bar 60 | 61 | import ( 62 | "errors" 63 | "fmt" 64 | ) 65 | 66 | const _thing = "thing" 67 | 68 | func Do() error { 69 | return errors.New(fmt.Sprintf("%v failed", _thing)) 70 | } 71 | 72 | -- leave_sprintf.diff -- 73 | --- leave_sprintf.go 74 | +++ leave_sprintf.go 75 | @@ -1,9 +1,12 @@ 76 | package bar 77 | 78 | -import "fmt" 79 | +import ( 80 | + "errors" 81 | + "fmt" 82 | +) 83 | 84 | const _thing = "thing" 85 | 86 | func Do() error { 87 | - return fmt.Errorf(fmt.Sprintf("%v failed", _thing)) 88 | + return errors.New(fmt.Sprintf("%v failed", _thing)) 89 | } 90 | -------------------------------------------------------------------------------- /testdata/redundant_fmt_sprintf: -------------------------------------------------------------------------------- 1 | -- redundant_fmt_sprintf.patch -- 2 | # Replace errors.New(fmt.Sprintf()) with fmt.Errorf() 3 | @@ 4 | # comments 5 - 5 | # comments 6 - 6 | @@ 7 | # comments 7 - 8 | -import "errors" 9 | -errors.New(fmt.Sprintf(...)) 10 | +fmt.Errorf(...) 11 | 12 | -- error.in.go -- 13 | package patch_examples 14 | 15 | import ( 16 | "errors" 17 | "fmt" 18 | ) 19 | 20 | func boo() error { 21 | err := errors.New("test") 22 | return errors.New(fmt.Sprintf("error: %v", err)) 23 | } 24 | 25 | func main() { 26 | fmt.Println(boo()) 27 | } 28 | 29 | -- error.out.go -- 30 | package patch_examples 31 | 32 | import ( 33 | "errors" 34 | "fmt" 35 | ) 36 | 37 | func boo() error { 38 | err := errors.New("test") 39 | return fmt.Errorf("error: %v", err) 40 | } 41 | 42 | func main() { 43 | fmt.Println(boo()) 44 | } 45 | 46 | -- error.diff -- 47 | --- error.go 48 | +++ error.go 49 | @@ -7,7 +7,7 @@ 50 | 51 | func boo() error { 52 | err := errors.New("test") 53 | - return errors.New(fmt.Sprintf("error: %v", err)) 54 | + return fmt.Errorf("error: %v", err) 55 | } 56 | 57 | func main() { 58 | 59 | -- error.diff.stderr -- 60 | error.go:Replace errors.New(fmt.Sprintf()) with fmt.Errorf() 61 | -------------------------------------------------------------------------------- /testdata/remove_context_field: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var Ctx identifier 4 | @@ 5 | type Request struct { 6 | ... 7 | - Ctx context.Context 8 | ... 9 | } 10 | 11 | -- alone.in.go -- 12 | package alone 13 | 14 | type Request struct { 15 | Context context.Context 16 | } 17 | 18 | -- alone.out.go -- 19 | package alone 20 | 21 | type Request struct { 22 | } 23 | 24 | -- alone.diff -- 25 | --- alone.go 26 | +++ alone.go 27 | @@ -1,5 +1,4 @@ 28 | package alone 29 | 30 | type Request struct { 31 | - Context context.Context 32 | } 33 | 34 | -- triple.in.go -- 35 | package triple 36 | 37 | type Request struct { 38 | User string 39 | Ctxt context.Context 40 | Time time.Duration 41 | } 42 | 43 | -- triple.out.go -- 44 | package triple 45 | 46 | type Request struct { 47 | User string 48 | 49 | Time time.Duration 50 | } 51 | 52 | -- triple.diff -- 53 | --- triple.go 54 | +++ triple.go 55 | @@ -2,6 +2,6 @@ 56 | 57 | type Request struct { 58 | User string 59 | - Ctxt context.Context 60 | + 61 | Time time.Duration 62 | } 63 | -------------------------------------------------------------------------------- /testdata/rename_named_import: -------------------------------------------------------------------------------- 1 | -- foo.patch -- 2 | @@ 3 | var X identifier 4 | @@ 5 | -import foo "bar" 6 | +import baz "bar" 7 | 8 | -foo.X 9 | +baz.X 10 | 11 | -- a.in.go -- 12 | package a 13 | 14 | import foo "bar" 15 | 16 | func stuff() { 17 | var a foo.Bar 18 | foo.Init(&a) 19 | } 20 | 21 | -- a.out.go -- 22 | package a 23 | 24 | import baz "bar" 25 | 26 | func stuff() { 27 | var a baz.Bar 28 | baz.Init(&a) 29 | } 30 | 31 | -- a.diff -- 32 | --- a.go 33 | +++ a.go 34 | @@ -1,8 +1,8 @@ 35 | package a 36 | 37 | -import foo "bar" 38 | +import baz "bar" 39 | 40 | func stuff() { 41 | - var a foo.Bar 42 | - foo.Init(&a) 43 | + var a baz.Bar 44 | + baz.Init(&a) 45 | } 46 | -------------------------------------------------------------------------------- /testdata/rename_package: -------------------------------------------------------------------------------- 1 | TODO: Delete Foo() once we support pgo.File without Nodes. 2 | 3 | -- rename.patch -- 4 | @@ 5 | @@ 6 | -package foo 7 | +package bar 8 | 9 | Foo() 10 | 11 | -- foo.in.go -- 12 | package foo 13 | 14 | import "fmt" 15 | 16 | func main() { 17 | Foo() 18 | } 19 | 20 | -- foo.out.go -- 21 | package bar 22 | 23 | import "fmt" 24 | 25 | func main() { 26 | Foo() 27 | } 28 | 29 | -- foo.diff -- 30 | --- foo.go 31 | +++ foo.go 32 | @@ -1,4 +1,4 @@ 33 | -package foo 34 | +package bar 35 | 36 | import "fmt" 37 | 38 | -------------------------------------------------------------------------------- /testdata/rename_unnamed_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var x identifier 4 | @@ 5 | 6 | -import "github.com/fake/lib" 7 | +import "github.com/totallyreal/lib" 8 | 9 | lib.x 10 | 11 | -- unnamed.in.go -- 12 | package main 13 | 14 | import "github.com/fake/lib" 15 | 16 | func foo() { 17 | lib.Bar() 18 | } 19 | 20 | -- unnamed.out.go -- 21 | package main 22 | 23 | import "github.com/totallyreal/lib" 24 | 25 | func foo() { 26 | lib.Bar() 27 | } 28 | 29 | -- unnamed.diff -- 30 | --- unnamed.go 31 | +++ unnamed.go 32 | @@ -1,6 +1,6 @@ 33 | package main 34 | 35 | -import "github.com/fake/lib" 36 | +import "github.com/totallyreal/lib" 37 | 38 | func foo() { 39 | lib.Bar() 40 | -------------------------------------------------------------------------------- /testdata/replace_any_import_with_unnamed: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var fooclient identifier 4 | @@ 5 | -import fooclient "example.com/foo/client" 6 | +import "example.com/foo-client/compat" 7 | 8 | -fooclient.Init() 9 | +compat.Init() 10 | 11 | -- unnamed.in.go -- 12 | package main 13 | 14 | import "example.com/foo/client" 15 | 16 | func main() { 17 | fooclient.Init() 18 | } 19 | 20 | -- unnamed.out.go -- 21 | package main 22 | 23 | import "example.com/foo-client/compat" 24 | 25 | func main() { 26 | compat.Init() 27 | } 28 | 29 | -- unnamed.diff -- 30 | --- unnamed.go 31 | +++ unnamed.go 32 | @@ -1,7 +1,7 @@ 33 | package main 34 | 35 | -import "example.com/foo/client" 36 | +import "example.com/foo-client/compat" 37 | 38 | func main() { 39 | - fooclient.Init() 40 | + compat.Init() 41 | } 42 | 43 | -- named.in.go -- 44 | package main 45 | 46 | import client "example.com/foo/client" 47 | 48 | func main() { 49 | client.Init() 50 | } 51 | 52 | -- named.out.go -- 53 | package main 54 | 55 | import "example.com/foo-client/compat" 56 | 57 | func main() { 58 | compat.Init() 59 | } 60 | 61 | -- named.diff -- 62 | --- named.go 63 | +++ named.go 64 | @@ -1,7 +1,7 @@ 65 | package main 66 | 67 | -import client "example.com/foo/client" 68 | +import "example.com/foo-client/compat" 69 | 70 | func main() { 71 | - client.Init() 72 | + compat.Init() 73 | } 74 | -------------------------------------------------------------------------------- /testdata/replace_import_with_top_level_comment: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var randomIdentifier identifier 4 | @@ 5 | -import "cmd/internal/edit" 6 | +import "github.com/foo/bar/internal/edit" 7 | 8 | edit.randomIdentifier 9 | 10 | -- comment.in.go -- 11 | // Copyright 2022 ... 12 | 13 | package main 14 | 15 | import ( 16 | "cmd/internal/edit" 17 | ) 18 | 19 | func main() { 20 | _ = edit.Buffer 21 | } 22 | 23 | -- comment.out.go -- 24 | // Copyright 2022 ... 25 | 26 | package main 27 | 28 | import "github.com/foo/bar/internal/edit" 29 | 30 | func main() { 31 | _ = edit.Buffer 32 | } 33 | 34 | -- comment.diff -- 35 | --- comment.go 36 | +++ comment.go 37 | @@ -2,9 +2,7 @@ 38 | 39 | package main 40 | 41 | -import ( 42 | - "cmd/internal/edit" 43 | -) 44 | +import "github.com/foo/bar/internal/edit" 45 | 46 | func main() { 47 | _ = edit.Buffer 48 | -------------------------------------------------------------------------------- /testdata/replace_net_context: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var context identifier 4 | @@ 5 | -import context "golang.org/x/net/context" 6 | +import context "context" 7 | 8 | context 9 | 10 | -- top_level.in.go -- 11 | package foo 12 | 13 | import "golang.org/x/net/context" 14 | 15 | func x() { 16 | context.Background() 17 | } 18 | 19 | -- top_level.out.go -- 20 | package foo 21 | 22 | import "context" 23 | 24 | func x() { 25 | context.Background() 26 | } 27 | 28 | -- top_level.diff -- 29 | --- top_level.go 30 | +++ top_level.go 31 | @@ -1,6 +1,6 @@ 32 | package foo 33 | 34 | -import "golang.org/x/net/context" 35 | +import "context" 36 | 37 | func x() { 38 | context.Background() 39 | 40 | -- import_group.in.go -- 41 | package foo 42 | 43 | import ( 44 | "time" 45 | 46 | "golang.org/x/net/context" 47 | ) 48 | 49 | func x() { 50 | context.WithTimeout(context.Background(), time.Second) 51 | } 52 | 53 | -- import_group.out.go -- 54 | package foo 55 | 56 | import ( 57 | "context" 58 | "time" 59 | ) 60 | 61 | func x() { 62 | context.WithTimeout(context.Background(), time.Second) 63 | } 64 | 65 | -- import_group.diff -- 66 | --- import_group.go 67 | +++ import_group.go 68 | @@ -1,9 +1,8 @@ 69 | package foo 70 | 71 | import ( 72 | + "context" 73 | "time" 74 | - 75 | - "golang.org/x/net/context" 76 | ) 77 | 78 | func x() { 79 | -------------------------------------------------------------------------------- /testdata/replace_with_time_since: -------------------------------------------------------------------------------- 1 | -- replace_with_time_since.patch -- 2 | # Replace time.Now().Sub() with time.Since() 3 | @@ 4 | # comments 11 - 5 | # comments 12 - 6 | var x identifier 7 | @@ 8 | -time.Now().Sub(x) 9 | +time.Since(x) 10 | # comments 11 - 11 | # comments 12 - 12 | 13 | -- time.in.go -- 14 | package patch_examples 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | ) 20 | 21 | func main() { 22 | startOfYear := time.Date(2021, 01, 01, 0, 0, 0, 0, time.UTC) 23 | result := time.Now().Sub(startOfYear) 24 | fmt.Println(result) 25 | } 26 | 27 | -- time.out.go -- 28 | package patch_examples 29 | 30 | import ( 31 | "fmt" 32 | "time" 33 | ) 34 | 35 | func main() { 36 | startOfYear := time.Date(2021, 01, 01, 0, 0, 0, 0, time.UTC) 37 | result := time.Since(startOfYear) 38 | fmt.Println(result) 39 | } 40 | 41 | -- time.diff -- 42 | --- time.go 43 | +++ time.go 44 | @@ -7,6 +7,6 @@ 45 | 46 | func main() { 47 | startOfYear := time.Date(2021, 01, 01, 0, 0, 0, 0, time.UTC) 48 | - result := time.Now().Sub(startOfYear) 49 | + result := time.Since(startOfYear) 50 | fmt.Println(result) 51 | } 52 | 53 | -- time.diff.stderr -- 54 | time.go:Replace time.Now().Sub() with time.Since() 55 | -------------------------------------------------------------------------------- /testdata/return_err: -------------------------------------------------------------------------------- 1 | -- err.patch -- 2 | @@ 3 | var f identifier 4 | @@ 5 | -func f() { 6 | +func f() error { 7 | + return nil 8 | } 9 | 10 | -- foo.in.go -- 11 | package foo 12 | 13 | func bar() { 14 | } 15 | 16 | -- foo.out.go -- 17 | package foo 18 | 19 | func bar() error { 20 | return nil 21 | } 22 | 23 | -- foo.diff -- 24 | --- foo.go 25 | +++ foo.go 26 | @@ -1,4 +1,5 @@ 27 | package foo 28 | 29 | -func bar() { 30 | +func bar() error { 31 | + return nil 32 | } 33 | -------------------------------------------------------------------------------- /testdata/rewrite_import_paths: -------------------------------------------------------------------------------- 1 | -- foo.patch -- 2 | @@ 3 | var fooclient, X identifier 4 | @@ 5 | -import fooclient "foo-go/client" 6 | +import fooclient "foo-client" 7 | 8 | fooclient.X 9 | 10 | -- has_same_named_import.in.go -- 11 | package bar 12 | 13 | import fooclient "foo-go/client" 14 | 15 | func x() { 16 | var client fooclient.Interface 17 | client = fooclient.New() 18 | client.SendRequest(fooclient.NewRequest()) 19 | } 20 | 21 | -- has_same_named_import.out.go -- 22 | package bar 23 | 24 | import fooclient "foo-client" 25 | 26 | func x() { 27 | var client fooclient.Interface 28 | client = fooclient.New() 29 | client.SendRequest(fooclient.NewRequest()) 30 | } 31 | 32 | -- has_same_named_import.diff -- 33 | --- has_same_named_import.go 34 | +++ has_same_named_import.go 35 | @@ -1,6 +1,6 @@ 36 | package bar 37 | 38 | -import fooclient "foo-go/client" 39 | +import fooclient "foo-client" 40 | 41 | func x() { 42 | var client fooclient.Interface 43 | 44 | -- has_different_named_import.in.go -- 45 | package baz 46 | 47 | import client "foo-go/client" 48 | 49 | func y() { 50 | c := client.New() 51 | c.SendRequest(client.NewRequest()) 52 | } 53 | 54 | -- has_different_named_import.out.go -- 55 | package baz 56 | 57 | import client "foo-client" 58 | 59 | func y() { 60 | c := client.New() 61 | c.SendRequest(client.NewRequest()) 62 | } 63 | 64 | -- has_different_named_import.diff -- 65 | --- has_different_named_import.go 66 | +++ has_different_named_import.go 67 | @@ -1,6 +1,6 @@ 68 | package baz 69 | 70 | -import client "foo-go/client" 71 | +import client "foo-client" 72 | 73 | func y() { 74 | c := client.New() 75 | 76 | -- no_named_import.in.go -- 77 | package baz 78 | 79 | import "foo-go/client" 80 | 81 | func z() { 82 | fooclient.NewRequest() 83 | } 84 | 85 | -- no_named_import.out.go -- 86 | package baz 87 | 88 | import "foo-client" 89 | 90 | func z() { 91 | fooclient.NewRequest() 92 | } 93 | 94 | -- no_named_import.diff -- 95 | --- no_named_import.go 96 | +++ no_named_import.go 97 | @@ -1,6 +1,6 @@ 98 | package baz 99 | 100 | -import "foo-go/client" 101 | +import "foo-client" 102 | 103 | func z() { 104 | fooclient.NewRequest() 105 | -------------------------------------------------------------------------------- /testdata/s1012: -------------------------------------------------------------------------------- 1 | Resolves https://staticcheck.io/docs/checks#S1012. 2 | 3 | -- s1012.patch -- 4 | => examples/s1012.patch 5 | 6 | -- example.in.go -- 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "time" 13 | ) 14 | 15 | func main() { 16 | start := time.Now() 17 | 18 | if err := realLogic(); err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | fmt.Println("elapsed time:", time.Now().Sub(start)) 23 | } 24 | 25 | -- example.out.go -- 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "log" 31 | "time" 32 | ) 33 | 34 | func main() { 35 | start := time.Now() 36 | 37 | if err := realLogic(); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | fmt.Println("elapsed time:", time.Since(start)) 42 | } 43 | 44 | -- example.diff -- 45 | --- example.go 46 | +++ example.go 47 | @@ -13,5 +13,5 @@ 48 | log.Fatal(err) 49 | } 50 | 51 | - fmt.Println("elapsed time:", time.Now().Sub(start)) 52 | + fmt.Println("elapsed time:", time.Since(start)) 53 | } 54 | -------------------------------------------------------------------------------- /testdata/s1028: -------------------------------------------------------------------------------- 1 | Resolves https://staticcheck.io/docs/checks#S1028. 2 | 3 | -- s1028.patch -- 4 | => examples/s1028.patch 5 | 6 | -- foo.in.go -- 7 | package foo 8 | 9 | import ( 10 | "fmt" 11 | "errors" 12 | ) 13 | 14 | func bar(i int) error { 15 | return errors.New(fmt.Sprintf("great sadness: %d", i)) 16 | } 17 | 18 | -- foo.out.go -- 19 | package foo 20 | 21 | import "fmt" 22 | 23 | func bar(i int) error { 24 | return fmt.Errorf("great sadness: %d", i) 25 | } 26 | 27 | -- foo.diff -- 28 | --- foo.go 29 | +++ foo.go 30 | @@ -1,10 +1,7 @@ 31 | package foo 32 | 33 | -import ( 34 | - "fmt" 35 | - "errors" 36 | -) 37 | +import "fmt" 38 | 39 | func bar(i int) error { 40 | - return errors.New(fmt.Sprintf("great sadness: %d", i)) 41 | + return fmt.Errorf("great sadness: %d", i) 42 | } 43 | -------------------------------------------------------------------------------- /testdata/s1038: -------------------------------------------------------------------------------- 1 | Resolves https://staticcheck.io/docs/checks#S1038. 2 | 3 | -- s1038.patch -- 4 | => examples/s1038.patch 5 | 6 | -- example.in.go -- 7 | package thing 8 | 9 | import "fmt" 10 | 11 | func foo() { 12 | fmt.Print(fmt.Sprintf("thing happend: %v", thing())) 13 | } 14 | 15 | -- example.out.go -- 16 | package thing 17 | 18 | import "fmt" 19 | 20 | func foo() { 21 | fmt.Printf("thing happend: %v", thing()) 22 | } 23 | 24 | -- example.diff -- 25 | --- example.go 26 | +++ example.go 27 | @@ -3,5 +3,5 @@ 28 | import "fmt" 29 | 30 | func foo() { 31 | - fmt.Print(fmt.Sprintf("thing happend: %v", thing())) 32 | + fmt.Printf("thing happend: %v", thing()) 33 | } 34 | -------------------------------------------------------------------------------- /testdata/select_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | select { 5 | ... 6 | -case <-done: 7 | +case <-ctx.Done(): 8 | ... 9 | } 10 | 11 | -- example.in.go -- 12 | package x 13 | 14 | func foo(ctx context.Context) { 15 | select { 16 | case <-foo: 17 | fmt.Println("foo") 18 | case <-done: 19 | fmt.Println("done") 20 | } 21 | } 22 | 23 | -- example.out.go -- 24 | package x 25 | 26 | func foo(ctx context.Context) { 27 | select { 28 | case <-foo: 29 | fmt.Println("foo") 30 | case <-ctx.Done(): 31 | fmt.Println("done") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata/slice_and_import: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | +import "example.com/constants" 5 | 6 | []string{ 7 | ..., 8 | - "foo", 9 | + constants.Foo, 10 | ..., 11 | } 12 | 13 | -- foo.in.go -- 14 | package foo 15 | 16 | func items() []string { 17 | return []string{"a", "b", "foo", "bar", "d"} 18 | } 19 | 20 | -- foo.out.go -- 21 | package foo 22 | 23 | import "example.com/constants" 24 | 25 | func items() []string { 26 | return []string{"a", "b", constants.Foo, "bar", "d"} 27 | } 28 | 29 | -- foo.diff -- 30 | --- foo.go 31 | +++ foo.go 32 | @@ -1,5 +1,7 @@ 33 | package foo 34 | 35 | +import "example.com/constants" 36 | + 37 | func items() []string { 38 | - return []string{"a", "b", "foo", "bar", "d"} 39 | + return []string{"a", "b", constants.Foo, "bar", "d"} 40 | } 41 | -------------------------------------------------------------------------------- /testdata/stmt_to_expr: -------------------------------------------------------------------------------- 1 | -- foo.patch -- 2 | @@ 3 | @@ 4 | -x := foo() 5 | +bar() 6 | 7 | -- a.in.go -- 8 | package a 9 | 10 | func b() { 11 | x := foo() 12 | } 13 | 14 | -- a.out.go -- 15 | package a 16 | 17 | func b() { 18 | bar() 19 | } 20 | 21 | -- a.diff -- 22 | --- a.go 23 | +++ a.go 24 | @@ -1,5 +1,5 @@ 25 | package a 26 | 27 | func b() { 28 | - x := foo() 29 | + bar() 30 | } 31 | -------------------------------------------------------------------------------- /testdata/string_repeat: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var i, s identifier 4 | var n expression 5 | @@ 6 | +import "strings" 7 | 8 | { 9 | -var s string 10 | -for i := 0; i < n; i++ { 11 | - s += "x" 12 | -} 13 | +s := strings.Repeat("x", n) 14 | } 15 | 16 | -- buffer.in.go -- 17 | package foo 18 | 19 | func do() { 20 | var s string 21 | for n := 0; n < getCount(); n++ { 22 | s += "x" 23 | } 24 | } 25 | 26 | -- buffer.out.go -- 27 | package foo 28 | 29 | import "strings" 30 | 31 | func do() { 32 | s := strings.Repeat("x", getCount()) 33 | } 34 | 35 | 36 | -- buffer.diff -- 37 | --- buffer.go 38 | +++ buffer.go 39 | @@ -1,8 +1,7 @@ 40 | package foo 41 | 42 | +import "strings" 43 | + 44 | func do() { 45 | - var s string 46 | - for n := 0; n < getCount(); n++ { 47 | - s += "x" 48 | - } 49 | + s := strings.Repeat("x", getCount()) 50 | } 51 | -------------------------------------------------------------------------------- /testdata/struct_decl_field_rename: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | type User struct { 5 | ... 6 | - UserName string 7 | + Name string 8 | ... 9 | } 10 | 11 | -- middle.in.go -- 12 | package foo 13 | 14 | type User struct { 15 | Email string 16 | UserName string 17 | ID UUID 18 | } 19 | 20 | -- middle.out.go -- 21 | package foo 22 | 23 | type User struct { 24 | Email string 25 | Name string 26 | ID UUID 27 | } 28 | 29 | -- middle.diff -- 30 | --- middle.go 31 | +++ middle.go 32 | @@ -1,7 +1,7 @@ 33 | package foo 34 | 35 | type User struct { 36 | - Email string 37 | - UserName string 38 | - ID UUID 39 | + Email string 40 | + Name string 41 | + ID UUID 42 | } 43 | -------------------------------------------------------------------------------- /testdata/struct_field_list: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | type Request struct { 5 | - FirstName, ..., LastName string 6 | + FirstName, ..., LastName Name 7 | } 8 | 9 | -- zero.in.go -- 10 | packager zero 11 | 12 | type Request struct { 13 | FirstName, LastName string 14 | } 15 | 16 | 17 | -- zero.out.go -- 18 | packager zero 19 | 20 | type Request struct { 21 | FirstName, LastName Name 22 | } 23 | 24 | -- middle.in.go -- 25 | packager middle 26 | 27 | type Request struct { 28 | FirstName, MiddleName, LastName string 29 | } 30 | 31 | -- middle.out.go -- 32 | packager middle 33 | 34 | type Request struct { 35 | FirstName, MiddleName, LastName Name 36 | } 37 | -------------------------------------------------------------------------------- /testdata/struct_field_pair: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var A, B identifier 4 | var Type expression 5 | @@ 6 | type Config struct { 7 | - A Type 8 | - B Type 9 | + A, B Type 10 | } 11 | 12 | -- config.in.go -- 13 | package main 14 | 15 | type Config struct { 16 | Mods []string 17 | Admins []string 18 | } 19 | 20 | -- config.out.go -- 21 | package main 22 | 23 | type Config struct { 24 | Mods, Admins []string 25 | } 26 | 27 | -- config.diff -- 28 | --- config.go 29 | +++ config.go 30 | @@ -1,6 +1,5 @@ 31 | package main 32 | 33 | type Config struct { 34 | - Mods []string 35 | - Admins []string 36 | + Mods, Admins []string 37 | } 38 | -------------------------------------------------------------------------------- /testdata/struct_init_field_rename: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var name expression 4 | @@ 5 | User{ 6 | ..., 7 | - Name: name, 8 | + ID: name, 9 | ..., 10 | } 11 | 12 | -- first.in.go -- 13 | package foo 14 | 15 | func thing() { 16 | do(User{ 17 | Name: "foo", 18 | Email: "foo@example.com", 19 | }) 20 | } 21 | 22 | -- first.out.go -- 23 | package foo 24 | 25 | func thing() { 26 | do(User{ 27 | ID: "foo", 28 | Email: "foo@example.com", 29 | }) 30 | } 31 | 32 | -- first.diff -- 33 | --- first.go 34 | +++ first.go 35 | @@ -2,7 +2,7 @@ 36 | 37 | func thing() { 38 | do(User{ 39 | - Name: "foo", 40 | + ID: "foo", 41 | Email: "foo@example.com", 42 | }) 43 | } 44 | 45 | -- middle.in.go -- 46 | package foo 47 | 48 | func thing() { 49 | do(User{ 50 | Role: Moderator, 51 | Name: "foo", 52 | Email: "foo@example.com", 53 | }) 54 | } 55 | 56 | -- middle.out.go -- 57 | package foo 58 | 59 | func thing() { 60 | do(User{ 61 | Role: Moderator, 62 | ID: "foo", 63 | Email: "foo@example.com", 64 | }) 65 | } 66 | 67 | -- middle.diff -- 68 | --- middle.go 69 | +++ middle.go 70 | @@ -3,7 +3,7 @@ 71 | func thing() { 72 | do(User{ 73 | Role: Moderator, 74 | - Name: "foo", 75 | + ID: "foo", 76 | Email: "foo@example.com", 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /testdata/switch_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var v expression 4 | @@ 5 | switch v { 6 | ... 7 | -case MyItem: 8 | +case NewItem: 9 | ... 10 | } 11 | 12 | -- body.in.go -- 13 | package x 14 | 15 | func foo() { 16 | switch something() { 17 | case Foo: 18 | fmt.Println("foo") 19 | case MyItem: 20 | fmt.Println("my item") 21 | default: 22 | fmt.Println("unknown") 23 | } 24 | } 25 | 26 | -- body.out.go -- 27 | package x 28 | 29 | func foo() { 30 | switch something() { 31 | case Foo: 32 | fmt.Println("foo") 33 | case NewItem: 34 | fmt.Println("my item") 35 | default: 36 | fmt.Println("unknown") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /testdata/test_files/diff_example/error.go: -------------------------------------------------------------------------------- 1 | package diff_example 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("test") 10 | return errors.New(fmt.Sprintf("error: %v", err)) 11 | } 12 | 13 | func main() { 14 | fmt.Println(foo()) 15 | } 16 | -------------------------------------------------------------------------------- /testdata/test_files/lint_example/time.go: -------------------------------------------------------------------------------- 1 | package lint_example 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | startOfYear := time.Date(2021, 0o1, 0o1, 0, 0, 0, 0, time.UTC) 10 | result := time.Now().Sub(startOfYear) 11 | fmt.Println(result) 12 | } 13 | -------------------------------------------------------------------------------- /testdata/test_files/skip_generated_files/simple_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by tool 2 | // @generated 3 | package lint_example 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func foo() { 11 | year := time.Date(2024, 0o1, 0o1, 0, 1, 2, 3, time.UTC) 12 | fmt.Println(time.Since(year)) 13 | } 14 | -------------------------------------------------------------------------------- /testdata/test_files/skip_generated_files/special_notation_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by tool. DO NOT EDIT. 2 | package lint_example 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | startOfYear := time.Date(2021, 0o1, 0o1, 0, 0, 0, 0, time.UTC) 11 | result := time.Now().Sub(startOfYear) 12 | fmt.Println(result) 13 | } 14 | -------------------------------------------------------------------------------- /testdata/test_files/skip_import_processing_example/test1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "conversion/to.git" 7 | ) 8 | 9 | func foo(s string) *bool { 10 | fmt.Println("Hello World") 11 | if to.StrPtr(s) == nil { 12 | return to.BoolPtr(false) 13 | } 14 | 15 | if to.DecimalPtr(s) == nil { 16 | return to.BoolPtr(false) 17 | } 18 | 19 | return to.BoolPtr(true) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/type_to_alias: -------------------------------------------------------------------------------- 1 | TODO: types nested inside functions 2 | 3 | -- foo.patch -- 4 | @@ 5 | var From, To identifier 6 | @@ 7 | -type From To 8 | +type From = To 9 | 10 | -- top_level.in.go -- 11 | package x 12 | 13 | type UUID string 14 | 15 | -- top_level.out.go -- 16 | package x 17 | 18 | type UUID = string 19 | 20 | -- top_level.diff -- 21 | --- top_level.go 22 | +++ top_level.go 23 | @@ -1,3 +1,3 @@ 24 | package x 25 | 26 | -type UUID string 27 | +type UUID = string 28 | -------------------------------------------------------------------------------- /testdata/undo_expr_patch: -------------------------------------------------------------------------------- 1 | This makes a series of transformations that end up amounting to a no-op. 2 | 3 | -- add_argument.patch -- 4 | @@ 5 | var x expression 6 | @@ 7 | foo(x, 8 | + 42, 9 | ) 10 | 11 | -- rename_func.patch -- 12 | @@ 13 | var x, y expression 14 | @@ 15 | -foo( 16 | +bar( 17 | x, y) 18 | 19 | -- delete_argument.patch -- 20 | @@ 21 | var x, y expression 22 | @@ 23 | bar(x, 24 | - y, 25 | ) 26 | 27 | -- reverse_rename.patch -- 28 | @@ 29 | var x expression 30 | @@ 31 | -bar( 32 | +foo( 33 | x) 34 | 35 | -- a.in.go -- 36 | package main 37 | 38 | func main() { 39 | foo(1) 40 | } 41 | 42 | -- a.out.go -- 43 | package main 44 | 45 | func main() { 46 | foo(1) 47 | } 48 | 49 | -- a.diff -- 50 | --- a.go 51 | +++ a.go 52 | -------------------------------------------------------------------------------- /testdata/unnamed_import_to_named: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var X identifier 4 | @@ 5 | -import "foo/bar.git" 6 | +import bar "foo/bar.git" 7 | 8 | bar.X 9 | 10 | -- match.in.go -- 11 | package a 12 | 13 | import "foo/bar.git" 14 | 15 | func foo() { 16 | var x bar.Baz 17 | x.Do(bar.Option()) 18 | } 19 | 20 | -- match.out.go -- 21 | package a 22 | 23 | import bar "foo/bar.git" 24 | 25 | func foo() { 26 | var x bar.Baz 27 | x.Do(bar.Option()) 28 | } 29 | 30 | -- match.diff -- 31 | --- match.go 32 | +++ match.go 33 | @@ -1,6 +1,6 @@ 34 | package a 35 | 36 | -import "foo/bar.git" 37 | +import bar "foo/bar.git" 38 | 39 | func foo() { 40 | var x bar.Baz 41 | -------------------------------------------------------------------------------- /testdata/value_group: -------------------------------------------------------------------------------- 1 | -- move.patch -- 2 | @@ 3 | @@ 4 | var ( 5 | - foo = 43 6 | bar = 42 7 | + foo = bar + 1 8 | ) 9 | 10 | -- top_level.in.go -- 11 | package a 12 | 13 | var ( 14 | foo = 43 15 | bar = 42 16 | ) 17 | 18 | -- top_level.out.go -- 19 | package a 20 | 21 | var ( 22 | bar = 42 23 | foo = bar + 1 24 | ) 25 | 26 | -- top_level.diff -- 27 | --- top_level.go 28 | +++ top_level.go 29 | @@ -1,6 +1,6 @@ 30 | package a 31 | 32 | var ( 33 | - foo = 43 34 | bar = 42 35 | + foo = bar + 1 36 | ) 37 | 38 | -- func.in.go -- 39 | package a 40 | 41 | func foo() { 42 | var ( 43 | foo = 43 44 | bar = 42 45 | ) 46 | fmt.Println(foo, bar) 47 | } 48 | 49 | -- func.out.go -- 50 | package a 51 | 52 | func foo() { 53 | var ( 54 | bar = 42 55 | foo = bar + 1 56 | ) 57 | fmt.Println(foo, bar) 58 | } 59 | 60 | -- func.diff -- 61 | --- func.go 62 | +++ func.go 63 | @@ -2,8 +2,8 @@ 64 | 65 | func foo() { 66 | var ( 67 | - foo = 43 68 | bar = 42 69 | + foo = bar + 1 70 | ) 71 | fmt.Println(foo, bar) 72 | } 73 | -------------------------------------------------------------------------------- /testdata/value_group_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var value expression 4 | @@ 5 | var ( 6 | ... 7 | - name = value 8 | + _name = value 9 | ... 10 | ) 11 | 12 | -- func.in.go -- 13 | package single 14 | 15 | func foo() { 16 | var ( 17 | foo = "bar" 18 | name = "name" 19 | ) 20 | } 21 | 22 | -- func.out.go -- 23 | package single 24 | 25 | func foo() { 26 | var ( 27 | foo = "bar" 28 | _name = "name" 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /testdata/value_list_elision: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | @@ 4 | { 5 | -var a, ..., c string 6 | +var a, ..., c int 7 | } 8 | 9 | -- foo.in.go -- 10 | package foo 11 | 12 | func x() { 13 | var a, b, c string 14 | } 15 | 16 | -- foo.out.go -- 17 | package foo 18 | 19 | func x() { 20 | var a, b, c int 21 | } 22 | -------------------------------------------------------------------------------- /testdata/writebytes_to_write: -------------------------------------------------------------------------------- 1 | -- in.patch -- 2 | @@ 3 | var b identifier 4 | var out, bs expression 5 | @@ 6 | { 7 | var out *bytes.Buffer 8 | -for _, b := range bs { 9 | - out.WriteByte(b) 10 | + out.Write(bs) 11 | -} 12 | } 13 | 14 | -- buffer.in.go -- 15 | package foo 16 | 17 | func do() { 18 | var buff *bytes.Buffer 19 | for _, c := range results() { 20 | buff.WriteByte(c) 21 | } 22 | } 23 | 24 | -- buffer.out.go -- 25 | package foo 26 | 27 | func do() { 28 | var buff *bytes.Buffer 29 | buff.Write(results()) 30 | } 31 | 32 | -- buffer.diff -- 33 | --- buffer.go 34 | +++ buffer.go 35 | @@ -2,7 +2,5 @@ 36 | 37 | func do() { 38 | var buff *bytes.Buffer 39 | - for _, c := range results() { 40 | - buff.WriteByte(c) 41 | - } 42 | + buff.Write(results()) 43 | } 44 | -------------------------------------------------------------------------------- /tools/cmd/extract-changelog/main.go: -------------------------------------------------------------------------------- 1 | // extract-changelog extracts the release notes for a specific version from a 2 | // file matching the format prescribed by https://keepachangelog.com/en/1.0.0/. 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func main() { 17 | cmd := mainCmd{ 18 | Stdout: os.Stdout, 19 | Stderr: os.Stderr, 20 | } 21 | if err := cmd.Run(os.Args[1:]); err != nil && err != flag.ErrHelp { 22 | fmt.Fprintln(cmd.Stderr, err) 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | type mainCmd struct { 28 | Stdout io.Writer 29 | Stderr io.Writer 30 | } 31 | 32 | const _usage = `USAGE 33 | 34 | %v [OPTIONS] VERSION 35 | 36 | Retrieves the release notes for VERSION from a CHANGELOG.md file and prints 37 | them to stdout. 38 | 39 | EXAMPLES 40 | 41 | extract-changelog -i CHANGELOG.md v1.2.3 42 | extract-changelog 0.2.5 43 | 44 | OPTIONS 45 | ` 46 | 47 | func (cmd *mainCmd) Run(args []string) error { 48 | flag := flag.NewFlagSet("extract-changelog", flag.ContinueOnError) 49 | flag.SetOutput(cmd.Stderr) 50 | flag.Usage = func() { 51 | fmt.Fprintf(flag.Output(), _usage, flag.Name()) 52 | flag.PrintDefaults() 53 | } 54 | 55 | file := flag.String("i", "CHANGELOG.md", "input file") 56 | 57 | if err := flag.Parse(args); err != nil { 58 | return err 59 | } 60 | 61 | var version string 62 | if args := flag.Args(); len(args) > 0 { 63 | version = args[0] 64 | } 65 | version = strings.TrimPrefix(version, "v") 66 | 67 | if len(version) == 0 { 68 | return errors.New("please provide a version") 69 | } 70 | 71 | f, err := os.Open(*file) 72 | if err != nil { 73 | return fmt.Errorf("open changelog: %v", err) 74 | } 75 | defer f.Close() 76 | 77 | s, err := extract(f, version) 78 | if err != nil { 79 | return err 80 | } 81 | _, err = io.WriteString(cmd.Stdout, s) 82 | return err 83 | } 84 | 85 | func extract(r io.Reader, version string) (string, error) { 86 | type _state int 87 | 88 | const ( 89 | initial _state = iota 90 | foundHeader 91 | ) 92 | 93 | var ( 94 | state _state 95 | buff bytes.Buffer 96 | scanner = bufio.NewScanner(r) 97 | ) 98 | 99 | scan: 100 | for scanner.Scan() { 101 | line := scanner.Text() 102 | 103 | switch state { 104 | case initial: 105 | // Version headers take one of the following forms: 106 | // 107 | // ## 0.1.3 - 2021-08-18 108 | // ## [0.1.3] - 2021-08-18 109 | switch { 110 | case strings.HasPrefix(line, "## "+version+" "), 111 | strings.HasPrefix(line, "## ["+version+"]"): 112 | fmt.Fprintln(&buff, line) 113 | state = foundHeader 114 | } 115 | 116 | case foundHeader: 117 | // Found a new version header. Stop extracting. 118 | if strings.HasPrefix(line, "## ") { 119 | break scan 120 | } 121 | fmt.Fprintln(&buff, line) 122 | 123 | default: 124 | // unreachable but guard against it. 125 | return "", fmt.Errorf("unexpected state %v at %q", state, line) 126 | } 127 | } 128 | 129 | if err := scanner.Err(); err != nil { 130 | return "", err 131 | } 132 | 133 | if state < foundHeader { 134 | return "", fmt.Errorf("changelog for %q not found", version) 135 | } 136 | 137 | out := buff.String() 138 | out = strings.TrimSpace(out) + "\n" // always end with a single newline 139 | return out, nil 140 | } 141 | -------------------------------------------------------------------------------- /tools/cmd/extract-changelog/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const _changelog = ` 15 | # Changelog 16 | 17 | All notable changes to this project will be documented in this file. 18 | 19 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 20 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 21 | 22 | ## Unreleased 23 | ### Added 24 | - Upcoming feature 25 | 26 | ## [1.0.0] - 2021-08-18 27 | Initial stable release. 28 | 29 | [1.0.0]: http://example.com/1.0.0 30 | 31 | ## 0.3.0 - 2020-09-01 32 | ### Removed 33 | - deprecated functionality 34 | 35 | ### Fixed 36 | - bug 37 | 38 | ## [0.2.0] - 2020-08-19 39 | ### Added 40 | - Fancy new feature. 41 | 42 | [0.2.0]: http://example.com/0.2.0 43 | 44 | ## 0.1.0 - 2020-08-18 45 | 46 | Initial release. 47 | ` 48 | 49 | func TestMain(t *testing.T) { 50 | t.Parallel() 51 | 52 | changelog := filepath.Join(t.TempDir(), "CHANGELOG.md") 53 | require.NoError(t, 54 | os.WriteFile(changelog, []byte(_changelog), 0o644)) 55 | 56 | tests := []struct { 57 | desc string 58 | 59 | version string 60 | want string // expected changelog 61 | wantErr string // expected error, if any 62 | }{ 63 | { 64 | desc: "not found", 65 | version: "0.1.1", 66 | wantErr: `changelog for "0.1.1" not found`, 67 | }, 68 | { 69 | desc: "missing version", 70 | wantErr: "please provide a version", 71 | }, 72 | { 73 | desc: "non-standard body", 74 | version: "1.0.0", 75 | want: joinLines( 76 | "## [1.0.0] - 2021-08-18", 77 | "Initial stable release.", 78 | "", 79 | "[1.0.0]: http://example.com/1.0.0", 80 | ), 81 | }, 82 | { 83 | desc: "unlinked", 84 | version: "0.3.0", 85 | want: joinLines( 86 | "## 0.3.0 - 2020-09-01", 87 | "### Removed", 88 | "- deprecated functionality", 89 | "", 90 | "### Fixed", 91 | "- bug", 92 | ), 93 | }, 94 | { 95 | desc: "end of file", 96 | version: "0.1.0", 97 | want: joinLines( 98 | "## 0.1.0 - 2020-08-18", 99 | "", 100 | "Initial release.", 101 | ), 102 | }, 103 | { 104 | desc: "linked", 105 | version: "0.2.0", 106 | want: joinLines( 107 | "## [0.2.0] - 2020-08-19", 108 | "### Added", 109 | "- Fancy new feature.", 110 | "", 111 | "[0.2.0]: http://example.com/0.2.0", 112 | ), 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | tt := tt 118 | t.Run(tt.desc, func(t *testing.T) { 119 | t.Parallel() 120 | 121 | var stdout, stderr bytes.Buffer 122 | defer func() { 123 | assert.Empty(t, stderr.String(), "stderr should be empty") 124 | }() 125 | 126 | err := (&mainCmd{ 127 | Stdout: &stdout, 128 | Stderr: &stderr, 129 | }).Run([]string{"-i", changelog, tt.version}) 130 | 131 | if len(tt.wantErr) > 0 { 132 | require.Error(t, err) 133 | assert.Contains(t, err.Error(), tt.wantErr) 134 | return 135 | } 136 | 137 | require.NoError(t, err) 138 | assert.Equal(t, tt.want, stdout.String()) 139 | }) 140 | } 141 | } 142 | 143 | // Join a bunch of lines with a trailing newline. 144 | func joinLines(lines ...string) string { 145 | return strings.Join(lines, "\n") + "\n" 146 | } 147 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/uber-go/gopatch/tools 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 8 | honnef.co/go/tools v0.4.7 9 | ) 10 | 11 | require ( 12 | github.com/BurntSushi/toml v1.2.1 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect 16 | golang.org/x/mod v0.12.0 // indirect 17 | golang.org/x/sys v0.11.0 // indirect 18 | golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= 12 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 13 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 14 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 15 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 16 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 17 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 22 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 23 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 26 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 29 | golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 h1:Vk4mysSz+GqQK2eqgWbo4zEO89wkeAjJiFIr9bpqa8k= 30 | golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= 31 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= 37 | honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= 38 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | //go:build tools 22 | // +build tools 23 | 24 | package tools 25 | 26 | import ( 27 | _ "golang.org/x/lint/golint" 28 | _ "honnef.co/go/tools/cmd/staticcheck" 29 | ) 30 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | // Filled by goreleaser. 24 | var _version = "unknown" 25 | --------------------------------------------------------------------------------