Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions pkg/linters/httpstatuscode/httpstatuscode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
}
Expand Down Expand Up @@ -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
}