diff --git a/.golangci.reference.yml b/.golangci.reference.yml index 57e73ac7..575852c5 100644 --- a/.golangci.reference.yml +++ b/.golangci.reference.yml @@ -1739,6 +1739,39 @@ linters-settings: # Default: ["200", "400", "404", "500"] http-status-code-whitelist: [ "200", "400", "404", "500" ] + tagalign: + # Align and sort can be used together or separately. + # + # Whether enable align. If true, the struct tags will be aligned. + # eg: + # type FooBar struct { + # Bar string `json:"bar" validate:"required"` + # FooFoo int8 `json:"foo_foo" validate:"required"` + # } + # will be formatted to: + # type FooBar struct { + # Bar string `json:"bar" validate:"required"` + # FooFoo int8 `json:"foo_foo" validate:"required"` + # } + # Default: true. + align: false + # Whether enable tags sort. + # If true, the tags will be sorted by name in ascending order. + # eg: `xml:"bar" json:"bar" validate:"required"` -> `json:"bar" validate:"required" xml:"bar"` + # Default: true + sort: false + # Specify the order of tags, the other tags will be sorted by name. + # This option will be ignored if `sort` is false. + # Default: [] + order: + - json + - yaml + - yml + - toml + - mapstructure + - binding + - validate + tagliatelle: # Check the struct tag name case. case: @@ -2118,6 +2151,7 @@ linters: - staticcheck - structcheck - stylecheck + - tagalign - tagliatelle - tenv - testableexamples @@ -2229,6 +2263,7 @@ linters: - staticcheck - structcheck - stylecheck + - tagalign - tagliatelle - tenv - testableexamples diff --git a/go.mod b/go.mod index 301b29e0..80b0abd1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( 4d63.com/gocheckcompilerdirectives v1.2.1 4d63.com/gochecknoglobals v0.2.1 + github.com/4meepo/tagalign v1.2.2 github.com/Abirdcfly/dupword v0.0.11 github.com/Antonboom/errname v0.1.9 github.com/Antonboom/nilnil v0.1.3 diff --git a/go.sum b/go.sum index 79b8e890..ca2ac603 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.2.2 h1:kQeUTkFTaBRtd/7jm8OKJl9iHk0gAO+TDFPHGSna0aw= +github.com/4meepo/tagalign v1.2.2/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU= github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA= github.com/Antonboom/errname v0.1.9 h1:BZDX4r3l4TBZxZ2o2LNrlGxSHran4d1u4veZdoORTT4= diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index e528e7f8..4ab94af0 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -110,6 +110,11 @@ var defaultLintersSettings = LintersSettings{ Ignore: "", Qualified: false, }, + TagAlign: TagAlignSettings{ + Align: true, + Sort: true, + Order: nil, + }, Testpackage: TestpackageSettings{ SkipRegexp: `(export|internal)_test\.go`, AllowPackages: []string{"main"}, @@ -203,6 +208,7 @@ type LintersSettings struct { Staticcheck StaticCheckSettings Structcheck StructCheckSettings Stylecheck StaticCheckSettings + TagAlign TagAlignSettings Tagliatelle TagliatelleSettings Tenv TenvSettings Testpackage TestpackageSettings @@ -655,6 +661,12 @@ type StructCheckSettings struct { CheckExportedFields bool `mapstructure:"exported-fields"` } +type TagAlignSettings struct { + Align bool `mapstructure:"align"` + Sort bool `mapstructure:"sort"` + Order []string `mapstructure:"order"` +} + type TagliatelleSettings struct { Case struct { Rules map[string]string diff --git a/pkg/golinters/tagalign.go b/pkg/golinters/tagalign.go new file mode 100644 index 00000000..07b75646 --- /dev/null +++ b/pkg/golinters/tagalign.go @@ -0,0 +1,70 @@ +package golinters + +import ( + "sync" + + "github.com/4meepo/tagalign" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" + "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/result" +) + +func NewTagAlign(settings *config.TagAlignSettings) *goanalysis.Linter { + var mu sync.Mutex + var resIssues []goanalysis.Issue + + options := []tagalign.Option{tagalign.WithMode(tagalign.GolangciLintMode)} + + if settings != nil { + options = append(options, tagalign.WithAlign(settings.Align)) + + if settings.Sort || len(settings.Order) > 0 { + options = append(options, tagalign.WithSort(settings.Order...)) + } + } + + analyzer := tagalign.NewAnalyzer(options...) + analyzer.Run = func(pass *analysis.Pass) (any, error) { + taIssues := tagalign.Run(pass, options...) + + issues := make([]goanalysis.Issue, len(taIssues)) + for i, issue := range taIssues { + report := &result.Issue{ + FromLinter: analyzer.Name, + Pos: issue.Pos, + Text: issue.Message, + Replacement: &result.Replacement{ + Inline: &result.InlineFix{ + StartCol: issue.InlineFix.StartCol, + Length: issue.InlineFix.Length, + NewString: issue.InlineFix.NewString, + }, + }, + } + + issues[i] = goanalysis.NewIssue(report, pass) + } + + if len(issues) == 0 { + return nil, nil + } + + mu.Lock() + resIssues = append(resIssues, issues...) + mu.Unlock() + + return nil, nil + } + + return goanalysis.NewLinter( + analyzer.Name, + analyzer.Doc, + []*analysis.Analyzer{analyzer}, + nil, + ).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { + return resIssues + }).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index 39214a94..128e7909 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -165,6 +165,7 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { staticcheckCfg *config.StaticCheckSettings structcheckCfg *config.StructCheckSettings stylecheckCfg *config.StaticCheckSettings + tagalignCfg *config.TagAlignSettings tagliatelleCfg *config.TagliatelleSettings tenvCfg *config.TenvSettings testpackageCfg *config.TestpackageSettings @@ -244,6 +245,7 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { staticcheckCfg = &m.cfg.LintersSettings.Staticcheck structcheckCfg = &m.cfg.LintersSettings.Structcheck stylecheckCfg = &m.cfg.LintersSettings.Stylecheck + tagalignCfg = &m.cfg.LintersSettings.TagAlign tagliatelleCfg = &m.cfg.LintersSettings.Tagliatelle tenvCfg = &m.cfg.LintersSettings.Tenv testpackageCfg = &m.cfg.LintersSettings.Testpackage @@ -777,6 +779,12 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithPresets(linter.PresetStyle). WithURL("https://github.com/dominikh/go-tools/tree/master/stylecheck"), + linter.NewConfig(golinters.NewTagAlign(tagalignCfg)). + WithSince("v1.53.0"). + WithPresets(linter.PresetStyle, linter.PresetFormatting). + WithAutoFix(). + WithURL("https://github.com/4meepo/tagalign"), + linter.NewConfig(golinters.NewTagliatelle(tagliatelleCfg)). WithSince("v1.40.0"). WithPresets(linter.PresetStyle). diff --git a/test/testdata/configs/tagalign_align_only.yml b/test/testdata/configs/tagalign_align_only.yml new file mode 100644 index 00000000..0895a58f --- /dev/null +++ b/test/testdata/configs/tagalign_align_only.yml @@ -0,0 +1,3 @@ +linters-settings: + tagalign: + sort: false diff --git a/test/testdata/configs/tagalign_order_only.yml b/test/testdata/configs/tagalign_order_only.yml new file mode 100644 index 00000000..4ea9664e --- /dev/null +++ b/test/testdata/configs/tagalign_order_only.yml @@ -0,0 +1,7 @@ +linters-settings: + tagalign: + align: false + order: + - "xml" + - "json" + - "yaml" diff --git a/test/testdata/configs/tagalign_sort_only.yml b/test/testdata/configs/tagalign_sort_only.yml new file mode 100644 index 00000000..19e64443 --- /dev/null +++ b/test/testdata/configs/tagalign_sort_only.yml @@ -0,0 +1,4 @@ +linters-settings: + tagalign: + align: false + sort: true diff --git a/test/testdata/tagalign.go b/test/testdata/tagalign.go new file mode 100644 index 00000000..428b237f --- /dev/null +++ b/test/testdata/tagalign.go @@ -0,0 +1,15 @@ +//golangcitest:args -Etagalign +package testdata + +import "time" + +type TagAlignExampleAlignSort struct { + Foo time.Duration `json:"foo,omitempty" yaml:"foo" xml:"foo" binding:"required" gorm:"column:foo" zip:"foo" validate:"required"` // want `binding:"required" gorm:"column:foo" json:"foo,omitempty" validate:"required" xml:"foo" yaml:"foo" zip:"foo"` + Bar int `validate:"required" yaml:"bar" xml:"bar" binding:"required" json:"bar,omitempty" gorm:"column:bar" zip:"bar" ` // want `binding:"required" gorm:"column:bar" json:"bar,omitempty" validate:"required" xml:"bar" yaml:"bar" zip:"bar"` + FooBar int `gorm:"column:fooBar" validate:"required" xml:"fooBar" binding:"required" json:"fooBar,omitempty" zip:"fooBar" yaml:"fooBar"` // want `binding:"required" gorm:"column:fooBar" json:"fooBar,omitempty" validate:"required" xml:"fooBar" yaml:"fooBar" zip:"fooBar"` +} + +type TagAlignExampleAlignSort2 struct { + Foo int ` xml:"foo" json:"foo,omitempty" yaml:"foo" zip:"foo" binding:"required" gorm:"column:foo" validate:"required"` // want `binding:"required" gorm:"column:foo" json:"foo,omitempty" validate:"required" xml:"foo" yaml:"foo" zip:"foo"` + Bar int `validate:"required" gorm:"column:bar" yaml:"bar" xml:"bar" binding:"required" json:"bar" zip:"bar" ` // want `binding:"required" gorm:"column:bar" json:"bar" validate:"required" xml:"bar" yaml:"bar" zip:"bar"` +} diff --git a/test/testdata/tagalign_align_only.go b/test/testdata/tagalign_align_only.go new file mode 100644 index 00000000..a71f958e --- /dev/null +++ b/test/testdata/tagalign_align_only.go @@ -0,0 +1,31 @@ +//golangcitest:args -Etagalign +//golangcitest:config_path testdata/configs/tagalign_align_only.yml +package testdata + +import "time" + +type TagAlignExampleAlignOnlyKO struct { + Foo time.Time `gorm:"column:foo" json:"foo,omitempty" xml:"foo" yaml:"foo" zip:"foo"` // want `gorm:"column:foo" json:"foo,omitempty" xml:"foo" yaml:"foo" zip:"foo"` + FooBar struct{} `gorm:"column:fooBar" zip:"fooBar" json:"fooBar,omitempty" xml:"fooBar" yaml:"fooBar"` // want `gorm:"column:fooBar" zip:"fooBar" json:"fooBar,omitempty" xml:"fooBar" yaml:"fooBar"` + FooFoo struct { + Foo int `json:"foo" yaml:"foo"` // want `json:"foo" yaml:"foo"` + Bar int `yaml:"bar" json:"bar"` // want `yaml:"bar" json:"bar"` + BarBar string `json:"barBar" yaml:"barBar"` + } `xml:"fooFoo" json:"fooFoo"` + NoTag struct{} + BarBar struct{} `json:"barBar,omitempty" gorm:"column:barBar" yaml:"barBar" xml:"barBar" zip:"barBar"` + Boo struct{} `gorm:"column:boo" json:"boo,omitempty" xml:"boo" yaml:"boo" zip:"boo"` // want `gorm:"column:boo" json:"boo,omitempty" xml:"boo" yaml:"boo" zip:"boo"` +} + +type TagAlignExampleAlignOnlyOK struct { + Foo time.Time `gorm:"column:foo" json:"foo,omitempty" xml:"foo" yaml:"foo" zip:"foo"` + FooBar struct{} `gorm:"column:fooBar" zip:"fooBar" json:"fooBar,omitempty" xml:"fooBar" yaml:"fooBar"` + FooFoo struct { + Foo int `json:"foo" yaml:"foo"` + Bar int `yaml:"bar" json:"bar"` + BarBar string `json:"barBar" yaml:"barBar"` + } `xml:"fooFoo" json:"fooFoo"` + NoTag struct{} + BarBar struct{} `json:"barBar,omitempty" gorm:"column:barBar" yaml:"barBar" xml:"barBar" zip:"barBar"` + Boo struct{} `gorm:"column:boo" json:"boo,omitempty" xml:"boo" yaml:"boo" zip:"boo"` +} diff --git a/test/testdata/tagalign_order_only.go b/test/testdata/tagalign_order_only.go new file mode 100644 index 00000000..5837fc6d --- /dev/null +++ b/test/testdata/tagalign_order_only.go @@ -0,0 +1,15 @@ +//golangcitest:args -Etagalign +//golangcitest:config_path testdata/configs/tagalign_order_only.yml +package testdata + +import "time" + +type TagAlignExampleOrderOnlyKO struct { + Foo time.Time `xml:"foo" json:"foo,omitempty" yaml:"foo" zip:"foo" gorm:"column:foo" validate:"required"` // want `xml:"foo" json:"foo,omitempty" yaml:"foo" gorm:"column:foo" validate:"required" zip:"foo"` + FooBar struct{} `gorm:"column:fooBar" validate:"required" zip:"fooBar" xml:"fooBar" json:"fooBar,omitempty" yaml:"fooBar"` // want `xml:"fooBar" json:"fooBar,omitempty" yaml:"fooBar" gorm:"column:fooBar" validate:"required" zip:"fooBar"` +} + +type TagAlignExampleOrderOnlyOK struct { + Foo time.Time `xml:"foo" json:"foo,omitempty" yaml:"foo" gorm:"column:foo" validate:"required" zip:"foo"` + FooBar struct{} `xml:"fooBar" json:"fooBar,omitempty" yaml:"fooBar" gorm:"column:fooBar" validate:"required" zip:"fooBar"` +} diff --git a/test/testdata/tagalign_sort_only.go b/test/testdata/tagalign_sort_only.go new file mode 100644 index 00000000..600f4bbb --- /dev/null +++ b/test/testdata/tagalign_sort_only.go @@ -0,0 +1,15 @@ +//golangcitest:args -Etagalign +//golangcitest:config_path testdata/configs/tagalign_sort_only.yml +package testdata + +import "time" + +type TagAlignExampleSortOnlyKO struct { + Foo time.Time `xml:"foo" json:"foo,omitempty" yaml:"foo" gorm:"column:foo" validate:"required" zip:"foo"` // want `gorm:"column:foo" json:"foo,omitempty" validate:"required" xml:"foo" yaml:"foo" zip:"foo"` + FooBar struct{} `gorm:"column:fooBar" validate:"required" zip:"fooBar" xml:"fooBar" json:"fooBar,omitempty" yaml:"fooBar"` // want `gorm:"column:fooBar" json:"fooBar,omitempty" validate:"required" xml:"fooBar" yaml:"fooBar" zip:"fooBar"` +} + +type TagAlignExampleSortOnlyOK struct { + Foo time.Time `gorm:"column:foo" json:"foo,omitempty" validate:"required" xml:"foo" yaml:"foo" zip:"foo"` + FooBar struct{} `gorm:"column:fooBar" json:"fooBar,omitempty" validate:"required" xml:"fooBar" yaml:"fooBar" zip:"fooBar"` +}