handle proto3 optional fields (synthetic oneofs)

fixes https://github.com/golang/open2opaque/issues/14

PiperOrigin-RevId: 730841203
Change-Id: Ie204e1ef8852226b4322c7f3b23eeb40d5f09a94
diff --git a/internal/fix/assign.go b/internal/fix/assign.go
index f3dd2f5..ab91d78 100644
--- a/internal/fix/assign.go
+++ b/internal/fix/assign.go
@@ -445,8 +445,8 @@
 	if _, ok := c.Parent().(*dst.BlockStmt); ok {
 		c.Logf("rewriting assignment with rhs Expr")
 		f := c.objectOf(lhsSel.Sel).(*types.Var)
-		isProto2 := !isProto3Field(c.typeOf(lhsSel.X), f.Name())
-		if slice, ok := types.Unalias(f.Type()).(*types.Slice); ok && isProto2 {
+		explicitPresence := fieldHasExplicitPresence(c.typeOf(lhsSel.X), f.Name())
+		if slice, ok := types.Unalias(f.Type()).(*types.Slice); ok && explicitPresence {
 			if basic, ok := types.Unalias(slice.Elem()).(*types.Basic); ok && basic.Kind() == types.Uint8 {
 				if isNeverNilSliceExpr(c, rhs) {
 					stmt := c.expr2stmt(sel2call(c, "Set", lhsSel, rhs, decs), lhsSel)
diff --git a/internal/fix/assign_test.go b/internal/fix/assign_test.go
index 7f72167..fb4c0fe 100644
--- a/internal/fix/assign_test.go
+++ b/internal/fix/assign_test.go
@@ -1583,3 +1583,39 @@
 
 	runTableTest(t, tt)
 }
+
+func TestProto3Optional(t *testing.T) {
+	tests := []test{{
+		desc: "clear all optional fields",
+		in: `
+m := &pb3.M3{}
+m.OptB = nil
+m.OptF32 = nil
+m.OptF64 = nil
+m.OptI32 = nil
+m.OptI64 = nil
+m.OptUi32 = nil
+m.OptUi64 = nil
+m.OptS = nil
+m.OptM = nil
+m.OptE = nil
+`,
+		want: map[Level]string{
+			Green: `
+m := &pb3.M3{}
+m.ClearOptB()
+m.ClearOptF32()
+m.ClearOptF64()
+m.ClearOptI32()
+m.ClearOptI64()
+m.ClearOptUi32()
+m.ClearOptUi64()
+m.ClearOptS()
+m.ClearOptM()
+m.ClearOptE()
+`,
+		},
+	}}
+
+	runTableTests(t, tests)
+}
diff --git a/internal/fix/fixcursor.go b/internal/fix/fixcursor.go
index 9abc0e5..e0bbc34 100644
--- a/internal/fix/fixcursor.go
+++ b/internal/fix/fixcursor.go
@@ -677,9 +677,9 @@
 			return true
 		}
 	}
-	// Handle non-enum proto2 scalars.
-	isProto2 := !isProto3Field(c.typeOf(sel.X), f.Name())
-	if isProto2 {
+
+	// Handle non-enum scalars.
+	if fieldHasExplicitPresence(c.typeOf(sel.X), f.Name()) {
 		switch ft := types.Unalias(f.Type()).(type) {
 		case *types.Pointer:
 			if _, ok := types.Unalias(ft.Elem()).(*types.Basic); ok {
@@ -695,18 +695,21 @@
 	return false
 }
 
-// isProto3Field returns true if given message is a proto3 message based on given field, else false.
-// It uses the "protobuf" struct tag on the field to determine if it contains "proto3" or not.
+// fieldHasExplicitPresence reports whether the specified field has explicit
+// presence: proto2 and edition 2023+ have explicit presence, proto3 defaults
+// to implicit presence, but offers explicit presence through the optional
+// keyword. See also https://protobuf.dev/programming-guides/field_presence/
 //
-// This function considers both value and pointer types to be protocol buffer messages.
-func isProto3Field(m types.Type, field string) bool {
+// This function considers both value and pointer types to be protocol buffer
+// messages.
+func fieldHasExplicitPresence(m types.Type, field string) bool {
 	p, ok := m.Underlying().(*types.Pointer)
 	if ok {
 		m = p.Elem()
 	}
 	s, ok := m.Underlying().(*types.Struct)
 	if !ok {
-		return false
+		return true
 	}
 
 	numFields := s.NumFields()
@@ -720,10 +723,14 @@
 			if i := strings.Index(pb, ",def="); i > -1 {
 				pb = pb[:i]
 			}
-			return strings.Contains(pb, ",proto3")
+			if !strings.Contains(pb, ",proto3") {
+				return true // not proto3? field has explicit presence
+			}
+			// proto3 optional fields are implemented with synthetic oneofs.
+			return strings.Contains(pb, ",oneof")
 		}
 	}
-	return false
+	return true
 }
 
 // Exactly one of expr or t should be non-nil. expr can be nil only if t is *types.Basic
diff --git a/internal/fix/testdata/proto3test_go_proto/proto3test.proto b/internal/fix/testdata/proto3test_go_proto/proto3test.proto
index 414b4b6..6947ca7 100644
--- a/internal/fix/testdata/proto3test_go_proto/proto3test.proto
+++ b/internal/fix/testdata/proto3test_go_proto/proto3test.proto
@@ -38,4 +38,17 @@
     string descriptor = 28;
   }
   int32 second_i32 = 30;
+
+  optional bool opt_b = 31;
+  optional bytes opt_bytes = 32;
+  optional float opt_f32 = 33;
+  optional double opt_f64 = 34;
+  optional int32 opt_i32 = 35;
+  optional int64 opt_i64 = 36;
+  optional uint32 opt_ui32 = 37;
+  optional uint64 opt_ui64 = 38;
+  optional string opt_s = 39;
+  optional M3 opt_m = 40;
+  // Repeated fields and maps cannot be optional.
+  optional Enum opt_e = 41;
 }