Skip to content
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
98 changes: 98 additions & 0 deletions .github/workflows/cli-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
name: cli-smoke-test

on:
workflow_dispatch: {}
pull_request:
branches:
- main
schedule:
- cron: 0 16 * * *

concurrency:
group: cli-smoke-test-${{ github.ref }}
cancel-in-progress: true

jobs:
cli-smoke-test:
if: >
github.event_name != 'schedule' ||
github.repository_owner == 'moderneinc' ||
github.repository_owner == 'openrewrite'
runs-on: ubuntu-latest
steps:
# No actions/setup-go: the CLI's build helper shells out to the ambient
# `go`, and a second toolchain on PATH mismatches the image's GOROOT.
# GOTOOLCHAIN=auto lets the preinstalled Go fetch a managed go1.25.x to
# satisfy the `go 1.25.0` floor.
- name: Configure the Go toolchain
shell: bash
run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV"

- name: Checkout recipes-go
uses: actions/checkout@v7

# Pinned for reproducibility: at this revision the demo marks
# golang.org/x/net as `// indirect` while importing golang.org/x/net/html
# directly, so GoModTidy has a real correction to make.
- name: Checkout demo repository (pinned)
uses: actions/checkout@v7
with:
repository: MarinNoFuture/boyafushi
ref: 161a5770288e3c79244b40670cbe6dadf67212e1
path: demo

- name: Set up Java for the Moderne CLI
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '25'

