Repository: googleapis/api-linter
Pull request #970 (2022-05-04 23:07:28)

feat: add cloud AIP group
This lays the ground work to have `"cloud"` as an AIP group for linter rules.


Patch:
diff --git a/docs/rules/cloud.md b/docs/rules/cloud.md
new file mode 100644
index 0000000..c495fcc
--- /dev/null
+++ b/docs/rules/cloud.md
@@ -0,0 +1,13 @@
+---
+permalink: /rules/cloud/
+---
+
+# Cloud rules
+
+Cloud rules are based on [Cloud-specific AIPs][]. They are not enabled by
+default, and should only be enabled for Cloud APIs.
+
+{% include linter-group-listing.html start=2500 end=2599 %}
+{% include linter-group-listing.html start=25000 end=25999 %}
+
+[Cloud-specific aips]: https://aip.dev/cloud
diff --git a/docs/rules/index.md b/docs/rules/index.md
index d676e60..b312338 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -61,6 +61,31 @@ into the "core" group of rules, and remaining blocks are grouped based on the
       </div>
     </a>
   </li>
+  <li>
+    <a class="glue-tile glue-tile--border glue-tile--hoverable"
+        aria-label="Image tile" href="rules/cloud/" tabindex="0">
+      <div class="glue-tile__header glue-tile__header--icon">
+        <svg role="img" class="glue-tile__icon">
+          <use xlink:href="#glue-color-google-logo" x="-76"></use>
+        </svg>
+      </div>
+      <div class="glue-tile__body">
+        <h4 class="glue-tile__headline">
+          Cloud rules
+        </h4>
+        <p class="glue-tile__description">
+          Cloud rules are based on Cloud-specific AIPs, and are disabled by default.
+        </p>
+        <ul class="glue-tile__links">
+          <li class="glue-tile__link">
+            <svg role="img" class="glue-tile__link glue-tile__link--arrow">
+              <use xlink:href="#mi-arrow-forward-no-bg"></use>
+            </svg>
+          </li>
+        </ul>
+      </div>
+    </a>
+  </li>
 </ul>
 
 [aip.dev]: https://aip.dev/
diff --git a/lint/config.go b/lint/config.go
index 895f73d..5e278a9 100644
--- a/lint/config.go
+++ b/lint/config.go
@@ -91,7 +91,9 @@ func ReadConfigsYAML(f io.Reader) (Configs, error) {
 
 // IsRuleEnabled returns true if a rule is enabled by the configs.
 func (configs Configs) IsRuleEnabled(rule string, path string) bool {
-	enabled := true
+	// Enabled by default if the rule does not belong to one of the default
+	// disabled groups. Otherwise, needs to be explicitly enabled.
+	enabled := !matchRule(rule, defaultDisabledRules...)
 	for _, c := range configs {
 		if c.matchPath(path) {
 			if matchRule(rule, c.DisabledRules...) {
diff --git a/lint/rule_enabled.go b/lint/rule_enabled.go
index bdad854..0fea0cd 100644
--- a/lint/rule_enabled.go
+++ b/lint/rule_enabled.go
@@ -21,6 +21,10 @@ import (
 	"github.com/jhump/protoreflect/desc"
 )
 
+// defaultDisabledRules is the list of rules or groups that are by default
+// disabled, because they are scoped to a very specific set of AIPs.
+var defaultDisabledRules = []string{"cloud"}
+
 // Disable all rules for deprecated descriptors.
 func disableDeprecated(d desc.Descriptor) bool {
 	switch v := d.(type) {
diff --git a/lint/rule_groups.go b/lint/rule_groups.go
index 1271bdb..d12dd1c 100644
--- a/lint/rule_groups.go
+++ b/lint/rule_groups.go
@@ -22,6 +22,7 @@ import "fmt"
 var aipGroups = []func(int) string{
 	aipCoreGroup,
 	aipClientLibrariesGroup,
+	aipCloudGroup,
 }
 
 func aipCoreGroup(aip int) string {
@@ -38,6 +39,13 @@ func aipClientLibrariesGroup(aip int) string {
 	return ""
 }
 
+func aipCloudGroup(aip int) string {
+	if (aip >= 2500 && aip <= 2599) || (aip >= 25000 && aip <= 25999) {
+		return "cloud"
+	}
+	return ""
+}
+
 // getRuleGroup takes an AIP number and returns the appropriate group.
 // It panics if no group is found.
 func getRuleGroup(aip int, groups []func(int) string) string {
diff --git a/lint/rule_urls.go b/lint/rule_urls.go
index 51033f1..7d966d7 100644
--- a/lint/rule_urls.go
+++ b/lint/rule_urls.go
@@ -18,6 +18,7 @@ import "strings"
 var ruleURLMappings = []func(string) string{
 	coreRuleURL,
 	clientLibrariesRuleUrl,
+	cloudRuleUrl,
 }
 
 func coreRuleURL(ruleName string) string {
@@ -28,6 +29,10 @@ func clientLibrariesRuleUrl(ruleName string) string {
 	return groupUrl(ruleName, "client-libraries")
 }
 
+func cloudRuleUrl(ruleName string) string {
+	return groupUrl(ruleName, "cloud")
+}
+
 func groupUrl(ruleName, groupName string) string {
 	base := "https://linter.aip.dev/"
 	nameParts := strings.Split(ruleName, "::") // e.g., client-libraries::0122::camel-case-uri -> ["client-libraries", "0122", "camel-case-uri"]


Test patch:
diff --git a/lint/config_test.go b/lint/config_test.go
index 433dc96..0321ac2 100644
--- a/lint/config_test.go
+++ b/lint/config_test.go
@@ -211,6 +211,30 @@ func TestRuleConfigs_IsRuleEnabled(t *testing.T) {
 			"testrule::a",
 			disabled,
 		},
+		{
+			"NoConfigMatched_DefaultDisabled",
+			Configs{
+				{
+					IncludedPaths: []string{"a.proto"},
+					DisabledRules: []string{"testrule"},
+				},
+			},
+			"b.proto",
+			"cloud::25164::generic-fields",
+			disabled,
+		},
+		{
+			"ConfigMatched_DefaultDisabled_Enabled",
+			Configs{
+				{
+					IncludedPaths: []string{"a.proto"},
+					EnabledRules:  []string{"cloud"},
+				},
+			},
+			"a.proto",
+			"cloud::25164::generic-fields",
+			enabled,
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
diff --git a/lint/rule_urls_test.go b/lint/rule_urls_test.go
index bc49755..f94b57c 100644
--- a/lint/rule_urls_test.go
+++ b/lint/rule_urls_test.go
@@ -42,6 +42,25 @@ func TestClientLibrariesRuleURL(t *testing.T) {
 	}
 }
 
+func TestCloudRuleURL(t *testing.T) {
+	tests := []struct {
+		name string
+		rule string
+		url  string
+	}{
+		{"CloudRule", "cloud::2500::generic-fields", "https://linter.aip.dev/2500/generic-fields"},
+		{"NotCloudRule", "test::0122::camel-case-uri", ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got := cloudRuleUrl(test.rule); got != test.url {
+				t.Errorf("cloudRuleUrl(%s) got %s, but want %s", test.name, got, test.url)
+			}
+		})
+	}
+}
+
 func TestGetRuleURL(t *testing.T) {
 	var mapping1 = func(name string) string {
 		if name == "one" {