diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/internal/lsp/analysis/fillstruct/fillstruct.go new file mode 100644 index 0000000000..b23a05f9cf --- /dev/null +++ b/internal/lsp/analysis/fillstruct/fillstruct.go @@ -0,0 +1,145 @@ +// 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 fillstruct defines an Analyzer that automatically +// fills in a struct declaration with zero value elements for each field. +package fillstruct + +import ( + "bytes" + "go/ast" + "go/format" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysisinternal" +) + +const Doc = `suggested input for incomplete struct initializations + +This analyzer provides the appropriate zero values for all +uninitialized fields of a struct. For example, given the following struct: + type Foo struct { + ID int64 + Name string + } +the initialization + var _ = Foo{} +will turn into + var _ = Foo{ + ID: 0, + Name: "", + } +` + +var Analyzer = &analysis.Analyzer{ + Name: "fillstruct", + Doc: Doc, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + RunDespiteErrors: true, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} + inspect.Preorder(nodeFilter, func(n ast.Node) { + info := pass.TypesInfo + if info == nil { + return + } + expr := n.(*ast.CompositeLit) + + // TODO: Handle partially-filled structs as well. + if len(expr.Elts) != 0 { + return + } + + var file *ast.File + for _, f := range pass.Files { + if f.Pos() <= expr.Pos() && expr.Pos() <= f.End() { + file = f + break + } + } + if file == nil { + return + } + + typ := info.TypeOf(expr) + if typ == nil { + return + } + + // Find reference to the type declaration of the struct being initialized. + for { + p, ok := typ.Underlying().(*types.Pointer) + if !ok { + break + } + typ = p.Elem() + } + typ = typ.Underlying() + + if typ == nil { + return + } + + switch obj := typ.(type) { + case *types.Struct: + fieldCount := obj.NumFields() + if fieldCount == 0 { + return + } + 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() != pass.Pkg && !field.Exported() { + continue + } + + label := field.Name() + value := analysisinternal.ZeroValue(pass.Fset, file, pass.Pkg, field.Type()) + if value == nil { + continue + } + var valBuf bytes.Buffer + if err := format.Node(&valBuf, pass.Fset, value); err != nil { + return + } + fieldSourceCode.WriteString("\n") + fieldSourceCode.WriteString(label) + fieldSourceCode.WriteString(" : ") + fieldSourceCode.WriteString(valBuf.String()) + fieldSourceCode.WriteString(",") + } + + if fieldSourceCode.Len() == 0 { + return + } + + fieldSourceCode.WriteString("\n") + + buf := []byte(fieldSourceCode.String()) + + pass.Report(analysis.Diagnostic{ + Pos: expr.Lbrace, + End: expr.Rbrace, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Fill struct with empty values", + TextEdits: []analysis.TextEdit{{ + Pos: expr.Lbrace + 1, + End: expr.Rbrace, + NewText: buf, + }}, + }}, + }) + } + }) + return nil, nil +} diff --git a/internal/lsp/analysis/fillstruct/fillstruct_test.go b/internal/lsp/analysis/fillstruct/fillstruct_test.go new file mode 100644 index 0000000000..cdd1bdea20 --- /dev/null +++ b/internal/lsp/analysis/fillstruct/fillstruct_test.go @@ -0,0 +1,17 @@ +// 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 fillstruct_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/internal/lsp/analysis/fillstruct" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, fillstruct.Analyzer, "a") +} diff --git a/internal/lsp/analysis/fillstruct/testdata/src/a/a.go b/internal/lsp/analysis/fillstruct/testdata/src/a/a.go new file mode 100644 index 0000000000..447ed15e3b --- /dev/null +++ b/internal/lsp/analysis/fillstruct/testdata/src/a/a.go @@ -0,0 +1,39 @@ +// 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 fillstruct + +import ( + data "b" +) + +type emptyStruct struct{} + +var _ = emptyStruct{} + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} // want "" + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} // want "" + +var _ = twoArgStruct{ + bar: "bar", +} + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} // want "" + +var _ = data.B{} // want "" diff --git a/internal/lsp/analysis/fillstruct/testdata/src/a/a.go.golden b/internal/lsp/analysis/fillstruct/testdata/src/a/a.go.golden new file mode 100644 index 0000000000..3fc564b372 --- /dev/null +++ b/internal/lsp/analysis/fillstruct/testdata/src/a/a.go.golden @@ -0,0 +1,49 @@ +// 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 fillstruct + +import ( + data "b" +) + +type emptyStruct struct{} + +var _ = emptyStruct{} + +type basicStruct struct { + foo int +} + +var _ = basicStruct{ + foo: 0, +} // want "" + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{ + foo: 0, + bar: "", +} // want "" + +var _ = twoArgStruct{ + bar: "bar", +} + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, +} // want "" + +var _ = data.B{ + ExportedInt: 0, +} // want "" diff --git a/internal/lsp/analysis/fillstruct/testdata/src/b/b.go b/internal/lsp/analysis/fillstruct/testdata/src/b/b.go new file mode 100644 index 0000000000..a4b394605a --- /dev/null +++ b/internal/lsp/analysis/fillstruct/testdata/src/b/b.go @@ -0,0 +1,6 @@ +package fillstruct + +type B struct { + ExportedInt int + unexportedInt int +} diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 11eaf0518e..13aec4ccd2 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -38,6 +38,7 @@ import ( "golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/internal/lsp/analysis/fillreturns" + "golang.org/x/tools/internal/lsp/analysis/fillstruct" "golang.org/x/tools/internal/lsp/analysis/nonewvars" "golang.org/x/tools/internal/lsp/analysis/noresultvalues" "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" @@ -130,11 +131,12 @@ func DefaultOptions() Options { TempModfile: true, }, Hooks: Hooks{ - ComputeEdits: myers.ComputeEdits, - URLRegexp: regexp.MustCompile(`(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?`), - DefaultAnalyzers: defaultAnalyzers(), - TypeErrorAnalyzers: typeErrorAnalyzers(), - GoDiff: true, + ComputeEdits: myers.ComputeEdits, + URLRegexp: regexp.MustCompile(`(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?`), + DefaultAnalyzers: defaultAnalyzers(), + TypeErrorAnalyzers: typeErrorAnalyzers(), + ConvenienceAnalyzers: convenienceAnalyzers(), + GoDiff: true, }, } } @@ -243,11 +245,12 @@ type completionOptions struct { // Hooks contains configuration that is provided to the Gopls command by the // main package. type Hooks struct { - GoDiff bool - ComputeEdits diff.ComputeEdits - URLRegexp *regexp.Regexp - DefaultAnalyzers map[string]Analyzer - TypeErrorAnalyzers map[string]Analyzer + GoDiff bool + ComputeEdits diff.ComputeEdits + URLRegexp *regexp.Regexp + DefaultAnalyzers map[string]Analyzer + TypeErrorAnalyzers map[string]Analyzer + ConvenienceAnalyzers map[string]Analyzer } func (o Options) AddDefaultAnalyzer(a *analysis.Analyzer) { @@ -625,6 +628,15 @@ func typeErrorAnalyzers() map[string]Analyzer { } } +func convenienceAnalyzers() map[string]Analyzer { + return map[string]Analyzer{ + fillstruct.Analyzer.Name: { + Analyzer: fillstruct.Analyzer, + enabled: true, + }, + } +} + func defaultAnalyzers() map[string]Analyzer { return map[string]Analyzer{ // The traditional vet suite: