blob: 23e16625c8acd3b9f003200342d16c14b261156b [file] [log] [blame]
// 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"
"fmt"
"go/ast"
"go/format"
"go/token"
"go/types"
"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 an empty 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()
obj, ok := typ.(*types.Struct)
if !ok {
return
}
fieldCount := obj.NumFields()
// Skip any struct that is already populated or that has no fields.
if fieldCount == 0 || fieldCount == len(expr.Elts) {
return
}
// Don't mutate the existing token.File. Instead, create a copy that we can use to modify
// position information.
original := pass.Fset.File(expr.Lbrace)
fset := token.NewFileSet()
tok := fset.AddFile(original.Name(), -1, original.Size())
pos := token.Pos(1)
var elts []ast.Expr
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
}
value := analysisinternal.ZeroValue(pass.Fset, file, pass.Pkg, field.Type())
if value == nil {
continue
}
pos = nextLinePos(tok, pos)
kv := &ast.KeyValueExpr{
Key: &ast.Ident{
NamePos: pos,
Name: field.Name(),
},
Colon: pos,
Value: value, // 'value' has no position. fomat.Node corrects for AST nodes with no position.
}
elts = append(elts, kv)
}
// If all of the struct's fields are unexported, we have nothing to do.
if len(elts) == 0 {
return
}
cl := ast.CompositeLit{
Type: expr.Type, // Don't adjust the expr.Type's position.
Lbrace: token.Pos(1),
Elts: elts,
Rbrace: nextLinePos(tok, elts[len(elts)-1].Pos()),
}
var buf bytes.Buffer
if err := format.Node(&buf, fset, &cl); err != nil {
return
}
msg := "Fill struct with default values"
if name, ok := expr.Type.(*ast.Ident); ok {
msg = fmt.Sprintf("Fill %s with default values", name)
}
pass.Report(analysis.Diagnostic{
Pos: expr.Lbrace,
End: expr.Rbrace,
SuggestedFixes: []analysis.SuggestedFix{{
Message: msg,
TextEdits: []analysis.TextEdit{{
Pos: expr.Pos(),
End: expr.End(),
NewText: buf.Bytes(),
}},
}},
})
})
return nil, nil
}
func nextLinePos(tok *token.File, pos token.Pos) token.Pos {
line := tok.Line(pos)
if line+1 > tok.LineCount() {
tok.AddLine(tok.Offset(pos) + 1)
}
return tok.LineStart(line + 1)
}