github.com/jd-ly/tools@v0.5.7/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/jd-ly/tools/internal/lsp/protocol"
    17  	"github.com/jd-ly/tools/internal/span"
    18  )
    19  
    20  type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error)
    21  
    22  // LensFuncs returns the supported lensFuncs for Go files.
    23  func LensFuncs() map[string]LensFunc {
    24  	return map[string]LensFunc{
    25  		CommandGenerate.Name:      goGenerateCodeLens,
    26  		CommandTest.Name:          runTestCodeLens,
    27  		CommandRegenerateCgo.Name: regenerateCgoLens,
    28  		CommandToggleDetails.Name: toggleDetailsCodeLens,
    29  	}
    30  }
    31  
    32  var (
    33  	testRe      = regexp.MustCompile("^Test[^a-z]")
    34  	benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]")
    35  )
    36  
    37  func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
    38  	codeLens := make([]protocol.CodeLens, 0)
    39  
    40  	fns, err := TestsAndBenchmarks(ctx, snapshot, fh)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	for _, fn := range fns.Tests {
    45  		jsonArgs, err := MarshalArgs(fh.URI(), []string{fn.Name}, nil)
    46  		if err != nil {
    47  			return nil, err
    48  		}
    49  		codeLens = append(codeLens, protocol.CodeLens{
    50  			Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start},
    51  			Command: protocol.Command{
    52  				Title:     "run test",
    53  				Command:   CommandTest.ID(),
    54  				Arguments: jsonArgs,
    55  			},
    56  		})
    57  	}
    58  
    59  	for _, fn := range fns.Benchmarks {
    60  		jsonArgs, err := MarshalArgs(fh.URI(), nil, []string{fn.Name})
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  		codeLens = append(codeLens, protocol.CodeLens{
    65  			Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start},
    66  			Command: protocol.Command{
    67  				Title:     "run benchmark",
    68  				Command:   CommandTest.ID(),
    69  				Arguments: jsonArgs,
    70  			},
    71  		})
    72  	}
    73  
    74  	_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	// add a code lens to the top of the file which runs all benchmarks in the file
    79  	rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	args, err := MarshalArgs(fh.URI(), []string{}, fns.Benchmarks)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	codeLens = append(codeLens, protocol.CodeLens{
    88  		Range: rng,
    89  		Command: protocol.Command{
    90  			Title:     "run file benchmarks",
    91  			Command:   CommandTest.ID(),
    92  			Arguments: args,
    93  		},
    94  	})
    95  	return codeLens, nil
    96  }
    97  
    98  type testFn struct {
    99  	Name string
   100  	Rng  protocol.Range
   101  }
   102  
   103  type testFns struct {
   104  	Tests      []testFn
   105  	Benchmarks []testFn
   106  }
   107  
   108  func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) (testFns, error) {
   109  	var out testFns
   110  
   111  	if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
   112  		return out, nil
   113  	}
   114  	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
   115  	if err != nil {
   116  		return out, err
   117  	}
   118  
   119  	for _, d := range pgf.File.Decls {
   120  		fn, ok := d.(*ast.FuncDecl)
   121  		if !ok {
   122  			continue
   123  		}
   124  
   125  		rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range()
   126  		if err != nil {
   127  			return out, err
   128  		}
   129  
   130  		if matchTestFunc(fn, pkg, testRe, "T") {
   131  			out.Tests = append(out.Tests, testFn{fn.Name.Name, rng})
   132  		}
   133  
   134  		if matchTestFunc(fn, pkg, benchmarkRe, "B") {
   135  			out.Benchmarks = append(out.Benchmarks, testFn{fn.Name.Name, rng})
   136  		}
   137  	}
   138  
   139  	return out, nil
   140  }
   141  
   142  func matchTestFunc(fn *ast.FuncDecl, pkg Package, nameRe *regexp.Regexp, paramID string) bool {
   143  	// Make sure that the function name matches a test function.
   144  	if !nameRe.MatchString(fn.Name.Name) {
   145  		return false
   146  	}
   147  	info := pkg.GetTypesInfo()
   148  	if info == nil {
   149  		return false
   150  	}
   151  	obj := info.ObjectOf(fn.Name)
   152  	if obj == nil {
   153  		return false
   154  	}
   155  	sig, ok := obj.Type().(*types.Signature)
   156  	if !ok {
   157  		return false
   158  	}
   159  	// Test functions should have only one parameter.
   160  	if sig.Params().Len() != 1 {
   161  		return false
   162  	}
   163  
   164  	// Check the type of the only parameter
   165  	paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer)
   166  	if !ok {
   167  		return false
   168  	}
   169  	named, ok := paramTyp.Elem().(*types.Named)
   170  	if !ok {
   171  		return false
   172  	}
   173  	namedObj := named.Obj()
   174  	if namedObj.Pkg().Path() != "testing" {
   175  		return false
   176  	}
   177  	return namedObj.Id() == paramID
   178  }
   179  
   180  func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
   181  	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	const ggDirective = "//go:generate"
   186  	for _, c := range pgf.File.Comments {
   187  		for _, l := range c.List {
   188  			if !strings.HasPrefix(l.Text, ggDirective) {
   189  				continue
   190  			}
   191  			rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range()
   192  			if err != nil {
   193  				return nil, err
   194  			}
   195  			dir := span.URIFromPath(filepath.Dir(fh.URI().Filename()))
   196  			nonRecursiveArgs, err := MarshalArgs(dir, false)
   197  			if err != nil {
   198  				return nil, err
   199  			}
   200  			recursiveArgs, err := MarshalArgs(dir, true)
   201  			if err != nil {
   202  				return nil, err
   203  			}
   204  			return []protocol.CodeLens{
   205  				{
   206  					Range: rng,
   207  					Command: protocol.Command{
   208  						Title:     "run go generate",
   209  						Command:   CommandGenerate.ID(),
   210  						Arguments: nonRecursiveArgs,
   211  					},
   212  				},
   213  				{
   214  					Range: rng,
   215  					Command: protocol.Command{
   216  						Title:     "run go generate ./...",
   217  						Command:   CommandGenerate.ID(),
   218  						Arguments: recursiveArgs,
   219  					},
   220  				},
   221  			}, nil
   222  
   223  		}
   224  	}
   225  	return nil, nil
   226  }
   227  
   228  func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
   229  	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	var c *ast.ImportSpec
   234  	for _, imp := range pgf.File.Imports {
   235  		if imp.Path.Value == `"C"` {
   236  			c = imp
   237  		}
   238  	}
   239  	if c == nil {
   240  		return nil, nil
   241  	}
   242  	rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range()
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	jsonArgs, err := MarshalArgs(fh.URI())
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	return []protocol.CodeLens{
   251  		{
   252  			Range: rng,
   253  			Command: protocol.Command{
   254  				Title:     "regenerate cgo definitions",
   255  				Command:   CommandRegenerateCgo.ID(),
   256  				Arguments: jsonArgs,
   257  			},
   258  		},
   259  	}, nil
   260  }
   261  
   262  func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
   263  	_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	jsonArgs, err := MarshalArgs(fh.URI())
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	return []protocol.CodeLens{{
   276  		Range: rng,
   277  		Command: protocol.Command{
   278  			Title:     "Toggle gc annotation details",
   279  			Command:   CommandToggleDetails.ID(),
   280  			Arguments: jsonArgs,
   281  		},
   282  	}}, nil
   283  }