Skip to content

Commit

Permalink
automatically detect Go module path
Browse files Browse the repository at this point in the history
This started as a proof-of-concept for #30.

Signed-off-by: Dominik Menke <dom@digineo.de>
  • Loading branch information
dmke committed May 30, 2022
1 parent bf921fe commit 9152fd2
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 3 deletions.
3 changes: 3 additions & 0 deletions cmd/gci/gcicommand.go
Expand Up @@ -30,6 +30,9 @@ func (e *Executor) newGciCommand(use, short, long string, aliases []string, stdI
if err != nil {
return err
}
if err = gciCfg.InitializeModules(args); err != nil {
return err
}
if *debug {
log.SetLevel(zapcore.DebugLevel)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -7,6 +7,7 @@ require (
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
go.uber.org/zap v1.17.0
golang.org/x/mod v0.5.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/tools v0.1.5
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
Expand All @@ -19,7 +20,6 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.5.0 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
39 changes: 39 additions & 0 deletions pkg/gci/configuration.go
Expand Up @@ -40,6 +40,45 @@ func (g GciStringConfiguration) Parse() (*GciConfiguration, error) {
return &GciConfiguration{g.Cfg, sections, sectionSeparators}, nil
}

// InitializeModules collects and remembers Go module names for the given
// files, by traversing the file system.
//
// This method requires that g.Sections contains the Module section,
// otherwise InitializeModules does nothing. This also implies that
// this method should be called after changes to g.Sections, for example
// right after (*GciStringConfiguration).Parse().
func (g *GciConfiguration) InitializeModules(files []string) error {
var moduleSection *sectionsPkg.Module
for _, section := range g.Sections {
if m, ok := section.(sectionsPkg.Module); ok {
moduleSection = &m
break
}
}
if moduleSection == nil {
// skip collecting Go modules when not needed
return nil
}

resolver := make(moduleResolver)
knownModulePaths := map[string]struct{}{} // unique list of Go modules
for _, file := range files {
path, err := resolver.Lookup(file)
if err != nil {
return err
}
if path != "" {
knownModulePaths[path] = struct{}{}
}
}
modulePaths := make([]string, 0, len(knownModulePaths))
for path := range knownModulePaths {
modulePaths = append(modulePaths, path)
}
moduleSection.SetModulePaths(modulePaths)
return nil
}

func initializeGciConfigFromYAML(filePath string) (*GciConfiguration, error) {
yamlCfg := GciStringConfiguration{}
yamlData, err := ioutil.ReadFile(filePath)
Expand Down
6 changes: 5 additions & 1 deletion pkg/gci/gci.go
Expand Up @@ -28,7 +28,11 @@ func (list SectionList) String() []string {
}

func DefaultSections() SectionList {
return SectionList{sectionsPkg.StandardPackage{}, sectionsPkg.DefaultSection{nil, nil}}
return SectionList{
sectionsPkg.StandardPackage{},
sectionsPkg.DefaultSection{nil, nil},
sectionsPkg.Module{},
}
}

func DefaultSectionSeparators() SectionList {
Expand Down
3 changes: 3 additions & 0 deletions pkg/gci/gci_test.go
Expand Up @@ -39,6 +39,9 @@ func TestRun(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err = gciCfg.InitializeModules([]string{testFile}); err != nil {
t.Fatal(err)
}

_, formattedFile, err := LoadFormatGoFile(io.File{fileBaseName + ".in.go"}, *gciCfg)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/gci/internal/testdata/modules.cfg.yaml
@@ -0,0 +1,4 @@
sections:
- Standard
- Module
- Default
8 changes: 8 additions & 0 deletions pkg/gci/internal/testdata/modules.in.go
@@ -0,0 +1,8 @@
package main
import (
"github.com/daixiang0/gci"

"golang.org/x/tools"

"fmt"
)
8 changes: 8 additions & 0 deletions pkg/gci/internal/testdata/modules.out.go
@@ -0,0 +1,8 @@
package main
import (
"fmt"

"github.com/daixiang0/gci"

"golang.org/x/tools"
)
100 changes: 100 additions & 0 deletions pkg/gci/mod.go
@@ -0,0 +1,100 @@
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

func (m moduleResolver) Lookup(file string) (string, error) {
abs, err := filepath.Abs(file)
if err != nil {
return "", fmt.Errorf("could not make path absolute: %w", err)
}

var bestMatch string
for path := range m {
if strings.HasPrefix(abs, path) && len(path) > len(bestMatch) {
bestMatch = path
}
}
if bestMatch != "" {
return m[bestMatch], nil
}

return m.findRecursively(filepath.Dir(abs))
}

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 exist 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)
if dir != "/" {
// add trailing path sep, but not for *nix root directory
dir += string(os.PathListSeparator)
}
m[dir] = mpath
return mpath, nil
}
}
60 changes: 60 additions & 0 deletions pkg/gci/sections/module.go
@@ -0,0 +1,60 @@
package sections

import (
"strings"

"github.com/daixiang0/gci/pkg/configuration"
importPkg "github.com/daixiang0/gci/pkg/gci/imports"
"github.com/daixiang0/gci/pkg/gci/specificity"
)

func init() {
prefixType := SectionType{
generatorFun: func(parameter string, sectionPrefix, sectionSuffix Section) (Section, error) {
return Module{}, nil
},
aliases: []string{"Module", "Mod"},
description: "Groups all imports of the corresponding Go module",
}.StandAloneSection().WithoutParameter()
SectionParserInst.registerSectionWithoutErr(&prefixType)
}

type Module struct {
// modulePaths contains all known Go module path names.
//
// This must be a pointer, because gci.formatImportBlock() will create
// mapping between sections and imports, and slices are unhashable.
modulePaths *[]string
}

func (m Module) MatchSpecificity(spec importPkg.ImportDef) specificity.MatchSpecificity {
if m.modulePaths == nil {
return specificity.MisMatch{}
}

importPath := spec.Path()
for _, path := range *m.modulePaths {
if strings.HasPrefix(importPath, path) {
return specificity.Module{}
}
}
return specificity.MisMatch{}
}

func (m Module) Format(imports []importPkg.ImportDef, cfg configuration.FormatterConfiguration) string {
return inorderSectionFormat(m, imports, cfg)
}

func (Module) sectionPrefix() Section { return nil }
func (Module) sectionSuffix() Section { return nil }

func (Module) String() string {
return "Module"
}

func (m *Module) SetModulePaths(paths []string) {
dup := make([]string, len(paths), len(paths))
copy(dup, paths)

m.modulePaths = &dup
}
19 changes: 19 additions & 0 deletions pkg/gci/specificity/module.go
@@ -0,0 +1,19 @@
package specificity

type Module struct{}

func (m Module) IsMoreSpecific(than MatchSpecificity) bool {
return isMoreSpecific(m, than)
}

func (m Module) Equal(to MatchSpecificity) bool {
return equalSpecificity(m, to)
}

func (Module) class() specificityClass {
return ModuleClass
}

func (Module) String() string {
return "Module"
}
1 change: 1 addition & 0 deletions pkg/gci/specificity/specificity.go
Expand Up @@ -7,6 +7,7 @@ const (
DefaultClass = 10
StandardPackageClass = 20
MatchClass = 30
ModuleClass = 40
)

// MatchSpecificity is used to determine which section matches an import best
Expand Down
2 changes: 1 addition & 1 deletion pkg/gci/specificity/specificity_test.go
Expand Up @@ -25,5 +25,5 @@ func TestSpecificityEquality(t *testing.T) {
}

func testCasesInSpecificityOrder() []MatchSpecificity {
return []MatchSpecificity{MisMatch{}, Default{}, StandardPackageMatch{}, Match{0}, Match{1}}
return []MatchSpecificity{MisMatch{}, Default{}, StandardPackageMatch{}, Match{0}, Match{1}, Module{}}
}

0 comments on commit 9152fd2

Please sign in to comment.