st1003.go raw

   1  package st1003
   2  
   3  import (
   4  	"fmt"
   5  	"go/ast"
   6  	"go/token"
   7  	"strings"
   8  	"unicode"
   9  
  10  	"honnef.co/go/tools/analysis/code"
  11  	"honnef.co/go/tools/analysis/facts/generated"
  12  	"honnef.co/go/tools/analysis/lint"
  13  	"honnef.co/go/tools/analysis/report"
  14  	"honnef.co/go/tools/config"
  15  
  16  	"golang.org/x/tools/go/analysis"
  17  	"golang.org/x/tools/go/analysis/passes/inspect"
  18  )
  19  
  20  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  21  	Analyzer: &analysis.Analyzer{
  22  		Name:     "ST1003",
  23  		Run:      run,
  24  		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer, config.Analyzer},
  25  	},
  26  	Doc: &lint.RawDocumentation{
  27  		Title: `Poorly chosen identifier`,
  28  		Text: `Identifiers, such as variable and package names, follow certain rules.
  29  
  30  See the following links for details:
  31  
  32  - https://go.dev/doc/effective_go#package-names
  33  - https://go.dev/doc/effective_go#mixed-caps
  34  - https://go.dev/wiki/CodeReviewComments#initialisms
  35  - https://go.dev/wiki/CodeReviewComments#variable-names`,
  36  		Since:      "2019.1",
  37  		NonDefault: true,
  38  		Options:    []string{"initialisms"},
  39  		MergeIf:    lint.MergeIfAny,
  40  	},
  41  })
  42  
  43  var Analyzer = SCAnalyzer.Analyzer
  44  
  45  // knownNameExceptions is a set of names that are known to be exempt from naming checks.
  46  // This is usually because they are constrained by having to match names in the
  47  // standard library.
  48  var knownNameExceptions = map[string]bool{
  49  	"LastInsertId": true, // must match database/sql
  50  	"kWh":          true,
  51  }
  52  
  53  func run(pass *analysis.Pass) (interface{}, error) {
  54  	// A large part of this function is copied from
  55  	// github.com/golang/lint, Copyright (c) 2013 The Go Authors,
  56  	// licensed under the BSD 3-clause license.
  57  
  58  	allCaps := func(s string) bool {
  59  		hasUppercaseLetters := false
  60  		for _, r := range s {
  61  			if !hasUppercaseLetters && r >= 'A' && r <= 'Z' {
  62  				hasUppercaseLetters = true
  63  			}
  64  			if !((r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
  65  				return false
  66  			}
  67  		}
  68  		return hasUppercaseLetters
  69  	}
  70  
  71  	check := func(id *ast.Ident, thing string, initialisms map[string]bool) {
  72  		if id.Name == "_" {
  73  			return
  74  		}
  75  		if knownNameExceptions[id.Name] {
  76  			return
  77  		}
  78  
  79  		// Handle two common styles from other languages that don't belong in Go.
  80  		if len(id.Name) >= 5 && allCaps(id.Name) && strings.Contains(id.Name, "_") {
  81  			report.Report(pass, id, "should not use ALL_CAPS in Go names; use CamelCase instead", report.FilterGenerated())
  82  			return
  83  		}
  84  
  85  		should := lintName(id.Name, initialisms)
  86  		if id.Name == should {
  87  			return
  88  		}
  89  
  90  		if len(id.Name) > 2 && strings.Contains(id.Name[1:len(id.Name)-1], "_") {
  91  			report.Report(pass, id, fmt.Sprintf("should not use underscores in Go names; %s %s should be %s", thing, id.Name, should), report.FilterGenerated())
  92  			return
  93  		}
  94  		report.Report(pass, id, fmt.Sprintf("%s %s should be %s", thing, id.Name, should), report.FilterGenerated())
  95  	}
  96  	checkList := func(fl *ast.FieldList, thing string, initialisms map[string]bool) {
  97  		if fl == nil {
  98  			return
  99  		}
 100  		for _, f := range fl.List {
 101  			for _, id := range f.Names {
 102  				check(id, thing, initialisms)
 103  			}
 104  		}
 105  	}
 106  
 107  	il := config.For(pass).Initialisms
 108  	initialisms := make(map[string]bool, len(il))
 109  	for _, word := range il {
 110  		initialisms[word] = true
 111  	}
 112  	for _, f := range pass.Files {
 113  		// Package names need slightly different handling than other names.
 114  		if !strings.HasSuffix(f.Name.Name, "_test") && strings.Contains(f.Name.Name, "_") {
 115  			report.Report(pass, f, "should not use underscores in package names", report.FilterGenerated())
 116  		}
 117  		if strings.IndexFunc(f.Name.Name, unicode.IsUpper) != -1 {
 118  			report.Report(pass, f, fmt.Sprintf("should not use MixedCaps in package name; %s should be %s", f.Name.Name, strings.ToLower(f.Name.Name)), report.FilterGenerated())
 119  		}
 120  	}
 121  
 122  	fn := func(node ast.Node) {
 123  		switch v := node.(type) {
 124  		case *ast.AssignStmt:
 125  			if v.Tok != token.DEFINE {
 126  				return
 127  			}
 128  			for _, exp := range v.Lhs {
 129  				if id, ok := exp.(*ast.Ident); ok {
 130  					check(id, "var", initialisms)
 131  				}
 132  			}
 133  		case *ast.FuncDecl:
 134  			// Functions with no body are defined elsewhere (in
 135  			// assembly, or via go:linkname). These are likely to
 136  			// be something very low level (such as the runtime),
 137  			// where our rules don't apply.
 138  			if v.Body == nil {
 139  				return
 140  			}
 141  
 142  			if code.IsInTest(pass, v) &&
 143  				(strings.HasPrefix(v.Name.Name, "Example") ||
 144  					strings.HasPrefix(v.Name.Name, "Test") ||
 145  					strings.HasPrefix(v.Name.Name, "Benchmark") ||
 146  					strings.HasPrefix(v.Name.Name, "Fuzz")) {
 147  				return
 148  			}
 149  
 150  			thing := "func"
 151  			if v.Recv != nil {
 152  				thing = "method"
 153  			}
 154  
 155  			if !isTechnicallyExported(v) {
 156  				check(v.Name, thing, initialisms)
 157  			}
 158  
 159  			checkList(v.Type.Params, thing+" parameter", initialisms)
 160  			checkList(v.Type.Results, thing+" result", initialisms)
 161  		case *ast.GenDecl:
 162  			if v.Tok == token.IMPORT {
 163  				return
 164  			}
 165  			var thing string
 166  			switch v.Tok {
 167  			case token.CONST:
 168  				thing = "const"
 169  			case token.TYPE:
 170  				thing = "type"
 171  			case token.VAR:
 172  				thing = "var"
 173  			}
 174  			for _, spec := range v.Specs {
 175  				switch s := spec.(type) {
 176  				case *ast.TypeSpec:
 177  					check(s.Name, thing, initialisms)
 178  				case *ast.ValueSpec:
 179  					for _, id := range s.Names {
 180  						check(id, thing, initialisms)
 181  					}
 182  				}
 183  			}
 184  		case *ast.InterfaceType:
 185  			// Do not check interface method names.
 186  			// They are often constrained by the method names of concrete types.
 187  			for _, x := range v.Methods.List {
 188  				ft, ok := x.Type.(*ast.FuncType)
 189  				if !ok { // might be an embedded interface name
 190  					continue
 191  				}
 192  				checkList(ft.Params, "interface method parameter", initialisms)
 193  				checkList(ft.Results, "interface method result", initialisms)
 194  			}
 195  		case *ast.RangeStmt:
 196  			if v.Tok == token.ASSIGN {
 197  				return
 198  			}
 199  			if id, ok := v.Key.(*ast.Ident); ok {
 200  				check(id, "range var", initialisms)
 201  			}
 202  			if id, ok := v.Value.(*ast.Ident); ok {
 203  				check(id, "range var", initialisms)
 204  			}
 205  		case *ast.StructType:
 206  			for _, f := range v.Fields.List {
 207  				for _, id := range f.Names {
 208  					check(id, "struct field", initialisms)
 209  				}
 210  			}
 211  		}
 212  	}
 213  
 214  	needle := []ast.Node{
 215  		(*ast.AssignStmt)(nil),
 216  		(*ast.FuncDecl)(nil),
 217  		(*ast.GenDecl)(nil),
 218  		(*ast.InterfaceType)(nil),
 219  		(*ast.RangeStmt)(nil),
 220  		(*ast.StructType)(nil),
 221  	}
 222  
 223  	code.Preorder(pass, fn, needle...)
 224  	return nil, nil
 225  }
 226  
 227  // lintName returns a different name if it should be different.
 228  func lintName(name string, initialisms map[string]bool) (should string) {
 229  	// A large part of this function is copied from
 230  	// github.com/golang/lint, Copyright (c) 2013 The Go Authors,
 231  	// licensed under the BSD 3-clause license.
 232  
 233  	// Fast path for simple cases: "_" and all lowercase.
 234  	if name == "_" {
 235  		return name
 236  	}
 237  	if strings.IndexFunc(name, func(r rune) bool { return !unicode.IsLower(r) }) == -1 {
 238  		return name
 239  	}
 240  
 241  	// Split camelCase at any lower->upper transition, and split on underscores.
 242  	// Check each word for common initialisms.
 243  	runes := []rune(name)
 244  	w, i := 0, 0 // index of start of word, scan
 245  	for i+1 <= len(runes) {
 246  		eow := false // whether we hit the end of a word
 247  		if i+1 == len(runes) {
 248  			eow = true
 249  		} else if runes[i+1] == '_' && i+1 != len(runes)-1 {
 250  			// underscore; shift the remainder forward over any run of underscores
 251  			eow = true
 252  			n := 1
 253  			for i+n+1 < len(runes) && runes[i+n+1] == '_' {
 254  				n++
 255  			}
 256  
 257  			// Leave at most one underscore if the underscore is between two digits
 258  			if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
 259  				n--
 260  			}
 261  
 262  			copy(runes[i+1:], runes[i+n+1:])
 263  			runes = runes[:len(runes)-n]
 264  		} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
 265  			// lower->non-lower
 266  			eow = true
 267  		}
 268  		i++
 269  		if !eow {
 270  			continue
 271  		}
 272  
 273  		// [w,i) is a word.
 274  		word := string(runes[w:i])
 275  		if u := strings.ToUpper(word); initialisms[u] {
 276  			// Keep consistent case, which is lowercase only at the start.
 277  			if w == 0 && unicode.IsLower(runes[w]) {
 278  				u = strings.ToLower(u)
 279  			}
 280  			// All the common initialisms are ASCII,
 281  			// so we can replace the bytes exactly.
 282  			// TODO(dh): this won't be true once we allow custom initialisms
 283  			copy(runes[w:], []rune(u))
 284  		} else if w > 0 && strings.ToLower(word) == word {
 285  			// already all lowercase, and not the first word, so uppercase the first character.
 286  			runes[w] = unicode.ToUpper(runes[w])
 287  		}
 288  		w = i
 289  	}
 290  	return string(runes)
 291  }
 292  
 293  func isTechnicallyExported(f *ast.FuncDecl) bool {
 294  	if f.Recv != nil || f.Doc == nil {
 295  		return false
 296  	}
 297  
 298  	const export = "//export "
 299  	const linkname = "//go:linkname "
 300  	for _, c := range f.Doc.List {
 301  		if strings.HasPrefix(c.Text, export) && len(c.Text) == len(export)+len(f.Name.Name) && c.Text[len(export):] == f.Name.Name {
 302  			return true
 303  		}
 304  
 305  		if strings.HasPrefix(c.Text, linkname) {
 306  			return true
 307  		}
 308  	}
 309  	return false
 310  }
 311