setapi: avoid duplicate setFileAPI() calls for clear log output

Before this change, the cleanup() function would unconditionally call setFileAPI.
That is not incorrect (ultimately a no-op), but results in confusing log output.

PiperOrigin-RevId: 707512218
Change-Id: Id1db4f53a7043f6c6b10500345faf809791bd714
Reviewed-on: https://go-review.googlesource.com/c/open2opaque/+/637356
Reviewed-by: Chressie Himpel <chressie@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Stapelberg <stapelberg@google.com>
Auto-Submit: Michael Stapelberg <stapelberg@google.com>
diff --git a/internal/o2o/setapi/setapi.go b/internal/o2o/setapi/setapi.go
index 49e8c2c..32d48d9 100644
--- a/internal/o2o/setapi/setapi.go
+++ b/internal/o2o/setapi/setapi.go
@@ -598,6 +598,29 @@
 	// Cleanup 1: If all messages are on the same API level, flip the file API
 	// level to that value. If said level is the default for the path, don't use
 	// an explicit flag.
+	var err error
+	content, err = cleanupAllMsgsToFile(path, content)
+	if err != nil {
+		return nil, err
+	}
+
+	// Cleanup 2: Remove the API flag from all messages that have the same API
+	// level as their parents. The parent is usually the file API level, but in
+	// case of editions proto that use the new edition feature, the parent of
+	// a nested message is its parent message.
+	//
+	// Parse again after the previous cleanup might have modified content.
+	content, err = cleanupSameAsParent(path, content)
+	if err != nil {
+		return nil, err
+	}
+
+	// Cleanup 3: ensure the go_features.proto import is present if and only if
+	// any parts of the file set the features.(pb.go).api_level option.
+	return cleanupGoFeaturesImport(path, content)
+}
+
+func cleanupAllMsgsToFile(path string, content []byte) ([]byte, error) {
 	fopt, err := parse(path, content, false)
 	if err != nil {
 		return nil, err
@@ -609,25 +632,29 @@
 			return nil
 		})
 	}
-	if len(apiMap) == 1 {
-		// All messages have same API level.
-		// Consider cleanup as a nice to have, don't error out on leading-comment
-		// exemptions.
-		const errorOnExempt = false
-		const skipCleanup = false
-		content, err = setFileAPI(path, content, fopt.MessageOpts[0].GoAPI, skipCleanup, errorOnExempt)
-		if err != nil {
-			return nil, fmt.Errorf("setFileAPI: %v", err)
-		}
+	if len(apiMap) != 1 {
+		// Not all messages are on the same API level.
+		return content, nil
 	}
+	targetAPI := fopt.MessageOpts[0].GoAPI
+	if fopt.GoAPI == targetAPI {
+		// File is already on the target API level, nothing to do.
+		return content, nil
+	}
+	// All messages have same API level.
+	// Consider cleanup as a nice to have, don't error out on leading-comment
+	// exemptions.
+	const errorOnExempt = false
+	const skipCleanup = false
+	content, err = setFileAPI(path, content, targetAPI, skipCleanup, errorOnExempt)
+	if err != nil {
+		return nil, fmt.Errorf("setFileAPI: %v", err)
+	}
+	return content, nil
+}
 
-	// Cleanup 2: Remove the API flag from all messages that have the same API
-	// level as their parents. The parent is usually the file API level, but in
-	// case of editions proto that use the new edition feature, the parent of
-	// a nested message is its parent message.
-	//
-	// Parse again after the previous cleanup might have modified content.
-	fopt, err = parse(path, content, false)
+func cleanupSameAsParent(path string, content []byte) ([]byte, error) {
+	fopt, err := parse(path, content, false)
 	if err != nil {
 		return nil, err
 	}
@@ -666,24 +693,23 @@
 			return nil, err
 		}
 	}
-	if len(removeByteRanges) > 0 {
-		slices.SortFunc(removeByteRanges, func(a, b byteRange) int {
-			return cmp.Compare(a.from, b.from)
-		})
-		for i := 1; i < len(removeByteRanges); i++ {
-			if removeByteRanges[i].from < removeByteRanges[i-1].to {
-				return nil, fmt.Errorf("text ranges overlap")
-			}
-		}
+	if len(removeByteRanges) == 0 {
+		return content, nil
+	}
 
-		for _, br := range slices.Backward(removeByteRanges) {
-			content = slices.Delete(content, br.from, br.to)
+	slices.SortFunc(removeByteRanges, func(a, b byteRange) int {
+		return cmp.Compare(a.from, b.from)
+	})
+	for i := 1; i < len(removeByteRanges); i++ {
+		if removeByteRanges[i].from < removeByteRanges[i-1].to {
+			return nil, fmt.Errorf("text ranges overlap")
 		}
 	}
 
-	// Cleanup 3: ensure the go_features.proto import is present if and only if
-	// any parts of the file set the features.(pb.go).api_level option.
-	return cleanupGoFeaturesImport(path, content)
+	for _, br := range slices.Backward(removeByteRanges) {
+		content = slices.Delete(content, br.from, br.to)
+	}
+	return content, nil
 }
 
 func insertLine(content []byte, insertLine string, ln int32) []byte {