| // 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. |
| // |
| // The analyzer's diagnostic is merely a prompt. |
| // The actual fix is created by a separate direct call from gopls to |
| // the SuggestedFixes function. |
| // Tests of Analyzer.Run can be found in ./testdata/src. |
| // Tests of the SuggestedFixes logic live in ../../testdata/fillstruct. |
| package fillstruct |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/format" |
| "go/printer" |
| "go/token" |
| "go/types" |
| "slices" |
| "strings" |
| "unicode" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/ast/inspector" |
| "golang.org/x/tools/gopls/internal/analysis/fillreturns" |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/cache/parsego" |
| "golang.org/x/tools/gopls/internal/fuzzy" |
| "golang.org/x/tools/gopls/internal/util/cursorutil" |
| "golang.org/x/tools/gopls/internal/util/safetoken" |
| "golang.org/x/tools/internal/astutil" |
| "golang.org/x/tools/internal/typeparams" |
| "golang.org/x/tools/internal/typesinternal" |
| "golang.org/x/tools/internal/versions" |
| ) |
| |
| // Diagnose computes a diagnostic for the enclosing struct literal enclosing |
| // the provided start and end position of curFile. |
| // |
| // If the target struct is already fully populated, no diagnostic is reported. |
| // |
| // The diagnostic contains a lazy fix; the actual patch is computed |
| // (via the ApplyFix command) by a call to [SuggestedFix]. |
| func Diagnose(curFile inspector.Cursor, start, end token.Pos, pkg *types.Package, info *types.Info) (diags []analysis.Diagnostic) { |
| cur, _, _, _ := astutil.Select(curFile, start, end) |
| |
| // Direct reference to embedded fields in struct literals requires Go 1.27+. |
| var supportsEmbedFields bool |
| if v := info.FileVersions[curFile.Node().(*ast.File)]; v != "" { |
| supportsEmbedFields = versions.AtLeast(v, versions.Go1_27) |
| } |
| |
| var lits []*ast.CompositeLit |
| for c := range cur.Enclosing((*ast.CompositeLit)(nil)) { |
| lits = append(lits, c.Node().(*ast.CompositeLit)) |
| } |
| for c := range cur.Preorder((*ast.CompositeLit)(nil)) { |
| expr := c.Node().(*ast.CompositeLit) |
| // Avoid double-counting when cur.Node() is itself a [ast.CompositeLit]. |
| if expr == cur.Node() { |
| continue |
| } |
| if expr.Pos() <= end && expr.End() >= start { |
| lits = append(lits, expr) |
| } |
| } |
| |
| nextComp: |
| for _, expr := range lits { |
| typ := info.TypeOf(expr) |
| if typ == nil { |
| continue |
| } |
| |
| // Find reference to the type declaration of the struct being initialized. |
| typ = typeparams.Deref(typ) |
| tStruct, ok := typeparams.CoreType(typ).(*types.Struct) |
| if !ok { |
| continue |
| } |
| |
| // Inv: typ is the possibly-named struct type. |
| |
| // fillableFields returns the number of fields in the struct that are |
| // accessible and thus can be filled in the current package. |
| fillableFields := func(t *types.Struct) (count int) { |
| for field := range t.Fields() { |
| if field.Pkg() == pkg || field.Exported() { |
| count++ |
| } |
| } |
| return count |
| } |
| |
| // fieldCount tracks the maximum number of fillable fields under the minimum |
| // required expansion of embedded structs. |
| // |
| // It starts as the number of direct fields of the struct. |
| // |
| // Example structure: |
| // A |
| // ├── A1 |
| // ├── A2 |
| // └── B (embedded) |
| // ├── B1 (promoted to A) |
| // ├── B2 |
| // └── C (embedded) |
| // ├── C1 |
| // └── C2 |
| // |
| // When a promoted field is filled (e.g., B1 in A{B1: 1}), we expand |
| // fieldCount to include the fields of that embedded struct (B1, B2, C), |
| // while removing the embedded struct B itself from the count. |
| // |
| // This means we need to fill all fields at B's level (B1, B2, C) and |
| // its parent levels (A1, A2), but we don't need to fill B anymore. C |
| // will be filled as C: C{} unless its own fields are accessed. |
| fieldCount := fillableFields(tStruct) |
| |
| var seen map[*types.Var]bool |
| |
| nextElem: |
| for _, el := range expr.Elts { |
| kv, ok := el.(*ast.KeyValueExpr) |
| if !ok { |
| continue nextComp |
| } |
| |
| key, ok := kv.Key.(*ast.Ident) |
| if !ok { |
| continue |
| } |
| |
| seln, ok := types.LookupSelection(typ, true, pkg, key.Name) |
| if !ok { |
| continue |
| } |
| |
| if len(seln.Index()) > 1 && !supportsEmbedFields { |
| continue |
| } |
| |
| length := len(seln.Index()) |
| if length == 0 { |
| continue |
| } |
| |
| var ( |
| fields = make([]*types.Var, length-1) |
| typs = make([]*types.Struct, length-1) |
| ) |
| |
| i := 0 |
| for field := range typesinternal.ImplicitFieldSelections(seln) { |
| t := field.Type() |
| ptr, isPtr := t.Underlying().(*types.Pointer) |
| if isPtr { |
| t = ptr.Elem() |
| } |
| structType, ok := t.Underlying().(*types.Struct) |
| if !ok { |
| continue nextElem |
| } |
| |
| fields[i] = field |
| typs[i] = structType |
| i++ |
| } |
| |
| for i, field := range fields { |
| if !seen[field] { |
| if seen == nil { |
| seen = make(map[*types.Var]bool) |
| } |
| seen[field] = true |
| fieldCount += fillableFields(typs[i]) - 1 |
| } |
| } |
| } |
| |
| // Skip any struct that is already populated or that has no fillable fields. |
| if fieldCount == 0 || fieldCount == len(expr.Elts) { |
| continue |
| } |
| |
| // Derive a name for the struct type. |
| var name string |
| if typ != tStruct { |
| // named struct type (e.g. pkg.S[T]) |
| name = types.TypeString(typ, typesinternal.NameRelativeTo(pkg)) |
| } else { |
| // anonymous struct type |
| var buf strings.Builder |
| buf.WriteString("anonymous struct{ ") |
| |
| var printedCount int |
| |
| for field := range tStruct.Fields() { |
| if field.Pkg() == pkg || field.Exported() { |
| if buf.Len() > 38 || printedCount > 3 { |
| buf.WriteString("...") |
| break |
| } |
| fmt.Fprintf(&buf, "%s: %s; ", field.Name(), field.Type().String()) |
| printedCount++ |
| } |
| } |
| |
| buf.WriteString(" }") |
| name = buf.String() |
| } |
| |
| diags = append(diags, analysis.Diagnostic{ |
| Message: fmt.Sprintf("%s literal has missing fields", name), |
| Pos: expr.Pos(), |
| End: expr.End(), |
| Category: FixCategory, |
| SuggestedFixes: []analysis.SuggestedFix{{ |
| Message: fmt.Sprintf("Fill %s", name), |
| // No TextEdits => computed later by gopls. |
| }}, |
| }) |
| } |
| |
| return diags |
| } |
| |
| const FixCategory = "fillstruct" // recognized by gopls ApplyFix |
| |
| // SuggestedFix computes the suggested fix for the kinds of |
| // diagnostics produced by the Analyzer above. |
| func SuggestedFix(cpkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { |
| var ( |
| file = pgf.File |
| fset = cpkg.FileSet() |
| pkg = cpkg.Types() |
| info = cpkg.TypesInfo() |
| pos = start // don't use end |
| ) |
| cur, ok := pgf.Cursor().FindByPos(pos, pos) |
| if !ok { |
| return nil, nil, fmt.Errorf("no enclosing ast.Node") |
| } |
| expr, _ := cursorutil.FirstEnclosing[*ast.CompositeLit](cur) |
| |
| // newElts accumulates the newly generated field element AST nodes. |
| newElts, err := populateMissingFields(info, pkg, file, expr, pos) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| // If we failed to generate any new fields to fill, we have nothing to do. |
| if len(newElts) == 0 { |
| return nil, nil, fmt.Errorf("no elements to fill") |
| } |
| |
| // Find the line on which the composite literal is declared. |
| split := bytes.Split(pgf.Src, []byte("\n")) |
| lineNumber := safetoken.StartPosition(fset, expr.Lbrace).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 := bytes.TrimLeftFunc(firstLine, unicode.IsSpace) |
| index := bytes.Index(firstLine, trimmed) |
| whitespace := firstLine[:index] |
| |
| var buf bytes.Buffer |
| buf.WriteString("_{\n") |
| fcmap := ast.NewCommentMap(fset, file, file.Comments) |
| comments := fcmap.Filter(expr).Comments() // comments inside the expr, in source order |
| for _, elt := range slices.Concat(expr.Elts, newElts) { |
| // Print comments before the current elt |
| for len(comments) > 0 && comments[0].Pos() < elt.Pos() { |
| for _, co := range comments[0].List { |
| fmt.Fprintln(&buf, co.Text) |
| } |
| comments = comments[1:] |
| } |
| |
| // Print the current elt with comments |
| eltcomments := fcmap.Filter(elt).Comments() |
| if err := format.Node(&buf, fset, &printer.CommentedNode{Node: elt, Comments: eltcomments}); err != nil { |
| return nil, nil, err |
| } |
| buf.WriteString(",") |
| |
| // Prune comments up to the end of the elt |
| for len(comments) > 0 && comments[0].Pos() < elt.End() { |
| comments = comments[1:] |
| } |
| |
| // Write comments associated with the current elt that appear after it |
| // printer.CommentedNode only prints comments inside the elt. |
| for _, cg := range eltcomments { |
| for _, co := range cg.List { |
| if co.Pos() >= elt.End() { |
| fmt.Fprintln(&buf, co.Text) |
| if len(comments) > 0 { |
| comments = comments[1:] |
| } |
| } |
| } |
| } |
| buf.WriteString("\n") |
| } |
| buf.WriteString("}") |
| formatted, err := format.Source(buf.Bytes()) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| sug := indent(formatted, whitespace) |
| // Remove _ |
| idx := bytes.IndexByte(sug, '{') // cannot fail |
| sug = sug[idx:] |
| |
| return fset, &analysis.SuggestedFix{ |
| TextEdits: []analysis.TextEdit{ |
| { |
| Pos: expr.Lbrace, |
| End: expr.Rbrace + token.Pos(len("}")), |
| NewText: sug, |
| }, |
| }, |
| }, nil |
| } |
| |
| // populateMissingFields returns a slice of ast.Expr (specifically *ast.KeyValueExpr) |
| // representing the populated missing fields of tStruct that can be filled. |
| // |
| // It traverses the struct fields in depth-first order (DFS), flattening embedded |
| // structs where the user has partially initialized their sub-fields, and attempts |
| // to generate a matching local variable or zero-value for each missing field. |
| // Fields that cannot be populated are skipped, returning only the ones that can |
| // be successfully populated. |
| func populateMissingFields(info *types.Info, pkg *types.Package, file *ast.File, expr *ast.CompositeLit, pos token.Pos) ([]ast.Expr, error) { |
| typ := info.TypeOf(expr) |
| if typ == nil { |
| return nil, fmt.Errorf("no composite literal") |
| } |
| |
| // Find reference to the type declaration of the struct being initialized. |
| typ = typeparams.Deref(typ) |
| tStruct, ok := typ.Underlying().(*types.Struct) |
| if !ok { |
| return nil, fmt.Errorf("%s is not a (pointer to) struct type", |
| types.TypeString(typ, typesinternal.NameRelativeTo(pkg))) |
| } |
| |
| // Inv: typ is the possibly-named struct type. |
| |
| // Direct reference to embedded fields in struct literals requires Go 1.27+. |
| var supportsEmbedFields bool |
| if v := info.FileVersions[file]; v != "" { |
| supportsEmbedFields = versions.AtLeast(v, versions.Go1_27) |
| } |
| |
| // explicit records whether each encountered field is explicitly initialized. |
| // Each non-last field in a selection path is accessed implicitly (false), |
| // and the last field is accessed explicitly (true). |
| // Fields not present in this map are completely missing and need to be populated. |
| explicit := make(map[*types.Var]bool) |
| for _, elem := range expr.Elts { |
| kv, ok := elem.(*ast.KeyValueExpr) |
| if !ok { |
| return nil, fmt.Errorf("cannot fill struct literal containing unkeyed elements") |
| } |
| |
| key, ok := kv.Key.(*ast.Ident) |
| if !ok { |
| continue |
| } |
| |
| seln, ok := types.LookupSelection(tStruct, true, pkg, key.Name) |
| if !ok { |
| continue |
| } |
| |
| if len(seln.Index()) > 1 && !supportsEmbedFields { |
| continue |
| } |
| |
| field, ok := seln.Obj().(*types.Var) |
| if !ok { |
| continue |
| } |
| |
| isExplicit, ok := explicit[field] |
| if ok && !isExplicit { |
| return nil, fmt.Errorf("cannot fill both %q and its subfields", field.Name()) |
| } |
| explicit[field] = true // last field is explicit |
| |
| for field := range typesinternal.ImplicitFieldSelections(seln) { |
| if explicit[field] { |
| return nil, fmt.Errorf("cannot fill both %q and its subfields", field.Name()) |
| } |
| explicit[field] = false // all the others are implicit |
| } |
| } |
| |
| // Collect the final list of fields to be filled. Traverse the struct fields |
| // in depth-first order, flattening embedded fields that are marked for |
| // expansion because the user has partially initialized their sub-fields. |
| var fields []*types.Var |
| { |
| var addFields func(*types.Struct) |
| addFields = func(tStruct *types.Struct) { |
| for field := range tStruct.Fields() { |
| if field.Pkg() != pkg && !field.Exported() { |
| continue |
| } |
| |
| isExplicit, ok := explicit[field] |
| if !ok { |
| fields = append(fields, field) |
| continue |
| } |
| |
| if !isExplicit { |
| tInner, ok := typeparams.Deref(field.Type()).Underlying().(*types.Struct) |
| if !ok { |
| continue // can't happen |
| } |
| addFields(tInner) |
| } |
| } |
| } |
| addFields(tStruct) |
| } |
| |
| typs := make([]types.Type, len(fields)) |
| for i, f := range fields { |
| typs[i] = f.Type() |
| } |
| |
| var newElts []ast.Expr |
| matches := fillreturns.MatchingIdents(typs, file, pos, info, pkg) |
| qual := typesinternal.FileQualifier(file, pkg) |
| |
| // Iterate in the order fields were discovered to ensure deterministic |
| // output and match the struct definition order. |
| for _, field := range fields { |
| // TODO(hxjiang): Provide a separate quick-fix option to fill the |
| // struct using nested composite literals, ensuring we always have a |
| // valid suggestion even if shadowing prevents flattening. |
| if obj, _, _ := types.LookupFieldOrMethod(tStruct, true, pkg, field.Name()); obj != field { |
| return nil, fmt.Errorf("field %s shadowed", field.Name()) |
| } |
| |
| kv := &ast.KeyValueExpr{ |
| Key: ast.NewIdent(field.Name()), |
| } |
| |
| names, ok := matches[field.Type()] |
| if !ok { |
| return nil, fmt.Errorf("invalid struct field type: %v", field.Type()) |
| } |
| |
| // Find the name most similar to the field name. |
| // If no name matches the pattern, generate a zero value. |
| // NOTE: We currently match on the name of the field key rather than the field type. |
| if best := fuzzy.BestMatch(field.Name(), names); best != "" { |
| kv.Value = ast.NewIdent(best) |
| } else if expr, isValid := populateValue(field.Type(), qual); isValid { |
| kv.Value = expr |
| } else { |
| continue |
| } |
| |
| newElts = append(newElts, kv) |
| } |
| return newElts, nil |
| } |
| |
| // indent works line by line through str, indenting (prefixing) each line with |
| // ind. |
| func indent(str, ind []byte) []byte { |
| split := bytes.Split(str, []byte("\n")) |
| newText := bytes.NewBuffer(nil) |
| for i, s := range split { |
| if len(s) == 0 { |
| continue |
| } |
| // Don't add the extra indentation to the first line. |
| if i != 0 { |
| newText.Write(ind) |
| } |
| newText.Write(s) |
| if i < len(split)-1 { |
| newText.WriteByte('\n') |
| } |
| } |
| return newText.Bytes() |
| } |
| |
| // 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 |
| // empty expressions such as []T{} or make(chan T) 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. |
| // |
| // If the input contains an invalid type, populateValue may panic or return |
| // expression that may not compile. |
| func populateValue(typ types.Type, qual types.Qualifier) (_ ast.Expr, isValid bool) { |
| switch t := typ.(type) { |
| case *types.TypeParam, *types.Interface, *types.Struct, *types.Basic: |
| return typesinternal.ZeroExpr(t, qual) |
| |
| case *types.Alias, *types.Named: |
| switch t.Underlying().(type) { |
| // Avoid typesinternal.ZeroExpr here as we don't want to return nil. |
| case *types.Map, *types.Slice: |
| return &ast.CompositeLit{ |
| Type: typesinternal.TypeExpr(t, qual), |
| }, true |
| default: |
| return typesinternal.ZeroExpr(t, qual) |
| } |
| |
| // Avoid typesinternal.ZeroExpr here as we don't want to return nil. |
| case *types.Map, *types.Slice: |
| return &ast.CompositeLit{ |
| Type: typesinternal.TypeExpr(t, qual), |
| }, true |
| |
| case *types.Array: |
| return &ast.CompositeLit{ |
| Type: &ast.ArrayType{ |
| Elt: typesinternal.TypeExpr(t.Elem(), qual), |
| Len: &ast.BasicLit{ |
| Kind: token.INT, Value: fmt.Sprintf("%v", t.Len()), |
| }, |
| }, |
| }, true |
| |
| case *types.Chan: |
| dir := ast.ChanDir(t.Dir()) |
| if t.Dir() == types.SendRecv { |
| dir = ast.SEND | ast.RECV |
| } |
| return &ast.CallExpr{ |
| Fun: ast.NewIdent("make"), |
| Args: []ast.Expr{ |
| &ast.ChanType{ |
| Dir: dir, |
| Value: typesinternal.TypeExpr(t.Elem(), qual), |
| }, |
| }, |
| }, true |
| |
| case *types.Signature: |
| return &ast.FuncLit{ |
| Type: typesinternal.TypeExpr(t, qual).(*ast.FuncType), |
| // The body of the function literal contains a panic statement to |
| // avoid type errors. |
| Body: &ast.BlockStmt{ |
| List: []ast.Stmt{ |
| &ast.ExprStmt{ |
| X: &ast.CallExpr{ |
| Fun: ast.NewIdent("panic"), |
| Args: []ast.Expr{ |
| &ast.BasicLit{ |
| Kind: token.STRING, |
| Value: `"TODO"`, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, true |
| |
| case *types.Pointer: |
| switch tt := types.Unalias(t.Elem()).(type) { |
| case *types.Basic: |
| return &ast.CallExpr{ |
| Fun: &ast.Ident{ |
| Name: "new", |
| }, |
| Args: []ast.Expr{ |
| &ast.Ident{ |
| Name: t.Elem().String(), |
| }, |
| }, |
| }, true |
| // Pointer to type parameter should return new(T) instead of &*new(T). |
| case *types.TypeParam: |
| return &ast.CallExpr{ |
| Fun: &ast.Ident{ |
| Name: "new", |
| }, |
| Args: []ast.Expr{ |
| &ast.Ident{ |
| Name: tt.Obj().Name(), |
| }, |
| }, |
| }, true |
| default: |
| // TODO(hxjiang): & prefix only works if populateValue returns a |
| // composite literal T{} or the expression new(T). |
| expr, isValid := populateValue(t.Elem(), qual) |
| return &ast.UnaryExpr{ |
| Op: token.AND, |
| X: expr, |
| }, isValid |
| } |
| } |
| return nil, false |
| } |