blob: 904016be71ec1f646d5fc740fa6f7c132d04ada3 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package unusedvariable defines an analyzer that checks for unused variables.
package unusedvariable
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
)
const Doc = `check for unused variables
The unusedvariable analyzer suggests fixes for unused variables errors.
`
var Analyzer = &analysis.Analyzer{
Name: "unusedvariable",
Doc: Doc,
Requires: []*analysis.Analyzer{},
Run: run,
RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error
}
// The suffix for this error message changed in Go 1.20.
var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"}
func run(pass *analysis.Pass) (interface{}, error) {
for _, typeErr := range pass.TypeErrors {
for _, suffix := range unusedVariableSuffixes {
if strings.HasSuffix(typeErr.Msg, suffix) {
varName := strings.TrimSuffix(typeErr.Msg, suffix)
err := runForError(pass, typeErr, varName)
if err != nil {
return nil, err
}
}
}
}
return nil, nil
}
func runForError(pass *analysis.Pass, err types.Error, name string) error {
var file *ast.File
for _, f := range pass.Files {
if f.Pos() <= err.Pos && err.Pos < f.End() {
file = f
break
}
}
if file == nil {
return nil
}
path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos)
if len(path) < 2 {
return nil
}
ident, ok := path[0].(*ast.Ident)
if !ok || ident.Name != name {
return nil
}
diag := analysis.Diagnostic{
Pos: ident.Pos(),
End: ident.End(),
Message: err.Msg,
}
for i := range path {
switch stmt := path[i].(type) {
case *ast.ValueSpec:
// Find GenDecl to which offending ValueSpec belongs.
if decl, ok := path[i+1].(*ast.GenDecl); ok {
fixes := removeVariableFromSpec(pass, path, stmt, decl, ident)
// fixes may be nil
if len(fixes) > 0 {
diag.SuggestedFixes = fixes
pass.Report(diag)
}
}
case *ast.AssignStmt:
if stmt.Tok != token.DEFINE {
continue
}
containsIdent := false
for _, expr := range stmt.Lhs {
if expr == ident {
containsIdent = true
}
}
if !containsIdent {
continue
}
fixes := removeVariableFromAssignment(pass, path, stmt, ident)
// fixes may be nil
if len(fixes) > 0 {
diag.SuggestedFixes = fixes
pass.Report(diag)
}
}
}
return nil
}
func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix {
newDecl := new(ast.GenDecl)
*newDecl = *decl
newDecl.Specs = nil
for _, spec := range decl.Specs {
if spec != stmt {
newDecl.Specs = append(newDecl.Specs, spec)
continue
}
newSpec := new(ast.ValueSpec)
*newSpec = *stmt
newSpec.Names = nil
for _, n := range stmt.Names {
if n != ident {
newSpec.Names = append(newSpec.Names, n)
}
}
if len(newSpec.Names) > 0 {
newDecl.Specs = append(newDecl.Specs, newSpec)
}
}
// decl.End() does not include any comments, so if a comment is present we
// need to account for it when we delete the statement
end := decl.End()
if stmt.Comment != nil && stmt.Comment.End() > end {
end = stmt.Comment.End()
}
// There are no other specs left in the declaration, the whole statement can
// be deleted
if len(newDecl.Specs) == 0 {
// Find parent DeclStmt and delete it
for _, node := range path {
if declStmt, ok := node.(*ast.DeclStmt); ok {
return []analysis.SuggestedFix{
{
Message: suggestedFixMessage(ident.Name),
TextEdits: deleteStmtFromBlock(path, declStmt),
},
}
}
}
}
var b bytes.Buffer
if err := format.Node(&b, pass.Fset, newDecl); err != nil {
return nil
}
return []analysis.SuggestedFix{
{
Message: suggestedFixMessage(ident.Name),
TextEdits: []analysis.TextEdit{
{
Pos: decl.Pos(),
// Avoid adding a new empty line
End: end + 1,
NewText: b.Bytes(),
},
},
},
}
}
func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix {
// The only variable in the assignment is unused
if len(stmt.Lhs) == 1 {
// If LHS has only one expression to be valid it has to have 1 expression
// on RHS
//
// RHS may have side effects, preserve RHS
if exprMayHaveSideEffects(stmt.Rhs[0]) {
// Delete until RHS
return []analysis.SuggestedFix{
{
Message: suggestedFixMessage(ident.Name),
TextEdits: []analysis.TextEdit{
{
Pos: ident.Pos(),
End: stmt.Rhs[0].Pos(),
},
},
},
}
}
// RHS does not have any side effects, delete the whole statement
return []analysis.SuggestedFix{
{
Message: suggestedFixMessage(ident.Name),
TextEdits: deleteStmtFromBlock(path, stmt),
},
}
}
// Otherwise replace ident with `_`
return []analysis.SuggestedFix{
{
Message: suggestedFixMessage(ident.Name),
TextEdits: []analysis.TextEdit{
{
Pos: ident.Pos(),
End: ident.End(),
NewText: []byte("_"),
},
},
},
}
}
func suggestedFixMessage(name string) string {
return fmt.Sprintf("Remove variable %s", name)
}
func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit {
// Find innermost enclosing BlockStmt.
var block *ast.BlockStmt
for i := range path {
if blockStmt, ok := path[i].(*ast.BlockStmt); ok {
block = blockStmt
break
}
}
nodeIndex := -1
for i, blockStmt := range block.List {
if blockStmt == stmt {
nodeIndex = i
break
}
}
// The statement we need to delete was not found in BlockStmt
if nodeIndex == -1 {
return nil
}
// Delete until the end of the block unless there is another statement after
// the one we are trying to delete
end := block.Rbrace
if nodeIndex < len(block.List)-1 {
end = block.List[nodeIndex+1].Pos()
}
return []analysis.TextEdit{
{
Pos: stmt.Pos(),
End: end,
},
}
}
// exprMayHaveSideEffects reports whether the expression may have side effects
// (because it contains a function call or channel receive). We disregard
// runtime panics as well written programs should not encounter them.
func exprMayHaveSideEffects(expr ast.Expr) bool {
var mayHaveSideEffects bool
ast.Inspect(expr, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.CallExpr: // possible function call
mayHaveSideEffects = true
return false
case *ast.UnaryExpr:
if n.Op == token.ARROW { // channel receive
mayHaveSideEffects = true
return false
}
case *ast.FuncLit:
return false // evaluating what's inside a FuncLit has no effect
}
return true
})
return mayHaveSideEffects
}