Complain about exported functions/methods returning unexported types.
diff --git a/lint.go b/lint.go
index dc8f807..af7e2e6 100644
--- a/lint.go
+++ b/lint.go
@@ -181,6 +181,7 @@
f.lintIncDec()
f.lintMake()
f.lintErrorReturn()
+ f.lintUnexportedReturn()
}
type link string
@@ -1198,6 +1199,61 @@
})
}
+// lintUnexportedReturn examines exported function declarations.
+// It complains if any return an unexported type.
+func (f *file) lintUnexportedReturn() {
+ f.walk(func(n ast.Node) bool {
+ fn, ok := n.(*ast.FuncDecl)
+ if !ok {
+ return true
+ }
+ if fn.Type.Results == nil {
+ return false
+ }
+ if !fn.Name.IsExported() {
+ return false
+ }
+ thing := "func"
+ if fn.Recv != nil && len(fn.Recv.List) > 0 {
+ thing = "method"
+ if !ast.IsExported(receiverType(fn)) {
+ // Don't report exported methods of unexported types,
+ // such as private implementations of sort.Interface.
+ return false
+ }
+ }
+ for _, ret := range fn.Type.Results.List {
+ typ := f.pkg.typeOf(ret.Type)
+ if exportedType(typ) {
+ continue
+ }
+ f.errorf(ret.Type, 0.8, "exported %s %s returns unexported type %s, which can be annoying to use",
+ thing, fn.Name.Name, typ)
+ break // only flag one
+ }
+ return false
+ })
+}
+
+// exportedType reports whether typ is an exported type.
+// It is imprecise, and will err on the side of returning true,
+// such as for composite types.
+func exportedType(typ types.Type) bool {
+ switch T := typ.(type) {
+ case *types.Named:
+ // Builtin types have no package.
+ return T.Obj().Pkg() == nil || T.Obj().Exported()
+ case *types.Map:
+ return exportedType(T.Key()) && exportedType(T.Elem())
+ case interface {
+ Elem() types.Type
+ }: // array, slice, pointer, chan
+ return exportedType(T.Elem())
+ }
+ // Be conservative about other types, such as struct, interface, etc.
+ return true
+}
+
func receiverType(fn *ast.FuncDecl) string {
switch e := fn.Recv.List[0].Type.(type) {
case *ast.Ident:
diff --git a/lint_test.go b/lint_test.go
index 12e3afb..f0a0acc 100644
--- a/lint_test.go
+++ b/lint_test.go
@@ -9,6 +9,7 @@
import (
"bytes"
"flag"
+ "go/ast"
"go/parser"
"go/printer"
"go/token"
@@ -17,6 +18,8 @@
"regexp"
"strings"
"testing"
+
+ "code.google.com/p/go.tools/go/types"
)
var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
@@ -195,3 +198,41 @@
}
}
}
+
+func TestExportedType(t *testing.T) {
+ tests := []struct {
+ typString string
+ exp bool
+ }{
+ {"int", true},
+ {"string", false}, // references the shadowed builtin "string"
+ {"T", true},
+ {"t", false},
+ {"*T", true},
+ {"*t", false},
+ {"map[int]complex128", true},
+ }
+ for _, test := range tests {
+ src := `package foo; type T int; type t int; type string struct{}`
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "foo.go", src, 0)
+ if err != nil {
+ t.Fatalf("Parsing %q: %v", src, err)
+ }
+ // use the package name as package path
+ pkg, err := types.Check(file.Name.Name, fset, []*ast.File{file})
+ if err != nil {
+ t.Fatalf("Type checking %q: %v", src, err)
+ }
+ // Use the first child scope of the package, which will be the file scope.
+ scope := pkg.Scope().Child(0)
+ typ, _, err := types.Eval(test.typString, pkg, scope)
+ if err != nil {
+ t.Errorf("types.Eval(%q): %v", test.typString, err)
+ continue
+ }
+ if got := exportedType(typ); got != test.exp {
+ t.Errorf("exportedType(%v) = %t, want %t", typ, got, test.exp)
+ }
+ }
+}
diff --git a/testdata/unexp-return.go b/testdata/unexp-return.go
new file mode 100644
index 0000000..7fc9769
--- /dev/null
+++ b/testdata/unexp-return.go
@@ -0,0 +1,46 @@
+// Test for unexported return types.
+
+// Package foo ...
+package foo
+
+import ()
+
+type hidden struct{}
+
+// Exported returns a hidden type, which is annoying.
+func Exported() hidden { // MATCH /Exported.*unexported.*hidden/
+ return hidden{}
+}
+
+// ExpErr returns a builtin type.
+func ExpErr() error { // ok
+}
+
+func (hidden) ExpOnHidden() hidden { // ok
+}
+
+// T is another test type.
+type T struct{}
+
+// MethodOnT returns a hidden type, which is annoying.
+func (T) MethodOnT() hidden { // MATCH /method MethodOnT.*unexported.*hidden/
+ return hidden{}
+}
+
+// ExpT returns a T.
+func ExpT() T { // ok
+ return T{}
+}
+
+func unexp() hidden { // ok
+ return hidden{}
+}
+
+// This is slightly sneaky: we shadow the builtin "int" type.
+
+type int struct{}
+
+// ExportedIntReturner returns an unexported type from this package.
+func ExportedIntReturner() int { // MATCH /ExportedIntReturner.*unexported.*int/
+ return int{}
+}