Skip to content

Commit 0caca12

Browse files
committed
Rename to nosprintfhostport
1 parent e6c52ff commit 0caca12

File tree

8 files changed

+84
-55
lines changed

8 files changed

+84
-55
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
with:
1919
go-version: 1.17
2020

21-
- name: Build
21+
- name: All
2222
run: make
2323

2424
- name: Test

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
gosprintfhostport.so
1+
nosprintfhostport.so
2+
nosprintfhostport
3+

Makefile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
all: gosprintfhostport.so
1+
all: nosprintfhostport.so nosprintfhostport
22
.PHONY: lint test
3+
34
clean:
4-
rm gosprintfhostport.so
5+
rm -f nosprintfhostport.so nosprintfhostport
56

67
test:
78
go test ./...
89

910
lint:
1011
golangci-lint run ./...
1112

12-
gosprintfhostport.so:
13-
go build -buildmode=plugin plugin/gosprintfhostport.go
13+
nosprintfhostport:
14+
go build ./cmd/nosprintfhostport
15+
16+
nosprintfhostport.so:
17+
go build -buildmode=plugin ./plugin/nosprintfhostport.go
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"golang.org/x/tools/go/analysis/singlechecker"
55

6-
"github.com/stbenjam/go-sprintf-host-port/pkg/analyzer"
6+
"github.com/stbenjam/no-sprintf-host-port/pkg/analyzer"
77
)
88

99
func main() {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/stbenjam/go-sprintf-host-port
1+
module github.com/stbenjam/no-sprintf-host-port
22

33
go 1.16
44

pkg/analyzer/analyzer.go

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package analyzer
22

33
import (
4+
"fmt"
45
"go/ast"
56
"go/token"
67
"regexp"
@@ -12,8 +13,8 @@ import (
1213
)
1314

1415
var 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
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"golang.org/x/tools/go/analysis"
55

6-
"github.com/stbenjam/go-sprintf-host-port/pkg/analyzer"
6+
"github.com/stbenjam/no-sprintf-host-port/pkg/analyzer"
77
)
88

99
type analyzerPlugin struct{}

testdata/src/p/p.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import (
66
)
77

88
func _() {
9-
_ = fmt.Sprintf("gopher://%s/foo", net.JoinHostPort("foo", "80"))
109

1110
_ = fmt.Sprintf("postgres://%s:%s@127.0.0.1/%s", "foo", "bar", "baz")
1211

13-
_ = fmt.Sprintf("http://%s/foo", net.JoinHostPort("foo", "80"))
12+
_ = fmt.Sprintf("http://api.%s/foo", "example.com")
1413

1514
_ = fmt.Sprintf("http://api.%s:6443/foo", "example.com")
1615

17-
_ = fmt.Sprintf("http://api.%s/foo", "example.com")
16+
_ = fmt.Sprintf("http://%s/foo", net.JoinHostPort("foo", "80"))
17+
18+
_ = fmt.Sprintf("9invalidscheme://%s:%d", "myHost", 70)
19+
20+
_ = fmt.Sprintf("gopher://%s/foo", net.JoinHostPort("foo", "80"))
1821

1922
_ = fmt.Sprintf("telnet+ssl://%s/foo", net.JoinHostPort("foo", "80"))
2023

@@ -28,6 +31,8 @@ func _() {
2831

2932
_ = fmt.Sprintf("telnet+ssl://%s:%d", "myHost", 23) // want "should be constructed with net.JoinHostPort"
3033

34+
_ = fmt.Sprintf("weird3.6://%s:%d", "myHost", 23) // want "should be constructed with net.JoinHostPort"
35+
3136
_ = fmt.Sprintf("https://user@%s:%d", "myHost", 8443) // want "should be constructed with net.JoinHostPort"
3237

3338
_ = fmt.Sprintf("postgres://%s:%s@%s:5050/%s", "foo", "bar", "baz", "qux") // want "should be constructed with net.JoinHostPort"

0 commit comments

Comments
 (0)