// Package httpstatus reports discouraged HTTP status-code usage. package httpstatus import ( "fmt" "go/ast" "go/constant" "go/types" "strconv" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" ) const netHTTPPath = "net/http" var defaultTypicalStatusCodes = []int{ 200, 204, 302, 400, 401, 403, 404, 429, 502, 504, } var Analyzer = NewAnalyzer(Settings{}) func NewAnalyzer(settings Settings) *analysis.Analyzer { if settings.TypicalStatusCodes == nil { settings.TypicalStatusCodes = defaultTypicalStatusCodes } typicalStatusCodes := make(map[int]struct{}, len(settings.TypicalStatusCodes)) for _, code := range settings.TypicalStatusCodes { typicalStatusCodes[code] = struct{}{} } return &analysis.Analyzer{ Name: "httpstatus", Doc: "checks HTTP status-code usage", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: func(pass *analysis.Pass) (any, error) { insp, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !ok { panic("inspect.Analyzer did not produce *inspector.Inspector") } // --------------- // Checks // --------------- // Disallows `http.StatusBlahBlah` symbols for typical status codes enforceIntegerLiterals := func(node *ast.SelectorExpr) { status, ok := pass.TypesInfo.Uses[node.Sel].(*types.Const) if !ok || status.Pkg() == nil || status.Pkg().Path() != netHTTPPath || !strings.HasPrefix(status.Name(), "Status") { return } // Check if it corresponds to one of the codes in the "typical" list code, ok := integerConstant(status.Val()) if !ok { return } if _, ok := typicalStatusCodes[code]; !ok { return } pass.Report(analysis.Diagnostic{ Pos: node.Pos(), End: node.End(), Message: fmt.Sprintf("HTTP status code %d should use its integer literal", code), SuggestedFixes: []analysis.SuggestedFix{{ Message: "Use the integer literal", TextEdits: []analysis.TextEdit{{ Pos: node.Pos(), End: node.End(), NewText: []byte(strconv.Itoa(code)), }}, }}, }) } // Disallows sending non-typical response statuses requireTypicalResponseStatus := func(node *ast.CallExpr) { checkArgument := func(argIdx int) { // Require a status-code argument. if argIdx < 0 || len(node.Args) <= argIdx { return } // Report non-typical codes. code, ok := integerConstant(pass.TypesInfo.Types[node.Args[argIdx]].Value) if !ok { return } if _, ok := typicalStatusCodes[code]; !ok { pass.Reportf(node.Args[argIdx].Pos(), "non-typical HTTP status code %d is not allowed", code) } } switch functionExpression := node.Fun.(type) { case *ast.Ident: // Handle dot-imported http.Error. function, ok := pass.TypesInfo.Uses[functionExpression].(*types.Func) if ok && function.Pkg() != nil && function.Pkg().Path() == netHTTPPath && function.Name() == "Error" { checkArgument(2) } case *ast.SelectorExpr: // Handle qualified http.Error. function, _ := pass.TypesInfo.Uses[functionExpression.Sel].(*types.Func) if function != nil && function.Pkg() != nil && function.Pkg().Path() == netHTTPPath && function.Name() == "Error" { checkArgument(2) return } // Handle both forms of WriteHeader. selection := pass.TypesInfo.Selections[functionExpression] if selection == nil { return } method, ok := selection.Obj().(*types.Func) if ok && method.Pkg() != nil && method.Pkg().Path() == netHTTPPath && method.Name() == "WriteHeader" { if selection.Kind() == types.MethodExpr { checkArgument(1) // `http.ResponseWriter.WriteHeader` } else { checkArgument(0) // `w.WriteHeader` } } default: return } } // ------------------- // Walk the tree // ------------------- insp.Preorder([]ast.Node{(*ast.SelectorExpr)(nil), (*ast.CallExpr)(nil)}, func(node ast.Node) { switch node := node.(type) { case *ast.SelectorExpr: enforceIntegerLiterals(node) case *ast.CallExpr: if !settings.IsRestricted { return } requireTypicalResponseStatus(node) default: panic("inspector passed an unexpected node") } }) return nil, nil }, } } // integerConstant attempts to extract the value of a constant.Value as an int. func integerConstant(value constant.Value) (int, bool) { if value == nil { return 0, false } integer, ok := constant.Int64Val(value) if !ok || int64(int(integer)) != integer { return 0, false } return int(integer), true }