Repository: googleapis/api-linter
Pull request #356 (2019-11-18 23:30:48)

[AIP-122] Prohibit the suffix `_name`.


Patch:
diff --git a/docs/rules/0122/camel-case-uris.md b/docs/rules/0122/camel-case-uris.md
index 04293de..8c8aba5 100644
--- a/docs/rules/0122/camel-case-uris.md
+++ b/docs/rules/0122/camel-case-uris.md
@@ -11,7 +11,7 @@ redirect_from:
 # HTTP URI case
 
 This rule enforces that the HTTP URI pattern only uses camel case for word
-separation, as mandated in [AIP-122](http://aip.dev/122).
+separation, as mandated in [AIP-122][].
 
 ## Details
 
@@ -46,9 +46,11 @@ rpc GetElectronicBook(GetElectronicBookRequest) returns (ElectronicBook) {
 ## Disabling
 
 If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
 
 ```proto
-// (-- api-linter: core::0122::camel-case-uri=disabled --)
+// (-- api-linter: core::0122::camel-case-uri=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
 rpc GetElectronicBook(GetElectronicBookRequest) returns (ElectronicBook) {
   option (google.api.http) = {
     // Should be "electronicBooks", not "electronic_books".
@@ -59,3 +61,6 @@ rpc GetElectronicBook(GetElectronicBookRequest) returns (ElectronicBook) {
 
 If you need to violate this rule for an entire file, place the comment at the
 top of the file.
+
+[aip-122]: https://aip.dev/122
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0122/name-suffix.md b/docs/rules/0122/name-suffix.md
new file mode 100644
index 0000000..3105ad2
--- /dev/null
+++ b/docs/rules/0122/name-suffix.md
@@ -0,0 +1,61 @@
+---
+rule:
+  aip: 122
+  name: [core, '0122', name-suffix]
+  summary: Fields should not use the suffix `_name`.
+permalink: /122/name-suffix
+redirect_from:
+  - /0122/name-suffix
+---
+
+# HTTP URI case
+
+This rule enforces that fields do not use the suffix `_name`, as mandated in
+[AIP-122][].
+
+## Details
+
+This rule scans all fields complains if it sees the suffix `_name` on a field.
+
+**Note:** The standard field `display_name` is exempt.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message Book {
+  string name = 1;
+  string author_name = 2;  // Should be `author`.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message Book {
+  string name = 1;
+  string author = 2;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+
+```proto
+// (-- api-linter: core::0122::name-suffix=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message Book {
+  string name = 1;
+  string author_name = 2;  // Should be `author`.
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-122]: http://aip.dev/122
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0122/aip0122.go b/rules/aip0122/aip0122.go
index a7a40c6..47db3b2 100644
--- a/rules/aip0122/aip0122.go
+++ b/rules/aip0122/aip0122.go
@@ -25,5 +25,6 @@ func AddRules(r lint.RuleRegistry) error {
 	return r.Register(
 		122,
 		httpURICase,
+		nameSuffix,
 	)
 }
diff --git a/rules/aip0122/name_suffix.go b/rules/aip0122/name_suffix.go
new file mode 100644
index 0000000..37ac620
--- /dev/null
+++ b/rules/aip0122/name_suffix.go
@@ -0,0 +1,42 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aip0122
+
+import (
+	"strings"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var nameSuffix = &lint.FieldRule{
+	Name: lint.NewRuleName(122, "name-suffix"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return !stringset.New("name", "display_name").Contains(f.GetName())
+	},
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		if n := f.GetName(); strings.HasSuffix(n, "_name") {
+			return []lint.Problem{{
+				Message:    "Fields should not use the `_name` suffix.",
+				Suggestion: strings.TrimSuffix(n, "_name"),
+				Descriptor: f,
+				Location:   locations.DescriptorName(f),
+			}}
+		}
+		return nil
+	},
+}


Test patch:
diff --git a/rules/aip0122/name_suffix_test.go b/rules/aip0122/name_suffix_test.go
new file mode 100644
index 0000000..a1503a9
--- /dev/null
+++ b/rules/aip0122/name_suffix_test.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aip0122
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestNameSuffix(t *testing.T) {
+	for _, test := range []struct {
+		name      string
+		FieldName string
+		problems  testutils.Problems
+	}{
+		{"Valid", "publisher", testutils.Problems{}},
+		{"ValidStandard", "display_name", testutils.Problems{}},
+		{"Invalid", "author_name", testutils.Problems{{Suggestion: "author"}}},
+	} {
+		f := testutils.ParseProto3Tmpl(t, `
+			message Book {
+				string name = 1;
+				string {{.FieldName}} = 2;
+			}
+		`, test)
+		field := f.GetMessageTypes()[0].GetFields()[1]
+		if diff := test.problems.SetDescriptor(field).Diff(nameSuffix.Lint(f)); diff != "" {
+			t.Errorf(diff)
+		}
+	}
+}