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  }