Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# deadcode

`deadcode` is a very simple utility which detects unused declarations in a Go package.

## Usage
```
deadcode [-test] [packages]

-test Include test files
packages A list of packages using the same conventions as the go tool
```

## Limitations

* Self-referential unused code is not currently reported
* A single package can be tested at a time
* Unused methods are not reported
223 changes: 85 additions & 138 deletions deadcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,176 +4,123 @@ import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"sort"
"strings"

"golang.org/x/tools/go/loader"
)

var exitCode int
var (
exitCode int
withTestFiles bool
)

func main() {
flag.BoolVar(&withTestFiles, "test", false, "include test files")
flag.Parse()

ctx := &Context{
withTests: withTestFiles,
}

if flag.NArg() == 0 {
doDir(".")
ctx.Load(".")
} else {
for _, name := range flag.Args() {
// Is it a directory?
if fi, err := os.Stat(name); err == nil && fi.IsDir() {
doDir(name)
} else {
errorf("not a directory: %s", name)
}
}
ctx.Load(flag.Args()...)
}
report := ctx.Process()
for _, obj := range report {
ctx.errorf(obj.Pos(), "%s is unused", obj.Name())
}
os.Exit(exitCode)
}

// error formats the error to standard error, adding program
// identification and a newline
func errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "deadcode: "+format+"\n", args...)
exitCode = 2
func fatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(1)
}

