Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
codeberg.org/chavacava/garif v0.2.0
github.com/BurntSushi/toml v1.6.0
github.com/fatih/color v1.18.0
github.com/fatih/structtag v1.2.0
github.com/hashicorp/go-version v1.8.0
github.com/mgechev/dots v1.0.0
github.com/spf13/afero v1.15.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
Expand Down
119 changes: 119 additions & 0 deletions internal/structtag/structtag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package structtag provides utilities to parse and manipulate struct tags.
package structtag

import (
"errors"
"strconv"
"strings"
)

var errValueSyntax = errors.New("invalid syntax for struct tag value")

// Tag defines a single struct's string literal tag.
type Tag struct {
// Key is the tag key, such as json, xml, etc.
// i.e., `json:"foo,omitempty"`. Here Key is "json".
Key string

// Name is the tag name.
// i.e., `json:"foo,omitempty"`. Here Name is "foo".
Name string

// Options are the tag options.
// i.e., `json:"foo,omitempty"`. Here Options is ["omitempty"].
Options []string
}

// Parse parses a single struct field tag and returns the set of tags.
func Parse(tag string) ([]*Tag, error) {
if tag == "" {
return nil, nil
}

var tags []*Tag

for tag != "" {
// Skip leading ASCII whitespace to match scanKey's delimiter rules.
for tag != "" && tag[0] <= ' ' {
tag = tag[1:]
}
if tag == "" {
break
}

i := scanKey(tag)
if i == 0 {
return nil, errors.New("invalid syntax for struct tag key")
}
if i+1 >= len(tag) || tag[i] != ':' {
return nil, errors.New("invalid syntax for struct tag pair")
}
if tag[i+1] != '"' {
return nil, errValueSyntax
}

key := tag[:i]
tag = tag[i+1:]

i, qvalue, err := scanValue(tag)
if err != nil {
return nil, err
}

value, err := strconv.Unquote(qvalue)
if err != nil {
return nil, errValueSyntax
}

name, options := parseTagValue(value)
tags = append(tags, &Tag{
Key: key,
Name: name,
Options: options,
})

tag = tag[i:]
}

return tags, nil
}

// scanKey finds the position of the colon that ends the tag key.
// It returns the index of the first invalid character (space, colon, quote, or control character),
// or len(tag) if all characters are valid.
func scanKey(tag string) int {
for i := 0; i < len(tag); i++ {
if tag[i] <= ' ' || tag[i] == ':' || tag[i] == '"' || tag[i] == 0x7f {
return i
}
}
return len(tag)
}

// scanValue scans a quoted string value and returns its index and quoted content.
// The tag string must start with a double-quote character.
func scanValue(tag string) (idx int, qvalue string, err error) {
// Find closing quote, handling escapes.
i := 1
for i < len(tag) && tag[i] != '"' {
if tag[i] == '\\' {
i++
}
i++
}
if i >= len(tag) {
return 0, "", errValueSyntax
}
return i + 1, tag[:i+1], nil
}

// parseTagValue parses an unquoted tag value into name and options.
// The format is "name" or "name,opt1,opt2,...".
func parseTagValue(value string) (name string, options []string) {
parts := strings.Split(value, ",")
name = parts[0]
if len(parts) > 1 {
options = parts[1:]
}
return name, options
}
228 changes: 228 additions & 0 deletions internal/structtag/structtag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package structtag_test

import (
"errors"
"reflect"
"testing"

"github.com/mgechev/revive/internal/structtag"
)

func TestParse(t *testing.T) {
test := []struct {
name string
tag string
want []*structtag.Tag
wantErr error
}{
{
name: "empty tag",
tag: "",
want: nil,
},
{
name: "tag with only whitespace",
tag: " ",
want: nil,
},
{
name: "tag with leading tabs and newlines",
tag: "\tjson:\"foo\"\n\txml:\"bar\"",
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
{
Key: "xml",
Name: "bar",
},
},
},
{
name: "tag with one key (valid)",
tag: `json:""`,
want: []*structtag.Tag{
{
Key: "json",
},
},
},
{
name: "tag with one key and dash name",
tag: `json:"-"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "-",
},
},
},
{
name: "tag with key and name",
tag: `json:"foo"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
},
},
{
name: "tag with key, name and option",
tag: `json:"foo,omitempty"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
Options: []string{"omitempty"},
},
},
},
{
name: "tag with multiple keys",
tag: `json:"" hcl:""`,
want: []*structtag.Tag{
{
Key: "json",
},
{
Key: "hcl",
},
},
},
{
name: "tag with multiple keys and names",
tag: `json:"foo" hcl:"foo"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
{
Key: "hcl",
Name: "foo",
},
},
},
{
name: "tag with multiple keys and different names",
tag: `json:"foo" hcl:"bar"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
{
Key: "hcl",
Name: "bar",
},
},
},
{
name: "tag with multiple keys, names, and options",
tag: `json:"foo,omitempty" structs:"bar,omitnested"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
Options: []string{"omitempty"},
},
{
Key: "structs",
Name: "bar",
Options: []string{"omitnested"},
},
},
},
{
name: "tag with multiple keys, names, options, and dash",
tag: `json:"foo" structs:"bar,omitnested" hcl:"-"`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
{
Key: "structs",
Name: "bar",
Options: []string{"omitnested"},
},
{
Key: "hcl",
Name: "-",
},
},
},
{
name: "tag with quoted name",
tag: `json:"foo,bar:\"baz\""`,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
Options: []string{`bar:"baz"`},
},
},
},
{
name: "tag with trailing space",
tag: `json:"foo" `,
want: []*structtag.Tag{
{
Key: "json",
Name: "foo",
},
},
},
{
name: "tag with one key (invalid)",
tag: "json",
wantErr: errors.New("invalid syntax for struct tag pair"),
},
{
name: "tag starting with colon (invalid key)",
tag: ":",
wantErr: errors.New("invalid syntax for struct tag key"),
},
{
name: "tag with colon not followed by quote (invalid value)",
tag: "json:foo",
wantErr: errors.New("invalid syntax for struct tag value"),
},
{
name: "tag with unclosed quote (invalid value)",
tag: `json:"foo`,
wantErr: errors.New("invalid syntax for struct tag value"),
},
{
name: "tag with invalid escape sequence (invalid value)",
tag: `json:"\x"`,
wantErr: errors.New("invalid syntax for struct tag value"),
},
}

for _, ts := range test {
t.Run(ts.name, func(t *testing.T) {
tags, err := structtag.Parse(ts.tag)

if ts.wantErr != nil {
if err == nil || err.Error() != ts.wantErr.Error() {
t.Errorf("unexpected error: got = %v, want = %v", err, ts.wantErr)
}
return
}

if ts.want == nil {
if tags != nil {
t.Errorf("expected tags to be nil, but got %#v", tags)
}
return
}

if !reflect.DeepEqual(ts.want, tags) {
t.Errorf("got = %#v, want = %#v", tags, ts.want)
}
})
}
}
5 changes: 2 additions & 3 deletions rule/struct_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import (
"strconv"
"strings"

"github.com/fatih/structtag"

"github.com/mgechev/revive/internal/astutils"
"github.com/mgechev/revive/internal/structtag"
"github.com/mgechev/revive/lint"
)

Expand Down Expand Up @@ -198,7 +197,7 @@ func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, field *ast.F
}

analyzedTags := map[tagKey]struct{}{}
for _, tag := range tags.Tags() {
for _, tag := range tags {
_, mustOmit := w.omittedTags[tagKey(tag.Key)]
if mustOmit {
continue
Expand Down