mirror of https://github.com/golang/go.git
301 lines
6.8 KiB
Go
301 lines
6.8 KiB
Go
// 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"
|
|
"golang.org/x/tools/internal/analysisinternal"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
type fixesForError map[types.Error][]analysis.SuggestedFix
|
|
|
|
const unusedVariableSuffix = " declared but not used"
|
|
|
|
func run(pass *analysis.Pass) (interface{}, error) {
|
|
for _, typeErr := range analysisinternal.GetTypeErrors(pass) {
|
|
if strings.HasSuffix(typeErr.Msg, unusedVariableSuffix) {
|
|
varName := strings.TrimSuffix(typeErr.Msg, unusedVariableSuffix)
|
|
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
|
|
}
|