diff --git a/go/analysis/passes/modernize/errorsastype.go b/go/analysis/passes/modernize/errorsastype.go
index f52f202..5c92fb4 100644
--- a/go/analysis/passes/modernize/errorsastype.go
+++ b/go/analysis/passes/modernize/errorsastype.go
@@ -33,7 +33,7 @@
}
// errorsastype offers a fix to replace error.As with the newer
-// errors.AsType[T] following this pattern:
+// errors.AsType[T] following these patterns:
//
// var myerr *MyErr
// if errors.As(err, &myerr) { ... }
@@ -42,6 +42,14 @@
//
// if myerr, ok := errors.AsType[*MyErr](err); ok { ... }
//
+// and:
+//
+// if myerr := new(MyErr); 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.)
//
@@ -63,7 +71,6 @@
// stylistic.
//
// TODO(adonovan): support more cases:
-// - if myerr := new(E); errors.As(err, myerr); { ... }
// - if errors.As(err, myerr) && othercond { ... }
func errorsastype(pass *analysis.Pass) (any, error) {
var (
@@ -122,9 +129,24 @@
// the argument in errors.As must lie inside the if statement.
usesV := moreiters.Len(index.Uses(v)) > 1
+ var deleteErrDecl []analysis.TextEdit
+ ifStmt := curIfStmt.Node().(*ast.IfStmt)
+ if ifStmt.Init == curDeclStmt.Node() {
+ // if myerr := new(MyErr); errors.As(err, &myerr) { ... }
+ // ---------------------
+ deleteErrDecl = []analysis.TextEdit{
+ {
+ Pos: ifStmt.Init.Pos(),
+ End: ifStmt.Cond.Pos(),
+ },
+ }
+ } else {
+ // Delete "var myerr *MyErr"
+ deleteErrDecl = refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt)
+ }
+
edits := append(
- // delete "var myerr *MyErr"
- refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
+ deleteErrDecl,
// if errors.As (err, &myerr) { ... }
// ------------- -------------- -------- ----
// if myerr, ok := errors.AsType[*MyErr](err ); ok { ... }
@@ -175,15 +197,18 @@
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.
+// canUseErrorsAsType reports whether curCall is one of the following:
+// 1. 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.
+// 2. a call to errors.As in the condition block of an if statement, where the
+// init block creates the typed error var.
// If the conditions are met, it returns the error var, the cursor for its
// DeclStmt, and the cursor for the IfStmt that contains the call to errors.As.
// Otherwise it returns a nil error var.
func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, curDeclStmt, curIfStmt inspector.Cursor) {
curCond := curCall
- if curCond.ParentEdgeKind() == edge.UnaryExpr_X { // if !errors.As(err, &v)
+ negated := curCond.ParentEdgeKind() == edge.UnaryExpr_X
+ if negated { // if !errors.As(err, &v)
curCond = curCond.Parent()
}
if curCond.ParentEdgeKind() != edge.IfStmt_Cond {
@@ -191,9 +216,6 @@
}
curIfStmt = curCond.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
@@ -203,6 +225,9 @@
return // not a simple ident (local var)
}
v := info.Uses[id].(*types.Var)
+ if v.Pkg() != nil && v.Parent() == v.Pkg().Scope() {
+ return // reject package-level variables
+ }
curDef, ok := index.Def(v)
if !ok {
return // var is not local (e.g. dot-imported)
@@ -216,23 +241,56 @@
return // v used before/after if statement
}
}
- if curDef.ParentEdgeKind() != 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(), curIfStmt // curDecl.Parent() is a DeclStmt
+ switch curDef.ParentEdgeKind() {
+ case edge.AssignStmt_Lhs:
+ // Want:
+ // if myerr := new(MyErr); errors.As(err, &myerr) { ... }
+ assign := curDef.Parent().Node().(*ast.AssignStmt)
+ if assign.Tok != token.DEFINE || len(assign.Lhs) != 1 || ifStmt.Init != assign {
+ return
+ }
+ // To avoid semantic changes, reject when the condition is negated or when
+ // there is an else case: when the errors.As check fails, "myerr" is the
+ // zero value of the error type in the original block, and nil in the
+ // transformed block.
+ if negated || ifStmt.Else != nil {
+ return
+ }
+ if !isCallToNew(info, assign.Rhs[0]) {
+ return
+ }
+ return v, curDef.Parent(), curIfStmt
+ case edge.ValueSpec_Names:
+ // Want:
+ // var v *MyErr
+ // ...
+ // if errors.As(err, &v) { ... }
+ // with no uses of v outside the IfStmt.
+ if ifStmt.Init != nil {
+ return // has unrelated init statement
+ }
+ 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
+ }
+ return v, curDecl.Parent(), curIfStmt // curDecl.Parent() is a DeclStmt
+ default:
+ return
+ }
+}
+
+// isCallToNew reports whether expr is a call to the builtin function new(T).
+func isCallToNew(info *types.Info, expr ast.Expr) bool {
+ call, ok := ast.Unparen(expr).(*ast.CallExpr)
+ if !ok || len(call.Args) != 1 {
+ return false
+ }
+ id, ok := call.Fun.(*ast.Ident)
+ return ok && info.ObjectOf(id) == builtinNew
}
diff --git a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go
index ae8e9d4..759cdd4 100644
--- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go
+++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go
@@ -5,6 +5,8 @@
"os"
)
+var pkgErr *os.PathError
+
func _(err error) {
{
var patherr *os.PathError
@@ -40,6 +42,11 @@
}
print(patherr)
}
+ {
+ if errors.As(err, &pkgErr) { // nope: package-level variable
+ print(pkgErr)
+ }
+ }
// Test of 'ok' var shadowing/freshness.
const ok = 1
@@ -85,4 +92,33 @@
print(patherr)
}
}
+ // Error in init of if block case.
+ {
+ if myerr := new(os.PathError); errors.As(err, &myerr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]`
+ print(myerr)
+ }
+ }
+ {
+ if myerr := new(os.PathError); !errors.As(err, &myerr) { // nope: negated condition with init statement
+ print(myerr)
+ }
+ }
+ {
+ if myerr := new(os.PathError); errors.As(err, &myerr) { // nope: has else branch
+ print(myerr)
+ } else {
+ print("not myerr")
+ }
+ }
+ {
+ getErr := func() *os.PathError { return nil }
+ if myerr := getErr(); errors.As(err, &myerr) { // nope: RHS has potential side effects
+ print(myerr)
+ }
+ }
+ {
+ if myerr := (&os.PathError{Path: "foo"}); errors.As(err, &myerr) { // nope: not a call to new
+ print(myerr)
+ }
+ }
}
diff --git a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden
index 0e1ca1c..a29a5cf 100644
--- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden
+++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden
@@ -5,6 +5,8 @@
"os"
)
+var pkgErr *os.PathError
+
func _(err error) {
{
if patherr, ok := errors.AsType[*os.PathError](err); ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]`
@@ -37,6 +39,11 @@
}
print(patherr)
}
+ {
+ if errors.As(err, &pkgErr) { // nope: package-level variable
+ print(pkgErr)
+ }
+ }
// Test of 'ok' var shadowing/freshness.
const ok = 1
@@ -76,4 +83,33 @@
print(patherr)
}
}
+ // Error in init of if block case.
+ {
+ if myerr, ok := errors.AsType[*os.PathError](err); ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]`
+ print(myerr)
+ }
+ }
+ {
+ if myerr := new(os.PathError); !errors.As(err, &myerr) { // nope: negated condition with init statement
+ print(myerr)
+ }
+ }
+ {
+ if myerr := new(os.PathError); errors.As(err, &myerr) { // nope: has else branch
+ print(myerr)
+ } else {
+ print("not myerr")
+ }
+ }
+ {
+ getErr := func() *os.PathError { return nil }
+ if myerr := getErr(); errors.As(err, &myerr) { // nope: RHS has potential side effects
+ print(myerr)
+ }
+ }
+ {
+ if myerr := (&os.PathError{Path: "foo"}); errors.As(err, &myerr) { // nope: not a call to new
+ print(myerr)
+ }
+ }
}