Repository: googleapis/api-linter
Pull request #233 (2019-10-14 22:20:08)

[AIP-141] Initial rule: num_foos -> foo_count


Patch:
diff --git a/go.mod b/go.mod
index d24da15..4a9426f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.12
 
 require (
 	github.com/bmatcuk/doublestar v1.1.5
+	github.com/gertd/go-pluralize v0.1.1
 	github.com/golang/protobuf v1.3.2
 	github.com/google/go-cmp v0.3.1
 	github.com/jhump/protoreflect v1.5.0
diff --git a/go.sum b/go.sum
index 340a954..779b44d 100644
--- a/go.sum
+++ b/go.sum
@@ -7,6 +7,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 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/gertd/go-pluralize v0.1.1 h1:fQhql/WRRwr4TVp+TCw12s2esCacvEVBdkTUUwNqF/Q=
+github.com/gertd/go-pluralize v0.1.1/go.mod h1:t5DfHcumb6m0RqyVJDrDLEzL2AGeaiqUXIcDNwLaeAs=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -55,10 +57,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0 h1:ZvI3lsq5AIkr7axxmT3tfwFlJVRFLqe6Fp0W03+MJ38=
 google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20191007162740-aa923e3a3354 h1:KJxw2DvYTCIxlEY4yqWyLdvFGlci4EKTCbrZwfyxDME=
-google.golang.org/genproto v0.0.0-20191007162740-aa923e3a3354/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd h1:84VQPzup3IpKLxuIAZjHMhVjJ8fZ4/i3yUnj3k6fUdw=
-google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8=
 google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/grpc v1.8.0 h1:HN69LlNA/SpyBIRxTfuU0QOntYfdeEeBWlVhRHRCOyw=
diff --git a/rules/aip0141/aip0141.go b/rules/aip0141/aip0141.go
new file mode 100644
index 0000000..365f0b7
--- /dev/null
+++ b/rules/aip0141/aip0141.go
@@ -0,0 +1,27 @@
+// 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 aip0141 contains rules defined in https://aip.dev/141.
+package aip0141
+
+import (
+	"github.com/googleapis/api-linter/lint"
+)
+
+// AddRules adds all of the AIP-141 rules to the provided registry.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		count,
+	)
+}
diff --git a/rules/aip0141/count.go b/rules/aip0141/count.go
new file mode 100644
index 0000000..09bbe05
--- /dev/null
+++ b/rules/aip0141/count.go
@@ -0,0 +1,40 @@
+// 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 aip0141
+
+import (
+	"strings"
+
+	pluralize "github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var count = &lint.FieldRule{
+	Name: lint.NewRuleName("core", "0141", "count-suffix"),
+	URI:  "https://aip.dev/141#guidance",
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		if n := f.GetName(); strings.HasPrefix(n, "num_") {
+			want := pluralize.NewClient().Singular(n[4:]) + "_count"
+			return []lint.Problem{{
+				Message:    "Quantities: Use a _count suffix, not a num_ prefix.",
+				Descriptor: f,
+				Location:   lint.DescriptorNameLocation(f),
+				Suggestion: want,
+			}}
+		}
+		return nil
+	},
+}
diff --git a/rules/rules.go b/rules/rules.go
index 3d5e145..0da3a50 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -23,6 +23,7 @@ import (
 	"github.com/googleapis/api-linter/rules/aip0134"
 	"github.com/googleapis/api-linter/rules/aip0135"
 	"github.com/googleapis/api-linter/rules/aip0140"
+	"github.com/googleapis/api-linter/rules/aip0141"
 	"github.com/googleapis/api-linter/rules/aip0151"
 	"github.com/googleapis/api-linter/rules/aip0158"
 	"github.com/googleapis/api-linter/rules/aip0191"
@@ -37,6 +38,7 @@ func init() {
 	aip0134.AddRules(coreRules)
 	aip0135.AddRules(coreRules)
 	aip0140.AddRules(coreRules)
+	aip0141.AddRules(coreRules)
 	aip0151.AddRules(coreRules)
 	aip0158.AddRules(coreRules)
 	aip0191.AddRules(coreRules)


Test patch:
diff --git a/rules/aip0141/aip0141_test.go b/rules/aip0141/aip0141_test.go
new file mode 100644
index 0000000..afe4cb1
--- /dev/null
+++ b/rules/aip0141/aip0141_test.go
@@ -0,0 +1,32 @@
+// 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 aip0141
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/lint"
+)
+
+func TestAddRules(t *testing.T) {
+	rules := make(lint.RuleRegistry)
+	AddRules(rules)
+	for ruleName := range rules {
+		if !strings.HasPrefix(string(ruleName), "core::0141") {
+			t.Errorf("Rule %s is not namespaced to core::0141.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0141/count_test.go b/rules/aip0141/count_test.go
new file mode 100644
index 0000000..0cc5799
--- /dev/null
+++ b/rules/aip0141/count_test.go
@@ -0,0 +1,57 @@
+// 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 aip0141
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestCount(t *testing.T) {
+	tests := []struct {
+		testName  string
+		FieldName string
+		want      string
+		err       bool
+	}{
+		{"Valid", "item_count", "item_count", false},
+		{"NumItems", "num_items", "item_count", true},
+		{"NumResults", "num_results", "result_count", true},
+		{"NumErrors", "num_errors", "error_count", true},
+		{"NumMoose", "num_moose", "moose_count", true}, // singular of "moose" is "moose"
+		{"NumGeese", "num_geese", "goose_count", true}, // singular of "geese" is "goose"
+	}
+	for _, test := range tests {
+		file := testutils.ParseProto3Tmpl(t, `
+			message Job {
+				int32 {{.FieldName}} = 1;
+			}
+		`, test)
+		field := file.GetMessageTypes()[0].GetFields()[0]
+		wantProblems := testutils.Problems{}
+		if test.err {
+			wantProblems = append(wantProblems, lint.Problem{
+				Message:    "_count suffix",
+				Suggestion: test.want,
+				Descriptor: field,
+			})
+		}
+		if diff := wantProblems.Diff(count.Lint(file)); diff != "" {
+			t.Errorf(diff)
+		}
+	}
+}