Repository: googleapis/api-linter
Pull request #495 (2020-02-14 19:10:12)

[AIP-192] Add rule for trademarked names.
Implements this AIP PR: googleapis/aip#420
~Do not merge until the AIP PR is merged.~ EDIT: It now is.


Patch:
diff --git a/docs/rules/0192/trademarked-names.md b/docs/rules/0192/trademarked-names.md
new file mode 100644
index 0000000..1ab5bf9
--- /dev/null
+++ b/docs/rules/0192/trademarked-names.md
@@ -0,0 +1,88 @@
+---
+rule:
+  aip: 192
+  name: [core, '0192', trademarked-names]
+  summary: Trademarked names should be used correctly.
+permalink: /192/trademarked-names
+redirect_from:
+  - /0192/trademarked-names
+---
+
+# Trademarked names
+
+This rule enforces trademarked names in public comments are not abbreviated and
+follow the trademark owner's branding style, as mandated in [AIP-192][].
+
+## Details
+
+This rule looks at each descriptor in each proto file (exempting oneofs and the
+file itself) and complains if it catches a common trademark mistake. It only
+checks against a known problem list, and does not make guesses beyond that.
+
+It currently catches common mistaken variants for:
+
+- App Engine
+- BigQuery
+- Bigtable
+- Bitbucket
+- Cloud Storage
+- Compute Engine
+- Dataflow
+- Dataprep
+- Dialogflow
+- Directory Sync
+- GitHub
+- GitLab
+- G Suite
+- Pub/Sub
+- Service Mesh
+- Stack Overflow
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message Book {
+  string name = 1;
+
+  // A repository containing Markdown files for each chapter of
+  // the book on Github.
+  // (--           ^ Should be GitHub. --)
+  string github_repo = 2;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message Book {
+  string name = 1;
+
+  // A repository containing Markdown files for each chapter of
+  // the book on GitHub.
+  string github_repo = 2;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the descriptor.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message Book {
+  string name = 1;
+
+  // (-- api-linter: core::0192::trademarked-names=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  // A repository containing Markdown files for each chapter of
+  // the book on Github.
+  string github_repo = 2;
+}
+```
+
+[aip-192]: https://aip.dev/192
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0192/aip0192.go b/rules/aip0192/aip0192.go
index b3cad12..a240719 100644
--- a/rules/aip0192/aip0192.go
+++ b/rules/aip0192/aip0192.go
@@ -28,6 +28,7 @@ func AddRules(r lint.RuleRegistry) error {
 		hasComments,
 		noHTML,
 		onlyLeadingComments,
+		trademarkedNames,
 	)
 }
 
diff --git a/rules/aip0192/trademarked_names.go b/rules/aip0192/trademarked_names.go
new file mode 100644
index 0000000..da54505
--- /dev/null
+++ b/rules/aip0192/trademarked_names.go
@@ -0,0 +1,82 @@
+// Copyright 2020 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 aip0192
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var trademarkAliases = map[string][]string{
+	"App Engine":     []string{"GAE", "gae", "AppEngine", "App engine"},
+	"BigQuery":       []string{"Bigquery", "Big Query", "BQ"},
+	"BigQuery ML":    []string{"BQML", "bqml"},
+	"Bigtable":       []string{"BigTable", "Big Table", "Big table"},
+	"Bitbucket":      []string{"BitBucket", "Bit Bucket"},
+	"Cloud Storage":  []string{"GCS", "gcs"},
+	"Compute Engine": []string{"GCE", "gce"},
+	"Dataflow":       []string{"Data Flow", "Data flow", "DataFlow"},
+	"Dataprep":       []string{"Data Prep", "Data prep", "DataPrep"},
+	"Dialogflow":     []string{"DialogFlow", "Dialog Flow", "Dialog flow"},
+	"Directory Sync": []string{"GCDS", "CDS", "gcds", "cds", "DirectorySync"},
+	"GitHub":         []string{"Github", "Git Hub"},
+	"GitLab":         []string{"Gitlab", "Git Lab"},
+	"G Suite":        []string{"GSuite", "G-Suite", "gSuite"},
+	"Pub/Sub":        []string{"PubSub", "Pubsub", "Cloud Pub/Sub"},
+	"Service Mesh":   []string{"ASM", "CSM", "GCSM", "csm", "gcsm"},
+	"Stack Overflow": []string{"StackOverflow"},
+}
+
+// We actually want regexes so we do not accidentally false-positive acronyms
+// that *contain* our matches. (For example, "BQD" should not match and tell us
+// to change to BigQuery.)
+func defaultTrademarkTypos() map[string][]*regexp.Regexp {
+	tmRegexes := map[string][]*regexp.Regexp{}
+	for k, tms := range trademarkAliases {
+		tmReg := []*regexp.Regexp{}
+		for _, tm := range tms {
+			tmReg = append(tmReg, regexp.MustCompile(`\b`+strings.ReplaceAll(tm, " ", `\s+`)+`\b`))
+		}
+		tmRegexes[k] = tmReg
+	}
+	return tmRegexes
+}
+
+var tmRegexes = defaultTrademarkTypos()
+
+var trademarkedNames = &lint.DescriptorRule{
+	Name: lint.NewRuleName(192, "trademarked-names"),
+	LintDescriptor: func(d desc.Descriptor) (problems []lint.Problem) {
+		c := strings.Join(
+			separateInternalComments(d.GetSourceInfo().GetLeadingComments()).External,
+			"\n",
+		)
+		for want, badThings := range tmRegexes {
+			for _, bad := range badThings {
+				if bad.MatchString(c) {
+					problems = append(problems, lint.Problem{
+						Message:    fmt.Sprintf("Use %q in comments, not %q.", want, bad),
+						Descriptor: d,
+					})
+				}
+			}
+		}
+		return
+	},
+}


Test patch:
diff --git a/rules/aip0192/trademarked_names_test.go b/rules/aip0192/trademarked_names_test.go
new file mode 100644
index 0000000..abb444c
--- /dev/null
+++ b/rules/aip0192/trademarked_names_test.go
@@ -0,0 +1,48 @@
+// Copyright 2020 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 aip0192
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestTrademarkedNames(t *testing.T) {
+	for _, test := range []struct {
+		Token    string
+		problems testutils.Problems
+	}{
+		{"GitHub", testutils.Problems{}},
+		{"Bigtable", testutils.Problems{}},
+		{"Pub/Sub", testutils.Problems{}},
+		{"Git Hub", testutils.Problems{{Message: "GitHub"}}},
+		{"Git  Hub", testutils.Problems{{Message: "GitHub"}}},
+		{"Git\n// Hub", testutils.Problems{{Message: "GitHub"}}},
+		{"Git \n// Hub", testutils.Problems{{Message: "GitHub"}}},
+		{"Git Hub Github", testutils.Problems{{Message: "GitHub"}, {Message: "GitHub"}}},
+		{"G-Suite", testutils.Problems{{Message: "G Suite"}}},
+		{"PubSub", testutils.Problems{{Message: "Pub/Sub"}}},
+	} {
+		f := testutils.ParseProto3Tmpl(t, `
+			// This is a comment that says {{.Token}}.
+			message Foo {}
+		`, test)
+		m := f.GetMessageTypes()[0]
+		if diff := test.problems.SetDescriptor(m).Diff(trademarkedNames.Lint(f)); diff != "" {
+			t.Errorf(diff)
+		}
+	}
+}