Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatically detect Go module path #31

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work, moduleSection is a pointer to a copy of g.Section[i].

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{}}
}