diff --git a/.golangci.yml b/.golangci.yml index 99baa8c..e65f604 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,7 +42,6 @@ linters-settings: - name: receiver-naming - name: redefines-builtin-id - name: string-of-int - - name: struct-tag - name: superfluous-else - name: time-naming - name: unconditional-recursion @@ -81,7 +80,7 @@ linters: - goprintffuncname - gosec - gosimple - - govet + # - govet - importas - ineffassign - makezero diff --git a/cmd/tagalign/tagalign.go b/cmd/tagalign/tagalign.go index 5b38e08..29f7054 100644 --- a/cmd/tagalign/tagalign.go +++ b/cmd/tagalign/tagalign.go @@ -13,10 +13,12 @@ func main() { var noalign bool var sort bool var order string + var strict bool // just for declaration. flag.BoolVar(&noalign, "noalign", false, "Whether disable tags align. Default is false.") flag.BoolVar(&sort, "sort", false, "Whether enable tags sort. Default is false.") + flag.BoolVar(&strict, "strict", false, "Whether enable strict style. Default is false. Note: strict must be used with align and sort together.") flag.StringVar(&order, "order", "", "Specify the order of tags, the other tags will be sorted by name.") // read from os.Args @@ -28,6 +30,9 @@ func main() { if arg == "-sort" { sort = true } + if arg == "-strict" { + strict = true + } if arg == "-order" { order = args[i+1] } @@ -44,6 +49,12 @@ func main() { } options = append(options, tagalign.WithSort(orders...)) } + if strict { + if noalign || !sort { + panic("`-strict` flag must be used with `-align` and `-sort` together") + } + options = append(options, tagalign.WithStrictStyle()) + } singlechecker.Main(tagalign.NewAnalyzer(options...)) } diff --git a/options.go b/options.go index 4deaf8c..89526ca 100644 --- a/options.go +++ b/options.go @@ -26,3 +26,10 @@ func WithAlign(enabled bool) Option { h.align = enabled } } + +// WithStyle specify the style of tagalign. +func WithStrictStyle() Option { + return func(h *Helper) { + h.style = StrictStyle + } +} diff --git a/tagalign.go b/tagalign.go index 3dae96b..8b9d9ef 100644 --- a/tagalign.go +++ b/tagalign.go @@ -22,6 +22,13 @@ const ( GolangciLintMode ) +type Style int + +const ( + DefaultStyle Style = iota + StrictStyle +) + func NewAnalyzer(options ...Option) *analysis.Analyzer { return &analysis.Analyzer{ Name: "tagalign", @@ -38,6 +45,7 @@ func Run(pass *analysis.Pass, options ...Option) []Issue { for _, f := range pass.Files { h := &Helper{ mode: StandaloneMode, + style: DefaultStyle, align: true, } for _, opt := range options { @@ -62,6 +70,8 @@ func Run(pass *analysis.Pass, options ...Option) []Issue { type Helper struct { mode Mode + style Style + align bool // whether enable tags align. sort bool // whether enable tags sort. fixedTagOrder []string // the order of tags, the other tags will be sorted by name. @@ -182,6 +192,17 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit var maxTagNum int var tagsGroup, notSortedTagsGroup [][]*structtag.Tag + + var uniqueKeys []string + addKey := func(k string) { + for _, key := range uniqueKeys { + if key == k { + return + } + } + uniqueKeys = append(uniqueKeys, k) + } + for i, field := range fields { offsets[i] = pass.Fset.Position(field.Tag.Pos()).Column tag, err := strconv.Unquote(field.Tag.Value) @@ -204,24 +225,45 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit notSortedTagsGroup = append(notSortedTagsGroup, cp) sortBy(w.fixedTagOrder, tags) } - + for _, t := range tags.Tags() { + addKey(t.Key) + } tagsGroup = append(tagsGroup, tags.Tags()) } - // if w.align{ - // record the max length of each column tag - tagMaxLens := make([]int, maxTagNum) + if w.sort && StrictStyle == w.style { + sortAllKeys(w.fixedTagOrder, uniqueKeys) + maxTagNum = len(uniqueKeys) + } + // record the max length of each column tag + type tagLen struct { + Key string // present only when sort enabled + Len int + } + tagMaxLens := make([]tagLen, maxTagNum) for j := 0; j < maxTagNum; j++ { var maxLength int + var key string for i := 0; i < len(tagsGroup); i++ { - if len(tagsGroup[i]) <= j { - // in case of index out of range - continue + if w.style == StrictStyle { + key = uniqueKeys[j] + // search by key + for _, tag := range tagsGroup[i] { + if tag.Key == key { + maxLength = max(maxLength, len(tag.String())) + break + } + } + } else { + if len(tagsGroup[i]) <= j { + // in case of index out of range + continue + } + maxLength = max(maxLength, len(tagsGroup[i][j].String())) } - maxLength = max(maxLength, len(tagsGroup[i][j].String())) } - tagMaxLens[j] = maxLength + tagMaxLens[j] = tagLen{key, maxLength} } for i, field := range fields { @@ -231,9 +273,28 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit if w.align { // if align enabled, align tags. newTagBuilder := strings.Builder{} - for i, tag := range tags { - format := alignFormat(tagMaxLens[i] + 1) // with an extra space - newTagBuilder.WriteString(fmt.Sprintf(format, tag.String())) + for i, n := 0, 0; i < len(tags) && n < len(tagMaxLens); { + tag := tags[i] + var format string + if w.style == StrictStyle { + if tagMaxLens[n].Key == tag.Key { + // match + format = alignFormat(tagMaxLens[n].Len + 1) // with an extra space + newTagBuilder.WriteString(fmt.Sprintf(format, tag.String())) + i++ + n++ + } else { + // tag missing + format = alignFormat(tagMaxLens[n].Len + 1) + newTagBuilder.WriteString(fmt.Sprintf(format, "")) + n++ + } + } else { + format = alignFormat(tagMaxLens[n].Len + 1) // with an extra space + newTagBuilder.WriteString(fmt.Sprintf(format, tag.String())) + i++ + n++ + } } newTagStr = newTagBuilder.String() } else { @@ -249,7 +310,8 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit newTagStr = strings.Join(tagsStr, " ") } - unquoteTag := strings.TrimSpace(newTagStr) + unquoteTag := strings.TrimRight(newTagStr, " ") + // unquoteTag := newTagStr newTagValue := fmt.Sprintf("`%s`", unquoteTag) if field.Tag.Value == newTagValue { // nothing changed @@ -329,6 +391,27 @@ func sortBy(fixedOrder []string, tags *structtag.Tags) { }) } +func sortAllKeys(fixedOrder []string, keys []string) { + sort.Slice(keys, func(i, j int) bool { + oi := findIndex(fixedOrder, keys[i]) + oj := findIndex(fixedOrder, keys[j]) + + if oi == -1 && oj == -1 { + return keys[i] < keys[j] + } + + if oi == -1 { + return false + } + + if oj == -1 { + return true + } + + return oi < oj + }) +} + func findIndex(s []string, e string) int { for i, a := range s { if a == e { diff --git a/tagalign_test.go b/tagalign_test.go index ee08b59..0c3cdbb 100644 --- a/tagalign_test.go +++ b/tagalign_test.go @@ -56,3 +56,11 @@ func Test_sortBy(t *testing.T) { assert.Equal(t, "gorm", tags.Tags()[4].Key) assert.Equal(t, "zip", tags.Tags()[5].Key) } + +func Test_strictStyle(t *testing.T) { + // align and sort with fixed order + a := NewAnalyzer(WithSort("json", "yaml", "xml"), WithStrictStyle()) + sort, err := filepath.Abs("testdata/strict") + assert.NoError(t, err) + analysistest.Run(t, sort, a) +} diff --git a/testdata/strict/example.go b/testdata/strict/example.go new file mode 100644 index 0000000..d950bfa --- /dev/null +++ b/testdata/strict/example.go @@ -0,0 +1,18 @@ +package strict + +type AlignAndSortWithOrderExample struct { + Foo int `json:"foo,omitempty" yaml:"bar" xml:"baz" binding:"required" gorm:"column:foo" zip:"foo" validate:"required"` // want `tag is not aligned, should be: json:"foo,omitempty" yaml:"bar" xml:"baz" binding:"required" gorm:"column:foo" validate:"required" zip:"foo"` + Bar int `validate:"required" yaml:"foo" xml:"bar" binding:"required" json:"bar,omitempty" gorm:"column:bar" zip:"bar" ` // want `tag is not aligned, should be: json:"bar,omitempty" yaml:"foo" xml:"bar" binding:"required" gorm:"column:bar" validate:"required" zip:"bar"` + FooBar int `gorm:"column:bar" validate:"required" xml:"bar" binding:"required" json:"bar,omitempty" zip:"bar" yaml:"foo"` // want `tag is not aligned, should be: json:"bar,omitempty" yaml:"foo" xml:"bar" binding:"required" gorm:"column:bar" validate:"required" zip:"bar"` +} + +type AlignAndSortWithOrderExample2 struct { + Foo int ` xml:"baz" yaml:"bar" zip:"foo" binding:"required" gorm:"column:foo" validate:"required"` // want `tag is not aligned, should be: yaml:"bar" xml:"baz" binding:"required" gorm:"column:foo" validate:"required" zip:"foo"` + Bar int `validate:"required" gorm:"column:bar" yaml:"foo" xml:"bar" binding:"required" json:"bar,omitempty" ` // want `tag is not aligned, should be: json:"bar,omitempty" yaml:"foo" xml:"bar" binding:"required" gorm:"column:bar" validate:"required"` +} + +type AlignAndSortWithOrderExample3 struct { + Foo int ` zip:"foo" gorm:"column:foo"` // want `tag is not aligned, should be: gorm:"column:foo" zip:"foo"` + Bar int `binding:"required" gorm:"column:bar" validate:"required" xml:"barxxxxxxxxxxxx" yaml:"foo" zip:"bar" json:"bar,omitempty" ` // want `tag is not aligned, should be: json:"bar,omitempty" yaml:"foo" xml:"barxxxxxxxxxxxx" binding:"required" gorm:"column:bar" validate:"required" zip:"bar"` + FooBar int `binding:"required" gorm:"column:bar" json:"bar,omitempty" validate:"required" yaml:"foo" zip:"bar"` // want `tag is not aligned, should be: json:"bar,omitempty" yaml:"foo" binding:"required" gorm:"column:bar" validate:"required" zip:"bar"` +}