Repository: googleapis/api-linter
Pull request #745 (2021-02-03 14:32:46)

refactor: Add and more widely use helpers for field presence and singular-string type
Contributes to #683.


Patch:
diff --git a/rules/aip0131/request_name_field.go b/rules/aip0131/request_name_field.go
index fb647dd..cb419a4 100644
--- a/rules/aip0131/request_name_field.go
+++ b/rules/aip0131/request_name_field.go
@@ -16,9 +16,8 @@ package aip0131
 
 import (
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 )
 
 // Get request should have a string name field.
@@ -27,16 +26,5 @@ var requestNameField = &lint.FieldRule{
 	OnlyIf: func(f *desc.FieldDescriptor) bool {
 		return isGetRequestMessage(f.GetOwner()) && f.GetName() == "name"
 	},
-	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
-		if f.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`name` field on Get RPCs should be a string",
-				Descriptor: f,
-				Location:   locations.FieldType(f),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	LintField: utils.LintSingularStringField,
 }
diff --git a/rules/aip0132/request_parent_field.go b/rules/aip0132/request_parent_field.go
index 0b2131f..f72af26 100644
--- a/rules/aip0132/request_parent_field.go
+++ b/rules/aip0132/request_parent_field.go
@@ -27,5 +27,5 @@ var requestParentField = &lint.FieldRule{
 	OnlyIf: func(f *desc.FieldDescriptor) bool {
 		return isListRequestMessage(f.GetOwner()) && f.GetName() == "parent"
 	},
-	LintField: utils.LintStringField,
+	LintField: utils.LintSingularStringField,
 }
diff --git a/rules/aip0133/request_parent_field.go b/rules/aip0133/request_parent_field.go
index b2fe0be..bca5f3b 100644
--- a/rules/aip0133/request_parent_field.go
+++ b/rules/aip0133/request_parent_field.go
@@ -20,11 +20,11 @@ import (
 	"github.com/jhump/protoreflect/desc"
 )
 
-// The type of the parent field in a creat request should be string.
+// The type of the parent field in a create request should be string.
 var requestParentField = &lint.FieldRule{
 	Name: lint.NewRuleName(133, "request-parent-field"),
 	OnlyIf: func(f *desc.FieldDescriptor) bool {
 		return isCreateRequestMessage(f.GetOwner()) && f.GetName() == "parent"
 	},
-	LintField: utils.LintStringField,
+	LintField: utils.LintSingularStringField,
 }
diff --git a/rules/aip0135/request_name_field.go b/rules/aip0135/request_name_field.go
index 9a68e68..ae93d9f 100644
--- a/rules/aip0135/request_name_field.go
+++ b/rules/aip0135/request_name_field.go
@@ -16,9 +16,8 @@ package aip0135
 
 import (
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 )
 
 var requestNameField = &lint.FieldRule{
@@ -26,16 +25,5 @@ var requestNameField = &lint.FieldRule{
 	OnlyIf: func(f *desc.FieldDescriptor) bool {
 		return isDeleteRequestMessage(f.GetOwner()) && f.GetName() == "name"
 	},
-	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
-		if f.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`name` field on Delete RPCs should be a string",
-				Suggestion: "string",
-				Descriptor: f,
-				Location:   locations.FieldType(f),
-			}}
-		}
-
-		return nil
-	},
+	LintField: utils.LintSingularStringField,
 }
