package dashboard import ( "go/ast" "go/parser" "go/token" "reflect" "strconv" "strings" "testing" ) func TestHandlerRequestParametersUseSnakeCase(t *testing.T) { for _, dir := range []string{".", "../api", "../../middleware", "../../pkg/httpx"} { fset := token.NewFileSet() packages, err := parser.ParseDir(fset, dir, nil, 0) if err != nil { t.Fatalf("parse handler directory %s: %v", dir, err) } for _, pkg := range packages { for filename, file := range pkg.Files { ast.Inspect(file, func(node ast.Node) bool { switch value := node.(type) { case *ast.CallExpr: checkHandlerParameterCall(t, filename, value) case *ast.CompositeLit: checkQueryFilterParameter(t, filename, value) case *ast.Field: checkHandlerRequestTag(t, filename, value) } return true }) } } } } func checkHandlerParameterCall(t *testing.T, filename string, call *ast.CallExpr) { if identifier, ok := call.Fun.(*ast.Ident); ok { if identifier.Name == "requestExternalValue" && len(call.Args) > 2 { checkSnakeCaseStringLiteral(t, filename, call.Args[2]) } return } selector, ok := call.Fun.(*ast.SelectorExpr) if !ok { return } identifier, ok := selector.X.(*ast.Ident) if !ok { return } argIndex := -1 if identifier.Name == "params" { switch selector.Sel.Name { case "Get", "GetInt", "GetInt64", "GetBool", "GetTime", "GetInt64Arr", "FormValue": argIndex = 1 } } else if identifier.Name == "ctx" { switch selector.Sel.Name { case "Query", "DefaultQuery", "GetQuery", "QueryArray", "PostForm", "PostFormArray": argIndex = 0 } } if argIndex < 0 || len(call.Args) <= argIndex { return } checkSnakeCaseStringLiteral(t, filename, call.Args[argIndex]) } func checkQueryFilterParameter(t *testing.T, filename string, literal *ast.CompositeLit) { selector, ok := literal.Type.(*ast.SelectorExpr) if !ok || selector.Sel.Name != "QueryFilter" { return } identifier, ok := selector.X.(*ast.Ident) if !ok || identifier.Name != "params" { return } for _, element := range literal.Elts { pair, ok := element.(*ast.KeyValueExpr) if !ok { continue } key, ok := pair.Key.(*ast.Ident) if ok && key.Name == "ParamName" { checkSnakeCaseStringLiteral(t, filename, pair.Value) } } } func checkHandlerRequestTag(t *testing.T, filename string, field *ast.Field) { if field.Tag == nil { return } rawTag, err := strconv.Unquote(field.Tag.Value) if err != nil { t.Errorf("%s: invalid struct tag %s: %v", filename, field.Tag.Value, err) return } for _, key := range []string{"json", "form", "query", "uri"} { name := strings.Split(reflect.StructTag(rawTag).Get(key), ",")[0] if name != "" && name != "-" && !isSnakeCaseHandlerParameter(name) { t.Errorf("%s: %s tag %q must use snake_case", filename, key, name) } } } func checkSnakeCaseStringLiteral(t *testing.T, filename string, expression ast.Expr) { literal, ok := expression.(*ast.BasicLit) if !ok || literal.Kind != token.STRING { return } name, err := strconv.Unquote(literal.Value) if err != nil { t.Errorf("%s: invalid request parameter %s: %v", filename, literal.Value, err) return } if !isSnakeCaseHandlerParameter(name) { t.Errorf("%s: request parameter %q must use snake_case", filename, name) } } func isSnakeCaseHandlerParameter(name string) bool { for _, r := range name { if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' { continue } return false } return name != "" }