Sortvisibility (#1452)

* Sort starlark load visibility lists

* Updating tests

- Breaking apart so that .bzl behavior can differ from Default
- Adding list of irreversible changes which can not be tested via Bzl->Build or Build->Bzl
- Updating added test case 077 to not test no-op behavior, but actually sort list for .bzl, and not sort list for .build

---------

Co-authored-by: Liam Miller-Cushon <cushon@google.com>
Co-authored-by: Tim Malmström <oreflow@google.com>
diff --git a/build/print_test.go b/build/print_test.go
index 39525d9..cacdb44 100644
--- a/build/print_test.go
+++ b/build/print_test.go
@@ -29,6 +29,13 @@
 	"github.com/bazelbuild/buildtools/testutils"
 )
 
+// List of test files that have irreversible changes (e.g. if a list is sorted only for Bzl,
+// the tests TestPrintBuildAsBzl and TestPrintBzlAsBuild will fail as the list will not be
+// un-sorted by formatting it in the reverse order).
+var hasIrreversibleChanges = map[string]bool{
+	"testdata/077": true,
+}
+
 // exists reports whether the named file exists.
 func exists(name string) bool {
 	_, err := os.Stat(name)
@@ -50,14 +57,14 @@
 	}
 }
 
-func testIdempotence(t *testing.T, file string, isBuild bool) {
+func testIdempotence(t *testing.T, file string, fileType FileType) {
 	defer setFlags(file)()
-	testPrint(t, file, file, isBuild)
+	testPrint(t, file, file, fileType)
 }
 
-func testFormat(t *testing.T, input, output string, isBuild bool) {
+func testFormat(t *testing.T, input, output string, fileType FileType) {
 	defer setFlags(output)()
-	testPrint(t, input, output, isBuild)
+	testPrint(t, input, output, fileType)
 }
 
 // Test that reading and then writing the golden files
@@ -68,18 +75,18 @@
 
 	// Run the tests with --type=build
 	for _, out := range outs {
-		if strings.HasSuffix(out, ".bzl.golden") {
+		if !strings.HasSuffix(out, ".build.golden") {
 			continue
 		}
-		testIdempotence(t, out, true)
+		testIdempotence(t, out, TypeBuild)
 	}
 
 	// Run the same tests with --type=bzl
 	for _, out := range outs {
-		if strings.HasSuffix(out, ".build.golden") {
+		if !strings.HasSuffix(out, ".bzl.golden") {
 			continue
 		}
-		testIdempotence(t, out, false)
+		testIdempotence(t, out, TypeBzl)
 	}
 }
 
@@ -89,21 +96,26 @@
 	defer chdir()
 	for _, in := range ins {
 		prefix := in[:len(in)-len(".in")]
-		outBzl := prefix + ".golden"
-		outBuild := prefix + ".golden"
 
-		if !exists(outBzl) {
-			outBzl = prefix + ".bzl.golden"
-			outBuild = prefix + ".build.golden"
+		if out := prefix + ".golden"; exists(out) {
+			// Tests with a single .golden suffix test that build, bzl and default
+			// all result in same output.
+			testFormat(t, in, out, TypeDefault)
+			testFormat(t, in, out, TypeBzl)
+			testFormat(t, in, out, TypeBuild)
 		}
-
-		testFormat(t, in, outBzl, false)
-		testFormat(t, in, outBuild, true)
-
-		stripslashesBuild := prefix + ".stripslashes.golden"
-		if exists(stripslashesBuild) {
-			// Test this file in BUILD mode only
-			testFormat(t, in, stripslashesBuild, true)
+		if out := prefix + ".default.golden"; exists(out) {
+			testFormat(t, in, out, TypeDefault)
+		}
+		if out := prefix + ".bzl.golden"; exists(out) {
+			testFormat(t, in, out, TypeBzl)
+		}
+		if out := prefix + ".build.golden"; exists(out) {
+			testFormat(t, in, out, TypeBuild)
+		}
+		if out := prefix + ".stripslashes.golden"; exists(out) {
+			// Stripslashes in BUILD mode only.
+			testFormat(t, in, out, TypeBuild)
 		}
 	}
 }
@@ -140,11 +152,14 @@
 	defer chdir()
 	for _, in := range ins {
 		prefix := in[:len(in)-len(".in")]
+		if hasIrreversibleChanges[prefix] {
+			continue
+		}
 		outBuild := prefix + ".build.golden"
 		if !exists(outBuild) {
 			continue
 		}
-		testIdempotence(t, outBuild, false)
+		testIdempotence(t, outBuild, TypeBzl|TypeDefault)
 	}
 }
 
@@ -155,12 +170,15 @@
 	defer chdir()
 	for _, in := range ins {
 		prefix := in[:len(in)-len(".in")]
+		if hasIrreversibleChanges[prefix] {
+			continue
+		}
 		outBuild := prefix + ".build.golden"
 		outBzl := prefix + ".bzl.golden"
 		if !exists(outBuild) {
 			continue
 		}
-		testFormat(t, outBzl, outBuild, true)
+		testFormat(t, outBzl, outBuild, TypeBuild)
 	}
 }
 
@@ -175,7 +193,7 @@
 // testPrint is a helper for testing the printer.
 // It reads the file named in, reformats it, and compares
 // the result to the file named out.
-func testPrint(t *testing.T, in, out string, isBuild bool) {
+func testPrint(t *testing.T, in, out string, fileType FileType) {
 	data, err := os.ReadFile(in)
 	if err != nil {
 		t.Error(err)
@@ -189,14 +207,15 @@
 	}
 
 	base := "testdata/" + filepath.Base(in)
-	parsers := map[string]func(string, []byte) (*File, error){
-		"bzl":     ParseBzl,
-		"default": ParseDefault,
+	parsers := make(map[string]func(string, []byte) (*File, error))
+	if (fileType & TypeBzl) != 0 {
+		parsers["bzl"] = ParseBzl
 	}
-	if isBuild {
-		parsers = map[string]func(string, []byte) (*File, error){
-			"build": ParseBuild,
-		}
+	if (fileType & TypeBuild) != 0 {
+		parsers["build"] = ParseBuild
+	}
+	if (fileType & TypeDefault) != 0 {
+		parsers["default"] = ParseDefault
 	}
 
 	for name, parser := range parsers {
diff --git a/build/rewrite.go b/build/rewrite.go
index 45bae67..75d4984 100644
--- a/build/rewrite.go
+++ b/build/rewrite.go
@@ -465,7 +465,7 @@
 	return p.index < q.index
 }
 
-// sortStringLists sorts lists of string literals used as specific rule arguments.
+// sortStringLists sorts lists of string literals used as specific rule or function arguments.
 func sortStringLists(f *File, w *Rewriter) {
 	sortStringList := func(x *Expr) {
 		SortStringList(*x)
@@ -474,13 +474,22 @@
 	Walk(f, func(e Expr, stk []Expr) {
 		switch v := e.(type) {
 		case *CallExpr:
-			if f.Type == TypeDefault || f.Type == TypeBzl {
-				// Rule parameters, not applicable to .bzl or default file types
+			if f.Type == TypeDefault {
+				// Rule parameters, not applicable to default file types
 				return
 			}
 			if leaveAlone(stk, v) {
 				return
 			}
+			if f.Type == TypeBzl {
+				if name, ok := v.X.(*Ident); ok && name.Name == "visibility" {
+					// Call args to the native "visibility function should be sorted per default.
+					for _, arg := range v.List {
+						findAndModifyStrings(&arg, sortStringList)
+					}
+				}
+				return
+			}
 			rule := callName(v)
 			for _, arg := range v.List {
 				if leaveAlone1(arg) {
diff --git a/build/testdata/077.build.golden b/build/testdata/077.build.golden
new file mode 100644
index 0000000..d2d4995
--- /dev/null
+++ b/build/testdata/077.build.golden
@@ -0,0 +1,11 @@
+visibility([
+    "//foo",
+    "//bar",
+])
+
+# Not modified due to leave-alone comment.
+# buildifier: leave-alone
+visibility([
+    "//foo",
+    "//bar",
+])
diff --git a/build/testdata/077.bzl.golden b/build/testdata/077.bzl.golden
new file mode 100644
index 0000000..074abaf
--- /dev/null
+++ b/build/testdata/077.bzl.golden
@@ -0,0 +1,11 @@
+visibility([
+    "//bar",
+    "//foo",
+])
+
+# Not modified due to leave-alone comment.
+# buildifier: leave-alone
+visibility([
+    "//foo",
+    "//bar",
+])
diff --git a/build/testdata/077.default.golden b/build/testdata/077.default.golden
new file mode 100644
index 0000000..d2d4995
--- /dev/null
+++ b/build/testdata/077.default.golden
@@ -0,0 +1,11 @@
+visibility([
+    "//foo",
+    "//bar",
+])
+
+# Not modified due to leave-alone comment.
+# buildifier: leave-alone
+visibility([
+    "//foo",
+    "//bar",
+])
diff --git a/build/testdata/077.in b/build/testdata/077.in
new file mode 100644
index 0000000..25971ce
--- /dev/null
+++ b/build/testdata/077.in
@@ -0,0 +1,11 @@
+visibility([
+    "//foo",
+    "//bar",
+])
+
+# Not modified due to leave-alone comment.
+# buildifier: leave-alone
+visibility([
+    "//foo",
+    "//bar",
+])
\ No newline at end of file