Repository: mgechev/revive (go)

Issue:
rule.UnexportedReturn: false positive when return type is exported but an alias to an unexported type
**Describe the bug**

Here is an example code that implements functional options, but uses type aliases to hide implementation details.

```go
// Command main...
package main

type config struct {
	N int
	// ...
}

// Option is an option for [New].
type Option = option

// option is the hidden concrete implementation of Option.
type option func(*config)

// WithN sets N.
func WithN(n int) Option { // <------ This should not trigger Unexported Return
	return func(c *config) {
		c.N = n
	}
}

// T is solid.
type T struct{}

// New builds a [T] from [Option]s.
func New(opts ...Option) T {
	var c config
	for _, opt := range opts {
		opt(&c)
	}
	return T{}
}
```

**To Reproduce**
Steps to reproduce the behavior:



```console
$ golangci-lint run -E revive
option.go:16:19: unexported-return: exported func WithN returns unexported type main.Option, which can be annoying to use (revive)
func WithN(n int) Option { // <------ This should not trigger Unexported Return
                  ^
1 issues:
* revive: 1
* $ golangci-lint version      
golangci-lint has version 2.1.6 built with go1.24.2 from eabc2638 on 2025-05-04T15:41:19Z
$ go version -m $(which golangci-lint) | grep revive
	dep	github.com/mgechev/revive	v1.9.0	h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM=
```

-->

**Expected behavior**

A function that returns an exported type (`Option` in the example) should not trigger the UnexportedReturn rule. Even if the type is an alias to an unexported type (`option` in the example).


Pull request:
unexported-return: fix type aliases support
Fixes #1406
Closes #1407

Patch:
diff --git a/rule/unexported_return.go b/rule/unexported_return.go
index 7a51d71..e82dcff 100644
--- a/rule/unexported_return.go
+++ b/rule/unexported_return.go
@@ -78,7 +78,18 @@ func (*UnexportedReturnRule) Name() string {
 // 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 := types.Unalias(typ).(type) {
+	switch t := typ.(type) {
+	case *types.Alias:
+		obj := t.Obj()
+		switch {
+		// Builtin types have no package.
+		case obj.Pkg() == nil:
+		case obj.Exported():
+		default:
+			_, ok := t.Underlying().(*types.Interface)
+			return ok
+		}
+		return true
 	case *types.Named:
 		obj := t.Obj()
 		switch {


Test patch:
diff --git a/testdata/golint/unexported_return.go b/testdata/golint/unexported_return.go
index 226e5d3..56329d2 100644
--- a/testdata/golint/unexported_return.go
+++ b/testdata/golint/unexported_return.go
@@ -58,3 +58,31 @@ type int struct{}
 func ExportedIntReturner() int { // MATCH /exported func ExportedIntReturner returns unexported type foo.int, which can be annoying to use/
 	return int{}
 }
+
+type config struct {
+	N int
+}
+
+// Option ...
+type Option = option
+
+type option func(*config)
+
+// WithN ...
+func WithN(n int) Option {
+	return func(c *config) {
+		c.N = n
+	}
+}
+
+type b = A
+
+// A ...
+type A func(*config)
+
+// WithA ...
+func WithA(n int) b { // MATCH /exported func WithA returns unexported type foo.b, which can be annoying to use/
+	return func(c *config) {
+		c.N = n
+	}
+}