Merge "Add comments and clarify errors in neverallow"
diff --git a/android/neverallow.go b/android/neverallow.go
index 0348619..4bb3e57 100644
--- a/android/neverallow.go
+++ b/android/neverallow.go
@@ -15,6 +15,7 @@
 package android
 
 import (
+	"fmt"
 	"path/filepath"
 	"reflect"
 	"regexp"
@@ -372,6 +373,20 @@
 	matcher ValueMatcher
 }
 
+func (r *ruleProperty) String() string {
+	return fmt.Sprintf("%q matches: %s", strings.Join(r.fields, "."), r.matcher)
+}
+
+type ruleProperties []ruleProperty
+
+func (r ruleProperties) String() string {
+	var s []string
+	for _, r := range r {
+		s = append(s, r.String())
+	}
+	return strings.Join(s, " ")
+}
+
 // A NeverAllow rule.
 type Rule interface {
 	In(path ...string) Rule
@@ -413,8 +428,8 @@
 	moduleTypes       []string
 	unlessModuleTypes []string
 
-	props       []ruleProperty
-	unlessProps []ruleProperty
+	props       ruleProperties
+	unlessProps ruleProperties
 
 	onlyBootclasspathJar bool
 }
@@ -424,16 +439,19 @@
 	return &rule{directDeps: make(map[string]bool)}
 }
 
+// In adds path(s) where this rule applies.
 func (r *rule) In(path ...string) Rule {
 	r.paths = append(r.paths, cleanPaths(path)...)
 	return r
 }
 
+// NotIn adds path(s) to that this rule does not apply to.
 func (r *rule) NotIn(path ...string) Rule {
 	r.unlessPaths = append(r.unlessPaths, cleanPaths(path)...)
 	return r
 }
 
