2018-06-26 22:21:03 +02:00
|
|
|
package rule
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
2024-12-03 16:39:07 +02:00
|
|
|
"strings"
|
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
2018-06-26 22:21:03 +02:00
|
|
|
|
|
|
|
"github.com/mgechev/revive/lint"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DeepExitRule lints program exit at functions other than main or init.
|
|
|
|
type DeepExitRule struct{}
|
|
|
|
|
|
|
|
// Apply applies the rule to given file.
|
2022-04-10 11:55:13 +02:00
|
|
|
func (*DeepExitRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
2018-06-26 22:21:03 +02:00
|
|
|
var failures []lint.Failure
|
|
|
|
onFailure := func(failure lint.Failure) {
|
|
|
|
failures = append(failures, failure)
|
|
|
|
}
|
|
|
|
|
2024-11-30 11:06:59 +02:00
|
|
|
w := &lintDeepExit{onFailure: onFailure, isTestFile: file.IsTest()}
|
2018-06-26 22:21:03 +02:00
|
|
|
ast.Walk(w, file.AST)
|
|
|
|
return failures
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the rule name.
|
2022-04-10 11:55:13 +02:00
|
|
|
func (*DeepExitRule) Name() string {
|
2018-06-26 22:21:03 +02:00
|
|
|
return "deep-exit"
|
|
|
|
}
|
|
|
|
|
|
|
|
type lintDeepExit struct {
|
2024-11-28 09:47:10 +02:00
|
|
|
onFailure func(lint.Failure)
|
|
|
|
isTestFile bool
|
2018-06-26 22:21:03 +02:00
|
|
|
}
|
|
|
|
|
2024-11-30 11:06:59 +02:00
|
|
|
func (w *lintDeepExit) Visit(node ast.Node) ast.Visitor {
|
2020-01-08 00:46:21 +02:00
|
|
|
if fd, ok := node.(*ast.FuncDecl); ok {
|
|
|
|
if w.mustIgnore(fd) {
|
|
|
|
return nil // skip analysis of this function
|
|
|
|
}
|
2018-06-26 22:21:03 +02:00
|
|
|
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
|
|
|
se, ok := node.(*ast.ExprStmt)
|
|
|
|
if !ok {
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
ce, ok := se.X.(*ast.CallExpr)
|
|
|
|
if !ok {
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
|
|
|
fc, ok := ce.Fun.(*ast.SelectorExpr)
|
|
|
|
if !ok {
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
id, ok := fc.X.(*ast.Ident)
|
|
|
|
if !ok {
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg := id.Name
|
2024-10-01 12:14:02 +02:00
|
|
|
fn := fc.Sel.Name
|
2024-11-28 09:47:10 +02:00
|
|
|
if isCallToExitFunction(pkg, fn) {
|
2018-06-26 22:21:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Confidence: 1,
|
|
|
|
Node: ce,
|
|
|
|
Category: "bad practice",
|
|
|
|
Failure: fmt.Sprintf("calls to %s.%s only in main() or init() functions", pkg, fn),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
2020-01-08 00:46:21 +02:00
|
|
|
func (w *lintDeepExit) mustIgnore(fd *ast.FuncDecl) bool {
|
2018-06-26 22:21:03 +02:00
|
|
|
fn := fd.Name.Name
|
2020-01-08 00:46:21 +02:00
|
|
|
|
2024-12-03 16:39:07 +02:00
|
|
|
return fn == "init" || fn == "main" || w.isTestMain(fd) || w.isTestExample(fd)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintDeepExit) isTestMain(fd *ast.FuncDecl) bool {
|
|
|
|
return w.isTestFile && fd.Name.Name == "TestMain"
|
|
|
|
}
|
|
|
|
|
|
|
|
// isTestExample returns true if the function is a testable example function.
|
|
|
|
// See https://go.dev/blog/examples#examples-are-tests for more information.
|
|
|
|
//
|
|
|
|
// Inspired by https://github.com/golang/go/blob/go1.23.0/src/go/doc/example.go#L72-L77
|
|
|
|
func (w *lintDeepExit) isTestExample(fd *ast.FuncDecl) bool {
|
|
|
|
if !w.isTestFile {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
name := fd.Name.Name
|
|
|
|
const prefix = "Example"
|
|
|
|
if !strings.HasPrefix(name, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if len(name) == len(prefix) { // "Example" is a package level example
|
|
|
|
return len(fd.Type.Params.List) == 0
|
|
|
|
}
|
|
|
|
r, _ := utf8.DecodeRuneInString(name[len(prefix):])
|
|
|
|
if unicode.IsLower(r) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return len(fd.Type.Params.List) == 0
|
2018-06-26 22:21:03 +02:00
|
|
|
}
|