From f45db8d86ba019b9226abf32eeda01492f9ee89c Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Mon, 19 May 2025 23:53:55 +0700 Subject: [PATCH 1/4] Add unexported analyzer --- .golangci.yml | 1 - README.md | 2 + cmd/ifacecheck/main.go | 2 + unexported/cmd/unexportediface/main.go | 10 ++ unexported/doc.go | 3 + unexported/testdata/src/a/a.go | 103 ++++++++++++ unexported/testdata/src/b/b.go | 104 ++++++++++++ unexported/unexported.go | 212 +++++++++++++++++++++++++ unexported/unexported_test.go | 14 ++ 9 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 unexported/cmd/unexportediface/main.go create mode 100644 unexported/doc.go create mode 100644 unexported/testdata/src/a/a.go create mode 100644 unexported/testdata/src/b/b.go create mode 100644 unexported/unexported.go create mode 100644 unexported/unexported_test.go diff --git a/.golangci.yml b/.golangci.yml index 0781ea9..92e2a90 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,7 +10,6 @@ linters: - copyloopvar - decorder - dogsled - - dupl - dupword - durationcheck - err113 diff --git a/README.md b/README.md index dfdb4a7..2f9f90f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ It consists of several analyzers: 1. `unused`: Identifies interfaces that are not used anywhere in the same package where the interface is defined. 2. `identical`: Identifies interfaces in the same package with identical methods or constraints. 3. `opaque`: Identifies functions that return interfaces, but the actual returned value is always a single concrete implementation. +4. `unexported`: Identifies interfaces that are not exported but are used in exported functions or methods. ## Usage @@ -21,6 +22,7 @@ To install individual linter, use the following command: go install github.com/uudashr/iface/unused/cmd/unusediface@latest go install github.com/uudashr/iface/identical/cmd/identicaliface@latest go install github.com/uudashr/iface/opaque/cmd/opaqueiface@latest +go install github.com/uudashr/iface/unexported/cmd/unexportediface@latest ``` Run the linter diff --git a/cmd/ifacecheck/main.go b/cmd/ifacecheck/main.go index b2ca484..3988fc6 100644 --- a/cmd/ifacecheck/main.go +++ b/cmd/ifacecheck/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/uudashr/iface/identical" "github.com/uudashr/iface/opaque" + "github.com/uudashr/iface/unexported" "github.com/uudashr/iface/unused" "golang.org/x/tools/go/analysis/multichecker" ) @@ -12,5 +13,6 @@ func main() { unused.Analyzer, identical.Analyzer, opaque.Analyzer, + unexported.Analyzer, ) } diff --git a/unexported/cmd/unexportediface/main.go b/unexported/cmd/unexportediface/main.go new file mode 100644 index 0000000..2c2b7ea --- /dev/null +++ b/unexported/cmd/unexportediface/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/uudashr/iface/unexported" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { + singlechecker.Main(unexported.Analyzer) +} diff --git a/unexported/doc.go b/unexported/doc.go new file mode 100644 index 0000000..2ac7af4 --- /dev/null +++ b/unexported/doc.go @@ -0,0 +1,3 @@ +// Package unexported defines an Analyzer that identifies interfaces that are +// not exported but are used in exported functions or methods. +package unexported diff --git a/unexported/testdata/src/a/a.go b/unexported/testdata/src/a/a.go new file mode 100644 index 0000000..0796120 --- /dev/null +++ b/unexported/testdata/src/a/a.go @@ -0,0 +1,103 @@ +package a + +import ( + "io" +) + +// unexportedReader is an unexported interface. +type unexportedReader interface { + Read([]byte) (int, error) +} + +// ExportedWriter is an exported interface. +type ExportedWriter interface { + Write([]byte) (int, error) +} + +// 1. Function with unexported arg type interface +func ReadAll(r unexportedReader) ([]byte, error) { // want "unexported interface 'unexportedReader' used as parameter in exported function 'ReadAll'" + buf := make([]byte, 1024) + _, err := r.Read(buf) + return buf, err +} + +// 2. Function with exported arg type interface +func WriteHello(w ExportedWriter) error { + _, err := w.Write([]byte("hello")) + return err +} + +// 3. Function with unexported return type interface +func NewUnexportedReader() unexportedReader { // want "unexported interface 'unexportedReader' used as return value in exported function 'NewUnexportedReader'" + return nil // stub +} + +// 4. Function with exported return type interface +func NewWriter() ExportedWriter { + return nil // stub +} + +// 5. Function with both unexported arg and return type interface +func WrapReader(r unexportedReader) unexportedReader { // want "unexported interface 'unexportedReader' used as parameter in exported function 'WrapReader'" "unexported interface 'unexportedReader' used as return value in exported function 'WrapReader'" + return r +} + +// 6. Function with both exported arg and return type interface +func WrapWriter(w ExportedWriter) ExportedWriter { + return w +} + +// 7. Function with unexported arg type interface and exported return type interface +func PromoteReader(r unexportedReader) ExportedWriter { // want "unexported interface 'unexportedReader' used as parameter in exported function 'PromoteReader'" + return nil // stub +} + +// 8. Function with exported arg type interface and unexported return type interface +func DemoteWriter(w ExportedWriter) unexportedReader { // want "unexported interface 'unexportedReader' used as return value in exported function 'DemoteWriter'" + return nil // stub +} + +// 9. Function with external arg type interface (io.Writer) +func WriteToExternal(w io.Writer) error { + _, err := w.Write([]byte("external")) + return err +} + +// 10. Function with external return type interface (io.Writer) +func NewExternalWriter() io.Writer { + return nil // stub +} + +// 11. Function with both external arg and return type interface (io.Writer) +func WrapExternalWriter(w io.Writer) io.Writer { + return w +} + +// 12. Function with any arg type +func Print(a any) { +} + +// 13. Function witn interface{} arg type +func Write(i interface{}) { +} + +// 14. Function with error arg type +func Capture(err error) {} + +// 15. Function with no arg and no return type interface +func Ping() {} + +// 16. Function with primitive arg type +func AddOne(x int) int { + return x + 1 +} + +// 17. Function with primitive return type +func GetZero() int { + return 0 +} + +// 18. Function with both primitive arg and return type +func Double(x int) int { + return x * 2 +} diff --git a/unexported/testdata/src/b/b.go b/unexported/testdata/src/b/b.go new file mode 100644 index 0000000..1903e8e --- /dev/null +++ b/unexported/testdata/src/b/b.go @@ -0,0 +1,104 @@ +package b + +import "io" + +// unexportedReader is an unexported interface. +type unexportedReader interface { + Read([]byte) (int, error) +} + +// ExportedWriter is an exported interface. +type ExportedWriter interface { + Write([]byte) (int, error) +} + +// ExportedType is an exported struct type. +type ExportedType struct{} + +// 1. Method with unexported arg type interface +func (e *ExportedType) ReadAll(r unexportedReader) ([]byte, error) { // want "unexported interface 'unexportedReader' used as parameter in exported method 'ExportedType.ReadAll'" + buf := make([]byte, 1024) + _, err := r.Read(buf) + return buf, err +} + +// 2. Method with exported arg type interface +func (e *ExportedType) WriteHello(w ExportedWriter) error { + _, err := w.Write([]byte("hello")) + return err +} + +// 3. Method with unexported return type interface +func (e *ExportedType) NewUnexportedReader() unexportedReader { // want "unexported interface 'unexportedReader' used as return value in exported method 'ExportedType.NewUnexportedReader'" + return nil // stub +} + +// 4. Method with exported return type interface +func (e *ExportedType) NewWriter() ExportedWriter { + return nil // stub +} + +// 5. Method with both unexported arg and return type interface +func (e *ExportedType) WrapReader(r unexportedReader) unexportedReader { // want "unexported interface 'unexportedReader' used as parameter in exported method 'ExportedType.WrapReader'" "unexported interface 'unexportedReader' used as return value in exported method 'ExportedType.WrapReader'" + return r +} + +// 6. Method with both exported arg and return type interface +func (e *ExportedType) WrapWriter(w ExportedWriter) ExportedWriter { + return w +} + +// 7. Method with unexported arg type interface and exported return type interface +func (e *ExportedType) PromoteReader(r unexportedReader) ExportedWriter { // want "unexported interface 'unexportedReader' used as parameter in exported method 'ExportedType.PromoteReader'" + return nil // stub +} + +// 8. Method with exported arg type interface and unexported return type interface +func (e *ExportedType) DemoteWriter(w ExportedWriter) unexportedReader { // want "unexported interface 'unexportedReader' used as return value in exported method 'ExportedType.DemoteWriter'" + return nil // stub +} + +// 9. Method with external arg type interface (io.Writer) +func (e *ExportedType) WriteToExternal(w io.Writer) error { + _, err := w.Write([]byte("external")) + return err +} + +// 10. Method with external return type interface (io.Writer) +func (e *ExportedType) NewExternalWriter() io.Writer { + return nil // stub +} + +// 11. Method with both external arg and return type interface (io.Writer) +func (e *ExportedType) WrapExternalWriter(w io.Writer) io.Writer { + return w +} + +// 13. Method with any arg type +func (e *ExportedType) Print(a any) { +} + +// 14. Method with interface{} arg type +func (e *ExportedType) Write(i interface{}) { +} + +// 15. Method with error arg type +func (e *ExportedType) Capture(err error) {} + +// 16. Method with no arg and no return type interface +func (e *ExportedType) Ping() {} + +// 17. Method with primitive arg type +func (e *ExportedType) AddOne(x int) int { + return x + 1 +} + +// 18. Method with primitive return type +func (e *ExportedType) GetZero() int { + return 0 +} + +// 19. Method with both primitive arg and return type +func (e *ExportedType) Double(x int) int { + return x * 2 +} diff --git a/unexported/unexported.go b/unexported/unexported.go new file mode 100644 index 0000000..23ee24d --- /dev/null +++ b/unexported/unexported.go @@ -0,0 +1,212 @@ +package unexported + +import ( + "fmt" + "go/ast" + "go/types" + "reflect" + + "github.com/uudashr/iface/internal/directive" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer is the unexported interface analyzer. +var Analyzer = newAnalyzer() + +func newAnalyzer() *analysis.Analyzer { + r := runner{} + + analyzer := &analysis.Analyzer{ + Name: "unexported", + Doc: "Identifies interfaces that are not exported but are used in exported functions or methods", + URL: "https://pkg.go.dev/github.com/uudashr/iface/visibility", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: r.run, + } + + analyzer.Flags.BoolVar(&r.debug, "nerd", false, "enable nerd mode") + + return analyzer +} + +type runner struct { + debug bool +} + +func (r *runner) run(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.FuncDecl)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + funcDecl := n.(*ast.FuncDecl) + + r.debugln("FuncDecl:", funcDecl.Name.Name) + + dir := directive.ParseIgnore(funcDecl.Doc) + if dir != nil && dir.ShouldIgnore(pass.Analyzer.Name) { + // skip ignored function + r.debugln(" skip ignored") + + return + } + + var recvName string + + if recv := funcDecl.Recv; recv != nil { + recvType := recv.List[0].Type + infoType := pass.TypesInfo.TypeOf(recvType) + + r.debugln(" recvType:", recvType, "infoType:", infoType, "reflectType:", reflect.TypeOf(recvType)) + + switch typ := recvType.(type) { + case *ast.Ident: + r.debugln(" recvIdent:", typ.Name) + + recvName = typ.Name + case *ast.StarExpr: + r.debugln(" recvStarExpr:", typ.X) + + if ident, ok := typ.X.(*ast.Ident); ok { + r.debugln(" recvIdent:", ident.Name) + recvName = ident.Name + } else { + r.debugln(" unhandled") + } + default: + r.debugln(" unhandled") + } + } + + if !funcDecl.Name.IsExported() { + // skip unexported functions + r.debugln(" skip non-exported") + + return + } + + r.debugln(" params:") + + params := funcDecl.Type.Params + + for _, param := range params.List { + paramType := param.Type + infoType := pass.TypesInfo.TypeOf(paramType) + + r.debugln(" paramType:", paramType, "infoType:", infoType, "reflectType:", reflect.TypeOf(paramType)) + + if !types.IsInterface(infoType) { + // skip non-interface + r.debugln(" skip non-interface") + + continue + } + + if infoType.String() == "error" { + // skip error interface + r.debugln(" skip error interface") + + continue + } + + if infoType.String() == "any" { + // skip any interface + r.debugln(" skip any interface") + + continue + } + + switch typ := paramType.(type) { + case *ast.Ident: + if !typ.IsExported() { + r.debugln(" unexported") + + funcMethod := "function" + funcMethodName := funcDecl.Name.Name + + if recvName != "" { + funcMethod = "method" + funcMethodName = recvName + "." + funcDecl.Name.Name + } + + pass.Report(analysis.Diagnostic{ + Pos: typ.Pos(), + Message: fmt.Sprintf("unexported interface '%s' used as parameter in exported %s '%s'", typ.Name, funcMethod, funcMethodName), + }) + } + default: + r.debugln(" unhandled") + } + } + + r.debugln(" results:") + + results := funcDecl.Type.Results + if results == nil { + r.debugln(" no results") + + return + } + + for _, result := range results.List { + resultType := result.Type + infoType := pass.TypesInfo.TypeOf(resultType) + + r.debugln(" resultType:", resultType, "infoType:", infoType, "reflectType:", reflect.TypeOf(resultType)) + + if !types.IsInterface(infoType) { + r.debugln(" skip non-interface") + + continue + } + + if infoType.String() == "error" { + // skip error interface + r.debugln(" skip error interface") + + continue + } + + if infoType.String() == "any" { + // skip any interface + r.debugln(" skip any interface") + + continue + } + + switch typ := resultType.(type) { + case *ast.Ident: + if !typ.IsExported() { + r.debugln(" unexported") + + funcMethod := "function" + funcMethodName := funcDecl.Name.Name + + if recvName != "" { + funcMethod = "method" + funcMethodName = recvName + "." + funcDecl.Name.Name + } + + pass.Report(analysis.Diagnostic{ + Pos: typ.Pos(), + Message: fmt.Sprintf("unexported interface '%s' used as return value in exported %s '%s'", typ.Name, funcMethod, funcMethodName), + }) + } + default: + r.debugln(" unhandled") + } + } + }) + + return nil, nil +} + +func (r *runner) debugln(a ...any) { + if r.debug { + fmt.Println(a...) + } +} diff --git a/unexported/unexported_test.go b/unexported/unexported_test.go new file mode 100644 index 0000000..56a7d77 --- /dev/null +++ b/unexported/unexported_test.go @@ -0,0 +1,14 @@ +package unexported_test + +import ( + "testing" + + "github.com/uudashr/iface/unexported" + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, unexported.Analyzer, "a") + analysistest.Run(t, testdata, unexported.Analyzer, "b") +} From 06c509b3456506ca3262884262a61e4cb3f322b3 Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Tue, 20 May 2025 00:17:28 +0700 Subject: [PATCH 2/4] Improve diagnostic message --- identical/identical.go | 2 +- identical/testdata/src/a/a.go | 6 +++--- identical/testdata/src/b/b.go | 4 ++-- identical/testdata/src/c/c.go | 4 ++-- opaque/opaque.go | 2 +- opaque/testdata/src/a/a.go | 10 +++++----- opaque/testdata/src/a/a.go.golden | 10 +++++----- opaque/testdata/src/f/f.go | 5 ++--- opaque/testdata/src/f/f.go.golden | 5 ++--- opaque/testdata/src/h/h.go | 2 +- opaque/testdata/src/h/h.go.golden | 2 +- opaque/testdata/src/x/x.go | 14 ++++---------- opaque/testdata/src/x/x.go.golden | 14 ++++---------- unused/testdata/src/a/a.go | 2 +- unused/testdata/src/agroup/agroup.go | 2 +- unused/testdata/src/d/d.go | 4 ++-- unused/unused.go | 8 +++++++- 17 files changed, 44 insertions(+), 52 deletions(-) diff --git a/identical/identical.go b/identical/identical.go index fd1816a..7ffa469 100644 --- a/identical/identical.go +++ b/identical/identical.go @@ -128,7 +128,7 @@ Loop: fmt.Println("Identical interface:", name, "and", otherName) } - pass.Reportf(ifaceDecls[name], "interface %s contains identical methods or type constraints from another interface, causing redundancy", name) + pass.Reportf(ifaceDecls[name], "interface '%s' contains identical methods or type constraints with another interface, causing redundancy", name) continue Loop } diff --git a/identical/testdata/src/a/a.go b/identical/testdata/src/a/a.go index 93db47f..726f0e3 100644 --- a/identical/testdata/src/a/a.go +++ b/identical/testdata/src/a/a.go @@ -1,14 +1,14 @@ package comm -type Pinger interface { // want "interface Pinger contains identical methods or type constraints from another interface, causing redundancy" +type Pinger interface { // want "interface 'Pinger' contains identical methods or type constraints with another interface, causing redundancy" Ping() error } -type Healthcheck interface { // want "interface Healthcheck contains identical methods or type constraints from another interface, causing redundancy" +type Healthcheck interface { // want "interface 'Healthcheck' contains identical methods or type constraints with another interface, causing redundancy" Ping() error } -type Checker interface { // want "interface Checker contains identical methods or type constraints from another interface, causing redundancy" +type Checker interface { // want "interface 'Checker' contains identical methods or type constraints with another interface, causing redundancy" Pinger } diff --git a/identical/testdata/src/b/b.go b/identical/testdata/src/b/b.go index e92f52a..94bb424 100644 --- a/identical/testdata/src/b/b.go +++ b/identical/testdata/src/b/b.go @@ -11,10 +11,10 @@ type ordered interface { ~string } -type Int interface { // want "interface Int contains identical methods or type constraints from another interface, causing redundancy" +type Int interface { // want "interface 'Int' contains identical methods or type constraints with another interface, causing redundancy" int32 } -type Int32 interface { // want "interface Int32 contains identical methods or type constraints from another interface, causing redundancy" +type Int32 interface { // want "interface 'Int32' contains identical methods or type constraints with another interface, causing redundancy" int32 } diff --git a/identical/testdata/src/c/c.go b/identical/testdata/src/c/c.go index 0caac69..ee5b1f5 100644 --- a/identical/testdata/src/c/c.go +++ b/identical/testdata/src/c/c.go @@ -1,11 +1,11 @@ package stream -type StreamNameLister interface { // want "interface StreamNameLister contains identical methods or type constraints from another interface, causing redundancy" +type StreamNameLister interface { // want "interface 'StreamNameLister' contains identical methods or type constraints with another interface, causing redundancy" Name() <-chan string Err() error } -type ConsumerNameLister interface { // want "interface ConsumerNameLister contains identical methods or type constraints from another interface, causing redundancy" +type ConsumerNameLister interface { // want "interface 'ConsumerNameLister' contains identical methods or type constraints with another interface, causing redundancy" Name() <-chan string Err() error } diff --git a/opaque/opaque.go b/opaque/opaque.go index 86c5db4..2ff31a2 100644 --- a/opaque/opaque.go +++ b/opaque/opaque.go @@ -277,7 +277,7 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) { stmtTypName = removePkgPrefix(stmtTypName) } - msg := fmt.Sprintf("%s function return %s interface at the %s result, abstract a single concrete implementation of %s", + msg := fmt.Sprintf("'%s' function return '%s' interface at the %s result, abstract a single concrete implementation of '%s'", funcDecl.Name.Name, retTypeName, positionStr(currentIdx), diff --git a/opaque/testdata/src/a/a.go b/opaque/testdata/src/a/a.go index 52cb352..f40abe2 100644 --- a/opaque/testdata/src/a/a.go +++ b/opaque/testdata/src/a/a.go @@ -14,19 +14,19 @@ func (s server) Serve() error { return nil } -func NewServer(addr string) Server { // want "NewServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer(addr string) Server { // want "'NewServer' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" return newServer(addr) } -func NewServer2(addr string) Server { // want "NewServer2 function return Server interface at the 1st result, abstract a single concrete implementation of server" +func NewServer2(addr string) Server { // want "'NewServer2' function return 'Server' interface at the 1st result, abstract a single concrete implementation of 'server'" return server{addr: addr} } -func NewServer3(addr string) Server { // want "NewServer3 function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer3(addr string) Server { // want "'NewServer3' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" return &server{addr: addr} } -func NewServer4(addr string) (Server, error) { // want "NewServer4 function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer4(addr string) (Server, error) { // want "'NewServer4' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" if addr == "" { return nil, errors.New("addr cannot be nil") } @@ -45,6 +45,6 @@ func (ss *secureServer) Serve() error { return nil } -func NewSecureServer(addr string) Server { // want "NewSecureServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*secureServer" +func NewSecureServer(addr string) Server { // want "'NewSecureServer' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*secureServer'" return &secureServer{addr: addr} } diff --git a/opaque/testdata/src/a/a.go.golden b/opaque/testdata/src/a/a.go.golden index 532e99c..1341efb 100644 --- a/opaque/testdata/src/a/a.go.golden +++ b/opaque/testdata/src/a/a.go.golden @@ -14,19 +14,19 @@ func (s server) Serve() error { return nil } -func NewServer(addr string) *server { // want "NewServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer(addr string) *server { // want "'NewServer' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" return newServer(addr) } -func NewServer2(addr string) server { // want "NewServer2 function return Server interface at the 1st result, abstract a single concrete implementation of server" +func NewServer2(addr string) server { // want "'NewServer2' function return 'Server' interface at the 1st result, abstract a single concrete implementation of 'server'" return server{addr: addr} } -func NewServer3(addr string) *server { // want "NewServer3 function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer3(addr string) *server { // want "'NewServer3' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" return &server{addr: addr} } -func NewServer4(addr string) (*server, error) { // want "NewServer4 function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" +func NewServer4(addr string) (*server, error) { // want "'NewServer4' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*server'" if addr == "" { return nil, errors.New("addr cannot be nil") } @@ -45,6 +45,6 @@ func (ss *secureServer) Serve() error { return nil } -func NewSecureServer(addr string) *secureServer { // want "NewSecureServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*secureServer" +func NewSecureServer(addr string) *secureServer { // want "'NewSecureServer' function return 'Server' interface at the 1st result, abstract a single concrete implementation of '\\*secureServer'" return &secureServer{addr: addr} } diff --git a/opaque/testdata/src/f/f.go b/opaque/testdata/src/f/f.go index d65fcbb..ad23c55 100644 --- a/opaque/testdata/src/f/f.go +++ b/opaque/testdata/src/f/f.go @@ -10,8 +10,7 @@ type Span interface { Finish() } -type span struct { -} +type span struct{} func (s *span) Finish() { } @@ -24,7 +23,7 @@ func StartSpanFromContextWithTracer(ctx context.Context, tracer Tracer) (Span, e return nil, nil } -func StartSpanFromContext2(ctx context.Context) (Span, error) { // want "StartSpanFromContext2 function return Span interface at the 1st result, abstract a single concrete implementation of \\*span" +func StartSpanFromContext2(ctx context.Context) (Span, error) { // want "'StartSpanFromContext2' function return 'Span' interface at the 1st result, abstract a single concrete implementation of '\\*span'" return startSpanFromContextWithTracer(ctx, GlobalTracer) } diff --git a/opaque/testdata/src/f/f.go.golden b/opaque/testdata/src/f/f.go.golden index 434941b..2a97d37 100644 --- a/opaque/testdata/src/f/f.go.golden +++ b/opaque/testdata/src/f/f.go.golden @@ -10,8 +10,7 @@ type Span interface { Finish() } -type span struct { -} +type span struct {} func (s *span) Finish() { } @@ -24,7 +23,7 @@ func StartSpanFromContextWithTracer(ctx context.Context, tracer Tracer) (Span, e return nil, nil } -func StartSpanFromContext2(ctx context.Context) (*span, error) { // want "StartSpanFromContext2 function return Span interface at the 1st result, abstract a single concrete implementation of \\*span" +func StartSpanFromContext2(ctx context.Context) (*span, error) { // want "'StartSpanFromContext2' function return 'Span' interface at the 1st result, abstract a single concrete implementation of '\\*span'" return startSpanFromContextWithTracer(ctx, GlobalTracer) } diff --git a/opaque/testdata/src/h/h.go b/opaque/testdata/src/h/h.go index 68feab4..a715505 100644 --- a/opaque/testdata/src/h/h.go +++ b/opaque/testdata/src/h/h.go @@ -17,7 +17,7 @@ func NewServer(addr string) Server { return newServer(addr) } -func NewServer2(addr string) Server { // want "NewServer2 function return Server interface at the 1st result, abstract a single concrete implementation of server" +func NewServer2(addr string) Server { // want "'NewServer2' function return 'Server' interface at the 1st result, abstract a single concrete implementation of 'server'" return server{addr: addr} } diff --git a/opaque/testdata/src/h/h.go.golden b/opaque/testdata/src/h/h.go.golden index d16e192..2f0e2dd 100644 --- a/opaque/testdata/src/h/h.go.golden +++ b/opaque/testdata/src/h/h.go.golden @@ -17,7 +17,7 @@ func NewServer(addr string) Server { return newServer(addr) } -func NewServer2(addr string) server { // want "NewServer2 function return Server interface at the 1st result, abstract a single concrete implementation of server" +func NewServer2(addr string) server { // want "'NewServer2' function return 'Server' interface at the 1st result, abstract a single concrete implementation of 'server'" return server{addr: addr} } diff --git a/opaque/testdata/src/x/x.go b/opaque/testdata/src/x/x.go index 717eb84..6775e01 100644 --- a/opaque/testdata/src/x/x.go +++ b/opaque/testdata/src/x/x.go @@ -11,29 +11,23 @@ type Doer interface { DoItAgain() } -type DoerImpl struct { -} +type DoerImpl struct{} func (di DoerImpl) Do() { - } func (di *DoerImpl) DoItAgain() { - } -type DoerV2Impl struct { -} +type DoerV2Impl struct{} func (d2i DoerV2Impl) Do() { - } func (d2i *DoerV2Impl) DoItAgain() { - } -func NewDoer() Doer { // want "NewDoer function return Doer interface at the 1st result, abstract a single concrete implementation of \\*DoerImpl" +func NewDoer() Doer { // want "'NewDoer' function return 'Doer' interface at the 1st result, abstract a single concrete implementation of '\\*DoerImpl'" return &DoerImpl{} } @@ -58,7 +52,7 @@ func NewDoerWithOK() (Doer, bool) { return true }) - var x = func() bool { + x := func() bool { return true } diff --git a/opaque/testdata/src/x/x.go.golden b/opaque/testdata/src/x/x.go.golden index 0c15a3b..fcfa72b 100644 --- a/opaque/testdata/src/x/x.go.golden +++ b/opaque/testdata/src/x/x.go.golden @@ -11,29 +11,23 @@ type Doer interface { DoItAgain() } -type DoerImpl struct { -} +type DoerImpl struct{} func (di DoerImpl) Do() { - } func (di *DoerImpl) DoItAgain() { - } -type DoerV2Impl struct { -} +type DoerV2Impl struct{} func (d2i DoerV2Impl) Do() { - } func (d2i *DoerV2Impl) DoItAgain() { - } -func NewDoer() *DoerImpl { // want "NewDoer function return Doer interface at the 1st result, abstract a single concrete implementation of \\*DoerImpl" +func NewDoer() *DoerImpl { // want "'NewDoer' function return 'Doer' interface at the 1st result, abstract a single concrete implementation of '\\*DoerImpl'" return &DoerImpl{} } @@ -58,7 +52,7 @@ func NewDoerWithOK() (Doer, bool) { return true }) - var x = func() bool { + x := func() bool { return true } diff --git a/unused/testdata/src/a/a.go b/unused/testdata/src/a/a.go index 211456a..9f97dad 100644 --- a/unused/testdata/src/a/a.go +++ b/unused/testdata/src/a/a.go @@ -7,7 +7,7 @@ type User struct { Name string } -type UserRepository interface { // want "interface UserRepository is declared but not used within the package" +type UserRepository interface { // want "interface 'UserRepository' is declared but not used within the package" UserOf(id string) (*User, error) } diff --git a/unused/testdata/src/agroup/agroup.go b/unused/testdata/src/agroup/agroup.go index 9abc4dc..6e878b7 100644 --- a/unused/testdata/src/agroup/agroup.go +++ b/unused/testdata/src/agroup/agroup.go @@ -8,7 +8,7 @@ type ( Name string } - UserRepository interface { // want "interface UserRepository is declared but not used within the package" + UserRepository interface { // want "interface 'UserRepository' is declared but not used within the package" UserOf(id string) (*User, error) } diff --git a/unused/testdata/src/d/d.go b/unused/testdata/src/d/d.go index c1e094c..0d16179 100644 --- a/unused/testdata/src/d/d.go +++ b/unused/testdata/src/d/d.go @@ -7,7 +7,7 @@ type Doer interface { Do() error } -type Greeter interface { // want "interface Greeter is declared but not used within the package" +type Greeter interface { // want "interface 'Greeter' is declared but not used within the package" Greet() error } @@ -17,7 +17,7 @@ type Runner interface { } //iface:ignore=other -type Executor interface { // want "interface Executor is declared but not used within the package" +type Executor interface { // want "interface 'Executor' is declared but not used within the package" Execute() error } diff --git a/unused/unused.go b/unused/unused.go index 85eecc3..ae3325c 100644 --- a/unused/unused.go +++ b/unused/unused.go @@ -147,7 +147,7 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) { node = ts } - msg := fmt.Sprintf("interface %s is declared but not used within the package", name) + msg := fmt.Sprintf("interface '%s' is declared but not used within the package", name) pass.Report(analysis.Diagnostic{ Pos: ts.Pos(), Message: msg, @@ -168,3 +168,9 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) { return nil, nil } + +func (r *runner) debugf(format string, a ...any) { + if r.debug { + fmt.Printf(format, a...) + } +} From 100cac169ca3ff8636875efdc57c81dd38f820d4 Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Tue, 20 May 2025 00:20:26 +0700 Subject: [PATCH 3/4] Optimize debugging --- unexported/unexported.go | 6 ++++-- unused/unused.go | 6 ------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/unexported/unexported.go b/unexported/unexported.go index 23ee24d..f85cab2 100644 --- a/unexported/unexported.go +++ b/unexported/unexported.go @@ -59,9 +59,11 @@ func (r *runner) run(pass *analysis.Pass) (any, error) { if recv := funcDecl.Recv; recv != nil { recvType := recv.List[0].Type - infoType := pass.TypesInfo.TypeOf(recvType) - r.debugln(" recvType:", recvType, "infoType:", infoType, "reflectType:", reflect.TypeOf(recvType)) + if r.debug { + infoType := pass.TypesInfo.TypeOf(recvType) + fmt.Println(" recvType:", recvType, "infoType:", infoType, "reflectType:", reflect.TypeOf(recvType)) + } switch typ := recvType.(type) { case *ast.Ident: diff --git a/unused/unused.go b/unused/unused.go index ae3325c..4bb04d6 100644 --- a/unused/unused.go +++ b/unused/unused.go @@ -168,9 +168,3 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) { return nil, nil } - -func (r *runner) debugf(format string, a ...any) { - if r.debug { - fmt.Printf(format, a...) - } -} From 626b754a50db9ef8c64f3cc7b610c737255ebff2 Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Tue, 20 May 2025 09:49:07 +0700 Subject: [PATCH 4/4] Improve doc --- README.md | 2 +- unexported/doc.go | 3 ++- unexported/unexported.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f9f90f..53fba3d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It consists of several analyzers: 1. `unused`: Identifies interfaces that are not used anywhere in the same package where the interface is defined. 2. `identical`: Identifies interfaces in the same package with identical methods or constraints. 3. `opaque`: Identifies functions that return interfaces, but the actual returned value is always a single concrete implementation. -4. `unexported`: Identifies interfaces that are not exported but are used in exported functions or methods. +4. `unexported`: Identifies interfaces that are not exported but are used in exported functions or methods as parameters or return values. ## Usage diff --git a/unexported/doc.go b/unexported/doc.go index 2ac7af4..775ed72 100644 --- a/unexported/doc.go +++ b/unexported/doc.go @@ -1,3 +1,4 @@ // Package unexported defines an Analyzer that identifies interfaces that are -// not exported but are used in exported functions or methods. +// not exported but are used in exported functions or methods as parameters or +// return values. package unexported diff --git a/unexported/unexported.go b/unexported/unexported.go index f85cab2..8eddb65 100644 --- a/unexported/unexported.go +++ b/unexported/unexported.go @@ -20,7 +20,7 @@ func newAnalyzer() *analysis.Analyzer { analyzer := &analysis.Analyzer{ Name: "unexported", - Doc: "Identifies interfaces that are not exported but are used in exported functions or methods", + Doc: "Identifies interfaces that are not exported but are used in exported functions or methods as parameters or return values", URL: "https://pkg.go.dev/github.com/uudashr/iface/visibility", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: r.run,