Repository: googleapis/api-linter
Pull request #248 (2019-10-20 23:45:30)

[AIP-136] Add initial rules for AIP-136.
This also renames `http-verb` in other AIPs to `http-method`, which is the term most folks use.

Uses #247 as a base, because I made a mistake.


Patch:
diff --git a/rules/aip0126/unspecified.go b/rules/aip0126/unspecified.go
index 9d8834e..42878b9 100644
--- a/rules/aip0126/unspecified.go
+++ b/rules/aip0126/unspecified.go
@@ -23,6 +23,52 @@ import (
 	"github.com/stoewer/go-strcase"
 )
 
+// This rule enforces that all enums have a default unspecified value,
+// as described in [AIP-126](http://aip.dev/126).
+//
+// Because our APIs create automatically-generated client libraries, we need
+// to consider languages that have varying behavior around default values.
+// To avoid any ambiguity or confusion across languages, all enumerations
+// should use an "unspecified" value beginning with the name of the enum
+// itself as the first (0) value.
+//
+// ## Details
+//
+// This rule finds all enumerations and ensures that the first one is
+// named after the enum itself with an `_UNSPECIFIED` suffix attached.
+//
+// ## Examples
+//
+// **Incorrect** code for this rule:
+//
+//   enum Format {
+//     HARDCOVER = 0;  // Should have "FORMAT_UNSPECIFIED" first.
+//   }
+//
+//   enum Format {
+//     UNSPECIFIED = 0;  // Should be "FORMAT_UNSPECIFIED".
+//     HARDCOVER = 1;
+//   }
+//
+// **Correct** code for this rule:
+//
+//   enum Format {
+//     FORMAT_UNSPECIFIED = 0;
+//     HARDCOVER = 1;
+//   }
+//
+// ## Disabling
+//
+// If you need to violate this rule, use a leading comment above the enum
+// value.
+//
+//   enum Format {
+//     // (-- api-linter: core::0126::unspecified=disabled --)
+//     HARDCOVER = 0;
+//   }
+//
+// If you need to violate this rule for an entire file, place the comment at
+// the top of the file.
 var unspecified = &lint.EnumRule{
 	Name: lint.NewRuleName("core", "0126", "unspecified"),
 	URI:  "https://aip.dev/126#guidance",
diff --git a/rules/aip0131/aip0131.go b/rules/aip0131/aip0131.go
index 0c2c97b..fedf2bb 100644
--- a/rules/aip0131/aip0131.go
+++ b/rules/aip0131/aip0131.go
@@ -27,8 +27,8 @@ import (
 func AddRules(r lint.RuleRegistry) {
 	r.Register(
 		httpBody,
+		httpMethod,
 		httpNameField,
-		httpVerb,
 		responseMessageName,
 		requestMessageName,
 		standardFields,
diff --git a/rules/aip0131/method_rules.go b/rules/aip0131/method_rules.go
index 3d7d391..a1e4372 100644
--- a/rules/aip0131/method_rules.go
+++ b/rules/aip0131/method_rules.go
@@ -70,8 +70,8 @@ var responseMessageName = &lint.MethodRule{
 }
 
 // Get methods should use the HTTP GET verb.
-var httpVerb = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0131", "http-verb"),
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0131", "http-method"),
 	URI:    "https://aip.dev/131#guidance",
 	OnlyIf: isGetMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
diff --git a/rules/aip0133/aip0133.go b/rules/aip0133/aip0133.go
index cd0c74e..a376d9d 100644
--- a/rules/aip0133/aip0133.go
+++ b/rules/aip0133/aip0133.go
@@ -27,9 +27,9 @@ import (
 // this AIP's rules to it.
 func AddRules(r lint.RuleRegistry) {
 	r.Register(
-		httpURIField,
-		httpVerb,
 		httpBody,
+		httpURIField,
+		httpMethod,
 		inputName,
 		outputName,
 		resourceField,
diff --git a/rules/aip0133/method_rules.go b/rules/aip0133/method_rules.go
index ae33df7..4190592 100644
--- a/rules/aip0133/method_rules.go
+++ b/rules/aip0133/method_rules.go
@@ -24,8 +24,8 @@ import (
 )
 
 // Create methods should use the HTTP POST verb.
-var httpVerb = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0133", "http-verb"),
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "http-method"),
 	URI:    "https://aip.dev/133#guidance",
 	OnlyIf: isCreateMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
diff --git a/rules/aip0134/aip0134.go b/rules/aip0134/aip0134.go
index 2cd8e1f..49813ca 100644
--- a/rules/aip0134/aip0134.go
+++ b/rules/aip0134/aip0134.go
@@ -27,8 +27,8 @@ import (
 func AddRules(r lint.RuleRegistry) {
 	r.Register(
 		httpBody,
+		httpMethod,
 		httpNameField,
-		httpVerb,
 		responseMessageName,
 		requestMessageName,
 		standardFields,
diff --git a/rules/aip0134/method_rules.go b/rules/aip0134/method_rules.go
index a5c6e53..f0ae0c8 100644
--- a/rules/aip0134/method_rules.go
+++ b/rules/aip0134/method_rules.go
@@ -84,8 +84,8 @@ var responseMessageName = &lint.MethodRule{
 }
 
 // Update methods should use the HTTP PATCH verb.
-var httpVerb = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0134", "http-verb"),
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0134", "http-method"),
 	URI:    "https://aip.dev/134#patch-and-put",
 	OnlyIf: isUpdateMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
diff --git a/rules/aip0135/aip0135.go b/rules/aip0135/aip0135.go
index 56afb3e..a3e2acf 100644
--- a/rules/aip0135/aip0135.go
+++ b/rules/aip0135/aip0135.go
@@ -27,8 +27,8 @@ import (
 func AddRules(r lint.RuleRegistry) {
 	r.Register(
 		httpBody,
+		httpMethod,
 		httpNameField,
-		httpVerb,
 		responseMessageName,
 		requestMessageName,
 		standardFields,
diff --git a/rules/aip0135/method_rules.go b/rules/aip0135/method_rules.go
index e386be5..0eead83 100644
--- a/rules/aip0135/method_rules.go
+++ b/rules/aip0135/method_rules.go
@@ -89,9 +89,9 @@ var responseMessageName = &lint.MethodRule{
 	},
 }
 
-// Delete methods should use the HTTP DELETE verb.
-var httpVerb = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0135", "http-verb"),
+// Delete methods should use the HTTP DELETE method.
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-method"),
 	URI:    "https://aip.dev/135#guidance",
 	OnlyIf: isDeleteMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
diff --git a/rules/aip0136/aip0136.go b/rules/aip0136/aip0136.go
new file mode 100644
index 0000000..28003ce
--- /dev/null
+++ b/rules/aip0136/aip0136.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 aip0136 contains rules defined in https://aip.dev/136.
+package aip0136
+
+import (
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules accepts a register function and registers each of
+// this AIP's rules to it.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		httpMethod,
+		noPrepositions,
+		verbNoun,
+	)
+}
+
+func isCustomMethod(m *desc.MethodDescriptor) bool {
+	for _, prefix := range []string{"Get", "List", "Create", "Update", "Delete", "Replace"} {
+		if strings.HasPrefix(m.GetName(), prefix) {
+			return false
+		}
+	}
+	return true
+}
diff --git a/rules/aip0136/http_method.go b/rules/aip0136/http_method.go
new file mode 100644
index 0000000..6d7f802
--- /dev/null
+++ b/rules/aip0136/http_method.go
@@ -0,0 +1,38 @@
+// 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 aip0136
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0136", "http-method"),
+	URI:    "https://aip.dev/136#guidance",
+	OnlyIf: isCustomMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.Method != "POST" && httpRule.Method != "GET" {
+				return []lint.Problem{{
+					Message:    "Custom methods should use the HTTP POST or GET method.",
+					Descriptor: m,
+				}}
+			}
+		}
+		return nil
+	},
+}
diff --git a/rules/aip0136/prepositions.go b/rules/aip0136/prepositions.go
new file mode 100644
index 0000000..eb9dd3e
--- /dev/null
+++ b/rules/aip0136/prepositions.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 aip0136
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/data"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/stoewer/go-strcase"
+)
+
+var noPrepositions = &lint.MethodRule{
+	Name: lint.NewRuleName("core", "0136", "prepositions"),
+	URI:  "https://aip.dev/136#guidance",
+	LintMethod: func(m *desc.MethodDescriptor) (problems []lint.Problem) {
+		for _, word := range strings.Split(strcase.SnakeCase(m.GetName()), "_") {
+			if data.Prepositions.Contains(word) {
+				problems = append(problems, lint.Problem{
+					Message:    fmt.Sprintf("Method names should not include prepositions (%q).", word),
+					Descriptor: m,
+					Location:   lint.DescriptorNameLocation(m),
+				})
+			}
+		}
+		return
+	},
+}
diff --git a/rules/aip0136/verb_noun.go b/rules/aip0136/verb_noun.go
new file mode 100644
index 0000000..8043884
--- /dev/null
+++ b/rules/aip0136/verb_noun.go
@@ -0,0 +1,43 @@
+// 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 aip0136
+
+import (
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/stoewer/go-strcase"
+)
+
+var verbNoun = &lint.MethodRule{
+	Name: lint.NewRuleName("core", "0136", "verb-noun"),
+	URI:  "https://aip.dev/136#guidance",
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// We can not detect this precisely without a full dictionary (probably
+		// not worth it), but we can catch some common mistakes.
+
+		// Are there at least two words?
+		if wordCount := len(strings.Split(strcase.SnakeCase(m.GetName()), "_")); wordCount == 1 {
+			return []lint.Problem{{
+				Message:    "Custom methods should be named using a verb followed by a noun.",
+				Descriptor: m,
+				Location:   lint.DescriptorNameLocation(m),
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0140/prepositions.go b/rules/aip0140/prepositions.go
index 71ec48f..19b4ad2 100644
--- a/rules/aip0140/prepositions.go
+++ b/rules/aip0140/prepositions.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"strings"
 
-	"bitbucket.org/creachadair/stringset"
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/data"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -27,13 +27,8 @@ var noPrepositions = &lint.FieldRule{
 	Name: lint.NewRuleName("core", "0140", "prepositions"),
 	URI:  "https://aip.dev/140#prepositions",
 	LintField: func(f *desc.FieldDescriptor) (problems []lint.Problem) {
-		banned := stringset.New(
-			"after", "at", "before", "between", "but", "by", "except",
-			"for", "in", "including", "into", "of", "over", "since", "to",
-			"toward", "under", "upon", "with", "within", "without",
-		)
 		for _, word := range strings.Split(f.GetName(), "_") {
-			if banned.Contains(word) {
+			if data.Prepositions.Contains(word) {
 				problems = append(problems, lint.Problem{
 					Message:    fmt.Sprintf("Avoid using %q in field names.", word),
 					Descriptor: f,
diff --git a/rules/internal/data/data.go b/rules/internal/data/data.go
new file mode 100644
index 0000000..bba0a80
--- /dev/null
+++ b/rules/internal/data/data.go
@@ -0,0 +1,28 @@
+// 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 data contains constants used in multiple AIP rules.
+package data
+
+import "bitbucket.org/creachadair/stringset"
+
+// Conjunctions is a set of conjunctions.
+var Conjunctions = stringset.New("and", "or")
+
+// Prepositions is a set of prepositions.
+var Prepositions = stringset.New(
+	"after", "at", "before", "between", "but", "by", "except",
+	"for", "from", "in", "including", "into", "of", "over", "since", "to",
+	"toward", "under", "upon", "with", "within", "without",
+)
diff --git a/rules/rules.go b/rules/rules.go
index bd1d6fa..7339add 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -57,6 +57,7 @@ import (
 	"github.com/googleapis/api-linter/rules/aip0133"
 	"github.com/googleapis/api-linter/rules/aip0134"
 	"github.com/googleapis/api-linter/rules/aip0135"
+	"github.com/googleapis/api-linter/rules/aip0136"
 	"github.com/googleapis/api-linter/rules/aip0140"
 	"github.com/googleapis/api-linter/rules/aip0141"
 	"github.com/googleapis/api-linter/rules/aip0142"
@@ -74,6 +75,7 @@ func init() {
 	aip0133.AddRules(coreRules)
 	aip0134.AddRules(coreRules)
 	aip0135.AddRules(coreRules)
+	aip0136.AddRules(coreRules)
 	aip0140.AddRules(coreRules)
 	aip0141.AddRules(coreRules)
 	aip0142.AddRules(coreRules)


Test patch:
diff --git a/rules/aip0131/method_rules_test.go b/rules/aip0131/method_rules_test.go
index b143ff3..2dd7659 100644
--- a/rules/aip0131/method_rules_test.go
+++ b/rules/aip0131/method_rules_test.go
@@ -97,7 +97,7 @@ func TestResponseMessageName(t *testing.T) {
 	}
 }
 
-func TestHttpVerb(t *testing.T) {
+func TestHttpMethod(t *testing.T) {
 	// Set up GET and POST HTTP annotations.
 	httpGet := &annotations.HttpRule{
 		Pattern: &annotations.HttpRule_Get{
@@ -142,7 +142,7 @@ func TestHttpVerb(t *testing.T) {
 			}
 
 			// Run the method, ensure we get what we expect.
-			problems := httpVerb.Lint(service.GetFile())
+			problems := httpMethod.Lint(service.GetFile())
 			if test.msg == "" && len(problems) > 0 {
 				t.Errorf("Got %v, expected no problems.", problems)
 			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
diff --git a/rules/aip0133/method_rules_test.go b/rules/aip0133/method_rules_test.go
index cbb1684..c0bd435 100644
--- a/rules/aip0133/method_rules_test.go
+++ b/rules/aip0133/method_rules_test.go
@@ -25,7 +25,7 @@ import (
 	"google.golang.org/genproto/googleapis/api/annotations"
 )
 
-func TestHttpVerb(t *testing.T) {
+func TestHttpMethod(t *testing.T) {
 	// Set up GET and POST HTTP annotations.
 	httpGet := &annotations.HttpRule{
 		Pattern: &annotations.HttpRule_Get{
@@ -70,7 +70,7 @@ func TestHttpVerb(t *testing.T) {
 			}
 
 			// Run the method, ensure we get what we expect.
-			problems := httpVerb.Lint(service.GetFile())
+			problems := httpMethod.Lint(service.GetFile())
 			if test.msg == "" && len(problems) > 0 {
 				t.Errorf("Got %v, expected no problems.", problems)
 			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
diff --git a/rules/aip0134/method_rules_test.go b/rules/aip0134/method_rules_test.go
index 9203227..2de0a9c 100644
--- a/rules/aip0134/method_rules_test.go
+++ b/rules/aip0134/method_rules_test.go
@@ -108,7 +108,7 @@ func TestResponseMessageName(t *testing.T) {
 	}
 }
 
-func TestHttpVerb(t *testing.T) {
+func TestHttpMethod(t *testing.T) {
 	// Set up POST and PATCH HTTP annotations.
 	httpPost := &annotations.HttpRule{
 		Pattern: &annotations.HttpRule_Post{
@@ -153,7 +153,7 @@ func TestHttpVerb(t *testing.T) {
 			}
 
 			// Run the method, ensure we get what we expect.
-			problems := httpVerb.Lint(service.GetFile())
+			problems := httpMethod.Lint(service.GetFile())
 			if test.msg == "" && len(problems) > 0 {
 				t.Errorf("Got %v, expected no problems.", problems)
 			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
diff --git a/rules/aip0135/method_rules_test.go b/rules/aip0135/method_rules_test.go
index 808c720..818748e 100644
--- a/rules/aip0135/method_rules_test.go
+++ b/rules/aip0135/method_rules_test.go
@@ -120,7 +120,7 @@ func TestResponseMessageName(t *testing.T) {
 	}
 }
 
-func TestHttpVerb(t *testing.T) {
+func TestHttpMethod(t *testing.T) {
 	// Set up GET and DELETE HTTP annotations.
 	httpGet := &annotations.HttpRule{
 		Pattern: &annotations.HttpRule_Get{
@@ -165,7 +165,7 @@ func TestHttpVerb(t *testing.T) {
 			}
 
 			// Run the method, ensure we get what we expect.
-			problems := httpVerb.Lint(service.GetFile())
+			problems := httpMethod.Lint(service.GetFile())
 			if test.msg == "" && len(problems) > 0 {
 				t.Errorf("Got %v, expected no problems.", problems)
 			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
diff --git a/rules/aip0136/aip0136_test.go b/rules/aip0136/aip0136_test.go
new file mode 100644
index 0000000..7ead22e
--- /dev/null
+++ b/rules/aip0136/aip0136_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 aip0136
+
+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::0136") {
+			t.Errorf("Rule %s is not namespaced to core::0136.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0136/http_method_test.go b/rules/aip0136/http_method_test.go
new file mode 100644
index 0000000..4f3c412
--- /dev/null
+++ b/rules/aip0136/http_method_test.go
@@ -0,0 +1,60 @@
+// 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 aip0136
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestHttpMethod(t *testing.T) {
+	tests := []struct {
+		testName   string
+		MethodName string
+		HTTPMethod string
+		problems   testutils.Problems
+	}{
+		{"ValidGet", "ArchiveBook", "get", testutils.Problems{}},
+		{"ValidPost", "ArchiveBook", "post", testutils.Problems{}},
+		{"InvalidPut", "ArchiveBook", "put", testutils.Problems{{Message: "POST or GET"}}},
+		{"InvalidPatch", "ArchiveBook", "patch", testutils.Problems{{Message: "POST or GET"}}},
+		{"InvalidDelete", "ArchiveBook", "delete", testutils.Problems{{Message: "POST or GET"}}},
+		{"IrrelevantPatch", "UpdateBook", "patch", testutils.Problems{}},
+		{"IrrelevantDelete", "DeleteBook", "delete", testutils.Problems{}},
+		{"IrrelevantPut", "ReplaceBook", "put", testutils.Problems{}},
+	}
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				service Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							{{.HTTPMethod}}: "/v1:{{.MethodName}}"
+						};
+					}
+				}
+				message {{.MethodName}}Request {}
+				message {{.MethodName}}Response {}
+			`, test)
+			method := file.GetServices()[0].GetMethods()[0]
+			got := httpMethod.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(got); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0136/prepositions_test.go b/rules/aip0136/prepositions_test.go
new file mode 100644
index 0000000..1321835
--- /dev/null
+++ b/rules/aip0136/prepositions_test.go
@@ -0,0 +1,48 @@
+// 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 aip0136
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestNoPrepositions(t *testing.T) {
+	tests := []struct {
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"ArchiveBook", testutils.Problems{}},
+		{"MoveToBook", testutils.Problems{{Message: "to"}}},
+		{"MoveFromShelfToShelf", testutils.Problems{{Message: "from"}, {Message: "to"}}},
+	}
+	for _, test := range tests {
+		t.Run(test.MethodName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				service Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response);
+				}
+				message {{.MethodName}}Request {}
+				message {{.MethodName}}Response {}
+			`, test)
+			method := file.GetServices()[0].GetMethods()[0]
+			got := noPrepositions.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(got); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0136/verb_noun_test.go b/rules/aip0136/verb_noun_test.go
new file mode 100644
index 0000000..9142ee2
--- /dev/null
+++ b/rules/aip0136/verb_noun_test.go
@@ -0,0 +1,47 @@
+// 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 aip0136
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestVerbNoun(t *testing.T) {
+	tests := []struct {
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"ArchiveBook", testutils.Problems{}},
+		{"Archive", testutils.Problems{{Message: "verb followed by a noun"}}},
+	}
+	for _, test := range tests {
+		t.Run(test.MethodName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				service Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response);
+				}
+				message {{.MethodName}}Request {}
+				message {{.MethodName}}Response {}
+			`, test)
+			method := file.GetServices()[0].GetMethods()[0]
+			got := verbNoun.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(got); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/internal/data/data_test.go b/rules/internal/data/data_test.go
new file mode 100644
index 0000000..4b44af4
--- /dev/null
+++ b/rules/internal/data/data_test.go
@@ -0,0 +1,25 @@
+// 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 data
+
+import "testing"
+
+// "per" is a special exception to the preposition rule, per AIP-140
+// (but it would be easy for a well-meaning person to add it to the set).
+func TestPrepositionsExcludesPer(t *testing.T) {
+	if Prepositions.Contains("per") {
+		t.Errorf("`per` should be an acceptable preposition.")
+	}
+}