linters: add httpstatus linter

This commit is contained in:
2026-10-18 15:02:44 -07:00
parent 7b5000b663
commit 9f10798be1
9 changed files with 315 additions and 0 deletions

153
pkg/httpstatus/analyzer.go Normal file
View File

@@ -0,0 +1,153 @@
// Package httpstatus reports discouraged HTTP status-code usage.
package httpstatus
import (
"go/ast"
"go/constant"
"go/types"
"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.Reportf(node.Pos(), "HTTP status code %d should use its integer literal", 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
}