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
89 changes: 89 additions & 0 deletions pkg/commands/internal/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -115,6 +116,11 @@ func (b Builder) addToGoMod(ctx context.Context) error {
return err
}

err = b.mergeReplaceDirectives(ctx, plugin.Path)
if err != nil {
return err
}

continue
}

Expand Down Expand Up @@ -162,6 +168,89 @@ func (b Builder) addReplaceDirective(ctx context.Context, plugin *Plugin) error
return nil
}

type goModReplace struct {
Replace []struct {
Old struct{ Path, Version string }
New struct{ Path, Version string }
}
}

func readGoModReplace(ctx context.Context, dir string) (*goModReplace, error) {
cmd := exec.CommandContext(ctx, "go", "mod", "edit", "-json")
cmd.Dir = dir

output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
}

var goMod goModReplace
err = json.Unmarshal(output, &goMod)
if err != nil {
return nil, fmt.Errorf("unmarshal go.mod json: %w", err)
}

return &goMod, nil
}

func (b Builder) mergeReplaceDirectives(ctx context.Context, pluginPath string) error {
pluginGoMod, err := readGoModReplace(ctx, pluginPath)
if err != nil {
return err
}

rootGoMod, err := readGoModReplace(ctx, b.repo)
if err != nil {
return err
}

for _, r := range pluginGoMod.Replace {
for _, rr := range rootGoMod.Replace {
if r.Old.Path == rr.Old.Path && r.Old.Version == rr.Old.Version {
return fmt.Errorf("duplicate replace directive for %s@%s", r.Old.Path, r.Old.Version)
}
}
}

for _, r := range pluginGoMod.Replace {
abs, err := filepath.Abs(filepath.Join(pluginPath, r.New.Path))
if err != nil {
return fmt.Errorf("get absolute path: %w", err)
}

stat, err := os.Stat(abs)
if err != nil {
return fmt.Errorf("%s: %w", abs, err)
}
if stat.IsDir() {
r.New.Path = abs
}

replace := fmt.Sprintf("%s=%s", r.Old.Path, r.New.Path)
if r.Old.Version != "" {
replace += "@" + r.Old.Version
}
if r.New.Version != "" {
replace += "@" + r.New.Version
}

cmd := exec.CommandContext(ctx, "go", "mod", "edit", "-replace", replace)
cmd.Dir = b.repo

output, err := cmd.CombinedOutput()
if err != nil {
b.log.Warnf("%s", string(output))

return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
}
}

// NOTE(sublee): We don't need to follow nested replace directives because
// the directives are effective only at the main module. All necessary
// replacements should be handled at this level.
return nil
}

func (b Builder) goModTidy(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
cmd.Dir = b.repo
Expand Down
109 changes: 109 additions & 0 deletions pkg/commands/internal/builder_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package internal

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_sanitizeVersion(t *testing.T) {
Expand Down Expand Up @@ -54,3 +59,107 @@ func Test_sanitizeVersion(t *testing.T) {
})
}
}

func TestMergeReplaceDirectives(t *testing.T) {
t.Parallel()

// Create a temporary module with the following structure:
// tmp/
// go.mod
// golangci-lint/
tmp := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(tmp, "go.mod"), []byte(`
module github.com/golangci/golangci-lint/v2
go 1.24.0
`), 0o600))
require.NoError(t, os.Mkdir(filepath.Join(tmp, "golangci-lint"), 0o700))

b := NewBuilder(nil, &Configuration{Plugins: []*Plugin{
{Module: "example.com/plugin", Path: "testdata/plugin"},
}}, tmp)

// Merge replace directives from the plugin's go.mod into the temporary
// repo. Only the plugin's own replace rules are applied; transitive
// replaces from its dependencies are not automatically merged.
err := b.mergeReplaceDirectives(t.Context(), filepath.Join("testdata", "plugin"))
require.NoError(t, err)

cmd := exec.CommandContext(t.Context(), "go", "mod", "edit", "-json")
cmd.Dir = b.repo
output, err := cmd.CombinedOutput()
require.NoError(t, err)

var goMod struct {
Replace []struct{ New struct{ Path string } }
}
err = json.Unmarshal(output, &goMod)
require.NoError(t, err)

