github.com/v2fly/tools@v0.100.0/internal/lsp/source/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 source
     6  
     7  import (
     8  	"context"
     9  	"go/ast"
    10  	"go/token"
    11  	"go/types"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/v2fly/tools/internal/lsp/command"
    17  	"github.com/v2fly/tools/internal/lsp/protocol"
    18  	"github.com/v2fly/tools/internal/span"
    19  )
    20  
    21  type LensFunc func(context.Context, Snapshot, FileHandle) ([]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]")
    35  	benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]")
    36  )
    37  
    38  func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
    39  	codeLens := make([]protocol.CodeLens, 0)
    40  
    41  	fns, err := TestsAndBenchmarks(ctx, snapshot, fh)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  	puri := protocol.URIFromSpanURI(fh.URI())
    46  	for _, fn := range fns.Tests {
    47  		cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil)
    48  		if err != nil {
    49  			return nil, err
    50  		}
    51  		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
    52  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
    53  	}
    54  
    55  	for _, fn := range fns.Benchmarks {
    56  		cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name})
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
    61  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
    62  	}
    63  
    64  	if len(fns.Benchmarks) > 0 {
    65  		_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  		// add a code lens to the top of the file which runs all benchmarks in the file
    70  		rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
    71  		if err != nil {
    72  			return nil, err
    73  		}
    74  		var benches []string
    75  		for _, fn := range fns.Benchmarks {
    76  			benches = append(benches, fn.Name)
    77  		}
    78  		cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches)
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
    83  	}
    84  	return codeLens, nil
    85  }
    86  
    87  type testFn struct {
    88  	Name string
    89  	Rng  protocol.Range
    90  }
    91  
    92  type testFns struct {
    93  	Tests      []testFn
    94  	Benchmarks []testFn
    95  }
    96  
    97  func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) (testFns, error) {
    98  	var out testFns
    99  
   100  	if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
   101  		return out, nil
   102  	}
   103  	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
   104  	if err != nil {
   105  		return out, err
   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 := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range()
   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 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 Snapshot, fh FileHandle) ([]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 := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range()
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  			dir := protocol.URIFromSpanURI(span.URIFromPath(filepath.Dir(fh.URI().Filename())))
   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 Snapshot, fh FileHandle) ([]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 := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range()
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	puri := protocol.URIFromSpanURI(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 Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
   230  	_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	puri := protocol.URIFromSpanURI(fh.URI())
   239  	cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil
   244  }