WIP: rebase for 2026-05-18 #7

Draft
codinget wants to merge 234 commits from rebase/2026-05-18 into webnet
8 changed files with 268 additions and 17 deletions
Showing only changes of commit 9bb7ca6116 - Show all commits
+132
View File
@@ -0,0 +1,132 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package lowerell forbids variables named "l" (lowercase ell) or "I"
// (uppercase i), because they are hard to distinguish from the digit
// "1" and from each other in too many fonts.
package lowerell
import (
"go/ast"
"go/token"
"golang.org/x/tools/go/analysis"
)
// Analyzer reports variables named "l" (lowercase ell) or "I" (uppercase i).
var Analyzer = &analysis.Analyzer{
Name: "lowerell",
Doc: `forbid variables named "l" (lowercase ell) or "I" (uppercase i), which are hard to distinguish from "1"`,
Run: run,
}
// messages maps a banned identifier name to the diagnostic shown to users.
// Each message names the specific symbol that triggered it, so the
// reader does not have to guess which of "l" or "I" they typed.
var messages = map[string]string{
"l": `do not use "l" (lowercase ell) as a variable name; it is hard to distinguish from "1" and "I" in too many fonts; see https://github.com/tailscale/tailscale/issues/19631`,
"I": `do not use "I" (uppercase i) as a variable name; it is hard to distinguish from "1" and "l" in too many fonts; see https://github.com/tailscale/tailscale/issues/19631`,
}
// reported tracks identifier positions already reported, to avoid duplicate
// diagnostics when the same declaration is reachable from multiple AST nodes.
type reportedSet map[token.Pos]bool
func (rs reportedSet) check(pass *analysis.Pass, ident *ast.Ident) {
if ident == nil {
return
}
msg, ok := messages[ident.Name]
if !ok {
return
}
if rs[ident.Pos()] {
return
}
rs[ident.Pos()] = true
pass.Reportf(ident.Pos(), "%s", msg)
}
func (rs reportedSet) checkFieldList(pass *analysis.Pass, fl *ast.FieldList) {
if fl == nil {
return
}
for _, f := range fl.List {
for _, n := range f.Names {
rs.check(pass, n)
}
}
}
func run(pass *analysis.Pass) (any, error) {
rs := reportedSet{}
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.FuncDecl:
// Receiver name.
rs.checkFieldList(pass, n.Recv)
// Parameters, results, and type parameters
// are checked via the FuncType case below.
case *ast.FuncType:
rs.checkFieldList(pass, n.TypeParams)
rs.checkFieldList(pass, n.Params)
rs.checkFieldList(pass, n.Results)
case *ast.StructType:
rs.checkFieldList(pass, n.Fields)
case *ast.GenDecl:
if n.Tok != token.VAR && n.Tok != token.CONST {
return true
}
for _, spec := range n.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for _, name := range vs.Names {
rs.check(pass, name)
}
}
case *ast.AssignStmt:
if n.Tok != token.DEFINE {
return true
}
for _, lhs := range n.Lhs {
if id, ok := lhs.(*ast.Ident); ok {
rs.check(pass, id)
}
}
case *ast.RangeStmt:
if n.Tok != token.DEFINE {
return true
}
if id, ok := n.Key.(*ast.Ident); ok {
rs.check(pass, id)
}
if id, ok := n.Value.(*ast.Ident); ok {
rs.check(pass, id)
}
case *ast.TypeSwitchStmt:
// switch l := x.(type) { ... }
as, ok := n.Assign.(*ast.AssignStmt)
if !ok || as.Tok != token.DEFINE {
return true
}
for _, lhs := range as.Lhs {
if id, ok := lhs.(*ast.Ident); ok {
rs.check(pass, id)
}
}
}
return true
})
}
return nil, nil
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package lowerell
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "example")
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package example
import "sync"
// Bad: var declarations.
var l int // want `do not use "l"`
var I int // want `do not use "I"`
// OK: variables named "ll", "II", "i" are fine.
var (
ll int
II int
i int
)
// Bad: const declaration in a function scope.
func F0() {
const l = 3 // want `do not use "l"`
const I = 4 // want `do not use "I"`
_ = l
_ = I
}
// Bad: function parameters.
func F1a(l int) {} // want `do not use "l"`
func F1b(I int) {} // want `do not use "I"`
// Bad: named return values.
func F2a() (l int) { return } // want `do not use "l"`
func F2b() (I int) { return } // want `do not use "I"`
// Bad: receiver names.
type T struct{}
func (l *T) Ml() {} // want `do not use "l"`
func (I *T) MI() {} // want `do not use "I"`
// Bad: struct fields.
type S struct {
l int // want `do not use "l"`
I int // want `do not use "I"`
}
// Bad: short variable declarations.
func F3() {
l := 1 // want `do not use "l"`
I := 2 // want `do not use "I"`
_ = l
_ = I
}
// Bad: var statement inside a function.
func F4() {
var l int // want `do not use "l"`
var I int // want `do not use "I"`
_ = l
_ = I
}
// Bad: range key/value.
func F5(xs []int) {
for l, v := range xs { // want `do not use "l"`
_ = l
_ = v
}
for _, I := range xs { // want `do not use "I"`
_ = I
}
}
// Bad: type parameters.
func F6a[l any](x l) l { return x } // want `do not use "l"`
func F6b[I any](x I) I { return x } // want `do not use "I"`
// Bad: type switch guards.
func F7(x any) {
switch l := x.(type) { // want `do not use "l"`
case int:
_ = l
}
switch I := x.(type) { // want `do not use "I"`
case int:
_ = I
}
}
// OK: clean code with no banned variables.
func F8() {
count := 0
for i := 0; i < 10; i++ {
count++
}
_ = count
}
// OK: sync.Mutex named "mu".
var mu sync.Mutex
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"golang.org/x/tools/go/analysis/unitchecker"
"tailscale.com/cmd/vet/jsontags"
"tailscale.com/cmd/vet/lowerell"
"tailscale.com/cmd/vet/subtestnames"
)
@@ -21,5 +22,5 @@ func init() {
}
func main() {
unitchecker.Main(jsontags.Analyzer, subtestnames.Analyzer)
unitchecker.Main(jsontags.Analyzer, lowerell.Analyzer, subtestnames.Analyzer)
}
+9 -9
View File
@@ -239,7 +239,7 @@ func TestLOCK(t *testing.T) {
}
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
s.local.l.Addr(),
s.local.ln.Addr(),
url.PathEscape(domain),
url.PathEscape(remote1),
url.PathEscape(share11),
@@ -365,7 +365,7 @@ func TestUNLOCK(t *testing.T) {
}
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
s.local.l.Addr(),
s.local.ln.Addr(),
url.PathEscape(domain),
url.PathEscape(remote1),
url.PathEscape(share11),
@@ -428,12 +428,12 @@ func TestUNLOCK(t *testing.T) {
}
type local struct {
l net.Listener
ln net.Listener
fs *FileSystemForLocal
}
type remote struct {
l net.Listener
ln net.Listener
fs *FileSystemForRemote
fileServer *FileServer
shares map[string]string
@@ -487,7 +487,7 @@ func newSystem(t *testing.T) *system {
client.SetTransport(&http.Transport{DisableKeepAlives: true})
s := &system{
t: t,
local: &local{l: ln, fs: fs},
local: &local{ln: ln, fs: fs},
client: client,
remotes: make(map[string]*remote),
}
@@ -510,7 +510,7 @@ func (s *system) addRemote(name string) string {
s.t.Logf("FileServer for %v listening at %s", name, fileServer.Addr())
r := &remote{
l: ln,
ln: ln,
fileServer: fileServer,
fs: NewFileSystemForRemote(log.Printf),
shares: make(map[string]string),
@@ -524,7 +524,7 @@ func (s *system) addRemote(name string) string {
for name, r := range s.remotes {
remotes = append(remotes, &drive.Remote{
Name: name,
URL: func() string { return fmt.Sprintf("http://%s", r.l.Addr()) },
URL: func() string { return fmt.Sprintf("http://%s", r.ln.Addr()) },
})
}
s.local.fs.SetRemotes(
@@ -683,7 +683,7 @@ func (s *system) stop() {
s.t.Fatalf("failed to Close fs: %s", err)
}
err = s.local.l.Close()
err = s.local.ln.Close()
if err != nil {
s.t.Fatalf("failed to Close listener: %s", err)
}
@@ -694,7 +694,7 @@ func (s *system) stop() {
s.t.Fatalf("failed to Close remote fs: %s", err)
}
err = r.l.Close()
err = r.ln.Close()
if err != nil {
s.t.Fatalf("failed to Close remote listener: %s", err)
}
+5 -5
View File
@@ -86,10 +86,10 @@ metricname{foo="si",bar="si"} 5
func TestMultiLabelMapTypes(t *testing.T) {
type LabelTypes struct {
S string
B bool
I int
U uint
S string
B bool
Int int
U uint
}
m := new(MultiLabelMap[LabelTypes])
@@ -100,7 +100,7 @@ func TestMultiLabelMapTypes(t *testing.T) {
m.WritePrometheus(&buf, "metricname")
const want = `# TYPE metricname counter
# HELP metricname some good stuff
metricname{s="a",b="true",i="-1",u="2"} 3
metricname{s="a",b="true",int="-1",u="2"} 3
`
if got := buf.String(); got != want {
t.Errorf("got %q; want %q", got, want)
+3
View File
@@ -41,6 +41,9 @@ func TestPackageDocs(t *testing.T) {
if fi.Mode().IsDir() && path != "." && strings.HasPrefix(filepath.Base(path), ".") {
return filepath.SkipDir // No documentation lives in dot directories (.git, .claude, etc)
}
if fi.Mode().IsDir() && filepath.Base(path) == "testdata" {
return filepath.SkipDir // testdata is ignored by the go tool; not real packages
}
if fi.Mode().IsRegular() && strings.HasSuffix(path, ".go") {
if strings.HasSuffix(path, "_test.go") {
return nil
+2 -2
View File
@@ -59,7 +59,7 @@ func TestHash(t *testing.T) {
I16 int16
I32 int32
I64 int64
I int
Int int
U8 uint8
U16 uint16
U32 uint32
@@ -92,7 +92,7 @@ func TestHash(t *testing.T) {
{in: tuple{scalars{I16: math.MinInt16}, scalars{I16: math.MinInt16 / 2}}, wantEq: false},
{in: tuple{scalars{I32: math.MinInt32}, scalars{I32: math.MinInt32 / 2}}, wantEq: false},
{in: tuple{scalars{I64: math.MinInt64}, scalars{I64: math.MinInt64 / 2}}, wantEq: false},
{in: tuple{scalars{I: -1234}, scalars{I: -1234 / 2}}, wantEq: false},
{in: tuple{scalars{Int: -1234}, scalars{Int: -1234 / 2}}, wantEq: false},
{in: tuple{scalars{U8: math.MaxUint8}, scalars{U8: math.MaxUint8 / 2}}, wantEq: false},
{in: tuple{scalars{U16: math.MaxUint16}, scalars{U16: math.MaxUint16 / 2}}, wantEq: false},
{in: tuple{scalars{U32: math.MaxUint32}, scalars{U32: math.MaxUint32 / 2}}, wantEq: false},