diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index c86316bff9bd..5e9afbb1eb74 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -63,6 +63,7 @@ linters: - gosmopolitan - govet - grouper + - iface - importas - inamedparam - ineffassign @@ -178,6 +179,7 @@ linters: - gosmopolitan - govet - grouper + - ifcae - importas - inamedparam - ineffassign @@ -1928,6 +1930,20 @@ linters-settings: # Default: false var-require-grouping: true + iface: + # List of analyzers. + # Default: ["identical"] + enable: + - identical # Identifies interfaces in the same package that have identical method sets. + - unused # Identifies interfaces that are not used anywhere in the same package where the interface is defined. + - opaque # Identifies functions that return interfaces, but the actual returned value is always a single concrete implementation. + settings: + unused: + # List of packages path to exclude from the check. + # Default: [] + exclude: + - github.com/example/log + importas: # Do not allow unaliased imports of aliased packages. # Default: false diff --git a/go.mod b/go.mod index 0fcfa7548431..6e48abaddeaf 100644 --- a/go.mod +++ b/go.mod @@ -116,6 +116,7 @@ require ( github.com/ultraware/funlen v0.1.0 github.com/ultraware/whitespace v0.1.1 github.com/uudashr/gocognit v1.1.3 + github.com/uudashr/iface v1.2.0 github.com/valyala/quicktemplate v1.8.0 github.com/xen0n/gosmopolitan v1.2.2 github.com/yagipy/maintidx v1.0.0 diff --git a/go.sum b/go.sum index 5191c16fb187..61ea9d20f80f 100644 --- a/go.sum +++ b/go.sum @@ -561,6 +561,8 @@ github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/ github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= +github.com/uudashr/iface v1.2.0 h1:ECJjh5q/1Zmnv/2yFpWV6H3oMg5+Mo+vL0aqw9Gjazo= +github.com/uudashr/iface v1.2.0/go.mod h1:Ux/7d/rAF3owK4m53cTVXL4YoVHKNqnoOeQHn2xrlp0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k= diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index 20253cb5baf5..7a012ec288df 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -295,6 +295,13 @@ "waitgroup-by-value" ] }, + "iface-analyzers": { + "enum": [ + "identical", + "unused", + "opaque" + ] + }, "linters": { "$comment": "anyOf with enum is used to allow auto completion of non-custom linters", "description": "Linters usable.", @@ -356,6 +363,7 @@ "gosmopolitan", "govet", "grouper", + "iface", "ifshort", "importas", "inamedparam", @@ -1922,6 +1930,37 @@ } } }, + "iface": { + "type": "object", + "additionalProperties": false, + "properties": { + "enable": { + "description": "Enable analyzers by name.", + "type": "array", + "items": { + "$ref": "#/definitions/iface-analyzers" + } + }, + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "unused": { + "type": "object", + "additionalProperties": false, + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, "importas": { "type": "object", "additionalProperties": false, diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 12f9725d6bdc..3760d6a40de8 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -237,6 +237,7 @@ type LintersSettings struct { Gosmopolitan GosmopolitanSettings Govet GovetSettings Grouper GrouperSettings + Iface IfaceSettings ImportAs ImportAsSettings Inamedparam INamedParamSettings InterfaceBloat InterfaceBloatSettings @@ -656,6 +657,11 @@ type GrouperSettings struct { VarRequireGrouping bool `mapstructure:"var-require-grouping"` } +type IfaceSettings struct { + Enable []string `mapstructure:"enable"` + Settings map[string]map[string]any `mapstructure:"settings"` +} + type ImportAsSettings struct { Alias []ImportAsAlias NoUnaliased bool `mapstructure:"no-unaliased"` diff --git a/pkg/golinters/iface/iface.go b/pkg/golinters/iface/iface.go new file mode 100644 index 000000000000..31f88160eafd --- /dev/null +++ b/pkg/golinters/iface/iface.go @@ -0,0 +1,57 @@ +package iface + +import ( + "slices" + + "github.com/uudashr/iface/identical" + "github.com/uudashr/iface/opaque" + "github.com/uudashr/iface/unused" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/goanalysis" +) + +func New(settings *config.IfaceSettings) *goanalysis.Linter { + var conf map[string]map[string]any + if settings != nil { + conf = settings.Settings + } + + return goanalysis.NewLinter( + "iface", + "Detect the incorrect use of interfaces, helping developers avoid interface pollution.", + analyzersFromSettings(settings), + conf, + ).WithLoadMode(goanalysis.LoadModeTypesInfo) +} + +func analyzersFromSettings(settings *config.IfaceSettings) []*analysis.Analyzer { + allAnalyzers := map[string]*analysis.Analyzer{ + "identical": identical.Analyzer, + "unused": unused.Analyzer, + "opaque": opaque.Analyzer, + } + + if settings == nil || len(settings.Enable) == 0 { + // Default enable `identical` analyzer only + return []*analysis.Analyzer{identical.Analyzer} + } + + var analyzers []*analysis.Analyzer + for _, name := range uniqueNames(settings.Enable) { + if _, ok := allAnalyzers[name]; !ok { + // skip unknown analyzer + continue + } + + analyzers = append(analyzers, allAnalyzers[name]) + } + + return analyzers +} + +func uniqueNames(names []string) []string { + slices.Sort(names) + return slices.Compact(names) +} diff --git a/pkg/golinters/iface/iface_integration_test.go b/pkg/golinters/iface/iface_integration_test.go new file mode 100644 index 000000000000..01d587c8a285 --- /dev/null +++ b/pkg/golinters/iface/iface_integration_test.go @@ -0,0 +1,11 @@ +package iface + +import ( + "testing" + + "github.com/golangci/golangci-lint/test/testshared/integration" +) + +func TestFromTestdata(t *testing.T) { + integration.RunTestdata(t) +} diff --git a/pkg/golinters/iface/testdata/iface_all.go b/pkg/golinters/iface/testdata/iface_all.go new file mode 100644 index 000000000000..3a6f912a1d2a --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_all.go @@ -0,0 +1,68 @@ +//golangcitest:args -Eiface +//golangcitest:config_path testdata/iface_all.yml +package testdata + +import "fmt" + +// identical + +type Pinger interface { // want "identical: interface Pinger contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +type Healthcheck interface { // want "identical: interface Healthcheck contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +// opaque + +type Server interface { + Serve() error +} + +type server struct { + addr string +} + +func (s server) Serve() error { + return nil +} + +func NewServer(addr string) Server { // want "opaque: NewServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" + return &server{addr: addr} +} + +// unused + +type User struct { + ID string + Name string +} + +type UserRepository interface { // want "unused: interface UserRepository is declared but not used within the package" + UserOf(id string) (*User, error) +} + +type UserRepositorySQL struct { +} + +func (r *UserRepositorySQL) UserOf(id string) (*User, error) { + return nil, nil +} + +type Granter interface { + Grant(permission string) error +} + +func AllowAll(g Granter) error { + return g.Grant("all") +} + +type Allower interface { + Allow(permission string) error +} + +func Allow(x any) { + _ = x.(Allower) + fmt.Println("allow") +} diff --git a/pkg/golinters/iface/testdata/iface_all.yml b/pkg/golinters/iface/testdata/iface_all.yml new file mode 100644 index 000000000000..e00cb8970659 --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_all.yml @@ -0,0 +1,6 @@ +linters-settings: + iface: + enable: + - unused + - identical + - opaque diff --git a/pkg/golinters/iface/testdata/iface_default.go b/pkg/golinters/iface/testdata/iface_default.go new file mode 100644 index 000000000000..34117389738b --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_default.go @@ -0,0 +1,67 @@ +//golangcitest:args -Eiface +package testdata + +import "fmt" + +// identical + +type Pinger interface { // want "identical: interface Pinger contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +type Healthcheck interface { // want "identical: interface Healthcheck contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +// opaque + +type Server interface { + Serve() error +} + +type server struct { + addr string +} + +func (s server) Serve() error { + return nil +} + +func NewServer(addr string) Server { + return &server{addr: addr} +} + +// unused + +type User struct { + ID string + Name string +} + +type UserRepository interface { + UserOf(id string) (*User, error) +} + +type UserRepositorySQL struct { +} + +func (r *UserRepositorySQL) UserOf(id string) (*User, error) { + return nil, nil +} + +type Granter interface { + Grant(permission string) error +} + +func AllowAll(g Granter) error { + return g.Grant("all") +} + +type Allower interface { + Allow(permission string) error +} + +func Allow(x any) { + _ = x.(Allower) + fmt.Println("allow") +} diff --git a/pkg/golinters/iface/testdata/iface_identical.go b/pkg/golinters/iface/testdata/iface_identical.go new file mode 100644 index 000000000000..821aff7bd41e --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_identical.go @@ -0,0 +1,68 @@ +//golangcitest:args -Eiface +//golangcitest:config_path testdata/iface_identical.yml +package testdata + +import "fmt" + +// identical + +type Pinger interface { // want "identical: interface Pinger contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +type Healthcheck interface { // want "identical: interface Healthcheck contains identical methods or type constraints from another interface, causing redundancy" + Ping() error +} + +// opaque + +type Server interface { + Serve() error +} + +type server struct { + addr string +} + +func (s server) Serve() error { + return nil +} + +func NewServer(addr string) Server { + return &server{addr: addr} +} + +// unused + +type User struct { + ID string + Name string +} + +type UserRepository interface { + UserOf(id string) (*User, error) +} + +type UserRepositorySQL struct { +} + +func (r *UserRepositorySQL) UserOf(id string) (*User, error) { + return nil, nil +} + +type Granter interface { + Grant(permission string) error +} + +func AllowAll(g Granter) error { + return g.Grant("all") +} + +type Allower interface { + Allow(permission string) error +} + +func Allow(x any) { + _ = x.(Allower) + fmt.Println("allow") +} diff --git a/pkg/golinters/iface/testdata/iface_identical.yml b/pkg/golinters/iface/testdata/iface_identical.yml new file mode 100644 index 000000000000..c6117380d661 --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_identical.yml @@ -0,0 +1,4 @@ +linters-settings: + iface: + enable: + - identical diff --git a/pkg/golinters/iface/testdata/iface_opaque.go b/pkg/golinters/iface/testdata/iface_opaque.go new file mode 100644 index 000000000000..b7ebeddc600f --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_opaque.go @@ -0,0 +1,68 @@ +//golangcitest:args -Eiface +//golangcitest:config_path testdata/iface_opaque.yml +package testdata + +import "fmt" + +// identical + +type Pinger interface { + Ping() error +} + +type Healthcheck interface { + Ping() error +} + +// opaque + +type Server interface { + Serve() error +} + +type server struct { + addr string +} + +func (s server) Serve() error { + return nil +} + +func NewServer(addr string) Server { // want "opaque: NewServer function return Server interface at the 1st result, abstract a single concrete implementation of \\*server" + return &server{addr: addr} +} + +// unused + +type User struct { + ID string + Name string +} + +type UserRepository interface { + UserOf(id string) (*User, error) +} + +type UserRepositorySQL struct { +} + +func (r *UserRepositorySQL) UserOf(id string) (*User, error) { + return nil, nil +} + +type Granter interface { + Grant(permission string) error +} + +func AllowAll(g Granter) error { + return g.Grant("all") +} + +type Allower interface { + Allow(permission string) error +} + +func Allow(x any) { + _ = x.(Allower) + fmt.Println("allow") +} diff --git a/pkg/golinters/iface/testdata/iface_opaque.yml b/pkg/golinters/iface/testdata/iface_opaque.yml new file mode 100644 index 000000000000..71bd7047cab7 --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_opaque.yml @@ -0,0 +1,4 @@ +linters-settings: + iface: + enable: + - opaque diff --git a/pkg/golinters/iface/testdata/iface_unused.go b/pkg/golinters/iface/testdata/iface_unused.go new file mode 100644 index 000000000000..b75fdc8964c6 --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_unused.go @@ -0,0 +1,68 @@ +//golangcitest:args -Eiface +//golangcitest:config_path testdata/iface_unused.yml +package testdata + +import "fmt" + +// identical + +type Pinger interface { // want "unused: interface Pinger is declared but not used within the package" + Ping() error +} + +type Healthcheck interface { // want "unused: interface Healthcheck is declared but not used within the package" + Ping() error +} + +// opaque + +type Server interface { + Serve() error +} + +type server struct { + addr string +} + +func (s server) Serve() error { + return nil +} + +func NewServer(addr string) Server { + return &server{addr: addr} +} + +// unused + +type User struct { + ID string + Name string +} + +type UserRepository interface { // want "unused: interface UserRepository is declared but not used within the package" + UserOf(id string) (*User, error) +} + +type UserRepositorySQL struct { +} + +func (r *UserRepositorySQL) UserOf(id string) (*User, error) { + return nil, nil +} + +type Granter interface { + Grant(permission string) error +} + +func AllowAll(g Granter) error { + return g.Grant("all") +} + +type Allower interface { + Allow(permission string) error +} + +func Allow(x any) { + _ = x.(Allower) + fmt.Println("allow") +} diff --git a/pkg/golinters/iface/testdata/iface_unused.yml b/pkg/golinters/iface/testdata/iface_unused.yml new file mode 100644 index 000000000000..a0511b3dd3bd --- /dev/null +++ b/pkg/golinters/iface/testdata/iface_unused.yml @@ -0,0 +1,4 @@ +linters-settings: + iface: + enable: + - unused diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index cb67d291ccfc..3db2ca393cfc 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -55,6 +55,7 @@ import ( "github.com/golangci/golangci-lint/pkg/golinters/gosmopolitan" "github.com/golangci/golangci-lint/pkg/golinters/govet" "github.com/golangci/golangci-lint/pkg/golinters/grouper" + "github.com/golangci/golangci-lint/pkg/golinters/iface" "github.com/golangci/golangci-lint/pkg/golinters/importas" "github.com/golangci/golangci-lint/pkg/golinters/inamedparam" "github.com/golangci/golangci-lint/pkg/golinters/ineffassign" @@ -478,6 +479,12 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithURL("https://github.com/esimonov/ifshort"). DeprecatedError("The repository of the linter has been deprecated by the owner.", "v1.48.0", ""), + linter.NewConfig(iface.New(&cfg.LintersSettings.Iface)). + WithSince("v1.62.0"). + WithLoadForGoAnalysis(). + WithPresets(linter.PresetStyle). + WithURL("https://github.com/uudashr/iface"), + linter.NewConfig(importas.New(&cfg.LintersSettings.ImportAs)). WithSince("v1.38.0"). WithPresets(linter.PresetStyle).