func doDir(name string) {
notests := func(info os.FileInfo) bool {
if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") &&
!strings.HasSuffix(info.Name(), "_test.go") {
return true
type Context struct {
cwd string
withTests bool

loader.Config
}

func (ctx *Context) Load(args ...string) {
for _, arg := range args {
if ctx.withTests {
ctx.Config.ImportWithTests(arg)
} else {
ctx.Config.Import(arg)
}
return false
}
fs := token.NewFileSet()
pkgs, err := parser.ParseDir(fs, name, notests, parser.Mode(0))
if err != nil {
errorf("%s", err)
return
}

// error formats the error to standard error, adding program
// identification and a newline
func (ctx *Context) errorf(pos token.Pos, format string, args ...interface{}) {
if ctx.cwd == "" {
ctx.cwd, _ = os.Getwd()
}
for _, pkg := range pkgs {
doPackage(fs, pkg)
p := ctx.Config.Fset.Position(pos)
f, err := filepath.Rel(ctx.cwd, p.Filename)
if err == nil {
p.Filename = f
}
fmt.Fprintf(os.Stderr, p.String()+": "+format+"\n", args...)
exitCode = 2
}

type Package struct {
p *ast.Package
fs *token.FileSet
decl map[string]ast.Node
used map[string]bool
func (ctx *Context) Process() []types.Object {
prog, err := ctx.Config.Load()
if err != nil {
fatalf("cannot load packages: %s", err)
}
var allUnused []types.Object
for _, pkg := range prog.Imported {
unused := doPackage(prog, pkg)
allUnused = append(allUnused, unused...)
}
sort.Sort(objects(allUnused))
return allUnused
}

func doPackage(fs *token.FileSet, pkg *ast.Package) {
p := &Package{
p: pkg,
fs: fs,
decl: make(map[string]ast.Node),
used: make(map[string]bool),
}
func doPackage(prog *loader.Program, pkg *loader.PackageInfo) []types.Object {
used := make(map[types.Object]bool)
for _, file := range pkg.Files {
for _, decl := range file.Decls {
switch n := decl.(type) {
case *ast.GenDecl:
// var, const, types
for _, spec := range n.Specs {
switch s := spec.(type) {
case *ast.ValueSpec:
// constants and variables.
for _, name := range s.Names {
p.decl[name.Name] = n
}
case *ast.TypeSpec:
// type definitions.
p.decl[s.Name.Name] = n
}
}
case *ast.FuncDecl:
// function declarations
// TODO(remy): do methods
if n.Recv == nil {
p.decl[n.Name.Name] = n
}
ast.Inspect(file, func(n ast.Node) bool {
id, ok := n.(*ast.Ident)
if !ok {
return true
}
}
}
// init() and _ are always used
p.used["init"] = true
p.used["_"] = true
if pkg.Name != "main" {
// exported names are marked used for non-main packages.
for name := range p.decl {
if ast.IsExported(name) {
p.used[name] = true
obj := pkg.Info.Uses[id]
if obj != nil {
used[obj] = true
}
}
} else {
// in main programs, main() is called.
p.used["main"] = true
return false
})
}
for _, file := range pkg.Files {
// walk file looking for used nodes.
ast.Walk(p, file)
}
// reports.
reports := Reports(nil)
for name, node := range p.decl {
if !p.used[name] {
reports = append(reports, Report{node.Pos(), name})
}
}
sort.Sort(reports)
for _, report := range reports {
errorf("%s: %s is unused", fs.Position(report.pos), report.name)
}
}

type Report struct {
pos token.Pos
name string
}
type Reports []Report

func (l Reports) Len() int { return len(l) }
func (l Reports) Less(i, j int) bool { return l[i].pos < l[j].pos }
func (l Reports) Swap(i, j int) { l[i], l[j] = l[j], l[i] }

// Visits files for used nodes.
func (p *Package) Visit(node ast.Node) ast.Visitor {
u := usedWalker(*p) // hopefully p fields are references.
switch n := node.(type) {
// don't walk whole file, but only:
case *ast.ValueSpec:
// - variable initializers
for _, value := range n.Values {
ast.Walk(&u, value)
}
// variable types.
if n.Type != nil {
ast.Walk(&u, n.Type)
global := pkg.Pkg.Scope()
var unused []types.Object
for _, name := range global.Names() {
if pkg.Pkg.Name() == "main" && name == "main" {
continue
}
case *ast.BlockStmt:
// - function bodies
for _, stmt := range n.List {
ast.Walk(&u, stmt)
obj := global.Lookup(name)
if !used[obj] && (pkg.Pkg.Name() == "main" || !ast.IsExported(name)) {
unused = append(unused, obj)
}
case *ast.FuncDecl:
// - function signatures
ast.Walk(&u, n.Type)
case *ast.TypeSpec:
// - type declarations
ast.Walk(&u, n.Type)
}
return p
return unused
}

type usedWalker Package
type objects []types.Object

// Walks through the AST marking used identifiers.
func (p *usedWalker) Visit(node ast.Node) ast.Visitor {
// just be stupid and mark all *ast.Ident
switch n := node.(type) {
case *ast.Ident:
p.used[n.Name] = true
}
return p
}
func (s objects) Len() int { return len(s) }
func (s objects) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s objects) Less(i, j int) bool { return s[i].Pos() < s[j].Pos() }
60 changes: 60 additions & 0 deletions deadcode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"go/types"
"testing"
)

func TestP1(t *testing.T) {
ctx := new(Context)
ctx.Load("./testdata/p1")
objs := ctx.Process()
compare(t, objs, []string{
"unused",
"g",
"H",
// "h", // recursive functions are not supported
})
}

func TestP2(t *testing.T) {
ctx := new(Context)
ctx.Load("./testdata/p2")
objs := ctx.Process()
compare(t, objs, []string{
"main",
"unused",
"g",
// "h", // recursive functions are not supported
})
}

func TestWithTestFiles(t *testing.T) {
ctx := &Context{withTests: true}
ctx.Load("./testdata/p3")
objs := ctx.Process()
// Only "y" is unused, x is used in tests.
compare(t, objs, []string{"y"})
}

func compare(t *testing.T, objs []types.Object, names []string) {
left := make(map[string]bool)
right := make(map[string]bool)
for _, o := range objs {
left[o.Name()] = true
}
for _, n := range names {
right[n] = true
}

for _, o := range objs {
if !right[o.Name()] {
t.Errorf("%s should not have been reported as unused", o.Name())
}
}
for _, n := range names {
if !left[n] {
t.Errorf("unused %s should not have been reported", n)
}
}
}
37 changes: 37 additions & 0 deletions testdata/p1/sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

// main is used
func main() {
f(x)
return
}

// x is used
var x int

// unused is unused
var unused int

// f is used
func f(x int) {
}

// g is unused
func g(x int) {
}

// H is exported
func H(x int) {
}

// init is used
func init() {
}

var _ int

func h(x int) {
if x > 0 {
h(x - 1)
}
}
Loading