Repository: googleapis/api-linter
Pull request #243 (2019-10-18 01:38:04)

[AIP-142] Check field names of timestamps.


Patch:
diff --git a/rules/aip0142/aip0142.go b/rules/aip0142/aip0142.go
new file mode 100644
index 0000000..79c14e7
--- /dev/null
+++ b/rules/aip0142/aip0142.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 aip0142 contains rules defined in https://aip.dev/142.
+package aip0142
+
+import (
+	"github.com/googleapis/api-linter/lint"
+)
+
+// AddRules adds all of the AIP-142 rules to the provided registry.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		fieldNames,
+	)
+}
diff --git a/rules/aip0142/timestamp.go b/rules/aip0142/timestamp.go
new file mode 100644
index 0000000..2a61098
--- /dev/null
+++ b/rules/aip0142/timestamp.go
@@ -0,0 +1,59 @@
+// 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 aip0142
+
+import (
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var fieldNames = &lint.FieldRule{
+	Name: lint.NewRuleName("core", "0142", "field-names"),
+	URI:  "https://aip.dev/142#timestamps",
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		// Look for common non-imperative terms.
+		mistakes := map[string]string{
+			"created":  "create_time",
+			"expired":  "expire_time",
+			"modified": "update_time",
+			"updated":  "update_time",
+		}
+		for got, want := range mistakes {
+			if strings.Contains(f.GetName(), got) {
+				return []lint.Problem{{
+					Message:    "Use the imperative mood and a `_time` suffix for timestamps.",
+					Descriptor: f,
+					Location:   lint.DescriptorNameLocation(f),
+					Suggestion: want,
+				}}
+			}
+		}
+
+		// Look for timestamps that do not end in `_time`.
+		if messageType := f.GetMessageType(); messageType != nil {
+			if messageType.GetFullyQualifiedName() == "google.protobuf.Timestamp" && !strings.HasSuffix(f.GetName(), "_time") {
+				return []lint.Problem{{
+					Message:    "Timestamp fields should end in `_time`.",
+					Descriptor: f,
+					Location:   lint.DescriptorNameLocation(f),
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/rules.go b/rules/rules.go
index 62c5ea4..bdb5df9 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -24,6 +24,7 @@ import (
 	"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/aip0142"
 	"github.com/googleapis/api-linter/rules/aip0143"
 	"github.com/googleapis/api-linter/rules/aip0151"
 	"github.com/googleapis/api-linter/rules/aip0158"
@@ -40,6 +41,7 @@ func init() {
 	aip0135.AddRules(coreRules)
 	aip0140.AddRules(coreRules)
 	aip0141.AddRules(coreRules)
+	aip0142.AddRules(coreRules)
 	aip0143.AddRules(coreRules)
 	aip0151.AddRules(coreRules)
 	aip0158.AddRules(coreRules)


Test patch:
diff --git a/rules/aip0142/aip0142_test.go b/rules/aip0142/aip0142_test.go
new file mode 100644
index 0000000..d1d1d7f
--- /dev/null
+++ b/rules/aip0142/aip0142_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 aip0142
+
+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::0142") {
+			t.Errorf("Rule %s is not namespaced to core::0142.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0142/timestamp_test.go b/rules/aip0142/timestamp_test.go
new file mode 100644
index 0000000..d3b41da
--- /dev/null
+++ b/rules/aip0142/timestamp_test.go
@@ -0,0 +1,50 @@
+// 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 aip0142
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestFieldName(t *testing.T) {
+	tests := []struct {
+		testName  string
+		FieldType string
+		FieldName string
+		problems  testutils.Problems
+	}{
+		{"Valid", "google.protobuf.Timestamp", "create_time", testutils.Problems{}},
+		{"InvalidIsMistake", "google.protobuf.Timestamp", "created", testutils.Problems{{Suggestion: "create_time"}}},
+		{"InvalidContainsMistake", "google.protobuf.Timestamp", "last_modified", testutils.Problems{{Suggestion: "update_time"}}},
+		{"InvalidNoSuffix", "google.protobuf.Timestamp", "create", testutils.Problems{{Message: "should end"}}},
+		{"InvalidIsTypeMistake", "int32", "created", testutils.Problems{{Suggestion: "create_time"}}},
+	}
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/protobuf/timestamp.proto";
+				message Book {
+					{{.FieldType}} {{.FieldName}} = 1;
+				}
+			`, test)
+			field := file.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(fieldNames.Lint(file)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}