golang.org/x/tools/gopls@v0.15.3/internal/golang/code_lens.go (about)

     1  // Copyright 2020 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 golang
     6  
     7  import (
     8  	"context"
     9  	"go/ast"
    10  	"go/token"
    11  	"go/types"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"golang.org/x/tools/gopls/internal/cache"
    16  	"golang.org/x/tools/gopls/internal/file"
    17  	"golang.org/x/tools/gopls/internal/protocol"
    18  	"golang.org/x/tools/gopls/internal/protocol/command"
    19  )
    20  
    21  type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error)
    22  
    23  // LensFuncs returns the supported lensFuncs for Go files.
    24  func LensFuncs() map[command.Command]LensFunc {
    25  	return map[command.Command]LensFunc{
    26  		command.Generate:      goGenerateCodeLens,
    27  		command.Test:          runTestCodeLens,
    28  		command.RegenerateCgo: regenerateCgoLens,
    29  		command.GCDetails:     toggleDetailsCodeLens,
    30  	}
    31  }
    32  
    33  var (
    34  	testRe      = regexp.MustCompile(`^Test([^a-z]|$)`) // TestFoo or Test but not Testable
    35  	benchmarkRe = regexp.MustCompile(`^Benchmark([^a-z]|$)`)
    36  )
    37  
    38  func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
    39  	var codeLens []protocol.CodeLens
    40  
    41  	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  	fns, err := TestsAndBenchmarks(pkg, pgf)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	puri := fh.URI()
    50  	for _, fn := range fns.Tests {
    51  		cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil)
    52  		if err != nil {
    53  			return nil, err
    54  		}
    55  		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
    56  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd})
    57  	}
    58  
    59  	for _, fn := range fns.Benchmarks {
    60  		cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name})
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
    65  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd})
    66  	}
    67  
    68  	if len(fns.Benchmarks) > 0 {
    69  		pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  		// add a code lens to the top of the file which runs all benchmarks in the file
    74  		rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package)
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  		var benches []string
    79  		for _, fn := range fns.Benchmarks {
    80  			benches = append(benches, fn.Name)
    81  		}
    82  		cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd})
    87  	}
    88  	return codeLens, nil
    89  }
    90  
    91  type TestFn struct {
    92  	Name string
    93  	Rng  protocol.Range
    94  }
    95  
    96  type TestFns struct {
    97  	Tests      []TestFn
    98  	Benchmarks []TestFn
    99  }
   100  
   101  func TestsAndBenchmarks(pkg *cache.Package, pgf *ParsedGoFile) (TestFns, error) {
   102  	var out TestFns
   103  
   104  	if !strings.HasSuffix(pgf.URI.Path(), "_test.go") {
   105  		return out, nil
   106  	}
   107  
   108  	for _, d := range pgf.File.Decls {
   109  		fn, ok := d.(*ast.FuncDecl)
   110  		if !ok {
   111  			continue
   112  		}
   113  
   114  		rng, err := pgf.NodeRange(fn)
   115  		if err != nil {
   116  			return out, err
   117  		}
   118  
   119  		if matchTestFunc(fn, pkg, testRe, "T") {
   120  			out.Tests = append(out.Tests, TestFn{fn.Name.Name, rng})
   121  		}
   122  
   123  		if matchTestFunc(fn, pkg, benchmarkRe, "B") {
   124  			out.Benchmarks = append(out.Benchmarks, TestFn{fn.Name.Name, rng})
   125  		}
   126  	}
   127  
   128  	return out, nil
   129  }
   130  
   131  func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, paramID string) bool {
   132  	// Make sure that the function name matches a test function.
   133  	if !nameRe.MatchString(fn.Name.Name) {
   134  		return false
   135  	}
   136  	info := pkg.GetTypesInfo()
   137  	if info == nil {
   138  		return false
   139  	}
   140  	obj := info.ObjectOf(fn.Name)
   141  	if obj == nil {
   142  		return false
   143  	}
   144  	sig, ok := obj.Type().(*types.Signature)
   145  	if !ok {
   146  		return false
   147  	}
   148  	// Test functions should have only one parameter.
   149  	if sig.Params().Len() != 1 {
   150  		return false
   151  	}
   152  
   153  	// Check the type of the only parameter
   154  	paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer)
   155  	if !ok {
   156  		return false
   157  	}
   158  	named, ok := paramTyp.Elem().(*types.Named)
   159  	if !ok {
   160  		return false
   161  	}
   162  	namedObj := named.Obj()
   163  	if namedObj.Pkg().Path() != "testing" {
   164  		return false
   165  	}
   166  	return namedObj.Id() == paramID
   167  }
   168  
   169  func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
   170  	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	const ggDirective = "//go:generate"
   175  	for _, c := range pgf.File.Comments {
   176  		for _, l := range c.List {
   177  			if !strings.HasPrefix(l.Text, ggDirective) {
   178  				continue
   179  			}
   180  			rng, err := pgf.PosRange(l.Pos(), l.Pos()+token.Pos(len(ggDirective)))
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  			dir := fh.URI().Dir()
   185  			nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false})
   186  			if err != nil {
   187  				return nil, err
   188  			}
   189  			recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true})
   190  			if err != nil {
   191  				return nil, err
   192  			}
   193  			return []protocol.CodeLens{
   194  				{Range: rng, Command: &recursiveCmd},
   195  				{Range: rng, Command: &nonRecursiveCmd},
   196  			}, nil
   197  
   198  		}
   199  	}
   200  	return nil, nil
   201  }
   202  
   203  func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
   204  	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	var c *ast.ImportSpec
   209  	for _, imp := range pgf.File.Imports {
   210  		if imp.Path.Value == `"C"` {
   211  			c = imp
   212  		}
   213  	}
   214  	if c == nil {
   215  		return nil, nil
   216  	}
   217  	rng, err := pgf.NodeRange(c)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	puri := fh.URI()
   222  	cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil
   227  }
   228  
   229  func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
   230  	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	if !pgf.File.Package.IsValid() {
   235  		// Without a package name we have nowhere to put the codelens, so give up.
   236  		return nil, nil
   237  	}
   238  	rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	puri := fh.URI()
   243  	cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil
   248  }