Skip to content

Commit bb4fe7d

Browse files
committed
Go: phase 5 — route go.mod tidy to the Java resolver; remove Go modgraph + Http
Complete the port: the GoModTidy recipe no longer resolves dependencies in the Go peer. It now delegates the entire `go mod tidy` require-set computation to the pure-Java resolver on the host via a new domain RPC method, GoModResolveTidy. - core RewriteRpc: replace the generic, network-performing `Http` RPC method with a `registerLanguageMethods(JsonRpc)` extension hook (called before bind) and a protected getHttpSender(). The generic "host, fetch this URL for the peer" capability — the SSRF/coupling concern raised in review — is gone from the shared protocol. - GoRewriteRpc: register GoModResolveTidy, which builds a CacheSource+ProxySource from the request and runs Resolver + Tidy. All GOPROXY HTTP happens here, in the host, through the configured HttpSender; GOPROXY=off resolves cache-only. - Go side: resolveTidyViaJava sends {goMod, mainImports, modulePath, separateIndirect, goproxy, gomodcache} and applies the returned require set; computeTidySet calls it instead of the in-process resolver, falling back to the LST-only pass when no resolver is installed (offline). The parse-time marker now carries only the declared model; the resolved build list is computed on demand. - Delete the Go pkg/parser/modgraph package (its algorithm now lives in Java) and the parse-time resolveModuleGraph/moduleSource/fetchHTTP plumbing. Tested: GoModResolveTidyTest drives the exact handler entry point (resolveTidy) and matches `go mod tidy` for gin's pruning-completeness case, fetching over the proxy via HttpSender. Full rewrite-go Java suite, rewrite-core rpc tests, and the Go unit suite are green. Note: a full modw corpus sweep could not be run here — the moderne-cli core/serialization module does not compile against the workspace rewrite (pre-existing API skew, 128 errors in V3LstReader: LstMetadata, ChangesetFilter, EditPage, UsesMethod.getMethodPattern, …), so the dev fat jar cannot be rebuilt. This is unrelated to these changes (none touch those files) and predates them (the on-disk fat jar is from before this work).
1 parent f1aeb35 commit bb4fe7d

15 files changed

Lines changed: 311 additions & 2357 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpc.java

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,8 @@
3838

3939
import org.openrewrite.HttpSenderExecutionContextView;
4040
import org.openrewrite.ipc.http.HttpSender;
41-
import org.openrewrite.ipc.http.HttpUrlConnectionSender;
4241

43-
import java.io.ByteArrayOutputStream;
44-
import java.io.InputStream;
4542
import java.io.PrintStream;
46-
import java.util.Base64;
4743
import java.util.HashMap;
4844
import java.util.Map;
4945
import java.nio.file.Files;
@@ -243,52 +239,38 @@ protected Boolean handle(Void noParams) {
243239
}
244240
});
245241

246-
// Http: lets the RPC peer (e.g. the Go module-graph resolver fetching
247-
// dependency go.mod files from a GOPROXY) perform an HTTP GET through
248-
// the configured HttpSender, so proxy/auth/TLS are honored. Returns the
249-
// status code and base64-encoded body.
250-
jsonRpc.rpc("Http", new JsonRpcMethod<HttpRequest>() {
251-
@Override
252-
protected Object handle(HttpRequest request) {
253-
HttpSender sender = httpSender != null ? httpSender : new HttpUrlConnectionSender();
254-
Map<String, Object> out = new HashMap<>();
255-
try (HttpSender.Response response = sender.get(request.url).send()) {
256-
ByteArrayOutputStream bos = new ByteArrayOutputStream();
257-
try (InputStream in = response.getBody()) {
258-
byte[] buf = new byte[8192];
259-
for (int n; (n = in.read(buf)) != -1; ) {
260-
bos.write(buf, 0, n);
261-
}
262-
}
263-
out.put("status", response.getCode());
264-
out.put("body", Base64.getEncoder().encodeToString(bos.toByteArray()));
265-
} catch (Exception e) {
266-
out.put("status", 0);
267-
out.put("body", "");
268-
}
269-
return out;
270-
}
271-
});
242+
// Language-specific RPC methods (registered by subclasses) are bound
243+
// here, before jsonRpc.bind(). This is where, for example, the Go
244+
// support registers its module-graph resolver — keeping domain-specific,
245+
// network-performing methods out of the generic core protocol.
246+
registerLanguageMethods(jsonRpc);
272247

273248
jsonRpc.bind();
274249
}
275250

