Skip to content

Commit

Permalink
stop align
Browse files Browse the repository at this point in the history
  • Loading branch information
4meepo committed Sep 18, 2024
1 parent e25313b commit 4592184
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 34 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ type StrictStyleExample struct {
}
```

> ⚠️Note: The strict style can't run without the align or sort feature enabled.
> ⚠️Note: The strict style must run with the align and sort feature both enabled.

## References

Expand Down
6 changes: 6 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ func WithStrictStyle() Option {
h.style = StrictStyle
}
}

func WithStopAlignThreshold(threshold int) Option {
return func(h *Helper) {
h.stopAlignThreshold = threshold
}
}
117 changes: 86 additions & 31 deletions tagalign.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ type Helper struct {

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.
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.
stopAlignThreshold int // specifies the maximum allowable length difference between struct tags in the same column before alignment stops.

singleFields []*ast.Field
consecutiveFieldsGroups [][]*ast.Field // fields in this group, must be consecutive in struct.
Expand Down Expand Up @@ -194,8 +195,15 @@ func (w *Helper) report(pass *analysis.Pass, field *ast.Field, startCol int, msg
}
}

func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
// process grouped fields
func (w *Helper) Process(pass *analysis.Pass) {
// process group fields
w.processGroup(pass)

// process single fields
w.processSingle(pass)
}

func (w *Helper) processGroup(pass *analysis.Pass) {
for _, fields := range w.consecutiveFieldsGroups {
offsets := make([]int, len(fields))

Expand All @@ -217,15 +225,15 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
column := pass.Fset.Position(field.Tag.Pos()).Column - 1
offsets[i] = column

tag, err := strconv.Unquote(field.Tag.Value)
tagVal, err := strconv.Unquote(field.Tag.Value)
if err != nil {
// if tag value is not a valid string, report it directly
w.report(pass, field, column, errTagValueSyntax, field.Tag.Value)
fields = removeField(fields, i)
continue
}

tags, err := structtag.Parse(tag)
tags, err := structtag.Parse(tagVal)
if err != nil {
// if tag value is not a valid struct tag, report it directly
w.report(pass, field, column, err.Error(), field.Tag.Value)
Expand All @@ -236,11 +244,14 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
maxTagNum = max(maxTagNum, tags.Len())

if w.sort {
// store the not sorted(original) tags for later comparison.
cp := make([]*structtag.Tag, tags.Len())
for i, tag := range tags.Tags() {
cp[i] = tag
}

notSortedTagsGroup = append(notSortedTagsGroup, cp)
// sort.
sortBy(w.fixedTagOrder, tags)
}
for _, t := range tags.Tags() {
Expand All @@ -251,8 +262,10 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
i++
}

if w.sort && StrictStyle == w.style {
if w.sort {
sortAllKeys(w.fixedTagOrder, uniqueKeys)
}
if w.style == StrictStyle {
maxTagNum = len(uniqueKeys)
}

Expand All @@ -271,7 +284,7 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
// search by key
for _, tag := range tagsGroup[i] {
if tag.Key == key {
maxLength = max(maxLength, len(tag.String()))
maxLength = max(maxLength, len([]rune(tag.String())))
break
}
}
Expand All @@ -280,39 +293,82 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
// in case of index out of range
continue
}
maxLength = max(maxLength, len(tagsGroup[i][j].String()))
maxLength = max(maxLength, len([]rune(tagsGroup[i][j].String())))
}
}
tagMaxLens[j] = tagLen{key, maxLength}
}

stopAlignIndex := -1

for i, field := range fields {
tags := tagsGroup[i]

var newTagStr string
if w.align {
// if align enabled, align tags.
newTagBuilder := strings.Builder{}
for i, n := 0, 0; i < len(tags) && n < len(tagMaxLens); {
tag := tags[i]
var format string
for m, n := 0, 0; m < len(tags) && n < len(tagMaxLens); {
tag := tags[m]
if w.style == StrictStyle {
if stopAlignIndex != -1 && m >= stopAlignIndex {
// stop align
format := alignFormat(len([]rune(tag.String())) + 1)
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))

m++
continue
}

if tagMaxLens[n].Key == tag.Key {
// match
format = alignFormat(tagMaxLens[n].Len + 1) // with an extra space
x := tagMaxLens[n].Len - len([]rune(tag.String())) // x means the len diff.
if w.stopAlignThreshold > 0 && x > w.stopAlignThreshold {
stopAlignIndex = m

format := alignFormat(len([]rune(tag.String())) + 1)
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))

m++
continue
}

format := alignFormat(tagMaxLens[n].Len + 1) // with an extra space
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))
i++

m++
n++
} else {
// tag missing
format = alignFormat(tagMaxLens[n].Len + 1)
newTagBuilder.WriteString(fmt.Sprintf(format, ""))
// tag absent
x := tagMaxLens[n].Len
if w.stopAlignThreshold > 0 && x > w.stopAlignThreshold {
stopAlignIndex = m

m++
continue
}

format := alignFormat(tagMaxLens[n].Len + 1)
newTagBuilder.WriteString(fmt.Sprintf(format, "")) // fill empty tag with space

n++
}
} else {
format = alignFormat(tagMaxLens[n].Len + 1) // with an extra space
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))
i++
x := tagMaxLens[n].Len - len([]rune(tag.String()))
if w.stopAlignThreshold > 0 && x > w.stopAlignThreshold {
stopAlignIndex = m
}

if stopAlignIndex != -1 && m >= stopAlignIndex {
// stop align.
format := alignFormat(len([]rune(tag.String())) + 1)
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))
} else {
format := alignFormat(tagMaxLens[n].Len + 1) // with an extra space
newTagBuilder.WriteString(fmt.Sprintf(format, tag.String()))
}

m++
n++
}
}
Expand All @@ -330,30 +386,29 @@ func (w *Helper) Process(pass *analysis.Pass) { //nolint:gocognit
newTagStr = strings.Join(tagsStr, " ")
}

// report
//
unquoteTag := strings.TrimRight(newTagStr, " ")
// unquoteTag := newTagStr
newTagValue := fmt.Sprintf("`%s`", unquoteTag)
if field.Tag.Value == newTagValue {
quoteTag := fmt.Sprintf("`%s`", unquoteTag)
if field.Tag.Value == quoteTag {
// nothing changed
continue
}

msg := "tag is not aligned, should be: " + unquoteTag

w.report(pass, field, offsets[i], msg, newTagValue)
w.report(pass, field, offsets[i], "tag is not aligned, should be: "+unquoteTag, quoteTag)
}
}
}

// process single fields
func (w *Helper) processSingle(pass *analysis.Pass) {
for _, field := range w.singleFields {
column := pass.Fset.Position(field.Tag.Pos()).Column - 1
tag, err := strconv.Unquote(field.Tag.Value)
tagVal, err := strconv.Unquote(field.Tag.Value)
if err != nil {
w.report(pass, field, column, errTagValueSyntax, field.Tag.Value)
continue
}

tags, err := structtag.Parse(tag)
tags, err := structtag.Parse(tagVal)
if err != nil {
w.report(pass, field, column, err.Error(), field.Tag.Value)
continue
Expand Down
8 changes: 8 additions & 0 deletions tagalign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,11 @@ func Test_badSyntaxTag(t *testing.T) {
assert.NoError(t, err)
analysistest.Run(t, unsort, a)
}

func Test_stopAlignThreshold(t *testing.T) {
a := NewAnalyzer(WithStopAlignThreshold(10))
orig, err := filepath.Abs("testdata/stop_align_threshold")
assert.NoError(t, err)

analysistest.Run(t, orig, a)
}
4 changes: 2 additions & 2 deletions testdata/align/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ type FooBar struct {
FooFoo int8 `json:"foo_foo" validate:"required" yaml:"fooFoo"` // want `tag is not aligned, should be: json:"foo_foo"`
BarBar int `json:"bar_bar" validate:"required"` // want `tag is not aligned, should be: json:"bar_bar"`
FooBar struct {
Foo int `json:"foo" yaml:"foo" validate:"required"` // want `tag is not aligned, should be: json:"foo" yaml:"foo" validate:"required"`
Bar222 string `json:"bar222" validate:"required" yaml:"bar"` // want `tag is not aligned, should be: json:"bar222" validate:"required" yaml:"bar"`
Foo int `json:"foo" yaml:"foo" validate:"required"` // want `tag is not aligned, should be: json:"foo" yaml:"foo" validate:"required"`
Bar222 string `json:"bar222啊" validate:"required" yaml:"bar"` // want `tag is not aligned, should be: json:"bar222啊" validate:"required" yaml:"bar"`
} `json:"foo_bar" validate:"required"`
FooFooFoo struct {
BarBarBar struct {
Expand Down
6 changes: 6 additions & 0 deletions testdata/stop_align_threshold/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package stopalignthreshold

type FooBar struct {
Foo string `json:"foo" yaml:"foo" required:"true" description:"this is a str" xml:"foo"` // want `json:"foo" yaml:"foo" required:"true" description:"this is a str" xml:"foo"`
FooBar string `json:"fooBar" yaml:"fooBar" description:"this is a long long str str str" xml:"fooBar"`
}

0 comments on commit 4592184

Please sign in to comment.