Files
tailscale/cmd/vet/lowerell/analyzer.go
T
Brad Fitzpatrick 9bb7ca6116 cmd/vet/lowerell, drive/driveimpl: forbid variables named "l" or "I"
Add a new vet checker that rejects variables, parameters, named
return values, receivers, range/type-switch bindings, type
parameters, struct fields, and constants named "l" (lowercase ell)
or "I" (uppercase i). Both are hard to distinguish from the digit
"1" and from each other in too many fonts.

Rename the two pre-existing struct fields named "l" (both of type
net.Listener) in drive/driveimpl/drive_test.go to "ln", matching the
convention used elsewhere for net.Listener locals.

Rename the test-fixture struct fields "I" (single int label) to
"Int" in metrics/multilabelmap_test.go and util/deephash/deephash_test.go,
preserving the "first letters of types" convention used alongside
neighboring fields like I8/I16/U/U8.

Also teach pkgdoc_test.go to skip testdata/ directories, which
the go tool ignores; they are not real packages.

Fixes #19631

Change-Id: I71ad2fa990705f7a070406ebcdb8cefa7487d849
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-05-04 14:03:28 -07:00

133 lines
3.4 KiB
Go

// 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
}