| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package splitpkg | |
| // This file produces the "Split package" HTML report. | |
| // | |
| // The server persistently holds, for each PackageID, the current set | |
| // of components and the mapping from declared names to components. On | |
| // each page reload or JS reload() call, the server type-checks the | |
| // package, computes its symbol reference graph, projects it onto | |
| // components, then returns the component reference graph, and if it | |
| // is cyclic, which edges form cycles. Thus changes to the package | |
| // source are reflected in the client UI at the next page reload or JS | |
| // reload() event. | |
| // | |
| // See also: | |
| // - ../codeaction.go - offers the CodeAction command | |
| // - ../../server/command.go - handles the command by opening a web page | |
| // - ../../server/server.go - handles the HTTP request and calls this function | |
| // - ../../server/assets/splitpkg.js - client-side logic | |
| // - ../../test/integration/web/splitpkg_test.go - integration test of server | |
| // | |
| // TODO(adonovan): future work | |
| // | |
| // Refine symbol reference graph: | |
| // - deal with enums (values must stay together; implicit dependency on iota expression) | |
| // - deal with coupled vars "var x, y = f()" | |
| // - deal with declared methods (coupled to receiver named type) | |
| // - deal with fields/interface methods (loosely coupled to struct/interface type) | |
| // In both cases the field/method name must be either exported or in the same component. | |
| // | |
| // UI: | |
| // - make shift click extend selection of a range of checkboxes. | |
| // - display two-level grouping of decls and specs: var ( x int; y int ) | |
| // - indicate when package has type errors (data may be incomplete). | |
| // | |
| // Code transformation: | |
| // - add "Split" button that is green when acyclic. It should: | |
| // 1) move each component into a new package, or separate file of | |
| // the same package. (The UI will need to hold this user | |
| // intent in the list of components.) | |
| // 2) ensure that each declaration referenced from another package | |
| // is public, renaming as needed. | |
| // 3) update package decls, imports, package docs, file docs, | |
| // doc comments, etc. | |
| // Should we call this feature "Reorganize package" or "Decompose package" | |
| // until the "Split" button actually exists? | |
| import ( | |
| "bytes" | |
| "crypto/sha256" | |
| _ "embed" | |
| "encoding/json" | |
| "fmt" | |
| "go/ast" | |
| "go/token" | |
| "go/types" | |
| "html/template" | |
| "log" | |
| "strconv" | |
| "strings" | |
| "golang.org/x/tools/gopls/internal/cache" | |
| "golang.org/x/tools/gopls/internal/cache/metadata" | |
| "golang.org/x/tools/gopls/internal/filecache" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| "golang.org/x/tools/gopls/internal/util/safetoken" | |
| "golang.org/x/tools/internal/moremaps" | |
| "golang.org/x/tools/internal/typesinternal" | |
| ) | |
| //go:embed splitpkg.html.tmpl | |
| var htmlTmpl string | |
| // HTML returns the HTML for the main "Split package" page, for the | |
| // /splitpkg endpoint. The real magic happens in JavaScript; see | |
| // ../../server/assets/splitpkg.js. | |
| func HTML(pkgpath metadata.PackagePath) []byte { | |
| t, err := template.New("splitpkg.html").Parse(htmlTmpl) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| data := struct { | |
| Title string | |
| }{ | |
| Title: fmt.Sprintf("Split package %s", pkgpath), | |
| } | |
| var buf bytes.Buffer | |
| if err := t.Execute(&buf, data); err != nil { | |
| log.Fatal(err) | |
| } | |
| return buf.Bytes() | |
| } | |
| const cacheKind = "splitpkg" // filecache kind | |
| func cacheKey(pkgID metadata.PackageID) [32]byte { | |
| return sha256.Sum256([]byte(pkgID)) | |
| } | |
| // UpdateComponentsJSON parses the JSON description of components and | |
| // their assigned declarations and updates the component state for the | |
| // specified package. | |
| func UpdateComponentsJSON(pkgID metadata.PackageID, data []byte) error { | |
| return filecache.Set(cacheKind, cacheKey(pkgID), data) | |
| } | |
| // Web is an abstraction of gopls' web server. | |
| type Web interface { | |
| // SrcURL forms URLs that cause the editor to open a file at a specific position. | |
| SrcURL(filename string, line, col8 int) protocol.URI | |
| } | |
| // JSON returns the JSON encoding of the data needed by | |
| // the /splitpkg-json endpoint for the specified package. It includes: | |
| // - the set of names declared by the package, grouped by file; | |
| // - the set of components and their assigned declarations from | |
| // the most recent call to [UpdateComponentsJSON]; and | |
| // - the component graph derived from them, along with the | |
| // sets of reference that give rise to each edge. | |
| func JSON(pkg *cache.Package, web Web) ([]byte, error) { | |
| // Retrieve package's most recent state from the file cache. | |
| var comp ComponentsJSON | |
| data, err := filecache.Get(cacheKind, cacheKey(pkg.Metadata().ID), filecache.Bytes) | |
| if err != nil { | |
| if err != filecache.ErrNotFound { | |
| return nil, err | |
| } | |
| // cache miss: use zero value | |
| } else if err := json.Unmarshal(data, &comp); err != nil { | |
| return nil, err | |
| } | |
| // Prepare to construct symbol reference graph. | |
| var ( | |
| info = pkg.TypesInfo() | |
| symbols = make(map[types.Object]*symbol) | |
| ) | |
| // setName records the UI name for an object. | |
| // (The UI name disambiguates "init", "_", etc.) | |
| setName := func(obj types.Object, name string) { | |
| symbols[obj] = &symbol{ | |
| name: name, | |
| component: comp.Assignments[name], // missing => "default" | |
| } | |
| } | |
| // Pass 1: name everything, since naming is order-dependent. | |
| var initCounter, blankCounter int | |
| for _, pgf := range pkg.CompiledGoFiles() { | |
| for _, decl := range pgf.File.Decls { | |
| switch decl := decl.(type) { | |
| case *ast.FuncDecl: | |
| if fn, ok := info.Defs[decl.Name].(*types.Func); ok { | |
| // For now we treat methods as first class decls, | |
| // but since they are coupled to the named type | |
| // they should be omitted in the UI for brevity. | |
| name := fn.Name() | |
| if fn.Signature().Recv() != nil { | |
| fn = fn.Origin() | |
| _, named := typesinternal.RecvBase(fn) | |
| name = named.Obj().Name() + "." + name | |
| } else if name == "init" { | |
| // Disambiguate top-level init functions. | |
| name += suffix(&initCounter) | |
| } | |
| if name == "_" { // (function or method) | |
| name += suffix(&blankCounter) | |
| } | |
| setName(fn, name) | |
| } | |
| case *ast.GenDecl: | |
| switch decl.Tok { | |
| case token.CONST, token.VAR: | |
| for _, spec := range decl.Specs { | |
| spec := spec.(*ast.ValueSpec) | |
| for _, id := range spec.Names { | |
| if obj := info.Defs[id]; obj != nil { | |
| name := obj.Name() | |
| if name == "_" { | |
| name += suffix(&blankCounter) | |
| } | |
| setName(obj, name) | |
| } | |
| } | |
| } | |
| case token.TYPE: | |
| for _, spec := range decl.Specs { | |
| spec := spec.(*ast.TypeSpec) | |
| if obj := info.Defs[spec.Name]; obj != nil { | |
| name := obj.Name() | |
| if name == "_" { | |
| name += suffix(&blankCounter) | |
| } | |
| setName(obj, name) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Pass 2: compute symbol reference graph, project onto | |
| // component dependency graph, and build JSON response. | |
| var ( | |
| files []*fileJSON | |
| refs []*refJSON | |
| ) | |
| for _, pgf := range pkg.CompiledGoFiles() { | |
| identURL := func(id *ast.Ident) string { | |
| posn := safetoken.Position(pgf.Tok, id.Pos()) | |
| return web.SrcURL(posn.Filename, posn.Line, posn.Column) | |
| } | |
| newCollector := func(from *symbol) *refCollector { | |
| return &refCollector{ | |
| from: from, | |
| identURL: identURL, | |
| pkg: pkg.Types(), | |
| info: info, | |
| symbols: symbols, | |
| } | |
| } | |
| var decls []*declJSON | |
| for _, decl := range pgf.File.Decls { | |
| var ( | |
| kind string | |
| specs []*specJSON | |
| ) | |
| switch decl := decl.(type) { | |
| case *ast.FuncDecl: | |
| kind = "func" | |
| if fn, ok := info.Defs[decl.Name].(*types.Func); ok { | |
| symbol := symbols[fn] | |
| rc := newCollector(symbol).collect(decl) | |
| refs = append(refs, rc.refs...) | |
| specs = append(specs, &specJSON{ | |
| Name: symbol.name, | |
| URL: identURL(decl.Name), | |
| }) | |
| } | |
| case *ast.GenDecl: | |
| kind = decl.Tok.String() | |
| switch decl.Tok { | |
| case token.CONST, token.VAR: | |
| for _, spec := range decl.Specs { | |
| spec := spec.(*ast.ValueSpec) | |
| for i, id := range spec.Names { | |
| if obj := info.Defs[id]; obj != nil { | |
| symbol := symbols[obj] | |
| rc := newCollector(symbol) | |
| // If there's a type, | |
| // all RHSs depend on it. | |
| if spec.Type != nil { | |
| rc.collect(spec.Type) | |
| } | |
| switch len(spec.Values) { | |
| case len(spec.Names): | |
| // var x, y = a, b | |
| rc.collect(spec.Values[i]) | |
| case 1: | |
| // var x, y = f() | |
| rc.collect(spec.Values[0]) | |
| case 0: | |
| // var x T | |
| } | |
| refs = append(refs, rc.refs...) | |
| specs = append(specs, &specJSON{ | |
| Name: symbol.name, | |
| URL: identURL(id), | |
| }) | |
| } | |
| } | |
| } | |
| case token.TYPE: | |
| for _, spec := range decl.Specs { | |
| spec := spec.(*ast.TypeSpec) | |
| if obj := info.Defs[spec.Name]; obj != nil { | |
| symbol := symbols[obj] | |
| rc := newCollector(symbol).collect(spec.Type) | |
| refs = append(refs, rc.refs...) | |
| specs = append(specs, &specJSON{ | |
| Name: symbol.name, | |
| URL: identURL(spec.Name), | |
| }) | |
| } | |
| } | |
| } | |
| } | |
| if len(specs) > 0 { | |
| decls = append(decls, &declJSON{Kind: kind, Specs: specs}) | |
| } | |
| } | |
| files = append(files, &fileJSON{ | |
| Base: pgf.URI.Base(), | |
| URL: web.SrcURL(pgf.URI.Path(), 1, 1), | |
| Decls: decls, | |
| }) | |
| } | |
| // Compute the graph of dependencies between components, by | |
| // projecting the symbol dependency graph through component | |
| // assignments. | |
| var ( | |
| g = make(graph) | |
| edgeRefs = make(map[[2]int][]*refJSON) // refs that induce each intercomponent edge | |
| ) | |
| for _, ref := range refs { | |
| from, to := ref.from, ref.to | |
| if from.component != to.component { | |
| // inter-component reference | |
| m, ok := g[from.component] | |
| if !ok { | |
| m = make(map[int]bool) | |
| g[from.component] = m | |
| } | |
| m[to.component] = true | |
| key := [2]int{from.component, to.component} | |
| edgeRefs[key] = append(edgeRefs[key], ref) | |
| } | |
| } | |
| // Detect cycles in the component graph | |
| // and record cyclic (⚠) components. | |
| cycles := [][]int{} // non-nil for JSON | |
| scmap := make(map[int]int) // maps component index to 1 + SCC index (0 => acyclic) | |
| for i, scc := range sccs(g) { | |
| for c := range scc { | |
| scmap[c] = i + 1 | |
| } | |
| cycles = append(cycles, moremaps.KeySlice(scc)) | |
| } | |
| // Record intercomponent edges and their references. | |
| edges := []*edgeJSON{} // non-nil for JSON | |
| for edge, refs := range edgeRefs { | |
| from, to := edge[0], edge[1] | |
| edges = append(edges, &edgeJSON{ | |
| From: from, | |
| To: to, | |
| Refs: refs, | |
| Cyclic: scmap[from] > 0 && scmap[from] == scmap[to], | |
| }) | |
| } | |
| return json.Marshal(ResultJSON{ | |
| Files: files, | |
| Components: comp, | |
| Edges: edges, | |
| Cycles: cycles, | |
| }) | |
| } | |
| // A refCollector gathers intra-package references to top-level | |
| // symbols from within one syntax tree, in lexical order. | |
| type refCollector struct { | |
| from *symbol | |
| identURL func(*ast.Ident) string | |
| pkg *types.Package | |
| info *types.Info | |
| index map[types.Object]*refJSON | |
| symbols map[types.Object]*symbol | |
| refs []*refJSON // output | |
| } | |
| // A symbol describes a declared name and its assigned component. | |
| type symbol struct { | |
| name string // unique name in the UI and JSON/HTTP protocol | |
| component int // index of assigned component | |
| } | |
| // collect adds the free references of n to the collection. | |
| func (rc *refCollector) collect(n ast.Node) *refCollector { | |
| var f func(n ast.Node) bool | |
| f = func(n ast.Node) bool { | |
| switch n := n.(type) { | |
| case *ast.SelectorExpr: | |
| if sel, ok := rc.info.Selections[n]; ok { | |
| rc.addRef(n.Sel, sel.Obj()) | |
| ast.Inspect(n.X, f) | |
| return false // don't visit n.Sel | |
| } | |
| case *ast.Ident: | |
| if obj := rc.info.Uses[n]; obj != nil { | |
| rc.addRef(n, obj) | |
| } | |
| } | |
| return true | |
| } | |
| ast.Inspect(n, f) | |
| return rc | |
| } | |
| // addRef records a reference from id to obj. | |
| func (rc *refCollector) addRef(id *ast.Ident, obj types.Object) { | |
| if obj.Pkg() != rc.pkg { | |
| return // cross-package reference | |
| } | |
| // Un-instantiate methods. | |
| if fn, ok := obj.(*types.Func); ok && fn.Signature().Recv() != nil { | |
| obj = fn.Origin() // G[int].method -> G[T].method | |
| } | |
| // We only care about refs to package-level symbols. | |
| // And methods, for now. | |
| decl := rc.symbols[obj] | |
| if decl == nil { | |
| return // not a package-level symbol or top-level method | |
| } | |
| if ref, ok := rc.index[obj]; !ok { | |
| ref = &refJSON{ | |
| From: rc.from.name, | |
| To: decl.name, | |
| URL: rc.identURL(id), | |
| from: rc.from, | |
| to: decl, | |
| } | |
| if rc.index == nil { | |
| rc.index = make(map[types.Object]*refJSON) | |
| } | |
| rc.index[obj] = ref | |
| rc.refs = append(rc.refs, ref) | |
| } | |
| } | |
| // suffix returns a subscripted decimal suffix, | |
| // preincrementing the specified counter. | |
| func suffix(counter *int) string { | |
| *counter++ | |
| n := *counter | |
| return subscripter.Replace(strconv.Itoa(n)) | |
| } | |
| var subscripter = strings.NewReplacer( | |
| "0", "₀", | |
| "1", "₁", | |
| "2", "₂", | |
| "3", "₃", | |
| "4", "₄", | |
| "5", "₅", | |
| "6", "₆", | |
| "7", "₇", | |
| "8", "₈", | |
| "9", "₉", | |
| ) | |
| // -- JSON types -- | |
| // ResultJSON describes the result of a /splitpkg-json query. | |
| // It is public for testing. | |
| type ResultJSON struct { | |
| Components ComponentsJSON // component names and their assigned declarations | |
| Files []*fileJSON // files of the packages and their declarations and references | |
| Edges []*edgeJSON // inter-component edges and their references | |
| Cycles [][]int // sets of strongly-connected components | |
| } | |
| // request body of a /splitpkg-components update; | |
| // also part of /splitpkg-json response. | |
| type ComponentsJSON struct { | |
| Names []string `json:",omitempty"` // if empty, implied Names[0]=="default". | |
| Assignments map[string]int `json:",omitempty"` // maps specJSON.Name to component index; missing => 0 | |
| } | |
| // edgeJSON describes an inter-component dependency. | |
| type edgeJSON struct { | |
| From, To int // component IDs | |
| Refs []*refJSON // references that give rise to this edge | |
| Cyclic bool // edge is part of nontrivial strongly connected component | |
| } | |
| // fileJSON records groups decl/spec information about a single file. | |
| type fileJSON struct { | |
| Base string // file base name | |
| URL string // showDocument link for file | |
| Decls []*declJSON `json:",omitempty"` | |
| } | |
| // declJSON groups specs (e.g. "var ( x int; y int )"). | |
| type declJSON struct { | |
| Kind string // const, var, type, func | |
| Specs []*specJSON `json:",omitempty"` | |
| } | |
| // specJSON describes a single declared name. | |
| // (A coupled declaration "var x, y = f()" results in two specJSONs.) | |
| type specJSON struct { | |
| Name string // x or T.x | |
| URL string // showDocument link for declaring identifier | |
| } | |
| // refJSON records the first reference from a given declaration to a symbol. | |
| // (Repeat uses of the same identifier are omitted.) | |
| type refJSON struct { | |
| From, To string // x or T.x of referenced spec | |
| URL string // showDocument link for referring identifier | |
| from, to *symbol // transient | |
| } |