// 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 simplifycompositelit defines an Analyzer that simplifies composite literals.
// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go
// https://golang.org/cmd/gofmt/#hdr-The_simplify_command
package simplifycompositelit

import (
	_ "embed"
	"fmt"
	"go/ast"
	"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/analysis/analyzerutil"
	"golang.org/x/tools/internal/astutil"
)

//go:embed doc.go
var doc string

var Analyzer = &analysis.Analyzer{
	Name:     "simplifycompositelit",
	Doc:      analyzerutil.MustExtractDoc(doc, "simplifycompositelit"),
	Requires: []*analysis.Analyzer{inspect.Analyzer},
	Run:      run,
	URL:      "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifycompositelit",
}

func run(pass *analysis.Pass) (any, error) {
	// Gather information whether file is generated or not
	generated := make(map[*token.File]bool)
	for _, file := range pass.Files {
		if ast.IsGenerated(file) {
			generated[pass.Fset.File(file.FileStart)] = true
		}
	}

	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	// Find each CompositeLit with an explicit type.
	// Then attempt to simplify each element that is
	// itself a CompositeLit with an explicit type.
	//
	// TODO(adonovan): note that go1.28 will permit types to be
	// omitted much more generally, so instead of starting with
	// the "outer" CompositeLit, we'll need to look for "inner"
	// literals appearing in a context that determines their type
	// (assuming we want to take a maximal approach to
	// simplification). We should also support named and alias
	// types more thoroughly.

	for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
		lit := curLit.Node().(*ast.CompositeLit)

		// Skip generated code.
		if _, ok := generated[pass.Fset.File(lit.Pos())]; ok {
			continue
		}

		var (
			kind             string
			keyType, eltType ast.Expr
		)
		switch typ := lit.Type.(type) {
		case *ast.ArrayType:
			eltType = typ.Elt
			if typ.Len != nil {
				kind = "array"
			} else {
				kind = "slice"
			}
		case *ast.MapType:
			keyType = typ.Key
			eltType = typ.Value
			kind = "map"
		default:
			// e.g. struct, named, or nil (no explicit type)
			continue
		}

		for _, elt := range lit.Elts {
			if kve, ok := elt.(*ast.KeyValueExpr); ok {
				if keyType != nil { // map
					simplifyLiteral(pass, keyType, kve.Key, kind)
				}
				elt = kve.Value
			}
			simplifyLiteral(pass, eltType, elt, kind)
		}
	}
	return nil, nil
}

// simplifyLiteral reports a diagnostic if expr's is a T{...} or
// &T{...} literal whose type is identical to want and therefore
// redundant.
func simplifyLiteral(pass *analysis.Pass, want, expr ast.Expr, kind string) {
	info := pass.TypesInfo

	report := func(ampPos token.Pos, innerType ast.Expr) {
		var (
			start = innerType.Pos()
			end   = innerType.End()
			amp   string
			edits []analysis.TextEdit
		)
		if ampPos.IsValid() {
			amp = "&"
			edits = append(edits, analysis.TextEdit{
				Pos: ampPos,
				End: ampPos + token.Pos(len("&")),
			})
			start = ampPos
		}
		edits = append(edits, analysis.TextEdit{Pos: innerType.Pos(), End: end})
		pass.Report(analysis.Diagnostic{
			Pos:     start,
			End:     end,
			Message: fmt.Sprintf("redundant type in %s literal", kind),
			SuggestedFixes: []analysis.SuggestedFix{{
				Message:   fmt.Sprintf("Remove '%s%s'", amp, astutil.Format(pass.Fset, innerType)),
				TextEdits: edits,
			}},
		})
	}

	// If the element is a composite literal whose explicit type
	// is identical to the outer literal's element type,
	// the inner literal's type may be omitted
	if inner, ok := expr.(*ast.CompositeLit); ok &&
		inner.Type != nil &&
		types.Identical(info.TypeOf(want), info.TypeOf(inner.Type)) {
		report(token.NoPos, inner.Type)
	}

	// if the outer literal's element type is a pointer type *T
	// and the element is & of a composite literal of type T,
	// the inner &T may be omitted.
	if star, ok := want.(*ast.StarExpr); ok {
		if addr, ok := expr.(*ast.UnaryExpr); ok && addr.Op == token.AND {
			if inner, ok := addr.X.(*ast.CompositeLit); ok &&
				inner.Type != nil &&
				types.Identical(info.TypeOf(star.X), info.TypeOf(inner.Type)) {
				report(addr.OpPos, inner.Type)
			}
		}
	}
}
