// Copyright 2020 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 source import ( "context" "go/ast" "go/token" "go/types" "path/filepath" "regexp" "strings" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) // LensFuncs returns the supported lensFuncs for Go files. func LensFuncs() map[command.Command]LensFunc { return map[command.Command]LensFunc{ command.Generate: goGenerateCodeLens, command.Test: runTestCodeLens, command.RegenerateCgo: regenerateCgoLens, command.GCDetails: toggleDetailsCodeLens, } } var ( testRe = regexp.MustCompile("^Test[^a-z]") benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]") ) func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { codeLens := make([]protocol.CodeLens, 0) fns, err := TestsAndBenchmarks(ctx, snapshot, fh) if err != nil { return nil, err } puri := protocol.URIFromSpanURI(fh.URI()) for _, fn := range fns.Tests { cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil) if err != nil { return nil, err } rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } for _, fn := range fns.Benchmarks { cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name}) if err != nil { return nil, err } rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } if len(fns.Benchmarks) > 0 { _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return nil, err } // add a code lens to the top of the file which runs all benchmarks in the file rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } var benches []string for _, fn := range fns.Benchmarks { benches = append(benches, fn.Name) } cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches) if err != nil { return nil, err } codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) } return codeLens, nil } type testFn struct { Name string Rng protocol.Range } type testFns struct { Tests []testFn Benchmarks []testFn } func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) (testFns, error) { var out testFns if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { return out, nil } pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return out, err } for _, d := range pgf.File.Decls { fn, ok := d.(*ast.FuncDecl) if !ok { continue } rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range() if err != nil { return out, err } if matchTestFunc(fn, pkg, testRe, "T") { out.Tests = append(out.Tests, testFn{fn.Name.Name, rng}) } if matchTestFunc(fn, pkg, benchmarkRe, "B") { out.Benchmarks = append(out.Benchmarks, testFn{fn.Name.Name, rng}) } } return out, nil } func matchTestFunc(fn *ast.FuncDecl, pkg Package, nameRe *regexp.Regexp, paramID string) bool { // Make sure that the function name matches a test function. if !nameRe.MatchString(fn.Name.Name) { return false } info := pkg.GetTypesInfo() if info == nil { return false } obj := info.ObjectOf(fn.Name) if obj == nil { return false } sig, ok := obj.Type().(*types.Signature) if !ok { return false } // Test functions should have only one parameter. if sig.Params().Len() != 1 { return false } // Check the type of the only parameter paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) if !ok { return false } named, ok := paramTyp.Elem().(*types.Named) if !ok { return false } namedObj := named.Obj() if namedObj.Pkg().Path() != "testing" { return false } return namedObj.Id() == paramID } func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } const ggDirective = "//go:generate" for _, c := range pgf.File.Comments { for _, l := range c.List { if !strings.HasPrefix(l.Text, ggDirective) { continue } rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() if err != nil { return nil, err } dir := protocol.URIFromSpanURI(span.URIFromPath(filepath.Dir(fh.URI().Filename()))) nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) if err != nil { return nil, err } recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) if err != nil { return nil, err } return []protocol.CodeLens{ {Range: rng, Command: recursiveCmd}, {Range: rng, Command: nonRecursiveCmd}, }, nil } } return nil, nil } func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } var c *ast.ImportSpec for _, imp := range pgf.File.Imports { if imp.Path.Value == `"C"` { c = imp } } if c == nil { return nil, nil } rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range() if err != nil { return nil, err } puri := protocol.URIFromSpanURI(fh.URI()) cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) if err != nil { return nil, err } return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil } func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return nil, err } rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } puri := protocol.URIFromSpanURI(fh.URI()) cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri) if err != nil { return nil, err } return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil }