|
| 1 | +package gci |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "io/ioutil" |
| 7 | + "os" |
| 8 | + "path/filepath" |
| 9 | + "strings" |
| 10 | + |
| 11 | + "golang.org/x/mod/modfile" |
| 12 | +) |
| 13 | + |
| 14 | +// moduleResolver looksup the module path for a given (Go) file. |
| 15 | +// To improve performance, the file paths and module paths are |
| 16 | +// cached. |
| 17 | +// |
| 18 | +// Given the following directory structure: |
| 19 | +// |
| 20 | +// /path/to/example |
| 21 | +// +-- go.mod (module example) |
| 22 | +// +-- cmd/sample/main.go (package main, imports example/util) |
| 23 | +// +-- util/util.go (package util) |
| 24 | +// |
| 25 | +// After looking up main.go and util.go, the internal cache will contain: |
| 26 | +// |
| 27 | +// "/path/to/foobar/": "example" |
| 28 | +// |
| 29 | +// For more complex module structures (i.e. sub-modules), the cache |
| 30 | +// might look like this: |
| 31 | +// |
| 32 | +// "/path/to/example/": "example" |
| 33 | +// "/path/to/example/cmd/sample/": "go.example.com/historic/path" |
| 34 | +// |
| 35 | +// When matching files against this cache, the resolver will select the |
| 36 | +// entry with the most specific path (so that, in this example, the file |
| 37 | +// cmd/sample/main.go will resolve to go.example.com/historic/path). |
| 38 | +type moduleResolver map[string]string |
| 39 | + |
| 40 | +func (m moduleResolver) Lookup(file string) (string, error) { |
| 41 | + abs, err := filepath.Abs(file) |
| 42 | + if err != nil { |
| 43 | + return "", fmt.Errorf("could not make path absolute: %w", err) |
| 44 | + } |
| 45 | + |
| 46 | + var bestMatch string |
| 47 | + for path := range m { |
| 48 | + if strings.HasPrefix(abs, path) && len(path) > len(bestMatch) { |
| 49 | + bestMatch = path |
| 50 | + } |
| 51 | + } |
| 52 | + if bestMatch != "" { |
| 53 | + return m[bestMatch], nil |
| 54 | + } |
| 55 | + |
| 56 | + return m.findRecursively(filepath.Dir(abs)) |
| 57 | +} |
| 58 | + |
| 59 | +func (m moduleResolver) findRecursively(dir string) (string, error) { |
| 60 | + // When going up the directory tree, we might never find a go.mod |
| 61 | + // file. In this case remember where we started, so that the next |
| 62 | + // time we can short circuit the recursive ascent. |
| 63 | + stop := dir |
| 64 | + |
| 65 | + for { |
| 66 | + gomod := filepath.Join(dir, "go.mod") |
| 67 | + _, err := os.Stat(gomod) |
| 68 | + if errors.Is(err, os.ErrNotExist) { |
| 69 | + // go.mod doesn't exist at current location |
| 70 | + next := filepath.Dir(dir) |
| 71 | + if next == dir { |
| 72 | + // we're at the top of the filesystem |
| 73 | + m[stop] = "" |
| 74 | + return "", nil |
| 75 | + } |
| 76 | + // go one level up |
| 77 | + dir = next |
| 78 | + continue |
| 79 | + } else if err != nil { |
| 80 | + // other error (likely EPERM) |
| 81 | + return "", fmt.Errorf("module lookup failed: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + // we found a go.mod |
| 85 | + mod, err := ioutil.ReadFile(gomod) |
| 86 | + if err != nil { |
| 87 | + return "", fmt.Errorf("reading module failed: %w", err) |
| 88 | + } |
| 89 | + |
| 90 | + // store module path at m[dir]. add path separator to avoid |
| 91 | + // false-positive (think of /foo and /foobar). |
| 92 | + mpath := modfile.ModulePath(mod) |
| 93 | + if dir != "/" { |
| 94 | + // add trailing path sep, but not for *nix root directory |
| 95 | + dir += string(os.PathListSeparator) |
| 96 | + } |
| 97 | + m[dir] = mpath |
| 98 | + return mpath, nil |
| 99 | + } |
| 100 | +} |
0 commit comments