- name: Install Moderne CLI (latest release)
shell: bash
run: |
CLI_VERSION=$(curl -s https://repo1.maven.org/maven2/io/moderne/moderne-cli/maven-metadata.xml | grep '<release>' | sed 's/.*<release>\(.*\)<\/release>.*/\1/')
curl -sSL -o mod.jar "https://repo1.maven.org/maven2/io/moderne/moderne-cli/${CLI_VERSION}/moderne-cli-${CLI_VERSION}.jar"
printf '#!/usr/bin/env bash\njava -jar "%s/mod.jar" "$@"\n' "$GITHUB_WORKSPACE" > mod
chmod +x mod
echo "$GITHUB_WORKSPACE" >> "$GITHUB_PATH"

# Go is not in the CLI's default build pipeline; enable it explicitly.
- name: Enable the Go build step in the CLI
shell: bash
run: |
mkdir -p "$HOME/.moderne/cli"
cat > "$HOME/.moderne/cli/moderne.yml" <<'EOF'
build:
steps:
- type: go
EOF

# Install the enclosing recipe module from this checkout so the smoke test
# runs exactly this commit's recipes.
- name: Build and install the recipe module (this checkout)
shell: bash
run: |
go build ./...
mod config recipes go install "$GITHUB_WORKSPACE"

- name: Build the demo LST
shell: bash
working-directory: demo
run: mod build . --no-download

- name: Run GoModTidy and assert a change
shell: bash
working-directory: demo
run: |
mod run . --recipe=org.openrewrite.golang.migration.GoModTidy
patch=$(find .moderne/run -name 'fix.patch' -size +0c | head -1)
if [ -z "$patch" ]; then
echo "GoModTidy produced no change"
exit 1
fi
if ! grep -q "golang.org/x/net" "$patch"; then
echo "Expected GoModTidy to correct the // indirect marker on golang.org/x/net"
exit 1
fi
echo "Smoke test passed: GoModTidy corrected go.mod on the pinned MarinNoFuture/boyafushi"
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/moderneinc/recipes-go

go 1.25.0

require github.com/openrewrite/rewrite/rewrite-go v0.0.23
require github.com/openrewrite/rewrite/rewrite-go v0.0.24

require (
github.com/google/uuid v1.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/openrewrite/rewrite/rewrite-go v0.0.23 h1:mXCSKT2jcsPr+5aW5QHpGbQ8VQE66/Mo3w2Al+sl6lM=
github.com/openrewrite/rewrite/rewrite-go v0.0.23/go.mod h1:YquZz5hJMNrl7quFsOhlgyc5+UTV05XyFBSLJITdtvw=
github.com/openrewrite/rewrite/rewrite-go v0.0.24 h1:FFMrK8XPe1ZIMnQhmPe42kMYXCySWdsY8CpEcoryIpA=
github.com/openrewrite/rewrite/rewrite-go v0.0.24/go.mod h1:YquZz5hJMNrl7quFsOhlgyc5+UTV05XyFBSLJITdtvw=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
7 changes: 7 additions & 0 deletions recipes/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,11 @@ func Activate(r *recipe.Registry) {
r.Register(&migration.UpgradeGoTo124{}, golang, codeQuality, migrationCategory)
r.Register(&migration.UpgradeGoTo125{}, golang, codeQuality, migrationCategory)
r.Register(&migration.UpgradeGoTo126{}, golang, codeQuality, migrationCategory)
r.Register(&migration.GoModTidy{}, golang, codeQuality, migrationCategory)
r.Register(&migration.AddMissingGoModRequires{}, golang, codeQuality, migrationCategory)
r.Register(&migration.RemoveUnusedGoModRequires{}, golang, codeQuality, migrationCategory)
r.Register(&migration.FixGoModIndirectMarkers{}, golang, codeQuality, migrationCategory)
r.Register(&migration.FormatGoMod{}, golang, codeQuality, migrationCategory)
r.Register(&migration.FindMissingGoModRequires{}, golang, codeQuality, migrationCategory)
r.Register(&migration.FindUnusedGoModRequires{}, golang, codeQuality, migrationCategory)
}
142 changes: 142 additions & 0 deletions recipes/migration/add_missing_gomod_requires.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

package migration

import (
"sort"

"github.com/openrewrite/rewrite/rewrite-go/pkg/preconditions"
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/golang"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/java"
"github.com/openrewrite/rewrite/rewrite-go/pkg/visitor"
)

// AddMissingGoModRequires adds `require` directives for modules that the
// resolved build list needs but go.mod does not yet declare — the requirements
// `go mod tidy` would add. Each module is added at its resolved version with
// the `// indirect` marker the toolchain assigned it.
//
// It reads the resolved build list from the go.mod's GoResolutionResult marker,
// which is populated at parse time by the rewrite-go toolchain resolver. When
// resolution did not run (marker has no resolved dependencies) it is a no-op.
type AddMissingGoModRequires struct {
recipe.Base
}

func (r *AddMissingGoModRequires) Name() string {
return "org.openrewrite.golang.migration.AddMissingGoModRequires"
}

func (r *AddMissingGoModRequires) DisplayName() string {
return "Add missing go.mod requirements"
}

func (r *AddMissingGoModRequires) Description() string {
return "Add `require` directives for modules the resolved build list needs but go.mod does not declare, at their resolved versions and with the `// indirect` marker the toolchain assigned. Mirrors what `go mod tidy` adds, using the module graph resolved at parse time."
}

func (r *AddMissingGoModRequires) Tags() []string { return []string{"gomod", "tidy"} }

func (r *AddMissingGoModRequires) Editor() recipe.TreeVisitor {
return preconditions.Check(
preconditions.HasSourcePath("**/go.mod"),
visitor.Init(&addMissingRequiresVisitor{}),
)
}

type addMissingRequiresVisitor struct {
visitor.GoVisitor
}

type missingRequire struct {
modulePath string
version string
indirect bool
}

func (v *addMissingRequiresVisitor) VisitGoMod(gm *golang.GoMod, p any) java.Tree {
mrr := java.FindMarker[golang.GoResolutionResult](gm.Markers)
if mrr == nil {
return gm
}

missing := missingRequires(gm, mrr)
if len(missing) == 0 {
return gm
}
return insertRequires(gm, missing)
}

// missingRequires returns, sorted by module path, the build-list modules that
// no `require` directive covers.
func missingRequires(gm *golang.GoMod, mrr *golang.GoResolutionResult) []missingRequire {
required := requiredModuleSet(gm)
seen := map[string]bool{}
var missing []missingRequire
for _, rd := range mrr.ResolvedDependencies {
if rd.Main || rd.ModulePath == "" || rd.ModulePath == mrr.ModulePath {
continue
}
if required[rd.ModulePath] || seen[rd.ModulePath] {
continue
}
seen[rd.ModulePath] = true
missing = append(missing, missingRequire{rd.ModulePath, rd.Version, rd.Indirect})
}
sort.Slice(missing, func(i, j int) bool { return missing[i].modulePath < missing[j].modulePath })
return missing
}

// insertRequires appends the missing requirements to the first `require` block,
// or creates a new block when none exists.
func insertRequires(gm *golang.GoMod, missing []missingRequire) *golang.GoMod {
for i, rp := range gm.Statements {
if b, ok := rp.Element.(*golang.GoModBlock); ok && b.Keyword == "require" {
rp.Element = appendToRequireBlock(b, missing)
statements := append([]java.RightPadded[golang.GoModStatement]{}, gm.Statements...)
statements[i] = rp
return gm.WithStatements(statements)
}
}

block := newRequireBlock(missing)
entry := java.RightPadded[golang.GoModStatement]{Element: block, After: java.Space{Whitespace: "\n"}, Markers: freshMarkers()}
return gm.WithStatements(append(append([]java.RightPadded[golang.GoModStatement]{}, gm.Statements...), entry))
}

func appendToRequireBlock(b *golang.GoModBlock, missing []missingRequire) *golang.GoModBlock {
indent := "\t"
if len(b.Entries) > 0 {
if d, ok := b.Entries[0].Element.(*golang.GoModDirective); ok {
indent = d.Prefix.Indent()
}
}
entries := append([]java.RightPadded[golang.GoModStatement]{}, b.Entries...)
for _, m := range missing {
entries = append(entries, newRequireEntry(indent, m.modulePath, m.version, m.indirect))
}
return b.WithEntries(entries)
}

func newRequireBlock(missing []missingRequire) *golang.GoModBlock {
entries := make([]java.RightPadded[golang.GoModStatement], len(missing))
for i, m := range missing {
prefix := "\t"
if i == 0 {
prefix = "\n\t"
}
entries[i] = newRequireEntry(prefix, m.modulePath, m.version, m.indirect)
}
return &golang.GoModBlock{
Ident: newIdent(),
Prefix: java.Space{Whitespace: "\n"},
Markers: freshMarkers(),
Keyword: "require",
BeforeLParen: java.SingleSpace,
Entries: entries,
BeforeRParen: java.EmptySpace,
}
}
92 changes: 92 additions & 0 deletions recipes/migration/find_missing_gomod_requires.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

package migration

import (
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/golang"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/java"
"github.com/openrewrite/rewrite/rewrite-go/pkg/visitor"
)

// FindMissingGoModRequires flags imports of third-party packages that no
// `require` (or `replace`) directive in the module's go.mod covers. These are
// exactly the requirements `go mod tidy` would add; the recipe cannot add them
// itself because resolving the version requires network access.
type FindMissingGoModRequires struct {
recipe.Base
}

func (r *FindMissingGoModRequires) Name() string {
return "org.openrewrite.golang.migration.FindMissingGoModRequires"
}

func (r *FindMissingGoModRequires) DisplayName() string {
return "Find missing go.mod requirements"
}

func (r *FindMissingGoModRequires) Description() string {
return "Find imports of third-party packages that are not covered by any `require` directive in the module's go.mod. " +
"These are the requirements `go mod tidy` would add; adding them automatically is not possible offline because it requires resolving module versions over the network."
}

func (r *FindMissingGoModRequires) Tags() []string { return []string{"gomod", "tidy", "search"} }

func (r *FindMissingGoModRequires) Editor() recipe.TreeVisitor {
return visitor.Init(&findMissingGoModRequiresVisitor{})
}

type findMissingGoModRequiresVisitor struct {
visitor.GoVisitor
}

func (v *findMissingGoModRequiresVisitor) VisitCompilationUnit(cu *golang.CompilationUnit, p any) java.J {
cu = v.GoVisitor.VisitCompilationUnit(cu, p).(*golang.CompilationUnit)

mrr := java.FindMarker[golang.GoResolutionResult](cu.Markers)
if mrr == nil || cu.Imports == nil {
return cu
}

elements := cu.Imports.Elements
newElements := make([]java.RightPadded[*java.Import], len(elements))
changed := false
for i, rp := range elements {
if isMissingRequire(importPathOf(rp.Element), mrr) {
rp.Element = rp.Element.WithMarkers(
java.FoundSearchResult(rp.Element.Markers, "missing go.mod requirement"),
)
changed = true
}
newElements[i] = rp
}
if !changed {
return cu
}
imports := *cu.Imports
imports.Elements = newElements
cu = cu.WithImports(&imports)
return cu
}

func isMissingRequire(importPath string, mrr *golang.GoResolutionResult) bool {
if importPath == "" || isStdlibImport(importPath) {
return false
}
if mrr.ModulePath != "" && moduleProvides(mrr.ModulePath, importPath) {
return false
}
for i := range mrr.Requires {
if moduleProvides(mrr.Requires[i].ModulePath, importPath) {
return false
}
}
for i := range mrr.Replaces {
if moduleProvides(mrr.Replaces[i].OldPath, importPath) {
return false
}
}
return true
}
Loading
Loading