├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Readme.md ├── action.yml ├── cmd ├── modver-action │ └── main.go └── modver │ ├── compare.go │ ├── compare_test.go │ ├── main.go │ ├── main_test.go │ ├── options.go │ ├── options_test.go │ ├── tags.go │ └── tags_test.go ├── compare.go ├── context.go ├── doc.go ├── embed_test.go ├── go.mod ├── go.sum ├── identical.go ├── identical_test.go ├── internal ├── comment.md.tmpl ├── github.go ├── github_test.go ├── pr.go └── pr_test.go ├── modver_test.go ├── public_test.go ├── result.go ├── result_test.go ├── term.go ├── testdata ├── Readme.md ├── major │ ├── addfntypeparam.tmpl │ ├── addmethod.tmpl │ ├── addparam.tmpl │ ├── addresult.tmpl │ ├── addtypeparam.tmpl │ ├── alltosomecomparable.tmpl │ ├── anytocomparable.tmpl │ ├── anytosomecomparable.tmpl │ ├── changetype.tmpl │ ├── charraylen.tmpl │ ├── charraytype.tmpl │ ├── chchandir.tmpl │ ├── chconstant.tmpl │ ├── chfield.tmpl │ ├── chintf.tmpl │ ├── chmodname.tmpl │ ├── chparam.tmpl │ ├── chtag.tmpl │ ├── diffunions.tmpl │ ├── fromcomparable.tmpl │ ├── fromconstraint.tmpl │ ├── hashsplit.tmpl │ ├── pointer.tmpl │ ├── remove.tmpl │ ├── rmfield.tmpl │ ├── rmmethod.tmpl │ ├── rmpackage.tmpl │ ├── rmtag.tmpl │ ├── rmunion.tmpl │ ├── tightenconstraint1.tmpl │ ├── tightenconstraint2.tmpl │ ├── tocomparable.tmpl │ ├── toconstraint.tmpl │ ├── tononfunc.tmpl │ ├── tononintf.tmpl │ ├── tononstruct.tmpl │ └── unassignablechan.tmpl ├── minor │ ├── add.tmpl │ ├── addfield.tmpl │ ├── addmethod1.tmpl │ ├── addmethod2.tmpl │ ├── addoptparam.tmpl │ ├── addpackage.tmpl │ ├── addtag.tmpl │ ├── basexx.tmpl │ ├── bumpgoversion.tmpl │ ├── familiarmethodname.tmpl │ ├── relaxconstraint1.tmpl │ ├── relaxconstraint2.tmpl │ ├── relaxparam.tmpl │ ├── somecomparabletoany.tmpl │ ├── sometoallcomparable.tmpl │ └── subcmd.tmpl ├── none │ ├── assignablechan1.tmpl │ ├── assignablechan2.tmpl │ ├── basexx.tmpl │ ├── chconstant.tmpl │ ├── comparablefield.tmpl │ ├── renametypeparam.tmpl │ ├── reordered.tmpl │ ├── reorderterms.tmpl │ ├── sameintf.tmpl │ └── sametags.tmpl └── patchlevel │ ├── chstructorder.tmpl │ ├── chtypeunexported.tmpl │ ├── map.tmpl │ ├── pointer.tmpl │ ├── rminternal.tmpl │ ├── rmpackage.tmpl │ ├── rmunexported.tmpl │ └── slice.tmpl └── types.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.22 22 | 23 | - name: Unit tests 24 | run: go test -coverprofile=cover.out ./... 25 | 26 | - name: Modver 27 | if: ${{ github.event_name == 'pull_request' }} 28 | run: go run ./cmd/modver -pr https://github.com/${{ github.repository }}/pull/${{ github.event.number }} -token ${{ github.token }} -pretty 29 | 30 | - name: Send coverage 31 | uses: shogo82148/actions-goveralls@v1 32 | with: 33 | path-to-profile: cover.out 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /cover.out 3 | /modver 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | ADD . /app 4 | 5 | WORKDIR /app 6 | 7 | RUN go build ./cmd/modver-action 8 | 9 | ENTRYPOINT ["/app/modver-action"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bob Glickstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Modver 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/bobg/modver/v2.svg)](https://pkg.go.dev/github.com/bobg/modver/v2) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/bobg/modver/v2)](https://goreportcard.com/report/github.com/bobg/modver/v2) 5 | [![Tests](https://github.com/bobg/modver/actions/workflows/go.yml/badge.svg)](https://github.com/bobg/modver/actions/workflows/go.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/bobg/modver/badge.svg?branch=master)](https://coveralls.io/github/bobg/modver?branch=master) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 8 | 9 | This is modver, 10 | a tool that helps you obey [semantic versioning rules](https://semver.org/) in your Go module. 11 | 12 | It can read and compare two different versions of the same module, 13 | from two different directories, 14 | or two different Git commits, 15 | or the base and head of a Git pull request. 16 | It then reports whether the changes require an increase in the major-version number, 17 | the minor-version number, 18 | or the patchlevel. 19 | 20 | ## Installation and usage 21 | 22 | Modver can be used from the command line, 23 | or in your Go program, 24 | or with [GitHub Actions](https://github.com/features/actions). 25 | 26 | ### Command-line interface 27 | 28 | Install the `modver` command like this: 29 | 30 | ```sh 31 | go install github.com/bobg/modver/v2/cmd/modver@latest 32 | ``` 33 | 34 | Assuming the current directory is the root of a cloned Git repository, 35 | you can run it like this: 36 | 37 | ```sh 38 | $ modver -git .git HEAD~1 HEAD 39 | ``` 40 | 41 | to tell what kind of version-number change is needed for the latest commit. 42 | The `-git .git` gives the path to the repository’s info; 43 | it can also be something like `https://github.com/bobg/modver`. 44 | The arguments `HEAD~1` and `HEAD` specify two Git revisions to compare; 45 | in this case, the latest two commits on the current branch. 46 | These could also be tags or commit hashes. 47 | 48 | ### GitHub Action 49 | 50 | You can arrange for Modver to inspect the changes on your pull-request branch 51 | as part of a GitHub Actions-based continuous-integration step. 52 | It will add a comment to the pull request with its findings, 53 | and will update the comment as new commits are pushed to the branch. 54 | 55 | To do this, you’ll need a directory in your GitHub repository named `.github/workflows`, 56 | and a Yaml file containing (at least) the following: 57 | 58 | ```yaml 59 | name: Tests 60 | 61 | on: 62 | push: 63 | branches: [ main ] 64 | pull_request: 65 | branches: [ main ] 66 | 67 | jobs: 68 | test: 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@v3 73 | with: 74 | fetch-depth: 0 75 | 76 | - name: Set up Go 77 | uses: actions/setup-go@v4 78 | with: 79 | go-version: 1.19 80 | 81 | - name: Modver 82 | if: ${{ github.event_name == 'pull_request' }} 83 | uses: bobg/modver@v2.5.0 84 | with: 85 | github_token: ${{ secrets.GITHUB_TOKEN }} 86 | pull_request_url: https://github.com/${{ github.repository }}/pull/${{ github.event.number }} 87 | ``` 88 | 89 | This can be combined with other steps that run unit tests, etc. 90 | You can change `Tests` to whatever name you like, 91 | and should change `main` to the name of your repository’s default branch. 92 | If your pull request is on a GitHub server other than `github.com`, 93 | change the hostname in the `pull_request_url` parameter to match. 94 | 95 | Note the `fetch-depth: 0` parameter for the `Checkout` step. 96 | This causes GitHub Actions to create a clone of your repo with its full history, 97 | as opposed to the default, 98 | which is a shallow clone. 99 | Modver requires enough history to be present in the clone 100 | for it to access the “base” and “head” revisions of your pull-request branch. 101 | 102 | For more information about configuring GitHub Actions, 103 | see [the GitHub Actions documentation](https://docs.github.com/actions). 104 | 105 | ### Go library 106 | 107 | Modver also has a simple API for use from within Go programs. 108 | Add it to your project with `go get github.com/bobg/modver/v2@latest`. 109 | See [the Go doc page](https://pkg.go.dev/github.com/bobg/modver/v2) for information about how to use it. 110 | 111 | ## Semantic versioning 112 | 113 | Briefly, a major-version bump is needed for incompatible changes in the public API, 114 | such as when a type is removed or renamed, 115 | or parameters or results are added to or removed from a function. 116 | Old callers cannot expect to use the new version without being updated. 117 | 118 | A minor-version bump is needed when new features are added to the public API, 119 | like a new entrypoint or new fields in an existing struct. 120 | Old callers _can_ continue using the new version without being updated, 121 | but callers depending on the new features cannot use the old version. 122 | 123 | A patchlevel bump is needed for most other changes. 124 | 125 | The result produced by modver is the _minimal_ change required. 126 | The actual change required may be greater. 127 | For example, 128 | if a new method is added to a type, 129 | this function will return `Minor`. 130 | However, if something also changed about an existing method that breaks the old contract - 131 | it accepts a narrower range of inputs, for example, 132 | or returns errors in some new cases - 133 | that may well require a major-version bump, 134 | and this function can't detect those cases. 135 | 136 | You can be assured, however, 137 | that if this function returns `Major`, 138 | a minor-version bump won't suffice, 139 | and if this function returns `Minor`, 140 | a patchlevel bump won't suffice, 141 | etc. 142 | 143 | The `modver` command 144 | (in the `cmd/modver` subdirectory) 145 | can be used, 146 | among other ways, 147 | to test that each commit to a Git repository increments the module’s version number appropriately. 148 | This is done for modver itself using GitHub Actions, 149 | [here](https://github.com/bobg/modver/blob/dd93eccb5674b13161a91bf6a6666889c21adb5b/.github/workflows/go.yml#L25-L26). 150 | 151 | (Note that the standard `actions/checkout@v2` action, 152 | for cloning a repository during GitHub Actions, 153 | creates a shallow clone with just one commit’s worth of history. 154 | For the usage here to work, 155 | you’ll need more history: 156 | at least two commit’s worth and maybe more to pull in the latest tag for the previous revision. 157 | The clone depth can be overridden with the `fetch-depth` parameter, 158 | which modver does [here](https://github.com/bobg/modver/blob/dd93eccb5674b13161a91bf6a6666889c21adb5b/.github/workflows/go.yml#L14-L15).) 159 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Modver 2 | description: Analyze pull requests for changes in Go code that require updating a module's version number. 3 | author: Bob Glickstein 4 | inputs: 5 | github_token: 6 | description: 'The GitHub token to use for authentication.' 7 | required: true 8 | pull_request_url: 9 | description: 'The full github.com URL of the pull request.' 10 | required: true 11 | runs: 12 | using: 'docker' 13 | image: 'Dockerfile' 14 | -------------------------------------------------------------------------------- /cmd/modver-action/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | 8 | "github.com/bobg/modver/v2" 9 | "github.com/bobg/modver/v2/internal" 10 | ) 11 | 12 | func main() { 13 | os.Setenv("GOROOT", "/usr/local/go") // Work around some Docker weirdness. 14 | 15 | prURL := os.Getenv("INPUT_PULL_REQUEST_URL") 16 | host, owner, reponame, prnum, err := internal.ParsePR(prURL) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | token := os.Getenv("INPUT_GITHUB_TOKEN") 21 | if token == "" { 22 | log.Fatal("No GitHub token in the environment variable INPUT_GITHUB_TOKEN") 23 | } 24 | ctx := context.Background() 25 | gh, err := internal.NewClient(ctx, host, token) 26 | if err != nil { 27 | log.Fatalf("Creating GitHub client: %s", err) 28 | } 29 | result, err := internal.PR(ctx, gh, owner, reponame, prnum) 30 | if err != nil { 31 | log.Fatalf("Running comparison: %s", err) 32 | } 33 | modver.Pretty(os.Stdout, result) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/modver/compare.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/bobg/errors" 9 | "github.com/google/go-github/v50/github" 10 | 11 | "github.com/bobg/modver/v2" 12 | "github.com/bobg/modver/v2/internal" 13 | ) 14 | 15 | func doCompare(ctx context.Context, opts options) (modver.Result, error) { 16 | return doCompareHelper(ctx, opts, internal.NewClient, internal.PR, modver.CompareGitWith, modver.CompareDirs) 17 | } 18 | 19 | type ( 20 | newClientType = func(ctx context.Context, host, token string) (*github.Client, error) 21 | prType = func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) 22 | compareGitWithType = func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error) 23 | compareDirsType = func(older, newer string) (modver.Result, error) 24 | ) 25 | 26 | func doCompareHelper(ctx context.Context, opts options, newClient newClientType, pr prType, compareGitWith compareGitWithType, compareDirs compareDirsType) (modver.Result, error) { 27 | if opts.pr != "" { 28 | host, owner, reponame, prnum, err := internal.ParsePR(opts.pr) 29 | if err != nil { 30 | return modver.None, errors.Wrap(err, "parsing pull-request URL") 31 | } 32 | if opts.ghtoken == "" { 33 | return modver.None, fmt.Errorf("usage: %s -pr URL [-token TOKEN]", os.Args[0]) 34 | } 35 | gh, err := newClient(ctx, host, opts.ghtoken) 36 | if err != nil { 37 | return modver.None, errors.Wrap(err, "creating GitHub client") 38 | } 39 | return pr(ctx, gh, owner, reponame, prnum) 40 | } 41 | 42 | if opts.gitRepo != "" { 43 | if len(opts.args) != 2 { 44 | return nil, fmt.Errorf("usage: %s -git REPO [-gitcmd GIT_COMMAND] [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV", os.Args[0]) 45 | } 46 | 47 | callback := modver.CompareDirs 48 | if opts.versions { 49 | callback = getTags(&opts.v1, &opts.v2, opts.args[0], opts.args[1]) 50 | } 51 | 52 | return compareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback) 53 | } 54 | if len(opts.args) != 2 { 55 | return nil, fmt.Errorf("usage: %s [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR", os.Args[0]) 56 | } 57 | return compareDirs(opts.args[0], opts.args[1]) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/modver/compare_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/go-github/v50/github" 9 | 10 | "github.com/bobg/modver/v2" 11 | ) 12 | 13 | func TestDoCompare(t *testing.T) { 14 | cases := []struct { 15 | opts options 16 | wantErr bool 17 | pr func(*testing.T, *int) prType 18 | compareGitWith func(*testing.T, *int) compareGitWithType 19 | compareDirs func(*testing.T, *int) compareDirsType 20 | }{{ 21 | opts: options{ 22 | pr: "https://github.com/foo/bar/pull/17", 23 | ghtoken: "token", 24 | }, 25 | pr: mockPR("foo", "bar", 17), 26 | }, { 27 | opts: options{ 28 | pr: "https://github.com/foo/bar/baz/pull/17", 29 | ghtoken: "token", 30 | }, 31 | wantErr: true, 32 | }, { 33 | opts: options{ 34 | pr: "https://github.com/foo/bar/pull/17", 35 | }, 36 | wantErr: true, 37 | }, { 38 | opts: options{ 39 | gitRepo: ".git", 40 | args: []string{"older", "newer"}, 41 | }, 42 | compareGitWith: mockCompareGitWith(".git", "older", "newer"), 43 | }, { 44 | opts: options{ 45 | gitRepo: ".git", 46 | args: []string{"older", "newer", "evenmorenewer"}, 47 | }, 48 | wantErr: true, 49 | }, { 50 | opts: options{ 51 | args: []string{"older", "newer"}, 52 | }, 53 | compareDirs: mockCompareDirs("older", "newer"), 54 | }, { 55 | opts: options{ 56 | args: []string{"older"}, 57 | }, 58 | wantErr: true, 59 | }} 60 | 61 | ctx := context.Background() 62 | 63 | for i, tc := range cases { 64 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 65 | var ( 66 | pr prType 67 | compareGitWith compareGitWithType 68 | compareDirs compareDirsType 69 | calls int 70 | ) 71 | if tc.pr != nil { 72 | pr = tc.pr(t, &calls) 73 | } 74 | if tc.compareGitWith != nil { 75 | compareGitWith = tc.compareGitWith(t, &calls) 76 | } 77 | if tc.compareDirs != nil { 78 | compareDirs = tc.compareDirs(t, &calls) 79 | } 80 | 81 | _, err := doCompareHelper(ctx, tc.opts, mockNewClient, pr, compareGitWith, compareDirs) 82 | if err != nil { 83 | if !tc.wantErr { 84 | t.Errorf("got error %s, wanted none", err) 85 | } 86 | return 87 | } 88 | if tc.wantErr { 89 | t.Error("got no error, wanted one") 90 | return 91 | } 92 | if calls != 1 { 93 | t.Errorf("got %d calls, want 1", calls) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func mockNewClient(ctx context.Context, host, token string) (*github.Client, error) { 100 | return nil, nil 101 | } 102 | 103 | func mockPR(wantOwner, wantRepo string, wantPRNum int) func(*testing.T, *int) prType { 104 | return func(t *testing.T, calls *int) prType { 105 | return func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { 106 | *calls++ 107 | if owner != wantOwner { 108 | t.Errorf("got owner %s, want %s", owner, wantOwner) 109 | } 110 | if reponame != wantRepo { 111 | t.Errorf("got repo %s, want %s", reponame, wantRepo) 112 | } 113 | if wantPRNum != prnum { 114 | t.Errorf("got PR number %d, want %d", prnum, wantPRNum) 115 | } 116 | return modver.None, nil 117 | } 118 | } 119 | } 120 | 121 | func mockCompareGitWith(wantGitRepo, wantOlder, wantNewer string) func(*testing.T, *int) compareGitWithType { 122 | return func(t *testing.T, calls *int) compareGitWithType { 123 | return func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error) { 124 | *calls++ 125 | if repoURL != wantGitRepo { 126 | t.Errorf("got repo URL %s, want %s", repoURL, wantGitRepo) 127 | } 128 | if olderRev != wantOlder { 129 | t.Errorf("got older rev %s, want %s", olderRev, wantOlder) 130 | } 131 | if newerRev != wantNewer { 132 | t.Errorf("got newer rev %s, want %s", newerRev, wantNewer) 133 | } 134 | return modver.None, nil 135 | } 136 | } 137 | } 138 | 139 | func mockCompareDirs(wantOlder, wantNewer string) func(*testing.T, *int) compareDirsType { 140 | return func(t *testing.T, calls *int) compareDirsType { 141 | return func(older, newer string) (modver.Result, error) { 142 | *calls++ 143 | if older != wantOlder { 144 | t.Errorf("got older dir %s, want %s", older, wantOlder) 145 | } 146 | if newer != wantNewer { 147 | t.Errorf("got newer dir %s, want %s", newer, wantNewer) 148 | } 149 | return modver.None, nil 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /cmd/modver/main.go: -------------------------------------------------------------------------------- 1 | // Command modver compares two versions of the same Go packages 2 | // and tells whether a Major, Minor, or Patchlevel version bump 3 | // (or None) 4 | // is needed to go from one to the other. 5 | // 6 | // Usage: 7 | // 8 | // modver -pr URL [-token GITHUB_TOKEN] 9 | // modver -git REPO [-gitcmd GIT_COMMAND] [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV 10 | // modver [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR 11 | // 12 | // With `-pr URL`, 13 | // the URL must be that of a github.com pull request 14 | // (having the form https://HOST/OWNER/REPO/pull/NUMBER). 15 | // The environment variable GITHUB_TOKEN must contain a valid GitHub access token, 16 | // or else one must be supplied on the command line with -token. 17 | // In this mode, 18 | // modver compares the base of the pull-request branch with the head 19 | // and produces a report that it adds as a comment to the pull request. 20 | // 21 | // With `-git REPO`, 22 | // where REPO is the path to a Git repository, 23 | // OLDER and NEWER are two revisions in the repository 24 | // (e.g. hexadecimal SHA strings or "HEAD", etc) 25 | // containing the older and newer versions of a Go module. 26 | // Without the -git flag, 27 | // OLDER and NEWER are two directories containing the older and newer versions of a Go module. 28 | // 29 | // With `-gitcmd GIT_COMMAND`, 30 | // modver uses the given command for Git operations. 31 | // This is "git" by default. 32 | // If the command does not exist or is not found in your PATH, 33 | // modver falls back to using the go-git library. 34 | // 35 | // With -v1 and -v2, 36 | // modver checks whether the change from OLDERVERSION to NEWERVERSION 37 | // (two version strings) 38 | // is adequate for the differences detected between OLDER and NEWER. 39 | // Output is either "OK" or "ERR" 40 | // (followed by a description) 41 | // and the exit code is 0 for OK and 1 for ERR. 42 | // In quiet mode (-q), 43 | // there is no output. 44 | // With -git REPO and -versions instead of -v1 and -v2, 45 | // the values for -v1 and -v2 are determined by querying the repo at the given revisions. 46 | // 47 | // Without -v1 and -v2 48 | // (or -versions), 49 | // output is a string describing the minimum version-number change required. 50 | // In quiet mode (-q), 51 | // there is no output, 52 | // and the exit status is 0, 1, 2, 3, or 4 53 | // for None, Patchlevel, Minor, Major, and error. 54 | package main 55 | 56 | import ( 57 | "context" 58 | "fmt" 59 | "io" 60 | "os" 61 | 62 | "golang.org/x/mod/semver" 63 | 64 | "github.com/bobg/modver/v2" 65 | ) 66 | 67 | const errorStatus = 4 68 | 69 | func main() { 70 | opts, err := parseArgs() 71 | if err != nil { 72 | fmt.Fprintf(os.Stderr, "Error parsing args: %s\n", err) 73 | os.Exit(errorStatus) 74 | } 75 | 76 | ctx := context.Background() 77 | if opts.gitCmd != "" { 78 | ctx = modver.WithGit(ctx, opts.gitCmd) 79 | } 80 | 81 | res, err := doCompare(ctx, opts) 82 | if err != nil { 83 | fmt.Fprintf(os.Stderr, "Error in comparing: %s\n", err) 84 | os.Exit(errorStatus) 85 | } 86 | 87 | exitCode := doShowResult(os.Stdout, res, opts) 88 | os.Exit(exitCode) 89 | } 90 | 91 | func doShowResult(out io.Writer, res modver.Result, opts options) int { 92 | if opts.v1 != "" && opts.v2 != "" { 93 | var ok bool 94 | 95 | cmp := semver.Compare(opts.v1, opts.v2) 96 | switch res.Code() { 97 | case modver.None: 98 | ok = cmp <= 0 // v1 <= v2 99 | 100 | case modver.Patchlevel: 101 | ok = cmp < 0 // v1 < v2 102 | 103 | case modver.Minor: 104 | var ( 105 | min1 = semver.MajorMinor(opts.v1) 106 | min2 = semver.MajorMinor(opts.v2) 107 | ) 108 | ok = semver.Compare(min1, min2) < 0 // min1 < min2 109 | 110 | case modver.Major: 111 | var ( 112 | maj1 = semver.Major(opts.v1) 113 | maj2 = semver.Major(opts.v2) 114 | ) 115 | ok = semver.Compare(maj1, maj2) < 0 // maj1 < maj2 116 | } 117 | 118 | if ok { 119 | if !opts.quiet { 120 | if opts.versions { 121 | fmt.Fprintf(out, "OK using versions %s and %s: %s\n", opts.v1, opts.v2, res) 122 | } else { 123 | fmt.Fprintf(out, "OK %s\n", res) 124 | } 125 | } 126 | return 0 127 | } 128 | if !opts.quiet { 129 | if opts.versions { 130 | fmt.Fprintf(out, "ERR using versions %s and %s: %s\n", opts.v1, opts.v2, res) 131 | } else { 132 | fmt.Fprintf(out, "ERR %s\n", res) 133 | } 134 | } 135 | return 1 136 | } 137 | 138 | if opts.quiet { 139 | return int(res.Code()) 140 | } 141 | 142 | if opts.pretty { 143 | modver.Pretty(out, res) 144 | } else { 145 | fmt.Fprintln(out, res) 146 | } 147 | 148 | return 0 149 | } 150 | -------------------------------------------------------------------------------- /cmd/modver/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/bobg/modver/v2" 9 | ) 10 | 11 | func TestDoShowResult(t *testing.T) { 12 | cases := []struct { 13 | res modver.Result 14 | opts options 15 | wantExitCode int 16 | want string 17 | }{{ 18 | res: modver.Patchlevel, 19 | opts: options{quiet: true}, 20 | wantExitCode: int(modver.Patchlevel), 21 | }, { 22 | res: modver.Patchlevel, 23 | want: modver.Patchlevel.String() + "\n", 24 | }, { 25 | res: modver.None, 26 | opts: options{v1: "v1.0.0", v2: "v1.0.1"}, 27 | want: "OK None\n", 28 | }, { 29 | res: modver.None, 30 | opts: options{v1: "v1.0.1", v2: "v1.0.0"}, 31 | want: "ERR None\n", 32 | wantExitCode: 1, 33 | }, { 34 | res: modver.Patchlevel, 35 | opts: options{v1: "v1.0.0", v2: "v1.0.1"}, 36 | want: "OK Patchlevel\n", 37 | }, { 38 | res: modver.Patchlevel, 39 | opts: options{v1: "v1.0.0", v2: "v1.0.0"}, 40 | want: "ERR Patchlevel\n", 41 | wantExitCode: 1, 42 | }, { 43 | res: modver.Minor, 44 | opts: options{v1: "v1.0.0", v2: "v1.1.0"}, 45 | want: "OK Minor\n", 46 | }, { 47 | res: modver.Minor, 48 | opts: options{v1: "v1.0.0", v2: "v1.0.1"}, 49 | want: "ERR Minor\n", 50 | wantExitCode: 1, 51 | }, { 52 | res: modver.Major, 53 | opts: options{v1: "v1.0.0", v2: "v2.0.0"}, 54 | want: "OK Major\n", 55 | }, { 56 | res: modver.Major, 57 | opts: options{v1: "v1.0.0", v2: "v1.1.0"}, 58 | want: "ERR Major\n", 59 | wantExitCode: 1, 60 | }} 61 | 62 | for i, tc := range cases { 63 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 64 | buf := new(bytes.Buffer) 65 | exitCode := doShowResult(buf, tc.res, tc.opts) 66 | if exitCode != tc.wantExitCode { 67 | t.Errorf("got exit code %d, want %d", exitCode, tc.wantExitCode) 68 | } 69 | if buf.String() != tc.want { 70 | t.Errorf("got %s, want %s", buf, tc.want) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/modver/options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/bobg/errors" 10 | "golang.org/x/mod/semver" 11 | ) 12 | 13 | type options struct { 14 | gitRepo, gitCmd, ghtoken, v1, v2, pr string 15 | quiet, pretty, versions bool 16 | args []string 17 | } 18 | 19 | func parseArgs() (options, error) { 20 | return parseArgsHelper(os.Args[1:]) 21 | } 22 | 23 | func parseArgsHelper(args []string) (opts options, err error) { 24 | var fs flag.FlagSet 25 | 26 | fs.BoolVar(&opts.pretty, "pretty", false, "result is shown in a pretty format with (possibly) multiple lines and indentation") 27 | fs.BoolVar(&opts.quiet, "q", false, "quiet mode: prints no output, exits with status 0, 1, 2, 3, or 4 to mean None, Patchlevel, Minor, Major, or error") 28 | fs.BoolVar(&opts.versions, "versions", false, "with -git, compute values for -v1 and -v2 from the Git repository") 29 | fs.StringVar(&opts.ghtoken, "token", os.Getenv("GITHUB_TOKEN"), "GitHub access token") 30 | fs.StringVar(&opts.gitCmd, "gitcmd", "git", "use this command for git operations, if found; otherwise use the go-git library") 31 | fs.StringVar(&opts.gitRepo, "git", "", "Git repo URL") 32 | fs.StringVar(&opts.pr, "pr", "", "URL of GitHub pull request") 33 | fs.StringVar(&opts.v1, "v1", "", "version string of older version; with -v2 changes output to OK (exit status 0) for adequate version-number change, ERR (exit status 1) for inadequate") 34 | fs.StringVar(&opts.v2, "v2", "", "version string of newer version") 35 | if err := fs.Parse(args); err != nil { 36 | return opts, errors.Wrap(err, "parsing args") 37 | } 38 | opts.args = fs.Args() 39 | 40 | if opts.pr != "" { 41 | if opts.gitRepo != "" { 42 | return opts, fmt.Errorf("do not specify -git with -pr") 43 | } 44 | if opts.v1 != "" || opts.v2 != "" || opts.versions { 45 | return opts, fmt.Errorf("do not specify -v1, -v2, or -versions with -pr") 46 | } 47 | } 48 | 49 | if opts.v1 != "" && opts.v2 != "" { 50 | if !strings.HasPrefix(opts.v1, "v") { 51 | opts.v1 = "v" + opts.v1 52 | } 53 | if !strings.HasPrefix(opts.v2, "v") { 54 | opts.v2 = "v" + opts.v2 55 | } 56 | if !semver.IsValid(opts.v1) { 57 | return opts, fmt.Errorf("not a valid version string: %s", opts.v1) 58 | } 59 | if !semver.IsValid(opts.v2) { 60 | return opts, fmt.Errorf("not a valid version string: %s", opts.v2) 61 | } 62 | } 63 | 64 | return opts, nil 65 | } 66 | -------------------------------------------------------------------------------- /cmd/modver/options_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestParseArgs(t *testing.T) { 11 | ghtok := os.Getenv("GITHUB_TOKEN") 12 | 13 | cases := []struct { 14 | args []string 15 | wantErr bool 16 | want options 17 | }{{ 18 | want: options{ 19 | ghtoken: ghtok, 20 | gitCmd: "git", 21 | }, 22 | }, { 23 | args: []string{"-pr", "foo"}, 24 | want: options{ 25 | pr: "foo", 26 | ghtoken: ghtok, 27 | gitCmd: "git", 28 | }, 29 | }, { 30 | args: []string{"-pr", "foo", "-git", "bar"}, 31 | wantErr: true, 32 | }, { 33 | args: []string{"-pr", "foo", "-v1", "bar"}, 34 | wantErr: true, 35 | }, { 36 | args: []string{"-pr", "foo", "-v2", "bar"}, 37 | wantErr: true, 38 | }, { 39 | args: []string{"-pr", "foo", "-versions"}, 40 | wantErr: true, 41 | }, { 42 | args: []string{"-v1", "1", "-v2", "2"}, 43 | want: options{ 44 | v1: "v1", 45 | v2: "v2", 46 | ghtoken: ghtok, 47 | gitCmd: "git", 48 | }, 49 | }, { 50 | args: []string{"-v1", "1", "-v2", "bar"}, 51 | wantErr: true, 52 | }, { 53 | args: []string{"-v1", "foo", "-v2", "2"}, 54 | wantErr: true, 55 | }} 56 | 57 | for i, tc := range cases { 58 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 59 | got, err := parseArgsHelper(tc.args) 60 | if err != nil { 61 | if !tc.wantErr { 62 | t.Errorf("got error %v, wanted no error", err) 63 | } 64 | return 65 | } 66 | if tc.wantErr { 67 | t.Fatal("got no error but wanted one") 68 | } 69 | if len(got.args) == 0 { 70 | got.args = nil // not []string{} 71 | } 72 | if !reflect.DeepEqual(got, tc.want) { 73 | t.Errorf("got %+v, want %+v", got, tc.want) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/modver/tags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/bobg/errors" 10 | "github.com/go-git/go-git/v5" 11 | "github.com/go-git/go-git/v5/plumbing" 12 | "github.com/go-git/go-git/v5/plumbing/object" 13 | "github.com/go-git/go-git/v5/plumbing/storer" 14 | "golang.org/x/mod/semver" 15 | 16 | "github.com/bobg/modver/v2" 17 | ) 18 | 19 | func getTags(v1, v2 *string, olderRev, newerRev string) func(older, newer string) (modver.Result, error) { 20 | return getTagsHelper(v1, v2, olderRev, newerRev, modver.CompareDirs) 21 | } 22 | 23 | func getTagsHelper(v1, v2 *string, olderRev, newerRev string, compareDirs compareDirsType) func(older, newer string) (modver.Result, error) { 24 | return func(older, newer string) (modver.Result, error) { 25 | tag, err := getTag(older, olderRev) 26 | if err != nil { 27 | return modver.None, fmt.Errorf("getting tag from %s: %w", older, err) 28 | } 29 | *v1 = tag 30 | 31 | tag, err = getTag(newer, newerRev) 32 | if err != nil { 33 | return modver.None, fmt.Errorf("getting tag from %s: %w", newer, err) 34 | } 35 | *v2 = tag 36 | 37 | return compareDirs(older, newer) 38 | } 39 | } 40 | 41 | func getTag(dir, rev string) (string, error) { 42 | repo, err := git.PlainOpen(dir) 43 | if err != nil { 44 | return "", fmt.Errorf("opening %s: %w", dir, err) 45 | } 46 | tags, err := repo.Tags() 47 | if err != nil { 48 | return "", fmt.Errorf("getting tags in %s: %w", dir, err) 49 | } 50 | hash, err := repo.ResolveRevision(plumbing.Revision(rev)) 51 | if err != nil { 52 | return "", fmt.Errorf(`resolving revision "%s" in %s: %w`, rev, dir, err) 53 | } 54 | repoCommit, err := object.GetCommit(repo.Storer, *hash) 55 | if err != nil { 56 | return "", fmt.Errorf("getting commit at %s: %w", rev, err) 57 | } 58 | 59 | return getTagHelper(dir, rev, repo.Storer, tags, hash, repoCommit) 60 | } 61 | 62 | func getTagHelper(dir, rev string, s storer.EncodedObjectStorer, tags storer.ReferenceIter, hash *plumbing.Hash, repoCommit *object.Commit) (string, error) { 63 | var result string 64 | 65 | OUTER: 66 | for { 67 | tref, err := tags.Next() 68 | if errors.Is(err, io.EOF) { 69 | return result, nil 70 | } 71 | if err != nil { 72 | return "", fmt.Errorf("iterating over tags in %s: %w", dir, err) 73 | } 74 | tag := strings.TrimPrefix(string(tref.Name()), "refs/tags/") 75 | if !semver.IsValid(tag) { 76 | continue 77 | } 78 | tagCommit, err := object.GetCommit(s, tref.Hash()) 79 | if err != nil { 80 | fmt.Fprintf(os.Stderr, "Warning: getting commit for tag %s: %s", tref.Name(), err) 81 | continue 82 | } 83 | if tagCommit.Hash != *hash { 84 | bases, err := repoCommit.MergeBase(tagCommit) 85 | if err != nil { 86 | fmt.Fprintf(os.Stderr, "Warning: getting merge base of %s and %s: %s", rev, tag, err) 87 | continue 88 | } 89 | INNER: 90 | for _, base := range bases { 91 | switch base.Hash { 92 | case *hash: 93 | // This tag comes later than the checked-out commit. 94 | continue OUTER 95 | case tagCommit.Hash: 96 | // The checked-out commit comes later than the tag. 97 | break INNER 98 | } 99 | } 100 | } 101 | if result == "" || semver.Compare(result, tag) < 0 { // result < tag 102 | result = tag 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cmd/modver/tags_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestGetTag(t *testing.T) { 6 | got, err := getTag("../..", "aa470e1b623810ea1434f51b569f37cf9a0782ab") 7 | if err != nil { 8 | t.Fatal(err) 9 | } 10 | 11 | const want = "v1.1.8" 12 | if got != want { 13 | t.Errorf("got %s, want %s", got, want) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /compare.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/ast" 7 | "go/types" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/go-git/go-git/v5" 14 | "github.com/go-git/go-git/v5/plumbing" 15 | "golang.org/x/mod/semver" 16 | "golang.org/x/tools/go/packages" 17 | ) 18 | 19 | // Compare compares an "older" version of a Go module to a "newer" version of the same module. 20 | // It tells whether the changes from "older" to "newer" require an increase in the major, minor, or patchlevel version numbers, 21 | // according to semver rules (https://semver.org/). 22 | // 23 | // Briefly, a major-version bump is needed for incompatible changes in the public API, 24 | // such as when a type is removed or renamed, 25 | // or parameters or results are added to or removed from a function. 26 | // Old callers cannot expect to use the new version without being updated. 27 | // 28 | // A minor-version bump is needed when new features are added to the public API, 29 | // like a new entrypoint or new fields in an existing struct. 30 | // Old callers _can_ continue using the new version without being updated, 31 | // but callers depending on the new features cannot use the old version. 32 | // 33 | // A patchlevel bump is needed for most other changes. 34 | // 35 | // The result of Compare is the _minimal_ change required. 36 | // The actual change required may be greater. 37 | // For example, 38 | // if a new method is added to a type, 39 | // this function will return Minor. 40 | // However, if something also changed about an existing method that breaks the old contract - 41 | // it accepts a narrower range of inputs, for example, 42 | // or returns errors in some new cases - 43 | // that may well require a major-version bump, 44 | // and this function can't detect those cases. 45 | // 46 | // You can be assured, however, 47 | // that if this function returns Major, 48 | // a minor-version bump won't suffice, 49 | // and if this function returns Minor, 50 | // a patchlevel bump won't suffice, 51 | // etc. 52 | // 53 | // The packages passed to this function should have no load errors 54 | // (that is, len(p.Errors) should be 0 for each package p in `olders` and `newers`). 55 | // If you are using packages.Load 56 | // (see https://pkg.go.dev/golang.org/x/tools/go/packages#Load), 57 | // you will need at least 58 | // 59 | // packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo 60 | // 61 | // in your Config.Mode. 62 | // See CompareDirs for an example of how to call Compare with the result of packages.Load. 63 | func Compare(olders, newers []*packages.Package) Result { 64 | var ( 65 | older = makePackageMap(olders) 66 | newer = makePackageMap(newers) 67 | ) 68 | 69 | c := newComparer() 70 | 71 | // Look for major-version changes. 72 | if res := c.compareMajor(older, newer); res != nil { 73 | return res 74 | } 75 | 76 | // Look for minor-version changes. 77 | if res := c.compareMinor(older, newer); res != nil { 78 | return res 79 | } 80 | 81 | // Finally, look for patchlevel-version changes. 82 | if res := c.comparePatchlevel(older, newer); res != nil { 83 | return res 84 | } 85 | 86 | return None 87 | } 88 | 89 | func isPublic(pkgpath string) bool { 90 | switch pkgpath { 91 | case "internal", "main": 92 | return false 93 | } 94 | if strings.HasSuffix(pkgpath, "/main") { 95 | return false 96 | } 97 | if strings.HasPrefix(pkgpath, "internal/") { 98 | return false 99 | } 100 | if strings.HasSuffix(pkgpath, "/internal") { 101 | return false 102 | } 103 | if strings.Contains(pkgpath, "/internal/") { 104 | return false 105 | } 106 | return true 107 | } 108 | 109 | func (c *comparer) compareMajor(older, newer map[string]*packages.Package) Result { 110 | for pkgPath, pkg := range older { 111 | if !isPublic(pkgPath) { 112 | continue 113 | } 114 | 115 | var ( 116 | topObjs = makeTopObjs(pkg) 117 | newTopObjs map[string]types.Object 118 | newPkg = newer[pkgPath] 119 | ) 120 | 121 | for id, obj := range topObjs { 122 | if !isExported(id) { 123 | continue 124 | } 125 | if newPkg == nil { 126 | return rwrapf(Major, "no new version of package %s", pkgPath) 127 | } 128 | if newTopObjs == nil { 129 | newTopObjs = makeTopObjs(newPkg) 130 | } 131 | newObj := newTopObjs[id] 132 | if newObj == nil { 133 | return rwrapf(Major, "no object %s in new version of package %s", id, pkgPath) 134 | } 135 | if res := c.compareTypes(obj.Type(), newObj.Type()); res.Code() == Major { 136 | return rwrapf(res, "checking %s", id) 137 | } 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (c *comparer) compareMinor(older, newer map[string]*packages.Package) Result { 145 | for pkgPath, pkg := range newer { 146 | if !isPublic(pkgPath) { 147 | continue 148 | } 149 | 150 | oldPkg := older[pkgPath] 151 | if oldPkg != nil { 152 | if oldMod, newMod := oldPkg.Module, pkg.Module; oldMod != nil && newMod != nil { 153 | if cmp := semver.Compare("v"+oldMod.GoVersion, "v"+newMod.GoVersion); cmp < 0 { 154 | return rwrapf(Minor, "minimum Go version changed from %s to %s", oldMod.GoVersion, newMod.GoVersion) 155 | } 156 | } 157 | } 158 | 159 | var ( 160 | topObjs = makeTopObjs(pkg) 161 | oldTopObjs map[string]types.Object 162 | ) 163 | 164 | for id, obj := range topObjs { 165 | if !isExported(id) { 166 | continue 167 | } 168 | if oldPkg == nil { 169 | return rwrapf(Minor, "no old version of package %s", pkgPath) 170 | } 171 | if oldTopObjs == nil { 172 | oldTopObjs = makeTopObjs(oldPkg) 173 | } 174 | oldObj := oldTopObjs[id] 175 | if oldObj == nil { 176 | return rwrapf(Minor, "no object %s in old version of package %s", id, pkgPath) 177 | } 178 | if res := c.compareTypes(oldObj.Type(), obj.Type()); res.Code() >= Minor { 179 | return rwrapf(res.sub(Minor), "checking %s", id) 180 | } 181 | } 182 | } 183 | 184 | return nil 185 | } 186 | 187 | func (c *comparer) comparePatchlevel(older, newer map[string]*packages.Package) Result { 188 | for pkgPath, pkg := range older { 189 | var ( 190 | topObjs = makeTopObjs(pkg) 191 | newPkg = newer[pkgPath] 192 | ) 193 | if newPkg == nil { 194 | return rwrapf(Patchlevel, "no new version of package %s", pkgPath) 195 | } 196 | newTopObjs := makeTopObjs(newPkg) 197 | for id, obj := range topObjs { 198 | newObj := newTopObjs[id] 199 | if newObj == nil { 200 | return rwrapf(Patchlevel, "no object %s in new version of package %s", id, pkgPath) 201 | } 202 | if res := c.compareTypes(obj.Type(), newObj.Type()); res.Code() != None { 203 | return rwrapf(res.sub(Patchlevel), "checking %s", id) 204 | } 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // CompareDirs loads Go modules from the directories at older and newer 212 | // and calls Compare on the results. 213 | func CompareDirs(older, newer string) (Result, error) { 214 | cfg := &packages.Config{ 215 | Mode: packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedModule, 216 | Dir: older, 217 | } 218 | olders, err := packages.Load(cfg, "./...") 219 | if err != nil { 220 | return None, fmt.Errorf("loading %s/...: %w", older, err) 221 | } 222 | for _, p := range olders { 223 | if len(p.Errors) > 0 { 224 | return None, errpkg{pkg: p} 225 | } 226 | } 227 | 228 | cfg.Dir = newer 229 | newers, err := packages.Load(cfg, "./...") 230 | if err != nil { 231 | return None, fmt.Errorf("loading %s/...: %w", newer, err) 232 | } 233 | for _, p := range newers { 234 | if len(p.Errors) > 0 { 235 | return None, errpkg{pkg: p} 236 | } 237 | } 238 | 239 | return Compare(olders, newers), nil 240 | } 241 | 242 | type errpkg struct { 243 | pkg *packages.Package 244 | } 245 | 246 | func (p errpkg) Error() string { 247 | strs := make([]string, 0, len(p.pkg.Errors)) 248 | for _, e := range p.pkg.Errors { 249 | strs = append(strs, e.Error()) 250 | } 251 | return fmt.Sprintf("error(s) loading package %s: %s", p.pkg.PkgPath, strings.Join(strs, "; ")) 252 | } 253 | 254 | // CompareGit compares the Go packages in two revisions of a Git repository at the given URL. 255 | func CompareGit(ctx context.Context, repoURL, olderRev, newerRev string) (Result, error) { 256 | return CompareGitWith(ctx, repoURL, olderRev, newerRev, CompareDirs) 257 | } 258 | 259 | // CompareGit2 compares the Go packages in one revision each of two Git repositories. 260 | func CompareGit2(ctx context.Context, olderRepoURL, olderRev, newerRepoURL, newerRev string) (Result, error) { 261 | return CompareGit2With(ctx, olderRepoURL, olderRev, newerRepoURL, newerRev, CompareDirs) 262 | } 263 | 264 | // CompareGitWith compares the Go packages in two revisions of a Git repository at the given URL. 265 | // It uses the given callback function to perform the comparison. 266 | // 267 | // The callback function receives the paths to two directories, 268 | // containing two clones of the repo: 269 | // one checked out at the older revision 270 | // and one checked out at the newer revision. 271 | // 272 | // Note that CompareGit(...) is simply CompareGitWith(..., CompareDirs). 273 | func CompareGitWith(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (Result, error)) (Result, error) { 274 | return CompareGit2With(ctx, repoURL, olderRev, repoURL, newerRev, f) 275 | } 276 | 277 | // CompareGit2With compares the Go packages in one revision each of two Git repositories. 278 | // It uses the given callback function to perform the comparison. 279 | // 280 | // The callback function receives the paths to two directories, 281 | // each containing a clone of one of the repositories at its selected revision. 282 | // 283 | // Note that CompareGit2(...) is simply CompareGit2With(..., CompareDirs). 284 | func CompareGit2With(ctx context.Context, olderRepoURL, olderRev, newerRepoURL, newerRev string, f func(older, newer string) (Result, error)) (Result, error) { 285 | parent, err := os.MkdirTemp("", "modver") 286 | if err != nil { 287 | return None, fmt.Errorf("creating tmpdir: %w", err) 288 | } 289 | defer os.RemoveAll(parent) 290 | 291 | olderDir := filepath.Join(parent, "older") 292 | newerDir := filepath.Join(parent, "newer") 293 | 294 | err = gitSetup(ctx, olderRepoURL, olderDir, olderRev) 295 | if err != nil { 296 | return None, fmt.Errorf("setting up older clone: %w", err) 297 | } 298 | 299 | err = gitSetup(ctx, newerRepoURL, newerDir, newerRev) 300 | if err != nil { 301 | return None, fmt.Errorf("setting up newer clone: %w", err) 302 | } 303 | 304 | return f(olderDir, newerDir) 305 | } 306 | 307 | func gitSetup(ctx context.Context, repoURL, dir, rev string) error { 308 | err := os.Mkdir(dir, 0755) 309 | if err != nil { 310 | return fmt.Errorf("creating %s: %w", dir, err) 311 | } 312 | 313 | gitCmd := GetGit(ctx) 314 | if gitCmd != "" { 315 | found, err := exec.LookPath(gitCmd) 316 | if err != nil { 317 | fmt.Fprintf(os.Stderr, "Cannot resolve git command %s, falling back to go-git library: %s\n", gitCmd, err) 318 | gitCmd = "" 319 | } else { 320 | gitCmd = found 321 | } 322 | } 323 | 324 | if gitCmd != "" { 325 | cmd := exec.CommandContext(ctx, gitCmd, "clone", repoURL, dir) 326 | err = cmd.Run() 327 | if err != nil { 328 | return fmt.Errorf("native git cloning %s into %s: %w", repoURL, dir, err) 329 | } 330 | 331 | cmd = exec.CommandContext(ctx, gitCmd, "checkout", rev) 332 | cmd.Dir = dir 333 | if err := cmd.Run(); err != nil { 334 | return fmt.Errorf("in native git checkout %s: %w", rev, err) 335 | } 336 | } else { 337 | cloneOpts := &git.CloneOptions{URL: repoURL, NoCheckout: true} 338 | repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts) 339 | if err != nil { 340 | return cloneBugErr{repoURL: repoURL, dir: dir, err: err} 341 | } 342 | worktree, err := repo.Worktree() 343 | if err != nil { 344 | return fmt.Errorf("getting worktree from %s: %w", dir, err) 345 | } 346 | hash, err := repo.ResolveRevision(plumbing.Revision(rev)) 347 | if err != nil { 348 | return fmt.Errorf(`resolving revision "%s": %w`, rev, err) 349 | } 350 | err = worktree.Checkout(&git.CheckoutOptions{Hash: *hash}) 351 | if err != nil { 352 | return fmt.Errorf(`checking out "%s" in %s: %w`, rev, dir, err) 353 | } 354 | } 355 | 356 | return nil 357 | } 358 | 359 | type cloneBugErr struct { 360 | repoURL, dir string 361 | err error 362 | } 363 | 364 | func (cb cloneBugErr) Error() string { 365 | return fmt.Sprintf("cloning %s into %s: %s", cb.repoURL, cb.dir, cb.err) 366 | } 367 | 368 | func (cb cloneBugErr) Unwrap() error { 369 | return cb.err 370 | } 371 | 372 | // Calls ast.IsExported on the final element of name 373 | // (which may be package/type-qualified). 374 | func isExported(name string) bool { 375 | if i := strings.LastIndex(name, "."); i > 0 { 376 | name = name[i+1:] 377 | } 378 | return ast.IsExported(name) 379 | } 380 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import "context" 4 | 5 | type ( 6 | gitKeyType struct{} 7 | ) 8 | 9 | // WithGit decorates a context with the value of the gitPath string. 10 | // This is the path of an executable to use for Git operations in calls to CompareGit. 11 | // Without it, the go-git library is used. 12 | // (But a git program is preferable.) 13 | // Retrieve it with GetGit. 14 | func WithGit(ctx context.Context, gitPath string) context.Context { 15 | return context.WithValue(ctx, gitKeyType{}, gitPath) 16 | } 17 | 18 | // GetGit returns the value of the gitPath string added to `ctx` with WithGit. 19 | // If the key is not set the default value is an empty string. 20 | func GetGit(ctx context.Context) string { 21 | val, _ := ctx.Value(gitKeyType{}).(string) 22 | return val 23 | } 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package modver compares two versions of the same Go module. 2 | // It can tell whether the differences require at least a patchlevel version change, 3 | // or a minor version change, 4 | // or a major version change, 5 | // according to semver rules 6 | // (https://semver.org/). 7 | package modver 8 | -------------------------------------------------------------------------------- /embed_test.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/bobg/errors" 12 | "golang.org/x/tools/go/packages" 13 | ) 14 | 15 | //go:embed *.go go.* 16 | var embedded embed.FS 17 | 18 | func withGoFiles(f func(string) error) error { 19 | tmpdir, err := os.MkdirTemp("", "modver") 20 | if err != nil { 21 | return errors.Wrap(err, "creating tempdir") 22 | } 23 | defer os.RemoveAll(tmpdir) 24 | 25 | entries, err := embedded.ReadDir(".") 26 | if err != nil { 27 | return errors.Wrap(err, "reading embedded dir") 28 | } 29 | 30 | for _, entry := range entries { 31 | name := entry.Name() 32 | if strings.HasSuffix(name, "_test.go") { 33 | continue 34 | } 35 | if err = copyToTmpdir(tmpdir, name); err != nil { 36 | return errors.Wrapf(err, "copying embedded file %s to tmpdir %s", name, tmpdir) 37 | } 38 | } 39 | 40 | return f(tmpdir) 41 | } 42 | 43 | func copyToTmpdir(tmpdir string, filename string) error { 44 | in, err := embedded.Open(filename) 45 | if err != nil { 46 | return errors.Wrapf(err, "opening embedded file %s", filename) 47 | } 48 | defer in.Close() 49 | 50 | destname := filepath.Join(tmpdir, filename) 51 | out, err := os.OpenFile(destname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 52 | if err != nil { 53 | return errors.Wrapf(err, "opening %s for writing", destname) 54 | } 55 | defer out.Close() 56 | 57 | _, err = io.Copy(out, in) 58 | return errors.Wrap(err, "copying data") 59 | } 60 | 61 | func withPackage(f func(pkg *packages.Package) error) error { 62 | return withGoFiles(func(tmpdir string) error { 63 | config := &packages.Config{ 64 | Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes | packages.NeedModule | packages.NeedEmbedFiles | packages.NeedEmbedPatterns, 65 | Dir: tmpdir, 66 | } 67 | pkgs, err := packages.Load(config, ".") 68 | if err != nil { 69 | return errors.Wrapf(err, "loading Go package from %s", tmpdir) 70 | } 71 | if len(pkgs) != 1 { 72 | return fmt.Errorf("loading Go package in %s, got %d packages, want 1", tmpdir, len(pkgs)) 73 | } 74 | pkg := pkgs[0] 75 | return f(pkg) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bobg/modver/v2 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/bobg/errors v1.1.0 9 | github.com/go-git/go-git/v5 v5.12.0 10 | github.com/google/go-github/v50 v50.2.0 11 | golang.org/x/mod v0.23.0 12 | golang.org/x/oauth2 v0.22.0 13 | golang.org/x/tools v0.30.0 14 | ) 15 | 16 | require ( 17 | dario.cat/mergo v1.0.0 // indirect 18 | github.com/Microsoft/go-winio v0.6.2 // indirect 19 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 20 | github.com/cloudflare/circl v1.3.9 // indirect 21 | github.com/cyphar/filepath-securejoin v0.3.1 // indirect 22 | github.com/emirpasic/gods v1.18.1 // indirect 23 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 24 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 26 | github.com/google/go-querystring v1.1.0 // indirect 27 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 28 | github.com/kevinburke/ssh_config v1.2.0 // indirect 29 | github.com/pjbgf/sha1cd v0.3.0 // indirect 30 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 31 | github.com/skeema/knownhosts v1.3.0 // indirect 32 | github.com/xanzy/ssh-agent v0.3.3 // indirect 33 | golang.org/x/crypto v0.33.0 // indirect 34 | golang.org/x/net v0.35.0 // indirect 35 | golang.org/x/sync v0.11.0 // indirect 36 | golang.org/x/sys v0.30.0 // indirect 37 | gopkg.in/warnings.v0 v0.1.2 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 7 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/bobg/errors v1.1.0 h1:gsVanPzJMpZQpwY+27/GQYElZez5CuMYwiIpk2A3RGw= 13 | github.com/bobg/errors v1.1.0/go.mod h1:Q4775qBZpnte7EGFJqmvnlB1U4pkI1XmU3qxqdp7Zcc= 14 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 15 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 16 | github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= 17 | github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 18 | github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= 19 | github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 24 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 25 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 26 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 27 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 28 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 29 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 30 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 31 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 32 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 33 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 34 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 35 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 36 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 39 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 41 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk= 43 | github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= 44 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 45 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 46 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 47 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 48 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 49 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 51 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 52 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 58 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 59 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 60 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 61 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 62 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 66 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 67 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 68 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 69 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 70 | github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= 71 | github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 76 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 77 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 78 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 79 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 82 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 83 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 84 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 85 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 86 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 87 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 88 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 89 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 90 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 91 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 92 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 93 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 94 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 95 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 96 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 97 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 98 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 99 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 100 | golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= 101 | golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 106 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 107 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 121 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 122 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 123 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 124 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 125 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 126 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 127 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 128 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 130 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 131 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 132 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 133 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 134 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 135 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 136 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 137 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 138 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 139 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 140 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 141 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 142 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 143 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 144 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 147 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 148 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 149 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 150 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 151 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 154 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 155 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 156 | -------------------------------------------------------------------------------- /identical.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import "go/types" 4 | 5 | // https://golang.org/ref/spec#Type_identity 6 | func (c *comparer) identical(a, b types.Type) (res bool) { 7 | tp := typePair{a: a, b: b} 8 | if res, ok := c.identicache[tp]; ok { 9 | return res 10 | } 11 | // identical(a, b) = identical(b, a) 12 | tp.a, tp.b = b, a 13 | if res, ok := c.identicache[tp]; ok { 14 | return res 15 | } 16 | defer func() { c.identicache[tp] = res }() 17 | 18 | if types.Identical(a, b) { 19 | return true 20 | } 21 | 22 | // Break any infinite regress, 23 | // e.g. when checking type Node struct { children []*Node } 24 | for _, pair := range c.stack { 25 | if a == pair.a && b == pair.b { 26 | return true 27 | } 28 | if a == pair.b && b == pair.a { 29 | return true 30 | } 31 | } 32 | c.stack = append(c.stack, tp) 33 | defer func() { c.stack = c.stack[:len(c.stack)-1] }() 34 | 35 | if na, ok := a.(*types.Named); ok { 36 | if nb, ok := b.(*types.Named); ok { 37 | if na.Obj().Name() != nb.Obj().Name() { 38 | return false 39 | } 40 | if !c.identicalTypeParamLists(na.TypeParams(), nb.TypeParams()) { 41 | return false 42 | } 43 | // Can't return true yet just because the types have equal names. 44 | // Continue to checking their underlying types. 45 | } else { 46 | return false 47 | } 48 | } 49 | 50 | ua, ub := a.Underlying(), b.Underlying() 51 | 52 | if types.Identical(ua, ub) { 53 | return true 54 | } 55 | 56 | return c.underlyingIdentical(ua, ub) 57 | } 58 | 59 | func (c *comparer) identicalTypeParamLists(a, b *types.TypeParamList) bool { 60 | if a.Len() != b.Len() { 61 | return false 62 | } 63 | for i := 0; i < a.Len(); i++ { 64 | if !c.identicalConstraint(a.At(i).Constraint(), b.At(i).Constraint()) { 65 | return false 66 | } 67 | } 68 | return true 69 | } 70 | 71 | func (c *comparer) identicalConstraint(a, b types.Type) bool { 72 | if na, ok := a.(*types.Named); ok { 73 | a = na.Underlying() 74 | } 75 | if nb, ok := b.(*types.Named); ok { 76 | b = nb.Underlying() 77 | } 78 | if types.Identical(a, b) { 79 | return true 80 | } 81 | return c.underlyingIdentical(a, b) 82 | } 83 | 84 | func (c *comparer) underlyingIdentical(ua, ub types.Type) bool { 85 | switch ua := ua.(type) { 86 | 87 | case *types.Array: 88 | // Two array types are identical if they have identical element types and the same array length. 89 | if ub, ok := ub.(*types.Array); ok { 90 | return ua.Len() == ub.Len() && c.identical(ua.Elem(), ub.Elem()) 91 | } 92 | return false 93 | 94 | case *types.Slice: 95 | // Two slice types are identical if they have identical element types. 96 | if ub, ok := ub.(*types.Slice); ok { 97 | return c.identical(ua.Elem(), ub.Elem()) 98 | } 99 | return false 100 | 101 | case *types.Struct: 102 | return c.identicalStructs(ua, ub) 103 | 104 | case *types.Pointer: 105 | // Two pointer types are identical if they have identical base types. 106 | if ub, ok := ub.(*types.Pointer); ok { 107 | return c.identical(ua.Elem(), ub.Elem()) 108 | } 109 | return false 110 | 111 | case *types.Signature: 112 | // Two function types are identical if they have the same number of parameters and result values, 113 | // corresponding parameter and result types are identical, 114 | // and either both functions are variadic or neither is. 115 | // Parameter and result names are not required to match. 116 | if ub, ok := ub.(*types.Signature); ok { 117 | return c.identicalSigs(ua, ub) 118 | } 119 | return false 120 | 121 | case *types.Interface: 122 | return c.identicalInterfaces(ua, ub) 123 | 124 | case *types.Map: 125 | return c.identicalMaps(ua, ub) 126 | 127 | case *types.Chan: 128 | return c.identicalChans(ua, ub) 129 | } 130 | 131 | return false 132 | } 133 | 134 | func (c *comparer) identicalStructs(ua *types.Struct, b types.Type) bool { 135 | ub, ok := b.(*types.Struct) 136 | if !ok { 137 | return false 138 | } 139 | 140 | // Two struct types are identical if they have the same sequence of fields, 141 | // and if corresponding fields have the same names, 142 | // and identical types, 143 | // and identical tags. 144 | // Non-exported field names from different packages are always different. 145 | 146 | if ua.NumFields() != ub.NumFields() { 147 | return false 148 | } 149 | for i := 0; i < ua.NumFields(); i++ { 150 | if ua.Tag(i) != ub.Tag(i) { 151 | return false 152 | } 153 | 154 | fa, fb := ua.Field(i), ub.Field(i) 155 | 156 | if fa.Name() != fb.Name() { 157 | return false 158 | } 159 | if !fa.Exported() && !c.samePackage(fa.Pkg(), fb.Pkg()) { 160 | return false 161 | } 162 | if !c.identical(fa.Type(), fb.Type()) { 163 | return false 164 | } 165 | } 166 | return true 167 | } 168 | 169 | func (c *comparer) identicalSigs(older, newer *types.Signature) bool { 170 | return c.compareSignatures(older, newer).Code() == None 171 | } 172 | 173 | func (c *comparer) identicalInterfaces(ua *types.Interface, b types.Type) bool { 174 | ub, ok := b.(*types.Interface) 175 | if !ok { 176 | return false 177 | } 178 | 179 | if ua.IsMethodSet() != ub.IsMethodSet() { 180 | return false 181 | } 182 | 183 | if ua.IsComparable() != ub.IsComparable() { 184 | return false 185 | } 186 | 187 | // Two interface types are identical if they have the same set of methods with the same names and identical function types. 188 | // Non-exported method names from different packages are always different. 189 | // The order of the methods is irrelevant. 190 | 191 | if ua.NumMethods() != ub.NumMethods() { // Warning: this panics on incomplete interfaces. 192 | return false 193 | } 194 | 195 | ma, mb := methodMap(ua), methodMap(ub) 196 | 197 | for aname, afn := range ma { 198 | bfn, ok := mb[aname] 199 | if !ok { 200 | return false 201 | } 202 | if !afn.Exported() && !c.samePackage(afn.Pkg(), bfn.Pkg()) { 203 | return false 204 | } 205 | if !c.identical(afn.Type(), bfn.Type()) { 206 | return false 207 | } 208 | } 209 | 210 | if ua.IsMethodSet() { 211 | return true 212 | } 213 | 214 | return types.Implements(ua, ub) && types.Implements(ub, ua) 215 | } 216 | 217 | func (c *comparer) identicalMaps(ua *types.Map, b types.Type) bool { 218 | ub, ok := b.(*types.Map) 219 | if !ok { 220 | return false 221 | } 222 | 223 | // Two map types are identical if they have identical key and element types. 224 | if !c.identical(ua.Key(), ub.Key()) { 225 | return false 226 | } 227 | return c.identical(ua.Elem(), ub.Elem()) 228 | } 229 | 230 | func (c *comparer) identicalChans(ua *types.Chan, b types.Type) bool { 231 | ub, ok := b.(*types.Chan) 232 | if !ok { 233 | return false 234 | } 235 | 236 | // Two channel types are identical if they have identical element types and the same direction. 237 | if ua.Dir() != ub.Dir() { 238 | return false 239 | } 240 | return c.identical(ua.Elem(), ub.Elem()) 241 | } 242 | -------------------------------------------------------------------------------- /identical_test.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "testing" 7 | 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | func TestIdenticalArray(t *testing.T) { 12 | err := withPackage(func(pkg *packages.Package) error { 13 | var ( 14 | scope = pkg.Types.Scope() 15 | resultType = scope.Lookup("Result").Type() 16 | resultCodeType = scope.Lookup("ResultCode").Type() 17 | ) 18 | 19 | var ( 20 | a1 = types.NewArray(resultType, 7) 21 | a2 = types.NewArray(resultType, 7) 22 | a3 = types.NewArray(resultType, 11) 23 | a4 = types.NewArray(resultCodeType, 7) 24 | ) 25 | 26 | cases := []struct { 27 | // t1 is always a1 28 | t2 *types.Array 29 | want bool 30 | }{{ 31 | t2: a2, 32 | want: true, 33 | }, { 34 | t2: a3, 35 | want: false, 36 | }, { 37 | t2: a4, 38 | want: false, 39 | }} 40 | 41 | for i, tc := range cases { 42 | t.Run(fmt.Sprintf("case_%d", i+1), func(t *testing.T) { 43 | c := newComparer() 44 | if got := c.identical(a1, tc.t2); got != tc.want { 45 | t.Errorf("got %v, want %v", got, tc.want) 46 | } 47 | }) 48 | } 49 | return nil 50 | }) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | 56 | func TestIdenticalChan(t *testing.T) { 57 | err := withPackage(func(pkg *packages.Package) error { 58 | var ( 59 | scope = pkg.Types.Scope() 60 | resultType = scope.Lookup("Result").Type() 61 | resultCodeType = scope.Lookup("ResultCode").Type() 62 | ) 63 | 64 | chans := []*types.Chan{ 65 | types.NewChan(types.SendRecv, resultType), 66 | types.NewChan(types.SendOnly, resultType), 67 | types.NewChan(types.RecvOnly, resultType), 68 | 69 | types.NewChan(types.SendRecv, resultCodeType), 70 | types.NewChan(types.SendOnly, resultCodeType), 71 | types.NewChan(types.RecvOnly, resultCodeType), 72 | } 73 | 74 | for i := 0; i < len(chans); i++ { 75 | for j := i; j < len(chans); j++ { 76 | c := newComparer() 77 | if got := c.identical(chans[i], chans[j]); got != (i == j) { 78 | t.Errorf("case %d/%d: got %v, want %v", i, j, got, i == j) 79 | } 80 | } 81 | } 82 | 83 | return nil 84 | }) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/comment.md.tmpl: -------------------------------------------------------------------------------- 1 | # Modver result 2 | 3 | This report was generated by [Modver](https://pkg.go.dev/github.com/bobg/modver/v2), 4 | a Go package and command that helps you obey [semantic versioning rules](https://semver.org/) in your Go module. 5 | 6 | {{ if eq .Code "Major" }} 7 | 8 | This PR requires an increase in your module’s major version number. 9 | If the new major version number is 2 or greater, 10 | you must also add or update the version suffix 11 | on the module path defined in your `go.mod` file. 12 | See [the Go Modules Reference](https://go.dev/ref/mod#major-version-suffixes) for more info. 13 | 14 | {{ else if eq .Code "Minor" }} 15 | 16 | This PR requires (at least) an increase in your module's minor version number. 17 | 18 | {{ else if eq .Code "Patchlevel" }} 19 | 20 | This PR requires (at least) an increase in your module's patchlevel. 21 | 22 | {{ else }} 23 | 24 | This PR does not require a change in your module’s version number. 25 | (You might still consider bumping the patchlevel anyway.) 26 | 27 | {{ end }} 28 | 29 | {{ if ne .Code "None" }} 30 | 31 | ``` 32 | {{ .Report -}} 33 | ``` 34 | 35 | {{ end }} 36 | -------------------------------------------------------------------------------- /internal/github.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/bobg/errors" 11 | "github.com/google/go-github/v50/github" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | // ParsePR parses a GitHub pull-request URL, 16 | // which should have the form http(s)://HOST/OWNER/REPO/pull/NUMBER. 17 | func ParsePR(pr string) (host, owner, reponame string, prnum int, err error) { 18 | u, err := url.Parse(pr) 19 | if err != nil { 20 | err = errors.Wrap(err, "parsing GitHub pull-request URL") 21 | return 22 | } 23 | path := strings.TrimLeft(u.Path, "/") 24 | parts := strings.Split(path, "/") 25 | if len(parts) < 4 { 26 | err = fmt.Errorf("too few path elements in pull-request URL (got %d, want 4)", len(parts)) 27 | return 28 | } 29 | if parts[2] != "pull" { 30 | err = fmt.Errorf("pull-request URL not in expected format") 31 | return 32 | } 33 | host = u.Host 34 | owner, reponame = parts[0], parts[1] 35 | prnum, err = strconv.Atoi(parts[3]) 36 | err = errors.Wrap(err, "parsing number from GitHub pull-request URL") 37 | return 38 | } 39 | 40 | // NewClient creates a new GitHub client talking to the given host and authenticated with the given token. 41 | func NewClient(ctx context.Context, host, token string) (*github.Client, error) { 42 | if strings.ToLower(host) == "github.com" { 43 | return github.NewTokenClient(ctx, token), nil 44 | } 45 | oClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})) 46 | u := "https://" + host 47 | return github.NewEnterpriseClient(u, u, oClient) 48 | } 49 | -------------------------------------------------------------------------------- /internal/github_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParsePR(t *testing.T) { 9 | cases := []struct { 10 | inp string 11 | wantErr bool 12 | host, owner, reponame string 13 | prnum int 14 | }{{ 15 | wantErr: true, 16 | }, { 17 | inp: "https://x/y", 18 | wantErr: true, 19 | }, { 20 | inp: "https://github.com/bobg/modver/bleah/17", 21 | wantErr: true, 22 | }, { 23 | inp: "https://github.com/bobg/modver/pull/17", 24 | host: "github.com", 25 | owner: "bobg", 26 | reponame: "modver", 27 | prnum: 17, 28 | }} 29 | 30 | for i, tc := range cases { 31 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 32 | host, owner, reponame, prnum, err := ParsePR(tc.inp) 33 | if err != nil { 34 | if !tc.wantErr { 35 | t.Errorf("got error %v, wanted no error", err) 36 | } 37 | return 38 | } 39 | if tc.wantErr { 40 | t.Fatal("got no error but wanted one") 41 | } 42 | if host != tc.host { 43 | t.Errorf("got host %s, want %s", host, tc.host) 44 | } 45 | if owner != tc.owner { 46 | t.Errorf("got owner %s, want %s", owner, tc.owner) 47 | } 48 | if reponame != tc.reponame { 49 | t.Errorf("got repo %s, want %s", reponame, tc.reponame) 50 | } 51 | if prnum != tc.prnum { 52 | t.Errorf("got PR number %d, want %d", prnum, tc.prnum) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/pr.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | _ "embed" 8 | "io" 9 | "regexp" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/bobg/errors" 14 | "github.com/google/go-github/v50/github" 15 | 16 | "github.com/bobg/modver/v2" 17 | ) 18 | 19 | // PR performs modver analysis on a GitHub pull request. 20 | func PR(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { 21 | return prHelper(ctx, gh.Repositories, gh.PullRequests, gh.Issues, modver.CompareGit2, owner, reponame, prnum) 22 | } 23 | 24 | type reposIntf interface { 25 | Get(ctx context.Context, owner, reponame string) (*github.Repository, *github.Response, error) 26 | } 27 | 28 | type prsIntf interface { 29 | Get(ctx context.Context, owner, reponame string, number int) (*github.PullRequest, *github.Response, error) 30 | } 31 | 32 | type issuesIntf interface { 33 | createCommenter 34 | editCommenter 35 | ListComments(ctx context.Context, owner, reponame string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) 36 | } 37 | 38 | func prHelper(ctx context.Context, repos reposIntf, prs prsIntf, issues issuesIntf, comparer func(ctx context.Context, baseURL, baseSHA, headURL, headSHA string) (modver.Result, error), owner, reponame string, prnum int) (modver.Result, error) { 39 | repo, _, err := repos.Get(ctx, owner, reponame) 40 | if err != nil { 41 | return modver.None, errors.Wrap(err, "getting repository") 42 | } 43 | pr, _, err := prs.Get(ctx, owner, reponame, prnum) 44 | if err != nil { 45 | return modver.None, errors.Wrap(err, "getting pull request") 46 | } 47 | result, err := comparer(ctx, *pr.Base.Repo.CloneURL, *pr.Base.SHA, *pr.Head.Repo.CloneURL, *pr.Head.SHA) 48 | if err != nil { 49 | return modver.None, errors.Wrap(err, "comparing versions") 50 | } 51 | comments, _, err := issues.ListComments(ctx, owner, reponame, prnum, nil) 52 | if err != nil { 53 | return modver.None, errors.Wrap(err, "listing PR comments") 54 | } 55 | 56 | for _, c := range comments { 57 | if isModverComment(c) { 58 | err = updateComment(ctx, issues, repo, c, result) 59 | return result, errors.Wrap(err, "updating PR comment") 60 | } 61 | } 62 | 63 | err = createComment(ctx, issues, repo, pr, result) 64 | return result, errors.Wrap(err, "creating PR comment") 65 | } 66 | 67 | var modverCommentRegex = regexp.MustCompile(`^# Modver result$`) 68 | 69 | func isModverComment(comment *github.IssueComment) bool { 70 | var r io.Reader = strings.NewReader(*comment.Body) 71 | r = &io.LimitedReader{R: r, N: 1024} 72 | sc := bufio.NewScanner(r) 73 | for sc.Scan() { 74 | if modverCommentRegex.MatchString(sc.Text()) { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | type createCommenter interface { 82 | CreateComment(ctx context.Context, owner, reponame string, num int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) 83 | } 84 | 85 | func createComment(ctx context.Context, issues createCommenter, repo *github.Repository, pr *github.PullRequest, result modver.Result) error { 86 | body, err := commentBody(result) 87 | if err != nil { 88 | return errors.Wrap(err, "rendering comment body") 89 | } 90 | comment := &github.IssueComment{ 91 | Body: &body, 92 | } 93 | _, _, err = issues.CreateComment(ctx, *repo.Owner.Login, *repo.Name, *pr.Number, comment) 94 | return errors.Wrap(err, "creating GitHub comment") 95 | } 96 | 97 | type editCommenter interface { 98 | EditComment(ctx context.Context, owner, reponame string, commentID int64, newComment *github.IssueComment) (*github.IssueComment, *github.Response, error) 99 | } 100 | 101 | func updateComment(ctx context.Context, issues editCommenter, repo *github.Repository, comment *github.IssueComment, result modver.Result) error { 102 | body, err := commentBody(result) 103 | if err != nil { 104 | return errors.Wrap(err, "rendering comment body") 105 | } 106 | newComment := &github.IssueComment{ 107 | Body: &body, 108 | } 109 | _, _, err = issues.EditComment(ctx, *repo.Owner.Login, *repo.Name, *comment.ID, newComment) 110 | return errors.Wrap(err, "editing GitHub comment") 111 | } 112 | 113 | //go:embed comment.md.tmpl 114 | var commentTplStr string 115 | 116 | var commentTpl = template.Must(template.New("").Parse(commentTplStr)) 117 | 118 | func commentBody(result modver.Result) (string, error) { 119 | report := new(bytes.Buffer) 120 | modver.Pretty(report, result) 121 | 122 | s := struct { 123 | Code string 124 | Report string 125 | }{ 126 | Code: result.Code().String(), 127 | Report: report.String(), 128 | } 129 | 130 | out := new(bytes.Buffer) 131 | err := commentTpl.Execute(out, s) 132 | return out.String(), err 133 | } 134 | -------------------------------------------------------------------------------- /internal/pr_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-github/v50/github" 9 | 10 | "github.com/bobg/modver/v2" 11 | ) 12 | 13 | func TestPRHelper(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | repos mockReposService 17 | prs mockPRsService 18 | ) 19 | 20 | t.Run("new-comment", func(t *testing.T) { 21 | var issues mockIssuesService 22 | 23 | result, err := prHelper(ctx, repos, prs, &issues, mockComparer(modver.Minor), "owner", "repo", 17) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if result.Code() != modver.Minor { 28 | t.Fatalf("got result %s, want %s", result, modver.Minor) 29 | } 30 | if !strings.HasPrefix(issues.body, "# Modver result") { 31 | t.Error("issues.body does not start with # Modver result") 32 | } 33 | if issues.commentID != 0 { 34 | t.Errorf("issues.commentID is %d, want 0", issues.commentID) 35 | } 36 | }) 37 | 38 | t.Run("new-comment", func(t *testing.T) { 39 | issues := mockIssuesService{update: true} 40 | 41 | result, err := prHelper(ctx, repos, prs, &issues, mockComparer(modver.Minor), "owner", "repo", 17) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if result.Code() != modver.Minor { 46 | t.Fatalf("got result %s, want %s", result, modver.Minor) 47 | } 48 | if !strings.HasPrefix(issues.body, "# Modver result") { 49 | t.Error("issues.body does not start with # Modver result") 50 | } 51 | if issues.commentID != 2 { 52 | t.Errorf("issues.commentID is %d, want 0", issues.commentID) 53 | } 54 | }) 55 | } 56 | 57 | type mockReposService struct{} 58 | 59 | func (mockReposService) Get(ctx context.Context, owner, reponame string) (*github.Repository, *github.Response, error) { 60 | return &github.Repository{ 61 | Owner: &github.User{ 62 | Login: &owner, 63 | }, 64 | Name: &reponame, 65 | CloneURL: ptr("cloneURL"), 66 | }, nil, nil 67 | } 68 | 69 | type mockPRsService struct{} 70 | 71 | func (mockPRsService) Get(ctx context.Context, owner, reponame string, number int) (*github.PullRequest, *github.Response, error) { 72 | return &github.PullRequest{ 73 | Base: &github.PullRequestBranch{ 74 | Repo: &github.Repository{ 75 | CloneURL: ptr("baseURL"), 76 | }, 77 | SHA: ptr("baseSHA"), 78 | }, 79 | Head: &github.PullRequestBranch{ 80 | Repo: &github.Repository{ 81 | CloneURL: ptr("headURL"), 82 | }, 83 | SHA: ptr("headSHA"), 84 | }, 85 | Number: ptr(17), 86 | }, nil, nil 87 | } 88 | 89 | type mockIssuesService struct { 90 | update bool 91 | owner, repo string 92 | commentID int64 93 | body string 94 | } 95 | 96 | func (m *mockIssuesService) CreateComment(ctx context.Context, owner, reponame string, num int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { 97 | m.owner = owner 98 | m.repo = reponame 99 | m.commentID = 0 100 | m.body = *comment.Body 101 | return nil, nil, nil 102 | } 103 | 104 | func (m *mockIssuesService) EditComment(ctx context.Context, owner, reponame string, commentID int64, newComment *github.IssueComment) (*github.IssueComment, *github.Response, error) { 105 | m.owner = owner 106 | m.repo = reponame 107 | m.commentID = commentID 108 | m.body = *newComment.Body 109 | return nil, nil, nil 110 | } 111 | 112 | func (m *mockIssuesService) ListComments(ctx context.Context, owner, reponame string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { 113 | result := []*github.IssueComment{{ 114 | ID: ptr(int64(1)), 115 | Body: ptr("not a modver comment"), 116 | }} 117 | if m.update { 118 | result = append(result, &github.IssueComment{ 119 | ID: ptr(int64(2)), 120 | Body: ptr("# Modver result\n\nwoop"), 121 | }) 122 | } 123 | return result, nil, nil 124 | } 125 | 126 | func mockComparer(result modver.Result) func(_ context.Context, _, _, _, _ string) (modver.Result, error) { 127 | return func(_ context.Context, _, _, _, _ string) (modver.Result, error) { 128 | return result, nil 129 | } 130 | } 131 | 132 | func ptr[T any](x T) *T { 133 | return &x 134 | } 135 | -------------------------------------------------------------------------------- /modver_test.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | "text/template" 15 | 16 | "github.com/bobg/errors" 17 | ) 18 | 19 | func TestCompare(t *testing.T) { 20 | tbCompare(t) 21 | } 22 | 23 | func BenchmarkCompare(b *testing.B) { 24 | tbCompare(b) 25 | } 26 | 27 | func tbCompare(tb testing.TB) { 28 | cases := []struct { 29 | dir string 30 | want ResultCode 31 | }{{ 32 | dir: "major", want: Major, 33 | }, { 34 | dir: "minor", want: Minor, 35 | }, { 36 | dir: "patchlevel", want: Patchlevel, 37 | }, { 38 | dir: "none", want: None, 39 | }} 40 | 41 | for _, c := range cases { 42 | runtest(tb, c.dir, c.want) 43 | } 44 | } 45 | 46 | func tbRun(tb testing.TB, name string, f func(testing.TB)) { 47 | switch tb := tb.(type) { 48 | case *testing.T: 49 | tb.Run(name, func(t *testing.T) { f(tb) }) 50 | case *testing.B: 51 | tb.Run(name, func(b *testing.B) { f(tb) }) 52 | } 53 | } 54 | 55 | func runtest(tb testing.TB, typ string, want ResultCode) { 56 | b, _ := tb.(*testing.B) 57 | 58 | tree := filepath.Join("testdata", typ) 59 | entries, err := os.ReadDir(tree) 60 | if err != nil { 61 | tb.Fatal(err) 62 | } 63 | for _, entry := range entries { 64 | if entry.IsDir() { 65 | continue 66 | } 67 | if !strings.HasSuffix(entry.Name(), ".tmpl") { 68 | continue 69 | } 70 | name := strings.TrimSuffix(entry.Name(), ".tmpl") 71 | tbRun(tb, fmt.Sprintf("%s/%s", typ, name), func(tb testing.TB) { 72 | err := withTestDirs(tree, name, func(olderTestDir, newerTestDir string) { 73 | if b != nil { 74 | b.ResetTimer() 75 | for i := 0; i < b.N; i++ { 76 | _, err := CompareDirs(olderTestDir, newerTestDir) 77 | if err != nil { 78 | b.Fatal(err) 79 | } 80 | } 81 | return 82 | } 83 | 84 | got, err := CompareDirs(olderTestDir, newerTestDir) 85 | if err != nil { 86 | tb.Fatal(err) 87 | } 88 | if got.Code() != want { 89 | tb.Errorf("want %s, got %s", want, got) 90 | } else { 91 | tb.Log(got) 92 | } 93 | }) 94 | if err != nil { 95 | tb.Fatal(err) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func withTestDirs(tree, name string, f func(olderTestDir, newerTestDir string)) error { 102 | tmpls, err := template.ParseFiles(filepath.Join(tree, name+".tmpl")) 103 | if err != nil { 104 | return errors.Wrap(err, "parsing templates") 105 | } 106 | 107 | tmpdir, err := os.MkdirTemp("", "modver") 108 | if err != nil { 109 | return errors.Wrap(err, "creating temp dir") 110 | } 111 | defer os.RemoveAll(tmpdir) 112 | 113 | var ( 114 | olderTestDir = filepath.Join(tmpdir, "older") 115 | newerTestDir = filepath.Join(tmpdir, "newer") 116 | ) 117 | 118 | if err = os.Mkdir(olderTestDir, 0755); err != nil { 119 | return errors.Wrap(err, "creating older test dir") 120 | } 121 | if err = os.Mkdir(newerTestDir, 0755); err != nil { 122 | return errors.Wrap(err, "creating newer test dir") 123 | } 124 | 125 | var sawGomod bool 126 | for _, tmpl := range tmpls.Templates() { 127 | // Skip the top-level template 128 | if strings.HasSuffix(tmpl.Name(), ".tmpl") { 129 | continue 130 | } 131 | 132 | sawGomod = sawGomod || (filepath.Base(tmpl.Name()) == "go.mod") 133 | 134 | parts := strings.Split(tmpl.Name(), "/") 135 | if !strings.Contains(parts[len(parts)-1], ".") { 136 | parts = append(parts, "x.go") 137 | } 138 | 139 | if len(parts) == 1 { 140 | // Only a filename is given. 141 | // Write it to both older and newer dirs. 142 | buf := new(bytes.Buffer) 143 | if err = executeTmpl(tmpl, buf); err != nil { 144 | return errors.Wrap(err, "executing template") 145 | } 146 | for _, subdir := range []string{olderTestDir, newerTestDir} { 147 | filename := filepath.Join(subdir, parts[0]) 148 | if err = os.WriteFile(filename, buf.Bytes(), 0644); err != nil { 149 | return errors.Wrapf(err, "writing file %s", filename) 150 | } 151 | } 152 | continue 153 | } 154 | 155 | if len(parts) > 1 { 156 | dirparts := append([]string{tmpdir}, parts[:len(parts)-1]...) 157 | dirname := filepath.Join(dirparts...) 158 | if err = os.MkdirAll(dirname, 0755); err != nil { 159 | return errors.Wrapf(err, "creating dir %s", dirname) 160 | } 161 | } 162 | fileparts := append([]string{tmpdir}, parts...) 163 | filename := filepath.Join(fileparts...) 164 | if err = executeTmplToFile(tmpl, filename); err != nil { 165 | return errors.Wrapf(err, "executing template to file %s", filename) 166 | } 167 | } 168 | if !sawGomod { 169 | buf := new(bytes.Buffer) 170 | fmt.Fprintf(buf, "module %s\n\ngo 1.18\n", name) 171 | if err = os.WriteFile(filepath.Join(olderTestDir, "go.mod"), buf.Bytes(), 0644); err != nil { 172 | return errors.Wrap(err, "writing older go.mod") 173 | } 174 | if err = os.WriteFile(filepath.Join(newerTestDir, "go.mod"), buf.Bytes(), 0644); err != nil { 175 | return errors.Wrap(err, "writing newer go.mod") 176 | } 177 | } 178 | 179 | f(olderTestDir, newerTestDir) 180 | 181 | return nil 182 | } 183 | 184 | func executeTmpl(tmpl *template.Template, w io.Writer) error { 185 | pr, pw := io.Pipe() 186 | go func() { 187 | err := tmpl.Execute(pw, nil) 188 | if err != nil { 189 | log.Printf("Error executing template: %s\n", err) 190 | } 191 | pw.Close() 192 | }() 193 | 194 | sc := bufio.NewScanner(pr) 195 | for sc.Scan() { 196 | line := sc.Text() 197 | line = strings.TrimPrefix(line, "//// ") 198 | fmt.Fprintln(w, line) 199 | } 200 | return sc.Err() 201 | } 202 | 203 | func executeTmplToFile(tmpl *template.Template, filename string) error { 204 | f, err := os.Create(filename) 205 | if err != nil { 206 | return err 207 | } 208 | defer f.Close() 209 | return executeTmpl(tmpl, f) 210 | } 211 | 212 | func TestGit(t *testing.T) { 213 | pwd, err := os.Getwd() 214 | if err != nil { 215 | t.Fatal(err) 216 | } 217 | 218 | gitDir := filepath.Join(pwd, ".git") 219 | _, err = os.Stat(gitDir) 220 | if os.IsNotExist(err) { 221 | t.Skip() 222 | } 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | 227 | ctx := context.Background() 228 | 229 | // Do it once with the go-git library. 230 | res, err := CompareGit(ctx, gitDir, "HEAD", "HEAD") 231 | var cberr cloneBugErr 232 | if errors.As(err, &cberr) { 233 | // Workaround for an apparent bug in go-git. See https://github.com/go-git/go-git/issues/726. 234 | t.Logf("Encountered clone bug, trying workaround: %s", cberr) 235 | res, err = CompareGit(ctx, "https://github.com/bobg/modver", "HEAD", "HEAD") 236 | } 237 | if err != nil { 238 | t.Fatal(err) 239 | } 240 | if res.Code() != None { 241 | t.Errorf("want None, got %s", res) 242 | } 243 | 244 | // Now with the git binary. 245 | ctx = WithGit(ctx, "git") 246 | res, err = CompareGit(ctx, gitDir, "HEAD", "HEAD") 247 | if err != nil { 248 | t.Fatal(err) 249 | } 250 | if res.Code() != None { 251 | t.Errorf("want None, got %s", res) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /public_test.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIsPublic(t *testing.T) { 9 | cases := []struct { 10 | inp string 11 | want bool 12 | }{{ 13 | inp: "main", 14 | want: false, 15 | }, { 16 | inp: "internal", 17 | want: false, 18 | }, { 19 | inp: "mainx", 20 | want: true, 21 | }, { 22 | inp: "internalx", 23 | want: true, 24 | }, { 25 | inp: "foo/main", 26 | want: false, 27 | }, { 28 | inp: "main/foo", 29 | want: true, 30 | }, { 31 | inp: "foo/mainx", 32 | want: true, 33 | }, { 34 | inp: "mainx/foo", 35 | want: true, 36 | }, { 37 | inp: "foo/internal", 38 | want: false, 39 | }, { 40 | inp: "internal/foo", 41 | want: false, 42 | }, { 43 | inp: "foo/internal/bar", 44 | want: false, 45 | }, { 46 | inp: "foo/internalx", 47 | want: true, 48 | }, { 49 | inp: "internalx/foo", 50 | want: true, 51 | }, { 52 | inp: "foo/xinternal/bar", 53 | want: true, 54 | }, { 55 | inp: "foo/xinternal", 56 | want: true, 57 | }, { 58 | inp: "xinternal/foo", 59 | want: true, 60 | }, { 61 | inp: "foo/xinternal/bar", 62 | want: true, 63 | }} 64 | for i, tc := range cases { 65 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 66 | got := isPublic(tc.inp) 67 | if got != tc.want { 68 | t.Errorf("got %v, want %v", got, tc.want) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // Result is the result of Compare. 10 | type Result interface { 11 | Code() ResultCode 12 | String() string 13 | 14 | sub(code ResultCode) Result 15 | } 16 | 17 | // ResultCode is the required version-bump level as detected by Compare. 18 | type ResultCode int 19 | 20 | // Values for ResultCode. 21 | const ( 22 | None ResultCode = iota 23 | Patchlevel 24 | Minor 25 | Major 26 | ) 27 | 28 | // Code implements Result.Code. 29 | func (r ResultCode) Code() ResultCode { return r } 30 | func (r ResultCode) sub(code ResultCode) Result { return code } 31 | 32 | // String implements Result.String. 33 | func (r ResultCode) String() string { 34 | switch r { 35 | case None: 36 | return "None" 37 | case Patchlevel: 38 | return "Patchlevel" 39 | case Minor: 40 | return "Minor" 41 | case Major: 42 | return "Major" 43 | default: 44 | return "unknown Result value" 45 | } 46 | } 47 | 48 | func (r ResultCode) MarshalText() ([]byte, error) { 49 | switch r { 50 | case None, Patchlevel, Minor, Major: 51 | return []byte(r.String()), nil 52 | } 53 | return nil, fmt.Errorf("unknown ResultCode value %d", r) 54 | } 55 | 56 | func (r *ResultCode) UnmarshalText(text []byte) error { 57 | switch string(text) { 58 | case "None": 59 | *r = None 60 | case "Patchlevel": 61 | *r = Patchlevel 62 | case "Minor": 63 | *r = Minor 64 | case "Major": 65 | *r = Major 66 | default: 67 | return fmt.Errorf("unknown ResultCode value %q", text) 68 | } 69 | return nil 70 | } 71 | 72 | type wrapped struct { 73 | r Result 74 | whyfmt string 75 | whyargs []any 76 | } 77 | 78 | // Code implements Result.Code. 79 | func (w wrapped) Code() ResultCode { return w.r.Code() } 80 | func (w wrapped) sub(code ResultCode) Result { 81 | result := w 82 | result.r = w.r.sub(code) 83 | return result 84 | } 85 | 86 | func (w wrapped) why() string { 87 | return fmt.Sprintf(w.whyfmt, w.whyargs...) 88 | } 89 | 90 | // String implements Result.String. 91 | func (w wrapped) String() string { 92 | return fmt.Sprintf("%s: %s", w.r, w.why()) 93 | } 94 | 95 | func (w wrapped) pretty(out io.Writer, level int) { 96 | fmt.Fprintf(out, "%s%s\n", strings.Repeat(" ", level), w.why()) 97 | prettyLevel(out, w.r, level+1) 98 | } 99 | 100 | func rwrap(r Result, s string) Result { 101 | return rwrapf(r, "%s", s) 102 | } 103 | 104 | func rwrapf(r Result, format string, args ...any) Result { 105 | if r.Code() == None { 106 | return r 107 | } 108 | return wrapped{r: r, whyfmt: format, whyargs: args} 109 | } 110 | 111 | type prettyer interface { 112 | pretty(io.Writer, int) 113 | } 114 | 115 | // Pretty writes a pretty representation of res to out. 116 | func Pretty(out io.Writer, res Result) { 117 | prettyLevel(out, res, 0) 118 | } 119 | 120 | func prettyLevel(out io.Writer, res Result, level int) { 121 | if p, ok := res.(prettyer); ok { 122 | p.pretty(out, level) 123 | } else { 124 | fmt.Fprintf(out, "%s%s\n", strings.Repeat(" ", level), res) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /result_test.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestPretty(t *testing.T) { 11 | buf := new(bytes.Buffer) 12 | Pretty(buf, Minor) 13 | if buf.String() != "Minor\n" { 14 | t.Errorf("got %s, want Minor\\n", buf) 15 | } 16 | 17 | buf.Reset() 18 | 19 | res := rwrap(Minor, "foo") 20 | Pretty(buf, res) 21 | const want = "foo\n Minor\n" 22 | if buf.String() != want { 23 | t.Errorf("got %s, want %s", buf, want) 24 | } 25 | } 26 | 27 | func TestMarshalResultCode(t *testing.T) { 28 | cases := []struct { 29 | rc ResultCode 30 | want string 31 | wantErr bool 32 | }{{ 33 | rc: None, 34 | want: `"None"`, 35 | }, { 36 | rc: Patchlevel, 37 | want: `"Patchlevel"`, 38 | }, { 39 | rc: Minor, 40 | want: `"Minor"`, 41 | }, { 42 | rc: Major, 43 | want: `"Major"`, 44 | }, { 45 | rc: ResultCode(42), 46 | wantErr: true, 47 | }} 48 | 49 | for i, tc := range cases { 50 | t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { 51 | got, err := json.Marshal(tc.rc) 52 | if err != nil && tc.wantErr { 53 | return 54 | } 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | if tc.wantErr { 59 | t.Fatal("got no error but want one") 60 | } 61 | if string(got) != tc.want { 62 | t.Errorf("marshaling: got %s, want %s", string(got), tc.want) 63 | } 64 | 65 | var rc ResultCode 66 | if err := json.Unmarshal(got, &rc); err != nil { 67 | t.Fatal(err) 68 | } 69 | if rc != tc.rc { 70 | t.Errorf("unmarshaling: got %v, want %v", rc, tc.rc) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /term.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | // This file duplicates logic from go/types that is sadly unexported. 4 | 5 | import "go/types" 6 | 7 | // termSubset reports whether x ⊆ y. 8 | func (c *comparer) termSubset(x, y *types.Term) bool { 9 | // easy cases 10 | switch { 11 | case x == nil: 12 | return true // ∅ ⊆ y == true 13 | case y == nil: 14 | return false // x ⊆ ∅ == false since x != ∅ 15 | case y.Type() == nil: 16 | return true // x ⊆ 𝓤 == true 17 | case x.Type() == nil: 18 | return false // 𝓤 ⊆ y == false since y != 𝓤 19 | } 20 | // ∅ ⊂ x, y ⊂ 𝓤 21 | 22 | if c.termDisjoint(x, y) { 23 | return false // x ⊆ y == false if x ∩ y == ∅ 24 | } 25 | // x.typ == y.typ 26 | 27 | // ~t ⊆ ~t == true 28 | // ~t ⊆ T == false 29 | // T ⊆ ~t == true 30 | // T ⊆ T == true 31 | return !x.Tilde() || y.Tilde() 32 | } 33 | 34 | // termDisjoint reports whether x ∩ y == ∅. 35 | // x.typ and y.typ must not be nil. 36 | func (c *comparer) termDisjoint(x, y *types.Term) bool { 37 | ux := x.Type() 38 | if y.Tilde() { 39 | ux = ux.Underlying() 40 | } 41 | uy := y.Type() 42 | if x.Tilde() { 43 | uy = uy.Underlying() 44 | } 45 | return !c.identical(ux, uy) 46 | } 47 | 48 | // termListSubset reports whether xl ⊆ yl. 49 | func (c *comparer) termListSubset(xl, yl []*types.Term) bool { 50 | if len(yl) == 0 { 51 | return len(xl) == 0 52 | } 53 | 54 | // each term x of xl must be a subset of yl 55 | for _, x := range xl { 56 | if !c.termListSuperset(yl, x) { 57 | return false // x is not a subset yl 58 | } 59 | } 60 | return true 61 | } 62 | 63 | // termListSuperset reports whether y ⊆ xl. 64 | func (c *comparer) termListSuperset(xl []*types.Term, y *types.Term) bool { 65 | for _, x := range xl { 66 | if c.termSubset(y, x) { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | -------------------------------------------------------------------------------- /testdata/Readme.md: -------------------------------------------------------------------------------- 1 | Files in this testdata tree are Go text templates, 2 | each producing an “older” version of a Go package and a “newer” version. 3 | The `runtest` function in `modver_test.go` compares the resulting two packages. 4 | 5 | Files in the `major` subdir are expected to produce a `Major` result when older and newer are compared. 6 | Files in the `minor`, `patchlevel`, and `none` subdirs 7 | are expected to produce `Minor`, `Patchlevel`, and `None` results. 8 | 9 | Each `.tmpl` file defines a number of named templates using the `{{ define "name" }}` construct. 10 | The name is the relative path of a file to create for testing. 11 | “Older” package files go in the subdir `older`. 12 | “Newer” package files go in the subdir `newer`. 13 | 14 | If the path does not end in a filename 15 | (that is, a path element containing a `.`), 16 | then the name `x.go` is assumed. 17 | 18 | If the path is _only_ a filename and no subdir part, 19 | then the file is copied to both `older` and `newer` subdirs. 20 | 21 | If no template with the name `go.mod` is seen, 22 | a `go.mod` file will be synthesized in `older` and `newer`. 23 | 24 | When copying template contents to their designated files, 25 | any lines with the prefix `//// ` 26 | (four slashes and a space) 27 | will first have that prefix removed. 28 | -------------------------------------------------------------------------------- /testdata/major/addfntypeparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package addfntypeparam 6 | 7 | func F[X any]() {} 8 | 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | 13 | package addfntypeparam 14 | 15 | func F[X, Y any]() {} 16 | 17 | //// {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/addmethod.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addmethod 5 | 6 | type X interface { 7 | A() int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package addmethod 13 | 14 | type X interface { 15 | A() int 16 | B() string 17 | } 18 | // {{ end }} 19 | -------------------------------------------------------------------------------- /testdata/major/addparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package addparam 6 | 7 | func F() {} 8 | 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | 13 | package addparam 14 | 15 | func F(x int) {} 16 | 17 | //// {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/addresult.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package addresult 6 | 7 | func F() {} 8 | 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | 13 | package addresult 14 | 15 | func F() int { return 0 } 16 | 17 | //// {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/addtypeparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | 5 | package addtypeparam 6 | 7 | type T[X any] struct { 8 | F func(X) X 9 | } 10 | 11 | // {{ end }} 12 | 13 | // {{ define "newer" }} 14 | 15 | package addtypeparam 16 | 17 | type T[X, Y any] struct { 18 | F func(X) Y 19 | } 20 | 21 | // {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/major/alltosomecomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package alltosomecomparable 5 | 6 | type X interface { 7 | comparable 8 | } 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | package alltosomecomparable 13 | 14 | type X interface { 15 | comparable 16 | int 17 | } 18 | //// {{ end }} 19 | -------------------------------------------------------------------------------- /testdata/major/anytocomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package anytocomparable 5 | 6 | type X any 7 | //// {{ end }} 8 | 9 | //// {{ define "newer" }} 10 | package anytocomparable 11 | 12 | type X interface { 13 | comparable 14 | } 15 | //// {{ end }} 16 | -------------------------------------------------------------------------------- /testdata/major/anytosomecomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package anytosomecomparable 5 | 6 | type X any 7 | //// {{ end }} 8 | 9 | //// {{ define "newer" }} 10 | package anytosomecomparable 11 | 12 | type X interface { 13 | comparable 14 | int 15 | } 16 | //// {{ end }} 17 | -------------------------------------------------------------------------------- /testdata/major/changetype.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package changetype 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package changetype 11 | 12 | var X string 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/charraylen.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package charraylen 5 | 6 | var X [10]int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package charraylen 11 | 12 | var X [12]int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/charraytype.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package charraytype 5 | 6 | var X [10]int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package charraytype 11 | 12 | var X [10]string 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/chchandir.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chchandir 5 | 6 | var X <-chan int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package chchandir 11 | 12 | var X chan<- int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/chconstant.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chconstant 5 | 6 | const X = 7 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package chconstant 11 | 12 | const X = "foo" 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/chfield.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chfield 5 | 6 | type X struct { 7 | A int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package chfield 13 | 14 | type X struct { 15 | A string 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/chintf.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chintf 5 | 6 | type X interface { 7 | A() int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package chintf 13 | 14 | type X interface { 15 | B() string 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/chmodname.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older/go.mod" }} 4 | //// module chmodname 5 | //// go 1.17 6 | //// {{ end }} 7 | 8 | //// {{ define "newer/go.mod" }} 9 | //// module newname 10 | //// go 1.17 11 | //// {{ end }} 12 | 13 | //// {{ define "older" }} 14 | package chmodname 15 | 16 | const X = 7 17 | //// {{ end }} 18 | 19 | //// {{ define "newer" }} 20 | package chmodname 21 | 22 | const X = 8 23 | //// {{ end }} 24 | -------------------------------------------------------------------------------- /testdata/major/chparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package chparam 6 | 7 | func F(int) {} 8 | 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | 13 | package chparam 14 | 15 | func F(string) {} 16 | 17 | //// {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/chtag.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chtag 5 | 6 | type X struct { 7 | A int `foo:"bar"` 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package chtag 13 | 14 | type X struct { 15 | A int `foo:"baz"` 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/diffunions.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package diffunions 5 | 6 | type X interface { 7 | int | uint 8 | } 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | package diffunions 13 | 14 | type X interface { 15 | string | []byte 16 | } 17 | //// {{ end }} -------------------------------------------------------------------------------- /testdata/major/fromcomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package fromcomparable 5 | 6 | type X interface { 7 | comparable 8 | Y() 9 | } 10 | //// {{ end }} 11 | 12 | //// {{ define "newer" }} 13 | package fromcomparable 14 | 15 | type X interface { 16 | []byte 17 | Y() 18 | } 19 | //// {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/major/fromconstraint.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package fromconstraint 5 | 6 | type X interface { 7 | int 8 | Y() 9 | } 10 | //// {{ end }} 11 | 12 | //// {{ define "newer" }} 13 | package fromconstraint 14 | 15 | type X interface { 16 | Y() 17 | } 18 | //// {{ end }} -------------------------------------------------------------------------------- /testdata/major/pointer.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package pointer 6 | 7 | type X struct { 8 | x int 9 | } 10 | 11 | func PrintX(x *X) { 12 | print(x.x) 13 | } 14 | 15 | //// {{ end }} 16 | 17 | //// {{ define "newer" }} 18 | 19 | package pointer 20 | 21 | type X struct { 22 | x int 23 | } 24 | 25 | func PrintX(x X) { 26 | print(x.x) 27 | } 28 | 29 | //// {{ end }} 30 | 31 | -------------------------------------------------------------------------------- /testdata/major/remove.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package remove 5 | 6 | var X, Y int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package remove 11 | 12 | var X int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/rmfield.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmfield 5 | 6 | type A struct { 7 | X, Y int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package rmfield 13 | 14 | type A struct { 15 | X int 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/rmmethod.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmmethod 5 | 6 | type X int 7 | 8 | func (X) M1() {} 9 | func (X) M2() {} 10 | // {{ end }} 11 | 12 | // {{ define "newer" }} 13 | package rmmethod 14 | 15 | type X int 16 | 17 | func (X) M1() {} 18 | // {{ end }} 19 | -------------------------------------------------------------------------------- /testdata/major/rmpackage.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmpackage 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "older/subpkg" }} 10 | package subpkg 11 | 12 | var Y int 13 | // {{ end }} 14 | 15 | // {{ define "newer" }} 16 | package rmpackage 17 | 18 | var X int 19 | // {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/major/rmtag.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmtag 5 | 6 | type X struct { 7 | A int `foo:"bar" baz:"quux"` 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package rmtag 13 | 14 | type X struct { 15 | A int `foo:"bar"` 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/rmunion.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package rmunion 6 | 7 | type X interface { 8 | []byte 9 | } 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package rmunion 16 | 17 | type X interface { 18 | } 19 | 20 | //// {{ end }} 21 | -------------------------------------------------------------------------------- /testdata/major/tightenconstraint1.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package tightenconstraint 5 | 6 | type T[X any] struct { 7 | Val X 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package tightenconstraint 13 | 14 | type T[X comparable] struct { 15 | Val X 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/tightenconstraint2.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package tightenconstraint2 5 | 6 | type T[X ~int | ~string] struct { 7 | Val X 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package tightenconstraint2 13 | 14 | type T[X ~int] struct { 15 | Val X 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/major/tocomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package tocomparable 5 | 6 | type X interface { 7 | []byte 8 | Y() 9 | } 10 | //// {{ end }} 11 | 12 | //// {{ define "newer" }} 13 | package tocomparable 14 | 15 | type X interface { 16 | comparable 17 | Y() 18 | } 19 | //// {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/major/toconstraint.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package toconstraint 5 | 6 | type X interface { 7 | Y() 8 | } 9 | //// {{ end }} 10 | 11 | //// {{ define "newer" }} 12 | package toconstraint 13 | 14 | type X interface { 15 | int 16 | Y() 17 | } 18 | //// {{ end }} -------------------------------------------------------------------------------- /testdata/major/tononfunc.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package tononfunc 5 | 6 | func X() {} 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package tononfunc 11 | 12 | var X int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/major/tononintf.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package tononintf 5 | 6 | type X interface { 7 | A() int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package tononintf 13 | 14 | type X int 15 | // {{ end }} 16 | -------------------------------------------------------------------------------- /testdata/major/tononstruct.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package tononstruct 5 | 6 | type X struct { 7 | A int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package tononstruct 13 | 14 | type X int 15 | // {{ end }} 16 | -------------------------------------------------------------------------------- /testdata/major/unassignablechan.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package unassignablechan 6 | 7 | type X chan int 8 | 9 | var Y chan int 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package unassignablechan 16 | 17 | type X chan int 18 | 19 | var Y X 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/minor/add.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package add 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package add 11 | 12 | var X, Y int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/minor/addfield.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addfield 5 | 6 | type A struct { 7 | X int 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package addfield 13 | 14 | type A struct { 15 | X, Y int 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/minor/addmethod1.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addmethod1 5 | 6 | type X interface { 7 | A() int 8 | unexported() 9 | } 10 | // {{ end }} 11 | 12 | // {{ define "newer" }} 13 | package addmethod1 14 | 15 | type X interface { 16 | A() int 17 | B() string 18 | unexported() 19 | } 20 | // {{ end }} 21 | -------------------------------------------------------------------------------- /testdata/minor/addmethod2.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older/internal/q.go" }} 4 | package internal 5 | 6 | type Q int 7 | // {{ end }} 8 | 9 | // {{ define "newer/internal/q.go" }} 10 | package internal 11 | 12 | type Q int 13 | // {{ end }} 14 | 15 | // {{ define "older" }} 16 | package addmethod2 17 | 18 | import "addmethod2/internal" 19 | 20 | type X interface { 21 | A() internal.Q 22 | } 23 | // {{ end }} 24 | 25 | // {{ define "newer" }} 26 | package addmethod2 27 | 28 | import "addmethod2/internal" 29 | 30 | type X interface { 31 | A() internal.Q 32 | B() string 33 | } 34 | // {{ end }} 35 | -------------------------------------------------------------------------------- /testdata/minor/addoptparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addoptparam 5 | 6 | func X(a int) {} 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package addoptparam 11 | 12 | func X(a int, b ...string) {} 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/minor/addpackage.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addpackage 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package addpackage 11 | 12 | var X int 13 | // {{ end }} 14 | 15 | // {{ define "newer/subpkg" }} 16 | package subpkg 17 | 18 | var Y int 19 | // {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/minor/addtag.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package addtag 5 | 6 | type X struct { 7 | A int `foo:"bar"` 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package addtag 13 | 14 | type X struct { 15 | A int `foo:"bar" baz:"quux"` 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/minor/basexx.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // Older parts are from github.com/bobg/basexx at commit dbebfe56b6709535c4458efe67f07e39036e601f. 4 | // Newer parts are from github.com/bobg/basexx at commit 17b80a746b356ae36343e0c4712191ff7331175f. 5 | 6 | // {{ define "older/alnum.go" }} 7 | package basexx 8 | 9 | // Alnum is a type for bases from 2 through 36, 10 | // where the digits for the first 10 digit values are '0' through '9' 11 | // and the remaining digits are 'a' through 'z'. 12 | // For decoding, upper-case 'A' through 'Z' are the same as lower-case. 13 | type Alnum int 14 | 15 | func (a Alnum) N() int64 { return int64(a) } 16 | 17 | func (a Alnum) Encode(val int64) ([]byte, error) { 18 | if val < 0 || val >= int64(a) { 19 | return nil, ErrInvalid 20 | } 21 | if val < 10 { 22 | return []byte{byte(val) + '0'}, nil 23 | } 24 | return []byte{byte(val) + 'a' - 10}, nil 25 | } 26 | 27 | func (a Alnum) Decode(inp []byte) (int64, error) { 28 | if len(inp) != 1 { 29 | return 0, ErrInvalid 30 | } 31 | digit := byte(inp[0]) 32 | switch { 33 | case '0' <= digit && digit <= '9': 34 | return int64(digit - '0'), nil 35 | case 'a' <= digit && digit <= 'z': 36 | return int64(digit - 'a' + 10), nil 37 | case 'A' <= digit && digit <= 'Z': 38 | return int64(digit - 'A' + 10), nil 39 | default: 40 | return 0, ErrInvalid 41 | } 42 | } 43 | 44 | const ( 45 | Base2 = Alnum(2) 46 | Base8 = Alnum(8) 47 | Base10 = Alnum(10) 48 | Base12 = Alnum(12) 49 | Base16 = Alnum(16) 50 | Base32 = Alnum(32) 51 | Base36 = Alnum(36) 52 | ) 53 | // {{ end }} 54 | 55 | // {{ define "older/base50.go" }} 56 | package basexx 57 | 58 | const base50digits = "0123456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ" 59 | 60 | var base50digitVals [256]int64 61 | 62 | type base50 struct{} 63 | 64 | func (b base50) N() int64 { return 50 } 65 | 66 | func (b base50) Encode(val int64) ([]byte, error) { 67 | if val < 0 || val > 49 { 68 | return nil, ErrInvalid 69 | } 70 | return []byte{byte(base50digits[val])}, nil 71 | } 72 | 73 | func (b base50) Decode(inp []byte) (int64, error) { 74 | if len(inp) != 1 { 75 | return 0, ErrInvalid 76 | } 77 | val := base50digitVals[inp[0]] 78 | if val < 0 { 79 | return 0, ErrInvalid 80 | } 81 | return val, nil 82 | } 83 | 84 | // Base50 uses digits 0-9, then lower-case bcdfghjkmnpqrstvwxyz, then upper-case BCDFGHJKMNPQRSTVWXYZ. 85 | // It excludes vowels (to avoid inadvertently spelling naughty words) plus lower- and upper-case L. 86 | var Base50 base50 87 | 88 | func init() { 89 | for i := 0; i < 256; i++ { 90 | base50digitVals[i] = -1 91 | } 92 | for i := 0; i < len(base50digits); i++ { 93 | base50digitVals[base50digits[i]] = int64(i) 94 | } 95 | } 96 | // {{ end }} 97 | 98 | // {{ define "older/base62.go" }} 99 | package basexx 100 | 101 | type base62 struct{} 102 | 103 | func (b base62) N() int64 { return 62 } 104 | 105 | func (b base62) Encode(val int64) ([]byte, error) { 106 | if val < 0 || val > 61 { 107 | return nil, ErrInvalid 108 | } 109 | if val < 10 { 110 | return []byte{byte(val) + '0'}, nil 111 | } 112 | if val < 36 { 113 | return []byte{byte(val) - 10 + 'a'}, nil 114 | } 115 | return []byte{byte(val) - 36 + 'A'}, nil 116 | } 117 | 118 | func (b base62) Decode(inp []byte) (int64, error) { 119 | if len(inp) != 1 { 120 | return 0, ErrInvalid 121 | } 122 | digit := byte(inp[0]) 123 | switch { 124 | case '0' <= digit && digit <= '9': 125 | return int64(digit - '0'), nil 126 | case 'a' <= digit && digit <= 'z': 127 | return int64(digit - 'a' + 10), nil 128 | case 'A' <= digit && digit <= 'Z': 129 | return int64(digit - 'A' + 36), nil 130 | default: 131 | return 0, ErrInvalid 132 | } 133 | } 134 | 135 | // Base62 uses digits 0..9, then a..z, then A..Z. 136 | var Base62 base62 137 | // {{ end }} 138 | 139 | // {{ define "older/base94.go" }} 140 | package basexx 141 | 142 | type base94 struct{} 143 | 144 | func (b base94) N() int64 { return 94 } 145 | 146 | func (b base94) Encode(val int64) ([]byte, error) { 147 | if val < 0 || val > 93 { 148 | return nil, ErrInvalid 149 | } 150 | return []byte{byte(val + 33)}, nil 151 | } 152 | 153 | func (b base94) Decode(inp []byte) (int64, error) { 154 | if len(inp) != 1 { 155 | return 0, ErrInvalid 156 | } 157 | digit := inp[0] 158 | if digit < 33 || digit > 126 { 159 | return 0, ErrInvalid 160 | } 161 | return int64(digit - 33), nil 162 | } 163 | 164 | // Base94 uses all printable ASCII characters (33 through 126) as digits. 165 | var Base94 base94 166 | // {{ end }} 167 | 168 | // {{ define "older/basexx.go" }} 169 | // Package basexx permits converting between digit strings of arbitrary bases. 170 | package basexx 171 | 172 | import ( 173 | "errors" 174 | "io" 175 | "math" 176 | "math/big" 177 | ) 178 | 179 | // Source is a source of digit values in a given base. 180 | type Source interface { 181 | // Read produces the value of the next-least-significant digit in the source. 182 | // The value must be between 0 and Base()-1, inclusive. 183 | // End of input is signaled with the error io.EOF. 184 | Read() (int64, error) 185 | 186 | // Base gives the base of the Source. 187 | // Digit values in the Source must all be between 0 and Base()-1, inclusive. 188 | // Behavior is undefined if the value of Base() varies during the lifetime of a Source 189 | // or if Base() < 2. 190 | Base() int64 191 | } 192 | 193 | // Dest is a destination for writing digits in a given base. 194 | // Digits are written right-to-left, from least significant to most. 195 | type Dest interface { 196 | // Prepend encodes the next-most-significant digit value and prepends it to the destination. 197 | Prepend(int64) error 198 | 199 | // Base gives the base of the Dest. 200 | // Digit values in the Dest must all be between 0 and Base()-1, inclusive. 201 | // Behavior is undefined if the value of Base() varies during the lifetime of a Dest 202 | // or if Base() < 2. 203 | Base() int64 204 | } 205 | 206 | // Base is the type of a base. 207 | type Base interface { 208 | // N is the number of the base, 209 | // i.e. the number of unique digits. 210 | // Behavior is undefined if the value of N() varies during the lifetime of a Base 211 | // or if N() < 2. 212 | N() int64 213 | 214 | // Encode converts a digit value to the string of bytes representing its digit. 215 | // The input must be a valid digit value between 0 and N()-1, inclusive. 216 | Encode(int64) ([]byte, error) 217 | 218 | // Decode converts a string of bytes representing a digit into its numeric value. 219 | Decode([]byte) (int64, error) 220 | } 221 | 222 | // ErrInvalid is used for invalid input to Base.Encode and Base.Decode. 223 | var ErrInvalid = errors.New("invalid") 224 | 225 | var zero = new(big.Int) 226 | 227 | // Convert converts the digits of src, writing them to dest. 228 | // Both src and dest specify their bases. 229 | // Return value is the number of digits written to dest (even in case of error). 230 | // This function consumes all of src before producing any of dest, 231 | // so it may not be suitable for input streams of arbitrary length. 232 | func Convert(dest Dest, src Source) (int, error) { 233 | var ( 234 | accum = new(big.Int) 235 | srcBase = big.NewInt(src.Base()) 236 | destBase = big.NewInt(dest.Base()) 237 | ) 238 | for { 239 | digit, err := src.Read() 240 | if err == io.EOF { 241 | break 242 | } 243 | if err != nil { 244 | return 0, err 245 | } 246 | accum.Mul(accum, srcBase) 247 | if digit != 0 { 248 | accum.Add(accum, big.NewInt(digit)) 249 | } 250 | } 251 | var written int 252 | for accum.Cmp(zero) > 0 { 253 | r := new(big.Int) 254 | accum.QuoRem(accum, destBase, r) 255 | err := dest.Prepend(r.Int64()) 256 | if err != nil { 257 | return written, err 258 | } 259 | written++ 260 | } 261 | if written == 0 { 262 | err := dest.Prepend(0) 263 | if err != nil { 264 | return written, err 265 | } 266 | written++ 267 | } 268 | return written, nil 269 | } 270 | 271 | // Length computes the maximum number of digits needed 272 | // to convert `n` digits in base `from` to base `to`. 273 | func Length(from, to int64, n int) int { 274 | ratio := math.Log(float64(from)) / math.Log(float64(to)) 275 | result := float64(n) * ratio 276 | return int(math.Ceil(result)) 277 | } 278 | // {{ end }} 279 | 280 | // {{ define "older/binary.go" }} 281 | package basexx 282 | 283 | type binary struct{} 284 | 285 | func (b binary) N() int64 { return 256 } 286 | 287 | func (b binary) Encode(val int64) ([]byte, error) { 288 | if val < 0 || val > 255 { 289 | return nil, ErrInvalid 290 | } 291 | return []byte{byte(val)}, nil 292 | } 293 | 294 | func (b binary) Decode(inp []byte) (int64, error) { 295 | if len(inp) != 1 { 296 | return 0, ErrInvalid 297 | } 298 | return int64(inp[0]), nil 299 | } 300 | 301 | // Binary is base 256 encoded the obvious way: digit value X = byte(X). 302 | var Binary binary 303 | // {{ end }} 304 | 305 | // {{ define "older/buffer.go" }} 306 | package basexx 307 | 308 | import "io" 309 | 310 | // Buffer can act as a Source or a Dest (but not both at the same time) 311 | // in the case where each byte in a given slice encodes a single digit in the desired base. 312 | // The digits in the buffer are in the expected order: 313 | // namely, most-significant first, least-significant last. 314 | type Buffer struct { 315 | buf []byte 316 | next int 317 | base Base 318 | } 319 | 320 | // NewBuffer produces a Buffer from the given byte slice described by the given Base. 321 | func NewBuffer(buf []byte, base Base) *Buffer { 322 | return &Buffer{ 323 | buf: buf, 324 | next: -1, // "unstarted" sentinel value 325 | base: base, 326 | } 327 | } 328 | 329 | func (s *Buffer) Read() (int64, error) { 330 | if s.next < 0 { 331 | s.next = 0 332 | } 333 | if s.next >= len(s.buf) { 334 | return 0, io.EOF 335 | } 336 | dec, err := s.base.Decode([]byte{s.buf[s.next]}) 337 | if err != nil { 338 | return 0, err 339 | } 340 | s.next++ 341 | return dec, nil 342 | } 343 | 344 | func (s *Buffer) Prepend(val int64) error { 345 | if s.next < 0 { 346 | s.next = len(s.buf) 347 | } 348 | if s.next == 0 { 349 | return io.EOF 350 | } 351 | enc, err := s.base.Encode(val) 352 | if err != nil { 353 | return err 354 | } 355 | if len(enc) != 1 { 356 | return ErrInvalid 357 | } 358 | s.next-- 359 | s.buf[s.next] = enc[0] 360 | return nil 361 | } 362 | 363 | func (s *Buffer) Written() []byte { 364 | if s.next < 0 { 365 | return nil 366 | } 367 | return s.buf[s.next:] 368 | } 369 | 370 | func (s *Buffer) Base() int64 { 371 | return s.base.N() 372 | } 373 | // {{ end }} 374 | 375 | // {{ define "newer/alnum.go" }} 376 | package basexx 377 | 378 | // Alnum is a type for bases from 2 through 36, 379 | // where the digits for the first 10 digit values are '0' through '9' 380 | // and the remaining digits are 'a' through 'z'. 381 | // For decoding, upper-case 'A' through 'Z' are the same as lower-case. 382 | type Alnum int 383 | 384 | func (a Alnum) N() int64 { return int64(a) } 385 | 386 | func (a Alnum) Encode(val int64) ([]byte, error) { 387 | if val < 0 || val >= int64(a) { 388 | return nil, ErrInvalid 389 | } 390 | if val < 10 { 391 | return []byte{byte(val) + '0'}, nil 392 | } 393 | return []byte{byte(val) + 'a' - 10}, nil 394 | } 395 | 396 | func (a Alnum) Decode(inp []byte) (int64, error) { 397 | if len(inp) != 1 { 398 | return 0, ErrInvalid 399 | } 400 | digit := byte(inp[0]) 401 | switch { 402 | case '0' <= digit && digit <= '9': 403 | return int64(digit - '0'), nil 404 | case 'a' <= digit && digit <= 'z': 405 | return int64(digit - 'a' + 10), nil 406 | case 'A' <= digit && digit <= 'Z': 407 | return int64(digit - 'A' + 10), nil 408 | default: 409 | return 0, ErrInvalid 410 | } 411 | } 412 | 413 | const ( 414 | Base2 = Alnum(2) 415 | Base8 = Alnum(8) 416 | Base10 = Alnum(10) 417 | Base12 = Alnum(12) 418 | Base16 = Alnum(16) 419 | Base32 = Alnum(32) 420 | Base36 = Alnum(36) 421 | ) 422 | // {{ end }} 423 | 424 | // {{ define "newer/base30.go" }} 425 | package basexx 426 | 427 | const base30digits = "0123456789bcdfghjkmnpqrstvwxyz" 428 | 429 | var base30digitVals [256]int64 430 | 431 | type base30 struct{} 432 | 433 | func (b base30) N() int64 { return 30 } 434 | 435 | func (b base30) Encode(val int64) ([]byte, error) { 436 | if val < 0 || val > 49 { 437 | return nil, ErrInvalid 438 | } 439 | return []byte{byte(base30digits[val])}, nil 440 | } 441 | 442 | func (b base30) Decode(inp []byte) (int64, error) { 443 | if len(inp) != 1 { 444 | return 0, ErrInvalid 445 | } 446 | val := base30digitVals[inp[0]] 447 | if val < 0 { 448 | return 0, ErrInvalid 449 | } 450 | return val, nil 451 | } 452 | 453 | // Base30 uses digits 0-9, then lower-case bcdfghjkmnpqrstvwxyz. 454 | // It excludes vowels (to avoid inadvertently spelling naughty words) and the letter "l". 455 | var Base30 base30 456 | 457 | func init() { 458 | for i := 0; i < 256; i++ { 459 | base30digitVals[i] = -1 460 | } 461 | for i := 0; i < len(base30digits); i++ { 462 | base30digitVals[base30digits[i]] = int64(i) 463 | } 464 | } 465 | // {{ end }} 466 | 467 | // {{ define "newer/base50.go" }} 468 | package basexx 469 | 470 | const base50digits = "0123456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ" 471 | 472 | var base50digitVals [256]int64 473 | 474 | type base50 struct{} 475 | 476 | func (b base50) N() int64 { return 50 } 477 | 478 | func (b base50) Encode(val int64) ([]byte, error) { 479 | if val < 0 || val > 49 { 480 | return nil, ErrInvalid 481 | } 482 | return []byte{byte(base50digits[val])}, nil 483 | } 484 | 485 | func (b base50) Decode(inp []byte) (int64, error) { 486 | if len(inp) != 1 { 487 | return 0, ErrInvalid 488 | } 489 | val := base50digitVals[inp[0]] 490 | if val < 0 { 491 | return 0, ErrInvalid 492 | } 493 | return val, nil 494 | } 495 | 496 | // Base50 uses digits 0-9, then lower-case bcdfghjkmnpqrstvwxyz, then upper-case BCDFGHJKMNPQRSTVWXYZ. 497 | // It excludes vowels (to avoid inadvertently spelling naughty words) plus lower- and upper-case L. 498 | var Base50 base50 499 | 500 | func init() { 501 | for i := 0; i < 256; i++ { 502 | base50digitVals[i] = -1 503 | } 504 | for i := 0; i < len(base50digits); i++ { 505 | base50digitVals[base50digits[i]] = int64(i) 506 | } 507 | } 508 | // {{ end }} 509 | 510 | // {{ define "newer/base62.go" }} 511 | package basexx 512 | 513 | type base62 struct{} 514 | 515 | func (b base62) N() int64 { return 62 } 516 | 517 | func (b base62) Encode(val int64) ([]byte, error) { 518 | if val < 0 || val > 61 { 519 | return nil, ErrInvalid 520 | } 521 | if val < 10 { 522 | return []byte{byte(val) + '0'}, nil 523 | } 524 | if val < 36 { 525 | return []byte{byte(val) - 10 + 'a'}, nil 526 | } 527 | return []byte{byte(val) - 36 + 'A'}, nil 528 | } 529 | 530 | func (b base62) Decode(inp []byte) (int64, error) { 531 | if len(inp) != 1 { 532 | return 0, ErrInvalid 533 | } 534 | digit := byte(inp[0]) 535 | switch { 536 | case '0' <= digit && digit <= '9': 537 | return int64(digit - '0'), nil 538 | case 'a' <= digit && digit <= 'z': 539 | return int64(digit - 'a' + 10), nil 540 | case 'A' <= digit && digit <= 'Z': 541 | return int64(digit - 'A' + 36), nil 542 | default: 543 | return 0, ErrInvalid 544 | } 545 | } 546 | 547 | // Base62 uses digits 0..9, then a..z, then A..Z. 548 | var Base62 base62 549 | // {{ end }} 550 | 551 | // {{ define "newer/base94.go" }} 552 | package basexx 553 | 554 | type base94 struct{} 555 | 556 | func (b base94) N() int64 { return 94 } 557 | 558 | func (b base94) Encode(val int64) ([]byte, error) { 559 | if val < 0 || val > 93 { 560 | return nil, ErrInvalid 561 | } 562 | return []byte{byte(val + 33)}, nil 563 | } 564 | 565 | func (b base94) Decode(inp []byte) (int64, error) { 566 | if len(inp) != 1 { 567 | return 0, ErrInvalid 568 | } 569 | digit := inp[0] 570 | if digit < 33 || digit > 126 { 571 | return 0, ErrInvalid 572 | } 573 | return int64(digit - 33), nil 574 | } 575 | 576 | // Base94 uses all printable ASCII characters (33 through 126) as digits. 577 | var Base94 base94 578 | // {{ end }} 579 | 580 | // {{ define "newer/basexx.go" }} 581 | // Package basexx permits converting between digit strings of arbitrary bases. 582 | package basexx 583 | 584 | import ( 585 | "errors" 586 | "io" 587 | "math" 588 | "math/big" 589 | ) 590 | 591 | // Source is a source of digit values in a given base. 592 | type Source interface { 593 | // Read produces the value of the next-least-significant digit in the source. 594 | // The value must be between 0 and Base()-1, inclusive. 595 | // End of input is signaled with the error io.EOF. 596 | Read() (int64, error) 597 | 598 | // Base gives the base of the Source. 599 | // Digit values in the Source must all be between 0 and Base()-1, inclusive. 600 | // Behavior is undefined if the value of Base() varies during the lifetime of a Source 601 | // or if Base() < 2. 602 | Base() int64 603 | } 604 | 605 | // Dest is a destination for writing digits in a given base. 606 | // Digits are written right-to-left, from least significant to most. 607 | type Dest interface { 608 | // Prepend encodes the next-most-significant digit value and prepends it to the destination. 609 | Prepend(int64) error 610 | 611 | // Base gives the base of the Dest. 612 | // Digit values in the Dest must all be between 0 and Base()-1, inclusive. 613 | // Behavior is undefined if the value of Base() varies during the lifetime of a Dest 614 | // or if Base() < 2. 615 | Base() int64 616 | } 617 | 618 | // Base is the type of a base. 619 | type Base interface { 620 | // N is the number of the base, 621 | // i.e. the number of unique digits. 622 | // Behavior is undefined if the value of N() varies during the lifetime of a Base 623 | // or if N() < 2. 624 | N() int64 625 | 626 | // Encode converts a digit value to the string of bytes representing its digit. 627 | // The input must be a valid digit value between 0 and N()-1, inclusive. 628 | Encode(int64) ([]byte, error) 629 | 630 | // Decode converts a string of bytes representing a digit into its numeric value. 631 | Decode([]byte) (int64, error) 632 | } 633 | 634 | // ErrInvalid is used for invalid input to Base.Encode and Base.Decode. 635 | var ErrInvalid = errors.New("invalid") 636 | 637 | var zero = new(big.Int) 638 | 639 | // Convert converts the digits of src, writing them to dest. 640 | // Both src and dest specify their bases. 641 | // Return value is the number of digits written to dest (even in case of error). 642 | // This function consumes all of src before producing any of dest, 643 | // so it may not be suitable for input streams of arbitrary length. 644 | func Convert(dest Dest, src Source) (int, error) { 645 | var ( 646 | accum = new(big.Int) 647 | srcBase = big.NewInt(src.Base()) 648 | destBase = big.NewInt(dest.Base()) 649 | ) 650 | for { 651 | digit, err := src.Read() 652 | if err == io.EOF { 653 | break 654 | } 655 | if err != nil { 656 | return 0, err 657 | } 658 | accum.Mul(accum, srcBase) 659 | if digit != 0 { 660 | accum.Add(accum, big.NewInt(digit)) 661 | } 662 | } 663 | var written int 664 | for accum.Cmp(zero) > 0 { 665 | r := new(big.Int) 666 | accum.QuoRem(accum, destBase, r) 667 | err := dest.Prepend(r.Int64()) 668 | if err != nil { 669 | return written, err 670 | } 671 | written++ 672 | } 673 | if written == 0 { 674 | err := dest.Prepend(0) 675 | if err != nil { 676 | return written, err 677 | } 678 | written++ 679 | } 680 | return written, nil 681 | } 682 | 683 | // Length computes the maximum number of digits needed 684 | // to convert `n` digits in base `from` to base `to`. 685 | func Length(from, to int64, n int) int { 686 | ratio := math.Log(float64(from)) / math.Log(float64(to)) 687 | result := float64(n) * ratio 688 | return int(math.Ceil(result)) 689 | } 690 | // {{ end }} 691 | 692 | // {{ define "newer/binary.go" }} 693 | package basexx 694 | 695 | type binary struct{} 696 | 697 | func (b binary) N() int64 { return 256 } 698 | 699 | func (b binary) Encode(val int64) ([]byte, error) { 700 | if val < 0 || val > 255 { 701 | return nil, ErrInvalid 702 | } 703 | return []byte{byte(val)}, nil 704 | } 705 | 706 | func (b binary) Decode(inp []byte) (int64, error) { 707 | if len(inp) != 1 { 708 | return 0, ErrInvalid 709 | } 710 | return int64(inp[0]), nil 711 | } 712 | 713 | // Binary is base 256 encoded the obvious way: digit value X = byte(X). 714 | var Binary binary 715 | // {{ end }} 716 | 717 | // {{ define "newer/buffer.go" }} 718 | package basexx 719 | 720 | import "io" 721 | 722 | // Buffer can act as a Source or a Dest (but not both at the same time) 723 | // in the case where each byte in a given slice encodes a single digit in the desired base. 724 | // The digits in the buffer are in the expected order: 725 | // namely, most-significant first, least-significant last. 726 | type Buffer struct { 727 | buf []byte 728 | next int 729 | base Base 730 | } 731 | 732 | // NewBuffer produces a Buffer from the given byte slice described by the given Base. 733 | func NewBuffer(buf []byte, base Base) *Buffer { 734 | return &Buffer{ 735 | buf: buf, 736 | next: -1, // "unstarted" sentinel value 737 | base: base, 738 | } 739 | } 740 | 741 | func (s *Buffer) Read() (int64, error) { 742 | if s.next < 0 { 743 | s.next = 0 744 | } 745 | if s.next >= len(s.buf) { 746 | return 0, io.EOF 747 | } 748 | dec, err := s.base.Decode([]byte{s.buf[s.next]}) 749 | if err != nil { 750 | return 0, err 751 | } 752 | s.next++ 753 | return dec, nil 754 | } 755 | 756 | func (s *Buffer) Prepend(val int64) error { 757 | if s.next < 0 { 758 | s.next = len(s.buf) 759 | } 760 | if s.next == 0 { 761 | return io.EOF 762 | } 763 | enc, err := s.base.Encode(val) 764 | if err != nil { 765 | return err 766 | } 767 | if len(enc) != 1 { 768 | return ErrInvalid 769 | } 770 | s.next-- 771 | s.buf[s.next] = enc[0] 772 | return nil 773 | } 774 | 775 | func (s *Buffer) Written() []byte { 776 | if s.next < 0 { 777 | return nil 778 | } 779 | return s.buf[s.next:] 780 | } 781 | 782 | func (s *Buffer) Base() int64 { 783 | return s.base.N() 784 | } 785 | // {{ end }} 786 | -------------------------------------------------------------------------------- /testdata/minor/bumpgoversion.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older/go.mod" }} 4 | //// module bumpgoversion 5 | //// go 1.16 6 | //// {{ end }} 7 | 8 | //// {{ define "newer/go.mod" }} 9 | //// module bumpgoversion 10 | //// go 1.17 11 | //// {{ end }} 12 | 13 | //// {{ define "older" }} 14 | package bumpgoversion 15 | 16 | const X = 7 17 | //// {{ end }} 18 | 19 | //// {{ define "newer" }} 20 | package bumpgoversion 21 | 22 | const X = 8 23 | //// {{ end }} 24 | -------------------------------------------------------------------------------- /testdata/minor/familiarmethodname.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package familiarmethodname 6 | 7 | type Val int 8 | 9 | const String Val = 1 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package familiarmethodname 16 | 17 | type Val int 18 | 19 | const String Val = 1 20 | 21 | func (v *Val) String() string { 22 | switch *v { 23 | case String: return "string" 24 | } 25 | return "" 26 | } 27 | 28 | //// {{ end }} 29 | -------------------------------------------------------------------------------- /testdata/minor/relaxconstraint1.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package relaxconstraint 5 | 6 | type T[X comparable] struct { 7 | Val X 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package relaxconstraint 13 | 14 | type T[X any] struct { 15 | Val X 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/minor/relaxconstraint2.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package relaxconstraint2 5 | 6 | type T[X ~int] struct { 7 | Val X 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package relaxconstraint2 13 | 14 | type T[X ~int | ~string] struct { 15 | Val X 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/minor/relaxparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go 2 | 3 | //// {{ define "older" }} 4 | 5 | package relaxparam 6 | 7 | type a struct { 8 | X int 9 | } 10 | 11 | func F(a) {} 12 | 13 | //// {{ end }} 14 | 15 | //// {{ define "newer" }} 16 | 17 | package relaxparam 18 | 19 | type a struct { 20 | X, Y int 21 | } 22 | func F(a) {} 23 | 24 | //// {{ end }} 25 | -------------------------------------------------------------------------------- /testdata/minor/somecomparabletoany.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package somecomparabletoany 5 | 6 | type X interface { 7 | comparable 8 | int 9 | } 10 | //// {{ end }} 11 | 12 | //// {{ define "newer" }} 13 | package somecomparabletoany 14 | 15 | type X any 16 | //// {{ end }} 17 | -------------------------------------------------------------------------------- /testdata/minor/sometoallcomparable.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | package sometoallcomparable 5 | 6 | type X interface { 7 | comparable 8 | int 9 | } 10 | //// {{ end }} 11 | 12 | //// {{ define "newer" }} 13 | package sometoallcomparable 14 | 15 | type X interface { 16 | comparable 17 | } 18 | //// {{ end }} 19 | -------------------------------------------------------------------------------- /testdata/minor/subcmd.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "go.mod" -}} 4 | //// module subcmd 5 | //// 6 | //// go 1.16 7 | //// 8 | //// require github.com/pkg/errors v0.9.1 {{- end }} 9 | 10 | // {{ define "go.sum" -}} 11 | //// github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | //// github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= {{- end }} 13 | 14 | // This is https://github.com/bobg/subcmd/blob/0c84b241b4b3a312ba10b7739eeafb26ae4c0017/subcmd.go 15 | // {{ define "older" }} 16 | // Package subcmd provides types and functions for creating command-line interfaces with subcommands and flags. 17 | package subcmd 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "fmt" 23 | "reflect" 24 | "sort" 25 | "time" 26 | 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | var errType = reflect.TypeOf((*error)(nil)).Elem() 31 | 32 | // Cmd is the way a command tells Run how to parse and run its subcommands. 33 | type Cmd interface { 34 | // Subcmds returns this Cmd's subcommands as a map, 35 | // whose keys are subcommand names and values are Subcmd objects. 36 | // Implementations may use the Commands function to build this map. 37 | Subcmds() map[string]Subcmd 38 | } 39 | 40 | // Subcmd is one subcommand of a Cmd. 41 | type Subcmd struct { 42 | // F is the function implementing the subcommand. 43 | // Its signature must be func(context.Context, ..., []string) error, 44 | // where the number and types of parameters between the context and the string slice 45 | // is given by Params. 46 | F interface{} 47 | 48 | // Params describes the parameters to F 49 | // (excluding the initial context.Context that F takes, and the final []string). 50 | Params []Param 51 | } 52 | 53 | // Param is one parameter of a Subcmd. 54 | type Param struct { 55 | // Name is the flag name for the parameter (e.g., "verbose" for a -verbose flag). 56 | Name string 57 | 58 | // Type is the type of the parameter. 59 | Type Type 60 | 61 | // Default is a default value for the parameter. 62 | // Its type must be suitable for Type. 63 | Default interface{} 64 | 65 | // Doc is a docstring for the parameter. 66 | Doc string 67 | } 68 | 69 | // Commands is a convenience function for producing the map needed by a Cmd. 70 | // It takes 3n arguments, 71 | // where n is the number of subcommands. 72 | // Each group of three is: 73 | // - the subcommand name, a string; 74 | // - the function implementing the subcommand; 75 | // - the list of parameters for the function, a slice of Param (which can be produced with Params). 76 | // 77 | // A call like this: 78 | // 79 | // Commands( 80 | // "foo", foo, Params( 81 | // "verbose", Bool, false, "be verbose", 82 | // ), 83 | // "bar", bar, Params( 84 | // "level", Int, 0, "barness level", 85 | // ), 86 | // ) 87 | // 88 | // is equivalent to: 89 | // 90 | // map[string]Subcmd{ 91 | // "foo": Subcmd{ 92 | // F: foo, 93 | // Params: []Param{ 94 | // { 95 | // Name: "verbose", 96 | // Type: Bool, 97 | // Default: false, 98 | // Doc: "be verbose", 99 | // }, 100 | // }, 101 | // }, 102 | // "bar": Subcmd{ 103 | // F: bar, 104 | // Params: []Param{ 105 | // { 106 | // Name: "level", 107 | // Type: Int, 108 | // Default: 0, 109 | // Doc: "barness level", 110 | // }, 111 | // }, 112 | // }, 113 | // } 114 | // 115 | // This function panics if the number or types of the arguments are wrong. 116 | func Commands(args ...interface{}) map[string]Subcmd { 117 | if len(args)%3 != 0 { 118 | panic(fmt.Sprintf("S has %d arguments, which is not divisible by 3", len(args))) 119 | } 120 | 121 | result := make(map[string]Subcmd) 122 | 123 | for len(args) > 0 { 124 | var ( 125 | name = args[0].(string) 126 | f = args[1] 127 | p = args[2] 128 | ) 129 | subcmd := Subcmd{F: f} 130 | if p != nil { 131 | subcmd.Params = p.([]Param) 132 | } 133 | result[name] = subcmd 134 | 135 | args = args[3:] 136 | } 137 | 138 | return result 139 | } 140 | 141 | // Params is a convenience function for producing the list of parameters needed by a Subcmd. 142 | // It takes 4n arguments, 143 | // where n is the number of parameters. 144 | // Each group of four is: 145 | // - the flag name for the parameter, a string (e.g. "verbose" for a -verbose flag); 146 | // - the type of the parameter, a Type constant; 147 | // - the default value of the parameter, 148 | // - the doc string for the parameter. 149 | // 150 | // This function panics if the number or types of the arguments are wrong. 151 | func Params(a ...interface{}) []Param { 152 | if len(a)%4 != 0 { 153 | panic(fmt.Sprintf("Params has %d arguments, which is not divisible by 4", len(a))) 154 | } 155 | var result []Param 156 | for len(a) > 0 { 157 | var ( 158 | name = a[0].(string) 159 | typ = a[1].(Type) 160 | dflt = a[2] 161 | doc = a[3].(string) 162 | ) 163 | result = append(result, Param{Name: name, Type: typ, Default: dflt, Doc: doc}) 164 | a = a[4:] 165 | } 166 | return result 167 | } 168 | 169 | var ( 170 | // ErrNoArgs is the error when Run is called with an empty list of args. 171 | ErrNoArgs = errors.New("no arguments") 172 | 173 | // ErrUnknown is the error when Run is called with an unknown subcommand as args[0]. 174 | ErrUnknown = errors.New("unknown subcommand") 175 | ) 176 | 177 | // Run runs the subcommand of c named in args[0]. 178 | // The remaining args are parsed with a new flag.FlagSet, 179 | // populated according to the parameters of the named Subcmd. 180 | // The Subcmd's function is invoked with a context object, 181 | // the parameter values parsed by the FlagSet, 182 | // and a slice of the args left over after FlagSet parsing. 183 | // The FlagSet is placed in the context object that's passed to the Subcmd's function, 184 | // and can be retrieved if needed with the FlagSet function. 185 | func Run(ctx context.Context, c Cmd, args []string) error { 186 | cmds := c.Subcmds() 187 | 188 | var cmdnames sort.StringSlice 189 | for cmdname := range cmds { 190 | cmdnames = append(cmdnames, cmdname) 191 | } 192 | cmdnames.Sort() 193 | 194 | if len(args) == 0 { 195 | return errors.Wrapf(ErrNoArgs, "possible subcommands: %v", cmdnames) 196 | } 197 | 198 | name := args[0] 199 | args = args[1:] 200 | subcmd, ok := cmds[name] 201 | if !ok { 202 | return errors.Wrapf(ErrUnknown, "got %s, want one of %v", name, cmdnames) 203 | } 204 | 205 | var ptrs []reflect.Value 206 | 207 | if len(subcmd.Params) > 0 { 208 | fs := flag.NewFlagSet("", flag.ContinueOnError) 209 | ctx = context.WithValue(ctx, fskey, fs) 210 | 211 | for _, p := range subcmd.Params { 212 | var v interface{} 213 | 214 | switch p.Type { 215 | case Bool: 216 | dflt, _ := p.Default.(bool) 217 | v = fs.Bool(p.Name, dflt, p.Doc) 218 | 219 | case Int: 220 | dflt, _ := p.Default.(int) 221 | v = fs.Int(p.Name, dflt, p.Doc) 222 | 223 | case Int64: 224 | dflt, _ := p.Default.(int64) 225 | v = fs.Int64(p.Name, dflt, p.Doc) 226 | 227 | case Uint: 228 | dflt, _ := p.Default.(uint) 229 | v = fs.Uint(p.Name, dflt, p.Doc) 230 | 231 | case Uint64: 232 | dflt, _ := p.Default.(uint64) 233 | v = fs.Uint64(p.Name, dflt, p.Doc) 234 | 235 | case String: 236 | dflt, _ := p.Default.(string) 237 | v = fs.String(p.Name, dflt, p.Doc) 238 | 239 | case Float64: 240 | dflt, _ := p.Default.(float64) 241 | v = fs.Float64(p.Name, dflt, p.Doc) 242 | 243 | case Duration: 244 | dflt, _ := p.Default.(time.Duration) 245 | v = fs.Duration(p.Name, dflt, p.Doc) 246 | 247 | default: 248 | return fmt.Errorf("unknown arg type %v", p.Type) 249 | } 250 | 251 | ptrs = append(ptrs, reflect.ValueOf(v)) 252 | } 253 | 254 | err := fs.Parse(args) 255 | if err != nil { 256 | return errors.Wrap(err, "parsing args") 257 | } 258 | 259 | args = fs.Args() 260 | } 261 | 262 | argvals := []reflect.Value{reflect.ValueOf(ctx)} 263 | for _, ptr := range ptrs { 264 | argvals = append(argvals, ptr.Elem()) 265 | } 266 | argvals = append(argvals, reflect.ValueOf(args)) 267 | 268 | fv := reflect.ValueOf(subcmd.F) 269 | ft := fv.Type() 270 | if ft.Kind() != reflect.Func { 271 | return fmt.Errorf("implementation for subcommand %s is a %s, want a function", name, ft.Kind()) 272 | } 273 | if numIn := ft.NumIn(); numIn != len(argvals) { 274 | return fmt.Errorf("function for subcommand %s takes %d arg(s), want %d", name, numIn, len(argvals)) 275 | } 276 | for i, argval := range argvals { 277 | if !argval.Type().AssignableTo(ft.In(i)) { 278 | return fmt.Errorf("type of arg %d is %s, want %s", i, ft.In(i), argval.Type()) 279 | } 280 | } 281 | 282 | if numOut := ft.NumOut(); numOut != 1 { 283 | return fmt.Errorf("function for subcommand %s returns %d args, want 1", name, numOut) 284 | } 285 | if !ft.Out(0).Implements(errType) { 286 | return fmt.Errorf("return type is not error") 287 | } 288 | 289 | rv := fv.Call(argvals) 290 | err, _ := rv[0].Interface().(error) 291 | return errors.Wrapf(err, "running %s", name) 292 | } 293 | 294 | // Type is the type of a Param. 295 | type Type int 296 | 297 | // Possible Param types. 298 | // These correspond with the types in the standard flag package. 299 | const ( 300 | Bool Type = iota + 1 301 | Int 302 | Int64 303 | Uint 304 | Uint64 305 | String 306 | Float64 307 | Duration 308 | ) 309 | 310 | type fskeytype struct{} 311 | 312 | var fskey fskeytype 313 | 314 | // FlagSet produces the *flag.FlagSet used in a call to a Subcmd function. 315 | func FlagSet(ctx context.Context) *flag.FlagSet { 316 | val := ctx.Value(fskey) 317 | return val.(*flag.FlagSet) 318 | } 319 | // {{ end }} 320 | 321 | // This is https://github.com/bobg/subcmd/blob/6475969230d0fd388803089c213df380c2283f55/subcmd.go 322 | // {{ define "newer" }} 323 | // Package subcmd provides types and functions for creating command-line interfaces with subcommands and flags. 324 | package subcmd 325 | 326 | import ( 327 | "context" 328 | "flag" 329 | "fmt" 330 | "reflect" 331 | "sort" 332 | "time" 333 | 334 | "github.com/pkg/errors" 335 | ) 336 | 337 | var errType = reflect.TypeOf((*error)(nil)).Elem() 338 | 339 | // Cmd is the way a command tells Run how to parse and run its subcommands. 340 | type Cmd interface { 341 | // Subcmds returns this Cmd's subcommands as a map, 342 | // whose keys are subcommand names and values are Subcmd objects. 343 | // Implementations may use the Commands function to build this map. 344 | Subcmds() Map 345 | } 346 | 347 | // Map is the type of the data structure returned by Cmd.Subcmds and by Commands. 348 | // It maps a subcommand name to its Subcmd structure. 349 | type Map = map[string]Subcmd 350 | 351 | // Subcmd is one subcommand of a Cmd. 352 | type Subcmd struct { 353 | // F is the function implementing the subcommand. 354 | // Its signature must be func(context.Context, ..., []string) error, 355 | // where the number and types of parameters between the context and the string slice 356 | // is given by Params. 357 | F interface{} 358 | 359 | // Params describes the parameters to F 360 | // (excluding the initial context.Context that F takes, and the final []string). 361 | Params []Param 362 | } 363 | 364 | // Param is one parameter of a Subcmd. 365 | type Param struct { 366 | // Name is the flag name for the parameter (e.g., "verbose" for a -verbose flag). 367 | Name string 368 | 369 | // Type is the type of the parameter. 370 | Type Type 371 | 372 | // Default is a default value for the parameter. 373 | // Its type must be suitable for Type. 374 | Default interface{} 375 | 376 | // Doc is a docstring for the parameter. 377 | Doc string 378 | } 379 | 380 | // Commands is a convenience function for producing the map needed by a Cmd. 381 | // It takes 3n arguments, 382 | // where n is the number of subcommands. 383 | // Each group of three is: 384 | // - the subcommand name, a string; 385 | // - the function implementing the subcommand; 386 | // - the list of parameters for the function, a slice of Param (which can be produced with Params). 387 | // 388 | // A call like this: 389 | // 390 | // Commands( 391 | // "foo", foo, Params( 392 | // "verbose", Bool, false, "be verbose", 393 | // ), 394 | // "bar", bar, Params( 395 | // "level", Int, 0, "barness level", 396 | // ), 397 | // ) 398 | // 399 | // is equivalent to: 400 | // 401 | // Map{ 402 | // "foo": Subcmd{ 403 | // F: foo, 404 | // Params: []Param{ 405 | // { 406 | // Name: "verbose", 407 | // Type: Bool, 408 | // Default: false, 409 | // Doc: "be verbose", 410 | // }, 411 | // }, 412 | // }, 413 | // "bar": Subcmd{ 414 | // F: bar, 415 | // Params: []Param{ 416 | // { 417 | // Name: "level", 418 | // Type: Int, 419 | // Default: 0, 420 | // Doc: "barness level", 421 | // }, 422 | // }, 423 | // }, 424 | // } 425 | // 426 | // This function panics if the number or types of the arguments are wrong. 427 | func Commands(args ...interface{}) Map { 428 | if len(args)%3 != 0 { 429 | panic(fmt.Sprintf("S has %d arguments, which is not divisible by 3", len(args))) 430 | } 431 | 432 | result := make(Map) 433 | 434 | for len(args) > 0 { 435 | var ( 436 | name = args[0].(string) 437 | f = args[1] 438 | p = args[2] 439 | ) 440 | subcmd := Subcmd{F: f} 441 | if p != nil { 442 | subcmd.Params = p.([]Param) 443 | } 444 | result[name] = subcmd 445 | 446 | args = args[3:] 447 | } 448 | 449 | return result 450 | } 451 | 452 | // Params is a convenience function for producing the list of parameters needed by a Subcmd. 453 | // It takes 4n arguments, 454 | // where n is the number of parameters. 455 | // Each group of four is: 456 | // - the flag name for the parameter, a string (e.g. "verbose" for a -verbose flag); 457 | // - the type of the parameter, a Type constant; 458 | // - the default value of the parameter, 459 | // - the doc string for the parameter. 460 | // 461 | // This function panics if the number or types of the arguments are wrong. 462 | func Params(a ...interface{}) []Param { 463 | if len(a)%4 != 0 { 464 | panic(fmt.Sprintf("Params has %d arguments, which is not divisible by 4", len(a))) 465 | } 466 | var result []Param 467 | for len(a) > 0 { 468 | var ( 469 | name = a[0].(string) 470 | typ = a[1].(Type) 471 | dflt = a[2] 472 | doc = a[3].(string) 473 | ) 474 | result = append(result, Param{Name: name, Type: typ, Default: dflt, Doc: doc}) 475 | a = a[4:] 476 | } 477 | return result 478 | } 479 | 480 | var ( 481 | // ErrNoArgs is the error when Run is called with an empty list of args. 482 | ErrNoArgs = errors.New("no arguments") 483 | 484 | // ErrUnknown is the error when Run is called with an unknown subcommand as args[0]. 485 | ErrUnknown = errors.New("unknown subcommand") 486 | ) 487 | 488 | // Run runs the subcommand of c named in args[0]. 489 | // The remaining args are parsed with a new flag.FlagSet, 490 | // populated according to the parameters of the named Subcmd. 491 | // The Subcmd's function is invoked with a context object, 492 | // the parameter values parsed by the FlagSet, 493 | // and a slice of the args left over after FlagSet parsing. 494 | // The FlagSet is placed in the context object that's passed to the Subcmd's function, 495 | // and can be retrieved if needed with the FlagSet function. 496 | func Run(ctx context.Context, c Cmd, args []string) error { 497 | cmds := c.Subcmds() 498 | 499 | var cmdnames sort.StringSlice 500 | for cmdname := range cmds { 501 | cmdnames = append(cmdnames, cmdname) 502 | } 503 | cmdnames.Sort() 504 | 505 | if len(args) == 0 { 506 | return errors.Wrapf(ErrNoArgs, "possible subcommands: %v", cmdnames) 507 | } 508 | 509 | name := args[0] 510 | args = args[1:] 511 | subcmd, ok := cmds[name] 512 | if !ok { 513 | return errors.Wrapf(ErrUnknown, "got %s, want one of %v", name, cmdnames) 514 | } 515 | 516 | var ptrs []reflect.Value 517 | 518 | if len(subcmd.Params) > 0 { 519 | fs := flag.NewFlagSet("", flag.ContinueOnError) 520 | ctx = context.WithValue(ctx, fskey, fs) 521 | 522 | for _, p := range subcmd.Params { 523 | var v interface{} 524 | 525 | switch p.Type { 526 | case Bool: 527 | dflt, _ := p.Default.(bool) 528 | v = fs.Bool(p.Name, dflt, p.Doc) 529 | 530 | case Int: 531 | dflt, _ := p.Default.(int) 532 | v = fs.Int(p.Name, dflt, p.Doc) 533 | 534 | case Int64: 535 | dflt, _ := p.Default.(int64) 536 | v = fs.Int64(p.Name, dflt, p.Doc) 537 | 538 | case Uint: 539 | dflt, _ := p.Default.(uint) 540 | v = fs.Uint(p.Name, dflt, p.Doc) 541 | 542 | case Uint64: 543 | dflt, _ := p.Default.(uint64) 544 | v = fs.Uint64(p.Name, dflt, p.Doc) 545 | 546 | case String: 547 | dflt, _ := p.Default.(string) 548 | v = fs.String(p.Name, dflt, p.Doc) 549 | 550 | case Float64: 551 | dflt, _ := p.Default.(float64) 552 | v = fs.Float64(p.Name, dflt, p.Doc) 553 | 554 | case Duration: 555 | dflt, _ := p.Default.(time.Duration) 556 | v = fs.Duration(p.Name, dflt, p.Doc) 557 | 558 | default: 559 | return fmt.Errorf("unknown arg type %v", p.Type) 560 | } 561 | 562 | ptrs = append(ptrs, reflect.ValueOf(v)) 563 | } 564 | 565 | err := fs.Parse(args) 566 | if err != nil { 567 | return errors.Wrap(err, "parsing args") 568 | } 569 | 570 | args = fs.Args() 571 | } 572 | 573 | argvals := []reflect.Value{reflect.ValueOf(ctx)} 574 | for _, ptr := range ptrs { 575 | argvals = append(argvals, ptr.Elem()) 576 | } 577 | argvals = append(argvals, reflect.ValueOf(args)) 578 | 579 | fv := reflect.ValueOf(subcmd.F) 580 | ft := fv.Type() 581 | if ft.Kind() != reflect.Func { 582 | return fmt.Errorf("implementation for subcommand %s is a %s, want a function", name, ft.Kind()) 583 | } 584 | if numIn := ft.NumIn(); numIn != len(argvals) { 585 | return fmt.Errorf("function for subcommand %s takes %d arg(s), want %d", name, numIn, len(argvals)) 586 | } 587 | for i, argval := range argvals { 588 | if !argval.Type().AssignableTo(ft.In(i)) { 589 | return fmt.Errorf("type of arg %d is %s, want %s", i, ft.In(i), argval.Type()) 590 | } 591 | } 592 | 593 | if numOut := ft.NumOut(); numOut != 1 { 594 | return fmt.Errorf("function for subcommand %s returns %d args, want 1", name, numOut) 595 | } 596 | if !ft.Out(0).Implements(errType) { 597 | return fmt.Errorf("return type is not error") 598 | } 599 | 600 | rv := fv.Call(argvals) 601 | err, _ := rv[0].Interface().(error) 602 | return errors.Wrapf(err, "running %s", name) 603 | } 604 | 605 | // Type is the type of a Param. 606 | type Type int 607 | 608 | // Possible Param types. 609 | // These correspond with the types in the standard flag package. 610 | const ( 611 | Bool Type = iota + 1 612 | Int 613 | Int64 614 | Uint 615 | Uint64 616 | String 617 | Float64 618 | Duration 619 | ) 620 | 621 | type fskeytype struct{} 622 | 623 | var fskey fskeytype 624 | 625 | // FlagSet produces the *flag.FlagSet used in a call to a Subcmd function. 626 | func FlagSet(ctx context.Context) *flag.FlagSet { 627 | val := ctx.Value(fskey) 628 | return val.(*flag.FlagSet) 629 | } 630 | // {{ end }} 631 | -------------------------------------------------------------------------------- /testdata/none/assignablechan1.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package assignablechan1 6 | 7 | type X chan int 8 | 9 | var Y chan int 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package assignablechan1 16 | 17 | type X = chan int 18 | 19 | var Y X 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/none/assignablechan2.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package assignablechan2 6 | 7 | type X chan int 8 | 9 | var Y X 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package assignablechan2 16 | 17 | type X chan int 18 | 19 | var Y chan int 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/none/basexx.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // Older parts are from github.com/bobg/basexx at commit dbebfe56b6709535c4458efe67f07e39036e601f. 4 | // Newer parts are from github.com/bobg/basexx at commit dbebfe56b6709535c4458efe67f07e39036e601f. 5 | 6 | // {{ define "older/alnum.go" }} 7 | package basexx 8 | 9 | // Alnum is a type for bases from 2 through 36, 10 | // where the digits for the first 10 digit values are '0' through '9' 11 | // and the remaining digits are 'a' through 'z'. 12 | // For decoding, upper-case 'A' through 'Z' are the same as lower-case. 13 | type Alnum int 14 | 15 | func (a Alnum) N() int64 { return int64(a) } 16 | 17 | func (a Alnum) Encode(val int64) ([]byte, error) { 18 | if val < 0 || val >= int64(a) { 19 | return nil, ErrInvalid 20 | } 21 | if val < 10 { 22 | return []byte{byte(val) + '0'}, nil 23 | } 24 | return []byte{byte(val) + 'a' - 10}, nil 25 | } 26 | 27 | func (a Alnum) Decode(inp []byte) (int64, error) { 28 | if len(inp) != 1 { 29 | return 0, ErrInvalid 30 | } 31 | digit := byte(inp[0]) 32 | switch { 33 | case '0' <= digit && digit <= '9': 34 | return int64(digit - '0'), nil 35 | case 'a' <= digit && digit <= 'z': 36 | return int64(digit - 'a' + 10), nil 37 | case 'A' <= digit && digit <= 'Z': 38 | return int64(digit - 'A' + 10), nil 39 | default: 40 | return 0, ErrInvalid 41 | } 42 | } 43 | 44 | const ( 45 | Base2 = Alnum(2) 46 | Base8 = Alnum(8) 47 | Base10 = Alnum(10) 48 | Base12 = Alnum(12) 49 | Base16 = Alnum(16) 50 | Base32 = Alnum(32) 51 | Base36 = Alnum(36) 52 | ) 53 | // {{ end }} 54 | 55 | // {{ define "older/base50.go" }} 56 | package basexx 57 | 58 | const base50digits = "0123456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ" 59 | 60 | var base50digitVals [256]int64 61 | 62 | type base50 struct{} 63 | 64 | func (b base50) N() int64 { return 50 } 65 | 66 | func (b base50) Encode(val int64) ([]byte, error) { 67 | if val < 0 || val > 49 { 68 | return nil, ErrInvalid 69 | } 70 | return []byte{byte(base50digits[val])}, nil 71 | } 72 | 73 | func (b base50) Decode(inp []byte) (int64, error) { 74 | if len(inp) != 1 { 75 | return 0, ErrInvalid 76 | } 77 | val := base50digitVals[inp[0]] 78 | if val < 0 { 79 | return 0, ErrInvalid 80 | } 81 | return val, nil 82 | } 83 | 84 | // Base50 uses digits 0-9, then lower-case bcdfghjkmnpqrstvwxyz, then upper-case BCDFGHJKMNPQRSTVWXYZ. 85 | // It excludes vowels (to avoid inadvertently spelling naughty words) plus lower- and upper-case L. 86 | var Base50 base50 87 | 88 | func init() { 89 | for i := 0; i < 256; i++ { 90 | base50digitVals[i] = -1 91 | } 92 | for i := 0; i < len(base50digits); i++ { 93 | base50digitVals[base50digits[i]] = int64(i) 94 | } 95 | } 96 | // {{ end }} 97 | 98 | // {{ define "older/base62.go" }} 99 | package basexx 100 | 101 | type base62 struct{} 102 | 103 | func (b base62) N() int64 { return 62 } 104 | 105 | func (b base62) Encode(val int64) ([]byte, error) { 106 | if val < 0 || val > 61 { 107 | return nil, ErrInvalid 108 | } 109 | if val < 10 { 110 | return []byte{byte(val) + '0'}, nil 111 | } 112 | if val < 36 { 113 | return []byte{byte(val) - 10 + 'a'}, nil 114 | } 115 | return []byte{byte(val) - 36 + 'A'}, nil 116 | } 117 | 118 | func (b base62) Decode(inp []byte) (int64, error) { 119 | if len(inp) != 1 { 120 | return 0, ErrInvalid 121 | } 122 | digit := byte(inp[0]) 123 | switch { 124 | case '0' <= digit && digit <= '9': 125 | return int64(digit - '0'), nil 126 | case 'a' <= digit && digit <= 'z': 127 | return int64(digit - 'a' + 10), nil 128 | case 'A' <= digit && digit <= 'Z': 129 | return int64(digit - 'A' + 36), nil 130 | default: 131 | return 0, ErrInvalid 132 | } 133 | } 134 | 135 | // Base62 uses digits 0..9, then a..z, then A..Z. 136 | var Base62 base62 137 | // {{ end }} 138 | 139 | // {{ define "older/base94.go" }} 140 | package basexx 141 | 142 | type base94 struct{} 143 | 144 | func (b base94) N() int64 { return 94 } 145 | 146 | func (b base94) Encode(val int64) ([]byte, error) { 147 | if val < 0 || val > 93 { 148 | return nil, ErrInvalid 149 | } 150 | return []byte{byte(val + 33)}, nil 151 | } 152 | 153 | func (b base94) Decode(inp []byte) (int64, error) { 154 | if len(inp) != 1 { 155 | return 0, ErrInvalid 156 | } 157 | digit := inp[0] 158 | if digit < 33 || digit > 126 { 159 | return 0, ErrInvalid 160 | } 161 | return int64(digit - 33), nil 162 | } 163 | 164 | // Base94 uses all printable ASCII characters (33 through 126) as digits. 165 | var Base94 base94 166 | // {{ end }} 167 | 168 | // {{ define "older/basexx.go" }} 169 | // Package basexx permits converting between digit strings of arbitrary bases. 170 | package basexx 171 | 172 | import ( 173 | "errors" 174 | "io" 175 | "math" 176 | "math/big" 177 | ) 178 | 179 | // Source is a source of digit values in a given base. 180 | type Source interface { 181 | // Read produces the value of the next-least-significant digit in the source. 182 | // The value must be between 0 and Base()-1, inclusive. 183 | // End of input is signaled with the error io.EOF. 184 | Read() (int64, error) 185 | 186 | // Base gives the base of the Source. 187 | // Digit values in the Source must all be between 0 and Base()-1, inclusive. 188 | // Behavior is undefined if the value of Base() varies during the lifetime of a Source 189 | // or if Base() < 2. 190 | Base() int64 191 | } 192 | 193 | // Dest is a destination for writing digits in a given base. 194 | // Digits are written right-to-left, from least significant to most. 195 | type Dest interface { 196 | // Prepend encodes the next-most-significant digit value and prepends it to the destination. 197 | Prepend(int64) error 198 | 199 | // Base gives the base of the Dest. 200 | // Digit values in the Dest must all be between 0 and Base()-1, inclusive. 201 | // Behavior is undefined if the value of Base() varies during the lifetime of a Dest 202 | // or if Base() < 2. 203 | Base() int64 204 | } 205 | 206 | // Base is the type of a base. 207 | type Base interface { 208 | // N is the number of the base, 209 | // i.e. the number of unique digits. 210 | // Behavior is undefined if the value of N() varies during the lifetime of a Base 211 | // or if N() < 2. 212 | N() int64 213 | 214 | // Encode converts a digit value to the string of bytes representing its digit. 215 | // The input must be a valid digit value between 0 and N()-1, inclusive. 216 | Encode(int64) ([]byte, error) 217 | 218 | // Decode converts a string of bytes representing a digit into its numeric value. 219 | Decode([]byte) (int64, error) 220 | } 221 | 222 | // ErrInvalid is used for invalid input to Base.Encode and Base.Decode. 223 | var ErrInvalid = errors.New("invalid") 224 | 225 | var zero = new(big.Int) 226 | 227 | // Convert converts the digits of src, writing them to dest. 228 | // Both src and dest specify their bases. 229 | // Return value is the number of digits written to dest (even in case of error). 230 | // This function consumes all of src before producing any of dest, 231 | // so it may not be suitable for input streams of arbitrary length. 232 | func Convert(dest Dest, src Source) (int, error) { 233 | var ( 234 | accum = new(big.Int) 235 | srcBase = big.NewInt(src.Base()) 236 | destBase = big.NewInt(dest.Base()) 237 | ) 238 | for { 239 | digit, err := src.Read() 240 | if err == io.EOF { 241 | break 242 | } 243 | if err != nil { 244 | return 0, err 245 | } 246 | accum.Mul(accum, srcBase) 247 | if digit != 0 { 248 | accum.Add(accum, big.NewInt(digit)) 249 | } 250 | } 251 | var written int 252 | for accum.Cmp(zero) > 0 { 253 | r := new(big.Int) 254 | accum.QuoRem(accum, destBase, r) 255 | err := dest.Prepend(r.Int64()) 256 | if err != nil { 257 | return written, err 258 | } 259 | written++ 260 | } 261 | if written == 0 { 262 | err := dest.Prepend(0) 263 | if err != nil { 264 | return written, err 265 | } 266 | written++ 267 | } 268 | return written, nil 269 | } 270 | 271 | // Length computes the maximum number of digits needed 272 | // to convert `n` digits in base `from` to base `to`. 273 | func Length(from, to int64, n int) int { 274 | ratio := math.Log(float64(from)) / math.Log(float64(to)) 275 | result := float64(n) * ratio 276 | return int(math.Ceil(result)) 277 | } 278 | // {{ end }} 279 | 280 | // {{ define "older/binary.go" }} 281 | package basexx 282 | 283 | type binary struct{} 284 | 285 | func (b binary) N() int64 { return 256 } 286 | 287 | func (b binary) Encode(val int64) ([]byte, error) { 288 | if val < 0 || val > 255 { 289 | return nil, ErrInvalid 290 | } 291 | return []byte{byte(val)}, nil 292 | } 293 | 294 | func (b binary) Decode(inp []byte) (int64, error) { 295 | if len(inp) != 1 { 296 | return 0, ErrInvalid 297 | } 298 | return int64(inp[0]), nil 299 | } 300 | 301 | // Binary is base 256 encoded the obvious way: digit value X = byte(X). 302 | var Binary binary 303 | // {{ end }} 304 | 305 | // {{ define "older/buffer.go" }} 306 | package basexx 307 | 308 | import "io" 309 | 310 | // Buffer can act as a Source or a Dest (but not both at the same time) 311 | // in the case where each byte in a given slice encodes a single digit in the desired base. 312 | // The digits in the buffer are in the expected order: 313 | // namely, most-significant first, least-significant last. 314 | type Buffer struct { 315 | buf []byte 316 | next int 317 | base Base 318 | } 319 | 320 | // NewBuffer produces a Buffer from the given byte slice described by the given Base. 321 | func NewBuffer(buf []byte, base Base) *Buffer { 322 | return &Buffer{ 323 | buf: buf, 324 | next: -1, // "unstarted" sentinel value 325 | base: base, 326 | } 327 | } 328 | 329 | func (s *Buffer) Read() (int64, error) { 330 | if s.next < 0 { 331 | s.next = 0 332 | } 333 | if s.next >= len(s.buf) { 334 | return 0, io.EOF 335 | } 336 | dec, err := s.base.Decode([]byte{s.buf[s.next]}) 337 | if err != nil { 338 | return 0, err 339 | } 340 | s.next++ 341 | return dec, nil 342 | } 343 | 344 | func (s *Buffer) Prepend(val int64) error { 345 | if s.next < 0 { 346 | s.next = len(s.buf) 347 | } 348 | if s.next == 0 { 349 | return io.EOF 350 | } 351 | enc, err := s.base.Encode(val) 352 | if err != nil { 353 | return err 354 | } 355 | if len(enc) != 1 { 356 | return ErrInvalid 357 | } 358 | s.next-- 359 | s.buf[s.next] = enc[0] 360 | return nil 361 | } 362 | 363 | func (s *Buffer) Written() []byte { 364 | if s.next < 0 { 365 | return nil 366 | } 367 | return s.buf[s.next:] 368 | } 369 | 370 | func (s *Buffer) Base() int64 { 371 | return s.base.N() 372 | } 373 | // {{ end }} 374 | 375 | // {{ define "newer/alnum.go" }} 376 | package basexx 377 | 378 | // Alnum is a type for bases from 2 through 36, 379 | // where the digits for the first 10 digit values are '0' through '9' 380 | // and the remaining digits are 'a' through 'z'. 381 | // For decoding, upper-case 'A' through 'Z' are the same as lower-case. 382 | type Alnum int 383 | 384 | func (a Alnum) N() int64 { return int64(a) } 385 | 386 | func (a Alnum) Encode(val int64) ([]byte, error) { 387 | if val < 0 || val >= int64(a) { 388 | return nil, ErrInvalid 389 | } 390 | if val < 10 { 391 | return []byte{byte(val) + '0'}, nil 392 | } 393 | return []byte{byte(val) + 'a' - 10}, nil 394 | } 395 | 396 | func (a Alnum) Decode(inp []byte) (int64, error) { 397 | if len(inp) != 1 { 398 | return 0, ErrInvalid 399 | } 400 | digit := byte(inp[0]) 401 | switch { 402 | case '0' <= digit && digit <= '9': 403 | return int64(digit - '0'), nil 404 | case 'a' <= digit && digit <= 'z': 405 | return int64(digit - 'a' + 10), nil 406 | case 'A' <= digit && digit <= 'Z': 407 | return int64(digit - 'A' + 10), nil 408 | default: 409 | return 0, ErrInvalid 410 | } 411 | } 412 | 413 | const ( 414 | Base2 = Alnum(2) 415 | Base8 = Alnum(8) 416 | Base10 = Alnum(10) 417 | Base12 = Alnum(12) 418 | Base16 = Alnum(16) 419 | Base32 = Alnum(32) 420 | Base36 = Alnum(36) 421 | ) 422 | // {{ end }} 423 | 424 | // {{ define "newer/base50.go" }} 425 | package basexx 426 | 427 | const base50digits = "0123456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ" 428 | 429 | var base50digitVals [256]int64 430 | 431 | type base50 struct{} 432 | 433 | func (b base50) N() int64 { return 50 } 434 | 435 | func (b base50) Encode(val int64) ([]byte, error) { 436 | if val < 0 || val > 49 { 437 | return nil, ErrInvalid 438 | } 439 | return []byte{byte(base50digits[val])}, nil 440 | } 441 | 442 | func (b base50) Decode(inp []byte) (int64, error) { 443 | if len(inp) != 1 { 444 | return 0, ErrInvalid 445 | } 446 | val := base50digitVals[inp[0]] 447 | if val < 0 { 448 | return 0, ErrInvalid 449 | } 450 | return val, nil 451 | } 452 | 453 | // Base50 uses digits 0-9, then lower-case bcdfghjkmnpqrstvwxyz, then upper-case BCDFGHJKMNPQRSTVWXYZ. 454 | // It excludes vowels (to avoid inadvertently spelling naughty words) plus lower- and upper-case L. 455 | var Base50 base50 456 | 457 | func init() { 458 | for i := 0; i < 256; i++ { 459 | base50digitVals[i] = -1 460 | } 461 | for i := 0; i < len(base50digits); i++ { 462 | base50digitVals[base50digits[i]] = int64(i) 463 | } 464 | } 465 | // {{ end }} 466 | 467 | // {{ define "newer/base62.go" }} 468 | package basexx 469 | 470 | type base62 struct{} 471 | 472 | func (b base62) N() int64 { return 62 } 473 | 474 | func (b base62) Encode(val int64) ([]byte, error) { 475 | if val < 0 || val > 61 { 476 | return nil, ErrInvalid 477 | } 478 | if val < 10 { 479 | return []byte{byte(val) + '0'}, nil 480 | } 481 | if val < 36 { 482 | return []byte{byte(val) - 10 + 'a'}, nil 483 | } 484 | return []byte{byte(val) - 36 + 'A'}, nil 485 | } 486 | 487 | func (b base62) Decode(inp []byte) (int64, error) { 488 | if len(inp) != 1 { 489 | return 0, ErrInvalid 490 | } 491 | digit := byte(inp[0]) 492 | switch { 493 | case '0' <= digit && digit <= '9': 494 | return int64(digit - '0'), nil 495 | case 'a' <= digit && digit <= 'z': 496 | return int64(digit - 'a' + 10), nil 497 | case 'A' <= digit && digit <= 'Z': 498 | return int64(digit - 'A' + 36), nil 499 | default: 500 | return 0, ErrInvalid 501 | } 502 | } 503 | 504 | // Base62 uses digits 0..9, then a..z, then A..Z. 505 | var Base62 base62 506 | // {{ end }} 507 | 508 | // {{ define "newer/base94.go" }} 509 | package basexx 510 | 511 | type base94 struct{} 512 | 513 | func (b base94) N() int64 { return 94 } 514 | 515 | func (b base94) Encode(val int64) ([]byte, error) { 516 | if val < 0 || val > 93 { 517 | return nil, ErrInvalid 518 | } 519 | return []byte{byte(val + 33)}, nil 520 | } 521 | 522 | func (b base94) Decode(inp []byte) (int64, error) { 523 | if len(inp) != 1 { 524 | return 0, ErrInvalid 525 | } 526 | digit := inp[0] 527 | if digit < 33 || digit > 126 { 528 | return 0, ErrInvalid 529 | } 530 | return int64(digit - 33), nil 531 | } 532 | 533 | // Base94 uses all printable ASCII characters (33 through 126) as digits. 534 | var Base94 base94 535 | // {{ end }} 536 | 537 | // {{ define "newer/basexx.go" }} 538 | // Package basexx permits converting between digit strings of arbitrary bases. 539 | package basexx 540 | 541 | import ( 542 | "errors" 543 | "io" 544 | "math" 545 | "math/big" 546 | ) 547 | 548 | // Source is a source of digit values in a given base. 549 | type Source interface { 550 | // Read produces the value of the next-least-significant digit in the source. 551 | // The value must be between 0 and Base()-1, inclusive. 552 | // End of input is signaled with the error io.EOF. 553 | Read() (int64, error) 554 | 555 | // Base gives the base of the Source. 556 | // Digit values in the Source must all be between 0 and Base()-1, inclusive. 557 | // Behavior is undefined if the value of Base() varies during the lifetime of a Source 558 | // or if Base() < 2. 559 | Base() int64 560 | } 561 | 562 | // Dest is a destination for writing digits in a given base. 563 | // Digits are written right-to-left, from least significant to most. 564 | type Dest interface { 565 | // Prepend encodes the next-most-significant digit value and prepends it to the destination. 566 | Prepend(int64) error 567 | 568 | // Base gives the base of the Dest. 569 | // Digit values in the Dest must all be between 0 and Base()-1, inclusive. 570 | // Behavior is undefined if the value of Base() varies during the lifetime of a Dest 571 | // or if Base() < 2. 572 | Base() int64 573 | } 574 | 575 | // Base is the type of a base. 576 | type Base interface { 577 | // N is the number of the base, 578 | // i.e. the number of unique digits. 579 | // Behavior is undefined if the value of N() varies during the lifetime of a Base 580 | // or if N() < 2. 581 | N() int64 582 | 583 | // Encode converts a digit value to the string of bytes representing its digit. 584 | // The input must be a valid digit value between 0 and N()-1, inclusive. 585 | Encode(int64) ([]byte, error) 586 | 587 | // Decode converts a string of bytes representing a digit into its numeric value. 588 | Decode([]byte) (int64, error) 589 | } 590 | 591 | // ErrInvalid is used for invalid input to Base.Encode and Base.Decode. 592 | var ErrInvalid = errors.New("invalid") 593 | 594 | var zero = new(big.Int) 595 | 596 | // Convert converts the digits of src, writing them to dest. 597 | // Both src and dest specify their bases. 598 | // Return value is the number of digits written to dest (even in case of error). 599 | // This function consumes all of src before producing any of dest, 600 | // so it may not be suitable for input streams of arbitrary length. 601 | func Convert(dest Dest, src Source) (int, error) { 602 | var ( 603 | accum = new(big.Int) 604 | srcBase = big.NewInt(src.Base()) 605 | destBase = big.NewInt(dest.Base()) 606 | ) 607 | for { 608 | digit, err := src.Read() 609 | if err == io.EOF { 610 | break 611 | } 612 | if err != nil { 613 | return 0, err 614 | } 615 | accum.Mul(accum, srcBase) 616 | if digit != 0 { 617 | accum.Add(accum, big.NewInt(digit)) 618 | } 619 | } 620 | var written int 621 | for accum.Cmp(zero) > 0 { 622 | r := new(big.Int) 623 | accum.QuoRem(accum, destBase, r) 624 | err := dest.Prepend(r.Int64()) 625 | if err != nil { 626 | return written, err 627 | } 628 | written++ 629 | } 630 | if written == 0 { 631 | err := dest.Prepend(0) 632 | if err != nil { 633 | return written, err 634 | } 635 | written++ 636 | } 637 | return written, nil 638 | } 639 | 640 | // Length computes the maximum number of digits needed 641 | // to convert `n` digits in base `from` to base `to`. 642 | func Length(from, to int64, n int) int { 643 | ratio := math.Log(float64(from)) / math.Log(float64(to)) 644 | result := float64(n) * ratio 645 | return int(math.Ceil(result)) 646 | } 647 | // {{ end }} 648 | 649 | // {{ define "newer/binary.go" }} 650 | package basexx 651 | 652 | type binary struct{} 653 | 654 | func (b binary) N() int64 { return 256 } 655 | 656 | func (b binary) Encode(val int64) ([]byte, error) { 657 | if val < 0 || val > 255 { 658 | return nil, ErrInvalid 659 | } 660 | return []byte{byte(val)}, nil 661 | } 662 | 663 | func (b binary) Decode(inp []byte) (int64, error) { 664 | if len(inp) != 1 { 665 | return 0, ErrInvalid 666 | } 667 | return int64(inp[0]), nil 668 | } 669 | 670 | // Binary is base 256 encoded the obvious way: digit value X = byte(X). 671 | var Binary binary 672 | // {{ end }} 673 | 674 | // {{ define "newer/buffer.go" }} 675 | package basexx 676 | 677 | import "io" 678 | 679 | // Buffer can act as a Source or a Dest (but not both at the same time) 680 | // in the case where each byte in a given slice encodes a single digit in the desired base. 681 | // The digits in the buffer are in the expected order: 682 | // namely, most-significant first, least-significant last. 683 | type Buffer struct { 684 | buf []byte 685 | next int 686 | base Base 687 | } 688 | 689 | // NewBuffer produces a Buffer from the given byte slice described by the given Base. 690 | func NewBuffer(buf []byte, base Base) *Buffer { 691 | return &Buffer{ 692 | buf: buf, 693 | next: -1, // "unstarted" sentinel value 694 | base: base, 695 | } 696 | } 697 | 698 | func (s *Buffer) Read() (int64, error) { 699 | if s.next < 0 { 700 | s.next = 0 701 | } 702 | if s.next >= len(s.buf) { 703 | return 0, io.EOF 704 | } 705 | dec, err := s.base.Decode([]byte{s.buf[s.next]}) 706 | if err != nil { 707 | return 0, err 708 | } 709 | s.next++ 710 | return dec, nil 711 | } 712 | 713 | func (s *Buffer) Prepend(val int64) error { 714 | if s.next < 0 { 715 | s.next = len(s.buf) 716 | } 717 | if s.next == 0 { 718 | return io.EOF 719 | } 720 | enc, err := s.base.Encode(val) 721 | if err != nil { 722 | return err 723 | } 724 | if len(enc) != 1 { 725 | return ErrInvalid 726 | } 727 | s.next-- 728 | s.buf[s.next] = enc[0] 729 | return nil 730 | } 731 | 732 | func (s *Buffer) Written() []byte { 733 | if s.next < 0 { 734 | return nil 735 | } 736 | return s.buf[s.next:] 737 | } 738 | 739 | func (s *Buffer) Base() int64 { 740 | return s.base.N() 741 | } 742 | // {{ end }} 743 | -------------------------------------------------------------------------------- /testdata/none/chconstant.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chconstant 5 | 6 | const X = 7 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package chconstant 11 | 12 | const X = 8 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/none/comparablefield.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package comparablefield 6 | 7 | type set[T comparable] map[T]struct{} 8 | 9 | type X struct { 10 | s set[int] 11 | } 12 | 13 | //// {{ end }} 14 | 15 | //// {{ define "newer" }} 16 | 17 | package comparablefield 18 | 19 | type set[T comparable] map[T]struct{} 20 | 21 | type X struct { 22 | s set[int] 23 | } 24 | 25 | //// {{ end }} 26 | -------------------------------------------------------------------------------- /testdata/none/renametypeparam.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package renametypeparam 5 | 6 | type T[X any] struct { 7 | Val X 8 | } 9 | // {{ end }} 10 | 11 | // {{ define "newer" }} 12 | package renametypeparam 13 | 14 | type T[Y any] struct { 15 | Val Y 16 | } 17 | // {{ end }} 18 | -------------------------------------------------------------------------------- /testdata/none/reordered.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package reordered 5 | 6 | var X, Y int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package reordered 11 | 12 | var Y, X int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/none/reorderterms.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package reorderterms 6 | 7 | type X interface { 8 | int | string 9 | } 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package reorderterms 16 | 17 | type X interface { 18 | string | int 19 | } 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/none/sameintf.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package sameintf 6 | 7 | type X interface { 8 | comparable 9 | } 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package sameintf 16 | 17 | type X interface { 18 | comparable 19 | } 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/none/sametags.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package sametags 6 | 7 | type X struct { 8 | Y int `tag:"foo"` 9 | } 10 | 11 | //// {{ end }} 12 | 13 | //// {{ define "newer" }} 14 | 15 | package sametags 16 | 17 | type X struct { 18 | Y int `tag:"foo"` 19 | } 20 | 21 | //// {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/patchlevel/chstructorder.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chstructorder 5 | 6 | type S struct { 7 | A int 8 | B string 9 | } 10 | // {{ end }} 11 | 12 | // {{ define "newer" }} 13 | package chstructorder 14 | 15 | type S struct { 16 | B string 17 | A int 18 | } 19 | // {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/patchlevel/chtypeunexported.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package chtypeunexported 5 | 6 | var x int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package chtypeunexported 11 | 12 | var x string 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/patchlevel/map.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package m 5 | 6 | var X map[foo]int 7 | 8 | type foo struct { 9 | a int 10 | } 11 | // {{ end }} 12 | 13 | // {{ define "newer" }} 14 | package m 15 | 16 | var X map[foo]int 17 | 18 | type foo struct { 19 | a, b int 20 | } 21 | // {{ end }} 22 | -------------------------------------------------------------------------------- /testdata/patchlevel/pointer.tmpl: -------------------------------------------------------------------------------- 1 | //// -*- mode: go -*- 2 | 3 | //// {{ define "older" }} 4 | 5 | package pointer 6 | 7 | type X struct { 8 | x int 9 | } 10 | 11 | func PrintX(x *X) { 12 | print(x.x) 13 | } 14 | 15 | //// {{ end }} 16 | 17 | //// {{ define "newer" }} 18 | 19 | package pointer 20 | 21 | type X struct { 22 | x int 23 | y string 24 | } 25 | 26 | func PrintX(x *X) { 27 | print(x.x) 28 | } 29 | 30 | //// {{ end }} 31 | 32 | -------------------------------------------------------------------------------- /testdata/patchlevel/rminternal.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rminternal 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "older/internal" }} 10 | package internal 11 | 12 | var Y, Z int 13 | // {{ end }} 14 | 15 | // {{ define "newer" }} 16 | package rminternal 17 | 18 | var X int 19 | // {{ end }} 20 | 21 | // {{ define "newer/internal" }} 22 | package internal 23 | 24 | var Y int 25 | // {{ end }} 26 | -------------------------------------------------------------------------------- /testdata/patchlevel/rmpackage.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmpackage 5 | 6 | var X int 7 | // {{ end }} 8 | 9 | // {{ define "older/subpkg" }} 10 | package subpkg 11 | 12 | var y int 13 | // {{ end }} 14 | 15 | // {{ define "newer" }} 16 | package rmpackage 17 | 18 | var X int 19 | // {{ end }} 20 | -------------------------------------------------------------------------------- /testdata/patchlevel/rmunexported.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package rmunexported 5 | 6 | var X, y int 7 | // {{ end }} 8 | 9 | // {{ define "newer" }} 10 | package rmunexported 11 | 12 | var X int 13 | // {{ end }} 14 | -------------------------------------------------------------------------------- /testdata/patchlevel/slice.tmpl: -------------------------------------------------------------------------------- 1 | // -*- mode: go -*- 2 | 3 | // {{ define "older" }} 4 | package slice 5 | 6 | var X []foo 7 | 8 | type foo struct { 9 | a int 10 | } 11 | // {{ end }} 12 | 13 | // {{ define "newer" }} 14 | package slice 15 | 16 | var X []foo 17 | 18 | type foo struct { 19 | a, b int 20 | } 21 | // {{ end }} 22 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package modver 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | 11 | "golang.org/x/tools/go/packages" 12 | ) 13 | 14 | type ( 15 | comparer struct { 16 | stack []typePair 17 | cache map[typePair]Result 18 | identicache map[typePair]bool 19 | } 20 | typePair struct{ a, b types.Type } 21 | ) 22 | 23 | func newComparer() *comparer { 24 | return &comparer{ 25 | cache: make(map[typePair]Result), 26 | identicache: make(map[typePair]bool), 27 | } 28 | } 29 | 30 | func (c *comparer) compareTypes(older, newer types.Type) (res Result) { 31 | pair := typePair{a: older, b: newer} 32 | if res, ok := c.cache[pair]; ok { 33 | if res == nil { 34 | // Break an infinite regress, 35 | // e.g. when checking type Node struct { children []*Node } 36 | return None 37 | } 38 | return res 39 | } 40 | 41 | c.cache[pair] = nil 42 | 43 | defer func() { 44 | c.cache[pair] = res 45 | }() 46 | 47 | switch older := older.(type) { 48 | case *types.Array: 49 | if newer, ok := newer.(*types.Array); ok { 50 | if res = c.compareTypes(older.Elem(), newer.Elem()); res.Code() != None { 51 | return rwrapf(res, "%s went from array of %s to array of %s", older, older.Elem(), newer.Elem()) 52 | } 53 | if older.Len() != newer.Len() { 54 | return rwrapf(Major, "%s went from length %d array to length %d", older, older.Len(), newer.Len()) 55 | } 56 | return None 57 | } 58 | return rwrapf(Major, "%s went from array to non-array", older) 59 | 60 | case *types.Chan: 61 | if newer, ok := newer.(*types.Chan); ok { 62 | if res = c.compareTypes(older.Elem(), newer.Elem()); res.Code() != None { 63 | return rwrapf(res, "%s went from channel of %s to channel of %s", older, older.Elem(), newer.Elem()) 64 | } 65 | if older.Dir() == newer.Dir() { 66 | return None 67 | } 68 | if older.Dir() == types.SendRecv { 69 | return rwrapf(Minor, "%s went from send/receive channel to %s", older, describeDirection(newer.Dir())) 70 | } 71 | return rwrapf(Major, "%s went from %s channel to %s", older, describeDirection(older.Dir()), describeDirection(newer.Dir())) 72 | } 73 | return rwrapf(Major, "%s went from channel to non-channel", older) 74 | 75 | case *types.Pointer: 76 | if newer, ok := newer.(*types.Pointer); ok { 77 | return c.compareTypes(older.Elem(), newer.Elem()) 78 | } 79 | return rwrapf(Major, "%s went from pointer to non-pointer", older) 80 | 81 | case *types.Named: 82 | if newer, ok := newer.(*types.Named); ok { 83 | return c.compareNamed(older, newer) 84 | } 85 | if older.TypeParams().Len() > 0 { 86 | return rwrapf(Major, "%s went from generic named type to unnamed %s", older, newer) 87 | } 88 | return c.compareTypes(older.Underlying(), newer) 89 | 90 | case *types.Struct: 91 | if newer, ok := newer.(*types.Struct); ok { 92 | return c.compareStructs(older, newer) 93 | } 94 | return rwrapf(Major, "%s went from struct to non-struct", older) 95 | 96 | case *types.Interface: 97 | if newer, ok := newer.(*types.Interface); ok { 98 | return c.compareInterfaces(older, newer) 99 | } 100 | return rwrapf(Major, "%s went from interface to non-interface", older) 101 | 102 | case *types.Signature: 103 | if newer, ok := newer.(*types.Signature); ok { 104 | return c.compareSignatures(older, newer) 105 | } 106 | return rwrapf(Major, "%s went from function to non-function", older) 107 | 108 | case *types.Map: 109 | if newer, ok := newer.(*types.Map); ok { 110 | kres := c.compareTypes(older.Key(), newer.Key()) 111 | vres := c.compareTypes(older.Elem(), newer.Elem()) 112 | if kres.Code() > vres.Code() { 113 | return rwrapf(kres, "in the map-key type of %s", older) 114 | } 115 | return rwrapf(vres, "in the map-value type of %s", older) 116 | } 117 | return rwrapf(Major, "%s went from map to non-map", older) 118 | 119 | case *types.Slice: 120 | if newer, ok := newer.(*types.Slice); ok { 121 | return c.compareTypes(older.Elem(), newer.Elem()) 122 | } 123 | return rwrapf(Major, "%s went from slice to non-slice", older) 124 | 125 | default: 126 | if !c.assignableTo(newer, older) { 127 | return rwrapf(Major, "%s is not assignable to %s", newer, older) 128 | } 129 | return None 130 | } 131 | } 132 | 133 | func describeDirection(dir types.ChanDir) string { 134 | switch dir { 135 | case types.SendRecv: 136 | return "send/receive" 137 | case types.SendOnly: 138 | return "send" 139 | case types.RecvOnly: 140 | return "receive" 141 | default: 142 | return fmt.Sprintf("[invalid direction %v]", dir) 143 | } 144 | } 145 | 146 | func (c *comparer) compareNamed(older, newer *types.Named) Result { 147 | res := c.compareTypeParamLists(older.TypeParams(), newer.TypeParams()) 148 | if r := c.compareTypes(older.Underlying(), newer.Underlying()); r.Code() > res.Code() { 149 | res = r 150 | } 151 | 152 | if w, ok := res.(wrapped); ok { 153 | var replaced bool 154 | for i, arg := range w.whyargs { 155 | if _, ok := arg.(types.Type); !ok { 156 | continue 157 | } 158 | if reflect.DeepEqual(arg, older.Underlying()) { 159 | w.whyargs[i] = older 160 | replaced = true 161 | } else if reflect.DeepEqual(arg, newer.Underlying()) { 162 | w.whyargs[i] = newer 163 | replaced = true 164 | } 165 | } 166 | if replaced { 167 | return w 168 | } 169 | } 170 | 171 | return rwrapf(res, "in type %s", older) 172 | } 173 | 174 | func (c *comparer) compareStructs(older, newer *types.Struct) Result { 175 | var ( 176 | olderMap = structMap(older) 177 | newerMap = structMap(newer) 178 | ) 179 | 180 | var res Result = None 181 | 182 | for i := 0; i < older.NumFields(); i++ { 183 | field := older.Field(i) 184 | if !ast.IsExported(field.Name()) { 185 | // Changes in unexported struct fields don't count. 186 | continue 187 | } 188 | newFieldIndex, ok := newerMap[field.Name()] 189 | if !ok { 190 | return rwrapf(Major, "old struct field %s was removed from %s", field.Name(), older) 191 | } 192 | newField := newer.Field(newFieldIndex) 193 | 194 | if r := c.compareTypes(field.Type(), newField.Type()); r.Code() > res.Code() { 195 | res = rwrapf(r, "struct field %s changed in %s", field.Name(), older) 196 | if res.Code() == Major { 197 | return res 198 | } 199 | } 200 | 201 | var ( 202 | tag = older.Tag(i) 203 | newTag = newer.Tag(newFieldIndex) 204 | ) 205 | if r := c.compareStructTags(tag, newTag); r.Code() == Major { 206 | return rwrapf(r, "tag change in field %s of %s", field.Name(), older) 207 | } 208 | } 209 | 210 | for i := 0; i < newer.NumFields(); i++ { 211 | field := newer.Field(i) 212 | if !ast.IsExported(field.Name()) { 213 | // Changes in unexported struct fields don't count. 214 | continue 215 | } 216 | oldFieldIndex, ok := olderMap[field.Name()] 217 | if !ok { 218 | return rwrapf(Minor, "struct field %s was added to %s", field.Name(), newer) 219 | } 220 | var ( 221 | oldTag = older.Tag(oldFieldIndex) 222 | tag = newer.Tag(i) 223 | ) 224 | if res := c.compareStructTags(oldTag, tag); res.Code() == Minor { 225 | return rwrapf(res, "tag change in field %s of %s", field.Name(), older) 226 | } 227 | } 228 | 229 | if !c.identical(older, newer) { 230 | return rwrapf(Patchlevel, "old and new versions of %s are not identical", older) 231 | } 232 | 233 | return None 234 | } 235 | 236 | func (c *comparer) compareInterfaces(older, newer *types.Interface) Result { 237 | var res Result = None 238 | 239 | if c.implements(newer, older) { 240 | if !c.implements(older, newer) { 241 | switch { 242 | case anyUnexportedMethods(older): 243 | res = rwrapf(Minor, "new interface %s is a superset of older, with unexported methods", newer) 244 | case anyInternalTypes(older): 245 | res = rwrapf(Minor, "new interface %s is a superset of older, using internal types", newer) 246 | default: 247 | res = rwrapf(Major, "new interface %s is a superset of older", newer) 248 | } 249 | } 250 | } else { 251 | return rwrapf(Major, "new interface %s does not implement old", newer) 252 | } 253 | 254 | if isNonEmptyMethodSet(older) { 255 | if isNonEmptyMethodSet(newer) { 256 | return res 257 | } 258 | return rwrap(Major, "new interface is a constraint, old one is not") 259 | } 260 | if isNonEmptyMethodSet(newer) { 261 | return rwrap(Major, "old interface is a constraint, new one is not") 262 | } 263 | 264 | olderTerms, newerTerms := termsOf(older), termsOf(newer) 265 | 266 | if len(olderTerms) == 0 { 267 | if len(newerTerms) == 0 { 268 | if older.IsComparable() { 269 | if newer.IsComparable() { 270 | return res 271 | } 272 | return rwrap(Minor, "constraint went from comparable to any") 273 | } 274 | if newer.IsComparable() { 275 | return rwrap(Major, "constraint went from any to comparable") 276 | } 277 | } 278 | if older.IsComparable() { 279 | if newer.IsComparable() { 280 | return rwrap(Major, "constraint went from all to some comparable types") 281 | } 282 | return rwrap(Major, "constraint went from comparable to (some) non-comparable types") 283 | } 284 | if newer.IsComparable() { 285 | return rwrap(Major, "constraint went from any to (some) comparable types") 286 | } 287 | return res 288 | } 289 | if len(newerTerms) == 0 { 290 | if older.IsComparable() { 291 | if newer.IsComparable() { 292 | return rwrap(Minor, "constraint went from some to all comparable types") 293 | } 294 | return rwrap(Minor, "constraint went from some comparable types to any") 295 | } 296 | if newer.IsComparable() { 297 | return rwrap(Major, "constraint went from (some) non-comparable types to comparable") 298 | } 299 | return rwrap(Major, "new constraint removes type union") 300 | } 301 | if c.termListSubset(olderTerms, newerTerms) { 302 | if c.termListSubset(newerTerms, olderTerms) { 303 | return res 304 | } 305 | return rwrapf(Minor, "older constraint type union is a subset of newer (constraint has relaxed)") 306 | } 307 | if c.termListSubset(newerTerms, olderTerms) { 308 | return rwrapf(Major, "newer constraint type union is a subset of older (constraint has tightened)") 309 | } 310 | return rwrapf(Major, "constraint type unions differ") 311 | } 312 | 313 | func anyUnexportedMethods(intf *types.Interface) bool { 314 | for i := 0; i < intf.NumMethods(); i++ { 315 | if !ast.IsExported(intf.Method(i).Name()) { 316 | return true 317 | } 318 | } 319 | return false 320 | } 321 | 322 | // Do any of the types in the method args or results have "internal" in their pkgpaths? 323 | func anyInternalTypes(intf *types.Interface) bool { 324 | for i := 0; i < intf.NumMethods(); i++ { 325 | sig, ok := intf.Method(i).Type().(*types.Signature) 326 | if !ok { 327 | // Should be impossible. 328 | continue 329 | } 330 | if anyInternalTypesInTuple(sig.Params()) || anyInternalTypesInTuple(sig.Results()) { 331 | return true 332 | } 333 | if recv := sig.Recv(); recv != nil && isInternalType(recv.Type()) { 334 | return true 335 | } 336 | } 337 | return false 338 | } 339 | 340 | func anyInternalTypesInTuple(tup *types.Tuple) bool { 341 | for i := 0; i < tup.Len(); i++ { 342 | if isInternalType(tup.At(i).Type()) { 343 | return true 344 | } 345 | } 346 | return false 347 | } 348 | 349 | func isInternalType(typ types.Type) bool { 350 | s := types.TypeString(typ, nil) 351 | if strings.HasPrefix(s, "internal.") { 352 | return true 353 | } 354 | if strings.Contains(s, "/internal.") { 355 | return true 356 | } 357 | if strings.Contains(s, "/internal/") { 358 | return true 359 | } 360 | if strings.HasPrefix(s, "main.") { 361 | return true 362 | } 363 | return strings.Contains(s, "/main.") 364 | } 365 | 366 | // This takes an interface and flattens its typelists by traversing embeds. 367 | func termsOf(typ types.Type) []*types.Term { 368 | var res []*types.Term 369 | 370 | switch typ := typ.(type) { 371 | case *types.Interface: 372 | for i := 0; i < typ.NumEmbeddeds(); i++ { 373 | emb := typ.EmbeddedType(i) 374 | res = append(res, termsOf(emb)...) 375 | } 376 | 377 | case *types.Named: 378 | res = append(res, termsOf(typ.Underlying())...) 379 | 380 | case *types.Union: 381 | for i := 0; i < typ.Len(); i++ { 382 | term := typ.Term(i) 383 | sub := termsOf(term.Type()) 384 | 385 | // TODO: Check this is the right logic for distributing term.Tilde() over the members of sub. 386 | if term.Tilde() { 387 | for _, s := range sub { 388 | res = append(res, types.NewTerm(true, s.Type())) 389 | } 390 | } else { 391 | res = append(res, sub...) 392 | } 393 | } 394 | 395 | default: 396 | return []*types.Term{types.NewTerm(false, typ)} 397 | } 398 | 399 | return res 400 | } 401 | 402 | func (c *comparer) compareSignatures(older, newer *types.Signature) Result { 403 | var ( 404 | typeParamsRes = c.compareTypeParamLists(older.TypeParams(), newer.TypeParams()) 405 | paramsRes = c.compareTuples(older.Params(), newer.Params(), !older.Variadic() && newer.Variadic()) 406 | resultsRes = c.compareTuples(older.Results(), newer.Results(), false) 407 | ) 408 | 409 | res := rwrapf(typeParamsRes, "in type parameters of %s", older) 410 | if paramsRes.Code() > res.Code() { 411 | res = rwrapf(paramsRes, "in parameters of %s", older) 412 | } 413 | if resultsRes.Code() > res.Code() { 414 | res = rwrapf(resultsRes, "in results of %s", older) 415 | } 416 | return res 417 | } 418 | 419 | func (c *comparer) compareTuples(older, newer *types.Tuple, variadicCheck bool) Result { 420 | la, lb := older.Len(), newer.Len() 421 | 422 | maybeVariadic := variadicCheck && (la+1 == lb) 423 | 424 | if la != lb && !maybeVariadic { 425 | return rwrapf(Major, "%d param(s) to %d", la, lb) 426 | } 427 | 428 | var res Result = None 429 | for i := 0; i < la; i++ { 430 | va, vb := older.At(i), newer.At(i) 431 | thisRes := c.compareTypes(va.Type(), vb.Type()) 432 | if thisRes.Code() == Major { 433 | return thisRes 434 | } 435 | if thisRes.Code() > res.Code() { 436 | res = thisRes 437 | } 438 | } 439 | 440 | if res.Code() < Minor && maybeVariadic { 441 | return rwrap(Minor, "added optional parameters") 442 | } 443 | return res 444 | } 445 | 446 | func (c *comparer) compareTypeParamLists(older, newer *types.TypeParamList) Result { 447 | if older.Len() != newer.Len() { 448 | return rwrapf(Major, "went from %d type parameter(s) to %d", older.Len(), newer.Len()) 449 | } 450 | 451 | var res Result = None 452 | 453 | for i := 0; i < older.Len(); i++ { 454 | thisRes := c.compareTypes(older.At(i).Constraint(), newer.At(i).Constraint()) 455 | if thisRes.Code() > res.Code() { 456 | res = thisRes 457 | if res.Code() == Major { 458 | break 459 | } 460 | } 461 | } 462 | 463 | return res 464 | } 465 | 466 | func (c *comparer) compareStructTags(a, b string) Result { 467 | if a == b { 468 | return None 469 | } 470 | var ( 471 | amap = tagMap(a) 472 | bmap = tagMap(b) 473 | ) 474 | for k, av := range amap { 475 | if bv, ok := bmap[k]; ok { 476 | if av != bv { 477 | return rwrapf(Major, `struct tag changed the value for key "%s" from "%s" to "%s"`, k, av, bv) 478 | } 479 | } else { 480 | return rwrapf(Major, "struct tag %s was removed", k) 481 | } 482 | } 483 | for k := range bmap { 484 | if _, ok := amap[k]; !ok { 485 | return rwrapf(Minor, "struct tag %s was added", k) 486 | } 487 | } 488 | return None 489 | } 490 | 491 | // https://golang.org/ref/spec#Assignability 492 | func (c *comparer) assignableTo(v, t types.Type) bool { 493 | if types.AssignableTo(v, t) { 494 | return true 495 | } 496 | 497 | // "x's type is identical to T" 498 | if c.identical(v, t) { 499 | return true 500 | } 501 | 502 | // "x's type V and T have identical underlying types 503 | // and at least one of V or T is not a defined type" 504 | uv, ut := v.Underlying(), t.Underlying() 505 | if c.identical(uv, ut) { 506 | if _, ok := v.(*types.Named); !ok { 507 | return true 508 | } 509 | if _, ok := t.(*types.Named); !ok { 510 | return true 511 | } 512 | } 513 | 514 | // "T is an interface type and x implements T" 515 | if intf, ok := ut.(*types.Interface); ok { 516 | if c.implements(v, intf) { 517 | return true 518 | } 519 | } 520 | 521 | if c.assignableChan(v, t, uv, ut) { 522 | return true 523 | } 524 | 525 | return c.assignableBasic(v, t, uv, ut) 526 | } 527 | 528 | func (c *comparer) assignableChan(v, t, uv, ut types.Type) bool { 529 | // "x is a bidirectional channel value, 530 | // T is a channel type, 531 | // x's type V and T have identical element types, 532 | // and at least one of V or T is not a defined type" 533 | if chv, ok := uv.(*types.Chan); ok && chv.Dir() == types.SendRecv { 534 | if cht, ok := ut.(*types.Chan); ok && c.identical(chv.Elem(), cht.Elem()) { 535 | if _, ok := v.(*types.Named); !ok { 536 | return true 537 | } 538 | if _, ok := t.(*types.Named); !ok { 539 | return true 540 | } 541 | } 542 | } 543 | return false 544 | } 545 | 546 | func (c *comparer) assignableBasic(v, t, uv, ut types.Type) bool { 547 | b, ok := v.(*types.Basic) 548 | if !ok { 549 | return false 550 | } 551 | 552 | // "x is the predeclared identifier nil 553 | // and T is a pointer, function, slice, map, channel, or interface type" 554 | if b.Kind() == types.UntypedNil { 555 | switch ut.(type) { 556 | case *types.Pointer: 557 | return true 558 | case *types.Signature: 559 | return true 560 | case *types.Slice: 561 | return true 562 | case *types.Map: 563 | return true 564 | case *types.Chan: 565 | return true 566 | case *types.Interface: 567 | return true 568 | } 569 | } 570 | 571 | // "x is an untyped constant representable by a value of type T" 572 | switch b.Kind() { 573 | case types.UntypedBool, types.UntypedInt, types.UntypedRune, types.UntypedFloat, types.UntypedComplex, types.UntypedString: 574 | return representable(b, t) 575 | } 576 | 577 | return false 578 | } 579 | 580 | // https://golang.org/ref/spec#Method_sets 581 | func (c *comparer) implements(v types.Type, t *types.Interface) bool { 582 | if types.Implements(v, t) { 583 | return true 584 | } 585 | 586 | mv, mt := methodMap(v), methodMap(t) 587 | for tname, tfn := range mt { 588 | vfn, ok := mv[tname] 589 | if !ok { 590 | return false 591 | } 592 | if !c.identical(vfn.Type(), tfn.Type()) { 593 | return false 594 | } 595 | } 596 | 597 | return true 598 | } 599 | 600 | func (c *comparer) samePackage(a, b *types.Package) bool { 601 | return a.Path() == b.Path() 602 | } 603 | 604 | // https://golang.org/ref/spec#Representability 605 | // TODO: Add range checking of literals. 606 | func representable(x *types.Basic, t types.Type) bool { 607 | tb, ok := t.Underlying().(*types.Basic) 608 | if !ok { 609 | return false 610 | } 611 | 612 | switch x.Kind() { 613 | case types.UntypedBool: 614 | return (tb.Info() & types.IsBoolean) == types.IsBoolean 615 | 616 | case types.UntypedInt: 617 | return (tb.Info() & types.IsNumeric) == types.IsNumeric 618 | 619 | case types.UntypedRune: 620 | switch tb.Kind() { 621 | case types.Int8, types.Int16, types.Uint8, types.Uint16: 622 | return false 623 | } 624 | return (tb.Info() & types.IsNumeric) == types.IsNumeric 625 | 626 | case types.UntypedFloat: 627 | if (tb.Info() & types.IsInteger) == types.IsInteger { 628 | return false 629 | } 630 | return (tb.Info() & types.IsNumeric) == types.IsNumeric 631 | 632 | case types.UntypedComplex: 633 | return (tb.Info() & types.IsComplex) == types.IsComplex 634 | 635 | case types.UntypedString: 636 | return (tb.Info() & types.IsString) == types.IsString 637 | } 638 | 639 | return false 640 | } 641 | 642 | func methodMap(t types.Type) map[string]types.Object { 643 | ms := types.NewMethodSet(t) 644 | result := make(map[string]types.Object) 645 | for i := 0; i < ms.Len(); i++ { 646 | fnobj := ms.At(i).Obj() 647 | result[fnobj.Name()] = fnobj 648 | } 649 | return result 650 | } 651 | 652 | func makePackageMap(pkgs []*packages.Package) map[string]*packages.Package { 653 | result := make(map[string]*packages.Package) 654 | for _, pkg := range pkgs { 655 | result[pkg.PkgPath] = pkg 656 | } 657 | return result 658 | } 659 | 660 | func makeTopObjs(pkg *packages.Package) map[string]types.Object { 661 | res := make(map[string]types.Object) 662 | for _, file := range pkg.Syntax { 663 | for _, decl := range file.Decls { 664 | switch decl := decl.(type) { 665 | case *ast.GenDecl: 666 | for _, spec := range decl.Specs { 667 | switch spec := spec.(type) { 668 | case *ast.ValueSpec: 669 | for _, name := range spec.Names { 670 | res[name.Name] = pkg.TypesInfo.Defs[name] 671 | } 672 | 673 | case *ast.TypeSpec: 674 | res[spec.Name.Name] = pkg.TypesInfo.Defs[spec.Name] 675 | } 676 | } 677 | 678 | case *ast.FuncDecl: 679 | // If decl is a method, qualify the name with the receiver type. 680 | name := decl.Name.Name 681 | if decl.Recv != nil && len(decl.Recv.List) > 0 { 682 | recv := decl.Recv.List[0].Type 683 | if info := pkg.TypesInfo.Types[recv]; info.Type != nil { 684 | name = types.TypeString(info.Type, types.RelativeTo(pkg.Types)) + "." + name 685 | } 686 | } 687 | 688 | res[name] = pkg.TypesInfo.Defs[decl.Name] 689 | } 690 | } 691 | } 692 | 693 | return res 694 | } 695 | 696 | func structMap(t *types.Struct) map[string]int { 697 | result := make(map[string]int) 698 | for i := 0; i < t.NumFields(); i++ { 699 | f := t.Field(i) 700 | result[f.Name()] = i 701 | } 702 | return result 703 | } 704 | 705 | var tagRE = regexp.MustCompile(`([^ ":[:cntrl:]]+):"(([^"]|\\")*)"`) 706 | 707 | func tagMap(inp string) map[string]string { 708 | res := make(map[string]string) 709 | matches := tagRE.FindAllStringSubmatch(inp, -1) 710 | for _, match := range matches { 711 | res[match[1]] = match[2] 712 | } 713 | return res 714 | } 715 | 716 | func isNonEmptyMethodSet(intf *types.Interface) bool { 717 | return intf.IsMethodSet() && intf.NumMethods() > 0 718 | } 719 | --------------------------------------------------------------------------------