...
1
2
3
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
40
41 func run(pass *analysis.Pass) (any, error) {
42 if !inTest {
43
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
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
71
72
73
74
75
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()
88 isPtr, named := typesinternal.ReceiverNamed(recv)
89 return isPtr && analysisutil.IsNamedType(named, "net/http", "ServeMux")
90 }
91
92
93
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
100 }
101 if f.Type().(*types.Signature).Recv() == nil {
102 return false
103 }
104 for _, n := range names {
105 if f.Name() == n {
106 return true
107 }
108 }
109 return false
110 }
111
112
113
114
115
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
125
126 var wildcardRegexp = regexp.MustCompile(`/\{[_\pL][_\pL\p{Nd}]*(\.\.\.)?\}`)
127
128
129
130 func likelyEnhancedPattern(pat string) bool {
131 if strings.Contains(pat, " ") {
132
133 return true
134 }
135 return wildcardRegexp.MatchString(pat)
136 }
137
138 func goVersionAfter121(goVersion string) bool {
139 if goVersion == "" {
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
148 if p, ok := any(pkg).(interface{ GoVersion() string }); ok {
149 return p.GoVersion()
150 }
151 return ""
152 }
153
154 var (
155
156
157
158
159
160
161 tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc)(\d+))?$`)
162 )
163
164
165 func versionFromGoVersion(goVersion string) string {
166
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 }
188
View as plain text