// 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"
	"go/ast"
	"go/format"
	"go/types"
	"strings"

	"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 a 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()

		if typ == nil {
			return
		}

		switch obj := typ.(type) {
		case *types.Struct:
			fieldCount := obj.NumFields()
			if fieldCount == 0 {
				return
			}
			var fieldSourceCode strings.Builder
			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
				}

				label := field.Name()
				value := analysisinternal.ZeroValue(pass.Fset, file, pass.Pkg, field.Type())
				if value == nil {
					continue
				}
				var valBuf bytes.Buffer
				if err := format.Node(&valBuf, pass.Fset, value); err != nil {
					return
				}
				fieldSourceCode.WriteString("\n")
				fieldSourceCode.WriteString(label)
				fieldSourceCode.WriteString(" : ")
				fieldSourceCode.WriteString(valBuf.String())
				fieldSourceCode.WriteString(",")
			}

			if fieldSourceCode.Len() == 0 {
				return
			}

			fieldSourceCode.WriteString("\n")

			buf := []byte(fieldSourceCode.String())

			pass.Report(analysis.Diagnostic{
				Pos: expr.Lbrace,
				End: expr.Rbrace,
				SuggestedFixes: []analysis.SuggestedFix{{
					Message: "Fill struct with empty values",
					TextEdits: []analysis.TextEdit{{
						Pos:     expr.Lbrace + 1,
						End:     expr.Rbrace,
						NewText: buf,
					}},
				}},
			})
		}
	})
	return nil, nil
}
