From be45039a81ffa8adbc2690d0cd740dac40a53fb1 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Tue, 26 Jan 2021 23:13:34 +0100 Subject: [PATCH] automatically detect Go module path This is just a proof of concept for #30. --- go.mod | 5 ++- go.sum | 2 + main.go | 1 + pkg/gci/gci.go | 7 ++++ pkg/gci/mod.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 pkg/gci/mod.go diff --git a/go.mod b/go.mod index 8eae436..1702945 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/daixiang0/gci go 1.14 -require golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394 +require ( + golang.org/x/mod v0.4.1 + golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394 +) diff --git a/go.sum b/go.sum index bfc445e..f8184cf 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= diff --git a/main.go b/main.go index 6884ae5..34a4aa3 100644 --- a/main.go +++ b/main.go @@ -61,5 +61,6 @@ func main() { } } } + gci.ClearModCache() os.Exit(exitCode) } diff --git a/pkg/gci/gci.go b/pkg/gci/gci.go index 7e31b87..bcaadee 100644 --- a/pkg/gci/gci.go +++ b/pkg/gci/gci.go @@ -294,6 +294,10 @@ func processFile(filename string, out io.Writer, set *FlagSet) error { ret := bytes.Split(src[start+len(importStartFlag):end], []byte(linebreak)) + if set.LocalFlag == "" { + set.LocalFlag, _ = modCache.Lookup(filename) + fmt.Println("file", filename, "mod", set.LocalFlag) + } p := newPkg(ret, set.LocalFlag) res := append(src[:start+len(importStartFlag)], append(p.fmt(), src[end+1:]...)...) @@ -356,6 +360,9 @@ func Run(filename string, set *FlagSet) ([]byte, []byte, error) { ret := bytes.Split(src[start+len(importStartFlag):end], []byte(linebreak)) + if set.LocalFlag == "" { + set.LocalFlag, _ = modCache.Lookup(filename) + } p := newPkg(ret, set.LocalFlag) res := append(src[:start+len(importStartFlag)], append(p.fmt(), src[end+1:]...)...) diff --git a/pkg/gci/mod.go b/pkg/gci/mod.go new file mode 100644 index 0000000..416fcad --- /dev/null +++ b/pkg/gci/mod.go @@ -0,0 +1,109 @@ +package gci + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "golang.org/x/mod/modfile" +) + +// moduleResolver looksup the module path for a given (Go) file. +// To improve performance, the file paths and module paths are +// cached. +// +// Given the following directory structure: +// +// /path/to/example +// +-- go.mod (module example) +// +-- cmd/sample/main.go (package main, imports example/util) +// +-- util/util.go (package util) +// +// After looking up main.go and util.go, the internal cache will contain: +// +// "/path/to/foobar/": "example" +// +// For more complex module structures (i.e. sub-modules), the cache +// might look like this: +// +// "/path/to/example/": "example" +// "/path/to/example/cmd/sample/": "go.example.com/historic/path" +// +// When matching files against this cache, the resolver will select the +// entry with the most specific path (so that, in this example, the file +// cmd/sample/main.go will resolve to go.example.com/historic/path). +type moduleResolver map[string]string + +var modCache = make(moduleResolver) + +// ClearModCache will reset the internal module cache used by ProcessFile +// and WalkDir. When invoking either of those functions with an empty +// FlagSet.LocalFlag, we will try to resolve a matching Go module path. +// +// You should call this method when you're done with processing files. +func ClearModCache() { + modCache = make(moduleResolver) +} + +func (m moduleResolver) Lookup(file string) (string, error) { + var bestMatch string + for path := range m { + if strings.HasPrefix(file, path) && len(path) > len(bestMatch) { + bestMatch = path + } + } + + if bestMatch != "" { + return m[bestMatch], nil + } + + dir, err := filepath.Abs(filepath.Dir(file)) + if err != nil { + return "", fmt.Errorf("could not make path absolute: %w", err) + } + + return m.findRecursively(dir) +} + +func (m moduleResolver) findRecursively(dir string) (string, error) { + // When going up the directory tree, we might never find a go.mod + // file. In this case remember where we started, so that the next + // time we can short circuit the recursive ascent. + stop := dir + + for { + gomod := filepath.Join(dir, "go.mod") + _, err := os.Stat(gomod) + if errors.Is(err, os.ErrNotExist) { + // go.mod doesn't exists at current location + next := filepath.Dir(dir) + if next == dir { + // we're at the top of the filesystem + m[stop] = "" + return "", nil + } + // go one level up + dir = next + continue + } else if err != nil { + // other error (likely EPERM + return "", fmt.Errorf("module lookup failed: %w", err) + } + + // we found a go.mod + mod, err := ioutil.ReadFile(gomod) + if err != nil { + return "", fmt.Errorf("reading module failed: %w", err) + } + + // store module path at m[dir]. add path separator to avoid + // false-positive (think of /foo and /foobar). + mpath := modfile.ModulePath(mod) + m[dir+string(os.PathSeparator)] = mpath + + return mpath, nil + } +}