diff --git a/go.mod b/go.mod index 078c66fb1..fb45e4b13 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9073d4dc7..74fcd2754 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/structtag/structtag.go b/internal/structtag/structtag.go new file mode 100644 index 000000000..6c41fa674 --- /dev/null +++ b/internal/structtag/structtag.go @@ -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 +} diff --git a/internal/structtag/structtag_test.go b/internal/structtag/structtag_test.go new file mode 100644 index 000000000..ec4db4636 --- /dev/null +++ b/internal/structtag/structtag_test.go @@ -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) + } + }) + } +} diff --git a/rule/struct_tag.go b/rule/struct_tag.go index c245f51fc..ecbb560e1 100644 --- a/rule/struct_tag.go +++ b/rule/struct_tag.go @@ -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" ) @@ -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