go.tools/go/types: "imported but not used" checks for packages

also:
- initial code for unused label errors
- some cleanups, better names
- additional tests

TODO: Dot-imported packages are not handled yet; i.e., they
      are always considered used for now.

R=adonovan
CC=golang-dev
https://golang.org/cl/13768043
This commit is contained in:
Robert Griesemer 2013-09-19 10:05:34 -07:00
parent 34fbb29ae0
commit 86e41f819a
14 changed files with 178 additions and 69 deletions

View File

@ -96,7 +96,7 @@ func TestScopesInfo(t *testing.T) {
{`package p`, []string{ {`package p`, []string{
"file:", "file:",
}}, }},
{`package p; import ( "fmt"; m "math"; _ "os" )`, []string{ {`package p; import ( "fmt"; m "math"; _ "os" ); var ( _ = fmt.Println; _ = m.Pi )`, []string{
"file:fmt m", "file:fmt m",
}}, }},
{`package p; func _() {}`, []string{ {`package p; func _() {}`, []string{

View File

@ -187,6 +187,7 @@ func (check *checker) selector(x *operand, e *ast.SelectorExpr) {
if ident, ok := e.X.(*ast.Ident); ok { if ident, ok := e.X.(*ast.Ident); ok {
if pkg, _ := check.topScope.LookupParent(ident.Name).(*PkgName); pkg != nil { if pkg, _ := check.topScope.LookupParent(ident.Name).(*PkgName); pkg != nil {
check.recordObject(ident, pkg) check.recordObject(ident, pkg)
pkg.used = true
exp := pkg.pkg.scope.Lookup(sel) exp := pkg.pkg.scope.Lookup(sel)
if exp == nil { if exp == nil {
if !pkg.pkg.fake { if !pkg.pkg.fake {
@ -196,7 +197,7 @@ func (check *checker) selector(x *operand, e *ast.SelectorExpr) {
} }
if !exp.IsExported() { if !exp.IsExported() {
check.errorf(e.Pos(), "%s not exported by package %s", sel, ident) check.errorf(e.Pos(), "%s not exported by package %s", sel, ident)
goto Error // ok to continue
} }
check.recordSelection(e, PackageObj, nil, exp, nil, false) check.recordSelection(e, PackageObj, nil, exp, nil, false)
// Simplified version of the code for *ast.Idents: // Simplified version of the code for *ast.Idents:

View File

@ -49,6 +49,7 @@ type checker struct {
// functions // functions
funcList []funcInfo // list of functions/methods with correct signatures and non-empty bodies funcList []funcInfo // list of functions/methods with correct signatures and non-empty bodies
funcSig *Signature // signature of currently type-checked function funcSig *Signature // signature of currently type-checked function
labels *Scope // label scope of currently type-checked function; lazily allocated
// debugging // debugging
indent int // indentation for tracing indent int // indentation for tracing

View File

@ -45,6 +45,7 @@ var (
// Each tests entry is list of files belonging to the same package. // Each tests entry is list of files belonging to the same package.
var tests = [][]string{ var tests = [][]string{
{"testdata/importdecl0a.src", "testdata/importdecl0b.src"},
{"testdata/cycles.src"}, {"testdata/cycles.src"},
{"testdata/decls0.src"}, {"testdata/decls0.src"},
{"testdata/decls1.src"}, {"testdata/decls1.src"},

View File

@ -87,6 +87,8 @@ import m "math"
const c = 3.0 const c = 3.0
type T []int type T []int
func f(a int, s string) float64 { func f(a int, s string) float64 {
fmt.Println("calling f")
_ = m.Pi // use package math
const d int = c + 1 const d int = c + 1
var x int var x int
x = a + len(s) x = a + len(s)

View File

@ -123,10 +123,12 @@ func (obj *object) sameId(pkg *Package, name string) bool {
// A PkgName represents an imported Go package. // A PkgName represents an imported Go package.
type PkgName struct { type PkgName struct {
object object
used bool
} }
func NewPkgName(pos token.Pos, pkg *Package, name string) *PkgName { func NewPkgName(pos token.Pos, pkg *Package, name string) *PkgName {
return &PkgName{object{nil, pos, pkg, name, Typ[Invalid]}} return &PkgName{object: object{nil, pos, pkg, name, Typ[Invalid]}}
} }
func (obj *PkgName) String() string { return obj.toString("package", nil) } func (obj *PkgName) String() string { return obj.toString("package", nil) }
@ -236,10 +238,12 @@ func (obj *Func) String() string {
// A Label represents a declared label. // A Label represents a declared label.
type Label struct { type Label struct {
object object
used bool
} }
func NewLabel(pos token.Pos, name string) *Label { func NewLabel(pos token.Pos, name string) *Label {
return &Label{object{nil, pos, nil, name, nil}} return &Label{object: object{nil, pos, nil, name, nil}}
} }
func (obj *Label) String() string { return fmt.Sprintf("label %s", obj.Name()) } func (obj *Label) String() string { return fmt.Sprintf("label %s", obj.Name()) }

View File

@ -114,11 +114,11 @@ func (check *checker) resolveFiles(files []*ast.File) {
pkg := check.pkg pkg := check.pkg
// Phase 1: Pre-declare all package-level objects so that they can be found // Phase 1: Pre-declare all package-level objects so that they can be found
// independent of source order. Collect methods for a later phase. // independent of source order. Associate methods with receiver
// base type names.
var ( var (
scopes []*Scope // corresponding file scope per file fileScope *Scope // current file scope
scope *Scope // current file scope
objList []Object // list of package-level objects to type-check objList []Object // list of package-level objects to type-check
objMap = make(map[Object]declInfo) // declaration info for each package-level object objMap = make(map[Object]declInfo) // declaration info for each package-level object
) )
@ -137,7 +137,7 @@ func (check *checker) resolveFiles(files []*ast.File) {
check.declareObj(pkg.scope, ident, obj) check.declareObj(pkg.scope, ident, obj)
objList = append(objList, obj) objList = append(objList, obj)
objMap[obj] = declInfo{scope, typ, init, nil} objMap[obj] = declInfo{fileScope, typ, init, nil}
} }
importer := check.conf.Import importer := check.conf.Import
@ -148,14 +148,15 @@ func (check *checker) resolveFiles(files []*ast.File) {
// only add packages to pkg.imports that have not been seen already // only add packages to pkg.imports that have not been seen already
seen := make(map[*Package]bool) seen := make(map[*Package]bool)
var fileScopes []*Scope // list of file scopes
for _, file := range files { for _, file := range files {
// The package identifier denotes the current package, // The package identifier denotes the current package,
// but there is no corresponding package object. // but there is no corresponding package object.
check.recordObject(file.Name, nil) check.recordObject(file.Name, nil)
scope = NewScope(pkg.scope) fileScope = NewScope(pkg.scope)
check.recordScope(file, scope) check.recordScope(file, fileScope)
scopes = append(scopes, scope) fileScopes = append(fileScopes, fileScope)
for _, decl := range file.Decls { for _, decl := range file.Decls {
switch d := decl.(type) { switch d := decl.(type) {
@ -223,13 +224,13 @@ func (check *checker) resolveFiles(files []*ast.File) {
if obj.IsExported() { if obj.IsExported() {
// Note: This will change each imported object's scope! // Note: This will change each imported object's scope!
// May be an issue for type aliases. // May be an issue for type aliases.
check.declareObj(scope, nil, obj) check.declareObj(fileScope, nil, obj)
check.recordImplicit(s, obj) check.recordImplicit(s, obj)
} }
} }
} else { } else {
// declare imported package object in file scope // declare imported package object in file scope
check.declareObj(scope, nil, obj) check.declareObj(fileScope, nil, obj)
} }
case *ast.ValueSpec: case *ast.ValueSpec:
@ -332,7 +333,7 @@ func (check *checker) resolveFiles(files []*ast.File) {
} }
} }
objList = append(objList, obj) objList = append(objList, obj)
objMap[obj] = declInfo{scope, nil, nil, d} objMap[obj] = declInfo{fileScope, nil, nil, d}
default: default:
check.invalidAST(d.Pos(), "unknown ast.Decl node %T", d) check.invalidAST(d.Pos(), "unknown ast.Decl node %T", d)
@ -342,7 +343,7 @@ func (check *checker) resolveFiles(files []*ast.File) {
// Phase 2: Verify that objects in package and file scopes have different names. // Phase 2: Verify that objects in package and file scopes have different names.
for _, scope := range scopes { for _, scope := range fileScopes {
for _, obj := range scope.elems { for _, obj := range scope.elems {
if alt := pkg.scope.Lookup(obj.Name()); alt != nil { if alt := pkg.scope.Lookup(obj.Name()); alt != nil {
check.errorf(alt.Pos(), "%s already declared in this file through import of package %s", obj.Name(), obj.Pkg().Name()) check.errorf(alt.Pos(), "%s already declared in this file through import of package %s", obj.Name(), obj.Pkg().Name())
@ -384,16 +385,43 @@ func (check *checker) resolveFiles(files []*ast.File) {
check.topScope = f.sig.scope // open function scope check.topScope = f.sig.scope // open function scope
check.funcSig = f.sig check.funcSig = f.sig
check.labels = nil // lazily allocated
check.stmtList(f.body.List, false) check.stmtList(f.body.List, false)
if f.sig.results.Len() > 0 && !check.isTerminating(f.body, "") { if f.sig.results.Len() > 0 && !check.isTerminating(f.body, "") {
check.errorf(f.body.Rbrace, "missing return") check.errorf(f.body.Rbrace, "missing return")
} }
// spec: "It is illegal to define a label that is never used."
if check.labels != nil {
for _, obj := range check.labels.elems {
if l := obj.(*Label); !l.used {
check.errorf(l.pos, "%s defined but not used", l.name)
}
}
}
} }
// Phase 5: Check for declared but not used packages and variables. // Phase 5: Check for declared but not used packages and variables.
// Note: must happen after checking all functions because closures may affect outer scopes // Note: must happen after checking all functions because closures may affect outer scopes
// Each set of implicitly declared lhs variables of a type switch acts collectively // spec: "It is illegal (...) to directly import a package without referring to
// any of its exported identifiers. To import a package solely for its side-effects
// (initialization), use the blank identifier as explicit package name."
for _, scope := range fileScopes {
// Unused "blank imports" are automatically ignored
// since _ identifiers are not entered into scopes.
for _, obj := range scope.elems {
if p, _ := obj.(*PkgName); p != nil && !p.used {
check.errorf(p.pos, "%q imported but not used", p.pkg.path)
}
// TODO(gri) dot-imports are not handled for now since we don't
// have mapping from Object to corresponding PkgName through
// which the object was imported.
}
}
// Each set of implicitly declared lhs variables in a type switch acts collectively
// as a single lhs variable. If any one was 'used', all of them are 'used'. Handle // as a single lhs variable. If any one was 'used', all of them are 'used'. Handle
// them before the general analysis. // them before the general analysis.
for _, vars := range check.lhsVarsList { for _, vars := range check.lhsVarsList {
@ -411,9 +439,9 @@ func (check *checker) resolveFiles(files []*ast.File) {
} }
} }
for _, f := range check.funcList {
// spec: "Implementation restriction: A compiler may make it illegal to // spec: "Implementation restriction: A compiler may make it illegal to
// declare a variable inside a function body if the variable is never used." // declare a variable inside a function body if the variable is never used."
for _, f := range check.funcList {
check.usage(f.sig.scope) check.usage(f.sig.scope)
} }
} }

View File

@ -116,7 +116,6 @@ func TestStdfixed(t *testing.T) {
"bug223.go", "bug413.go", "bug459.go", // TODO(gri) complete initialization checks "bug223.go", "bug413.go", "bug459.go", // TODO(gri) complete initialization checks
"bug248.go", "bug302.go", "bug369.go", // complex test instructions - ignore "bug248.go", "bug302.go", "bug369.go", // complex test instructions - ignore
"bug250.go", // TODO(gri) fix recursive interfaces "bug250.go", // TODO(gri) fix recursive interfaces
"bug373.go", // TODO(gri) implement use checks
"bug376.go", // TODO(gri) built-ins must be called (no built-in function expressions) "bug376.go", // TODO(gri) built-ins must be called (no built-in function expressions)
"issue3924.go", // TODO(gri) && and || produce bool result (not untyped bool) "issue3924.go", // TODO(gri) && and || produce bool result (not untyped bool)
"issue4847.go", // TODO(gri) initialization cycle error not found "issue4847.go", // TODO(gri) initialization cycle error not found

View File

@ -63,6 +63,14 @@ func (check *checker) closeScope() {
check.topScope = check.topScope.Parent() check.topScope = check.topScope.Parent()
} }
func assignOp(op token.Token) token.Token {
// token_test.go verifies the token ordering this function relies on
if token.ADD_ASSIGN <= op && op <= token.AND_NOT_ASSIGN {
return op + (token.ADD - token.ADD_ASSIGN)
}
return token.ILLEGAL
}
// stmt typechecks statement s. // stmt typechecks statement s.
func (check *checker) stmt(s ast.Stmt, fallthroughOk bool) { func (check *checker) stmt(s ast.Stmt, fallthroughOk bool) {
// statements cannot use iota in general // statements cannot use iota in general
@ -77,13 +85,17 @@ func (check *checker) stmt(s ast.Stmt, fallthroughOk bool) {
check.declStmt(s.Decl) check.declStmt(s.Decl)
case *ast.LabeledStmt: case *ast.LabeledStmt:
scope := check.funcSig.labels scope := check.labels
if scope == nil { if scope == nil {
scope = NewScope(nil) // no label scope chain scope = NewScope(nil) // no label scope chain
check.funcSig.labels = scope check.labels = scope
} }
label := s.Label label := s.Label
check.declareObj(scope, label, NewLabel(label.Pos(), label.Name)) l := NewLabel(label.Pos(), label.Name)
// Labels are not resolved yet - mark them as used to avoid errors.
// TODO(gri) fix this
l.used = true
check.declareObj(scope, label, l)
check.stmt(s.Stmt, fallthroughOk) check.stmt(s.Stmt, fallthroughOk)
case *ast.ExprStmt: case *ast.ExprStmt:
@ -169,32 +181,8 @@ func (check *checker) stmt(s ast.Stmt, fallthroughOk bool) {
check.errorf(s.TokPos, "assignment operation %s requires single-valued expressions", s.Tok) check.errorf(s.TokPos, "assignment operation %s requires single-valued expressions", s.Tok)
return return
} }
// TODO(gri) make this conversion more efficient op := assignOp(s.Tok)
var op token.Token if op == token.ILLEGAL {
switch s.Tok {
case token.ADD_ASSIGN:
op = token.ADD
case token.SUB_ASSIGN:
op = token.SUB
case token.MUL_ASSIGN:
op = token.MUL
case token.QUO_ASSIGN:
op = token.QUO
case token.REM_ASSIGN:
op = token.REM
case token.AND_ASSIGN:
op = token.AND
case token.OR_ASSIGN:
op = token.OR
case token.XOR_ASSIGN:
op = token.XOR
case token.SHL_ASSIGN:
op = token.SHL
case token.SHR_ASSIGN:
op = token.SHR
case token.AND_NOT_ASSIGN:
op = token.AND_NOT
default:
check.invalidAST(s.TokPos, "unknown assignment operation %s", s.Tok) check.invalidAST(s.TokPos, "unknown assignment operation %s", s.Tok)
return return
} }

View File

@ -6,23 +6,7 @@
package decls0 package decls0
import ( import "unsafe"
"unsafe"
// we can have multiple blank imports (was bug)
_ "math"
_ "net/rpc"
// reflect defines a type "flag" which shows up in the gc export data
"reflect"
. "reflect"
init /* ERROR "cannot declare init" */ "fmt"
)
// reflect.flag must not be visible in this package
type flag int
type _ reflect /* ERROR "not exported" */ .flag
// dot-imported exported objects may conflict with local objects
type Value /* ERROR "already declared in this file" */ struct{}
const pi = 3.1415 const pi = 3.1415

43
go/types/testdata/importdecl0a.src vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2013 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 importdecl0
import ()
import (
// we can have multiple blank imports (was bug)
_ "math"
_ "net/rpc"
init /* ERROR "cannot declare init" */ "fmt"
// reflect defines a type "flag" which shows up in the gc export data
"reflect"
. "reflect"
)
import "math" /* ERROR "imported but not used" */
import m /* ERROR "imported but not used" */ "math"
import _ "math"
import (
"math/big" /* ERROR "imported but not used" */
b /* ERROR "imported but not used" */ "math/big"
_ "math/big"
)
import "fmt"
import f "fmt"
// reflect.flag must not be visible in this package
type flag int
type _ reflect /* ERROR "not exported" */ .flag
// dot-imported exported objects may conflict with local objects
type Value /* ERROR "already declared in this file" */ struct{}
var _ = fmt.Println // use "fmt"
func _() {
f.Println() // use "fmt"
}

12
go/types/testdata/importdecl0b.src vendored Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2013 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 importdecl0
import "math"
import m "math"
// using "math" in this file doesn't affect its use in other files
const Pi0 = math.Pi
const Pi1 = m.Pi

47
go/types/token_test.go Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2013 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.
// This file checks invariants of token.Token ordering that we rely on
// since package go/token doesn't provide any guarantees at the moment.
package types
import (
"go/token"
"testing"
)
var assignOps = map[token.Token]token.Token{
token.ADD_ASSIGN: token.ADD,
token.SUB_ASSIGN: token.SUB,
token.MUL_ASSIGN: token.MUL,
token.QUO_ASSIGN: token.QUO,
token.REM_ASSIGN: token.REM,
token.AND_ASSIGN: token.AND,
token.OR_ASSIGN: token.OR,
token.XOR_ASSIGN: token.XOR,
token.SHL_ASSIGN: token.SHL,
token.SHR_ASSIGN: token.SHR,
token.AND_NOT_ASSIGN: token.AND_NOT,
}
func TestZeroTok(t *testing.T) {
// zero value for token.Token must be token.ILLEGAL
var zero token.Token
if token.ILLEGAL != zero {
t.Errorf("%s == %d; want 0", token.ILLEGAL, zero)
}
}
func TestAssignOp(t *testing.T) {
// there are fewer than 256 tokens
for i := 0; i < 256; i++ {
tok := token.Token(i)
got := assignOp(tok)
want := assignOps[tok]
if got != want {
t.Errorf("for assignOp(%s): got %s; want %s", tok, got, want)
}
}
}

View File

@ -194,7 +194,6 @@ func (t *Tuple) At(i int) *Var { return t.vars[i] }
// A Signature represents a (non-builtin) function or method type. // A Signature represents a (non-builtin) function or method type.
type Signature struct { type Signature struct {
scope *Scope // function scope, always present scope *Scope // function scope, always present
labels *Scope // label scope, or nil (lazily allocated)
recv *Var // nil if not a method recv *Var // nil if not a method
params *Tuple // (incoming) parameters from left to right; or nil params *Tuple // (incoming) parameters from left to right; or nil
results *Tuple // (outgoing) results from left to right; or nil results *Tuple // (outgoing) results from left to right; or nil
@ -217,7 +216,7 @@ func NewSignature(scope *Scope, recv *Var, params, results *Tuple, isVariadic bo
panic("types.NewSignature: variadic parameter must be of unnamed slice type") panic("types.NewSignature: variadic parameter must be of unnamed slice type")
} }
} }
return &Signature{scope, nil, recv, params, results, isVariadic} return &Signature{scope, recv, params, results, isVariadic}
} }
// Recv returns the receiver of signature s (if a method), or nil if a // Recv returns the receiver of signature s (if a method), or nil if a