Repository: mgechev/revive
Pull request #1309 (2025-04-10 01:31:50)

chore: remove edited trigger
The `edited` event is related to the edition of the title or the description of a PR.
I think you don't need to re-launch the CI after an edition of a title.


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 6630591..b22ce30 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,7 +2,7 @@ name: Build
 on:
   workflow_dispatch:
   pull_request:
-    types: [opened, edited, synchronize, reopened]
+    types: [opened, synchronize, reopened]
 
 jobs:
   build:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index e7b5b8f..eb1b274 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -2,7 +2,7 @@ name: Lint
 on:
   workflow_dispatch:
   pull_request:
-    types: [opened, edited, synchronize, reopened]
+    types: [opened, synchronize, reopened]
 
 jobs:
   go-mod:
diff --git a/lint/package.go b/lint/package.go
index 6d8fed7..f0c249e 100644
--- a/lint/package.go
+++ b/lint/package.go
@@ -47,6 +47,9 @@ var (
 
 // Files return package's files.
 func (p *Package) Files() map[string]*File {
+	p.RLock()
+	defer p.RUnlock()
+
 	return p.files
 }
 
@@ -74,6 +77,7 @@ func (p *Package) IsMain() bool {
 func (p *Package) TypesPkg() *types.Package {
 	p.RLock()
 	defer p.RUnlock()
+
 	return p.typesPkg
 }
 
@@ -81,6 +85,7 @@ func (p *Package) TypesPkg() *types.Package {
 func (p *Package) TypesInfo() *types.Info {
 	p.RLock()
 	defer p.RUnlock()
+
 	return p.typesInfo
 }
 
@@ -88,6 +93,7 @@ func (p *Package) TypesInfo() *types.Info {
 func (p *Package) Sortable() map[string]bool {
 	p.RLock()
 	defer p.RUnlock()
+
 	return p.sortable
 }
 
@@ -150,9 +156,13 @@ func check(config *types.Config, n string, fset *token.FileSet, astFiles []*ast.
 
 // TypeOf returns the type of expression.
 func (p *Package) TypeOf(expr ast.Expr) types.Type {
+	p.RLock()
+	defer p.RUnlock()
+
 	if p.typesInfo == nil {
 		return nil
 	}
+
 	return p.typesInfo.TypeOf(expr)
 }
 
@@ -166,6 +176,9 @@ const (
 )
 
 func (p *Package) scanSortable() {
+	p.Lock()
+	defer p.Unlock()
+
 	sortableFlags := map[string]sortableMethodsFlags{}
 	for _, f := range p.files {
 		for _, decl := range f.AST.Decls {
@@ -191,7 +204,7 @@ func (p *Package) scanSortable() {
 func (p *Package) lint(rules []Rule, config Config, failures chan Failure) error {
 	p.scanSortable()
 	var eg errgroup.Group
-	for _, file := range p.files {
+	for _, file := range p.Files() {
 		eg.Go(func() error {
 			return file.lint(rules, config, failures)
 		})
@@ -202,6 +215,9 @@ func (p *Package) lint(rules []Rule, config Config, failures chan Failure) error
 
 // IsAtLeastGoVersion returns true if the Go version for this package is v or higher, false otherwise
 func (p *Package) IsAtLeastGoVersion(v *goversion.Version) bool {
+	p.RLock()
+	defer p.RUnlock()
+
 	return p.goVersion.GreaterThanOrEqual(v)
 }
 
diff --git a/rule/struct_tag.go b/rule/struct_tag.go
index e6a69e6..5cfa31c 100644
--- a/rule/struct_tag.go
+++ b/rule/struct_tag.go
@@ -13,7 +13,62 @@ import (
 
 // StructTagRule lints struct tags.
 type StructTagRule struct {
-	userDefined map[string][]string // map: key -> []option
+	userDefined map[tagKey][]string // map: key -> []option
+}
+
+type tagKey string
+
+const (
+	keyASN1         tagKey = "asn1"
+	keyBSON         tagKey = "bson"
+	keyDatastore    tagKey = "datastore"
+	keyDefault      tagKey = "default"
+	keyJSON         tagKey = "json"
+	keyMapstructure tagKey = "mapstructure"
+	keyProperties   tagKey = "properties"
+	keyProtobuf     tagKey = "protobuf"
+	keyRequired     tagKey = "required"
+	keyTOML         tagKey = "toml"
+	keyURL          tagKey = "url"
+	keyValidate     tagKey = "validate"
+	keyXML          tagKey = "xml"
+	keyYAML         tagKey = "yaml"
+)
+
+type tagChecker func(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool)
+
+// populate tag checkers map
+var tagCheckers = map[tagKey]tagChecker{
+	keyASN1:         checkASN1Tag,
+	keyBSON:         checkBSONTag,
+	keyDatastore:    checkDatastoreTag,
+	keyDefault:      checkDefaultTag,
+	keyJSON:         checkJSONTag,
+	keyMapstructure: checkMapstructureTag,
+	keyProperties:   checkPropertiesTag,
+	keyProtobuf:     checkProtobufTag,
+	keyRequired:     checkRequiredTag,
+	keyTOML:         checkTOMLTag,
+	keyURL:          checkURLTag,
+	keyValidate:     checkValidateTag,
+	keyXML:          checkXMLTag,
+	keyYAML:         checkYAMLTag,
+}
+
+type checkContext struct {
+	userDefined    map[tagKey][]string // map: key -> []option
+	usedTagNbr     map[int]bool        // list of used tag numbers
+	usedTagName    map[string]bool     // list of used tag keys
+	isAtLeastGo124 bool
+}
+
+func (checkCtx checkContext) isUserDefined(key tagKey, opt string) bool {
+	if checkCtx.userDefined == nil {
+		return false
+	}
+
+	options := checkCtx.userDefined[key]
+	return slices.Contains(options, opt)
 }
 
 // Configure validates the rule configuration, and configures the rule accordingly.
@@ -28,7 +83,8 @@ func (r *StructTagRule) Configure(arguments lint.Arguments) error {
 	if err != nil {
 		return err
 	}
-	r.userDefined = make(map[string][]string, len(arguments))
+
+	r.userDefined = make(map[tagKey][]string, len(arguments))
 	for _, arg := range arguments {
 		item, ok := arg.(string)
 		if !ok {
@@ -38,12 +94,13 @@ func (r *StructTagRule) Configure(arguments lint.Arguments) error {
 		if len(parts) < 2 {
 			return fmt.Errorf("invalid argument to the %s rule. Expecting a string of the form key[,option]+, got %s", r.Name(), item)
 		}
-		key := strings.TrimSpace(parts[0])
+		key := tagKey(strings.TrimSpace(parts[0]))
 		for i := 1; i < len(parts); i++ {
 			option := strings.TrimSpace(parts[i])
 			r.userDefined[key] = append(r.userDefined[key], option)
 		}
 	}
+
 	return nil
 }
 
@@ -58,6 +115,7 @@ func (r *StructTagRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure
 		onFailure:      onFailure,
 		userDefined:    r.userDefined,
 		isAtLeastGo124: file.Pkg.IsAtLeastGoVersion(lint.Go124),
+		tagCheckers:    tagCheckers,
 	}
 
 	ast.Walk(w, file.AST)
@@ -72,10 +130,9 @@ func (*StructTagRule) Name() string {
 
 type lintStructTagRule struct {
 	onFailure      func(lint.Failure)
-	userDefined    map[string][]string // map: key -> []option
-	usedTagNbr     map[int]bool        // list of used tag numbers
-	usedTagName    map[string]bool     // list of used tag keys
+	userDefined    map[tagKey][]string // map: key -> []option
 	isAtLeastGo124 bool
+	tagCheckers    map[tagKey]tagChecker
 }
 
 func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
@@ -86,11 +143,16 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
 			return nil // skip empty structs
 		}
 
-		w.usedTagNbr = map[int]bool{}
-		w.usedTagName = map[string]bool{}
+		checkCtx := &checkContext{
+			userDefined:    w.userDefined,
+			usedTagNbr:     map[int]bool{},
+			usedTagName:    map[string]bool{},
+			isAtLeastGo124: w.isAtLeastGo124,
+		}
+
 		for _, f := range n.Fields.List {
 			if f.Tag != nil {
-				w.checkTaggedField(f)
+				w.checkTaggedField(checkCtx, f)
 			}
 		}
 	}
@@ -98,30 +160,44 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
 	return w
 }
 
-const (
-	keyASN1         = "asn1"
-	keyBSON         = "bson"
-	keyDatastore    = "datastore"
-	keyDefault      = "default"
-	keyJSON         = "json"
-	keyMapstructure = "mapstructure"
-	keyProperties   = "properties"
-	keyProtobuf     = "protobuf"
-	keyRequired     = "required"
-	keyTOML         = "toml"
-	keyURL          = "url"
-	keyValidate     = "validate"
-	keyXML          = "xml"
-	keyYAML         = "yaml"
-)
+// checkTaggedField checks the tag of the given field.
+// precondition: the field has a tag
+func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, f *ast.Field) {
+	if len(f.Names) > 0 && !f.Names[0].IsExported() {
+		w.addFailuref(f, "tag on not-exported field %s", f.Names[0].Name)
+	}
 
-func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool) {
+	tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
+	if err != nil || tags == nil {
+		w.addFailuref(f.Tag, "malformed tag")
+		return
+	}
+
+	for _, tag := range tags.Tags() {
+		if msg, ok := w.checkTagNameIfNeed(checkCtx, tag); !ok {
+			w.addFailureWithTagKey(f.Tag, msg, tag.Key)
+		}
+
+		checker, ok := w.tagCheckers[tagKey(tag.Key)]
+		if !ok {
+			continue // we don't have a checker for the tag
+		}
+
+		msg, ok := checker(checkCtx, tag, f.Type)
+		if !ok {
+			w.addFailureWithTagKey(f.Tag, msg, tag.Key)
+		}
+	}
+}
+
+func (w lintStructTagRule) checkTagNameIfNeed(checkCtx *checkContext, tag *structtag.Tag) (message string, succeeded bool) {
 	isUnnamedTag := tag.Name == "" || tag.Name == "-"
 	if isUnnamedTag {
 		return "", true
 	}
 
-	switch tag.Key {
+	key := tagKey(tag.Key)
+	switch key {
 	case keyBSON, keyJSON, keyXML, keyYAML, keyProtobuf:
 	default:
 		return "", true
@@ -134,22 +210,23 @@ func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool)
 
 	// We concat the key and name as the mapping key here
 	// to allow the same tag name in different tag type.
-	key := tag.Key + ":" + tagName
-	if _, ok := w.usedTagName[key]; ok {
-		return fmt.Sprintf("duplicate tag name: '%s'", tagName), false
+	mapKey := tag.Key + ":" + tagName
+	if _, ok := checkCtx.usedTagName[mapKey]; ok {
+		return fmt.Sprintf("duplicated tag name %q", tagName), false
 	}
 
-	w.usedTagName[key] = true
+	checkCtx.usedTagName[mapKey] = true
 
 	return "", true
 }
 
 func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
-	switch tag.Key {
+	key := tagKey(tag.Key)
+	switch key {
 	case keyProtobuf:
 		for _, option := range tag.Options {
-			if tagName, found := strings.CutPrefix(option, "name="); found {
-				return tagName
+			if tagKey, found := strings.CutPrefix(option, "name="); found {
+				return tagKey
 			}
 		}
 		return "" // protobuf tag lacks 'name' option
@@ -158,286 +235,289 @@ func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
 	}
 }
 
-// checkTaggedField checks the tag of the given field.
-// precondition: the field has a tag
-func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
-	if len(f.Names) > 0 && !f.Names[0].IsExported() {
-		w.addFailure(f, "tag on not-exported field "+f.Names[0].Name)
+func checkASN1Tag(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) {
+	checkList := append(tag.Options, tag.Name)
+	for _, opt := range checkList {
+		switch opt {
+		case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8":
+			// do nothing
+		default:
+			msg, ok := checkCompoundANS1Option(checkCtx, opt, fieldType)
+			if !ok {
+				return msg, false
+			}
+		}
 	}
 
-	tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
-	if err != nil || tags == nil {
-		w.addFailure(f.Tag, "malformed tag")
-		return
-	}
+	return "", true
+}
 
-	for _, tag := range tags.Tags() {
-		if msg, ok := w.checkTagNameIfNeed(tag); !ok {
-			w.addFailure(f.Tag, msg)
+func checkCompoundANS1Option(checkCtx *checkContext, opt string, fieldType ast.Expr) (message string, succeeded bool) {
+	key, value, _ := strings.Cut(opt, ":")
+	switch key {
+	case "tag":
+		number, err := strconv.Atoi(value)
+		if err != nil {
+			return fmt.Sprintf("tag must be a number but is %q", value), false
 		}
-
-		switch key := tag.Key; key {
-		case keyASN1:
-			msg, ok := w.checkASN1Tag(f.Type, tag)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyBSON:
-			msg, ok := w.checkBSONTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyDatastore:
-			msg, ok := w.checkDatastoreTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyDefault:
-			if !w.typeValueMatch(f.Type, tag.Name) {
-				w.addFailure(f.Tag, "field's type and default value's type mismatch")
-			}
-		case keyJSON:
-			msg, ok := w.checkJSONTag(tag.Name, tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyMapstructure:
-			msg, ok := w.checkMapstructureTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyProperties:
-			msg, ok := w.checkPropertiesTag(f.Type, tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyProtobuf:
-			msg, ok := w.checkProtobufTag(tag)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyRequired:
-			if tag.Name != "true" && tag.Name != "false" {
-				w.addFailure(f.Tag, "required should be 'true' or 'false'")
-			}
-		case keyTOML:
-			msg, ok := w.checkTOMLTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyURL:
-			msg, ok := w.checkURLTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyValidate:
-			opts := append([]string{tag.Name}, tag.Options...)
-			msg, ok := w.checkValidateTag(opts)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyXML:
-			msg, ok := w.checkXMLTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		case keyYAML:
-			msg, ok := w.checkYAMLTag(tag.Options)
-			if !ok {
-				w.addFailure(f.Tag, msg)
-			}
-		default:
-			// unknown key
+		if checkCtx.usedTagNbr[number] {
+			return fmt.Sprintf(msgDuplicatedTagNumber, number), false
+		}
+		checkCtx.usedTagNbr[number] = true
+	case "default":
+		if !typeValueMatch(fieldType, value) {
+			return msgTypeMismatch, false
+		}
+	default:
+		if !checkCtx.isUserDefined(keyASN1, opt) {
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
+	return "", true
 }
 
-func (w lintStructTagRule) checkASN1Tag(t ast.Expr, tag *structtag.Tag) (string, bool) {
-	checkList := append(tag.Options, tag.Name)
-	for _, opt := range checkList {
+func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
-		case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8":
-
+		case "flatten", "noindex", "omitempty":
 		default:
-			if strings.HasPrefix(opt, "tag:") {
-				parts := strings.Split(opt, ":")
-				tagNumber := parts[1]
-				number, err := strconv.Atoi(tagNumber)
-				if err != nil {
-					return fmt.Sprintf("ASN1 tag must be a number, got '%s'", tagNumber), false
-				}
-				if w.usedTagNbr[number] {
-					return fmt.Sprintf("duplicated tag number %v", number), false
-				}
-				w.usedTagNbr[number] = true
-
-				continue
-			}
-
-			if strings.HasPrefix(opt, "default:") {
-				parts := strings.Split(opt, ":")
-				if len(parts) < 2 {
-					return "malformed default for ASN1 tag", false
-				}
-				if !w.typeValueMatch(t, parts[1]) {
-					return "field's type and default value's type mismatch", false
-				}
-
+			if checkCtx.isUserDefined(keyDatastore, opt) {
 				continue
 			}
+			return fmt.Sprintf(msgUnknownOption, opt), false
+		}
+	}
 
-			if w.isUserDefined(keyASN1, opt) {
-				continue
-			}
+	return "", true
+}
 
-			return fmt.Sprintf("unknown option '%s' in ASN1 tag", opt), false
-		}
+func checkDefaultTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) {
+	if !typeValueMatch(fieldType, tag.Name) {
+		return msgTypeMismatch, false
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkBSONTag(options []string) (string, bool) {
-	for _, opt := range options {
+func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
 		case "inline", "minsize", "omitempty":
 		default:
-			if w.isUserDefined(keyBSON, opt) {
+			if checkCtx.isUserDefined(keyBSON, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in BSON tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkJSONTag(name string, options []string) (string, bool) {
-	for _, opt := range options {
+func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
 		case "omitempty", "string":
 		case "":
 			// special case for JSON key "-"
-			if name != "-" {
-				return "option can not be empty in JSON tag", false
+			if tag.Name != "-" {
+				return "option can not be empty", false
 			}
 		case "omitzero":
-			if w.isAtLeastGo124 {
+			if checkCtx.isAtLeastGo124 {
 				continue
 			}
-			fallthrough
+			return `prior Go 1.24, option "omitzero" is unsupported`, false
 		default:
-			if w.isUserDefined(keyJSON, opt) {
+			if checkCtx.isUserDefined(keyJSON, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in JSON tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkXMLTag(options []string) (string, bool) {
-	for _, opt := range options {
+func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
-		case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
+		case "omitempty", "reminder", "squash":
 		default:
-			if w.isUserDefined(keyXML, opt) {
+			if checkCtx.isUserDefined(keyMapstructure, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in XML tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkYAMLTag(options []string) (string, bool) {
+func checkPropertiesTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) {
+	options := tag.Options
+	if len(options) == 0 {
+		return "", true
+	}
+
+	seenOptions := map[string]bool{}
 	for _, opt := range options {
-		switch opt {
-		case "flow", "inline", "omitempty":
-		default:
-			if w.isUserDefined(keyYAML, opt) {
-				continue
-			}
-			return fmt.Sprintf("unknown option '%s' in YAML tag", opt), false
+		msg, ok := fmt.Sprintf("unknown or malformed option %q", opt), false
+		if key, value, found := strings.Cut(opt, "="); found {
+			msg, ok = checkCompoundPropertiesOption(key, value, fieldType, seenOptions)
+		}
+
+		if !ok {
+			return msg, false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkURLTag(options []string) (string, bool) {
-	var delimiter = ""
+func checkCompoundPropertiesOption(key, value string, fieldType ast.Expr, seenOptions map[string]bool) (message string, succeeded bool) {
+	if _, ok := seenOptions[key]; ok {
+		return fmt.Sprintf(msgDuplicatedOption, key), false
+	}
+	seenOptions[key] = true
+
+	if strings.TrimSpace(value) == "" {
+		return fmt.Sprintf("option %q not of the form %s=value", key, key), false
+	}
+
+	switch key {
+	case "default":
+		if !typeValueMatch(fieldType, value) {
+			return msgTypeMismatch, false
+		}
+	case "layout":
+		if gofmt(fieldType) != "time.Time" {
+			return "layout option is only applicable to fields of type time.Time", false
+		}
+	}
+
+	return "", true
+}
+
+func checkProtobufTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	// check name
+	switch tag.Name {
+	case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64":
+		// do nothing
+	default:
+		return fmt.Sprintf("invalid tag name %q", tag.Name), false
+	}
+
+	return checkProtobufOptions(checkCtx, tag.Options)
+}
+
+func checkProtobufOptions(checkCtx *checkContext, options []string) (message string, succeeded bool) {
+	seenOptions := map[string]bool{}
+	hasName := false
 	for _, opt := range options {
-		switch opt {
-		case "int", "omitempty", "numbered", "brackets":
-		case "unix", "unixmilli", "unixnano": // TODO : check that the field is of type time.Time
-		case "comma", "semicolon", "space":
-			if delimiter == "" {
-				delimiter = opt
-				continue
+		opt := strings.Split(opt, "=")[0]
+
+		if number, err := strconv.Atoi(opt); err == nil {
+			_, alreadySeen := checkCtx.usedTagNbr[number]
+			if alreadySeen {
+				return fmt.Sprintf(msgDuplicatedTagNumber, number), false
 			}
-			return fmt.Sprintf("can not set both '%s' and '%s' as delimiters in URL tag", opt, delimiter), false
+			checkCtx.usedTagNbr[number] = true
+			continue // option is an integer
+		}
+
+		switch opt {
+		case "json", "opt", "proto3", "rep", "req":
+			// do nothing
+		case "name":
+			hasName = true
 		default:
-			if w.isUserDefined(keyURL, opt) {
+			if checkCtx.isUserDefined(keyProtobuf, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in URL tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
+
+		_, alreadySeen := seenOptions[opt]
+		if alreadySeen {
+			return fmt.Sprintf(msgDuplicatedOption, opt), false
+		}
+		seenOptions[opt] = true
+	}
+
+	if !hasName {
+		return `mandatory option "name" not found`, false
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkDatastoreTag(options []string) (string, bool) {
-	for _, opt := range options {
+func checkRequiredTag(_ *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	switch tag.Name {
+	case "true", "false":
+		return "", true
+	default:
+		return `required should be "true" or "false"`, false
+	}
+}
+
+func checkTOMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
-		case "flatten", "noindex", "omitempty":
+		case "omitempty":
 		default:
-			if w.isUserDefined(keyDatastore, opt) {
+			if checkCtx.isUserDefined(keyTOML, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in Datastore tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkMapstructureTag(options []string) (string, bool) {
-	for _, opt := range options {
+func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	var delimiter = ""
+	for _, opt := range tag.Options {
 		switch opt {
-		case "omitempty", "reminder", "squash":
+		case "int", "omitempty", "numbered", "brackets":
+		case "unix", "unixmilli", "unixnano": // TODO : check that the field is of type time.Time
+		case "comma", "semicolon", "space":
+			if delimiter == "" {
+				delimiter = opt
+				continue
+			}
+			return fmt.Sprintf("can not set both %q and %q as delimiters", opt, delimiter), false
 		default:
-			if w.isUserDefined(keyMapstructure, opt) {
+			if checkCtx.isUserDefined(keyURL, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in Mapstructure tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) {
+func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
 	previousOption := ""
 	seenKeysOption := false
+	options := append([]string{tag.Name}, tag.Options...)
 	for _, opt := range options {
 		switch opt {
 		case "keys":
 			if previousOption != "dive" {
-				return "option 'keys' must follow a 'dive' option in validate tag", false
+				return `option "keys" must follow a "dive" option`, false
 			}
 			seenKeysOption = true
 		case "endkeys":
 			if !seenKeysOption {
-				return "option 'endkeys' without a previous 'keys' option in validate tag", false
+				return `option "endkeys" without a previous "keys" option`, false
 			}
 			seenKeysOption = false
 		default:
 			parts := strings.Split(opt, "|")
-			errMsg, ok := w.checkValidateOptionsAlternatives(parts)
+			errMsg, ok := checkValidateOptionsAlternatives(checkCtx, parts)
 			if !ok {
 				return errMsg, false
 			}
@@ -448,47 +528,60 @@ func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) {
 	return "", true
 }
 
-func (w lintStructTagRule) checkTOMLTag(options []string) (string, bool) {
-	for _, opt := range options {
+func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
 		switch opt {
-		case "omitempty":
+		case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
 		default:
-			if w.isUserDefined(keyTOML, opt) {
+			if checkCtx.isUserDefined(keyXML, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in TOML tag", opt), false
+			return fmt.Sprintf(msgUnknownOption, opt), false
 		}
 	}
 
 	return "", true
 }
 
-func (w lintStructTagRule) checkValidateOptionsAlternatives(alternatives []string) (string, bool) {
-	for _, alternative := range alternatives {
-		alternative := strings.TrimSpace(alternative)
-		parts := strings.Split(alternative, "=")
-		switch len(parts) {
-		case 1:
-			badOpt, ok := areValidateOpts(parts[0])
-			if ok || w.isUserDefined(keyValidate, badOpt) {
+func checkYAMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
+	for _, opt := range tag.Options {
+		switch opt {
+		case "flow", "inline", "omitempty":
+		default:
+			if checkCtx.isUserDefined(keyYAML, opt) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in validate tag", badOpt), false
-		case 2:
-			lhs := parts[0]
+			return fmt.Sprintf(msgUnknownOption, opt), false
+		}
+	}
+
+	return "", true
+}
+
+func checkValidateOptionsAlternatives(checkCtx *checkContext, alternatives []string) (message string, succeeded bool) {
+	for _, alternative := range alternatives {
+		alternative := strings.TrimSpace(alternative)
+		lhs, _, found := strings.Cut(alternative, "=")
+		if found {
 			_, ok := validateLHS[lhs]
-			if ok || w.isUserDefined(keyValidate, lhs) {
+			if ok || checkCtx.isUserDefined(keyValidate, lhs) {
 				continue
 			}
-			return fmt.Sprintf("unknown option '%s' in validate tag", lhs), false
-		default:
-			return fmt.Sprintf("malformed options '%s' in validate tag, not expected more than one '='", alternative), false
+			return fmt.Sprintf(msgUnknownOption, lhs), false
 		}
+
+		badOpt, ok := areValidateOpts(alternative)
+		if ok || checkCtx.isUserDefined(keyValidate, badOpt) {
+			continue
+		}
+
+		return fmt.Sprintf(msgUnknownOption, badOpt), false
 	}
+
 	return "", true
 }
 
-func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
+func typeValueMatch(t ast.Expr, val string) bool {
 	tID, ok := t.(*ast.Ident)
 	if !ok {
 		return true
@@ -512,115 +605,19 @@ func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
 
 	return typeMatches
 }
-func (w lintStructTagRule) checkPropertiesTag(t ast.Expr, options []string) (string, bool) {
-	if len(options) == 0 {
-		return "", true
-	}
-
-	hasDefault := false
-	for _, opt := range options {
-		key, val, found := strings.Cut(opt, "=")
-		switch key {
-		case "default":
-			if hasDefault {
-				return "properties tag accepts only one default option", false
-			}
-			hasDefault = true
-
-			if !found {
-				return "malformed default for properties tag", false
-			}
-
-			if !w.typeValueMatch(t, val) {
-				return "field type and default value type mismatch in properties tag", false
-			}
-		case "layout":
-			if !found || strings.TrimSpace(val) == "" {
-				return "malformed layout option for properties tag", false
-			}
-
-			if gofmt(t) != "time.Time" {
-				return "layout option is only applicable to fields of type time.Time in properties tag", false
-			}
-		default:
-			return fmt.Sprintf("unknown option %q in properties tag", opt), false
-		}
-	}
-
-	return "", true
-}
-
-func (w lintStructTagRule) checkProtobufTag(tag *structtag.Tag) (string, bool) {
-	// check name
-	switch tag.Name {
-	case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64":
-		// do nothing
-	default:
-		return fmt.Sprintf("invalid protobuf tag name '%s'", tag.Name), false
-	}
-
-	// check options
-	seenOptions := map[string]bool{}
-	for _, opt := range tag.Options {
-		if number, err := strconv.Atoi(opt); err == nil {
-			_, alreadySeen := w.usedTagNbr[number]
-			if alreadySeen {
-				return fmt.Sprintf("duplicated tag number %v", number), false
-			}
-			w.usedTagNbr[number] = true
-			continue // option is an integer
-		}
-
-		switch {
-		case opt == "opt" || opt == "proto3" || opt == "rep" || opt == "req":
-			// do nothing
-		case strings.Contains(opt, "="):
-			o := strings.Split(opt, "=")[0]
-			_, alreadySeen := seenOptions[o]
-			if alreadySeen {
-				return fmt.Sprintf("protobuf tag has duplicated option '%s'", o), false
-			}
-			seenOptions[o] = true
-			continue
-		}
-	}
-	_, hasName := seenOptions["name"]
-	if !hasName {
-		return "protobuf tag lacks mandatory option 'name'", false
-	}
 
-	for k := range seenOptions {
-		switch k {
-		case "name", "json":
-			// do nothing
-		default:
-			if w.isUserDefined(keyProtobuf, k) {
-				continue
-			}
-			return fmt.Sprintf("unknown option '%s' in protobuf tag", k), false
-		}
-	}
-
-	return "", true
+func (w lintStructTagRule) addFailureWithTagKey(n ast.Node, msg string, tagKey string) {
+	w.addFailuref(n, "%s in %s tag", msg, tagKey)
 }
 
-func (w lintStructTagRule) addFailure(n ast.Node, msg string) {
+func (w lintStructTagRule) addFailuref(n ast.Node, msg string, args ...any) {
 	w.onFailure(lint.Failure{
 		Node:       n,
-		Failure:    msg,
+		Failure:    fmt.Sprintf(msg, args...),
 		Confidence: 1,
 	})
 }
 
-func (w lintStructTagRule) isUserDefined(key, opt string) bool {
-	if w.userDefined == nil {
-		return false
-	}
-
-	options := w.userDefined[key]
-	return slices.Contains(options, opt)
-}
-
 func areValidateOpts(opts string) (string, bool) {
 	parts := strings.Split(opts, "|")
 	for _, opt := range parts {
@@ -633,6 +630,13 @@ func areValidateOpts(opts string) (string, bool) {
 	return "", true
 }
 
+const (
+	msgDuplicatedOption    = "duplicated option %q"
+	msgDuplicatedTagNumber = "duplicated tag number %v"
+	msgUnknownOption       = "unknown option %q"
+	msgTypeMismatch        = "type mismatch between field type and default value type"
+)
+
 var validateSingleOptions = map[string]struct{}{
 	"alpha":                     {},
 	"alphanum":                  {},


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 2597f16..ca7e9dd 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -2,7 +2,7 @@ name: Test
 on:
   workflow_dispatch:
   pull_request:
-    types: [opened, edited, synchronize, reopened]
+    types: [opened, synchronize, reopened]
 
 jobs:
   tests-on-unix:
diff --git a/testdata/go1.24/struct_tag.go b/testdata/go1.24/struct_tag.go
index 5982c76..91dadc7 100644
--- a/testdata/go1.24/struct_tag.go
+++ b/testdata/go1.24/struct_tag.go
@@ -1,22 +1,20 @@
 package fixtures
 
-import "time"
-
 type decodeAndValidateRequest struct {
 	// BEAWRE : the flag of URLParam should match the const string URLParam
 	URLParam    string `json:"-" path:"url_param" validate:"numeric"`
 	Text        string `json:"text" validate:"max=10"`
-	DefaultInt  int    `json:"defaultInt" default:"10.0"` // MATCH /field's type and default value's type mismatch/
+	DefaultInt  int    `json:"defaultInt" default:"10.0"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultInt2 int    `json:"defaultInt2" default:"10"`
-	// MATCH:12 /unknown option 'inline' in JSON tag/
-	DefaultInt3      int             `json:"defaultInt2,inline" default:"11"` // MATCH /duplicate tag name: 'defaultInt2'/
+	// MATCH:10 /unknown option "inline" in json tag/
+	DefaultInt3      int             `json:"defaultInt2,inline" default:"11"` // MATCH /duplicated tag name "defaultInt2" in json tag/
 	DefaultString    string          `json:"defaultString" default:"foo"`
-	DefaultBool      bool            `json:"defaultBool" default:"trues"` // MATCH /field's type and default value's type mismatch/
+	DefaultBool      bool            `json:"defaultBool" default:"trues"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultBool2     bool            `json:"defaultBool2" default:"true"`
 	DefaultBool3     bool            `json:"defaultBool3" default:"false"`
-	DefaultFloat     float64         `json:"defaultFloat" default:"f10.0"` // MATCH /field's type and default value's type mismatch/
+	DefaultFloat     float64         `json:"defaultFloat" default:"f10.0"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultFloat2    float64         `json:"defaultFloat2" default:"10.0"`
-	MandatoryStruct  mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be 'true' or 'false'/
+	MandatoryStruct  mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be "true" or "false" in required tag/
 	MandatoryStruct2 mandatoryStruct `json:"mandatoryStruct2" required:"true"`
 	MandatoryStruct4 mandatoryStruct `json:"mandatoryStruct4" required:"false"`
 	OptionalStruct   *optionalStruct `json:"optionalStruct,omitempty"`
@@ -24,102 +22,13 @@ type decodeAndValidateRequest struct {
 	optionalQuery    string          `json:"-" querystring:"queryfoo"` // MATCH /tag on not-exported field optionalQuery/
 	// No-reg test for bug https://github.com/mgechev/revive/issues/208
 	Tiret       string `json:"-,"`
-	BadTiret    string `json:"other,"`               // MATCH /option can not be empty in JSON tag/
-	ForOmitzero string `json:"forOmitZero,omitzero"` // 'omitzero' is valid in go 1.24
+	BadTiret    string `json:"other,"`               // MATCH /option can not be empty in json tag/
+	ForOmitzero string `json:"forOmitZero,omitzero"` // Go 1.24 introduces omitzero
 }
 
 type RangeAllocation struct {
-	metav1.TypeMeta   `json:",inline"` // MATCH /unknown option 'inline' in JSON tag/
+	metav1.TypeMeta   `json:",inline"` // MATCH /unknown option "inline" in json tag/
 	metav1.ObjectMeta `json:"metadata,omitempty"`
-	Range             string `json:"range,flow"`  // MATCH /unknown option 'flow' in JSON tag/
-	Data              []byte `json:"data,inline"` // MATCH /unknown option 'inline' in JSON tag/
-}
-
-type RangeAllocation struct {
-	metav1.TypeMeta   `bson:",minsize"`
-	metav1.ObjectMeta `bson:"metadata,omitempty"`
-	Range             string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/
-	Data              []byte `bson:"data,inline"`
-}
-
-type TestContextSpecificTags2 struct {
-	A       int       `asn1:"explicit,tag:1"`
-	B       int       `asn1:"tag:2"`
-	S       string    `asn1:"tag:0,utf8"`
-	Ints    []int     `asn1:"set"`
-	Version int       `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0/
-	Time    time.Time `asn1:"explicit,tag:4,other"`                // MATCH /unknown option 'other' in ASN1 tag/
-	X       int       `asn1:"explicit,tag:invalid"`                // MATCH /ASN1 tag must be a number, got 'invalid'/
-}
-
-type VirtualMachineRelocateSpecDiskLocator struct {
-	DynamicData
-
-	DiskId          int32                           `xml:"diskId,attr,cdata"`
-	Datastore       ManagedObjectReference          `xml:"datastore,chardata,innerxml"`
-	DiskMoveType    string                          `xml:"diskMoveType,omitempty,comment"`
-	DiskBackingInfo BaseVirtualDeviceBackingInfo    `xml:"diskBackingInfo,omitempty,any"`
-	Profile         []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option 'other' in XML tag/
-}
-
-type TestDuplicatedXMLTags struct {
-	A int `xml:"a"`
-	B int `xml:"a"` // MATCH /duplicate tag name: 'a'/
-	C int `xml:"c"`
-}
-
-type TestDuplicatedBSONTags struct {
-	A int `bson:"b"`
-	B int `bson:"b"` // MATCH /duplicate tag name: 'b'/
-	C int `bson:"c"`
-}
-
-type TestDuplicatedYAMLTags struct {
-	A int `yaml:"b"`
-	B int `yaml:"c"`
-	C int `yaml:"c"` // MATCH /duplicate tag name: 'c'/
-}
-
-type TestDuplicatedProtobufTags struct {
-	A int `protobuf:"varint,name=b"`
-	B int `protobuf:"varint,name=c"`
-	C int `protobuf:"varint,name=c"` // MATCH /duplicate tag name: 'c'/
-}
-
-// test case from
-// sigs.k8s.io/kustomize/api/types/helmchartargs.go
-
-type HelmChartArgs struct {
-	ChartName        string                 `json:"chartName,omitempty" yaml:"chartName,omitempty"`
-	ChartVersion     string                 `json:"chartVersion,omitempty" yaml:"chartVersion,omitempty"`
-	ChartRepoURL     string                 `json:"chartRepoUrl,omitempty" yaml:"chartRepoUrl,omitempty"`
-	ChartHome        string                 `json:"chartHome,omitempty" yaml:"chartHome,omitempty"`
-	ChartRepoName    string                 `json:"chartRepoName,omitempty" yaml:"chartRepoName,omitempty"`
-	HelmBin          string                 `json:"helmBin,omitempty" yaml:"helmBin,omitempty"`
-	HelmHome         string                 `json:"helmHome,omitempty" yaml:"helmHome,omitempty"`
-	Values           string                 `json:"values,omitempty" yaml:"values,omitempty"`
-	ValuesLocal      map[string]interface{} `json:"valuesLocal,omitempty" yaml:"valuesLocal,omitempty"`
-	ValuesMerge      string                 `json:"valuesMerge,omitempty" yaml:"valuesMerge,omitempty"`
-	ReleaseName      string                 `json:"releaseName,omitempty" yaml:"releaseName,omitempty"`
-	ReleaseNamespace string                 `json:"releaseNamespace,omitempty" yaml:"releaseNamespace,omitempty"`
-	ExtraArgs        []string               `json:"extraArgs,omitempty" yaml:"extraArgs,omitempty"`
-}
-
-// Test message for holding primitive types.
-type Simple struct {
-	OBool                *bool    `protobuf:"varint,1,req,json=oBool"`                           // MATCH /protobuf tag lacks mandatory option 'name'/
-	OInt32               *int32   `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"`            // MATCH /unknown option 'jsonx' in protobuf tag/
-	OInt32Str            *int32   `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"`      // MATCH /protobuf tag has duplicated option 'name'/
-	OInt64               *int64   `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /protobuf tag has duplicated option 'json'/
-	OSint32Str           *int32   `protobuf:"zigzag32,11,opt,name=o_sint32_str,json=oSint32Str"`
-	OSint64Str           *int64   `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicate tag name: 'o_sint32_str'/
-	OFloat               *float32 `protobuf:"fixed32,14,opt,name=o_float,json=oFloat"`
-	ODouble              *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"`      // MATCH /duplicated tag number 14/
-	ODoubleStr           *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid protobuf tag name 'fixed6'/
-	OString              *string  `protobuf:"bytes,18,opt,name=o_string,json=oString"`
-	OString2             *string  `protobuf:"bytes,name=ameno"`
-	OString3             *string  `protobuf:"bytes,name=ameno"` // MATCH /duplicate tag name: 'ameno'/
-	XXX_NoUnkeyedLiteral struct{} `json:"-"`
-	XXX_unrecognized     []byte   `json:"-"`
-	XXX_sizecache        int32    `json:"-"`
+	Range             string `json:"range,flow"`  // MATCH /unknown option "flow" in json tag/
+	Data              []byte `json:"data,inline"` // MATCH /unknown option "inline" in json tag/
 }
diff --git a/testdata/struct_tag.go b/testdata/struct_tag.go
index 4518140..d039619 100644
--- a/testdata/struct_tag.go
+++ b/testdata/struct_tag.go
@@ -6,17 +6,17 @@ type decodeAndValidateRequest struct {
 	// BEAWRE : the flag of URLParam should match the const string URLParam
 	URLParam    string `json:"-" path:"url_param" validate:"numeric"`
 	Text        string `json:"text" validate:"max=10"`
-	DefaultInt  int    `json:"defaultInt" default:"10.0"` // MATCH /field's type and default value's type mismatch/
+	DefaultInt  int    `json:"defaultInt" default:"10.0"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultInt2 int    `json:"defaultInt2" default:"10"`
-	// MATCH:12 /unknown option 'inline' in JSON tag/
-	DefaultInt3      int             `json:"defaultInt2,inline" default:"11"` // MATCH /duplicate tag name: 'defaultInt2'/
+	// MATCH:12 /unknown option "inline" in json tag/
+	DefaultInt3      int             `json:"defaultInt2,inline" default:"11"` // MATCH /duplicated tag name "defaultInt2" in json tag/
 	DefaultString    string          `json:"defaultString" default:"foo"`
-	DefaultBool      bool            `json:"defaultBool" default:"trues"` // MATCH /field's type and default value's type mismatch/
+	DefaultBool      bool            `json:"defaultBool" default:"trues"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultBool2     bool            `json:"defaultBool2" default:"true"`
 	DefaultBool3     bool            `json:"defaultBool3" default:"false"`
-	DefaultFloat     float64         `json:"defaultFloat" default:"f10.0"` // MATCH /field's type and default value's type mismatch/
+	DefaultFloat     float64         `json:"defaultFloat" default:"f10.0"` // MATCH /type mismatch between field type and default value type in default tag/
 	DefaultFloat2    float64         `json:"defaultFloat2" default:"10.0"`
-	MandatoryStruct  mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be 'true' or 'false'/
+	MandatoryStruct  mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be "true" or "false" in required tag/
 	MandatoryStruct2 mandatoryStruct `json:"mandatoryStruct2" required:"true"`
 	MandatoryStruct4 mandatoryStruct `json:"mandatoryStruct4" required:"false"`
 	OptionalStruct   *optionalStruct `json:"optionalStruct,omitempty"`
@@ -24,21 +24,23 @@ type decodeAndValidateRequest struct {
 	optionalQuery    string          `json:"-" querystring:"queryfoo"` // MATCH /tag on not-exported field optionalQuery/
 	// No-reg test for bug https://github.com/mgechev/revive/issues/208
 	Tiret       string `json:"-,"`
-	BadTiret    string `json:"other,"`               // MATCH /option can not be empty in JSON tag/
-	ForOmitzero string `json:"forOmitZero,omitzero"` // MATCH /unknown option 'omitzero' in JSON tag/
+	BadTiret    string `json:"other,"`               // MATCH /option can not be empty in json tag/
+	ForOmitzero string `json:"forOmitZero,omitzero"` // MATCH /prior Go 1.24, option "omitzero" is unsupported in json tag/
+	// MATCH:30 /option can not be empty in json tag/
+	BadTiret string `json:"other,"` // MATCH /duplicated tag name "other" in json tag/
 }
 
 type RangeAllocation struct {
-	metav1.TypeMeta   `json:",inline"` // MATCH /unknown option 'inline' in JSON tag/
+	metav1.TypeMeta   `json:",inline"` // MATCH /unknown option "inline" in json tag/
 	metav1.ObjectMeta `json:"metadata,omitempty"`
-	Range             string `json:"range,flow"`  // MATCH /unknown option 'flow' in JSON tag/
-	Data              []byte `json:"data,inline"` // MATCH /unknown option 'inline' in JSON tag/
+	Range             string `json:"range,flow"`  // MATCH /unknown option "flow" in json tag/
+	Data              []byte `json:"data,inline"` // MATCH /unknown option "inline" in json tag/
 }
 
 type RangeAllocation struct {
 	metav1.TypeMeta   `bson:",minsize"`
 	metav1.ObjectMeta `bson:"metadata,omitempty"`
-	Range             string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/
+	Range             string `bson:"range,flow"` // MATCH /unknown option "flow" in bson tag/
 	Data              []byte `bson:"data,inline"`
 }
 
@@ -47,9 +49,9 @@ type TestContextSpecificTags2 struct {
 	B       int       `asn1:"tag:2"`
 	S       string    `asn1:"tag:0,utf8"`
 	Ints    []int     `asn1:"set"`
-	Version int       `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0/
-	Time    time.Time `asn1:"explicit,tag:4,other"`                // MATCH /unknown option 'other' in ASN1 tag/
-	X       int       `asn1:"explicit,tag:invalid"`                // MATCH /ASN1 tag must be a number, got 'invalid'/
+	Version int       `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0 in asn1 tag/
+	Time    time.Time `asn1:"explicit,tag:4,other"`                // MATCH /unknown option "other" in asn1 tag/
+	X       int       `asn1:"explicit,tag:invalid"`                // MATCH /tag must be a number but is "invalid" in asn1 tag/
 }
 
 type VirtualMachineRelocateSpecDiskLocator struct {
@@ -59,31 +61,31 @@ type VirtualMachineRelocateSpecDiskLocator struct {
 	Datastore       ManagedObjectReference          `xml:"datastore,chardata,innerxml"`
 	DiskMoveType    string                          `xml:"diskMoveType,omitempty,comment"`
 	DiskBackingInfo BaseVirtualDeviceBackingInfo    `xml:"diskBackingInfo,omitempty,any"`
-	Profile         []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option 'other' in XML tag/
+	Profile         []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option "other" in xml tag/
 }
 
-type TestDuplicatedXMLTags struct {
+type TestDuplicatedxmlTags struct {
 	A int `xml:"a"`
-	B int `xml:"a"` // MATCH /duplicate tag name: 'a'/
+	B int `xml:"a"` // MATCH /duplicated tag name "a" in xml tag/
 	C int `xml:"c"`
 }
 
-type TestDuplicatedBSONTags struct {
+type TestDuplicatedbsonTags struct {
 	A int `bson:"b"`
-	B int `bson:"b"` // MATCH /duplicate tag name: 'b'/
+	B int `bson:"b"` // MATCH /duplicated tag name "b" in bson tag/
 	C int `bson:"c"`
 }
 
 type TestDuplicatedYAMLTags struct {
 	A int `yaml:"b"`
 	B int `yaml:"c"`
-	C int `yaml:"c"` // MATCH /duplicate tag name: 'c'/
+	C int `yaml:"c"` // MATCH /duplicated tag name "c" in yaml tag/
 }
 
 type TestDuplicatedProtobufTags struct {
 	A int `protobuf:"varint,name=b"`
 	B int `protobuf:"varint,name=c"`
-	C int `protobuf:"varint,name=c"` // MATCH /duplicate tag name: 'c'/
+	C int `protobuf:"varint,name=c"` // MATCH /duplicated tag name "c" in protobuf tag/
 }
 
 // test case from
@@ -107,18 +109,18 @@ type HelmChartArgs struct {
 
 // Test message for holding primitive types.
 type Simple struct {
-	OBool                *bool    `protobuf:"varint,1,req,json=oBool"`                           // MATCH /protobuf tag lacks mandatory option 'name'/
-	OInt32               *int32   `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"`            // MATCH /unknown option 'jsonx' in protobuf tag/
-	OInt32Str            *int32   `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"`      // MATCH /protobuf tag has duplicated option 'name'/
-	OInt64               *int64   `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /protobuf tag has duplicated option 'json'/
+	OBool                *bool    `protobuf:"varint,1,req,json=oBool"`                           // MATCH /mandatory option "name" not found in protobuf tag/
+	OInt32               *int32   `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"`            // MATCH /unknown option "jsonx" in protobuf tag/
+	OInt32Str            *int32   `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"`      // MATCH /duplicated option "name" in protobuf tag/
+	OInt64               *int64   `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /duplicated option "json" in protobuf tag/
 	OSint32Str           *int32   `protobuf:"zigzag32,11,opt,name=o_sint32_str,json=oSint32Str"`
-	OSint64Str           *int64   `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicate tag name: 'o_sint32_str'/
+	OSint64Str           *int64   `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicated tag name "o_sint32_str" in protobuf tag/
 	OFloat               *float32 `protobuf:"fixed32,14,opt,name=o_float,json=oFloat"`
-	ODouble              *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"`      // MATCH /duplicated tag number 14/
-	ODoubleStr           *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid protobuf tag name 'fixed6'/
+	ODouble              *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"`      // MATCH /duplicated tag number 14 in protobuf tag/
+	ODoubleStr           *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid tag name "fixed6" in protobuf tag/
 	OString              *string  `protobuf:"bytes,18,opt,name=o_string,json=oString"`
 	OString2             *string  `protobuf:"bytes,name=ameno"`
-	OString3             *string  `protobuf:"bytes,name=ameno"` // MATCH /duplicate tag name: 'ameno'/
+	OString3             *string  `protobuf:"bytes,name=ameno"` // MATCH /duplicated tag name "ameno" in protobuf tag/
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
@@ -131,20 +133,20 @@ type RequestQueryOption struct {
 	Associations2        []string `url:"associations2,semicolon,omitempty"`
 	Associations3        []string `url:"associations3,space,brackets,omitempty"`
 	Associations4        []string `url:"associations4,numbered,omitempty"`
-	Associations5        []string `url:"associations5,space,semicolon,omitempty"` // MATCH /can not set both 'semicolon' and 'space' as delimiters in URL tag/
+	Associations5        []string `url:"associations5,space,semicolon,omitempty"` // MATCH /can not set both "semicolon" and "space" as delimiters in url tag/
 	PaginateAssociations bool     `url:"paginateAssociations,int,omitempty"`
-	Archived             bool     `url:"archived,myURLOption"` // MATCH /unknown option 'myURLOption' in URL tag/
+	Archived             bool     `url:"archived,myURLOption"` // MATCH /unknown option "myURLOption" in url tag/
 	IDProperty           string   `url:"idProperty,omitempty"`
 }
 
 type Fields struct {
 	Field      string `datastore:",noindex,flatten,omitempty"`
-	OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/
+	OtherField string `datastore:",unknownOption"` // MATCH /unknown option "unknownOption" in datastore tag/
 }
 
 type MapStruct struct {
 	Field1     string `mapstructure:",squash,reminder,omitempty"`
-	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/
+	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option "unknownOption" in mapstructure tag/
 }
 
 type ValidateUser struct {
@@ -152,30 +154,30 @@ type ValidateUser struct {
 	Email       string `validate:"required,email"`
 	Password    string `validate:"required,min=8,max=32"`
 	Biography   string `validate:"min=0,max=1000"`
-	DisplayName string `validate:"displayName,min=3,max=32"` // MATCH /unknown option 'displayName' in validate tag/
+	DisplayName string `validate:"displayName,min=3,max=32"` // MATCH /unknown option "displayName" in validate tag/
 	Complex     string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"`
-	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option 'keys' must follow a 'dive' option in validate tag/
-	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
-	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option "keys" must follow a "dive" option in validate tag/
+	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option "endkeys" without a previous "keys" option in validate tag/
+	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/
 }
 
 type TomlUser struct {
 	Username string `toml:"username,omitempty"`
-	Location string `toml:"location,unknown"` // MATCH /unknown option 'unknown' in TOML tag/
+	Location string `toml:"location,unknown"` // MATCH /unknown option "unknown" in toml tag/
 }
 
 type PropertiesTags struct {
 	Field int               `properties:"-"`
 	Field int               `properties:"myName"`
 	Field int               `properties:"myName,default=15"`
-	Field int               `properties:"myName,default=sString"` // MATCH /field type and default value type mismatch in properties tag/
-	Field int               `properties:",default:15"`            // MATCH /unknown option "default:15" in properties tag/
-	Field int               `properties:",default=15,default=2"`  // MATCH /properties tag accepts only one default option/
+	Field int               `properties:"myName,default=sString"` // MATCH /type mismatch between field type and default value type in properties tag/
+	Field int               `properties:",default:15"`            // MATCH /unknown or malformed option "default:15" in properties tag/
+	Field int               `properties:",default=15,default=2"`  // MATCH /duplicated option "default" in properties tag/
 	Field time.Time         `properties:"date,layout=2006-01-02"`
 	Field time.Time         `properties:",layout=2006-01-02"`
-	Field time.Time         `properties:"date,layout"`            // MATCH /malformed layout option for properties tag/
-	Field time.Time         `properties:"date,layout=  "`         // MATCH /malformed layout option for properties tag/
+	Field time.Time         `properties:"date,layout"`            // MATCH /unknown or malformed option "layout" in properties tag/
+	Field time.Time         `properties:"date,layout=  "`         // MATCH /option "layout" not of the form layout=value in properties tag/
 	Field string            `properties:"date,layout=2006-01-02"` // MATCH /layout option is only applicable to fields of type time.Time in properties tag/
 	Field []string          `properties:",default=a;b;c"`
-	Field map[string]string `properties:"myName,omitempty"` // MATCH /unknown option "omitempty" in properties tag/
+	Field map[string]string `properties:"myName,omitempty"` // MATCH /unknown or malformed option "omitempty" in properties tag/
 }
diff --git a/testdata/struct_tag_user_options.go b/testdata/struct_tag_user_options.go
index 66585e6..f2a925b 100644
--- a/testdata/struct_tag_user_options.go
+++ b/testdata/struct_tag_user_options.go
@@ -4,30 +4,30 @@ type RangeAllocation struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 	Range             string `json:"range,outline"`
-	Data              []byte `json:"data,flow"` // MATCH /unknown option 'flow' in JSON tag/
+	Data              []byte `json:"data,flow"` // MATCH /unknown option "flow" in json tag/
 }
 
 type RangeAllocation struct {
 	metav1.TypeMeta   `bson:",minsize,gnu"`
 	metav1.ObjectMeta `bson:"metadata,omitempty"`
-	Range             string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/
+	Range             string `bson:"range,flow"` // MATCH /unknown option "flow" in bson tag/
 	Data              []byte `bson:"data,inline"`
 }
 
 type RequestQueryOptions struct {
-	Properties       []string `url:"properties,commmma,omitempty"` // MATCH /unknown option 'commmma' in URL tag/
+	Properties       []string `url:"properties,commmma,omitempty"` // MATCH /unknown option "commmma" in url tag/
 	CustomProperties []string `url:"-"`
 	Archived         bool     `url:"archived,myURLOption"`
 }
 
 type Fields struct {
 	Field      string `datastore:",noindex,flatten,omitempty,myDatastoreOption"`
-	OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/
+	OtherField string `datastore:",unknownOption"` // MATCH /unknown option "unknownOption" in datastore tag/
 }
 
 type MapStruct struct {
 	Field1     string `mapstructure:",squash,reminder,omitempty,myMapstructureOption"`
-	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/
+	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option "unknownOption" in mapstructure tag/
 }
 
 type ValidateUser struct {
@@ -37,9 +37,9 @@ type ValidateUser struct {
 	Biography   string `validate:"min=0,max=1000"`
 	DisplayName string `validate:"displayName,min=3,max=32"`
 	Complex     string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"`
-	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option 'keys' must follow a 'dive' option in validate tag/
-	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
-	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option "keys" must follow a "dive" option in validate tag/
+	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option "endkeys" without a previous "keys" option in validate tag/
+	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/
 }
 
 type TomlUser struct {