diff --git a/pkg/linters/httpstatuscode/httpstatuscode.go b/pkg/linters/httpstatuscode/httpstatuscode.go index 8666e817ed1..702e3a1584b 100644 --- a/pkg/linters/httpstatuscode/httpstatuscode.go +++ b/pkg/linters/httpstatuscode/httpstatuscode.go @@ -8,6 +8,7 @@ import ( "go/token" "go/types" "strconv" + "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -173,9 +174,24 @@ func extractStatusLiteral(expr *ast.BinaryExpr) (*ast.BasicLit, ast.Expr) { func isHTTPStatusContext(pass *analysis.Pass, expr ast.Expr) bool { switch e := expr.(type) { case *ast.Ident: - return e.Name == "status" || e.Name == "statusCode" + obj, ok := pass.TypesInfo.Uses[e] + if !ok { + return false + } + t := obj.Type() + if !isIntegerType(t) { + return false + } + // For named integer types (custom enums/aliases), check whether the type + // name itself indicates HTTP status to avoid false positives on non-HTTP + // integer types (e.g. type JobState int). + if named, isNamed := t.(*types.Named); isNamed { + return isHTTPStatusTypeName(named.Obj().Name()) + } + // For plain integer types, fall back to variable name heuristic. + return isHTTPStatusVarName(e.Name) case *ast.SelectorExpr: - if e.Sel.Name != "StatusCode" { + if !isHTTPStatusFieldName(e.Sel.Name) { return false } if sel, ok := pass.TypesInfo.Selections[e]; ok { @@ -194,6 +210,35 @@ func isHTTPStatusContext(pass *analysis.Pass, expr ast.Expr) bool { return false } +// isHTTPStatusVarName returns true if a plain-integer variable/parameter name +// suggests it holds an HTTP status code. +func isHTTPStatusVarName(name string) bool { + switch name { + case "status", "statusCode", "httpStatus": + return true + } + return false +} + +// isHTTPStatusFieldName returns true if a struct field name suggests HTTP status. +// Accepts StatusCode, Status, and HTTPStatus to cover common response field spellings. +func isHTTPStatusFieldName(name string) bool { + switch name { + case "StatusCode", "Status", "HTTPStatus": + return true + } + return false +} + +// isHTTPStatusTypeName returns true if a named integer type's name indicates that +// it represents an HTTP status code (e.g. HTTPStatusCode, HTTPStatus). +// Both "http" and "status" must appear in the name (case-insensitive) to avoid +// matching unrelated HTTP types such as HTTPVersion or HTTPMethod. +func isHTTPStatusTypeName(name string) bool { + lower := strings.ToLower(name) + return strings.Contains(lower, "http") && strings.Contains(lower, "status") +} + func isIntegerType(t types.Type) bool { basic, ok := t.Underlying().(*types.Basic) return ok && basic.Info()&types.IsInteger != 0 diff --git a/pkg/linters/httpstatuscode/testdata/src/httpstatuscode/httpstatuscode.go b/pkg/linters/httpstatuscode/testdata/src/httpstatuscode/httpstatuscode.go index cc67edc907d..234ac578127 100644 --- a/pkg/linters/httpstatuscode/testdata/src/httpstatuscode/httpstatuscode.go +++ b/pkg/linters/httpstatuscode/testdata/src/httpstatuscode/httpstatuscode.go @@ -32,6 +32,13 @@ func compareStatusCode(statusCode int) { } } +func compareHTTPStatus(httpStatus int) { + if httpStatus == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200` + } + if httpStatus == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404` + } +} + func compareResponse(resp *http.Response) { if resp.StatusCode == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200` } @@ -118,3 +125,80 @@ func compareCustomIntStatusCode(r customResponse) { if r.StatusCode == 418 { // want `use http\.StatusTeapot instead of magic HTTP status code 418` } } + +// httpEntry is a response type with a field named Status (not StatusCode). +type httpEntry struct { + Status int +} + +func compareFieldStatus(entry httpEntry) { + if entry.Status == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200` + } + if entry.Status == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404` + } +} + +func compareSwitchFieldStatus(entry httpEntry) { + switch entry.Status { + case 200: // want `use http\.StatusOK instead of magic HTTP status code 200` + case 500: // want `use http\.StatusInternalServerError instead of magic HTTP status code 500` + } +} + +// httpClientInfo is a type with a field named HTTPStatus. +type httpClientInfo struct { + HTTPStatus int +} + +func compareFieldHTTPStatus(c httpClientInfo) { + if c.HTTPStatus == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404` + } + if c.HTTPStatus == 500 { // want `use http\.StatusInternalServerError instead of magic HTTP status code 500` + } +} + +func compareSwitchFieldHTTPStatus(c httpClientInfo) { + switch c.HTTPStatus { + case 200: // want `use http\.StatusOK instead of magic HTTP status code 200` + case 404: // want `use http\.StatusNotFound instead of magic HTTP status code 404` + } +} + +// JobState is a non-HTTP integer enum (state machine). Integer literals that +// happen to fall in the HTTP status-code range (100-599) must not be flagged: +// the type name lacks both "http" and "status", so isHTTPStatusTypeName returns +// false regardless of the variable name. +type JobState int + +const ( + JobPending JobState = iota + JobRunning + JobDone +) + +func compareNonHTTPJobState(state JobState) { + if state == 200 { + } + if state == 404 { + } +} + +func compareSwitchNonHTTPJobState(state JobState) { + switch state { + case 200: + case 404: + } +} + +func compareNonStatusNamedLocal(resp *http.Response) { + // False negative: plain int local with non-status name requires flow analysis + // to detect, which is out of scope for this linter (tracking value origins + // across assignments would require SSA/dataflow infrastructure). The trade-off + // is documented here intentionally. No want comment = analysistest ensures + // this remains unflagged (any future regression that starts flagging it + // would fail the test). + code := resp.StatusCode + if code == 404 { + } + _ = code +}