| // 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 intconv implements the logic of the stringintconv | |
| // analyzer, which flags string(int) conversions. It is shared by | |
| // the analyzer and by gopls, which offers the same fixes for the | |
| // type error that go1.28 reports for such conversions. | |
| package intconv | |
| import ( | |
| "fmt" | |
| "go/ast" | |
| "go/constant" | |
| "go/token" | |
| "go/types" | |
| "math" | |
| "strings" | |
| "golang.org/x/tools/go/analysis" | |
| "golang.org/x/tools/go/ast/inspector" | |
| "golang.org/x/tools/internal/astutil" | |
| "golang.org/x/tools/internal/refactor" | |
| "golang.org/x/tools/internal/typeparams" | |
| "golang.org/x/tools/internal/typesinternal" | |
| ) | |
| // describe returns a string describing the type typ contained within the type | |
| // set of inType. If non-empty, inName is used as the name of inType (this is | |
| // necessary so that we can use alias type names that may not be reachable from | |
| // inType itself). | |
| func describe(typ, inType types.Type, inName string) string { | |
| name := inName | |
| if typ != inType { | |
| name = typeName(typ) | |
| } | |
| if name == "" { | |
| return "" | |
| } | |
| var parentheticals []string | |
| if underName := typeName(typ.Underlying()); underName != "" && underName != name { | |
| parentheticals = append(parentheticals, underName) | |
| } | |
| if typ != inType && inName != "" && inName != name { | |
| parentheticals = append(parentheticals, "in "+inName) | |
| } | |
| if len(parentheticals) > 0 { | |
| name += " (" + strings.Join(parentheticals, ", ") + ")" | |
| } | |
| return name | |
| } | |
| func typeName(t types.Type) string { | |
| if basic, ok := t.(*types.Basic); ok { | |
| return basic.Name() // may be (e.g.) "untyped int", which has no TypeName | |
| } | |
| if tname := typesinternal.TypeNameFor(t); tname != nil { | |
| return tname.Name() | |
| } | |
| return "" | |
| } | |
| // Check reports whether the call is a conversion string(x) from a | |
| // non-byte, non-rune integer x, and if so, returns a diagnostic | |
| // with suggested fixes. | |
| // | |
| // Such conversions are rejected by the type checker in files | |
| // using go1.28 or later, so in practice the analyzer only sees | |
| // them in older files. (An exception is a tool built with a | |
| // pre-go1.28 go/types analyzing a go1.28 file, in which case | |
| // the diagnostic and its fix are just as useful.) However, | |
| // Check does not require that the package be well typed, so | |
| // gopls uses it to offer fixes for the type error. | |
| func Check(info *types.Info, curCall inspector.Cursor) (_ analysis.Diagnostic, ok bool) { | |
| call := curCall.Node().(*ast.CallExpr) | |
| if len(call.Args) != 1 { | |
| return | |
| } | |
| arg := call.Args[0] | |
| // Retrieve target type name. | |
| // | |
| // TODO(adonovan): simplify. info.Types[call.Fun].IsType() yields the | |
| // conversion's target type directly (and handles parenthesized and | |
| // instantiated types too), though describe still needs a name | |
| // to report aliases. | |
| var tname *types.TypeName | |
| switch fun := call.Fun.(type) { | |
| case *ast.Ident: | |
| tname, _ = info.Uses[fun].(*types.TypeName) | |
| case *ast.SelectorExpr: | |
| tname, _ = info.Uses[fun.Sel].(*types.TypeName) | |
| } | |
| if tname == nil { | |
| return | |
| } | |
| // In the conversion T(v) of a value v of type V to a target type T, we | |
| // look for types T0 in the type set of T and V0 in the type set of V, such | |
| // that V0->T0 is a problematic conversion. If T and V are not type | |
| // parameters, this amounts to just checking if V->T is a problematic | |
| // conversion. | |
| // First, find a type T0 in T that has an underlying type of string. | |
| T := tname.Type() | |
| ttypes, err := structuralTypes(T) | |
| if err != nil { | |
| return // invalid type | |
| } | |
| var T0 types.Type // string type in the type set of T | |
| for _, tt := range ttypes { | |
| u, _ := tt.Underlying().(*types.Basic) | |
| if u != nil && u.Kind() == types.String { | |
| T0 = tt | |
| break | |
| } | |
| } | |
| if T0 == nil { | |
| // No target types have an underlying type of string. | |
| return | |
| } | |
| // Next, find a type V0 in V that has an underlying integral type that is | |
| // not byte or rune. | |
| V := info.TypeOf(arg) | |
| vtypes, err := structuralTypes(V) | |
| if err != nil { | |
| return // invalid type | |
| } | |
| var V0 types.Type // integral type in the type set of V | |
| for _, vt := range vtypes { | |
| u, _ := vt.Underlying().(*types.Basic) | |
| if u != nil && u.Info()&types.IsInteger != 0 { | |
| switch u.Kind() { | |
| case types.Byte, types.Rune, types.UntypedRune: | |
| continue | |
| } | |
| V0 = vt | |
| break | |
| } | |
| } | |
| if V0 == nil { | |
| // No source types are non-byte or rune integer types. | |
| return | |
| } | |
| convertibleToRune := true // if true, we can suggest a fix | |
| for _, t := range vtypes { | |
| if !types.ConvertibleTo(t, types.Typ[types.Rune]) { | |
| convertibleToRune = false | |
| break | |
| } | |
| } | |
| target := describe(T0, T, tname.Name()) | |
| source := describe(V0, V, typeName(V)) | |
| if target == "" || source == "" { | |
| return // something went wrong | |
| } | |
| diag := analysis.Diagnostic{ | |
| Pos: call.Pos(), | |
| Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits", source, target), | |
| } | |
| if !convertibleToRune { | |
| return diag, true // no fixes | |
| } | |
| addFix := func(message string, edits ...analysis.TextEdit) { | |
| diag.SuggestedFixes = append(diag.SuggestedFixes, analysis.SuggestedFix{ | |
| Message: message, | |
| TextEdits: edits, | |
| }) | |
| } | |
| edit := func(start, end token.Pos, text string) analysis.TextEdit { | |
| return analysis.TextEdit{Pos: start, End: end, NewText: []byte(text)} | |
| } | |
| // sprintf returns edits that replace x by fmt.Sprintf("%verb", x), | |
| // adding an import of "fmt" as needed. | |
| // The conversion is retained unless T is exactly string: | |
| // | |
| // string(x) -> fmt.Sprintf("%verb", x) | |
| // mystring(x) -> mystring(fmt.Sprintf("%verb", x)) | |
| sprintf := func(verb string) []analysis.TextEdit { | |
| start, end := arg.Pos(), arg.End() | |
| if types.Identical(T, types.Typ[types.String]) { | |
| start, end = call.Pos(), call.End() | |
| } | |
| file := astutil.EnclosingFile(curCall) | |
| prefix, importEdits := refactor.AddImport(info, file, "fmt", "fmt", "Sprintf", arg.Pos()) | |
| return append(importEdits, | |
| edit(start, arg.Pos(), prefix+`Sprintf("%`+verb+`", `), | |
| edit(arg.End(), end, ")")) | |
| } | |
| // sprintfOK reports whether fmt.Sprintf may be used. | |
| // | |
| // Do not use it if type parameters are involved, | |
| // as there are too many combinations and subtleties. | |
| // Consider x = rune | int16 | []byte: in all cases, | |
| // string(x) is legal, but the appropriate diagnostic | |
| // and fix differs. Similarly, don't use it if the type | |
| // has methods, as a Format method may change the behavior | |
| // of fmt.Sprintf. | |
| sprintfOK := len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 | |
| // The first fix must preserve the existing behavior, | |
| // since it is the one applied by "go fix". (This lets | |
| // "go fix" migrate code before its go.mod file is | |
| // upgraded to go1.28, at which point string(int) | |
| // conversions become a compile error.) | |
| // | |
| // If no behavior-preserving fix is available, | |
| // we offer no fixes at all. | |
| // Fix 1: use string(rune(x)) or fmt.Sprintf("%c", x), | |
| // preserving behavior. | |
| // | |
| // string(x) yields "\uFFFD" for any x outside the range of valid | |
| // code points, whereas rune(x) truncates x to 32 bits, which may | |
| // turn an invalid code point into a valid one. So rune(x) is | |
| // exact only if x is a constant that fits in 32 bits, or if every | |
| // type in the type set of V is at most 32 bits wide. | |
| // (For uint32, values ≥ 1<<31 become negative runes, which are | |
| // just as invalid.) | |
| // | |
| // For a wider type we use fmt.Sprintf("%c", x), which is exactly | |
| // equivalent: fmt converts x to uint64 and treats any value | |
| // above MaxRune (including negative values) as "\uFFFD". | |
| // In addition to the sprintfOK conditions, we avoid a | |
| // single-term type parameter (e.g. ~int64), since it | |
| // could be instantiated by a type with a Format method, | |
| // and untyped constants (whose default type int might | |
| // overflow). | |
| if runeIsExact(info, arg, vtypes) { | |
| addFix("Convert a single rune to a string", | |
| edit(arg.Pos(), arg.Pos(), "rune("), | |
| edit(arg.End(), arg.End(), ")")) | |
| } else if sprintfOK && | |
| !is[*types.TypeParam](types.Unalias(V)) && | |
| !isUntypedConst(info, arg) { | |
| addFix("Convert single rune to string (preserves behavior)", sprintf("c")...) | |
| } else { | |
| return diag, true // no behavior-preserving fix, so no fixes | |
| } | |
| // Fix 2: use fmt.Sprintf("%d", x), which changes behavior. | |
| // | |
| // Prefer fmt.Sprintf over strconv.Itoa, FormatInt, | |
| // or FormatUint, as it works for any integer type. | |
| if sprintfOK { | |
| addFix("Format number as decimal (changes behavior)", sprintf("d")...) | |
| } | |
| return diag, true | |
| } | |
| // runeIsExact reports whether string(rune(x)) is equivalent to | |
| // string(x) for the argument x, whose type set is vtypes. | |
| func runeIsExact(info *types.Info, x ast.Expr, vtypes []types.Type) bool { | |
| // Constant that fits in 32 bits? | |
| if tv := info.Types[x]; tv.Value != nil { | |
| v, exact := constant.Int64Val(constant.ToInt(tv.Value)) | |
| return exact && math.MinInt32 <= v && v <= math.MaxInt32 | |
| } | |
| // All types in the type set at most 32 bits wide? | |
| for _, t := range vtypes { | |
| u, ok := t.Underlying().(*types.Basic) | |
| if !ok { | |
| return false | |
| } | |
| switch u.Kind() { | |
| case types.Int8, types.Int16, types.Int32, | |
| types.Uint8, types.Uint16, types.Uint32: | |
| default: | |
| return false // int, int64, uint, uint64, uintptr, etc | |
| } | |
| } | |
| return true | |
| } | |
| // isUntypedConst reports whether x is an untyped constant. | |
| func isUntypedConst(info *types.Info, x ast.Expr) bool { | |
| tv := info.Types[x] | |
| if tv.Value == nil { | |
| return false | |
| } | |
| u, ok := tv.Type.(*types.Basic) | |
| return ok && u.Info()&types.IsUntyped != 0 | |
| } | |
| func is[T any](x any) bool { | |
| _, ok := x.(T) | |
| return ok | |
| } | |
| func structuralTypes(t types.Type) ([]types.Type, error) { | |
| var structuralTypes []types.Type | |
| if tp, ok := types.Unalias(t).(*types.TypeParam); ok { | |
| terms, err := typeparams.StructuralTerms(tp) | |
| if err != nil { | |
| return nil, err | |
| } | |
| for _, term := range terms { | |
| structuralTypes = append(structuralTypes, term.Type()) | |
| } | |
| } else { | |
| structuralTypes = append(structuralTypes, t) | |
| } | |
| return structuralTypes, nil | |
| } |