Skip to content

Commit 762e4dd

Browse files
committed
gopls/internal/golang: add web-based "split package" tool
This CL contains a prototype of an interactive tool to help you split a package into two or more independent components whose dependency graph is acyclic. The "source.splitPackage" code action opens a web browser on a page that displays all the declarations in the current package, grouped by file. You can create two or more components, then assign declarations to components by clicking checkboxes. As the assignment (or code) changes, the tool displays the component dependency graph, along with the symbol references that create that graph. If there is a cycle in the dependency graph, it is flagged. It doesn't actually move the declarations yet, but IMHO that's actually a lesser problem than the analysis. The component names and their assignments for each package are saved persistently in the filecache. + integration test (using a Go client in lieu of JS+DOM), docs, relnote Change-Id: I45d5e5d47950bcab988a51d314c5c41831562de3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/679815 Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Madeline Kalil <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent d788a6f commit 762e4dd

File tree

24 files changed

+1544
-105
lines changed

24 files changed

+1544
-105
lines changed

gopls/doc/assets/splitpkg-deps.png

563 KB
Loading

gopls/doc/assets/splitpkg.png

370 KB
Loading

gopls/doc/features/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ when making significant changes to existing features or when adding new ones.
5656
- [Package documentation](web.md#doc): browse documentation for current Go package
5757
- [Free symbols](web.md#freesymbols): show symbols used by a selected block of code
5858
- [Assembly](web.md#assembly): show listing of assembly code for selected function
59+
- [Split package](web.md#splitpkg): split a package into two or more components
5960
- Support for non-Go files:
6061
- [Template files](templates.md): files parsed by `text/template` and `html/template`
6162
- [go.mod and go.work files](modfiles.md): Go module and workspace manifests
62-
- [Go *.s assembly files](assembly.ms): Go assembly files
63+
- [Go *.s assembly files](assembly.md): Go assembly files
6364
- [Command-line interface](../command-line.md): CLI for debugging and scripting (unstable)
6465

6566
You can find this page from within your editor by executing the

gopls/doc/features/web.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,35 @@ Client support:
149149
- **VS Code**: Use the "Source Action... > Browse GOARCH assembly for f" menu.
150150
- **Emacs + eglot**: Use `M-x go-browse-assembly` in [go-mode](https://github.com/dominikh/go-mode.el).
151151
- **Vim + coc.nvim**: ??
152+
153+
154+
<a name='splitpkg'></a>
155+
## `source.splitPackage`: Split package into components
156+
157+
The web-based "Split package" tool can help you split a complex
158+
package into two or more components, ensuring that the dependencies
159+
among those components are acyclic.
160+
161+
Follow the instructions on the page to choose a set of named components,
162+
assign each declaration to the most appropriate component, and then
163+
visualize the dependencies between those components created by references
164+
from one symbol to another.
165+
166+
The figure below shows the tool operating on the `fmt` package, which
167+
could (in principle) be split into three subpackages, one for
168+
formatting (`Printf` and friends), one for scanning (`Scanf`), and one
169+
for their common dependencies.
170+
171+
<img title="Split package 'fmt'" src="../assets/splitpkg.png">
172+
173+
(Try playing with the tool on this package: it's an instructive
174+
exercise. The figure below shows the solution.)
175+
176+
<img title="Split package 'fmt'" src="../assets/splitpkg-deps.png">
177+
178+
The tool does not currently perform the code transformation (moving
179+
declarations to new packages, renaming symbols to export them as
180+
needed), but we hope to add that in a future release.
181+
182+
Client support:
183+
- **VS Code**: Use the "Source Action... > Split package P" menu.

gopls/doc/release/v0.20.0.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
# Navigation features
44

5+
# Web-based features
6+
7+
## "Split package" tool
8+
9+
The `source.splitPackage` code action opens a web-based tool that
10+
helps you split a package into two or more components whose
11+
dependencies are acyclic.
12+
13+
To use it, name a set of components, assign each declaration to a
14+
component, then visualize the dependencies among the components
15+
(including whether they form a cycle).
16+
Refresh the page each time you edit your code to see the latest
17+
information.
18+
19+
<img title="Split package 'fmt'" src="../assets/splitpkg.png">
20+
21+
The tool makes it easy to iterate over potential decompositions
22+
until you find one you are happy with. A future version of
23+
the tool will automate the code transformation, but for now
24+
you must do that step by hand.
25+
526
## $feature
627

728
<!-- golang/go#$issue -->

gopls/internal/cmd/integration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,8 @@ type C struct{}
10241024
res.checkExit(true)
10251025
got := res.stdout
10261026
want := `command "Browse documentation for package a" [source.doc]` +
1027+
"\n" +
1028+
`command "Split package \"a\"" [source.splitPackage]` +
10271029
"\n" +
10281030
`command "Show compiler optimization details for \"a\"" [source.toggleCompilerOptDetails]` +
10291031
"\n"

gopls/internal/golang/assembly.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,19 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.Response
5757
escape := html.EscapeString
5858

5959
// Emit the start of the report.
60-
title := fmt.Sprintf("%s assembly for %s",
60+
titleHTML := fmt.Sprintf("%s assembly for %s",
6161
escape(snapshot.View().GOARCH()),
6262
escape(symbol))
6363
io.WriteString(w, `<!DOCTYPE html>
6464
<html>
6565
<head>
6666
<meta charset="UTF-8">
67-
<title>`+escape(title)+`</title>
67+
<title>`+titleHTML+`</title>
6868
<link rel="stylesheet" href="/assets/common.css">
6969
<script src="/assets/common.js"></script>
7070
</head>
7171
<body>
72-
<h1>`+title+`</h1>
72+
<h1>`+titleHTML+`</h1>
7373
<p>
7474
<a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a>
7575
</p>

gopls/internal/golang/codeaction.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ var codeActionProducers = [...]codeActionProducer{
242242
{kind: settings.GoAssembly, fn: goAssembly, needPkg: true},
243243
{kind: settings.GoDoc, fn: goDoc, needPkg: true},
244244
{kind: settings.GoFreeSymbols, fn: goFreeSymbols},
245+
{kind: settings.GoSplitPackage, fn: goSplitPackage, needPkg: true},
245246
{kind: settings.GoTest, fn: goTest, needPkg: true},
246247
{kind: settings.GoToggleCompilerOptDetails, fn: toggleCompilerOptDetails},
247248
{kind: settings.RefactorExtractFunction, fn: refactorExtractFunction},
@@ -440,6 +441,23 @@ func goFreeSymbols(ctx context.Context, req *codeActionsRequest) error {
440441
return nil
441442
}
442443

444+
// goSplitPackage produces "Split package p" code actions.
445+
// See [server.commandHandler.SplitPackage] for command implementation.
446+
func goSplitPackage(ctx context.Context, req *codeActionsRequest) error {
447+
// TODO(adonovan): ideally we would key by the package path,
448+
// or the ID of the widest package for the current file,
449+
// so that we don't see different results when toggling
450+
// between p.go and p_test.go.
451+
//
452+
// TODO(adonovan): opt: req should always provide metadata so
453+
// that we don't have to request type checking (needPkg=true).
454+
meta := req.pkg.Metadata()
455+
title := fmt.Sprintf("Split package %q", meta.Name)
456+
cmd := command.NewSplitPackageCommand(title, req.snapshot.View().ID(), string(meta.ID))
457+
req.addCommandAction(cmd, false)
458+
return nil
459+
}
460+
443461
// goplsDocFeatures produces "Browse gopls feature documentation" code actions.
444462
// See [server.commandHandler.ClientOpenURL] for command implementation.
445463
func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package splitpkg
6+
7+
// SCC algorithm stolen from cmd/digraph.
8+
9+
type (
10+
graph = map[int]map[int]bool
11+
nodeList = []int
12+
nodeSet = map[int]bool
13+
)
14+
15+
// addNode ensures a node exists in the graph with an initialized edge set.
16+
func addNode(g graph, node int) map[int]bool {
17+
edges := g[node]
18+
if edges == nil {
19+
edges = make(map[int]bool)
20+
g[node] = edges
21+
}
22+
return edges
23+
}
24+
25+
// addEdges adds one or more edges from a 'from' node.
26+
func addEdges(g graph, from int, to ...int) {
27+
edges := addNode(g, from)
28+
for _, toNode := range to {
29+
addNode(g, toNode)
30+
edges[toNode] = true
31+
}
32+
}
33+
34+
// transpose creates the transpose (reverse) of the graph.
35+
func transpose(g graph) graph {
36+
rev := make(graph)
37+
for node, edges := range g {
38+
addNode(rev, node) // Ensure all nodes exist in the transposed graph
39+
for succ := range edges {
40+
addEdges(rev, succ, node)
41+
}
42+
}
43+
return rev
44+
}
45+
46+
// sccs returns the non-trivial strongly connected components of the graph.
47+
func sccs(g graph) []nodeSet {
48+
// Kosaraju's algorithm---Tarjan is overkill here.
49+
//
50+
// TODO(adonovan): factor with Tarjan's algorithms from
51+
// go/ssa/dom.go,
52+
// go/callgraph/vta/propagation.go,
53+
// ../../cache/typerefs/refs.go,
54+
// ../../cache/metadata/graph.go.
55+
56+
// Forward pass.
57+
S := make(nodeList, 0, len(g)) // postorder stack
58+
seen := make(nodeSet)
59+
var visit func(node int)
60+
visit = func(node int) {
61+
if !seen[node] {
62+
seen[node] = true
63+
for e := range g[node] {
64+
visit(e)
65+
}
66+
S = append(S, node)
67+
}
68+
}
69+
for node := range g {
70+
visit(node)
71+
}
72+
73+
// Reverse pass.
74+
rev := transpose(g)
75+
var scc nodeSet
76+
seen = make(nodeSet)
77+
var rvisit func(node int)
78+
rvisit = func(node int) {
79+
if !seen[node] {
80+
seen[node] = true
81+
scc[node] = true
82+
for e := range rev[node] {
83+
rvisit(e)
84+
}
85+
}
86+
}
87+
var sccs []nodeSet
88+
for len(S) > 0 {
89+
top := S[len(S)-1]
90+
S = S[:len(S)-1] // pop
91+
if !seen[top] {
92+
scc = make(nodeSet)
93+
rvisit(top)
94+
if len(scc) == 1 && !g[top][top] {
95+
continue
96+
}
97+
sccs = append(sccs, scc)
98+
}
99+
}
100+
return sccs
101+
}

0 commit comments

Comments
 (0)