Skip to content

Commit 72eb0c3

Browse files
committed
automatically detect Go module path
This started as a proof-of-concept for #30. Signed-off-by: Dominik Menke <[email protected]>
1 parent bf921fe commit 72eb0c3

File tree

13 files changed

+256
-3
lines changed

13 files changed

+256
-3
lines changed

cmd/gci/gcicommand.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func (e *Executor) newGciCommand(use, short, long string, aliases []string, stdI
3030
if err != nil {
3131
return err
3232
}
33+
if err = gciCfg.InitializeModules(args); err != nil {
34+
return err
35+
}
3336
if *debug {
3437
log.SetLevel(zapcore.DebugLevel)
3538
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/spf13/cobra v1.3.0
88
github.com/stretchr/testify v1.7.0
99
go.uber.org/zap v1.17.0
10+
golang.org/x/mod v0.5.0
1011
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
1112
golang.org/x/tools v0.1.5
1213
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
@@ -19,7 +20,6 @@ require (
1920
github.com/spf13/pflag v1.0.5 // indirect
2021
go.uber.org/atomic v1.7.0 // indirect
2122
go.uber.org/multierr v1.6.0 // indirect
22-
golang.org/x/mod v0.5.0 // indirect
2323
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
2424
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
2525
)

pkg/gci/configuration.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,45 @@ func (g GciStringConfiguration) Parse() (*GciConfiguration, error) {
4040
return &GciConfiguration{g.Cfg, sections, sectionSeparators}, nil
4141
}
4242

43+
// InitializeModules collects and remembers Go module names for the given
44+
// files, by traversing the file system.
45+
//
46+
// This method requires that g.Sections contains the Module section,
47+
// otherwise InitializeModules does nothing. This also implies that
48+
// this method should be called after changes to g.Sections, for example
49+
// right after (*GciStringConfiguration).Parse().
50+
func (g *GciConfiguration) InitializeModules(files []string) error {
51+
var moduleSection *sectionsPkg.Module
52+
for _, section := range g.Sections {
53+
if m, ok := section.(sectionsPkg.Module); ok {
54+
moduleSection = &m
55+
break
56+
}
57+
}
58+
if moduleSection == nil {
59+
// skip collecting Go modules when not needed
60+
return nil
61+
}
62+
63+
resolver := make(moduleResolver)
64+
knownModulePaths := map[string]struct{}{} // unique list of Go modules
65+
for _, file := range files {
66+
path, err := resolver.Lookup(file)
67+
if err != nil {
68+
return err
69+
}
70+
if path != "" {
71+
knownModulePaths[path] = struct{}{}
72+
}
73+
}
74+
modulePaths := make([]string, 0, len(knownModulePaths))
75+
for path := range knownModulePaths {
76+
modulePaths = append(modulePaths, path)
77+
}
78+
moduleSection.SetModulePaths(modulePaths)
79+
return nil
80+
}
81+
4382
func initializeGciConfigFromYAML(filePath string) (*GciConfiguration, error) {
4483
yamlCfg := GciStringConfiguration{}
4584
yamlData, err := ioutil.ReadFile(filePath)

pkg/gci/gci.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ func (list SectionList) String() []string {
2828
}
2929

3030
func DefaultSections() SectionList {
31-
return SectionList{sectionsPkg.StandardPackage{}, sectionsPkg.DefaultSection{nil, nil}}
31+
return SectionList{
32+
sectionsPkg.StandardPackage{},
33+
sectionsPkg.DefaultSection{nil, nil},
34+
sectionsPkg.Module{},
35+
}
3236
}
3337

3438
func DefaultSectionSeparators() SectionList {

pkg/gci/gci_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,20 @@ func TestRun(t *testing.T) {
3232
}
3333
for _, testFile := range testFiles {
3434
fileBaseName := strings.TrimSuffix(testFile, ".in.go")
35+
if fileBaseName != testFilesPath+"/modules" {
36+
continue
37+
}
38+
3539
t.Run(fileBaseName, func(t *testing.T) {
3640
t.Parallel()
3741

3842
gciCfg, err := initializeGciConfigFromYAML(fileBaseName + ".cfg.yaml")
3943
if err != nil {
4044
t.Fatal(err)
4145
}
46+
if err = gciCfg.InitializeModules([]string{testFile}); err != nil {
47+
t.Fatal(err)
48+
}
4249

4350
_, formattedFile, err := LoadFormatGoFile(io.File{fileBaseName + ".in.go"}, *gciCfg)
4451
if err != nil {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sections:
2+
- Standard
3+
- Module
4+
- Default
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package main
2+
import (
3+
"github.com/daixiang0/gci"
4+
5+
"golang.org/x/tools"
6+
7+
"fmt"
8+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package main
2+
import (
3+
"fmt"
4+
5+
"github.com/daixiang0/gci"
6+
7+
"golang.org/x/tools"
8+
)

pkg/gci/mod.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
}

pkg/gci/sections/module.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package sections
2+
3+
import (
4+
"strings"
5+
6+
"github.com/daixiang0/gci/pkg/configuration"
7+
importPkg "github.com/daixiang0/gci/pkg/gci/imports"
8+
"github.com/daixiang0/gci/pkg/gci/specificity"
9+
)
10+
11+
func init() {
12+
prefixType := SectionType{
13+
generatorFun: func(parameter string, sectionPrefix, sectionSuffix Section) (Section, error) {
14+
return Module{}, nil
15+
},
16+
aliases: []string{"Module", "Mod"},
17+
description: "Groups all imports of the corresponding Go module",
18+
}.StandAloneSection().WithoutParameter()
19+
SectionParserInst.registerSectionWithoutErr(&prefixType)
20+
}
21+
22+
type Module struct {
23+
// modulePaths contains all known Go module path names.
24+
//
25+
// This must be a pointer, because gci.formatImportBlock() will create
26+
// mapping between sections and imports, and slices are unhashable.
27+
modulePaths *[]string
28+
}
29+
30+
func (m Module) MatchSpecificity(spec importPkg.ImportDef) specificity.MatchSpecificity {
31+
if m.modulePaths == nil {
32+
return specificity.MisMatch{}
33+
}
34+
35+
importPath := spec.Path()
36+
for _, path := range *m.modulePaths {
37+
if strings.HasPrefix(importPath, path) {
38+
return specificity.Module{}
39+
}
40+
}
41+
return specificity.MisMatch{}
42+
}
43+
44+
func (m Module) Format(imports []importPkg.ImportDef, cfg configuration.FormatterConfiguration) string {
45+
return inorderSectionFormat(m, imports, cfg)
46+
}
47+
48+
func (Module) sectionPrefix() Section { return nil }
49+
func (Module) sectionSuffix() Section { return nil }
50+
51+
func (Module) String() string {
52+
return "Module"
53+
}
54+
55+
func (m *Module) SetModulePaths(paths []string) {
56+
dup := make([]string, len(paths), len(paths))
57+
copy(dup, paths)
58+
59+
m.modulePaths = &dup
60+
}

0 commit comments

Comments
 (0)