276251
/**
277-
* Configure the {@link HttpSender} used by the {@code Http} RPC method.
278-
* Typically set from a parse/recipe ExecutionContext so the peer's HTTP
252+
* Hook for a subclass to register additional, language-specific RPC methods
253+
* on the bidirectional channel before it is bound. The default registers
254+
* nothing. Implementations may delegate network calls to {@link
255+
* #getHttpSender()} so proxy/auth/TLS are honored. Invoked from the
256+
* constructor, so implementations must not rely on subclass instance state.
257+
*/
258+
protected void registerLanguageMethods(JsonRpc jsonRpc) {
259+
}
260+
261+
/**
262+
* Configure the {@link HttpSender} a language method may use to perform HTTP
263+
* on the peer's behalf. Typically set from a parse/recipe ExecutionContext so
279264
* fetches use the CLI-configured sender (proxy, auth, TLS).
280265
*/
281266
public RewriteRpc setHttpSender(@Nullable HttpSender httpSender) {
282267
this.httpSender = httpSender;
283268
return this;
284269
}
285270

286-
/**
287-
* Request body for the {@code Http} RPC method.
288-
*/
289-
static class HttpRequest {
290-
public String url;
291-
public @Nullable String method;
271+
/** The configured {@link HttpSender}, for use by language RPC methods. */
272+
protected @Nullable HttpSender getHttpSender() {
273+
return httpSender;
292274
}
293275

294276
public RewriteRpc livenessCheck(Supplier<? extends @Nullable RuntimeException> livenessCheck) {

rewrite-go/cmd/rpc/main.go

Lines changed: 31 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package main
2020

2121
import (
2222
"bufio"
23-
"encoding/base64"
2423
"encoding/csv"
2524
"encoding/json"
2625
"flag"
@@ -40,7 +39,6 @@ import (
4039
"github.com/grafana/pyroscope-go"
4140

4241
goparser "github.com/openrewrite/rewrite/rewrite-go/pkg/parser"
43-
"github.com/openrewrite/rewrite/rewrite-go/pkg/parser/modgraph"
4442
"github.com/openrewrite/rewrite/rewrite-go/pkg/preconditions"
4543
"github.com/openrewrite/rewrite/rewrite-go/pkg/printer"
4644
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
@@ -829,95 +827,53 @@ func mapMarkerPrinter(mp *string) printer.MarkerPrinter {
829827
}
830828
}
831829

832-
// fetchHTTP performs an HTTP GET by delegating to the Java side over
833-
// bidirectional RPC, which executes the request through the CLI-configured
834-
// OpenRewrite HttpSender (proxy, auth, TLS all honored). Returns the response
835-
// body and status code. Used by the module-graph resolver to fetch dependency
836-
// go.mod files from a GOPROXY when they are not in the local module cache.
837-
func (s *server) fetchHTTP(url string) ([]byte, int, error) {
830+
// resolveTidyViaJava computes the `go mod tidy` require set by delegating to the
831+
// pure-Java module resolver over bidirectional RPC. The Java side performs all
832+
// GOPROXY HTTP through the CLI-configured OpenRewrite HttpSender (proxy, auth,
833+
// TLS honored) and writes fetched modules through to the standard module cache,
834+
// so no network egress originates from this peer. We pass the GOPROXY/GOMODCACHE
835+
// resolved from the environment; GOPROXY=off degrades to cache-only on the host.
836+
func (s *server) resolveTidyViaJava(content string, mainImports []string, modulePath string, separateIndirect bool) (recipes.TidyRequireSet, bool) {
838837
s.httpMu.Lock()
839838
defer s.httpMu.Unlock()
840-
params, _ := json.Marshal(map[string]any{"url": url, "method": "GET"})
839+
params, _ := json.Marshal(map[string]any{
840+
"goMod": content,
841+
"mainImports": mainImports,
842+
"modulePath": modulePath,
843+
"separateIndirect": separateIndirect,
844+
"goproxy": envOr("GOPROXY", "https://proxy.golang.org,direct"),
845+
"gomodcache": envOr("GOMODCACHE", defaultModCache()),
846+
})
841847
req, _ := json.Marshal(map[string]any{
842848
"jsonrpc": "2.0",
843-
"id": "go-Http",
844-
"method": "Http",
849+
"id": "go-GoModResolveTidy",
850+
"method": "GoModResolveTidy",
845851
"params": json.RawMessage(params),
846852
})
847853
header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(req))
848854
if _, err := s.writer.Write(append([]byte(header), req...)); err != nil {
849-
return nil, 0, err
855+
return recipes.TidyRequireSet{}, false
850856
}
851857
resp, err := s.readMessage()
852858
if err != nil {
853-
return nil, 0, err
859+
return recipes.TidyRequireSet{}, false
854860
}
855861
raw := resp.Result
856862
if raw == nil {
857863
raw = resp.Params
858864
}
859865
if raw == nil {
860-
return nil, 0, fmt.Errorf("Http: empty response")
866+
return recipes.TidyRequireSet{}, false
861867
}
862868
var out struct {
863-
Status int `json:"status"`
864-
Body string `json:"body"` // base64
869+
Direct map[string]string `json:"direct"`
870+
Indirect map[string]string `json:"indirect"`
871+
Complete bool `json:"complete"`
865872
}
866873
if err := json.Unmarshal(raw, &out); err != nil {
867-
return nil, 0, err
868-
}
869-
body, err := base64.StdEncoding.DecodeString(out.Body)
870-
if err != nil {
871-
return nil, 0, err
872-
}
873-
return body, out.Status, nil
874-
}
875-
876-
// resolveModuleGraph enriches a go.mod's GoResolutionResult marker with the
877-
// resolved module graph + build list. Dependency go.mod files are read from the
878-
// local module cache first and fetched from the GOPROXY (via the Java
879-
// HttpSender) on a miss. Best-effort: on any failure the marker keeps whatever
880-
// was resolved and GraphComplete reflects partiality.
881-
//
882-
// Proxy fetching is on by default and disabled the Go-native way with
883-
// GOPROXY=off. Fetched modules are written through to the standard module
884-
// cache, so the first parse warms it and every later parse/recipe run — across
885-
// projects on the machine — resolves the full graph offline.
886-
func (s *server) resolveModuleGraph(goModContent []byte, mrr *golang.GoResolutionResult) {
887-
res, err := modgraph.Resolve(goModContent, s.moduleSource())
888-
if err != nil {
889-
s.logger.Printf("resolveModuleGraph: %v", err)
890-
return
874+
return recipes.TidyRequireSet{}, false
891875
}
892-
modgraph.ApplyTo(res, mrr)
893-
}
894-
895-
// moduleSource builds the dependency-resolution source: the local module cache
896-
// first, then (unless disabled) a write-through GOPROXY tier whose HTTP is
897-
// delegated to the Java HttpSender via the bidirectional Http method. Proxy
898-
// fetches persist .mod/.zip/.ziphash into the standard $GOMODCACHE/cache/download
899-
// layout, so the cache fills in on first use and is shared by every subsequent
900-
// lookup. The same source is used at parse time and installed into the recipe
901-
// ExecutionContext for recipe-time re-resolution, giving both the full graph.
902-
func (s *server) moduleSource() modgraph.ModSource {
903-
gomodcache := envOr("GOMODCACHE", defaultModCache())
904-
sources := []modgraph.ModSource{modgraph.CacheSource(gomodcache)}
905-
if proxyResolveEnabled() {
906-
goproxy := envOr("GOPROXY", "https://proxy.golang.org,direct")
907-
sources = append(sources, modgraph.ProxyWriteThroughSource(goproxy, gomodcache, s.fetchHTTP))
908-
}
909-
return modgraph.TieredSource(sources...)
910-
}
911-
912-
// proxyResolveEnabled reports whether the GOPROXY tier should be added. Network
913-
// module resolution is on by default — like rewrite's other ecosystems we
914-
// attempt the network (via the CLI HttpSender) and degrade gracefully to the
915-
// local cache and the existing require set when it is unavailable. It is
916-
// disabled the Go-native way, GOPROXY=off, the standard mechanism for
917-
// air-gapped builds. (A GOPROXY list such as "https://corp,off" still enables
918-
// the proxy; only the bare value "off" means no network.)
919-
func proxyResolveEnabled() bool {
920-
return strings.TrimSpace(os.Getenv("GOPROXY")) != "off"
876+
return recipes.TidyRequireSet{Direct: out.Direct, Indirect: out.Indirect, Complete: out.Complete}, true
921877
}
922878

923879
func envOr(key, def string) string {
@@ -1164,10 +1120,10 @@ func (s *server) resolveExecutionContext(pid *string) *recipe.ExecutionContext {
11641120
s.preparedContexts[*pid] = ctx
11651121
}
11661122
s.installDataTableStore(ctx)
1167-
// Make the dependency-resolution source available so recipes that mutate
1168-
// dependencies can re-resolve the module model at recipe time (network via
1169-
// the Java HttpSender).
1170-
recipes.SetModSource(ctx, s.moduleSource())
1123+
// Install the recipe-time tidy resolver, which delegates the require-set
1124+
// computation to the pure-Java module resolver on the host (all GOPROXY HTTP
1125+
// runs there through the CLI HttpSender).
1126+
recipes.SetTidyResolver(ctx, s.resolveTidyViaJava)
11711127
return ctx
11721128
}
11731129

@@ -2083,9 +2039,8 @@ func (s *server) handleParseProject(params json.RawMessage) (any, *rpcError) {
20832039
if sumData, err := os.ReadFile(sumPath); err == nil {
20842040
mrr.ResolvedDependencies = goparser.ParseGoSum(string(sumData))
20852041
}
2086-
// Enrich with the resolved module graph + build list (cache, then
2087-
// GOPROXY via the Java HttpSender) so recipes get the full graph.
2088-
s.resolveModuleGraph(data, mrr)
2042+
// The marker carries only the declared model at parse time; the resolved
2043+
// build list is computed on demand by the Java resolver at recipe time.
20892044
mods[filepath.Dir(modPath)] = &modCtx{dir: filepath.Dir(modPath), mrr: mrr}
20902045
}
20912046

rewrite-go/pkg/parser/modgraph/marker.go

Lines changed: 0 additions & 85 deletions
This file was deleted.

0 commit comments

Comments
 (0)