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 }