168 lines
4.6 KiB
Go
168 lines
4.6 KiB
Go
// 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
|
|
}
|