Repository: googleapis/api-linter
Pull request #724 (2021-01-06 18:39:06)

feat: Add rule prohibiting Markdown headings.
Toward #720.


Patch:
diff --git a/docs/rules/0192/no-markdown-headings.md b/docs/rules/0192/no-markdown-headings.md
new file mode 100644
index 0000000..3ea6ff8
--- /dev/null
+++ b/docs/rules/0192/no-markdown-headings.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 192
+  name: [core, '0192', no-markdown-headings]
+  summary: Public comments should not include Markdown headings.
+permalink: /192/no-markdown-headings
+redirect_from:
+  - /0192/no-markdown-headings
+---
+
+# No Markdown headings
+
+This rule enforces that public comments for proto descriptors do not have
+Markdown headings (`#`, `##`, etc.), 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 _public_ comments include Markdown headings (that
+become `<h1>`, `<h2>`, etc.).
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// # A representation of a book.
+message Book {
+  // ## The resource name of the book.
+  string name = 1;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+// A representation of a book.
+message Book {
+  // The resource name of the book.
+  string name = 1;
+}
+```
+
+## 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
+// # A representation of a book.
+// (-- api-linter: core::0192::no-markdown-headings=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message Book {
+  // # The resource name of the book.
+  // (-- api-linter: core::0192::no-markdown-headings=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string name = 1;
+}
+```
+
+[aip-192]: https://aip.dev/192
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0192/only-leading-comments.md b/docs/rules/0192/only-leading-comments.md
index 024b991..7edee27 100644
--- a/docs/rules/0192/only-leading-comments.md
+++ b/docs/rules/0192/only-leading-comments.md
@@ -8,7 +8,7 @@ redirect_from:
   - /0192/only-leading-comments
 ---
 
-# Proto3 syntax
+# Only leading comments
 
 This rule enforces that every descriptor in every proto file has a public
 comment information only in leading comment (not trailing comments or detached
diff --git a/rules/aip0192/aip0192.go b/rules/aip0192/aip0192.go
index eba3696..c0f546e 100644
--- a/rules/aip0192/aip0192.go
+++ b/rules/aip0192/aip0192.go
@@ -28,6 +28,7 @@ func AddRules(r lint.RuleRegistry) error {
 		absoluteLinks,
 		hasComments,
 		noHTML,
+		noMarkdownHeadings,
 		onlyLeadingComments,
 		trademarkedNames,
 	)
diff --git a/rules/aip0192/no_markdown_headings.go b/rules/aip0192/no_markdown_headings.go
new file mode 100644
index 0000000..61ef13a
--- /dev/null
+++ b/rules/aip0192/no_markdown_headings.go
@@ -0,0 +1,42 @@
+// Copyright 2021 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 (
+	"regexp"
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var noMarkdownHeadings = &lint.DescriptorRule{
+	Name: lint.NewRuleName(192, "no-markdown-headings"),
+	LintDescriptor: func(d desc.Descriptor) []lint.Problem {
+		for _, cmt := range separateInternalComments(d.GetSourceInfo().GetLeadingComments()).External {
+			for _, line := range strings.Split(cmt, "\n") {
+				if heading.FindString(line) != "" {
+					return []lint.Problem{{
+						Message:    "Comments should not include Markdown headings.",
+						Descriptor: d,
+					}}
+				}
+			}
+		}
+		return nil
+	},
+}
+
+var heading = regexp.MustCompile(`^\s*[#]+ `)


Test patch:
diff --git a/rules/aip0192/no_markdown_headings_test.go b/rules/aip0192/no_markdown_headings_test.go
new file mode 100644
index 0000000..def716d
--- /dev/null
+++ b/rules/aip0192/no_markdown_headings_test.go
@@ -0,0 +1,51 @@
+// Copyright 2021 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 (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestNoMarkdownHeadings(t *testing.T) {
+	problem := testutils.Problems{{Message: "Markdown headings"}}
+	for _, test := range []struct {
+		name     string
+		comment  string
+		problems testutils.Problems
+	}{
+		{"Valid", "foo bar baz", nil},
+		{"ValidMidline", "foo bar # baz", nil},
+		{"H1", "# foo bar baz", problem},
+		{"H2", "## foo bar baz", problem},
+		{"H3", "### foo bar baz", problem},
+		{"H4", "#### foo bar baz", problem},
+		{"InvalidThirdLine", "foo bar baz\n\n# spam eggs", problem},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			cmt := "// " + strings.ReplaceAll(test.comment, "\n", "\n// ")
+			f := testutils.ParseProto3Tmpl(t, `
+			  {{.Comment}}
+			  message Foo {}
+			`, struct{ Comment string }{cmt})
+			m := f.GetMessageTypes()[0]
+			if diff := test.problems.SetDescriptor(m).Diff(noMarkdownHeadings.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}