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

Add PackagesInDirMatchingRootModule function to pkgpath #351

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkgpath/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ require (
github.com/palantir/pkg v1.1.0
github.com/palantir/pkg/matcher v1.2.0
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.20.0
golang.org/x/tools v0.24.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sync v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 6 additions & 0 deletions pkgpath/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
Expand Down
128 changes: 119 additions & 9 deletions pkgpath/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import (
"go/build"
"go/parser"
"go/token"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"

"github.com/palantir/pkg/matcher"
"golang.org/x/mod/modfile"
gopackages "golang.org/x/tools/go/packages"
)

// DefaultGoPkgExcludeMatcher returns a matcher that matches names that standard Go tools generally exclude as Go
Expand Down Expand Up @@ -228,36 +231,80 @@ func PackagesFromPaths(rootDir string, relPaths []string) (Packages, error) {
// PackagesInDir creates a Packages that contains all of the packages rooted at the provided directory. Every directory
// rooted in the provided directory whose path does not match the provided exclude matcher is considered as a package.
func PackagesInDir(rootDir string, exclude matcher.Matcher) (Packages, error) {
return packagesInDir(rootDir, nil, exclude)
}

// PackagesInDirMatchingRootModule creates a Packages that contains all of the packages rooted at the provided directory
// that are part of the same module as the root directory. Every directory rooted in the provided directory whose path
// does not match the provided exclude matcher and is part of the same module as the module of the root directory is
// considered as a package.
func PackagesInDirMatchingRootModule(rootDir string, exclude matcher.Matcher) (Packages, error) {
return packagesInDir(rootDir, nonRootModuleExcluder(rootDir), exclude)
}

// packagesInDir creates a Packages that contains all of the packages rooted at the provided directory. Every
// directory rooted in the provided directory whose path does not match the provided exclude matcher is considered as a
// package. A directory is only considered if pkgDirExcluder and pkgFileExcluder do not match it. If a directory is
// considered, its package will be determined based on considering only the files in the directory that do not match the
// pkgFileExcluder. If pkgDirExcluder returns a value that indicates that the subdirectories should be skipped, those
// subdirectories will not be considered.
func packagesInDir(rootDir string, pkgDirExcluder pkgExcluder, pkgFileExcluder matcher.Matcher) (Packages, error) {
dirAbsolutePath, err := filepath.Abs(rootDir)
if err != nil {
return nil, fmt.Errorf("failed to convert %s to absolute path: %v", rootDir, err)
}

allPkgs := make(map[string]string)
if err := filepath.Walk(dirAbsolutePath, func(currPath string, currInfo os.FileInfo, err error) error {
currRelPath, currRelPathErr := filepath.Rel(dirAbsolutePath, currPath)

// skip current path if it matches an exclude
if currRelPathErr == nil && exclude != nil && exclude.Match(currRelPath) {
return nil
}

if err := filepath.WalkDir(dirAbsolutePath, func(currPath string, currInfo fs.DirEntry, err error) error {
// if there was any error reading path, return error
if err != nil {
return err
}

// skip path if it is not a directory
if !currInfo.IsDir() {
return nil
}

// determine relative path for package
currRelPath, currRelPathErr := filepath.Rel(dirAbsolutePath, currPath)
if currRelPathErr != nil {
return currRelPathErr
}

skipDir := false

// if pkgDirExcluder is non-nil, check if directory should be excluded
if pkgDirExcluder != nil {
// determine whether current directory should be excluded
excludeDir, skipAllSubDirs := pkgDirExcluder.Exclude(currRelPath)

// update value of "skipDir" parameter: this will be used later (even if current directory is not excluded,
// if this value is true, the subdirectories will still be skipped).
skipDir = skipAllSubDirs

// if current directory should be excluded, return. Use "skipDir" parameter to determine whether all
// subdirectories should be skipped.
if excludeDir {
if skipDir {
return filepath.SkipDir
}
return nil
}
}

// if pkgFileExcluder is non-nil, check if directory should be excluded
if pkgFileExcluder != nil && pkgFileExcluder.Match(currRelPath) {
if skipDir {
return filepath.SkipDir
}
return nil
}

// create a filter for processing package files that only passes if it does not match an exclude
filter := func(info os.FileInfo) bool {
// if exclude exists and matches the file, skip it
if exclude != nil && exclude.Match(path.Join(currRelPath, info.Name())) {
if pkgFileExcluder != nil && pkgFileExcluder.Match(path.Join(currRelPath, info.Name())) {
return false
}
// process file if it would be included in build context (handles things like build tags)
Expand All @@ -274,6 +321,12 @@ func PackagesInDir(rootDir string, exclude matcher.Matcher) (Packages, error) {
allPkgs[currPath] = pkgName
}

// if this directory matched but return value indicates that subdirectories should not be considered, return
// filepath.SkipDir
if skipDir {
return filepath.SkipDir
}

return nil
}); err != nil {
return nil, err
Expand All @@ -282,6 +335,63 @@ func PackagesInDir(rootDir string, exclude matcher.Matcher) (Packages, error) {
return createPkgsWithValidation(dirAbsolutePath, allPkgs)
}

func nonRootModuleExcluder(wd string) pkgExcluder {
wdPkgs, err := gopackages.Load(&gopackages.Config{
Mode: gopackages.NeedModule,
Dir: wd,
}, ".")
if err != nil || len(wdPkgs) == 0 || wdPkgs[0].Module == nil || wdPkgs[0].Module.Path == "" {
return nil
}

wdModulePath := wdPkgs[0].Module.Path
return pkgExcluderFn(func(relPath string) (exclude, excludeAllSubdirs bool) {
fullPath := filepath.Join(wd, relPath)
dirEntries, readDirErr := os.ReadDir(fullPath)
// path cannot be read: do not exclude
if readDirErr != nil {
return false, false
}
var goModFilePath string
for _, entry := range dirEntries {
// skip all entries that are not the "go.mod" file
if entry.IsDir() || entry.Name() != "go.mod" {
continue
}
goModFilePath = filepath.Join(fullPath, entry.Name())
break
}
// no "go.mod" file in directory: do not exclude
if goModFilePath == "" {
return false, false
}

// "go.mod" file exists in directory
modFileBytes, err := os.ReadFile(goModFilePath)

// "go.mod" file cannot be read: do not exclude
if err != nil {
return false, false
}
// if module path of directory does not match root module, exclude this directory and all subdirectories
differentModule := modfile.ModulePath(modFileBytes) != wdModulePath
return differentModule, differentModule
})
}

type pkgExcluder interface {
// Exclude returns true if the package at the specified path should be excluded when matching packages. The relpath
// parameter will always be a directory. If the second return value is true, then no subdirectories of the package
// directories will be considered.
Exclude(relPath string) (exclude, excludeAllSubdirs bool)
}

type pkgExcluderFn func(relPath string) (exclude, excludeAllSubdirs bool)

func (fn pkgExcluderFn) Exclude(relPath string) (exclude, excludeAllSubdirs bool) {
return fn(relPath)
}

func createPkgsWithValidation(rootDir string, pkgs map[string]string) (*packages, error) {
if !path.IsAbs(rootDir) {
return nil, fmt.Errorf("rootDir %s is not an absolute path", rootDir)
Expand Down
27 changes: 27 additions & 0 deletions pkgpath/vendor/golang.org/x/mod/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions pkgpath/vendor/golang.org/x/mod/PATENTS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions pkgpath/vendor/golang.org/x/mod/internal/lazyregexp/lazyre.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading