Skip to content

Commit

Permalink
go/analysis: add suggested fix for unkeyed composite literals
Browse files Browse the repository at this point in the history
Include a suggested fix with the diagnostic for unkeyed composite
literals. This suggested fix will add the name of each of the
fields.

For golang/go#53062

Change-Id: I0c33191ff3cf66c95a9a055848274cc2b0c38224
Reviewed-on: https://go-review.googlesource.com/c/tools/+/414674
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
  • Loading branch information
suzmue committed Jun 30, 2022
1 parent 8865782 commit 8314b7a
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 12 deletions.
41 changes: 35 additions & 6 deletions go/analysis/passes/composite/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package composite

import (
"fmt"
"go/ast"
"go/types"
"strings"
Expand Down Expand Up @@ -83,7 +84,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
for _, typ := range structuralTypes {
under := deref(typ.Underlying())
if _, ok := under.(*types.Struct); !ok {
strct, ok := under.(*types.Struct)
if !ok {
// skip non-struct composite literals
continue
}
Expand All @@ -92,20 +94,47 @@ func run(pass *analysis.Pass) (interface{}, error) {
continue
}

// check if the CompositeLit contains an unkeyed field
// check if the struct contains an unkeyed field
allKeyValue := true
for _, e := range cl.Elts {
var suggestedFixAvailable = len(cl.Elts) == strct.NumFields()
var missingKeys []analysis.TextEdit
for i, e := range cl.Elts {
if _, ok := e.(*ast.KeyValueExpr); !ok {
allKeyValue = false
break
if i >= strct.NumFields() {
break
}
field := strct.Field(i)
if !field.Exported() {
// Adding unexported field names for structs not defined
// locally will not work.
suggestedFixAvailable = false
break
}
missingKeys = append(missingKeys, analysis.TextEdit{
Pos: e.Pos(),
End: e.Pos(),
NewText: []byte(fmt.Sprintf("%s: ", field.Name())),
})
}
}
if allKeyValue {
// all the composite literal fields are keyed
// all the struct fields are keyed
continue
}

pass.ReportRangef(cl, "%s composite literal uses unkeyed fields", typeName)
diag := analysis.Diagnostic{
Pos: cl.Pos(),
End: cl.End(),
Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
}
if suggestedFixAvailable {
diag.SuggestedFixes = []analysis.SuggestedFix{{
Message: "Add field names to struct literal",
TextEdits: missingKeys,
}}
}
pass.Report(diag)
return
}
})
Expand Down
2 changes: 1 addition & 1 deletion go/analysis/passes/composite/composite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ func Test(t *testing.T) {
if typeparams.Enabled {
pkgs = append(pkgs, "typeparams")
}
analysistest.Run(t, testdata, composite.Analyzer, pkgs...)
analysistest.RunWithSuggestedFixes(t, testdata, composite.Analyzer, pkgs...)
}
17 changes: 17 additions & 0 deletions go/analysis/passes/composite/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"go/scanner"
"go/token"
"image"
"sync"
"unicode"
)

Expand Down Expand Up @@ -79,6 +80,18 @@ var badStructLiteral = flag.Flag{ // want "unkeyed fields"
nil, // Value
"DefValue",
}
var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields"
"Name",
"Usage",
nil, // Value
"DefValue",
"Extra Field",
}
var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields"
"Name",
"Usage",
nil, // Value
}

var delta [3]rune

Expand All @@ -100,6 +113,10 @@ var badScannerErrorList = scanner.ErrorList{
&scanner.Error{token.Position{}, "foobar"}, // want "unkeyed fields"
}

// sync.Mutex has unexported fields. We expect a diagnostic but no
// suggested fix.
var mu = sync.Mutex{0, 0} // want "unkeyed fields"

// Check whitelisted structs: if vet is run with --compositewhitelist=false,
// this line triggers an error.
var whitelistedPoint = image.Point{1, 2}
Expand Down
144 changes: 144 additions & 0 deletions go/analysis/passes/composite/testdata/src/a/a.go.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2012 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.

// This file contains the test for untagged struct literals.

package a

import (
"flag"
"go/scanner"
"go/token"
"image"
"sync"
"unicode"
)

var Okay1 = []string{
"Name",
"Usage",
"DefValue",
}

var Okay2 = map[string]bool{
"Name": true,
"Usage": true,
"DefValue": true,
}

