Repository: googleapis/api-linter
Pull request #214 (2019-10-07 20:37:08)

Added field annotation input_only rule.
This PR will fix issue #173 


Patch:
diff --git a/rules/aip0203/aip0203.go b/rules/aip0203/aip0203.go
new file mode 100644
index 0000000..3a4abfd
--- /dev/null
+++ b/rules/aip0203/aip0203.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 aip0203 contains rules defined in https://aip.dev/203.
+package aip0203
+
+import (
+	"fmt"
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules adds all of the AIP-203 rules to the provided registry.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		inputOnly,
+	)
+}
+
+// check leading comments of a field and produce a problem
+// if the comments match the give pattern.
+func checkLeadingComments(f *desc.FieldDescriptor, pattern *regexp.Regexp, annotation string) []lint.Problem {
+	leadingComments := f.GetSourceInfo().GetLeadingComments()
+	if pattern.MatchString(leadingComments) {
+		return []lint.Problem{lint.Problem{
+			Message:    fmt.Sprintf("Use the `google.api.field_behavior` annotation instead of %q in the leading comments. For example, `string name = 1 [(google.api.field_behavior) = %s];`.", pattern.FindString(leadingComments), annotation),
+			Descriptor: f,
+		}}
+	}
+	return nil
+}
diff --git a/rules/aip0203/input_only.go b/rules/aip0203/input_only.go
new file mode 100644
index 0000000..8c891f4
--- /dev/null
+++ b/rules/aip0203/input_only.go
@@ -0,0 +1,44 @@
+package aip0203
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// This rule inspects the leading comments of each field
+// and if anything looks similar to "Input only.", it throws
+// a problem.
+//
+// Examples:
+// Incorrect code for this rule:
+//
+// message Book {
+//   // Secrets to be stored in the book
+//   // @InputOnly
+//   string secret = 1;
+// }
+//
+// or
+//
+// message Book {
+//   // Input only. Secret to be stored in the book.
+//   string secret = 1;
+// }
+//
+// Correct code for this rule:
+//
+// message Book {
+//   // Secret to be stored in the book.
+//   string secret = 1 [(google.api.field_behavior) = INPUT_ONLY];
+// }
+var inputOnly = &lint.FieldRule{
+	Name: lint.NewRuleName("core", "0203", "input-only"),
+	URI:  "http://api.dev/203#guidance",
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		return checkLeadingComments(f, inputOnlyRegexp, "INPUT_ONLY")
+	},
+}
+
+var inputOnlyRegexp = regexp.MustCompile("(?i).*input.?only.*")


Test patch:
diff --git a/rules/aip0203/aip203_test.go b/rules/aip0203/aip203_test.go
new file mode 100644
index 0000000..61c25c6
--- /dev/null
+++ b/rules/aip0203/aip203_test.go
@@ -0,0 +1,18 @@
+package aip0203
+
+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::0203") {
+			t.Errorf("Rule %s is not namespaced to core::0203.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0203/input_only_test.go b/rules/aip0203/input_only_test.go
new file mode 100644
index 0000000..f4f08f5
--- /dev/null
+++ b/rules/aip0203/input_only_test.go
@@ -0,0 +1,104 @@
+package aip0203
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestInputOnly(t *testing.T) {
+	testCases := []struct {
+		name       string
+		annotation string
+		problems   testutils.Problems
+	}{
+		{
+			name:       "input_only",
+			annotation: "input_only",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "inputonly",
+			annotation: "inputonly",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "@inputonly",
+			annotation: "@inputonly",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "@input_only",
+			annotation: "@input_only",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "INPUT_ONLY",
+			annotation: "INPUT_ONLY",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "inputOnly",
+			annotation: "inputOnly",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "@inputOnly",
+			annotation: "@inputonly",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "@INPUT_ONLY",
+			annotation: "@INPUT_ONLY",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "input_only_free_text",
+			annotation: "This field is input only",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:       "!inputOnly",
+			annotation: "!inputOnly",
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			template := `
+				message Book {
+					// Secrets to be stored in the book
+					// {{.Annotation}}
+					string secrect = 1;
+				}
+				`
+			file := testutils.ParseProto3Tmpl(t, template, struct{ Annotation string }{test.annotation})
+			f := file.GetMessageTypes()[0].GetFields()[0]
+			problems := inputOnly.LintField(f)
+			if diff := test.problems.SetDescriptor(f).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}