minio/buildscripts/verifier.go

248 lines
5.1 KiB
Go

package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
)
var exitCode int
var dirs []string
func appendUniq(slice []string, i string) []string {
for _, ele := range slice {
if ele == i {
return slice
}
}
return append(slice, i)
}
// error formats the error to standard error, adding program
// identification and a newline
func errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "verifier: "+format+"\n", args...)
exitCode = 2
}
type goPackage struct {
p *ast.Package
fs *token.FileSet
decl map[string]ast.Node
missingcomments map[string]ast.Node
used map[string]bool
}
type usedWalker goPackage
// 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
}
type report struct {
pos token.Pos
name string
}
type reports []report
// Len
func (l reports) Len() int { return len(l) }
// Less
func (l reports) Less(i, j int) bool { return l[i].pos < l[j].pos }
// Swap
func (l reports) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
// Visits files for used nodes.
func (p *goPackage) 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)
}
case *ast.BlockStmt:
// - function bodies
for _, stmt := range n.List {
ast.Walk(&u, stmt)
}
case *ast.FuncDecl:
// - function signatures
ast.Walk(&u, n.Type)
case *ast.TypeSpec:
// - type declarations
ast.Walk(&u, n.Type)
}
return p
}
func getAllMinioPkgs(path string, fl os.FileInfo, err error) error {
if err != nil {
return err
}
if fl.IsDir() {
// Skip godeps
if strings.Contains(path, "Godeps") {
return nil
}
dirs = appendUniq(dirs, path)
}
return nil
}
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
}
return false
}
fs := token.NewFileSet()
pkgs, err := parser.ParseDir(fs, name, notests, parser.ParseComments|parser.Mode(0))
if err != nil {
errorf("%s", err)
return
}
for _, pkg := range pkgs {
doPackage(fs, pkg)
}
}
func doDecl(p *goPackage, decl interface{}, cmap ast.CommentMap) bool {
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:
// if function is 'main', never check
if n.Name.Name == "main" {
return true
}
// Do not be strict on non-exported functions
if !ast.IsExported(n.Name.Name) {
return true
}
// Do not be strict for field list functions
// if n.Recv != nil {
// continue
//}
// Be strict for global functions
_, ok := cmap[n]
if ok == false {
p.missingcomments[n.Name.Name] = n
}
// function declarations
// TODO(remy): do methods
if n.Recv == nil {
p.decl[n.Name.Name] = n
}
}
return false
}
func doPackage(fs *token.FileSet, pkg *ast.Package) {
p := &goPackage{
p: pkg,
fs: fs,
decl: make(map[string]ast.Node),
missingcomments: make(map[string]ast.Node),
used: make(map[string]bool),
}
for _, file := range pkg.Files {
cmap := ast.NewCommentMap(fs, file, file.Comments)
for _, decl := range file.Decls {
if doDecl(p, decl, cmap) {
continue
}
}
}
// init() is always used
p.used["init"] = 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
}
}
} else {
// in main programs, main() is called.
p.used["main"] = true
}
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)
}
for name, node := range p.missingcomments {
errorf("%s: comment is missing for 'func %s'", fs.Position(node.Pos()), name)
}
}
func main() {
flag.Parse()
if flag.NArg() == 0 {
doDir(".")
} else {
for _, name := range flag.Args() {
// Is it a directory?
if fi, err := os.Stat(name); err == nil && fi.IsDir() {
err := filepath.Walk(name, getAllMinioPkgs)
if err != nil {
errorf(err.Error())
}
for _, dir := range dirs {
doDir(dir)
}
} else {
errorf("not a directory: %s", name)
}
}
}
os.Exit(exitCode)
}