+// InDirectDeps adds dep(s) that are not allowed with this rule.
 func (r *rule) InDirectDeps(deps ...string) Rule {
 	for _, d := range deps {
 		r.directDeps[d] = true
@@ -441,25 +459,30 @@
 	return r
 }
 
+// WithOsClass adds osClass(es) that this rule applies to.
 func (r *rule) WithOsClass(osClasses ...OsClass) Rule {
 	r.osClasses = append(r.osClasses, osClasses...)
 	return r
 }
 
+// ModuleType adds type(s) that this rule applies to.
 func (r *rule) ModuleType(types ...string) Rule {
 	r.moduleTypes = append(r.moduleTypes, types...)
 	return r
 }
 
+// NotModuleType adds type(s) that this rule does not apply to..
 func (r *rule) NotModuleType(types ...string) Rule {
 	r.unlessModuleTypes = append(r.unlessModuleTypes, types...)
 	return r
 }
 
+// With specifies property/value combinations that are restricted for this rule.
 func (r *rule) With(properties, value string) Rule {
 	return r.WithMatcher(properties, selectMatcher(value))
 }
 
+// WithMatcher specifies property/matcher combinations that are restricted for this rule.
 func (r *rule) WithMatcher(properties string, matcher ValueMatcher) Rule {
 	r.props = append(r.props, ruleProperty{
 		fields:  fieldNamesForProperties(properties),
@@ -468,10 +491,12 @@
 	return r
 }
 
+// Without specifies property/value combinations that this rule does not apply to.
 func (r *rule) Without(properties, value string) Rule {
 	return r.WithoutMatcher(properties, selectMatcher(value))
 }
 
+// Without specifies property/matcher combinations that this rule does not apply to.
 func (r *rule) WithoutMatcher(properties string, matcher ValueMatcher) Rule {
 	r.unlessProps = append(r.unlessProps, ruleProperty{
 		fields:  fieldNamesForProperties(properties),
@@ -487,49 +512,54 @@
 	return &equalMatcher{expected: expected}
 }
 
+// Because specifies a reason for this rule.
 func (r *rule) Because(reason string) Rule {
 	r.reason = reason
 	return r
 }
 
+// BootclasspathJar whether this rule only applies to Jars in the Bootclasspath
 func (r *rule) BootclasspathJar() Rule {
 	r.onlyBootclasspathJar = true
 	return r
 }
 
 func (r *rule) String() string {
-	s := "neverallow"
-	for _, v := range r.paths {
-		s += " dir:" + v + "*"
+	s := []string{"neverallow requirements. Not allowed:"}
+	if len(r.paths) > 0 {
+		s = append(s, fmt.Sprintf("in dirs: %q", r.paths))
 	}
-	for _, v := range r.unlessPaths {
-		s += " -dir:" + v + "*"
+	if len(r.moduleTypes) > 0 {
+		s = append(s, fmt.Sprintf("module types: %q", r.moduleTypes))
 	}
-	for _, v := range r.moduleTypes {
-		s += " type:" + v
+	if len(r.props) > 0 {
+		s = append(s, fmt.Sprintf("properties matching: %s", r.props))
 	}
-	for _, v := range r.unlessModuleTypes {
-		s += " -type:" + v
+	if len(r.directDeps) > 0 {
+		s = append(s, fmt.Sprintf("dep(s): %q", SortedStringKeys(r.directDeps)))
 	}
-	for _, v := range r.props {
-		s += " " + strings.Join(v.fields, ".") + v.matcher.String()
-	}
-	for _, v := range r.unlessProps {
-		s += " -" + strings.Join(v.fields, ".") + v.matcher.String()
-	}
-	for k := range r.directDeps {
-		s += " deps:" + k
-	}
-	for _, v := range r.osClasses {
-		s += " os:" + v.String()
+	if len(r.osClasses) > 0 {
+		s = append(s, fmt.Sprintf("os class(es): %q", r.osClasses))
 	}
 	if r.onlyBootclasspathJar {
-		s += " inBcp"
+		s = append(s, "in bootclasspath jar")
+	}
+	if len(r.unlessPaths) > 0 {
+		s = append(s, fmt.Sprintf("EXCEPT in dirs: %q", r.unlessPaths))
+	}
+	if len(r.unlessModuleTypes) > 0 {
+		s = append(s, fmt.Sprintf("EXCEPT module types: %q", r.unlessModuleTypes))
+	}
+	if len(r.unlessProps) > 0 {
+		s = append(s, fmt.Sprintf("EXCEPT properties matching: %q", r.unlessProps))
 	}
 	if len(r.reason) != 0 {
-		s += " which is restricted because " + r.reason
+		s = append(s, " which is restricted because "+r.reason)
 	}
-	return s
+	if len(s) == 1 {
+		s[0] = "neverallow requirements (empty)"
+	}
+	return strings.Join(s, "\n\t")
 }
 
 func (r *rule) appliesToPath(dir string) bool {
diff --git a/android/neverallow_test.go b/android/neverallow_test.go
index 18a8705..58a90b3 100644
--- a/android/neverallow_test.go
+++ b/android/neverallow_test.go
@@ -15,6 +15,7 @@
 package android
 
 import (
+	"regexp"
 	"testing"
 
 	"github.com/google/blueprint"
@@ -55,7 +56,37 @@
 				}`),
 		},
 		expectedErrors: []string{
-			`module "libother": violates neverallow deps:not_allowed_in_direct_deps`,
+			regexp.QuoteMeta("module \"libother\": violates neverallow requirements. Not allowed:\n\tdep(s): [\"not_allowed_in_direct_deps\"]"),
+		},
+	},
+	{
+		name: "multiple constraints",
+		rules: []Rule{
+			NeverAllow().
+				InDirectDeps("not_allowed_in_direct_deps").
+				In("other").
+				ModuleType("cc_library").
+				NotIn("top").
+				NotModuleType("cc_binary"),
+		},
+		fs: map[string][]byte{
+			"top/Android.bp": []byte(`
+				cc_library {
+					name: "not_allowed_in_direct_deps",
+				}`),
+			"other/Android.bp": []byte(`
+				cc_library {
+					name: "libother",
+					static_libs: ["not_allowed_in_direct_deps"],
+				}`),
+		},
+		expectedErrors: []string{
+			regexp.QuoteMeta(`module "libother": violates neverallow requirements. Not allowed:
+	in dirs: ["other/"]
+	module types: ["cc_library"]
+	dep(s): ["not_allowed_in_direct_deps"]
+	EXCEPT in dirs: ["top/"]
+	EXCEPT module types: ["cc_binary"]`),
 		},
 	},
 
diff --git a/apex/apex_test.go b/apex/apex_test.go
index 9ab8ca1..727a1f2 100644
--- a/apex/apex_test.go
+++ b/apex/apex_test.go
@@ -7457,7 +7457,7 @@
 		},
 		{
 			name:          "Bootclasspath apex jar not satisfying allowed module packages on Q.",
-			expectedError: `module "bcp_lib2" .* which is restricted because jars that are part of the myapex module may only allow these packages: foo.bar with min_sdk < T. Please jarjar or move code around.`,
+			expectedError: `(?s)module "bcp_lib2" .* which is restricted because jars that are part of the myapex module may only allow these packages: foo.bar with min_sdk < T. Please jarjar or move code around.`,
 			bp: `
 				java_library {
 					name: "bcp_lib1",
@@ -7494,7 +7494,7 @@
 		},
 		{
 			name:          "Bootclasspath apex jar not satisfying allowed module packages on R.",
-			expectedError: `module "bcp_lib2" .* which is restricted because jars that are part of the myapex module may only allow these packages: foo.bar with min_sdk < T. Please jarjar or move code around.`,
+			expectedError: `(?s)module "bcp_lib2" .* which is restricted because jars that are part of the myapex module may only allow these packages: foo.bar with min_sdk < T. Please jarjar or move code around.`,
 			bp: `
 				java_library {
 					name: "bcp_lib1",