golang.org/x/tools/gopls@v0.15.3/internal/analysis/unusedvariable/unusedvariable.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package unusedvariable defines an analyzer that checks for unused variables. 6 package unusedvariable 7 8 import ( 9 "bytes" 10 "fmt" 11 "go/ast" 12 "go/format" 13 "go/token" 14 "go/types" 15 "strings" 16 17 "golang.org/x/tools/go/analysis" 18 "golang.org/x/tools/go/ast/astutil" 19 ) 20 21 const Doc = `check for unused variables and suggest fixes` 22 23 var Analyzer = &analysis.Analyzer{ 24 Name: "unusedvariable", 25 Doc: Doc, 26 Requires: []*analysis.Analyzer{}, 27 Run: run, 28 RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error 29 URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable", 30 } 31 32 // The suffix for this error message changed in Go 1.20. 33 var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} 34 35 func run(pass *analysis.Pass) (interface{}, error) { 36 for _, typeErr := range pass.TypeErrors { 37 for _, suffix := range unusedVariableSuffixes { 38 if strings.HasSuffix(typeErr.Msg, suffix) { 39 varName := strings.TrimSuffix(typeErr.Msg, suffix) 40 // Beginning in Go 1.23, go/types began quoting vars as `v'. 41 varName = strings.Trim(varName, "'`'") 42 43 err := runForError(pass, typeErr, varName) 44 if err != nil { 45 return nil, err 46 } 47 } 48 } 49 } 50 51 return nil, nil 52 } 53 54 func runForError(pass *analysis.Pass, err types.Error, name string) error { 55 var file *ast.File 56 for _, f := range pass.Files { 57 if f.Pos() <= err.Pos && err.Pos < f.End() { 58 file = f 59 break 60 } 61 } 62 if file == nil { 63 return nil 64 } 65 66 path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) 67 if len(path) < 2 { 68 return nil 69 } 70 71 ident, ok := path[0].(*ast.Ident) 72 if !ok || ident.Name != name { 73 return nil 74 } 75 76 diag := analysis.Diagnostic{ 77 Pos: ident.Pos(), 78 End: ident.End(), 79 Message: err.Msg, 80 } 81 82 for i := range path { 83 switch stmt := path[i].(type) { 84 case *ast.ValueSpec: 85 // Find GenDecl to which offending ValueSpec belongs. 86 if decl, ok := path[i+1].(*ast.GenDecl); ok { 87 fixes := removeVariableFromSpec(pass, path, stmt, decl, ident) 88 // fixes may be nil 89 if len(fixes) > 0 { 90 diag.SuggestedFixes = fixes 91 pass.Report(diag) 92 } 93 } 94 95 case *ast.AssignStmt: 96 if stmt.Tok != token.DEFINE { 97 continue 98 } 99 100 containsIdent := false 101 for _, expr := range stmt.Lhs { 102 if expr == ident { 103 containsIdent = true 104 } 105 } 106 if !containsIdent { 107 continue 108 } 109 110 fixes := removeVariableFromAssignment(path, stmt, ident) 111 // fixes may be nil 112 if len(fixes) > 0 { 113 diag.SuggestedFixes = fixes 114 pass.Report(diag) 115 } 116 } 117 } 118 119 return nil 120 } 121 122 func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix { 123 newDecl := new(ast.GenDecl) 124 *newDecl = *decl 125 newDecl.Specs = nil 126 127 for _, spec := range decl.Specs { 128 if spec != stmt { 129 newDecl.Specs = append(newDecl.Specs, spec) 130 continue 131 } 132 133 newSpec := new(ast.ValueSpec) 134 *newSpec = *stmt 135 newSpec.Names = nil 136 137 for _, n := range stmt.Names { 138 if n != ident { 139 newSpec.Names = append(newSpec.Names, n) 140 } 141 } 142 143 if len(newSpec.Names) > 0 { 144 newDecl.Specs = append(newDecl.Specs, newSpec) 145 } 146 } 147 148 // decl.End() does not include any comments, so if a comment is present we 149 // need to account for it when we delete the statement 150 end := decl.End() 151 if stmt.Comment != nil && stmt.Comment.End() > end { 152 end = stmt.Comment.End() 153 } 154 155 // There are no other specs left in the declaration, the whole statement can 156 // be deleted 157 if len(newDecl.Specs) == 0 { 158 // Find parent DeclStmt and delete it 159 for _, node := range path { 160 if declStmt, ok := node.(*ast.DeclStmt); ok { 161 edits := deleteStmtFromBlock(path, declStmt) 162 if len(edits) == 0 { 163 return nil // can this happen? 164 } 165 return []analysis.SuggestedFix{ 166 { 167 Message: suggestedFixMessage(ident.Name), 168 TextEdits: edits, 169 }, 170 } 171 } 172 } 173 } 174 175 var b bytes.Buffer 176 if err := format.Node(&b, pass.Fset, newDecl); err != nil { 177 return nil 178 } 179 180 return []analysis.SuggestedFix{ 181 { 182 Message: suggestedFixMessage(ident.Name), 183 TextEdits: []analysis.TextEdit{ 184 { 185 Pos: decl.Pos(), 186 // Avoid adding a new empty line 187 End: end + 1, 188 NewText: b.Bytes(), 189 }, 190 }, 191 }, 192 } 193 } 194 195 func removeVariableFromAssignment(path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { 196 // The only variable in the assignment is unused 197 if len(stmt.Lhs) == 1 { 198 // If LHS has only one expression to be valid it has to have 1 expression 199 // on RHS 200 // 201 // RHS may have side effects, preserve RHS 202 if exprMayHaveSideEffects(stmt.Rhs[0]) { 203 // Delete until RHS 204 return []analysis.SuggestedFix{ 205 { 206 Message: suggestedFixMessage(ident.Name), 207 TextEdits: []analysis.TextEdit{ 208 { 209 Pos: ident.Pos(), 210 End: stmt.Rhs[0].Pos(), 211 }, 212 }, 213 }, 214 } 215 } 216 217 // RHS does not have any side effects, delete the whole statement 218 edits := deleteStmtFromBlock(path, stmt) 219 if len(edits) == 0 { 220 return nil // can this happen? 221 } 222 return []analysis.SuggestedFix{ 223 { 224 Message: suggestedFixMessage(ident.Name), 225 TextEdits: edits, 226 }, 227 } 228 } 229 230 // Otherwise replace ident with `_` 231 return []analysis.SuggestedFix{ 232 { 233 Message: suggestedFixMessage(ident.Name), 234 TextEdits: []analysis.TextEdit{ 235 { 236 Pos: ident.Pos(), 237 End: ident.End(), 238 NewText: []byte("_"), 239 }, 240 }, 241 }, 242 } 243 } 244 245 func suggestedFixMessage(name string) string { 246 return fmt.Sprintf("Remove variable %s", name) 247 } 248 249 func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { 250 // Find innermost enclosing BlockStmt. 251 var block *ast.BlockStmt 252 for i := range path { 253 if blockStmt, ok := path[i].(*ast.BlockStmt); ok { 254 block = blockStmt 255 break 256 } 257 } 258 259 nodeIndex := -1 260 for i, blockStmt := range block.List { 261 if blockStmt == stmt { 262 nodeIndex = i 263 break 264 } 265 } 266 267 // The statement we need to delete was not found in BlockStmt 268 if nodeIndex == -1 { 269 return nil 270 } 271 272 // Delete until the end of the block unless there is another statement after 273 // the one we are trying to delete 274 end := block.Rbrace 275 if nodeIndex < len(block.List)-1 { 276 end = block.List[nodeIndex+1].Pos() 277 } 278 279 return []analysis.TextEdit{ 280 { 281 Pos: stmt.Pos(), 282 End: end, 283 }, 284 } 285 } 286 287 // exprMayHaveSideEffects reports whether the expression may have side effects 288 // (because it contains a function call or channel receive). We disregard 289 // runtime panics as well written programs should not encounter them. 290 func exprMayHaveSideEffects(expr ast.Expr) bool { 291 var mayHaveSideEffects bool 292 ast.Inspect(expr, func(n ast.Node) bool { 293 switch n := n.(type) { 294 case *ast.CallExpr: // possible function call 295 mayHaveSideEffects = true 296 return false 297 case *ast.UnaryExpr: 298 if n.Op == token.ARROW { // channel receive 299 mayHaveSideEffects = true 300 return false 301 } 302 case *ast.FuncLit: 303 return false // evaluating what's inside a FuncLit has no effect 304 } 305 return true 306 }) 307 308 return mayHaveSideEffects 309 }