Skip to content

Commit a0a0bd5

Browse files
authored
fix(httpstatuscode): replace spelling-based gating with type-aware detection (#42009)
1 parent 6be90bd commit a0a0bd5

2 files changed

Lines changed: 131 additions & 2 deletions

File tree

pkg/linters/httpstatuscode/httpstatuscode.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"go/token"
99
"go/types"
1010
"strconv"
11+
"strings"
1112

1213
"golang.org/x/tools/go/analysis"
1314
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -173,9 +174,24 @@ func extractStatusLiteral(expr *ast.BinaryExpr) (*ast.BasicLit, ast.Expr) {
173174
func isHTTPStatusContext(pass *analysis.Pass, expr ast.Expr) bool {
174175
switch e := expr.(type) {
175176
case *ast.Ident:
176-
return e.Name == "status" || e.Name == "statusCode"
177+
obj, ok := pass.TypesInfo.Uses[e]
178+
if !ok {
179+
return false
180+
}
181+
t := obj.Type()
182+
if !isIntegerType(t) {
183+
return false
184+
}
185+
// For named integer types (custom enums/aliases), check whether the type
186+
// name itself indicates HTTP status to avoid false positives on non-HTTP
187+
// integer types (e.g. type JobState int).
188+
if named, isNamed := t.(*types.Named); isNamed {
189+
return isHTTPStatusTypeName(named.Obj().Name())
190+
}
191+
// For plain integer types, fall back to variable name heuristic.
192+
return isHTTPStatusVarName(e.Name)
177193
case *ast.SelectorExpr:
178-
if e.Sel.Name != "StatusCode" {
194+
if !isHTTPStatusFieldName(e.Sel.Name) {
179195
return false
180196
}
181197
if sel, ok := pass.TypesInfo.Selections[e]; ok {
@@ -194,6 +210,35 @@ func isHTTPStatusContext(pass *analysis.Pass, expr ast.Expr) bool {
194210
return false
195211
}
196212

213+
// isHTTPStatusVarName returns true if a plain-integer variable/parameter name
214+
// suggests it holds an HTTP status code.
215+
func isHTTPStatusVarName(name string) bool {
216+
switch name {
217+
case "status", "statusCode", "httpStatus":
218+
return true
219+
}
220+
return false
221+
}
222+
223+
// isHTTPStatusFieldName returns true if a struct field name suggests HTTP status.
224+
// Accepts StatusCode, Status, and HTTPStatus to cover common response field spellings.
225+
func isHTTPStatusFieldName(name string) bool {
226+
switch name {
227+
case "StatusCode", "Status", "HTTPStatus":
228+
return true
229+
}
230+
return false
231+
}
232+
233+
// isHTTPStatusTypeName returns true if a named integer type's name indicates that
234+
// it represents an HTTP status code (e.g. HTTPStatusCode, HTTPStatus).
235+
// Both "http" and "status" must appear in the name (case-insensitive) to avoid
236+
// matching unrelated HTTP types such as HTTPVersion or HTTPMethod.
237+
func isHTTPStatusTypeName(name string) bool {
238+
lower := strings.ToLower(name)
239+
return strings.Contains(lower, "http") && strings.Contains(lower, "status")
240+
}
241+
197242
func isIntegerType(t types.Type) bool {
198243
basic, ok := t.Underlying().(*types.Basic)
199244
return ok && basic.Info()&types.IsInteger != 0

pkg/linters/httpstatuscode/testdata/src/httpstatuscode/httpstatuscode.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ func compareStatusCode(statusCode int) {
3232
}
3333
}
3434

35+
func compareHTTPStatus(httpStatus int) {
36+
if httpStatus == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200`
37+
}
38+
if httpStatus == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404`
39+
}
40+
}
41+
3542
func compareResponse(resp *http.Response) {
3643
if resp.StatusCode == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200`
3744
}
@@ -118,3 +125,80 @@ func compareCustomIntStatusCode(r customResponse) {
118125
if r.StatusCode == 418 { // want `use http\.StatusTeapot instead of magic HTTP status code 418`
119126
}
120127
}
128+
129+
// httpEntry is a response type with a field named Status (not StatusCode).
130+
type httpEntry struct {
131+
Status int
132+
}
133+
134+
func compareFieldStatus(entry httpEntry) {
135+
if entry.Status == 200 { // want `use http\.StatusOK instead of magic HTTP status code 200`
136+
}
137+
if entry.Status == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404`
138+
}
139+
}
140+
141+
func compareSwitchFieldStatus(entry httpEntry) {
142+
switch entry.Status {
143+
case 200: // want `use http\.StatusOK instead of magic HTTP status code 200`
144+
case 500: // want `use http\.StatusInternalServerError instead of magic HTTP status code 500`
145+
}
146+
}
147+
148+
// httpClientInfo is a type with a field named HTTPStatus.
149+
type httpClientInfo struct {
150+
HTTPStatus int
151+
}
152+
153+
func compareFieldHTTPStatus(c httpClientInfo) {
154+
if c.HTTPStatus == 404 { // want `use http\.StatusNotFound instead of magic HTTP status code 404`
155+
}
156+
if c.HTTPStatus == 500 { // want `use http\.StatusInternalServerError instead of magic HTTP status code 500`
157+
}
158+
}
159+
160+
func compareSwitchFieldHTTPStatus(c httpClientInfo) {
161+
switch c.HTTPStatus {
162+
case 200: // want `use http\.StatusOK instead of magic HTTP status code 200`
163+
case 404: // want `use http\.StatusNotFound instead of magic HTTP status code 404`
164+
}
165+
}
166+
167+
// JobState is a non-HTTP integer enum (state machine). Integer literals that
168+
// happen to fall in the HTTP status-code range (100-599) must not be flagged:
169+
// the type name lacks both "http" and "status", so isHTTPStatusTypeName returns
170+
// false regardless of the variable name.
171+
type JobState int
172+
173+
const (
174+
JobPending JobState = iota
175+
JobRunning
176+
JobDone
177+
)
178+
179+
func compareNonHTTPJobState(state JobState) {
180+
if state == 200 {
181+
}
182+
if state == 404 {
183+
}
184+
}
185+
186+
func compareSwitchNonHTTPJobState(state JobState) {
187+
switch state {
188+
case 200:
189+
case 404:
190+
}
191+
}
192+
193+
func compareNonStatusNamedLocal(resp *http.Response) {
194+
// False negative: plain int local with non-status name requires flow analysis
195+
// to detect, which is out of scope for this linter (tracking value origins
196+
// across assignments would require SSA/dataflow infrastructure). The trade-off
197+
// is documented here intentionally. No want comment = analysistest ensures
198+
// this remains unflagged (any future regression that starts flagging it
199+
// would fail the test).
200+
code := resp.StatusCode
201+
if code == 404 {
202+
}
203+
_ = code
204+
}

0 commit comments

Comments
 (0)