// The go.mod file should include a replace directive for
// example.com/target, pointing to the local path, because
// example.com/plugin's go.mod defines it. However, it should not include a
// replace directive for example.com/other, since example.com/plugin does
// not directly depend on it, and go mod ignores such transitive
// replacements.
require.Len(t, goMod.Replace, 1)
assert.Contains(t, goMod.Replace[0].New.Path, "testdata/plugin/target")
}

func TestMergeReplaceDirectives_DuplicateReplace(t *testing.T) {
t.Parallel()

tmp := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(tmp, "go.mod"), []byte(`
module github.com/golangci/golangci-lint/v2
go 1.24.0
`), 0o600))
require.NoError(t, os.Mkdir(filepath.Join(tmp, "golangci-lint"), 0o700))

// Both plugins define a replace directive for example.com/target.
b := NewBuilder(nil, &Configuration{Plugins: []*Plugin{
{Module: "example.com/plugin", Path: "testdata/plugin"},
{Module: "example.com/plugin2", Path: "testdata/plugin2"},
}}, tmp)

err := b.mergeReplaceDirectives(t.Context(), filepath.Join("testdata", "plugin"))
require.NoError(t, err)

err = b.mergeReplaceDirectives(t.Context(), filepath.Join("testdata", "plugin2"))
assert.ErrorContains(t, err, "duplicate replace directive for example.com/target")
}

func TestMergeReplaceDirectives_DuplicateButDifferentVersion(t *testing.T) {
t.Parallel()

tmp := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(tmp, "go.mod"), []byte(`
module github.com/golangci/golangci-lint/v2
go 1.24.0
`), 0o600))
require.NoError(t, os.Mkdir(filepath.Join(tmp, "golangci-lint"), 0o700))

// Both plugins define a replace directive for example.com/target.
b := NewBuilder(nil, &Configuration{Plugins: []*Plugin{
{Module: "example.com/plugin", Path: "testdata/plugin"},
{Module: "example.com/plugin3", Path: "testdata/plugin3"},
}}, tmp)

err := b.mergeReplaceDirectives(t.Context(), filepath.Join("testdata", "plugin"))
require.NoError(t, err)

err = b.mergeReplaceDirectives(t.Context(), filepath.Join("testdata", "plugin3"))
require.NoError(t, err)

cmd := exec.CommandContext(t.Context(), "go", "mod", "edit", "-json")
cmd.Dir = b.repo
output, err := cmd.CombinedOutput()
require.NoError(t, err)

var goMod struct {
Replace []struct{ New struct{ Path string } }
}
err = json.Unmarshal(output, &goMod)
require.NoError(t, err)

assert.Len(t, goMod.Replace, 2)
}
7 changes: 7 additions & 0 deletions pkg/commands/internal/testdata/plugin/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example.com/plugin

go 1.24.0

require example.com/target v0.0.0

replace example.com/target => ./target
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package plugin

import _ "example.com/target"
7 changes: 7 additions & 0 deletions pkg/commands/internal/testdata/plugin/target/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example.com/target

go 1.24.0

require example.com/other v0.0.0

replace example.com/other => ./other
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin/target/other/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/other

go 1.24.0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package other
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin/target/target.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package target

import _ "example.com/other"
7 changes: 7 additions & 0 deletions pkg/commands/internal/testdata/plugin2/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example.com/plugin2

go 1.24.0

require example.com/target v0.0.0

replace example.com/target => ./target
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin2/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package plugin

import _ "example.com/target"
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin2/target/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/target

go 1.24.0
1 change: 1 addition & 0 deletions pkg/commands/internal/testdata/plugin2/target/target.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package target
7 changes: 7 additions & 0 deletions pkg/commands/internal/testdata/plugin3/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example.com/plugin2

go 1.24.0

require example.com/target v1.0.0

replace example.com/[email protected] => ./target
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin3/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package plugin

import _ "example.com/target"
3 changes: 3 additions & 0 deletions pkg/commands/internal/testdata/plugin3/target/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/target

go 1.24.0
1 change: 1 addition & 0 deletions pkg/commands/internal/testdata/plugin3/target/target.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package target