mirror of
https://github.com/imjasonh/pkgerrors-analyzer
synced 2026-07-07 00:32:34 +00:00
175 lines
4 KiB
Go
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]
|
|
}
|