src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

st1003.go (8763B)


      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) (any, 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.Name, "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.Name, 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 }