gopls/internal/golang: add "Show assembly of f" code action This CL adds a new code action to display the Go assembly code produced for the enclosing function, and all its nested functions. Each instruction is marked up with a link that causes cursor navigation in the editor, using the magic of LSP's showDocument. Each page load causes a recompile. (A cold build may take a few seconds in a large project.) The architecture is determined by the view: for most files it will be the default GOARCH, but for arch-tagged files it will be the appropriate one. + Test + Release note Fixes golang/go#67478 Change-Id: I51285215e9b27c510076c64eeb7b7ae3899f8a59 Reviewed-on: https://go-review.googlesource.com/c/tools/+/588395 Reviewed-by: Robert Findley <rfindley@google.com> Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 69c3af8..357e349 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md
@@ -140,6 +140,20 @@ } ``` +## `gopls.assembly`: **Show disassembly of current function.** + +This command opens a web-based disassembly listing of the +specified function symbol (plus any nested lambdas and defers). +The machine architecture is determined by the view. + +Args: + +``` +string, +string, +string +``` + ## `gopls.change_signature`: **Perform a "change signature" refactoring** This command is experimental, currently only supporting parameter removal.
diff --git a/gopls/doc/release/v0.16.0.md b/gopls/doc/release/v0.16.0.md index dc004c4..ab54ae2 100644 --- a/gopls/doc/release/v0.16.0.md +++ b/gopls/doc/release/v0.16.0.md
@@ -88,6 +88,31 @@ ``` TODO(dominikh/go-mode.el#436): add both of these to go-mode.el. +### Show assembly + +Gopls offers a third web-based code action, "Show assembly for f", +which displays an assembly listing of the function declaration +enclosing the selected code, plus any nested functions (function +literals, deferred calls). +It invokes the compiler to generate the report. +Reloading the page will update the report. + +The machine architecture is determined by the "view" or build +configuration that gopls selects for the current file. +This is usually the same as your machine's GOARCH unless you are +working in a file with `go:build` tags for a different architecture. + +- TODO screenshot + +Gopls cannot yet display assembly for generic functions: generic +functions are not fully compiled until they are instantiated, but any +function declaration enclosing the selection cannot be an instantiated +generic function. +<!-- Clearly the ideal UX for generic functions is to use the function + symbol under the cursor, e.g. Vector[string], rather than the + enclosing function; but computing the name of the linker symbol + remains a challenge. --> + ### `unusedwrite` analyzer The new
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index e170385..555c56a 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -961,6 +961,13 @@ "ResultDoc": "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"CreateFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"uri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t\t\"DeleteFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"uri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}" }, { + "Command": "gopls.assembly", + "Title": "Show disassembly of current function.", + "Doc": "This command opens a web-based disassembly listing of the\nspecified function symbol (plus any nested lambdas and defers).\nThe machine architecture is determined by the view.", + "ArgDoc": "string,\nstring,\nstring", + "ResultDoc": "" + }, + { "Command": "gopls.change_signature", "Title": "Perform a \"change signature\" refactoring", "Doc": "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).",
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go new file mode 100644 index 0000000..cbb9512 --- /dev/null +++ b/gopls/internal/golang/assembly.go
@@ -0,0 +1,166 @@ +// Copyright 2024 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 golang + +// This file produces the "Show GOARCH assembly of f" HTML report. +// +// See also: +// - ./codeaction.go - computes the symbol and 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. + +import ( + "bytes" + "context" + "fmt" + "html" + "path/filepath" + "regexp" + "strconv" + "strings" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/internal/gocommand" +) + +// AssemblyHTML returns an HTML document containing an assembly listing of the selected function. +// +// TODO(adonovan): +// - display a "Compiling..." message as a cold build can be slow. +// - cross-link jumps and block labels, like github.com/aclements/objbrowse. +func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, posURL PosURLFunc) ([]byte, error) { + // Compile the package with -S, and capture its stderr stream. + inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{ + Verb: "build", + Args: []string{"-gcflags=-S", "."}, + WorkingDir: filepath.Dir(pkg.Metadata().CompiledGoFiles[0].Path()), + }) + if err != nil { + return nil, err // e.g. failed to write overlays (rare) + } + defer cleanupInvocation() + _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv) + if err != nil { + return nil, err // e.g. won't compile + } + content := stderr.String() + + escape := html.EscapeString + + // Produce the report. + // TODO(adonovan): factor with RenderPkgDoc, FreeSymbolsHTML + title := fmt.Sprintf("%s assembly for %s", + escape(snapshot.View().GOARCH()), + escape(symbol)) + var buf bytes.Buffer + buf.WriteString(`<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <style>` + pkgDocStyle + `</style> + <title>` + escape(title) + `</title> + <script type='text/javascript'> +// httpGET requests a URL for its effects only. +function httpGET(url) { + var xhttp = new XMLHttpRequest(); + xhttp.open("GET", url, true); + xhttp.send(); + return false; // disable usual <a href=...> behavior +} + +// Start a GET /hang request. If it ever completes, the server +// has disconnected. Show a banner in that case. +{ + var x = new XMLHttpRequest(); + x.open("GET", "/hang", true); + x.onloadend = () => { + document.getElementById("disconnected").style.display = 'block'; + }; + x.send(); +}; + </script> +</head> +<body> +<div id='disconnected'>Gopls server has terminated. Page is inactive.</div> +<h1>` + title + `</h1> +<p> + <a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a> +</p> +<p> + Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a> +</p> +<p> + Click on a source line marker <code>L1234</code> to navigate your editor there. + (Beware: <a href='https://github.com/microsoft/vscode/issues/207634'>#207634</a>) +</p> +<p> + Reload the page to recompile. +</p> +<pre> +`) + + // sourceLink returns HTML for a link to open a file in the client editor. + // TODO(adonovan): factor with two other copies. + sourceLink := func(text, url string) string { + // The /open URL returns nothing but has the side effect + // of causing the LSP client to open the requested file. + // So we use onclick to prevent the browser from navigating. + // We keep the href attribute as it causes the <a> to render + // as a link: blue, underlined, with URL hover information. + return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`, + escape(url), text) + } + + // insnRx matches an assembly instruction line. + // Submatch groups are: (offset-hex-dec, file-line-column, instruction). + insnRx := regexp.MustCompile(`^(\s+0x[0-9a-f ]+)\(([^)]*)\)\s+(.*)$`) + + // Parse the functions of interest out of the listing. + // Each function is of the form: + // + // symbol STEXT k=v... + // 0x0000 00000 (/file.go:123) NOP... + // ... + // + // Allow matches of symbol, symbol.func1, symbol.deferwrap, etc. + on := false + for _, line := range strings.Split(content, "\n") { + // start of function symbol? + if strings.Contains(line, " STEXT ") { + on = strings.HasPrefix(line, symbol) && + (line[len(symbol)] == ' ' || line[len(symbol)] == '.') + } + if !on { + continue // within uninteresting symbol + } + + // In lines of the form + // "\t0x0000 00000 (/file.go:123) NOP..." + // replace the "(/file.go:123)" portion with an "L0123" source link. + // Skip filenames of the form "<foo>". + if parts := insnRx.FindStringSubmatch(line); parts != nil { + link := " " // if unknown + if file, linenum, ok := cutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") { + if linenum, err := strconv.Atoi(linenum); err == nil { + text := fmt.Sprintf("L%04d", linenum) + link = sourceLink(text, posURL(file, linenum, 1)) + } + } + fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3])) + } else { + buf.WriteString(escape(line)) + } + buf.WriteByte('\n') + } + return buf.Bytes(), nil +} + +// cutLast is the "last" analogue of [strings.Cut]. +func cutLast(s, sep string) (before, after string, ok bool) { + if i := strings.LastIndex(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +}
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 2208822..07f777e 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go
@@ -9,8 +9,10 @@ "encoding/json" "fmt" "go/ast" + "go/types" "strings" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/analysis/fillstruct" "golang.org/x/tools/gopls/internal/analysis/fillswitch" "golang.org/x/tools/gopls/internal/cache" @@ -24,6 +26,7 @@ "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/typesinternal" ) // CodeActions returns all wanted code actions (edits and other @@ -118,7 +121,7 @@ if err != nil { return nil, err } - // For implementation, see commandHandler.showFreeSymbols. + // For implementation, see commandHandler.FreeSymbols. actions = append(actions, protocol.CodeAction{ Title: cmd.Title, Kind: protocol.GoFreeSymbols, @@ -130,6 +133,7 @@ // Code actions requiring type information. if want[protocol.RefactorRewrite] || want[protocol.RefactorInline] || + want[protocol.GoAssembly] || want[protocol.GoTest] { pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) if err != nil { @@ -160,6 +164,14 @@ } actions = append(actions, fixes...) } + + if want[protocol.GoAssembly] { + fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng) + if err != nil { + return nil, err + } + actions = append(actions, fixes...) + } } return actions, nil } @@ -521,3 +533,81 @@ Command: &cmd, }}, nil } + +// getGoAssemblyAction returns any "Show assembly for f" code actions for the selection. +func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { + start, end, err := pgf.RangePos(rng) + if err != nil { + return nil, err + } + + // Find the enclosing toplevel function or method, + // and compute its symbol name (e.g. "pkgpath.(T).method"). + // The report will show this method and all its nested + // functions (FuncLit, defers, etc). + // + // TODO(adonovan): this is no good for generics, since they + // will always be uninstantiated when they enclose the cursor. + // Instead, we need to query the func symbol under the cursor, + // rather than the enclosing function. It may be an explicitly + // or implicitly instantiated generic, and it may be defined + // in another package, though we would still need to compile + // the current package to see its assembly. The challenge, + // however, is that computing the linker name for a generic + // symbol is quite tricky. Talk with the compiler team for + // ideas. + // + // TODO(adonovan): think about a smoother UX for jumping + // directly to (say) a lambda of interest. + // Perhaps we could scroll to STEXT for the innermost + // enclosing nested function? + var actions []protocol.CodeAction + path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + if len(path) >= 2 { // [... FuncDecl File] + if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok { + if fn, ok := pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { + sig := fn.Type().(*types.Signature) + + // Compute the linker symbol of the enclosing function. + var sym strings.Builder + if fn.Pkg().Name() == "main" { + sym.WriteString("main") + } else { + sym.WriteString(fn.Pkg().Path()) + } + sym.WriteString(".") + if sig.Recv() != nil { + if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil { + sym.WriteString("(") + if isPtr { + sym.WriteString("*") + } + sym.WriteString(named.Obj().Name()) + sym.WriteString(").") + } + } + sym.WriteString(fn.Name()) + + if fn.Name() != "_" && // blank functions are not compiled + (fn.Name() != "init" || sig.Recv() != nil) && // init functions aren't linker functions + sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly + cmd, err := command.NewAssemblyCommand( + fmt.Sprintf("Show %s assembly for %s", view.GOARCH(), decl.Name), + view.ID(), + string(pkg.Metadata().ID), + sym.String()) + if err != nil { + return nil, err + } + // For handler, see commandHandler.Assembly. + actions = append(actions, protocol.CodeAction{ + Title: cmd.Title, + Kind: protocol.GoAssembly, + Command: &cmd, + }) + } + } + } + } + return actions, nil +}
diff --git a/gopls/internal/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go index f3d953f..d493b45 100644 --- a/gopls/internal/protocol/codeactionkind.go +++ b/gopls/internal/protocol/codeactionkind.go
@@ -74,9 +74,10 @@ // instead of == for CodeActionKinds throughout gopls. // See golang/go#40438 for related discussion. const ( - GoTest CodeActionKind = "goTest" + GoAssembly CodeActionKind = "source.assembly" GoDoc CodeActionKind = "source.doc" GoFreeSymbols CodeActionKind = "source.freesymbols" + GoTest CodeActionKind = "goTest" // TODO(adonovan): rename "source.test" ) // CodeActionUnknownTrigger indicates that the trigger for a
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 5e267af..2ba73db 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go
@@ -28,6 +28,7 @@ AddImport Command = "gopls.add_import" AddTelemetryCounters Command = "gopls.add_telemetry_counters" ApplyFix Command = "gopls.apply_fix" + Assembly Command = "gopls.assembly" ChangeSignature Command = "gopls.change_signature" CheckUpgrades Command = "gopls.check_upgrades" DiagnoseFiles Command = "gopls.diagnose_files" @@ -66,6 +67,7 @@ AddImport, AddTelemetryCounters, ApplyFix, + Assembly, ChangeSignature, CheckUpgrades, DiagnoseFiles, @@ -125,6 +127,14 @@ return nil, err } return s.ApplyFix(ctx, a0) + case Assembly: + var a0 string + var a1 string + var a2 string + if err := UnmarshalArgs(params.Arguments, &a0, &a1, &a2); err != nil { + return nil, err + } + return nil, s.Assembly(ctx, a0, a1, a2) case ChangeSignature: var a0 ChangeSignatureArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -350,6 +360,18 @@ }, nil } +func NewAssemblyCommand(title string, a0 string, a1 string, a2 string) (protocol.Command, error) { + args, err := MarshalArgs(a0, a1, a2) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: Assembly.String(), + Arguments: args, + }, nil +} + func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil {
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index e8d6d7d..9f76add 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go
@@ -252,6 +252,13 @@ // block of code depends on, perhaps as a precursor to // extracting it into a separate function. FreeSymbols(context.Context, protocol.DocumentURI, protocol.Range) error + + // Assembly: Show disassembly of current function. + // + // This command opens a web-based disassembly listing of the + // specified function symbol (plus any nested lambdas and defers). + // The machine architecture is determined by the view. + Assembly(_ context.Context, viewID, packageID, symbol string) error } type RunTestsArgs struct {
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index 45f230e..7252aa9 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go
@@ -139,7 +139,10 @@ if golang.IsGenerated(ctx, snapshot, uri) { actions = slices.DeleteFunc(actions, func(a protocol.CodeAction) bool { switch a.Kind { - case protocol.GoTest, protocol.GoDoc, protocol.GoFreeSymbols: + case protocol.GoTest, + protocol.GoDoc, + protocol.GoFreeSymbols, + protocol.GoAssembly: return false // read-only query } return true // potential write operation
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 0a64d1c..04febab 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go
@@ -104,7 +104,7 @@ // be populated, depending on which configuration is set. See comments in-line // for details. type commandDeps struct { - snapshot *cache.Snapshot // present if cfg.forURI was set + snapshot *cache.Snapshot // present if cfg.forURI or forView was set fh file.Handle // present if cfg.forURI was set work *progress.WorkDone // present if cfg.progress was set } @@ -1466,6 +1466,9 @@ } func (c *commandHandler) FreeSymbols(ctx context.Context, uri protocol.DocumentURI, rng protocol.Range) error { + // TODO(adonovan): simplify, following Assembly, by putting the + // viewID in the command so that c.run isn't necessary. + // (freesymbolsURL needs only a viewID, not a view.) return c.run(ctx, commandConfig{ forURI: uri, }, func(ctx context.Context, deps commandDeps) error { @@ -1481,3 +1484,13 @@ return nil }) } + +func (c *commandHandler) Assembly(ctx context.Context, viewID, packageID, symbol string) error { + web, err := c.s.getWeb() + if err != nil { + return err + } + url := web.assemblyURL(viewID, packageID, symbol) + openClientBrowser(ctx, c.s.client, url) + return nil +}
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go index 3418215..2c0c34a 100644 --- a/gopls/internal/server/server.go +++ b/gopls/internal/server/server.go
@@ -205,6 +205,8 @@ // // open?file=%s&line=%d&col=%d - open a file // pkg/PKGPATH?view=%s - show doc for package in a given view +// assembly?pkg=%s&view=%s&symbol=%s - show assembly of specified func symbol +// freesymbols?file=%s&range=%d:%d:%d:%d:&view=%s - show report of free symbols type web struct { server *http.Server addr url.URL // "http://127.0.0.1:PORT/gopls/SECRET" @@ -315,7 +317,7 @@ // Get snapshot of specified view. view, err := s.session.View(req.Form.Get("view")) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusNotFound) return } snapshot, release, err := view.Snapshot() @@ -410,6 +412,55 @@ w.Write(html) }) + // The /assembly?pkg=...&view=...&symbol=... handler shows + // the assembly of the current function. + webMux.HandleFunc("/assembly", func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + if err := req.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Get parameters. + var ( + viewID = req.Form.Get("view") + pkgID = metadata.PackageID(req.Form.Get("pkg")) + symbol = req.Form.Get("symbol") + ) + if viewID == "" || pkgID == "" || symbol == "" { + http.Error(w, "/assembly requires view, pkg, symbol", http.StatusBadRequest) + return + } + + // Get snapshot of specified view. + view, err := s.session.View(viewID) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + snapshot, release, err := view.Snapshot() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer release() + + pkgs, err := snapshot.TypeCheck(ctx, pkgID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pkg := pkgs[0] + + // Produce report. + html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web.openURL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(html) + }) + return web, nil } @@ -456,6 +507,17 @@ "") } +// assemblyURL returns the URL of an assembly listing of the specified function symbol. +func (w *web) assemblyURL(viewID, packageID, symbol string) protocol.URI { + return w.url( + "assembly", + fmt.Sprintf("view=%s&pkg=%s&symbol=%s", + url.QueryEscape(viewID), + url.QueryEscape(packageID), + url.QueryEscape(symbol)), + "") +} + // url returns a URL by joining a relative path, an (encoded) query, // and an (unencoded) fragment onto the authenticated base URL of the // web server.
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index e280c75..fec64e3 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go
@@ -49,6 +49,7 @@ protocol.RefactorRewrite: true, protocol.RefactorInline: true, protocol.RefactorExtract: true, + protocol.GoAssembly: true, protocol.GoDoc: true, protocol.GoFreeSymbols: true, },
diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index 1a169c6..09629b4 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -23,12 +23,12 @@ module example.com go 1.19 --- src.go -- +-- src/a.go -- package a func f() { g() } func g() {} --- gen.go -- +-- gen/a.go -- // Code generated by hand; DO NOT EDIT. package a @@ -62,12 +62,14 @@ } } - check("src.go", + check("src/a.go", + protocol.GoAssembly, protocol.GoDoc, protocol.GoFreeSymbols, protocol.RefactorExtract, protocol.RefactorInline) - check("gen.go", + check("gen/a.go", + protocol.GoAssembly, protocol.GoDoc, protocol.GoFreeSymbols) })
diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 851cc68..22f65ec 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go
@@ -9,12 +9,14 @@ "io" "net/http" "regexp" + "runtime" "strings" "testing" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" . "golang.org/x/tools/gopls/internal/test/integration" + "golang.org/x/tools/internal/testenv" ) // TestWebServer exercises the web server created on demand @@ -295,6 +297,81 @@ }) } +// TestAssembly is a basic test of the web-based assembly listing. +func TestAssembly(t *testing.T) { + testenv.NeedsGo1Point(t, 22) // for up-to-date assembly listing + + const files = ` +-- go.mod -- +module example.com + +-- a/a.go -- +package a + +func f() { + println("hello") + defer println("world") +} + +func g() { + println("goodbye") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + + // Invoke the "Show assembly" code action to start the server. + loc := env.RegexpSearch("a/a.go", "println") + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) + if err != nil { + t.Fatalf("CodeAction: %v", err) + } + const wantTitle = "Show " + runtime.GOARCH + " assembly for f" + var action *protocol.CodeAction + for _, a := range actions { + if a.Title == wantTitle { + action = &a + break + } + } + if action == nil { + t.Fatalf("can't find action with Title %s, only %#v", + wantTitle, actions) + } + + // Execute the command. + // Its side effect should be a single showDocument request. + params := &protocol.ExecuteCommandParams{ + Command: action.Command.Command, + Arguments: action.Command.Arguments, + } + var result command.DebuggingResult + env.ExecuteCommand(params, &result) + doc := shownDocument(t, env, "http:") + if doc == nil { + t.Fatalf("no showDocument call had 'file:' prefix") + } + t.Log("showDocument(package doc) URL:", doc.URI) + + // Get the report and do some minimal checks for sensible results. + // Use only portable instructions below! + report := get(t, doc.URI) + checkMatch(t, true, report, `TEXT.*example.com/a.f`) + checkMatch(t, true, report, `CALL runtime.printlock`) + checkMatch(t, true, report, `CALL runtime.printstring`) + checkMatch(t, true, report, `CALL runtime.printunlock`) + checkMatch(t, true, report, `CALL example.com/a.f.deferwrap1`) + checkMatch(t, true, report, `RET`) + checkMatch(t, true, report, `CALL runtime.morestack_noctxt`) + + // Nested functions are also shown. + checkMatch(t, true, report, `TEXT.*example.com/a.f.deferwrap1`) + + // But other functions are not. + checkMatch(t, false, report, `TEXT.*example.com/a.g`) + }) +} + // shownDocument returns the first shown document matching the URI prefix. // It may be nil. //