diff --git a/rules/aip0152/request_name_field.go b/rules/aip0152/request_name_field.go
index b0637e5..cdabe14 100644
--- a/rules/aip0152/request_name_field.go
+++ b/rules/aip0152/request_name_field.go
@@ -15,37 +15,12 @@
 package aip0152
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 var requestNameField = &lint.MessageRule{
-	Name:   lint.NewRuleName(152, "request-name-field"),
-	OnlyIf: isRunRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `name` field is present.
-		nameField := m.FindFieldByName("name")
-		if nameField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `name` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the `name` field is a string.
-		if nameField.GetType() != builder.FieldTypeString().GetType() || nameField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`name` field on Run request message must be a singular string.",
-				Descriptor: nameField,
-				Location:   locations.FieldType(nameField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(152, "request-name-field"),
+	OnlyIf:      isRunRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("name"),
 }
diff --git a/rules/aip0158/request_page_token_field.go b/rules/aip0158/request_page_token_field.go
index 4635c40..a0ff58c 100644
--- a/rules/aip0158/request_page_token_field.go
+++ b/rules/aip0158/request_page_token_field.go
@@ -15,37 +15,12 @@
 package aip0158
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 var requestPaginationPageToken = &lint.MessageRule{
-	Name:   lint.NewRuleName(158, "request-page-token-field"),
-	OnlyIf: isPaginatedRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
-		// Rule check: Establish that a page_size field is present.
-		pageToken := m.FindFieldByName("page_token")
-		if pageToken == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `page_token` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Ensure that the name page_size is the correct type.
-		if pageToken.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`page_token` field on List RPCs should be a string",
-				Suggestion: "string",
-				Descriptor: pageToken,
-				Location:   locations.FieldType(pageToken),
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(158, "request-page-token-field"),
+	OnlyIf:      isPaginatedRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("page_token"),
 }
diff --git a/rules/aip0158/response_next_page_token_field.go b/rules/aip0158/response_next_page_token_field.go
index bc11e51..d6847f3 100644
--- a/rules/aip0158/response_next_page_token_field.go
+++ b/rules/aip0158/response_next_page_token_field.go
@@ -15,37 +15,12 @@
 package aip0158
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 var responsePaginationNextPageToken = &lint.MessageRule{
-	Name:   lint.NewRuleName(158, "response-next-page-token-field"),
-	OnlyIf: isPaginatedResponseMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a next_page_token field is present.
-		nextPageToken := m.FindFieldByName("next_page_token")
-		if nextPageToken == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `next_page_token` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Ensure that the name next_page_token is the correct type.
-		if nextPageToken.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`next_page_token` field on List RPCs should be a string",
-				Suggestion: "string",
-				Descriptor: nextPageToken,
-				Location:   locations.FieldType(nextPageToken),
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(158, "response-next-page-token-field"),
+	OnlyIf:      isPaginatedResponseMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("next_page_token"),
 }
diff --git a/rules/aip0162/commit_request_name_field.go b/rules/aip0162/commit_request_name_field.go
index 853aef5..fdae255 100644
--- a/rules/aip0162/commit_request_name_field.go
+++ b/rules/aip0162/commit_request_name_field.go
@@ -15,38 +15,13 @@
 package aip0162
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 // The Commit request message should have a name field.
 var commitRequestNameField = &lint.MessageRule{
-	Name:   lint.NewRuleName(162, "commit-request-name-field"),
-	OnlyIf: isCommitRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `name` field is present.
-		nameField := m.FindFieldByName("name")
-		if nameField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `name` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the name field is a string.
-		if nameField.GetType() != builder.FieldTypeString().GetType() || nameField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`name` field on Commit request message must be a singular string.",
-				Descriptor: nameField,
-				Location:   locations.FieldType(nameField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(162, "commit-request-name-field"),
+	OnlyIf:      isCommitRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("name"),
 }
diff --git a/rules/aip0162/tag_revision_request_name_field.go b/rules/aip0162/tag_revision_request_name_field.go
index ada4276..57b6466 100644
--- a/rules/aip0162/tag_revision_request_name_field.go
+++ b/rules/aip0162/tag_revision_request_name_field.go
@@ -15,38 +15,13 @@
 package aip0162
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 // The Tag Revision request message should have a name field.
 var tagRevisionRequestNameField = &lint.MessageRule{
-	Name:   lint.NewRuleName(162, "tag-revision-request-name-field"),
-	OnlyIf: isTagRevisionRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `name` field is present.
-		nameField := m.FindFieldByName("name")
-		if nameField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `name` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the name field is a string.
-		if nameField.GetType() != builder.FieldTypeString().GetType() || nameField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`name` field on Tag Revision request message must be a singular string.",
-				Descriptor: nameField,
-				Location:   locations.FieldType(nameField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(162, "tag-revision-request-name-field"),
+	OnlyIf:      isTagRevisionRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("name"),
 }
diff --git a/rules/aip0162/tag_revision_request_tag_field.go b/rules/aip0162/tag_revision_request_tag_field.go
index 0591278..2a16700 100644
--- a/rules/aip0162/tag_revision_request_tag_field.go
+++ b/rules/aip0162/tag_revision_request_tag_field.go
@@ -15,38 +15,13 @@
 package aip0162
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 // The Tag Revision request message should have a tag field.
 var tagRevisionRequestTagField = &lint.MessageRule{
-	Name:   lint.NewRuleName(162, "tag-revision-request-tag-field"),
-	OnlyIf: isTagRevisionRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `tag` field is present.
-		tagField := m.FindFieldByName("tag")
-		if tagField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `tag` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the tag field is a string.
-		if tagField.GetType() != builder.FieldTypeString().GetType() || tagField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`tag` field on Tag Revision request message must be a singular string.",
-				Descriptor: tagField,
-				Location:   locations.FieldType(tagField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(162, "tag-revision-request-tag-field"),
+	OnlyIf:      isTagRevisionRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("tag"),
 }
diff --git a/rules/aip0164/request_name_field.go b/rules/aip0164/request_name_field.go
index 9498e71..1bc22b2 100644
--- a/rules/aip0164/request_name_field.go
+++ b/rules/aip0164/request_name_field.go
@@ -15,39 +15,12 @@
 package aip0164
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 var requestNameField = &lint.MessageRule{
-	Name: lint.NewRuleName(164, "request-name-field"),
-	OnlyIf: func(m *desc.MessageDescriptor) bool {
-		return isUndeleteRequestMessage(m)
-	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `name` field is present.
-		nameField := m.FindFieldByName("name")
-		if nameField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `name` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the `name` field is a string.
-		if nameField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`name` field on Undelete request message must be a string.",
-				Descriptor: nameField,
-				Location:   locations.FieldType(nameField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(164, "request-name-field"),
+	OnlyIf:      isUndeleteRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("name"),
 }
diff --git a/rules/aip0165/request_filter_field.go b/rules/aip0165/request_filter_field.go
index 59f688b..6122d84 100644
--- a/rules/aip0165/request_filter_field.go
+++ b/rules/aip0165/request_filter_field.go
@@ -15,38 +15,13 @@
 package aip0165
 
 import (
-	"fmt"
-
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
-	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/googleapis/api-linter/rules/internal/utils"
 )
 
 // The Purge request message should have filter field.
 var requestFilterField = &lint.MessageRule{
-	Name:   lint.NewRuleName(165, "request-filter-field"),
-	OnlyIf: isPurgeRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `filter` field is present.
-		filterField := m.FindFieldByName("filter")
-		if filterField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `filter` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the filter field is a string.
-		if filterField.GetType() != builder.FieldTypeString().GetType() || filterField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`filter` field on Purge request message must be a singular string.",
-				Descriptor: filterField,
-				Location:   locations.FieldType(filterField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	Name:        lint.NewRuleName(165, "request-filter-field"),
+	OnlyIf:      isPurgeRequestMessage,
+	LintMessage: utils.LintFieldPresentAndSingularString("filter"),
 }
diff --git a/rules/aip0165/request_parent_field.go b/rules/aip0165/request_parent_field.go
index 9c9d380..8da23f7 100644
--- a/rules/aip0165/request_parent_field.go
+++ b/rules/aip0165/request_parent_field.go
@@ -19,10 +19,8 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
@@ -51,26 +49,5 @@ var requestParentField = &lint.MessageRule{
 
 		return isPurgeRequestMessage(m)
 	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() || parentField.IsRepeated() {
-			return []lint.Problem{{
-				Message:    "`parent` field on Purge request message must be a singular string.",
-				Descriptor: parentField,
-				Location:   locations.FieldType(parentField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	LintMessage: utils.LintFieldPresentAndSingularString("parent"),
 }
diff --git a/rules/aip0231/request_parent_field.go b/rules/aip0231/request_parent_field.go
index 945a626..9d77a7a 100644
--- a/rules/aip0231/request_parent_field.go
+++ b/rules/aip0231/request_parent_field.go
@@ -19,10 +19,8 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
@@ -51,26 +49,5 @@ var requestParentField = &lint.MessageRule{
 
 		return isBatchGetRequestMessage(m)
 	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`parent` field on create request message must be a string",
-				Suggestion: "string",
-				Descriptor: parentField,
-				Location:   locations.FieldType(parentField),
-			}}
-		}
-
-		return nil
-	},
+	LintMessage: utils.LintFieldPresentAndSingularString("parent"),
 }
diff --git a/rules/aip0233/request_parent_field.go b/rules/aip0233/request_parent_field.go
index 9d1b120..9b0c9e9 100644
--- a/rules/aip0233/request_parent_field.go
+++ b/rules/aip0233/request_parent_field.go
@@ -19,10 +19,8 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
@@ -51,26 +49,5 @@ var requestParentField = &lint.MessageRule{
 
 		return isBatchCreateRequestMessage(m)
 	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`parent` field on create request message must be a string.",
-				Descriptor: parentField,
-				Location:   locations.FieldType(parentField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	LintMessage: utils.LintFieldPresentAndSingularString("parent"),
 }
diff --git a/rules/aip0234/request_parent_field.go b/rules/aip0234/request_parent_field.go
index 464dd0b..8435f27 100644
--- a/rules/aip0234/request_parent_field.go
+++ b/rules/aip0234/request_parent_field.go
@@ -19,10 +19,8 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
@@ -51,26 +49,5 @@ var requestParentField = &lint.MessageRule{
 
 		return isBatchUpdateRequestMessage(m)
 	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`parent` field on Update request message must be a string.",
-				Descriptor: parentField,
-				Location:   locations.FieldType(parentField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	LintMessage: utils.LintFieldPresentAndSingularString("parent"),
 }
diff --git a/rules/aip0235/request_parent_field.go b/rules/aip0235/request_parent_field.go
index 870e9f7..94a45e8 100644
--- a/rules/aip0235/request_parent_field.go
+++ b/rules/aip0235/request_parent_field.go
@@ -19,10 +19,8 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
@@ -51,26 +49,5 @@ var requestParentField = &lint.MessageRule{
 
 		return isBatchDeleteRequestMessage(m)
 	},
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field.", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`parent` field on Batch Delete request message must be a string.",
-				Descriptor: parentField,
-				Location:   locations.FieldType(parentField),
-				Suggestion: "string",
-			}}
-		}
-
-		return nil
-	},
+	LintMessage: utils.LintFieldPresentAndSingularString("parent"),
 }
diff --git a/rules/internal/utils/common_lints.go b/rules/internal/utils/common_lints.go
index b71fd2e..9a184bd 100644
--- a/rules/internal/utils/common_lints.go
+++ b/rules/internal/utils/common_lints.go
@@ -23,11 +23,23 @@ import (
 	"github.com/jhump/protoreflect/desc/builder"
 )
 
-// LintStringField returns a problem if the field is not a string.
-func LintStringField(f *desc.FieldDescriptor) []lint.Problem {
-	if f.GetType() != builder.FieldTypeString().GetType() {
+// LintFieldPresent returns a problem if the given message does not have the given field.
+func LintFieldPresent(m *desc.MessageDescriptor, field string) (*desc.FieldDescriptor, []lint.Problem) {
+	f := m.FindFieldByName(field)
+	if f == nil {
+		return nil, []lint.Problem{{
+			Message:    fmt.Sprintf("Message `%s` has no `%s` field.", m.GetName(), field),
+			Descriptor: m,
+		}}
+	}
+	return f, nil
+}
+
+// LintSingularStringField returns a problem if the field is not a singular string.
+func LintSingularStringField(f *desc.FieldDescriptor) []lint.Problem {
+	if f.GetType() != builder.FieldTypeString().GetType() || f.IsRepeated() {
 		return []lint.Problem{{
-			Message:    fmt.Sprintf("The `%s` field must be a string.", f.GetType()),
+			Message:    fmt.Sprintf("The `%s` field must be a singular string.", f.GetType()),
 			Suggestion: "string",
 			Descriptor: f,
 			Location:   locations.FieldType(f),
@@ -36,6 +48,17 @@ func LintStringField(f *desc.FieldDescriptor) []lint.Problem {
 	return nil
 }
 
+// LintFieldPresentAndSingularString returns a problem if a message does not have the given singular-string field.
+func LintFieldPresentAndSingularString(field string) func(*desc.MessageDescriptor) []lint.Problem {
+	return func(m *desc.MessageDescriptor) []lint.Problem {
+		f, problems := LintFieldPresent(m, field)
+		if f == nil {
+			return problems
+		}
+		return LintSingularStringField(f)
+	}
+}
+
 // LintRequiredField returns a problem if the field's behavior is not REQUIRED.
 func LintRequiredField(f *desc.FieldDescriptor) []lint.Problem {
 	if !GetFieldBehavior(f).Contains("REQUIRED") {


Test patch:
diff --git a/rules/internal/utils/common_lints_test.go b/rules/internal/utils/common_lints_test.go
index c60d52a..d9cb4af 100644
--- a/rules/internal/utils/common_lints_test.go
+++ b/rules/internal/utils/common_lints_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/googleapis/api-linter/rules/internal/testutils"
 )
 
-func TestLintStringField(t *testing.T) {
+func TestLintSingularStringField(t *testing.T) {
 	for _, test := range []struct {
 		testName  string
 		FieldType string
@@ -14,6 +14,7 @@ func TestLintStringField(t *testing.T) {
 	}{
 		{"Valid", `string`, nil},
 		{"Invalid", `int32`, testutils.Problems{{Suggestion: "string"}}},
+		{"InvalidRepeated", `repeated string`, testutils.Problems{{Suggestion: "string"}}},
 	} {
 		t.Run(test.testName, func(t *testing.T) {
 			f := testutils.ParseProto3Tmpl(t, `
@@ -22,7 +23,7 @@ func TestLintStringField(t *testing.T) {
 				}
 			`, test)
 			field := f.GetMessageTypes()[0].GetFields()[0]
-			problems := LintStringField(field)
+			problems := LintSingularStringField(field)
 			if diff := test.problems.SetDescriptor(field).Diff(problems); diff != "" {
 				t.Error(diff)
 			}