11package analyzer
22
33import (
4+ "fmt"
45 "go/ast"
56 "go/token"
67 "regexp"
@@ -12,8 +13,8 @@ import (
1213)
1314
1415var Analyzer = & analysis.Analyzer {
15- Name : "gosprintfhostport " ,
16- Doc : "Checks that sprintf is not used to construct a host:port combination in a URL." ,
16+ Name : "nosprintfhostport " ,
17+ Doc : "Checks for misuse of Sprintf to construct a host with port in a URL." ,
1718 Run : run ,
1819 Requires : []* analysis.Analyzer {inspect .Analyzer },
1920}
@@ -26,53 +27,70 @@ func run(pass *analysis.Pass) (interface{}, error) {
2627
2728 inspector .Preorder (nodeFilter , func (node ast.Node ) {
2829 callExpr := node .(* ast.CallExpr )
29-
30- selector , ok := callExpr .Fun .(* ast.SelectorExpr )
31- if ! ok {
32- return
33- }
34- pkg , ok := selector .X .(* ast.Ident )
35- if ! ok {
36- return
37- }
38- if pkg .Name != "fmt" || selector .Sel .Name != "Sprintf" {
39- return
30+ if p , f , ok := getCallExprFunction (callExpr ); ok && p == "fmt" && f == "Sprintf" {
31+ if err := checkForHostPortConstruction (callExpr ); err != nil {
32+ pass .Reportf (node .Pos (), err .Error ())
33+ }
4034 }
35+ })
4136
42- if len (callExpr .Args ) < 2 {
43- return
44- }
37+ return nil , nil
38+ }
4539
46- // Let's see if our format string is a string literal.
47- fsRaw , ok := callExpr .Args [0 ].(* ast.BasicLit )
48- if ! ok {
49- return
50- }
51- if fsRaw .Kind != token .STRING {
52- return
53- }
40+ // getCallExprFunction returns the package and function name from a callExpr, if any.
41+ func getCallExprFunction (callExpr * ast.CallExpr ) (pkg string , fn string , result bool ) {
42+ selector , ok := callExpr .Fun .(* ast.SelectorExpr )
43+ if ! ok {
44+ return "" , "" , false
45+ }
46+ gopkg , ok := selector .X .(* ast.Ident )
47+ if ! ok {
48+ return "" , "" , false
49+ }
50+ return gopkg .Name , selector .Sel .Name , true
51+ }
5452
55- // Remove quotes
56- fs := fsRaw .Value [1 : len (fsRaw .Value )- 1 ]
53+ // getStringLiteral returns the value at a position if it's a string literal.
54+ func getStringLiteral (args []ast.Expr , pos int ) (string , bool ) {
55+ if len (args ) < pos + 1 {
56+ return "" , false
57+ }
5758
58- regexes := []* regexp.Regexp {
59- // These check to see if it looks like a URI with a port, basically scheme://%s:<something else>,
60- // or scheme://user:pass@%s:<something else>.
61- // Matching requirements:
62- // - Scheme as per RFC3986 is ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
63- // - A format string substitution in the host portion, preceded by an optional username/password@
64- // - A colon indicating a port will be specified
65- regexp .MustCompile (`^[a-zA-Z0-9+-.]*://%s:[^@]*$` ),
66- regexp .MustCompile (`^[a-zA-Z0-9+-.]*://[^/]*@%s:.*$` ),
67- }
59+ // Let's see if our format string is a string literal.
60+ fsRaw , ok := args [pos ].(* ast.BasicLit )
61+ if ! ok {
62+ return "" , false
63+ }
64+ if fsRaw .Kind == token .STRING && len (fsRaw .Value ) >= 2 {
65+ return fsRaw .Value [1 : len (fsRaw .Value )- 1 ], true
66+ } else {
67+ return "" , false
68+ }
69+ }
6870
69- for _ , re := range regexes {
70- if re .MatchString (fs ) {
71- pass .Reportf (node .Pos (), "host:port in url should be constructed with net.JoinHostPort and not directly with fmt.Sprintf" )
72- break
73- }
71+ // checkForHostPortConstruction checks to see if a sprintf call looks like a URI with a port,
72+ // essentially scheme://%s:<something else>, or scheme://user:pass@%s:<something else>.
73+ //
74+ // Matching requirements:
75+ // - Scheme as per RFC3986 is ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
76+ // - A format string substitution in the host portion, preceded by an optional username/password@
77+ // - A colon indicating a port will be specified
78+ func checkForHostPortConstruction (sprintf * ast.CallExpr ) error {
79+ fs , ok := getStringLiteral (sprintf .Args , 0 )
80+ if ! ok {
81+ return nil
82+ }
83+
84+ regexes := []* regexp.Regexp {
85+ regexp .MustCompile (`^[a-zA-Z][a-zA-Z0-9+-.]*://%s:[^@]*$` ), // URL without basic auth user
86+ regexp .MustCompile (`^[a-zA-Z][a-zA-Z0-9+-.]*://[^/]*@%s:.*$` ), // URL with basic auth
87+ }
88+
89+ for _ , re := range regexes {
90+ if re .MatchString (fs ) {
91+ return fmt .Errorf ("host:port in url should be constructed with net.JoinHostPort and not directly with fmt.Sprintf" )
7492 }
75- })
93+ }
7694
77- return nil , nil
95+ return nil
7896}
0 commit comments