golang.org/x/tools@v0.21.0/go/analysis/passes/httpmux/httpmux.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package httpmux 6 7 import ( 8 "go/ast" 9 "go/constant" 10 "go/types" 11 "regexp" 12 "strings" 13 14 "golang.org/x/mod/semver" 15 "golang.org/x/tools/go/analysis" 16 "golang.org/x/tools/go/analysis/passes/inspect" 17 "golang.org/x/tools/go/analysis/passes/internal/analysisutil" 18 "golang.org/x/tools/go/ast/inspector" 19 "golang.org/x/tools/go/types/typeutil" 20 "golang.org/x/tools/internal/typesinternal" 21 ) 22 23 const Doc = `report using Go 1.22 enhanced ServeMux patterns in older Go versions 24 25 The httpmux analysis is active for Go modules configured to run with Go 1.21 or 26 earlier versions. It reports calls to net/http.ServeMux.Handle and HandleFunc 27 methods whose patterns use features added in Go 1.22, like HTTP methods (such as 28 "GET") and wildcards. (See https://pkg.go.dev/net/http#ServeMux for details.) 29 Such patterns can be registered in older versions of Go, but will not behave as expected.` 30 31 var Analyzer = &analysis.Analyzer{ 32 Name: "httpmux", 33 Doc: Doc, 34 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpmux", 35 Requires: []*analysis.Analyzer{inspect.Analyzer}, 36 Run: run, 37 } 38 39 var inTest bool // So Go version checks can be skipped during testing. 40 41 func run(pass *analysis.Pass) (any, error) { 42 if !inTest { 43 // Check that Go version is 1.21 or earlier. 44 if goVersionAfter121(goVersion(pass.Pkg)) { 45 return nil, nil 46 } 47 } 48 if !analysisutil.Imports(pass.Pkg, "net/http") { 49 return nil, nil 50 } 51 // Look for calls to ServeMux.Handle or ServeMux.HandleFunc. 52 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 53 54 nodeFilter := []ast.Node{ 55 (*ast.CallExpr)(nil), 56 } 57 58 inspect.Preorder(nodeFilter, func(n ast.Node) { 59 call := n.(*ast.CallExpr) 60 if isServeMuxRegisterCall(pass, call) { 61 pat, ok := stringConstantExpr(pass, call.Args[0]) 62 if ok && likelyEnhancedPattern(pat) { 63 pass.ReportRangef(call.Args[0], "possible enhanced ServeMux pattern used with Go version before 1.22 (update go.mod file?)") 64 } 65 } 66 }) 67 return nil, nil 68 } 69 70 // isServeMuxRegisterCall reports whether call is a static call to one of: 71 // - net/http.Handle 72 // - net/http.HandleFunc 73 // - net/http.ServeMux.Handle 74 // - net/http.ServeMux.HandleFunc 75 // TODO(jba): consider expanding this to accommodate wrappers around these functions. 76 func isServeMuxRegisterCall(pass *analysis.Pass, call *ast.CallExpr) bool { 77 fn := typeutil.StaticCallee(pass.TypesInfo, call) 78 if fn == nil { 79 return false 80 } 81 if analysisutil.IsFunctionNamed(fn, "net/http", "Handle", "HandleFunc") { 82 return true 83 } 84 if !isMethodNamed(fn, "net/http", "Handle", "HandleFunc") { 85 return false 86 } 87 recv := fn.Type().(*types.Signature).Recv() // isMethodNamed() -> non-nil 88 isPtr, named := typesinternal.ReceiverNamed(recv) 89 return isPtr && analysisutil.IsNamedType(named, "net/http", "ServeMux") 90 } 91 92 // isMethodNamed reports when a function f is a method, 93 // in a package with the path pkgPath and the name of f is in names. 94 func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { 95 if f == nil { 96 return false 97 } 98 if f.Pkg() == nil || f.Pkg().Path() != pkgPath { 99 return false // not at pkgPath 100 } 101 if f.Type().(*types.Signature).Recv() == nil { 102 return false // not a method 103 } 104 for _, n := range names { 105 if f.Name() == n { 106 return true 107 } 108 } 109 return false // not in names 110 } 111 112 // stringConstantExpr returns expression's string constant value. 113 // 114 // ("", false) is returned if expression isn't a string 115 // constant. 116 func stringConstantExpr(pass *analysis.Pass, expr ast.Expr) (string, bool) { 117 lit := pass.TypesInfo.Types[expr].Value 118 if lit != nil && lit.Kind() == constant.String { 119 return constant.StringVal(lit), true 120 } 121 return "", false 122 } 123 124 // A valid wildcard must start a segment, and its name must be valid Go 125 // identifier. 126 var wildcardRegexp = regexp.MustCompile(`/\{[_\pL][_\pL\p{Nd}]*(\.\.\.)?\}`) 127 128 // likelyEnhancedPattern reports whether the ServeMux pattern pat probably 129 // contains either an HTTP method name or a wildcard, extensions added in Go 1.22. 130 func likelyEnhancedPattern(pat string) bool { 131 if strings.Contains(pat, " ") { 132 // A space in the pattern suggests that it begins with an HTTP method. 133 return true 134 } 135 return wildcardRegexp.MatchString(pat) 136 } 137 138 func goVersionAfter121(goVersion string) bool { 139 if goVersion == "" { // Maybe the stdlib? 140 return true 141 } 142 version := versionFromGoVersion(goVersion) 143 return semver.Compare(version, "v1.21") > 0 144 } 145 146 func goVersion(pkg *types.Package) string { 147 // types.Package.GoVersion did not exist before Go 1.21. 148 if p, ok := any(pkg).(interface{ GoVersion() string }); ok { 149 return p.GoVersion() 150 } 151 return "" 152 } 153 154 var ( 155 // Regexp for matching go tags. The groups are: 156 // 1 the major.minor version 157 // 2 the patch version, or empty if none 158 // 3 the entire prerelease, if present 159 // 4 the prerelease type ("beta" or "rc") 160 // 5 the prerelease number 161 tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc)(\d+))?$`) 162 ) 163 164 // Copied from pkgsite/internal/stdlib.VersionForTag. 165 func versionFromGoVersion(goVersion string) string { 166 // Special cases for go1. 167 if goVersion == "go1" { 168 return "v1.0.0" 169 } 170 if goVersion == "go1.0" { 171 return "" 172 } 173 m := tagRegexp.FindStringSubmatch(goVersion) 174 if m == nil { 175 return "" 176 } 177 version := "v" + m[1] 178 if m[2] != "" { 179 version += m[2] 180 } else { 181 version += ".0" 182 } 183 if m[3] != "" { 184 version += "-" + m[4] + "." + m[5] 185 } 186 return version 187 }