Skip to content

Commit

Permalink
automatically detect Go module path
Browse files Browse the repository at this point in the history
This is just a proof of concept for #30.
  • Loading branch information
dmke committed Jan 26, 2021
1 parent 7eb50f3 commit be45039
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 1 deletion.
5 changes: 4 additions & 1 deletion go.mod
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions main.go
Expand Up @@ -61,5 +61,6 @@ func main() {
}
}
}
gci.ClearModCache()
os.Exit(exitCode)
}
7 changes: 7 additions & 0 deletions pkg/gci/gci.go
Expand Up @@ -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:]...)...)
Expand Down Expand Up @@ -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:]...)...)
Expand Down
109 changes: 109 additions & 0 deletions 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
}
}

0 comments on commit be45039

Please sign in to comment.