blob: 3e4f3113f9ec2e06b88ad19cc67544d75d18aa8c [file] [log] [blame]
// 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
import (
"context"
"encoding/json"
"fmt"
"go/ast"
"go/token"
"go/types"
"reflect"
"slices"
"sort"
"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"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/golang/stubmethods"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/typesinternal"
)
// CodeActions returns all enabled code actions (edits and other
// commands) available for the selected range.
//
// Depending on how the request was triggered, fewer actions may be
// offered, e.g. to avoid UI distractions after mere cursor motion.
//
// See ../protocol/codeactionkind.go for some code action theory.
func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) {
loc := protocol.Location{URI: fh.URI(), Range: rng}
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
if err != nil {
return nil, err
}
start, end, err := pgf.RangePos(rng)
if err != nil {
return nil, err
}
// Scan to see if any enabled producer needs type information.
var enabledMemo [len(codeActionProducers)]bool
needTypes := false
for i, p := range codeActionProducers {
if enabled(p.kind) {
enabledMemo[i] = true
if p.needPkg {
needTypes = true
}
}
}
// Compute type information if needed.
// Also update pgf, start, end to be consistent with pkg.
// They may differ in case of parse cache miss.
var pkg *cache.Package
if needTypes {
var err error
pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, loc.URI)
if err != nil {
return nil, err
}
start, end, err = pgf.RangePos(loc.Range)
if err != nil {
return nil, err
}
}
// Execute each enabled producer function.
req := &codeActionsRequest{
actions: &actions,
lazy: make(map[reflect.Type]any),
snapshot: snapshot,
fh: fh,
pgf: pgf,
loc: loc,
start: start,
end: end,
diagnostics: diagnostics,
trigger: trigger,
pkg: pkg,
}
for i, p := range codeActionProducers {
if !enabledMemo[i] {
continue
}
req.kind = p.kind
if p.needPkg {
req.pkg = pkg
} else {
req.pkg = nil
}
if err := p.fn(ctx, req); err != nil {
return nil, err
}
}
sort.Slice(actions, func(i, j int) bool {
return actions[i].Kind < actions[j].Kind
})
return actions, nil
}
// A codeActionsRequest is passed to each function
// that produces code actions.
type codeActionsRequest struct {
// internal fields for use only by [CodeActions].
actions *[]protocol.CodeAction // pointer to output slice; call addAction to populate
lazy map[reflect.Type]any // lazy construction
// inputs to the producer function:
kind protocol.CodeActionKind
snapshot *cache.Snapshot
fh file.Handle
pgf *parsego.File
loc protocol.Location
start, end token.Pos
diagnostics []protocol.Diagnostic
trigger protocol.CodeActionTriggerKind
pkg *cache.Package // set only if producer.needPkg
}
// addApplyFixAction adds an ApplyFix command-based CodeAction to the result.
func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol.Location) {
cmd := command.NewApplyFixCommand(title, command.ApplyFixArgs{
Fix: fix,
Location: loc,
ResolveEdits: req.resolveEdits(),
})
req.addCommandAction(cmd, true)
}
// addCommandAction adds a CodeAction to the result based on the provided command.
//
// If allowResolveEdits (and the client supports codeAction/resolve)
// then the command is embedded into the code action data field so
// that the client can later ask the server to "resolve" a command
// into an edit that they can preview and apply selectively.
// Set allowResolveEdits only for actions that generate edits.
//
// Otherwise, the command is set as the code action operation.
func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowResolveEdits bool) {
act := protocol.CodeAction{
Title: cmd.Title,
Kind: req.kind,
}
if allowResolveEdits && req.resolveEdits() {
data, err := json.Marshal(cmd)
if err != nil {
panic("unable to marshal")
}
msg := json.RawMessage(data)
act.Data = &msg
} else {
act.Command = cmd
}
req.addAction(act)
}
// addCommandAction adds an edit-based CodeAction to the result.
func (req *codeActionsRequest) addEditAction(title string, fixedDiagnostics []protocol.Diagnostic, changes ...protocol.DocumentChange) {
req.addAction(protocol.CodeAction{
Title: title,
Kind: req.kind,
Diagnostics: fixedDiagnostics,
Edit: protocol.NewWorkspaceEdit(changes...),
})
}
// addAction adds a code action to the response.
func (req *codeActionsRequest) addAction(act protocol.CodeAction) {
*req.actions = append(*req.actions, act)
}
// resolveEdits reports whether the client can resolve edits lazily.
func (req *codeActionsRequest) resolveEdits() bool {
opts := req.snapshot.Options()
return opts.CodeActionResolveOptions != nil &&
slices.Contains(opts.CodeActionResolveOptions, "edit")
}
// lazyInit[*T](ctx, req) returns a pointer to an instance of T,
// calling new(T).init(ctx.req) on the first request.
//
// It is conceptually a (generic) method of req.
func lazyInit[P interface {
init(ctx context.Context, req *codeActionsRequest)
*T
}, T any](ctx context.Context, req *codeActionsRequest) P {
t := reflect.TypeFor[T]()
v, ok := req.lazy[t].(P)
if !ok {
v = new(T)
v.init(ctx, req)
req.lazy[t] = v
}
return v
}
// -- producers --
// A codeActionProducer describes a function that produces CodeActions
// of a particular kind.
// The function is only called if that kind is enabled.
type codeActionProducer struct {
kind protocol.CodeActionKind
fn func(ctx context.Context, req *codeActionsRequest) error
needPkg bool // fn needs type information (req.pkg)
}
var codeActionProducers = [...]codeActionProducer{
{kind: protocol.QuickFix, fn: quickFix, needPkg: true},
{kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports},
{kind: settings.AddTest, fn: addTest, needPkg: true},
{kind: settings.GoAssembly, fn: goAssembly, needPkg: true},
{kind: settings.GoDoc, fn: goDoc, needPkg: true},
{kind: settings.GoFreeSymbols, fn: goFreeSymbols},
{kind: settings.GoTest, fn: goTest},
{kind: settings.GoplsDocFeatures, fn: goplsDocFeatures},
{kind: settings.RefactorExtractFunction, fn: refactorExtractFunction},
{kind: settings.RefactorExtractMethod, fn: refactorExtractMethod},
{kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile},
{kind: settings.RefactorExtractVariable, fn: refactorExtractVariable},
{kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true},
{kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote},
{kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true},
{kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true},
{kind: settings.RefactorRewriteInvertIf, fn: refactorRewriteInvertIf},
{kind: settings.RefactorRewriteJoinLines, fn: refactorRewriteJoinLines, needPkg: true},
{kind: settings.RefactorRewriteRemoveUnusedParam, fn: refactorRewriteRemoveUnusedParam, needPkg: true},
{kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true},
// Note: don't forget to update the allow-list in Server.CodeAction
// when adding new query operations like GoTest and GoDoc that
// are permitted even in generated source files.
}
// sourceOrganizeImports produces "Organize Imports" code actions.
func sourceOrganizeImports(ctx context.Context, req *codeActionsRequest) error {
res := lazyInit[*allImportsFixesResult](ctx, req)
// Send all of the import edits as one code action
// if the file is being organized.
if len(res.allFixEdits) > 0 {
req.addEditAction("Organize Imports", nil, protocol.DocumentChangeEdit(req.fh, res.allFixEdits))
}
return nil
}
// quickFix produces code actions that fix errors,
// for example by adding/deleting/renaming imports,
// or declaring the missing methods of a type.
func quickFix(ctx context.Context, req *codeActionsRequest) error {
// Only compute quick fixes if there are any diagnostics to fix.
if len(req.diagnostics) == 0 {
return nil
}
// Process any missing imports and pair them with the diagnostics they fix.
res := lazyInit[*allImportsFixesResult](ctx, req)
if res.err != nil {
return nil
}
// Separate this into a set of codeActions per diagnostic, where
// each action is the addition, removal, or renaming of one import.
for _, importFix := range res.editsPerFix {
fixedDiags := fixedByImportFix(importFix.fix, req.diagnostics)
if len(fixedDiags) == 0 {
continue
}
req.addEditAction(importFixTitle(importFix.fix), fixedDiags, protocol.DocumentChangeEdit(req.fh, importFix.edits))
}
// Quick fixes for type errors.
info := req.pkg.TypesInfo()
for _, typeError := range req.pkg.TypeErrors() {
// Does type error overlap with CodeAction range?
start, end := typeError.Pos, typeError.Pos
if _, _, endPos, ok := typesinternal.ReadGo116ErrorData(typeError); ok {
end = endPos
}
typeErrorRange, err := req.pgf.PosRange(start, end)
if err != nil || !protocol.Intersect(typeErrorRange, req.loc.Range) {
continue
}
msg := typeError.Error()
switch {
// "Missing method" error? (stubmethods)
// Offer a "Declare missing methods of INTERFACE" code action.
// See [stubMissingInterfaceMethodsFixer] for command implementation.
case strings.Contains(msg, "missing method"),
strings.HasPrefix(msg, "cannot convert"),
strings.Contains(msg, "not implement"):
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, path, start)
if si != nil {
qf := typesutil.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg(), info)
iface := types.TypeString(si.Interface.Type(), qf)
msg := fmt.Sprintf("Declare missing methods of %s", iface)
req.addApplyFixAction(msg, fixMissingInterfaceMethods, req.loc)
}
// "type X has no field or method Y" compiler error.
// Offer a "Declare missing method T.f" code action.
// See [stubMissingCalledFunctionFixer] for command implementation.
case strings.Contains(msg, "has no field or method"):
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start)
if si != nil {
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
}
}
}
return nil
}
// allImportsFixesResult is the result of a lazy call to allImportsFixes.
// It implements the codeActionsRequest lazyInit interface.
type allImportsFixesResult struct {
allFixEdits []protocol.TextEdit
editsPerFix []*importFix
err error
}
func (res *allImportsFixesResult) init(ctx context.Context, req *codeActionsRequest) {
res.allFixEdits, res.editsPerFix, res.err = allImportsFixes(ctx, req.snapshot, req.pgf)
if res.err != nil {
event.Error(ctx, "imports fixes", res.err, label.File.Of(req.loc.URI.Path()))
}
}
func importFixTitle(fix *imports.ImportFix) string {
var str string
switch fix.FixType {
case imports.AddImport:
str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
case imports.DeleteImport:
str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
case imports.SetImportName:
str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
}
return str
}
// fixedByImportFix filters the provided slice of diagnostics to those that
// would be fixed by the provided imports fix.
func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic {
var results []protocol.Diagnostic
for _, diagnostic := range diagnostics {
switch {
// "undeclared name: X" may be an unresolved import.
case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
if ident == fix.IdentName {
results = append(results, diagnostic)
}
// "undefined: X" may be an unresolved import at Go 1.20+.
case strings.HasPrefix(diagnostic.Message, "undefined: "):
ident := strings.TrimPrefix(diagnostic.Message, "undefined: ")
if ident == fix.IdentName {
results = append(results, diagnostic)
}
// "could not import: X" may be an invalid import.
case strings.HasPrefix(diagnostic.Message, "could not import: "):
ident := strings.TrimPrefix(diagnostic.Message, "could not import: ")
if ident == fix.IdentName {
results = append(results, diagnostic)
}
// "X imported but not used" is an unused import.
// "X imported but not used as Y" is an unused import.
case strings.Contains(diagnostic.Message, " imported but not used"):
idx := strings.Index(diagnostic.Message, " imported but not used")
importPath := diagnostic.Message[:idx]
if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
results = append(results, diagnostic)
}
}
}
return results
}
// goFreeSymbols produces "Browse free symbols" code actions.
// See [server.commandHandler.FreeSymbols] for command implementation.
func goFreeSymbols(ctx context.Context, req *codeActionsRequest) error {
if !req.loc.Empty() {
cmd := command.NewFreeSymbolsCommand("Browse free symbols", req.snapshot.View().ID(), req.loc)
req.addCommandAction(cmd, false)
}
return nil
}
// goplsDocFeatures produces "Browse gopls feature documentation" code actions.
// See [server.commandHandler.ClientOpenURL] for command implementation.
func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error {
// TODO(adonovan): after the docs are published in gopls/v0.17.0,
// use the gopls release tag instead of master.
cmd := command.NewClientOpenURLCommand(
"Browse gopls feature documentation",
"https://github.com/golang/tools/blob/master/gopls/doc/features/README.md")
req.addCommandAction(cmd, false)
return nil
}
// goDoc produces "Browse documentation for X" code actions.
// See [server.commandHandler.Doc] for command implementation.
func goDoc(ctx context.Context, req *codeActionsRequest) error {
_, _, title := DocFragment(req.pkg, req.pgf, req.start, req.end)
cmd := command.NewDocCommand(title, command.DocArgs{Location: req.loc, ShowDocument: true})
req.addCommandAction(cmd, false)
return nil
}
// refactorExtractFunction produces "Extract function" code actions.
// See [extractFunction] for command implementation.
func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error {
if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok {
req.addApplyFixAction("Extract function", fixExtractFunction, req.loc)
}
return nil
}
// refactorExtractMethod produces "Extract method" code actions.
// See [extractMethod] for command implementation.
func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error {
if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK {
req.addApplyFixAction("Extract method", fixExtractMethod, req.loc)
}
return nil
}
// refactorExtractVariable produces "Extract variable" code actions.
// See [extractVariable] for command implementation.
func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error {
if _, _, ok, _ := canExtractVariable(req.start, req.end, req.pgf.File); ok {
req.addApplyFixAction("Extract variable", fixExtractVariable, req.loc)
}
return nil
}
// refactorExtractToNewFile produces "Extract declarations to new file" code actions.
// See [server.commandHandler.ExtractToNewFile] for command implementation.
func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) error {
if canExtractToNewFile(req.pgf, req.start, req.end) {
cmd := command.NewExtractToNewFileCommand("Extract declarations to new file", req.loc)
req.addCommandAction(cmd, true)
}
return nil
}
// addTest produces "Add a test for FUNC" code actions.
// See [server.commandHandler.AddTest] for command implementation.
func addTest(ctx context.Context, req *codeActionsRequest) error {
// Reject if the feature is turned off.
if !req.snapshot.Options().AddTestSourceCodeAction {
return nil
}
// Reject test package.
if req.pkg.Metadata().ForTest != "" {
return nil
}
path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end)
if len(path) < 2 {
return nil
}
decl, ok := path[len(path)-2].(*ast.FuncDecl)
if !ok {
return nil
}
// Don't offer to create tests of "init" or "_".
if decl.Name.Name == "_" || decl.Name.Name == "init" {
return nil
}
cmd := command.NewAddTestCommand("Add a test for "+decl.Name.String(), req.loc)
req.addCommandAction(cmd, true)
// TODO(hxjiang): add code action for generate test for package/file.
return nil
}
// refactorRewriteRemoveUnusedParam produces "Remove unused parameter" code actions.
// See [server.commandHandler.ChangeSignature] for command implementation.
func refactorRewriteRemoveUnusedParam(ctx context.Context, req *codeActionsRequest) error {
if canRemoveParameter(req.pkg, req.pgf, req.loc.Range) {
cmd := command.NewChangeSignatureCommand("Refactor: remove unused parameter", command.ChangeSignatureArgs{
RemoveParameter: req.loc,
ResolveEdits: req.resolveEdits(),
})
req.addCommandAction(cmd, true)
}
return nil
}
// refactorRewriteChangeQuote produces "Convert to {raw,interpreted} string literal" code actions.
func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) error {
convertStringLiteral(req)
return nil
}
// refactorRewriteChangeQuote produces "Invert 'if' condition" code actions.
// See [invertIfCondition] for command implementation.
func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error {
if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok {
req.addApplyFixAction("Invert 'if' condition", fixInvertIfCondition, req.loc)
}
return nil
}
// refactorRewriteSplitLines produces "Split ITEMS into separate lines" code actions.
// See [splitLines] for command implementation.
func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error {
// TODO(adonovan): opt: don't set needPkg just for FileSet.
if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok {
req.addApplyFixAction(msg, fixSplitLines, req.loc)
}
return nil
}
// refactorRewriteJoinLines produces "Join ITEMS into one line" code actions.
// See [joinLines] for command implementation.
func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error {
// TODO(adonovan): opt: don't set needPkg just for FileSet.
if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok {
req.addApplyFixAction(msg, fixJoinLines, req.loc)
}
return nil
}
// refactorRewriteFillStruct produces "Fill STRUCT" code actions.
// See [fillstruct.SuggestedFix] for command implementation.
func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) error {
// fillstruct.Diagnose is a lazy analyzer: all it gives us is
// the (start, end, message) of each SuggestedFix; the actual
// edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix.
for _, diag := range fillstruct.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) {
loc, err := req.pgf.Mapper.PosLocation(req.pgf.Tok, diag.Pos, diag.End)
if err != nil {
return err
}
for _, fix := range diag.SuggestedFixes {
req.addApplyFixAction(fix.Message, diag.Category, loc)
}
}
return nil
}
// refactorRewriteFillSwitch produces "Add cases for TYPE/ENUM" code actions.
func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) error {
for _, diag := range fillswitch.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) {
changes, err := suggestedFixToDocumentChange(ctx, req.snapshot, req.pkg.FileSet(), &diag.SuggestedFixes[0])
if err != nil {
return err
}
req.addEditAction(diag.Message, nil, changes...)
}
return nil
}
// canRemoveParameter reports whether we can remove the function parameter
// indicated by the given [start, end) range.
//
// This is true if:
// - there are no parse or type errors, and
// - [start, end) is contained within an unused field or parameter name
// - ... of a non-method function declaration.
//
// (Note that the unusedparam analyzer also computes this property, but
// much more precisely, allowing it to report its findings as diagnostics.)
//
// TODO(adonovan): inline into refactorRewriteRemoveUnusedParam.
func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool {
if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 {
return false // can't remove parameters from packages with errors
}
info, err := findParam(pgf, rng)
if err != nil {
return false // e.g. invalid range
}
if info.field == nil {
return false // range does not span a parameter
}
if info.decl.Body == nil {
return false // external function
}
if len(info.field.Names) == 0 {
return true // no names => field is unused
}
if info.name == nil {
return false // no name is indicated
}
if info.name.Name == "_" {
return true // trivially unused
}
obj := pkg.TypesInfo().Defs[info.name]
if obj == nil {
return false // something went wrong
}
used := false
ast.Inspect(info.decl.Body, func(node ast.Node) bool {
if n, ok := node.(*ast.Ident); ok && pkg.TypesInfo().Uses[n] == obj {
used = true
}
return !used // keep going until we find a use
})
return !used
}
// refactorInlineCall produces "Inline call to FUNC" code actions.
// See [inlineCall] for command implementation.
func refactorInlineCall(ctx context.Context, req *codeActionsRequest) error {
// To avoid distraction (e.g. VS Code lightbulb), offer "inline"
// only after a selection or explicit menu operation.
// TODO(adonovan): remove this (and req.trigger); see comment at TestVSCodeIssue65167.
if req.trigger == protocol.CodeActionAutomatic && req.loc.Empty() {
return nil
}
// If range is within call expression, offer to inline the call.
if _, fn, err := enclosingStaticCall(req.pkg, req.pgf, req.start, req.end); err == nil {
req.addApplyFixAction("Inline call to "+fn.Name(), fixInlineCall, req.loc)
}
return nil
}
// goTest produces "Run tests and benchmarks" code actions.
// See [server.commandHandler.runTests] for command implementation.
func goTest(ctx context.Context, req *codeActionsRequest) error {
testFuncs, benchFuncs, err := testsAndBenchmarks(req.pkg.TypesInfo(), req.pgf)
if err != nil {
return err
}
var tests, benchmarks []string
for _, fn := range testFuncs {
if protocol.Intersect(fn.rng, req.loc.Range) {
tests = append(tests, fn.name)
}
}
for _, fn := range benchFuncs {
if protocol.Intersect(fn.rng, req.loc.Range) {
benchmarks = append(benchmarks, fn.name)
}
}
if len(tests) == 0 && len(benchmarks) == 0 {
return nil
}
cmd := command.NewTestCommand("Run tests and benchmarks", req.loc.URI, tests, benchmarks)
req.addCommandAction(cmd, false)
return nil
}
// goAssembly produces "Browse ARCH assembly for FUNC" code actions.
// See [server.commandHandler.Assembly] for command implementation.
func goAssembly(ctx context.Context, req *codeActionsRequest) error {
view := req.snapshot.View()
// 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?
path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end)
if len(path) >= 2 { // [... FuncDecl File]
if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok {
if fn, ok := req.pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok {
sig := fn.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 := command.NewAssemblyCommand(
fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), decl.Name),
view.ID(),
string(req.pkg.Metadata().ID),
sym.String())
req.addCommandAction(cmd, false)
}
}
}
}
return nil
}