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  }