internal: redirect relevant requests to symbolsearch mode

When a user makes a single word search query containing a dot, there is
a good chance that they are searching for a <package>.<symbol>.

In that case, redirect the request if the <symbol> component does not
match a TLD in our database.

For golang/go#44142

Change-Id: I70ab0a74154b6c292fa649be6b2112f8906e291e
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/343569
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/domain.go b/internal/domain.go
new file mode 100644
index 0000000..8bdb07c
--- /dev/null
+++ b/internal/domain.go
@@ -0,0 +1,186 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package internal
+
+// TopLevelDomains contains all of the top level domains in the discovery
+// database.
+var TopLevelDomains = map[string]bool{
+	"af":          true,
+	"africa":      true,
+	"ag":          true,
+	"agency":      true,
+	"ai":          true,
+	"app":         true,
+	"ar":          true,
+	"as":          true,
+	"at":          true,
+	"au":          true,
+	"beer":        true,
+	"berlin":      true,
+	"biz":         true,
+	"blue":        true,
+	"br":          true,
+	"build":       true,
+	"by":          true,
+	"ca":          true,
+	"cafe":        true,
+	"casa":        true,
+	"cc":          true,
+	"ch":          true,
+	"ci":          true,
+	"city":        true,
+	"cl":          true,
+	"click":       true,
+	"cloud":       true,
+	"club":        true,
+	"cn":          true,
+	"co":          true,
+	"codes":       true,
+	"coffee":      true,
+	"com":         true,
+	"computer":    true,
+	"consulting":  true,
+	"coop":        true,
+	"cx":          true,
+	"cz":          true,
+	"de":          true,
+	"design":      true,
+	"dev":         true,
+	"digital":     true,
+	"direct":      true,
+	"dk":          true,
+	"dog":         true,
+	"download":    true,
+	"earth":       true,
+	"edu":         true,
+	"ee":          true,
+	"engineer":    true,
+	"engineering": true,
+	"es":          true,
+	"eu":          true,
+	"farm":        true,
+	"fi":          true,
+	"fm":          true,
+	"fr":          true,
+	"fun":         true,
+	"fyi":         true,
+	"ga":          true,
+	"gay":         true,
+	"gg":          true,
+	"gmbh":        true,
+	"gov":         true,
+	"gq":          true,
+	"gt":          true,
+	"haus":        true,
+	"host":        true,
+	"ht":          true,
+	"hu":          true,
+	"icu":         true,
+	"id":          true,
+	"ie":          true,
+	"im":          true,
+	"in":          true,
+	"info":        true,
+	"ink":         true,
+	"io":          true,
+	"ir":          true,
+	"is":          true,
+	"it":          true,
+	"jp":          true,
+	"ke":          true,
+	"kr":          true,
+	"kz":          true,
+	"la":          true,
+	"land":        true,
+	"lgbt":        true,
+	"li":          true,
+	"life":        true,
+	"link":        true,
+	"london":      true,
+	"lt":          true,
+	"lv":          true,
+	"me":          true,
+	"media":       true,
+	"mil":         true,
+	"ml":          true,
+	"mn":          true,
+	"moe":         true,
+	"ms":          true,
+	"name":        true,
+	"nc":          true,
+	"net":         true,
+	"network":     true,
+	"ninja":       true,
+	"nl":          true,
+	"no":          true,
+	"nu":          true,
+	"nz":          true,
+	"one":         true,
+	"online":      true,
+	"org":         true,
+	"pe":          true,
+	"pl":          true,
+	"pm":          true,
+	"pro":         true,
+	"pub":         true,
+	"pw":          true,
+	"re":          true,
+	"red":         true,
+	"ren":         true,
+	"rip":         true,
+	"ro":          true,
+	"rocks":       true,
+	"ru":          true,
+	"run":         true,
+	"school":      true,
+	"science":     true,
+	"se":          true,
+	"services":    true,
+	"sh":          true,
+	"site":        true,
+	"sm":          true,
+	"software":    true,
+	"solutions":   true,
+	"space":       true,
+	"st":          true,
+	"std":         true,
+	"studio":      true,
+	"study":       true,
+	"su":          true,
+	"supply":      true,
+	"systems":     true,
+	"taxi":        true,
+	"team":        true,
+	"tech":        true,
+	"technology":  true,
+	"tf":          true,
+	"th":          true,
+	"tickets":     true,
+	"tk":          true,
+	"tm":          true,
+	"to":          true,
+	"today":       true,
+	"tools":       true,
+	"top":         true,
+	"town":        true,
+	"trade":       true,
+	"tv":          true,
+	"tw":          true,
+	"ua":          true,
+	"uk":          true,
+	"us":          true,
+	"uz":          true,
+	"ve":          true,
+	"vip":         true,
+	"vn":          true,
+	"wang":        true,
+	"website":     true,
+	"work":        true,
+	"works":       true,
+	"wtf":         true,
+	"xyz":         true,
+	"yandex":      true,
+	"zone":        true,
+}
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index 4bb1151..ca25d95 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -373,9 +373,23 @@
 	if prefix := searchModePackage + ":"; strings.HasPrefix(q, prefix) {
 		return strings.TrimPrefix(q, prefix), false
 	}
+	if isPackageDotSymbol(q) {
+		return q, true
+	}
 	return q, strings.TrimSpace(r.FormValue("m")) == searchModeSymbol
 }
 
+// isPackageDotSymbol reports whether the search query is of the form
+// <package>.<symbol>. The <symbol> component should not be a top-level domain
+// that is in our database.
+func isPackageDotSymbol(q string) bool {
+	if len(strings.Fields(q)) != 1 || !strings.ContainsAny(q, ".") {
+		return false
+	}
+	parts := strings.Split(q, ".")
+	return !internal.TopLevelDomains[parts[len(parts)-1]]
+}
+
 // elapsedTime takes a date and returns returns human-readable,
 // relative timestamps based on the following rules:
 // (1) 'X hours ago' when X < 6
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index 50aac8e..55580c5 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -394,3 +394,25 @@
 		})
 	}
 }
+
+func TestIsPackageDotSymbol(t *testing.T) {
+	for _, test := range []struct {
+		q    string
+		want bool
+	}{
+		{"barista.run", false},
+		{"github.com", false},
+		{"julie.io", false},
+		{"my.name", false},
+		{"sql", false},
+		{"sql.DB", true},
+		{"sql.DB.Begin", true},
+	} {
+		t.Run(test.q, func(t *testing.T) {
+			got := isPackageDotSymbol(test.q)
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}