1
0
Fork 0
mirror of https://github.com/imjasonh/pkgerrors-analyzer synced 2026-07-07 00:32:34 +00:00
pkgerrors-analyzer/analyzer.go
Jason Hall 925783f971 initial commit
Signed-off-by: Jason Hall <imjasonh@gmail.com>
2026-03-24 16:19:23 -04:00

175 lines
4 KiB
Go

package analyzer
import (
"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"
)
const pkgErrorsPath = "github.com/pkg/errors"
var Analyzer = &analysis.Analyzer{
Name: "pkgerrors",
Doc: "detects uses of github.com/pkg/errors and suggests stdlib replacements",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
// pendingDiag is a diagnostic whose import edits haven't been finalized yet.
type pendingDiag struct {
file *ast.File
diag analysis.Diagnostic
}
// fileContext tracks per-file import needs.
type fileContext struct {
needsErrors bool
needsFmt bool
}
func run(pass *analysis.Pass) (interface{}, error) {
// Fast path: skip packages that don't import pkg/errors.
var pkgErrorsImported bool
for _, imp := range pass.Pkg.Imports() {
if imp.Path() == pkgErrorsPath {
pkgErrorsImported = true
break
}
}
if !pkgErrorsImported {
return nil, nil
}
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
files := make(map[*ast.File]*fileContext)
for _, f := range pass.Files {
files[f] = &fileContext{}
}
var pending []pendingDiag
nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
insp.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
if !push {
return true
}
call := n.(*ast.CallExpr)
funcName, ok := resolvePkgErrorsCall(pass, call)
if !ok {
return true
}
file := fileForPos(pass, call.Pos())
if file == nil {
return true
}
fc := files[file]
var diag *analysis.Diagnostic
switch funcName {
case "New":
fc.needsErrors = true
diag = diagSimple(pass, call, "errors.New", funcName)
case "Unwrap":
fc.needsErrors = true
diag = diagSimple(pass, call, "errors.Unwrap", funcName)
case "Is":
fc.needsErrors = true
diag = diagSimple(pass, call, "errors.Is", funcName)
case "As":
fc.needsErrors = true
diag = diagSimple(pass, call, "errors.As", funcName)
case "Errorf":
fc.needsFmt = true
diag = diagErrorf(pass, call)
case "Wrap":
fc.needsFmt = true
diag = diagWrap(pass, call)
case "Wrapf":
fc.needsFmt = true
diag = diagWrapf(pass, call)
case "WithMessage":
fc.needsFmt = true
diag = diagWithMessage(pass, call)
case "WithMessagef":
fc.needsFmt = true
diag = diagWithMessagef(pass, call)
case "WithStack":
diag = diagWithStack(pass, call)
case "Cause":
diag = diagCause(pass, call, stack, fc)
}
if diag != nil {
pending = append(pending, pendingDiag{file: file, diag: *diag})
}
return true
})
// Emit all diagnostics, attaching per-file import edits.
for _, pd := range pending {
fc := files[pd.file]
if len(pd.diag.SuggestedFixes) > 0 {
ie := importEdits(pass, pd.file, fc.needsErrors, fc.needsFmt)
pd.diag.SuggestedFixes[0].TextEdits = append(pd.diag.SuggestedFixes[0].TextEdits, ie...)
}
pass.Report(pd.diag)
}
return nil, nil
}
// resolvePkgErrorsCall checks whether call is a call to a function in
// github.com/pkg/errors. Returns the function name and true if so.
func resolvePkgErrorsCall(pass *analysis.Pass, call *ast.CallExpr) (string, bool) {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return "", false
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return "", false
}
obj := pass.TypesInfo.Uses[ident]
if obj == nil {
return "", false
}
pkgName, ok := obj.(*types.PkgName)
if !ok {
return "", false
}
if pkgName.Imported().Path() != pkgErrorsPath {
return "", false
}
return sel.Sel.Name, true
}
// fileForPos returns the *ast.File containing pos.
func fileForPos(pass *analysis.Pass, pos token.Pos) *ast.File {
tokFile := pass.Fset.File(pos)
if tokFile == nil {
return nil
}
for _, f := range pass.Files {
if pass.Fset.File(f.Pos()) == tokFile {
return f
}
}
return nil
}
// importPath extracts the import path from an ImportSpec.
func importPath(imp *ast.ImportSpec) string {
return imp.Path.Value[1 : len(imp.Path.Value)-1]
}