// Copyright 2025 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 modernize

// This file defines modernizers that use the "reflect" package.

import (
	"go/ast"
	"go/types"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/edge"
	"golang.org/x/tools/go/types/typeutil"
	"golang.org/x/tools/internal/analysis/analyzerutil"
	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
	"golang.org/x/tools/internal/astutil"
	"golang.org/x/tools/internal/refactor"
	"golang.org/x/tools/internal/typesinternal"
	"golang.org/x/tools/internal/typesinternal/typeindex"
	"golang.org/x/tools/internal/versions"
)

var ReflectTypeForAnalyzer = &analysis.Analyzer{
	Name: "reflecttypefor",
	Doc:  analyzerutil.MustExtractDoc(doc, "reflecttypefor"),
	Requires: []*analysis.Analyzer{
		inspect.Analyzer,
		typeindexanalyzer.Analyzer,
	},
	Run: reflecttypefor,
	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor",
}

func reflecttypefor(pass *analysis.Pass) (any, error) {
	var (
		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
		info  = pass.TypesInfo

		reflectTypeOf = index.Object("reflect", "TypeOf")
	)

	for curCall := range index.Calls(reflectTypeOf) {
		call := curCall.Node().(*ast.CallExpr)
		// Have: reflect.TypeOf(expr)

		expr := call.Args[0]

		// reflect.TypeFor cannot be instantiated with an untyped nil.
		// We use type information rather than checking the identifier name
		// to correctly handle edge cases where "nil" is shadowed (e.g. nil := "nil").
		if info.Types[expr].IsNil() {
			continue
		}

		if !typesinternal.NoEffects(info, expr) {
			continue // don't eliminate operand: may have effects
		}

		t := info.TypeOf(expr)
		var edits []analysis.TextEdit

		// Special cases for TypeOf((*T)(nil)).Elem(), and
		// TypeOf([]T(nil)).Elem(), needed when T is an interface type.
		if curCall.ParentEdgeKind() == edge.SelectorExpr_X {
			curSel := astutil.UnparenEnclosingCursor(curCall).Parent()
			if curSel.ParentEdgeKind() == edge.CallExpr_Fun {
				call2 := astutil.UnparenEnclosingCursor(curSel).Parent().Node().(*ast.CallExpr) // potentially .Elem()
				obj := typeutil.Callee(info, call2)
				if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
					// reflect.TypeOf(expr).Elem()
					//                     -------
					// reflect.TypeOf(expr)
					if typ, hasElem := t.(interface{ Elem() types.Type }); hasElem {
						// Have: TypeOf(expr).Elem() where expr is *T, []T, [k]T, chan T, map[K]T, etc.
						t = typ.Elem()
						edits = []analysis.TextEdit{{
							Pos: call.End(),
							End: call2.End(),
						}}
					}
				}
			}
		}

		// TypeOf(x) where x has an interface type is a
		// dynamic operation; don't transform it to TypeFor.
		// (edits == nil means "not the Elem() special case".)
		if types.IsInterface(t) && edits == nil {
			continue
		}

		file := astutil.EnclosingFile(curCall)
		if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_22) {
			continue // TypeFor requires go1.22
		}
		tokFile := pass.Fset.File(file.Pos())

		// Format the type as valid Go syntax.
		// TODO(adonovan): FileQualifier needs to respect
		// visibility at the current point, and either fail
		// or edit the imports as needed.
		qual := typesinternal.FileQualifier(file, pass.Pkg)
		tstr := types.TypeString(t, qual)

		sel, ok := call.Fun.(*ast.SelectorExpr)
		if !ok {
			continue // e.g. reflect was dot-imported
		}

		// Don't offer a fix if the type contains an unnamed struct or unnamed
		// interface because the replacement would be significantly more verbose.
		// (See golang/go#76698)
		if isComplicatedType(t) {
			continue
		}

		// Don't offer the fix if the type string is too long. We define "too
		// long" as more than three times the length of the original expression
		// and at least 16 characters (a 3x length increase of a very
		// short expression should not be cause for skipping the fix).
		oldLen := int(expr.End() - expr.Pos())
		newLen := len(tstr)
		if newLen >= 16 && newLen > 3*oldLen {
			continue
		}

		// If the call argument contains the last use
		// of a variable, as in:
		//	var zero T
		//	reflect.TypeOf(zero)
		// remove the declaration of that variable.
		curArg0 := curCall.ChildAt(edge.CallExpr_Args, 0)
		edits = append(edits, refactor.DeleteUnusedVars(index, info, tokFile, curArg0)...)

		pass.Report(analysis.Diagnostic{
			Pos:     call.Fun.Pos(),
			End:     call.Fun.End(),
			Message: "reflect.TypeOf call can be simplified using TypeFor",
			SuggestedFixes: []analysis.SuggestedFix{{
				// reflect.TypeOf    (...T value...)
				//         ------     -------------
				// reflect.TypeFor[T](             )
				Message: "Replace TypeOf by TypeFor",
				TextEdits: append([]analysis.TextEdit{
					{
						Pos:     sel.Sel.Pos(),
						End:     sel.Sel.End(),
						NewText: []byte("TypeFor[" + tstr + "]"),
					},
					// delete (pure) argument
					{
						Pos: call.Lparen + 1,
						End: call.Rparen,
					},
				}, edits...),
			}},
		})
	}

	return nil, nil
}

// isComplicatedType reports whether type t is complicated, e.g. it is or contains an
// unnamed struct, interface, or function signature.
func isComplicatedType(t types.Type) bool {
	var check func(typ types.Type) bool
	check = func(typ types.Type) bool {
		switch t := typ.(type) {
		case typesinternal.NamedOrAlias:
			for ta := range t.TypeArgs().Types() {
				if check(ta) {
					return true
				}
			}
			return false
		case *types.Struct, *types.Interface, *types.Signature:
			// These are complex types with potentially many elements
			// so we should avoid duplicating their definition.
			return true
		case *types.Pointer:
			return check(t.Elem())
		case *types.Slice:
			return check(t.Elem())
		case *types.Array:
			return check(t.Elem())
		case *types.Chan:
			return check(t.Elem())
		case *types.Map:
			return check(t.Key()) || check(t.Elem())
		case *types.Basic:
			return false
		case *types.TypeParam:
			return false
		default:
			// Includes types.Union
			return true
		}
	}

	return check(t)
}
