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

     1  package sa9004
     2  
     3  import (
     4  	"go/ast"
     5  	"go/token"
     6  	"go/types"
     7  
     8  	"github.com/amarpal/go-tools/analysis/code"
     9  	"github.com/amarpal/go-tools/analysis/edit"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/go/ast/astutil"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  )
    17  
    18  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    19  	Analyzer: &analysis.Analyzer{
    20  		Name:     "SA9004",
    21  		Run:      run,
    22  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    23  	},
    24  	Doc: &lint.Documentation{
    25  		Title: `Only the first constant has an explicit type`,
    26  
    27  		Text: `In a constant declaration such as the following:
    28  
    29      const (
    30          First byte = 1
    31          Second     = 2
    32      )
    33  
    34  the constant Second does not have the same type as the constant First.
    35  This construct shouldn't be confused with
    36  
    37      const (
    38          First byte = iota
    39          Second
    40      )
    41  
    42  where \'First\' and \'Second\' do indeed have the same type. The type is only
    43  passed on when no explicit value is assigned to the constant.
    44  
    45  When declaring enumerations with explicit values it is therefore
    46  important not to write
    47  
    48      const (
    49            EnumFirst EnumType = 1
    50            EnumSecond         = 2
    51            EnumThird          = 3
    52      )
    53  
    54  This discrepancy in types can cause various confusing behaviors and
    55  bugs.
    56  
    57  
    58  Wrong type in variable declarations
    59  
    60  The most obvious issue with such incorrect enumerations expresses
    61  itself as a compile error:
    62  
    63      package pkg
    64  
    65      const (
    66          EnumFirst  uint8 = 1
    67          EnumSecond       = 2
    68      )
    69  
    70      func fn(useFirst bool) {
    71          x := EnumSecond
    72          if useFirst {
    73              x = EnumFirst
    74          }
    75      }
    76  
    77  fails to compile with
    78  
    79      ./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment
    80  
    81  
    82  Losing method sets
    83  
    84  A more subtle issue occurs with types that have methods and optional
    85  interfaces. Consider the following:
    86  
    87      package main
    88  
    89      import "fmt"
    90  
    91      type Enum int
    92  
    93      func (e Enum) String() string {
    94          return "an enum"
    95      }
    96  
    97      const (
    98          EnumFirst  Enum = 1
    99          EnumSecond      = 2
   100      )
   101  
   102      func main() {
   103          fmt.Println(EnumFirst)
   104          fmt.Println(EnumSecond)
   105      }
   106  
   107  This code will output
   108  
   109      an enum
   110      2
   111  
   112  as \'EnumSecond\' has no explicit type, and thus defaults to \'int\'.`,
   113  		Since:    "2019.1",
   114  		Severity: lint.SeverityWarning,
   115  		MergeIf:  lint.MergeIfAny,
   116  	},
   117  })
   118  
   119  var Analyzer = SCAnalyzer.Analyzer
   120  
   121  func run(pass *analysis.Pass) (interface{}, error) {
   122  	convertibleTo := func(V, T types.Type) bool {
   123  		if types.ConvertibleTo(V, T) {
   124  			return true
   125  		}
   126  		// Go <1.16 returns false for untyped string to string conversion
   127  		if V, ok := V.(*types.Basic); ok && V.Kind() == types.UntypedString {
   128  			if T, ok := T.Underlying().(*types.Basic); ok && T.Kind() == types.String {
   129  				return true
   130  			}
   131  		}
   132  		return false
   133  	}
   134  	fn := func(node ast.Node) {
   135  		decl := node.(*ast.GenDecl)
   136  		if !decl.Lparen.IsValid() {
   137  			return
   138  		}
   139  		if decl.Tok != token.CONST {
   140  			return
   141  		}
   142  
   143  		groups := astutil.GroupSpecs(pass.Fset, decl.Specs)
   144  	groupLoop:
   145  		for _, group := range groups {
   146  			if len(group) < 2 {
   147  				continue
   148  			}
   149  			if group[0].(*ast.ValueSpec).Type == nil {
   150  				// first constant doesn't have a type
   151  				continue groupLoop
   152  			}
   153  
   154  			firstType := pass.TypesInfo.TypeOf(group[0].(*ast.ValueSpec).Values[0])
   155  			for i, spec := range group {
   156  				spec := spec.(*ast.ValueSpec)
   157  				if i > 0 && spec.Type != nil {
   158  					continue groupLoop
   159  				}
   160  				if len(spec.Names) != 1 || len(spec.Values) != 1 {
   161  					continue groupLoop
   162  				}
   163  
   164  				if !convertibleTo(pass.TypesInfo.TypeOf(spec.Values[0]), firstType) {
   165  					continue groupLoop
   166  				}
   167  
   168  				switch v := spec.Values[0].(type) {
   169  				case *ast.BasicLit:
   170  				case *ast.UnaryExpr:
   171  					if _, ok := v.X.(*ast.BasicLit); !ok {
   172  						continue groupLoop
   173  					}
   174  				default:
   175  					// if it's not a literal it might be typed, such as
   176  					// time.Microsecond = 1000 * Nanosecond
   177  					continue groupLoop
   178  				}
   179  			}
   180  			var edits []analysis.TextEdit
   181  			typ := group[0].(*ast.ValueSpec).Type
   182  			for _, spec := range group[1:] {
   183  				nspec := *spec.(*ast.ValueSpec)
   184  				nspec.Type = typ
   185  				// The position of `spec` node excludes comments (if any).
   186  				// However, on generating the source back from the node, the comments are included. Setting `Comment` to nil ensures deduplication of comments.
   187  				nspec.Comment = nil
   188  				edits = append(edits, edit.ReplaceWithNode(pass.Fset, spec, &nspec))
   189  			}
   190  			report.Report(pass, group[0], "only the first constant in this group has an explicit type", report.Fixes(edit.Fix("add type to all constants in group", edits...)))
   191  		}
   192  	}
   193  	code.Preorder(pass, fn, (*ast.GenDecl)(nil))
   194  	return nil, nil
   195  }