diff --git a/internal/lsp/cmd/test/refactor_rewrite.go b/internal/lsp/cmd/test/refactor_rewrite.go new file mode 100644 index 00000000000..9d7a1d734d4 --- /dev/null +++ b/internal/lsp/cmd/test/refactor_rewrite.go @@ -0,0 +1,9 @@ +package cmdtest + +import ( + "testing" + + "golang.org/x/tools/internal/span" +) + +func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) {} diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index c226c1f8b8f..25c6d511025 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -143,6 +143,11 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara }) } } + fillActions, err := source.FillStruct(ctx, snapshot, fh, params.Range) + if err != nil { + return nil, err + } + codeActions = append(codeActions, fillActions...) default: // Unsupported file kind for a code action. return nil, nil diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index ff421153c1f..d3be6523be5 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -369,6 +369,49 @@ func (r *runner) Import(t *testing.T, spn span.Span) { } } +func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) { + uri := spn.URI() + m, err := r.data.Mapper(uri) + if err != nil { + t.Fatal(err) + } + rng, err := m.Range(spn) + if err != nil { + t.Fatal(err) + } + actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(uri), + }, + Range: rng, + }) + + if len(actions) == 0 { + return + } + for _, action := range actions { + // There may be more code actions available at spn (Span), + // we only need the one specified in the title + if action.Kind != protocol.RefactorRewrite || action.Title != title { + continue + } + res, err := applyWorkspaceEdits(r, action.Edit) + if err != nil { + t.Fatal(err) + } + for u, got := range res { + fixed := string(r.data.Golden(tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + return []byte(got), nil + })) + if fixed != got { + t.Errorf("%s failed for %s, expected:\n%#v\ngot:\n%#v", title, u.Filename(), fixed, got) + } + } + return + } + t.Fatalf("expected code action: %v but none", title) +} + func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) { uri := spn.URI() view, err := r.server.session.ViewOf(uri) diff --git a/internal/lsp/source/fill_struct.go b/internal/lsp/source/fill_struct.go new file mode 100644 index 00000000000..376b904ab25 --- /dev/null +++ b/internal/lsp/source/fill_struct.go @@ -0,0 +1,144 @@ +// 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" + "fmt" + "go/format" + "go/types" + "strings" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/internal/lsp/protocol" +) + +// FillStruct completes all of targeted struct's fields with their default values. +func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, protoRng protocol.Range) ([]protocol.CodeAction, error) { + + pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle) + if err != nil { + return nil, fmt.Errorf("getting file for struct fill code action: %v", err) + } + file, src, m, _, err := pgh.Cached() + if err != nil { + return nil, err + } + spn, err := m.PointSpan(protoRng.Start) + if err != nil { + return nil, err + } + spanRng, err := spn.Range(m.Converter) + if err != nil { + return nil, err + } + path, _ := astutil.PathEnclosingInterval(file, spanRng.Start, spanRng.End) + if path == nil { + return nil, nil + } + + ecl := enclosingCompositeLiteral(path, spanRng.Start, pkg.GetTypesInfo()) + if ecl == nil || !ecl.isStruct() { + return nil, nil + } + + // If in F{ Bar<> : V} or anywhere in F{Bar : V, ...} + // we should not fill the struct. + if ecl.inKey || len(ecl.cl.Elts) != 0 { + return nil, nil + } + + var codeActions []protocol.CodeAction + qfFunc := qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()) + switch obj := ecl.clType.(type) { + case *types.Struct: + fieldCount := obj.NumFields() + if fieldCount == 0 { + return nil, nil + } + var fieldSourceCode strings.Builder + for i := 0; i < fieldCount; i++ { + field := obj.Field(i) + // Ignore fields that are not accessible in the current package. + if field.Pkg() != nil && field.Pkg() != pkg.GetTypes() && !field.Exported() { + continue + } + + label := field.Name() + value := formatZeroValue(field.Type(), qfFunc) + fieldSourceCode.WriteString("\n") + fieldSourceCode.WriteString(label) + fieldSourceCode.WriteString(" : ") + fieldSourceCode.WriteString(value) + fieldSourceCode.WriteString(",") + } + + if fieldSourceCode.Len() == 0 { + return nil, nil + } + + fieldSourceCode.WriteString("\n") + + // the range of all text between '<>', inclusive. E.g. {<> ... <}> + mappedRange := newMappedRange(snapshot.View().Session().Cache().FileSet(), m, ecl.cl.Lbrace, ecl.cl.Rbrace+1) + protoRange, err := mappedRange.Range() + if err != nil { + return nil, err + } + // consider formatting from the first character of the line the lbrace is on. + // ToOffset is 1-based + beginOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, 1) + if err != nil { + return nil, err + } + + endOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, int(protoRange.Start.Character)+1) + if err != nil { + return nil, err + } + + // An increment to make sure the lbrace is included in the slice. + endOffset++ + // Append the edits. Then append the closing brace. + var newSourceCode strings.Builder + newSourceCode.Grow(endOffset - beginOffset + fieldSourceCode.Len() + 1) + newSourceCode.WriteString(string(src[beginOffset:endOffset])) + newSourceCode.WriteString(fieldSourceCode.String()) + newSourceCode.WriteString("}") + + buf, err := format.Source([]byte(newSourceCode.String())) + if err != nil { + return nil, err + } + + // it is guaranteed that a left brace exists. + var edit = string(buf[strings.IndexByte(string(buf), '{'):]) + + codeActions = append(codeActions, protocol.CodeAction{ + Title: "Fill struct", + Kind: protocol.RefactorRewrite, + Edit: protocol.WorkspaceEdit{ + DocumentChanges: []protocol.TextDocumentEdit{ + { + TextDocument: protocol.VersionedTextDocumentIdentifier{ + Version: fh.Identity().Version, + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.Identity().URI), + }, + }, + Edits: []protocol.TextEdit{ + { + Range: protoRange, + NewText: edit, + }, + }, + }, + }, + }, + }) + } + + return codeActions, nil +} diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index fd19ad0e783..dc1f803d391 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -89,6 +89,7 @@ func DefaultOptions() Options { protocol.SourceFixAll: true, protocol.SourceOrganizeImports: true, protocol.QuickFix: true, + protocol.RefactorRewrite: true, }, Mod: { protocol.SourceOrganizeImports: true, diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index b0e50d5a574..73d716fa2c3 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -473,6 +473,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { } func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {} +func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) {} func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { _, srcRng, err := spanToRange(r.data, d.Src) diff --git a/internal/lsp/testdata/indirect/summary.txt.golden b/internal/lsp/testdata/indirect/summary.txt.golden index 5c4f74a660b..fdd759352e1 100644 --- a/internal/lsp/testdata/indirect/summary.txt.golden +++ b/internal/lsp/testdata/indirect/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0 SignaturesCount = 0 LinksCount = 0 ImplementationsCount = 0 +RefactorRewriteCount = 0 diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/data/a.go b/internal/lsp/testdata/lsp/primarymod/fillstruct/data/a.go new file mode 100644 index 00000000000..2860da931ea --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/data/a.go @@ -0,0 +1,6 @@ +package data + +type A struct { + ExportedInt int + unexportedInt int +} diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go new file mode 100644 index 00000000000..9bc5c4588e6 --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go @@ -0,0 +1,23 @@ +package fillstruct + +type StructA struct { + unexportedIntField int + ExportedIntField int + MapA map[int]string + Array []int + StructB +} + +type StructA2 struct { + B *StructB +} + +type StructA3 struct { + B StructB +} + +func fill() { + a := StructA{} //@refactorrewrite("}", "Fill struct") + b := StructA2{} //@refactorrewrite("}", "Fill struct") + c := StructA3{} //@refactorrewrite("}", "Fill struct") +} diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden new file mode 100644 index 00000000000..e29f04053a3 --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden @@ -0,0 +1,85 @@ +-- fill_struct_20_15 -- +package fillstruct + +type StructA struct { + unexportedIntField int + ExportedIntField int + MapA map[int]string + Array []int + StructB +} + +type StructA2 struct { + B *StructB +} + +type StructA3 struct { + B StructB +} + +func fill() { + a := StructA{ + unexportedIntField: 0, + ExportedIntField: 0, + MapA: nil, + Array: nil, + StructB: StructB{}, + } //@refactorrewrite("}", "Fill struct") + b := StructA2{} //@refactorrewrite("}", "Fill struct") + c := StructA3{} //@refactorrewrite("}", "Fill struct") +} + +-- fill_struct_21_16 -- +package fillstruct + +type StructA struct { + unexportedIntField int + ExportedIntField int + MapA map[int]string + Array []int + StructB +} + +type StructA2 struct { + B *StructB +} + +type StructA3 struct { + B StructB +} + +func fill() { + a := StructA{} //@refactorrewrite("}", "Fill struct") + b := StructA2{ + B: nil, + } //@refactorrewrite("}", "Fill struct") + c := StructA3{} //@refactorrewrite("}", "Fill struct") +} + +-- fill_struct_22_16 -- +package fillstruct + +type StructA struct { + unexportedIntField int + ExportedIntField int + MapA map[int]string + Array []int + StructB +} + +type StructA2 struct { + B *StructB +} + +type StructA3 struct { + B StructB +} + +func fill() { + a := StructA{} //@refactorrewrite("}", "Fill struct") + b := StructA2{} //@refactorrewrite("}", "Fill struct") + c := StructA3{ + B: StructB{}, + } //@refactorrewrite("}", "Fill struct") +} + diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go new file mode 100644 index 00000000000..0c4c1d88c2c --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go @@ -0,0 +1,15 @@ +package fillstruct + +type StructB struct { + StructC +} + +type StructC struct { + unexportedInt int +} + +func nested() { + c := StructB{ + StructC: StructC{}, //@refactorrewrite("}", "Fill struct") + } +} diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden new file mode 100644 index 00000000000..74c1e6a6245 --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden @@ -0,0 +1,19 @@ +-- fill_struct_nested_13_20 -- +package fillstruct + +type StructB struct { + StructC +} + +type StructC struct { + unexportedInt int +} + +func nested() { + c := StructB{ + StructC: StructC{ + unexportedInt: 0, + }, //@refactorrewrite("}", "Fill struct") + } +} + diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go new file mode 100644 index 00000000000..e7b30e53aa7 --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go @@ -0,0 +1,9 @@ +package fillstruct + +import ( + data "golang.org/x/tools/internal/lsp/fillstruct/data" +) + +func unexported() { + a := data.A{} //@refactorrewrite("}", "Fill struct") +} diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden new file mode 100644 index 00000000000..df33786e6c1 --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden @@ -0,0 +1,13 @@ +-- fill_struct_package_8_14 -- +package fillstruct + +import ( + data "golang.org/x/tools/internal/lsp/fillstruct/data" +) + +func unexported() { + a := data.A{ + ExportedInt: 0, + } //@refactorrewrite("}", "Fill struct") +} + diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden new file mode 100644 index 00000000000..c938b501aeb --- /dev/null +++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden @@ -0,0 +1,13 @@ +-- fill_struct_spaces_10_1 -- +package fillstruct + +type StructB struct { + ExportedIntField int +} + +func spaces() { + b := StructB{ + ExportedIntField: 0, + } +} + diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden index 47c1e7b5b9f..2990fec677f 100644 --- a/internal/lsp/testdata/lsp/summary.txt.golden +++ b/internal/lsp/testdata/lsp/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 2 SignaturesCount = 32 LinksCount = 8 ImplementationsCount = 14 +RefactorRewriteCount = 5 diff --git a/internal/lsp/testdata/missingdep/summary.txt.golden b/internal/lsp/testdata/missingdep/summary.txt.golden index 5c4f74a660b..fdd759352e1 100644 --- a/internal/lsp/testdata/missingdep/summary.txt.golden +++ b/internal/lsp/testdata/missingdep/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0 SignaturesCount = 0 LinksCount = 0 ImplementationsCount = 0 +RefactorRewriteCount = 0 diff --git a/internal/lsp/testdata/missingtwodep/summary.txt.golden b/internal/lsp/testdata/missingtwodep/summary.txt.golden index 96ac4750a8a..a061dc661ab 100644 --- a/internal/lsp/testdata/missingtwodep/summary.txt.golden +++ b/internal/lsp/testdata/missingtwodep/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0 SignaturesCount = 0 LinksCount = 0 ImplementationsCount = 0 +RefactorRewriteCount = 0 diff --git a/internal/lsp/testdata/unused/summary.txt.golden b/internal/lsp/testdata/unused/summary.txt.golden index 5c4f74a660b..fdd759352e1 100644 --- a/internal/lsp/testdata/unused/summary.txt.golden +++ b/internal/lsp/testdata/unused/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0 SignaturesCount = 0 LinksCount = 0 ImplementationsCount = 0 +RefactorRewriteCount = 0 diff --git a/internal/lsp/testdata/upgradedep/summary.txt.golden b/internal/lsp/testdata/upgradedep/summary.txt.golden index 79042cc6c8e..90c373043f9 100644 --- a/internal/lsp/testdata/upgradedep/summary.txt.golden +++ b/internal/lsp/testdata/upgradedep/summary.txt.golden @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0 SignaturesCount = 0 LinksCount = 4 ImplementationsCount = 0 +RefactorRewriteCount = 0 diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 14901fa1e89..d989a50eb94 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -57,6 +57,7 @@ type FoldingRanges []span.Span type Formats []span.Span type Imports []span.Span type SuggestedFixes map[span.Span][]string +type RefactorRewriteActions map[span.Span]string type Definitions map[span.Span]Definition type Implementations map[span.Span][]span.Span type Highlights map[span.Span][]span.Span @@ -87,6 +88,7 @@ type Data struct { Formats Formats Imports Imports SuggestedFixes SuggestedFixes + RefactorRewrite RefactorRewriteActions Definitions Definitions Implementations Implementations Highlights Highlights @@ -140,6 +142,7 @@ type Tests interface { CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp) Link(*testing.T, span.URI, []Link) + RefactorRewrite(*testing.T, span.Span, string) } type Definition struct { @@ -293,6 +296,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { CaseSensitiveWorkspaceSymbols: make(WorkspaceSymbols), Signatures: make(Signatures), Links: make(Links), + RefactorRewrite: make(RefactorRewriteActions), t: t, dir: folder, @@ -334,6 +338,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { Filename: goldFile, Archive: archive, } + fmt.Printf("Trimmed : %s, goldfile: %s, ArchiveCount : %d\n", trimmed, goldFile, len(archive.Files)) } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment { delete(m.Files, fragment) m.Files[trimmed] = operation @@ -417,6 +422,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { "signature": datum.collectSignatures, "link": datum.collectLinks, "suggestedfix": datum.collectSuggestedFixes, + "refactorrewrite": datum.collectRefactorRewrite, }); err != nil { t.Fatal(err) } @@ -594,6 +600,17 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) + t.Run("RefactorRewrite", func(t *testing.T) { + t.Helper() + + for spn, title := range data.RefactorRewrite { + t.Run(SpanName(spn), func(t *testing.T) { + t.Helper() + tests.RefactorRewrite(t, spn, title) + }) + } + }) + t.Run("SuggestedFix", func(t *testing.T) { t.Helper() for spn, actionKinds := range data.SuggestedFixes { @@ -811,6 +828,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures)) fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations)) + fmt.Fprintf(buf, "RefactorRewriteCount = %v\n", len(data.RefactorRewrite)) want := string(data.Golden("summary", summaryFile, func() ([]byte, error) { return buf.Bytes(), nil @@ -886,6 +904,7 @@ func (data *Data) Golden(tag string, target string, update func() ([]byte, error } file.Data = append(contents, '\n') // add trailing \n for txtar golden.Modified = true + } if file == nil { data.t.Fatalf("could not find golden contents %v: %v", fragment, tag) @@ -1019,6 +1038,10 @@ func (data *Data) collectSuggestedFixes(spn span.Span, actionKind string) { data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind) } +func (data *Data) collectRefactorRewrite(spn span.Span, title string) { + data.RefactorRewrite[spn] = title +} + func (data *Data) collectDefinitions(src, target span.Span) { data.Definitions[src] = Definition{ Src: src,