Skip to content

Commit 737320c

Browse files
committed
support running against golangci-lint against other directories
1 parent 374a8cf commit 737320c

File tree

3 files changed

+236
-9
lines changed

3 files changed

+236
-9
lines changed

pkg/config/base_loader.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ func (l *BaseLoader) getConfigSearchPaths() []string {
130130
}
131131

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

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

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

147+
// Add current working directory if it's not already included and we haven't found a config yet
148+
cwd, err := os.Getwd()
149+
if err == nil {
150+
absCwd, err := filepath.Abs(cwd)
151+
if err == nil && !slices.Contains(searchPaths, absCwd) {
152+
searchPaths = append(searchPaths, "./")
153+
}
154+
}
155+
146156
// find home directory for global config
147157
if home, err := homedir.Dir(); err != nil {
148158
l.log.Warnf("Can't get user's home directory: %v", err)

pkg/lint/package.go

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,118 @@ func NewPackageLoader(log logutils.Log, cfg *config.Config, args []string, env *
5353

5454
// Load loads packages.
5555
func (l *PackageLoader) Load(ctx context.Context, linters []*linter.Config) (pkgs, deduplicatedPkgs []*packages.Package, err error) {
56+
// Check for multiple modules and provide helpful error
57+
if err := l.detectMultipleModules(ctx); err != nil {
58+
return nil, nil, err
59+
}
60+
5661
loadMode := findLoadMode(linters)
5762

58-
pkgs, err = l.loadPackages(ctx, loadMode)
59-
if err != nil {
60-
return nil, nil, fmt.Errorf("failed to load packages: %w", err)
63+
pkgs, loadErr := l.loadPackages(ctx, loadMode)
64+
if loadErr != nil {
65+
return nil, nil, fmt.Errorf("failed to load packages: %w", loadErr)
6166
}
6267

6368
return pkgs, l.filterDuplicatePackages(pkgs), nil
6469
}
6570

71+
// detectMultipleModules checks if multiple arguments refer to different modules
72+
func (l *PackageLoader) detectMultipleModules(ctx context.Context) error {
73+
if len(l.args) <= 1 {
74+
return nil
75+
}
76+
77+
var moduleRoots []string
78+
seenRoots := make(map[string]bool)
79+
80+
for _, arg := range l.args {
81+
moduleRoot := l.findModuleRootForArg(ctx, arg)
82+
if moduleRoot != "" && !seenRoots[moduleRoot] {
83+
moduleRoots = append(moduleRoots, moduleRoot)
84+
seenRoots[moduleRoot] = true
85+
}
86+
}
87+
88+
if len(moduleRoots) > 1 {
89+
return fmt.Errorf("multiple Go modules detected: %v\n\n"+
90+
"Multi-module analysis is not supported. Each module should be analyzed separately:\n"+
91+
" golangci-lint run %s\n golangci-lint run %s",
92+
moduleRoots, moduleRoots[0], moduleRoots[1])
93+
}
94+
95+
return nil
96+
}
97+
98+
// findModuleRootForArg finds the module root for a given argument using go env
99+
func (l *PackageLoader) findModuleRootForArg(ctx context.Context, arg string) string {
100+
absPath, err := filepath.Abs(arg)
101+
if err != nil {
102+
if l.debugf != nil {
103+
l.debugf("Failed to get absolute path for %s: %v", arg, err)
104+
}
105+
return ""
106+
}
107+
108+
// Determine the directory to check
109+
var targetDir string
110+
if info, statErr := os.Stat(absPath); statErr == nil && info.IsDir() {
111+
targetDir = absPath
112+
} else if statErr == nil {
113+
targetDir = filepath.Dir(absPath)
114+
} else {
115+
return ""
116+
}
117+
118+
// Save current directory
119+
originalWd, err := os.Getwd()
120+
if err != nil {
121+
if l.debugf != nil {
122+
l.debugf("Failed to get current directory: %v", err)
123+
}
124+
return ""
125+
}
126+
defer func() {
127+
if chErr := os.Chdir(originalWd); chErr != nil && l.debugf != nil {
128+
l.debugf("Failed to restore directory %s: %v", originalWd, chErr)
129+
}
130+
}()
131+
132+
// Change to target directory and use go env GOMOD
133+
if chdirErr := os.Chdir(targetDir); chdirErr != nil {
134+
if l.debugf != nil {
135+
l.debugf("Failed to change to directory %s: %v", targetDir, chdirErr)
136+
}
137+
return ""
138+
}
139+
140+
goModPath, err := goenv.GetOne(ctx, goenv.GOMOD)
141+
if err != nil || goModPath == "" {
142+
if l.debugf != nil {
143+
l.debugf("go env GOMOD failed in %s: err=%v, path=%s", targetDir, err, goModPath)
144+
}
145+
return ""
146+
}
147+
148+
return filepath.Dir(goModPath)
149+
}
150+
151+
// determineWorkingDir determines the working directory for package loading.
152+
// If the first argument is within a directory tree that has a go.mod file, use that module root.
153+
// Otherwise, use the current working directory.
154+
func (l *PackageLoader) determineWorkingDir(ctx context.Context) string {
155+
if len(l.args) == 0 {
156+
return ""
157+
}
158+
159+
moduleRoot := l.findModuleRootForArg(ctx, l.args[0])
160+
if moduleRoot != "" {
161+
if l.debugf != nil {
162+
l.debugf("Found module root %s, using as working dir", moduleRoot)
163+
}
164+
}
165+
return moduleRoot
166+
}
167+
66168
func (l *PackageLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
67169
defer func(startedAt time.Time) {
68170
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
76178
Context: ctx,
77179
BuildFlags: l.makeBuildFlags(),
78180
Logf: l.debugf,
181+
Dir: l.determineWorkingDir(ctx),
79182
// TODO: use fset, parsefile, overlay
80183
}
81184

82-
args := buildArgs(l.args)
185+
args := l.buildArgs(ctx)
83186

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

@@ -233,13 +336,23 @@ func (l *PackageLoader) makeBuildFlags() []string {
233336
return buildFlags
234337
}
235338

236-
func buildArgs(args []string) []string {
237-
if len(args) == 0 {
339+
// buildArgs processes the arguments for package loading, handling directory changes appropriately.
340+
func (l *PackageLoader) buildArgs(ctx context.Context) []string {
341+
if len(l.args) == 0 {
342+
return []string{"./..."}
343+
}
344+
345+
workingDir := l.determineWorkingDir(ctx)
346+
347+
// If we're using a different working directory, we need to adjust the arguments
348+
if workingDir != "" {
349+
// We're switching to the target directory as working dir, so use "./..." to analyze it
238350
return []string{"./..."}
239351
}
240352

353+
// Use the original buildArgs logic for the current working directory
241354
var retArgs []string
242-
for _, arg := range args {
355+
for _, arg := range l.args {
243356
if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
244357
retArgs = append(retArgs, arg)
245358
} else {

pkg/lint/package_test.go

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package lint
22

33
import (
4+
"context"
5+
"os"
46
"path/filepath"
57
"testing"
68

@@ -40,13 +42,115 @@ func Test_buildArgs(t *testing.T) {
4042
t.Run(test.desc, func(t *testing.T) {
4143
t.Parallel()
4244

43-
results := buildArgs(test.args)
45+
// Create a PackageLoader with the test args
46+
loader := &PackageLoader{args: test.args}
47+
results := loader.buildArgs(context.Background())
4448

4549
assert.Equal(t, test.expected, results)
4650
})
4751
}
4852
}
4953

54+
func Test_buildArgs_withGoMod(t *testing.T) {
55+
// Create a temporary directory with go.mod
56+
tmpDir := t.TempDir()
57+
goModPath := filepath.Join(tmpDir, "go.mod")
58+
err := os.WriteFile(goModPath, []byte("module testmod\n"), 0600)
59+
require.NoError(t, err)
60+
61+
loader := &PackageLoader{args: []string{tmpDir}}
62+
results := loader.buildArgs(context.Background())
63+
64+
// When targeting a directory with go.mod, should return "./..."
65+
assert.Equal(t, []string{"./..."}, results)
66+
}
67+
68+
func Test_detectMultipleModules(t *testing.T) {
69+
// Create temporary directories with go.mod files
70+
tmpDir1 := t.TempDir()
71+
tmpDir2 := t.TempDir()
72+
tmpDir3 := t.TempDir()
73+
74+
// Create go.mod files in each directory with unique module names
75+
goModPath1 := filepath.Join(tmpDir1, "go.mod")
76+
err := os.WriteFile(goModPath1, []byte("module testmod1\n\ngo 1.21\n"), 0600)
77+
require.NoError(t, err)
78+
79+
goModPath2 := filepath.Join(tmpDir2, "go.mod")
80+
err = os.WriteFile(goModPath2, []byte("module testmod2\n\ngo 1.21\n"), 0600)
81+
require.NoError(t, err)
82+
83+
goModPath3 := filepath.Join(tmpDir3, "go.mod")
84+
err = os.WriteFile(goModPath3, []byte("module testmod3\n\ngo 1.21\n"), 0600)
85+
require.NoError(t, err)
86+
87+
// Create subdirectories within tmpDir1 and tmpDir2
88+
subDir1 := filepath.Join(tmpDir1, "subdir")
89+
subDir2 := filepath.Join(tmpDir2, "subdir")
90+
err = os.MkdirAll(subDir1, 0755)
91+
require.NoError(t, err)
92+
err = os.MkdirAll(subDir2, 0755)
93+
require.NoError(t, err)
94+
95+
// Create another subdirectory within tmpDir1 for same module test
96+
anotherSubDir := filepath.Join(tmpDir1, "another")
97+
err = os.MkdirAll(anotherSubDir, 0755)
98+
require.NoError(t, err)
99+
100+
testCases := []struct {
101+
desc string
102+
args []string
103+
shouldError bool
104+
}{
105+
{
106+
desc: "single directory",
107+
args: []string{tmpDir1},
108+
shouldError: false,
109+
},
110+
{
111+
desc: "multiple directories with go.mod",
112+
args: []string{tmpDir1, tmpDir2},
113+
shouldError: true,
114+
},
115+
{
116+
desc: "three directories with go.mod",
117+
args: []string{tmpDir1, tmpDir2, tmpDir3},
118+
shouldError: true,
119+
},
120+
{
121+
desc: "subdirectories of different modules",
122+
args: []string{subDir1, subDir2},
123+
shouldError: true,
124+
},
125+
{
126+
desc: "subdirectories of same module",
127+
args: []string{subDir1, anotherSubDir},
128+
shouldError: false,
129+
},
130+
{
131+
desc: "no arguments",
132+
args: []string{},
133+
shouldError: false,
134+
},
135+
}
136+
137+
for _, test := range testCases {
138+
t.Run(test.desc, func(t *testing.T) {
139+
t.Parallel()
140+
141+
loader := &PackageLoader{args: test.args}
142+
err := loader.detectMultipleModules(context.Background())
143+
144+
if test.shouldError {
145+
require.Error(t, err)
146+
assert.Contains(t, err.Error(), "multiple Go modules detected")
147+
} else {
148+
assert.NoError(t, err)
149+
}
150+
})
151+
}
152+
}
153+
50154
func mustAbs(t *testing.T, p string) string {
51155
t.Helper()
52156

0 commit comments

Comments
 (0)