github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/stylecheck/st1003/st1003.go (about)

     1  package st1003
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"strings"
     8  	"unicode"
     9  
    10  	"github.com/amarpal/go-tools/analysis/code"
    11  	"github.com/amarpal/go-tools/analysis/facts/generated"
    12  	"github.com/amarpal/go-tools/analysis/lint"
    13  	"github.com/amarpal/go-tools/analysis/report"
    14  	"github.com/amarpal/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.Documentation{
    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://golang.org/doc/effective_go.html#package-names
    33  - https://golang.org/doc/effective_go.html#mixed-caps
    34  - https://github.com/golang/go/wiki/CodeReviewComments#initialisms
    35  - https://github.com/golang/go/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  }