blob: e9fab3e565453ae7269f20c9e709479820e7e1c0 [file] [log] [blame]
// 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
import (
"go/ast"
"go/token"
"go/types"
"fmt"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/analysisinternal/generated"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
)
var errorsastypeAnalyzer = &analysis.Analyzer{
Name: "errorsastype",
Doc: analysisinternal.MustExtractDoc(doc, "errorsastype"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype",
Requires: []*analysis.Analyzer{generated.Analyzer, typeindexanalyzer.Analyzer},
Run: errorsastype,
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.ErrorsAsTypeModernizer = errorsastypeAnalyzer
}
// errorsastype offers a fix to replace error.As with the newer
// errors.AsType[T] following this pattern:
//
// var myerr *MyErr
// if errors.As(err, &myerr) { ... }
//
// =>
//
// if myerr, ok := errors.AsType[*MyErr](err); ok { ... }
//
// (In principle several of these can then be chained using if/else,
// but we don't attempt that.)
//
// We offer the fix only within an if statement, but not within a
// switch case such as:
//
// var myerr *MyErr
// switch {
// case errors.As(err, &myerr):
// }
//
// because the transformation in that case would be ungainly.
//
// Note that the cmd/vet suite includes the "errorsas" analyzer, which
// detects actual mistakes in the use of errors.As. This logic does
// not belong in errorsas because the problems it fixes are merely
// stylistic.
//
// TODO(adonovan): support more cases:
//
// - Negative cases
// var myerr E
// if !errors.As(err, &myerr) { ... }
// =>
// myerr, ok := errors.AsType[E](err)
// if !ok { ... }
//
// - if myerr := new(E); errors.As(err, myerr); { ... }
//
// - if errors.As(err, myerr) && othercond { ... }
func errorsastype(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
)
for curCall := range index.Calls(index.Object("errors", "As")) {
call := curCall.Node().(*ast.CallExpr)
if len(call.Args) < 2 {
continue // spread call: errors.As(pair())
}
v, curDeclStmt := canUseErrorsAsType(info, index, curCall)
if v == nil {
continue
}
file := analysisinternal.EnclosingFile(curDeclStmt)
if !fileUses(info, file, "go1.26") {
continue // errors.AsType is too new
}
// Locate identifier "As" in errors.As.
var asIdent *ast.Ident
switch n := ast.Unparen(call.Fun).(type) {
case *ast.Ident:
asIdent = n // "errors" was dot-imported
case *ast.SelectorExpr:
asIdent = n.Sel
default:
panic("no Ident for errors.As")
}
// Format the type as valid Go syntax.
// TODO(adonovan): fix: FileQualifier needs to respect
// visibility at the current point, and either fail
// or edit the imports as needed.
// TODO(adonovan): fix: TypeString is not a sound way
// to print types as Go syntax as it does not respect
// symbol visibility, etc. We need something loosely
// integrated with FileQualifier that accumulates
// import edits, and may fail (e.g. for unexported
// type or field names from other packages).
// See https://go.dev/issues/75604.
qual := typesinternal.FileQualifier(file, pass.Pkg)
errtype := types.TypeString(v.Type(), qual)
// Choose a name for the "ok" variable.
okName := "ok"
if okVar := lookup(info, curCall, "ok"); okVar != nil {
// The name 'ok' is already declared, but
// don't choose a fresh name unless okVar
// is also used within the if-statement.
curIf := curCall.Parent()
for curUse := range index.Uses(okVar) {
if curIf.Contains(curUse) {
scope := info.Scopes[curIf.Node().(*ast.IfStmt)]
okName = analysisinternal.FreshName(scope, v.Pos(), "ok")
break
}
}
}
pass.Report(analysis.Diagnostic{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
Message: fmt.Sprintf("errors.As can be simplified using AsType[%s]", errtype),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype),
TextEdits: append(
// delete "var myerr *MyErr"
analysisinternal.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
// if errors.As (err, &myerr) { ... }
// ------------- -------------- -------- ----
// if myerr, ok := errors.AsType[*MyErr](err ); ok { ... }
analysis.TextEdit{
// insert "myerr, ok := "
Pos: call.Pos(),
End: call.Pos(),
NewText: fmt.Appendf(nil, "%s, %s := ", v.Name(), okName),
},
analysis.TextEdit{
// replace As with AsType[T]
Pos: asIdent.Pos(),
End: asIdent.End(),
NewText: fmt.Appendf(nil, "AsType[%s]", errtype),
},
analysis.TextEdit{
// delete ", &myerr"
Pos: call.Args[0].End(),
End: call.Args[1].End(),
},
analysis.TextEdit{
// insert "; ok"
Pos: call.End(),
End: call.End(),
NewText: fmt.Appendf(nil, "; %s", okName),
},
),
}},
})
}
return nil, nil
}
// canUseErrorsAsType reports whether curCall is a call to
// errors.As beneath an if statement, preceded by a
// declaration of the typed error var. The var must not be
// used outside the if statement.
func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, _ inspector.Cursor) {
if !analysisinternal.IsChildOf(curCall, edge.IfStmt_Cond) {
return // not beneath if statement
}
var (
curIfStmt = curCall.Parent()
ifStmt = curIfStmt.Node().(*ast.IfStmt)
)
if ifStmt.Init != nil {
return // if statement already has an init part
}
unary, ok := curCall.Node().(*ast.CallExpr).Args[1].(*ast.UnaryExpr)
if !ok || unary.Op != token.AND {
return // 2nd arg is not &var
}
id, ok := unary.X.(*ast.Ident)
if !ok {
return // not a simple ident (local var)
}
v := info.Uses[id].(*types.Var)
curDef, ok := index.Def(v)
if !ok {
return // var is not local (e.g. dot-imported)
}
// Have: if errors.As(err, &v) { ... }
// Reject if v is used outside (before or after) the
// IfStmt, since that will become its new scope.
for curUse := range index.Uses(v) {
if !curIfStmt.Contains(curUse) {
return // v used before/after if statement
}
}
if !analysisinternal.IsChildOf(curDef, edge.ValueSpec_Names) {
return // v not declared by "var v T"
}
var (
curSpec = curDef.Parent() // ValueSpec
curDecl = curSpec.Parent() // GenDecl
spec = curSpec.Node().(*ast.ValueSpec)
)
if len(spec.Names) != 1 || len(spec.Values) != 0 ||
len(curDecl.Node().(*ast.GenDecl).Specs) != 1 {
return // not a simple "var v T" decl
}
// Have:
// var v *MyErr
// ...
// if errors.As(err, &v) { ... }
// with no uses of v outside the IfStmt.
return v, curDecl.Parent() // DeclStmt
}