Skip to content
Closed
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
12 changes: 11 additions & 1 deletion pkg/config/base_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ func (l *BaseLoader) getConfigSearchPaths() []string {
}

// find all dirs from it up to the root
searchPaths := []string{"./"}
searchPaths := []string{}

// Add the target directory and its parents first (highest priority)
for {
searchPaths = append(searchPaths, currentDir)

Expand All @@ -143,6 +144,15 @@ func (l *BaseLoader) getConfigSearchPaths() []string {
currentDir = parent
}

// Add current working directory if it's not already included and we haven't found a config yet
cwd, err := os.Getwd()
if err == nil {
absCwd, err := filepath.Abs(cwd)
if err == nil && !slices.Contains(searchPaths, absCwd) {
searchPaths = append(searchPaths, "./")
}
}

// find home directory for global config
if home, err := homedir.Dir(); err != nil {
l.log.Warnf("Can't get user's home directory: %v", err)
Expand Down
127 changes: 120 additions & 7 deletions pkg/lint/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,118 @@ func NewPackageLoader(log logutils.Log, cfg *config.Config, args []string, env *

// Load loads packages.
func (l *PackageLoader) Load(ctx context.Context, linters []*linter.Config) (pkgs, deduplicatedPkgs []*packages.Package, err error) {
// Check for multiple modules and provide helpful error
if err := l.detectMultipleModules(ctx); err != nil {
return nil, nil, err
}

loadMode := findLoadMode(linters)

pkgs, err = l.loadPackages(ctx, loadMode)
if err != nil {
return nil, nil, fmt.Errorf("failed to load packages: %w", err)
pkgs, loadErr := l.loadPackages(ctx, loadMode)
if loadErr != nil {
return nil, nil, fmt.Errorf("failed to load packages: %w", loadErr)
}

return pkgs, l.filterDuplicatePackages(pkgs), nil
}

// detectMultipleModules checks if multiple arguments refer to different modules
func (l *PackageLoader) detectMultipleModules(ctx context.Context) error {
if len(l.args) <= 1 {
return nil
}

var moduleRoots []string
seenRoots := make(map[string]bool)

for _, arg := range l.args {
moduleRoot := l.findModuleRootForArg(ctx, arg)
if moduleRoot != "" && !seenRoots[moduleRoot] {
moduleRoots = append(moduleRoots, moduleRoot)
seenRoots[moduleRoot] = true
}
}

if len(moduleRoots) > 1 {
return fmt.Errorf("multiple Go modules detected: %v\n\n"+
"Multi-module analysis is not supported. Each module should be analyzed separately:\n"+
" golangci-lint run %s\n golangci-lint run %s",
moduleRoots, moduleRoots[0], moduleRoots[1])
}

return nil
}

// findModuleRootForArg finds the module root for a given argument using go env
func (l *PackageLoader) findModuleRootForArg(ctx context.Context, arg string) string {
absPath, err := filepath.Abs(arg)
if err != nil {
if l.debugf != nil {
l.debugf("Failed to get absolute path for %s: %v", arg, err)
}
return ""
}

// Determine the directory to check
var targetDir string
if info, statErr := os.Stat(absPath); statErr == nil && info.IsDir() {
targetDir = absPath
} else if statErr == nil {
targetDir = filepath.Dir(absPath)
} else {
return ""
}

// Save current directory
originalWd, err := os.Getwd()
if err != nil {
if l.debugf != nil {
l.debugf("Failed to get current directory: %v", err)
}
return ""
}
defer func() {
if chErr := os.Chdir(originalWd); chErr != nil && l.debugf != nil {
l.debugf("Failed to restore directory %s: %v", originalWd, chErr)
}
}()

// Change to target directory and use go env GOMOD
if chdirErr := os.Chdir(targetDir); chdirErr != nil {
if l.debugf != nil {
l.debugf("Failed to change to directory %s: %v", targetDir, chdirErr)
}
return ""
}

goModPath, err := goenv.GetOne(ctx, goenv.GOMOD)
if err != nil || goModPath == "" {
if l.debugf != nil {
l.debugf("go env GOMOD failed in %s: err=%v, path=%s", targetDir, err, goModPath)
}
return ""
}

return filepath.Dir(goModPath)
}

// determineWorkingDir determines the working directory for package loading.
// If the first argument is within a directory tree that has a go.mod file, use that module root.
// Otherwise, use the current working directory.
func (l *PackageLoader) determineWorkingDir(ctx context.Context) string {
if len(l.args) == 0 {
return ""
}

moduleRoot := l.findModuleRootForArg(ctx, l.args[0])
if moduleRoot != "" {
if l.debugf != nil {
l.debugf("Found module root %s, using as working dir", moduleRoot)
}
}
return moduleRoot
}

func (l *PackageLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
defer func(startedAt time.Time) {
l.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt))
Expand All @@ -76,10 +178,11 @@ func (l *PackageLoader) loadPackages(ctx context.Context, loadMode packages.Load
Context: ctx,
BuildFlags: l.makeBuildFlags(),
Logf: l.debugf,
Dir: l.determineWorkingDir(ctx),
// TODO: use fset, parsefile, overlay
}

args := buildArgs(l.args)
args := l.buildArgs(ctx)

l.debugf("Built loader args are %s", args)

Expand Down Expand Up @@ -233,13 +336,23 @@ func (l *PackageLoader) makeBuildFlags() []string {
return buildFlags
}

func buildArgs(args []string) []string {
if len(args) == 0 {
// buildArgs processes the arguments for package loading, handling directory changes appropriately.
func (l *PackageLoader) buildArgs(ctx context.Context) []string {
if len(l.args) == 0 {
return []string{"./..."}
}

workingDir := l.determineWorkingDir(ctx)

// If we're using a different working directory, we need to adjust the arguments
if workingDir != "" {
// We're switching to the target directory as working dir, so use "./..." to analyze it
return []string{"./..."}
}

// Use the original buildArgs logic for the current working directory
var retArgs []string
for _, arg := range args {
for _, arg := range l.args {
if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
retArgs = append(retArgs, arg)
} else {
Expand Down
106 changes: 105 additions & 1 deletion pkg/lint/package_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package lint

import (
"context"
"os"
"path/filepath"
"testing"

Expand Down Expand Up @@ -40,13 +42,115 @@ func Test_buildArgs(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

results := buildArgs(test.args)
// Create a PackageLoader with the test args
loader := &PackageLoader{args: test.args}
results := loader.buildArgs(context.Background())

assert.Equal(t, test.expected, results)
})
}
}

func Test_buildArgs_withGoMod(t *testing.T) {
// Create a temporary directory with go.mod
tmpDir := t.TempDir()
goModPath := filepath.Join(tmpDir, "go.mod")
err := os.WriteFile(goModPath, []byte("module testmod\n"), 0600)
require.NoError(t, err)

loader := &PackageLoader{args: []string{tmpDir}}
results := loader.buildArgs(context.Background())

// When targeting a directory with go.mod, should return "./..."
assert.Equal(t, []string{"./..."}, results)
}

func Test_detectMultipleModules(t *testing.T) {
// Create temporary directories with go.mod files
tmpDir1 := t.TempDir()
tmpDir2 := t.TempDir()
tmpDir3 := t.TempDir()

// Create go.mod files in each directory with unique module names
goModPath1 := filepath.Join(tmpDir1, "go.mod")
err := os.WriteFile(goModPath1, []byte("module testmod1\n\ngo 1.21\n"), 0600)
require.NoError(t, err)

goModPath2 := filepath.Join(tmpDir2, "go.mod")
err = os.WriteFile(goModPath2, []byte("module testmod2\n\ngo 1.21\n"), 0600)
require.NoError(t, err)

goModPath3 := filepath.Join(tmpDir3, "go.mod")
err = os.WriteFile(goModPath3, []byte("module testmod3\n\ngo 1.21\n"), 0600)
require.NoError(t, err)

// Create subdirectories within tmpDir1 and tmpDir2
subDir1 := filepath.Join(tmpDir1, "subdir")
subDir2 := filepath.Join(tmpDir2, "subdir")
err = os.MkdirAll(subDir1, 0755)
require.NoError(t, err)
err = os.MkdirAll(subDir2, 0755)
require.NoError(t, err)

// Create another subdirectory within tmpDir1 for same module test
anotherSubDir := filepath.Join(tmpDir1, "another")
err = os.MkdirAll(anotherSubDir, 0755)
require.NoError(t, err)

testCases := []struct {
desc string
args []string
shouldError bool
}{
{
desc: "single directory",
args: []string{tmpDir1},
shouldError: false,
},
{
desc: "multiple directories with go.mod",
args: []string{tmpDir1, tmpDir2},
shouldError: true,
},
{
desc: "three directories with go.mod",
args: []string{tmpDir1, tmpDir2, tmpDir3},
shouldError: true,
},
{
desc: "subdirectories of different modules",
args: []string{subDir1, subDir2},
shouldError: true,
},
{
desc: "subdirectories of same module",
args: []string{subDir1, anotherSubDir},
shouldError: false,
},
{
desc: "no arguments",
args: []string{},
shouldError: false,
},
}

for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

loader := &PackageLoader{args: test.args}
err := loader.detectMultipleModules(context.Background())

if test.shouldError {
require.Error(t, err)
assert.Contains(t, err.Error(), "multiple Go modules detected")
} else {
assert.NoError(t, err)
}
})
}
}

func mustAbs(t *testing.T, p string) string {
t.Helper()

Expand Down
Loading