linters: add httpstatus linter
This commit is contained in:
153
pkg/httpstatus/analyzer.go
Normal file
153
pkg/httpstatus/analyzer.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user