var Okay3 = struct {
X string
Y string
Z string
}{
"Name",
"Usage",
"DefValue",
}

var Okay4 = []struct {
A int
B int
}{
{1, 2},
{3, 4},
}

type MyStruct struct {
X string
Y string
Z string
}

var Okay5 = &MyStruct{
"Name",
"Usage",
"DefValue",
}

var Okay6 = []MyStruct{
{"foo", "bar", "baz"},
{"aa", "bb", "cc"},
}

var Okay7 = []*MyStruct{
{"foo", "bar", "baz"},
{"aa", "bb", "cc"},
}

// Testing is awkward because we need to reference things from a separate package
// to trigger the warnings.

var goodStructLiteral = flag.Flag{
Name: "Name",
Usage: "Usage",
}
var badStructLiteral = flag.Flag{ // want "unkeyed fields"
Name: "Name",
Usage: "Usage",
Value: nil, // Value
DefValue: "DefValue",
}
var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields"
"Name",
"Usage",
nil, // Value
"DefValue",
"Extra Field",
}
var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields"
"Name",
"Usage",
nil, // Value
}

var delta [3]rune

// SpecialCase is a named slice of CaseRange to test issue 9171.
var goodNamedSliceLiteral = unicode.SpecialCase{
{Lo: 1, Hi: 2, Delta: delta},
unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta},
}
var badNamedSliceLiteral = unicode.SpecialCase{
{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields"
unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields"
}

// ErrorList is a named slice, so no warnings should be emitted.
var goodScannerErrorList = scanner.ErrorList{
&scanner.Error{Msg: "foobar"},
}
var badScannerErrorList = scanner.ErrorList{
&scanner.Error{Pos: token.Position{}, Msg: "foobar"}, // want "unkeyed fields"
}

// sync.Mutex has unexported fields. We expect a diagnostic but no
// suggested fix.
var mu = sync.Mutex{0, 0} // want "unkeyed fields"

// Check whitelisted structs: if vet is run with --compositewhitelist=false,
// this line triggers an error.
var whitelistedPoint = image.Point{1, 2}

// Do not check type from unknown package.
// See issue 15408.
var unknownPkgVar = unicode.NoSuchType{"foo", "bar"}

// A named pointer slice of CaseRange to test issue 23539. In
// particular, we're interested in how some slice elements omit their
// type.
var goodNamedPointerSliceLiteral = []*unicode.CaseRange{
{Lo: 1, Hi: 2},
&unicode.CaseRange{Lo: 1, Hi: 2},
}
var badNamedPointerSliceLiteral = []*unicode.CaseRange{
{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields"
&unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields"
}

// unicode.Range16 is whitelisted, so there'll be no vet error
var range16 = unicode.Range16{0xfdd0, 0xfdef, 1}

// unicode.Range32 is whitelisted, so there'll be no vet error
var range32 = unicode.Range32{0x1fffe, 0x1ffff, 1}
16 changes: 16 additions & 0 deletions go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2022 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.

//go:build go1.18
// +build go1.18

package a

import "testing"

var fuzzTargets = []testing.InternalFuzzTarget{
{"Fuzz", Fuzz},
}

func Fuzz(f *testing.F) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package typeparams

import "typeparams/lib"

type localStruct struct { F int }
type localStruct struct{ F int }

func F[
T1 ~struct{ f int },
Expand All @@ -20,8 +20,8 @@ func F[
_ = T1{2}
_ = T2a{2}
_ = T2b{2} // want "unkeyed fields"
_ = T3{1,2}
_ = T4{1,2}
_ = T5{1:2}
_ = T6{1:2}
_ = T3{1, 2}
_ = T4{1, 2}
_ = T5{1: 2}
_ = T6{1: 2}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2021 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 typeparams

import "typeparams/lib"

type localStruct struct{ F int }

func F[
T1 ~struct{ f int },
T2a localStruct,
T2b lib.Struct,
T3 ~[]int,
T4 lib.Slice,
T5 ~map[int]int,
T6 lib.Map,
]() {
_ = T1{2}
_ = T2a{2}
_ = T2b{F: 2} // want "unkeyed fields"
_ = T3{1, 2}
_ = T4{1, 2}
_ = T5{1: 2}
_ = T6{1: 2}
}

0 comments on commit 8314b7a

Please sign in to comment.