| // 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) |
| } |