diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2023-10-24 07:34:00 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2023-10-24 07:34:00 +0200 |
| commit | 435397de49adc9740bc67416166aba4d0be8ae2a (patch) | |
| tree | 1642dccae517840f3256f35e29e8d7d1802f700f /cmd/cvet | |
| parent | 18476c6cba2d5d2bb3ecf3ad136dacbf41d0360e (diff) | |
feat: Add cmds
Diffstat (limited to 'cmd/cvet')
| -rw-r--r-- | cmd/cvet/check_command.go | 104 | ||||
| -rw-r--r-- | cmd/cvet/check_query.go | 25 | ||||
| -rw-r--r-- | cmd/cvet/code_smells.go | 64 | ||||
| -rw-r--r-- | cmd/cvet/logger.go | 71 | ||||
| -rw-r--r-- | cmd/cvet/main.go | 9 |
5 files changed, 273 insertions, 0 deletions
diff --git a/cmd/cvet/check_command.go b/cmd/cvet/check_command.go new file mode 100644 index 0000000..47db8fa --- /dev/null +++ b/cmd/cvet/check_command.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + + "github.com/samber/lo" + "golang.org/x/tools/go/analysis" +) + +func isCommandFunc(node *ast.FuncDecl) bool { + return strings.HasSuffix(node.Name.Name, "Command") +} + +func loggerCommandKeyValueArgFinder(pass *analysis.Pass, commandName string) func(arg ast.Expr) bool { + return func(arg ast.Expr) bool { + call, ok := arg.(*ast.CallExpr) + if !ok { + return false + } + firstArg, ok := call.Args[0].(*ast.BasicLit) + if !ok { + return false + } + if firstArg.Kind == token.STRING && firstArg.Value == `"command"` { + secondArg, ok := call.Args[1].(*ast.BasicLit) + if !ok { + pass.Reportf(arg.Pos(), "logger command name must be a literal") + return true + } + if secondArg.Kind != token.STRING { + pass.Reportf(arg.Pos(), "logger command name must be a string literal") + return true + } + + value := secondArg.Value[1 : len(secondArg.Value)-1] + if value != commandName { + pass.Reportf(arg.Pos(), fmt.Sprintf("logger command name must be function name %s, but found %s", commandName, value)) + return true + } + return true + } + + return false + } +} + +func checkStmtHaveNoRawSlog(pass *analysis.Pass, stmts []ast.Stmt) { + for _, stmt := range stmts[1:] { + switch stmt := stmt.(type) { + case *ast.ExprStmt: + if call, ok := stmt.X.(*ast.CallExpr); ok { + if selector, ok := call.Fun.(*ast.SelectorExpr); ok { + if isIdent(selector.X, "slog") { + pass.Reportf(stmt.Pos(), "logger must be used to log") + } + } + } + case *ast.IfStmt: + checkStmtHaveNoRawSlog(pass, stmt.Body.List) + } + } +} + +func checkCommandLogger(pass *analysis.Pass, node *ast.FuncDecl) { + commandName := node.Name.Name + firstStmt, ok := node.Body.List[0].(*ast.AssignStmt) + if !ok || !isIdent(firstStmt.Lhs[0], "logger") { + pass.Reportf(node.Pos(), "command first statement must be a logger creation") + return + } + + call, ok := firstStmt.Rhs[0].(*ast.CallExpr) + if !ok || !isSelector(call.Fun, "slog", "With") { + pass.Reportf(node.Pos(), "logger creation must use slog.With") + return + } + if !lo.ContainsBy(call.Args, loggerCommandKeyValueArgFinder(pass, commandName)) { + pass.Reportf(node.Pos(), "logger creation must contain command arg") + } + + checkStmtHaveNoRawSlog(pass, node.Body.List[1:]) +} + +func checkCommandFunc(pass *analysis.Pass, node *ast.FuncDecl) { + if node.Type.Results.NumFields() != 2 { + pass.Reportf(node.Pos(), "command function must have 2 return value") + } else { + if !isIdent(node.Type.Results.List[0].Type, "int") { + pass.Reportf(node.Pos(), "command function must return an int code as the first return value") + } + if !isIdent(node.Type.Results.List[1].Type, "error") { + pass.Reportf(node.Pos(), "command function must return an error as the second return value") + } + } + if lo.ContainsBy(node.Type.Params.List, func(field *ast.Field) bool { + return isSelector(field.Type, "auth", "User") + }) { + pass.Reportf(node.Pos(), "command function must not have an auth.User parameter") + } + checkCommandLogger(pass, node) +} diff --git a/cmd/cvet/check_query.go b/cmd/cvet/check_query.go new file mode 100644 index 0000000..bde3b9f --- /dev/null +++ b/cmd/cvet/check_query.go @@ -0,0 +1,25 @@ +package main + +import ( + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" +) + +func isQueryFunc(node *ast.FuncDecl) bool { + return strings.HasSuffix(node.Name.Name, "Query") +} + +func checkQueryFunc(pass *analysis.Pass, node *ast.FuncDecl) { + if node.Type.Results.NumFields() != 3 { + pass.Reportf(node.Pos(), "query function must have 3 return values") + } else { + if !isIdent(node.Type.Results.List[1].Type, "int") { + pass.Reportf(node.Pos(), "query function must return an int code as the second return value") + } + if !isIdent(node.Type.Results.List[2].Type, "error") { + pass.Reportf(node.Pos(), "query function must return an error as the third return value") + } + } +} diff --git a/cmd/cvet/code_smells.go b/cmd/cvet/code_smells.go new file mode 100644 index 0000000..f306d85 --- /dev/null +++ b/cmd/cvet/code_smells.go @@ -0,0 +1,64 @@ +package main + +import ( + "go/ast" + + "github.com/samber/lo" + "golang.org/x/tools/go/analysis" +) + +var codeSmellsAnalyzer = &analysis.Analyzer{ + Name: "logging", + Doc: "Logging best practices", + Run: run, +} + +func isIdent(node ast.Node, name string) bool { + if ident, ok := node.(*ast.Ident); ok { + return ident.Name == name + } + return false +} + +func isSelector(node ast.Node, left string, right string) bool { + if selector, ok := node.(*ast.SelectorExpr); ok { + return isIdent(selector.X, left) && isIdent(selector.Sel, right) + } + return false +} + +func visit(pass *analysis.Pass) func(node ast.Node) bool { + return func(node ast.Node) bool { + switch node := node.(type) { + case *ast.CallExpr: + checkLogUsage(pass, node) + case *ast.FuncDecl: + if isQueryFunc(node) { + checkQueryFunc(pass, node) + } + if isCommandFunc(node) { + checkCommandFunc(pass, node) + } + if field, ok := lo.Find(node.Type.Params.List, func(field *ast.Field) bool { + return isSelector(field.Type, "context", "Context") + }); ok { + if field != node.Type.Params.List[0] { + pass.Reportf(node.Pos(), "context.Context must be the first parameter") + } + if field.Names[0].Name != "ctx" { + pass.Reportf(node.Pos(), "context.Context parameter must be named ctx") + } + } + case *ast.BlockStmt: + checkLogBeforePanic(pass, node) + } + return true + } +} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, file := range pass.Files { + ast.Inspect(file, visit(pass)) + } + return nil, nil //nolint:nilnil +} diff --git a/cmd/cvet/logger.go b/cmd/cvet/logger.go new file mode 100644 index 0000000..0852f38 --- /dev/null +++ b/cmd/cvet/logger.go @@ -0,0 +1,71 @@ +package main + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" +) + +func checkNotLog(pass *analysis.Pass, node *ast.ExprStmt) { + prevCall, ok := node.X.(*ast.CallExpr) + if !ok { + return + } + selector, ok := prevCall.Fun.(*ast.SelectorExpr) + if !ok { + return + } + switch { + case isIdent(selector.X, "slog"): + case isIdent(selector.X, "logger"): + pass.Reportf(node.Pos(), "no log before panic") + } +} + +func checkLogBeforePanic(pass *analysis.Pass, node *ast.BlockStmt) { + for i, stmt := range node.List { + expr, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + call, ok := expr.X.(*ast.CallExpr) + if !ok { + continue + } + if i > 0 && isIdent(call.Fun, "panic") { + if prev, ok := node.List[i-1].(*ast.ExprStmt); ok { + checkNotLog(pass, prev) + } + } + } +} + +func checkLogWithArgs(node *ast.CallExpr, pass *analysis.Pass) { + for _, arg := range node.Args { + call, ok := arg.(*ast.CallExpr) + if !ok { + continue + } + if selector, ok := call.Fun.(*ast.SelectorExpr); ok { + ident, ok := selector.X.(*ast.Ident) + if ok && ident.Name != "slog" { + pass.Reportf(node.Pos(), "slog.With and logger.With must be called with a slog arg") + } + } else { + pass.Reportf(node.Pos(), "slog.With and logger.With must be called with a slog arg") + } + } +} + +func checkLogUsage(pass *analysis.Pass, node *ast.CallExpr) { + if selector, ok := node.Fun.(*ast.SelectorExpr); ok { + if ident, ok := selector.X.(*ast.Ident); ok { + if ident.Name == "log" { + pass.Reportf(node.Pos(), "found old log usage") + } + } + } + if isSelector(node.Fun, "slog", "With") || isSelector(node.Fun, "logger", "With") { + checkLogWithArgs(node, pass) + } +} diff --git a/cmd/cvet/main.go b/cmd/cvet/main.go new file mode 100644 index 0000000..540515f --- /dev/null +++ b/cmd/cvet/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "golang.org/x/tools/go/analysis/multichecker" +) + +func main() { + multichecker.Main(codeSmellsAnalyzer) +} |
