diff --git a/pkg/config/base_loader.go b/pkg/config/base_loader.go index 9ad332ac2398..0f5ae6fc43b8 100644 --- a/pkg/config/base_loader.go +++ b/pkg/config/base_loader.go @@ -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) @@ -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) diff --git a/pkg/lint/package.go b/pkg/lint/package.go index 3127a24b8e56..94678a46ea75 100644 --- a/pkg/lint/package.go +++ b/pkg/lint/package.go @@ -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)) @@ -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) @@ -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 { diff --git a/pkg/lint/package_test.go b/pkg/lint/package_test.go index 02778146bde5..99e29b464fcf 100644 --- a/pkg/lint/package_test.go +++ b/pkg/lint/package_test.go @@ -1,6 +1,8 @@ package lint import ( + "context" + "os" "path/filepath" "testing" @@ -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()