Repository: mvdan/gofumpt
Pull request #239 (2022-07-27 16:03:17)

revamp //gofumpt:diagnose for real use cases
(see commit message)



mvdan: Drat, Go 1.17. I will temporarily disable the VCS stuff until Go 1.19 is released and we drop 1.17.
mvdan: Hm, that would be a larger refactor than I thought. I'll instead wait for 1.19 to be out, then drop 1.17, then rebase this to get it green.
mvdan: Rebased, should be green now.

Patch:
diff --git a/format/format.go b/format/format.go
index 00c8e2d..fa4f2fc 100644
--- a/format/format.go
+++ b/format/format.go
@@ -394,7 +394,9 @@ func (f *fumpter) applyPre(c *astutil.Cursor) {
 				if comment.Text == "//gofumpt:diagnose" || strings.HasPrefix(comment.Text, "//gofumpt:diagnose ") {
 					slc := []string{
 						"//gofumpt:diagnose",
+						"version:",
 						version.String(),
+						"flags:",
 						"-lang=" + f.LangVersion,
 						"-modpath=" + f.ModulePath,
 					}
diff --git a/gofmt.go b/gofmt.go
index e0210a8..d2a6290 100644
--- a/gofmt.go
+++ b/gofmt.go
@@ -463,7 +463,7 @@ func gofmtMain(s *sequencer) {
 
 	// Print the gofumpt version if the user asks for it.
 	if *showVersion {
-		version.Print()
+		fmt.Println(version.String())
 		return
 	}
 
diff --git a/internal/version/version.go b/internal/version/version.go
index 3ec8830..9929304 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -4,28 +4,99 @@
 package version
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
+	"runtime"
 	"runtime/debug"
+	"time"
+
+	"golang.org/x/mod/module"
 )
 
-var version = "(devel)" // to match the default from runtime/debug
+// Note that this is not a main package, so a "var version" will not work with
+// our go-cross script which uses -ldflags=main.version=xxx.
 
-func String() string {
-	if testVersion := os.Getenv("GOFUMPT_VERSION_TEST"); testVersion != "" {
-		return testVersion
+const ourModulePath = "mvdan.cc/gofumpt"
+
+const fallbackVersion = "(devel)" // to match the default from runtime/debug
+
+func findModule(info *debug.BuildInfo, modulePath string) *debug.Module {
+	if info.Main.Path == modulePath {
+		return &info.Main
 	}
-	// don't overwrite the version if it was set by -ldflags=-X
-	if info, ok := debug.ReadBuildInfo(); ok && version == "(devel)" {
-		mod := &info.Main
-		if mod.Replace != nil {
-			mod = mod.Replace
+	for _, dep := range info.Deps {
+		if dep.Path == modulePath {
+			return dep
 		}
-		version = mod.Version
 	}
-	return version
+	return nil
 }
 
-func Print() {
-	fmt.Println(String())
+func gofumptVersion() string {
+	info, ok := debug.ReadBuildInfo()
+	if !ok {
+		return fallbackVersion // no build info available
+	}
+	// Note that gofumpt may be used as a library via the format package,
+	// so we cannot assume it is the main module in the build.
+	mod := findModule(info, ourModulePath)
+	if mod == nil {
+		return fallbackVersion // not found?
+	}
+	if mod.Replace != nil {
+		mod = mod.Replace
+	}
+
+	// If we found a meaningful version, we are done.
+	// If gofumpt is not the main module, stop as well,
+	// as VCS info is only for the main module.
+	if mod.Version != "(devel)" || mod != &info.Main {
+		return mod.Version
+	}
+
+	// Fall back to trying to use VCS information.
+	// Until https://github.com/golang/go/issues/50603 is implemented,
+	// manually construct something like a pseudo-version.
+	// TODO: remove when this code is dead, hopefully in Go 1.20.
+
+	// For the tests, as we don't want the VCS information to change over time.
+	if v := os.Getenv("GARBLE_TEST_BUILDSETTINGS"); v != "" {
+		var extra []debug.BuildSetting
+		if err := json.Unmarshal([]byte(v), &extra); err != nil {
+			panic(err)
+		}
+		info.Settings = append(info.Settings, extra...)
+	}
+
+	var vcsTime time.Time
+	var vcsRevision string
+	for _, setting := range info.Settings {
+		switch setting.Key {
+		case "vcs.time":
+			// If the format is invalid, we'll print a zero timestamp.
+			vcsTime, _ = time.Parse(time.RFC3339Nano, setting.Value)
+		case "vcs.revision":
+			vcsRevision = setting.Value
+			if len(vcsRevision) > 12 {
+				vcsRevision = vcsRevision[:12]
+			}
+		}
+	}
+	if vcsRevision != "" {
+		return module.PseudoVersion("", "", vcsTime, vcsRevision)
+	}
+	return fallbackVersion
+}
+
+func goVersion() string {
+	// For the tests, as we don't want the Go version to change over time.
+	if testVersion := os.Getenv("GO_VERSION_TEST"); testVersion != "" {
+		return testVersion
+	}
+	return runtime.Version()
+}
+
+func String() string {
+	return fmt.Sprintf("%s (%s)", gofumptVersion(), goVersion())
 }


Test patch:
diff --git a/main_test.go b/main_test.go
index cae92eb..06cea00 100644
--- a/main_test.go
+++ b/main_test.go
@@ -4,12 +4,14 @@
 package main
 
 import (
+	"encoding/json"
 	"flag"
 	"os"
 	"path/filepath"
 	"testing"
 
 	qt "github.com/frankban/quicktest"
+	exec "golang.org/x/sys/execabs"
 
 	"github.com/rogpeppe/go-internal/gotooltest"
 	"github.com/rogpeppe/go-internal/testscript"
@@ -25,11 +27,31 @@ var update = flag.Bool("u", false, "update testscript output files")
 
 func TestScripts(t *testing.T) {
 	t.Parallel()
+
+	var goEnv struct {
+		GOCACHE    string
+		GOMODCACHE string
+		GOMOD      string
+	}
+	out, err := exec.Command("go", "env", "-json").CombinedOutput()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := json.Unmarshal(out, &goEnv); err != nil {
+		t.Fatal(err)
+	}
+
 	p := testscript.Params{
 		Dir:           filepath.Join("testdata", "script"),
 		UpdateScripts: *update,
+		Setup: func(env *testscript.Env) error {
+			env.Setenv("GOCACHE", goEnv.GOCACHE)
+			env.Setenv("GOMODCACHE", goEnv.GOMODCACHE)
+			env.Setenv("GOMOD_DIR", filepath.Dir(goEnv.GOMOD))
+			return nil
+		},
 	}
-	err := gotooltest.Setup(&p)
+	err = gotooltest.Setup(&p)
 	qt.Assert(t, err, qt.IsNil)
 	testscript.Run(t, p)
 }
diff --git a/testdata/gofumpt-external/go.mod b/testdata/gofumpt-external/go.mod
new file mode 100644
index 0000000..5d0976a
--- /dev/null
+++ b/testdata/gofumpt-external/go.mod
@@ -0,0 +1,11 @@
+module test/gofumpt-external
+
+go 1.18
+
+require mvdan.cc/gofumpt v0.3.2-0.20220627183521-8dda8068d9f3
+
+require (
+	github.com/google/go-cmp v0.5.8 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/tools v0.1.11 // indirect
+)
diff --git a/testdata/gofumpt-external/go.sum b/testdata/gofumpt-external/go.sum
new file mode 100644
index 0000000..da5c2a6
--- /dev/null
+++ b/testdata/gofumpt-external/go.sum
@@ -0,0 +1,13 @@
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/rogpeppe/go-internal v1.8.2-0.20220624104257-af73bbc5c731 h1:LkP6LNQyXrQoVVXMpb+sbb0iibcaOZH97feeh778heA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0=
+golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
+golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
+mvdan.cc/gofumpt v0.3.2-0.20220627183521-8dda8068d9f3 h1:Kk6yeaipDobigAr+NZqq37LjLN1q3LHSOpcaDLDwyoI=
+mvdan.cc/gofumpt v0.3.2-0.20220627183521-8dda8068d9f3/go.mod h1:TSc7K1qXnyCCK7LUmDAWp4UMntOys3CzN8ksMKaxFrE=
diff --git a/testdata/gofumpt-external/main.go b/testdata/gofumpt-external/main.go
new file mode 100644
index 0000000..22058e6
--- /dev/null
+++ b/testdata/gofumpt-external/main.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+	"io"
+	"os"
+
+	"mvdan.cc/gofumpt/format"
+)
+
+func main() {
+	orig, err := io.ReadAll(os.Stdin)
+	if err != nil {
+		panic(err)
+	}
+	formatted, err := format.Source(orig, format.Options{
+		LangVersion: "v1.16",
+	})
+	if err != nil {
+		panic(err)
+	}
+	os.Stdout.Write(formatted)
+}
diff --git a/testdata/script/diagnose.txtar b/testdata/script/diagnose.txtar
index 52f31c1..e2d6549 100644
--- a/testdata/script/diagnose.txtar
+++ b/testdata/script/diagnose.txtar
@@ -1,19 +1,21 @@
-cp foo.go foo.go.orig
-env GOFUMPT_VERSION_TEST=v0.1.1-0.20210524140936-ba6406b58f36
+env GO_VERSION_TEST=go1.18.29
 
-gofumpt -w foo.go
-cmp foo.go foo.go.golden
+# First, test a local build of gofumpt resulting from 'git clone'.
+# Its version will be inferred from VCS, but since we want a stable test,
+# we mock the VCS information. Note that test binaries do not have VCS info.
+# Data obtained from a real build while developing.
+env GARBLE_TEST_BUILDSETTINGS='[{"Key":"vcs","Value":"git"},{"Key":"vcs.revision","Value":"8dda8068d9f339047fc1777b688afb66a0a0db17"},{"Key":"vcs.time","Value":"2022-07-27T15:58:40Z"},{"Key":"vcs.modified","Value":"true"}]'  
+gofumpt foo.go
+cmp stdout foo.go.golden
 
-gofumpt -w outdated.go
-cmp outdated.go foo.go.golden
+gofumpt outdated.go
+cmp stdout foo.go.golden
 
-cp foo.go.orig foo.go
-gofumpt -w -extra foo.go
-cmp foo.go foo.go.golden-extra
+gofumpt -extra foo.go
+cmp stdout foo.go.golden-extra
 
-cp foo.go.orig foo.go
-gofumpt -w -lang=v1 foo.go
-cmp foo.go foo.go.golden-lang
+gofumpt -lang=v1 foo.go
+cmp stdout foo.go.golden-lang
 
 gofumpt -d nochange.go
 ! stdout .
@@ -24,6 +26,40 @@ gofumpt -d foo.go.golden
 gofumpt -d -extra foo.go.golden-extra
 ! stdout .
 
+# A local build without VCS information will result in a missing version.
+env GARBLE_TEST_BUILDSETTINGS='[]'
+gofumpt foo.go
+cmp stdout foo.go.golden-devel
+
+[short] stop 'the rest of this test builds gofumpt binaries'
+
+# We want a published version of gofumpt on the public module proxies,
+# because that's the only way that its module version will be included.
+# Using a directory replace directive will not work.
+# This means that any change in how gofumpt reports its own version
+# will require two pull requests, the second one updating the test script.
+# We could consider using go-internal/goproxytest, but then we would need to
+# manually run something like go-internal/cmd/txtar-addmod reguarly.
+# Or teach goproxytest to serve a mock version of gofumpt from its local checkout.
+# Either way, both are relatively overkill for now.
+env GOBIN=${WORK}/bin
+env GOFUMPT_PUBLISHED_VERSION=v0.3.2-0.20220627183521-8dda8068d9f3
+
+# TODO: update these once the library fix hits master
+
+# gofumpt as the main binary with a real module version.
+go install mvdan.cc/gofumpt@${GOFUMPT_PUBLISHED_VERSION}
+exec ${GOBIN}/gofumpt foo.go
+cmp stdout foo.go.golden-released
+
+# gofumpt as a library with a real module version.
+cd ${GOMOD_DIR}/testdata/gofumpt-external
+go install .
+cd ${WORK}
+stdin foo.go
+exec ${GOBIN}/gofumpt-external
+cmp stdout foo.go.golden-external
+
 -- go.mod --
 module test
 
@@ -43,12 +79,24 @@ package p
 -- foo.go.golden --
 package p
 
-//gofumpt:diagnose v0.1.1-0.20210524140936-ba6406b58f36 -lang=v1.16 -modpath=test
+//gofumpt:diagnose version: v0.0.0-20220727155840-8dda8068d9f3 (go1.18.29) flags: -lang=v1.16 -modpath=test
+-- foo.go.golden-devel --
+package p
+
+//gofumpt:diagnose version: (devel) (go1.18.29) flags: -lang=v1.16 -modpath=test
 -- foo.go.golden-extra --
 package p
 
-//gofumpt:diagnose v0.1.1-0.20210524140936-ba6406b58f36 -lang=v1.16 -modpath=test -extra
+//gofumpt:diagnose version: v0.0.0-20220727155840-8dda8068d9f3 (go1.18.29) flags: -lang=v1.16 -modpath=test -extra
 -- foo.go.golden-lang --
 package p
 
-//gofumpt:diagnose v0.1.1-0.20210524140936-ba6406b58f36 -lang=v1 -modpath=test
+//gofumpt:diagnose version: v0.0.0-20220727155840-8dda8068d9f3 (go1.18.29) flags: -lang=v1 -modpath=test
+-- foo.go.golden-released --
+package p
+
+//gofumpt:diagnose v0.3.2-0.20220627183521-8dda8068d9f3 -lang=v1.16 -modpath=test
+-- foo.go.golden-external --
+package p
+
+//gofumpt:diagnose (devel) -lang=v1.16 -modpath=