summaryrefslogtreecommitdiff
path: root/cmd/cvet
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2023-10-24 07:34:00 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2023-10-24 07:34:00 +0200
commit435397de49adc9740bc67416166aba4d0be8ae2a (patch)
tree1642dccae517840f3256f35e29e8d7d1802f700f /cmd/cvet
parent18476c6cba2d5d2bb3ecf3ad136dacbf41d0360e (diff)
feat: Add cmds
Diffstat (limited to 'cmd/cvet')
-rw-r--r--cmd/cvet/check_command.go104
-rw-r--r--cmd/cvet/check_query.go25
-rw-r--r--cmd/cvet/code_smells.go64
-rw-r--r--cmd/cvet/logger.go71
-rw-r--r--cmd/cvet/main.go9
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)
+}