1
0
mirror of https://github.com/golang/go synced 2024-11-05 22:36:10 -07:00
go/internal/lsp/analysis/fillstruct/fillstruct.go
Rebecca Stambler aa12c9ebf5 internal/lsp: handle panics due to line numbers in fillstruct
Change-Id: I90f3fd2daf180705048d494476acac5a213f5fb3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/239751
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Josh Baum <joshbaum@google.com>
2020-06-25 15:39:20 +00:00

358 lines
8.7 KiB
Go

// 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/printer"
"go/token"
"go/types"
"log"
"strings"
"unicode"
"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
}
var name string
switch typ := expr.Type.(type) {
case *ast.Ident:
name = typ.Name
case *ast.SelectorExpr:
name = fmt.Sprintf("%s.%s", typ.X, typ.Sel.Name)
default:
log.Printf("anonymous structs are not yet supported: (%T)", expr.Type)
return
}
// Use a new fileset to build up a token.File for the new composite
// literal. We need one line for foo{, one line for }, and one line for
// each field we're going to set. format.Node only cares about line
// numbers, so we don't need to set columns, and each line can be
// 1 byte long.
fset := token.NewFileSet()
tok := fset.AddFile("", -1, fieldCount+2)
line := 2 // account for 1-based lines and the left brace
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 := populateValue(pass.Fset, file, pass.Pkg, field.Type())
if value == nil {
continue
}
tok.AddLine(line - 1) // add 1 byte per line
if line > tok.LineCount() {
panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct %s", line, tok.LineCount(), name))
}
pos := tok.LineStart(line)
kv := &ast.KeyValueExpr{
Key: &ast.Ident{
NamePos: pos,
Name: field.Name(),
},
Colon: pos,
Value: value,
}
elts = append(elts, kv)
line++
}
// If all of the struct's fields are unexported, we have nothing to do.
if len(elts) == 0 {
return
}
// Add the final line for the right brace. Offset is the number of
// bytes already added plus 1.
tok.AddLine(len(elts) + 1)
line = len(elts) + 2
if line > tok.LineCount() {
panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct %s", line, tok.LineCount(), name))
}
cl := &ast.CompositeLit{
Type: expr.Type,
Lbrace: tok.LineStart(1),
Elts: elts,
Rbrace: tok.LineStart(line),
}
// Print the AST to get the the original source code.
var b bytes.Buffer
if err := printer.Fprint(&b, pass.Fset, file); err != nil {
log.Printf("failed to print original file: %s", err)
return
}
// Find the line on which the composite literal is declared.
split := strings.Split(b.String(), "\n")
lineNumber := pass.Fset.Position(expr.Type.Pos()).Line
firstLine := split[lineNumber-1] // lines are 1-indexed
// Trim the whitespace from the left of the line, and use the index
// to get the amount of whitespace on the left.
trimmed := strings.TrimLeftFunc(firstLine, unicode.IsSpace)
index := strings.Index(firstLine, trimmed)
whitespace := firstLine[:index]
var newExpr bytes.Buffer
if err := format.Node(&newExpr, fset, cl); err != nil {
log.Printf("failed to format %s: %v", cl.Type, err)
return
}
split = strings.Split(newExpr.String(), "\n")
var newText strings.Builder
for i, s := range split {
// Don't add the extra indentation to the first line.
if i != 0 {
newText.WriteString(whitespace)
}
newText.WriteString(s)
if i < len(split)-1 {
newText.WriteByte('\n')
}
}
pass.Report(analysis.Diagnostic{
Pos: expr.Lbrace,
End: expr.Rbrace,
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Fill %s with default values", name),
TextEdits: []analysis.TextEdit{{
Pos: expr.Pos(),
End: expr.End(),
NewText: []byte(newText.String()),
}},
}},
})
})
return nil, nil
}
// populateValue constructs an expression to fill the value of a struct field.
//
// When the type of a struct field is a basic literal or interface, we return
// default values. For other types, such as maps, slices, and channels, we create
// expressions rather than using default values.
//
// The reasoning here is that users will call fillstruct with the intention of
// initializing the struct, in which case setting these fields to nil has no effect.
func populateValue(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
under := typ
if n, ok := typ.(*types.Named); ok {
under = n.Underlying()
}
switch u := under.(type) {
case *types.Basic:
switch {
case u.Info()&types.IsNumeric != 0:
return &ast.BasicLit{Kind: token.INT, Value: "0"}
case u.Info()&types.IsBoolean != 0:
return &ast.Ident{Name: "false"}
case u.Info()&types.IsString != 0:
return &ast.BasicLit{Kind: token.STRING, Value: `""`}
default:
panic("unknown basic type")
}
case *types.Map:
k := analysisinternal.TypeExpr(fset, f, pkg, u.Key())
v := analysisinternal.TypeExpr(fset, f, pkg, u.Elem())
if k == nil || v == nil {
return nil
}
return &ast.CompositeLit{
Type: &ast.MapType{
Key: k,
Value: v,
},
}
case *types.Slice:
s := analysisinternal.TypeExpr(fset, f, pkg, u.Elem())
if s == nil {
return nil
}
return &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: s,
},
}
case *types.Array:
a := analysisinternal.TypeExpr(fset, f, pkg, u.Elem())
if a == nil {
return nil
}
return &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: a,
Len: &ast.BasicLit{
Kind: token.INT, Value: fmt.Sprintf("%v", u.Len())},
},
}
case *types.Chan:
v := analysisinternal.TypeExpr(fset, f, pkg, u.Elem())
if v == nil {
return nil
}
dir := ast.ChanDir(u.Dir())
if u.Dir() == types.SendRecv {
dir = ast.SEND | ast.RECV
}
return &ast.CallExpr{
Fun: ast.NewIdent("make"),
Args: []ast.Expr{
&ast.ChanType{
Dir: dir,
Value: v,
},
},
}
case *types.Struct:
s := analysisinternal.TypeExpr(fset, f, pkg, typ)
if s == nil {
return nil
}
return &ast.CompositeLit{
Type: s,
}
case *types.Signature:
var params []*ast.Field
for i := 0; i < u.Params().Len(); i++ {
p := analysisinternal.TypeExpr(fset, f, pkg, u.Params().At(i).Type())
if p == nil {
return nil
}
params = append(params, &ast.Field{
Type: p,
Names: []*ast.Ident{
{
Name: u.Params().At(i).Name(),
},
},
})
}
var returns []*ast.Field
for i := 0; i < u.Results().Len(); i++ {
r := analysisinternal.TypeExpr(fset, f, pkg, u.Results().At(i).Type())
if r == nil {
return nil
}
returns = append(returns, &ast.Field{
Type: r,
})
}
return &ast.FuncLit{
Type: &ast.FuncType{
Params: &ast.FieldList{
List: params,
},
Results: &ast.FieldList{
List: returns,
},
},
Body: &ast.BlockStmt{},
}
case *types.Pointer:
return &ast.UnaryExpr{
Op: token.AND,
X: populateValue(fset, f, pkg, u.Elem()),
}
case *types.Interface:
return ast.NewIdent("nil")
}
return nil
}