reflect/protoreflect: add FieldDescriptor.TextName

Add a new TextName accessor that returns the field name that should
be used for the text format. It is usually just the field name, except:
1) it uses the inlined message name for groups,
2) uses the full name surrounded by brackets for extensions, and
3) strips the "message_set_extension" for well-formed extensions
to the proto1 MessageSet.

We make similar adjustments to the JSONName accessor so that it applies
similar semantics for extensions.

The two changes simplifies all logic that wants the humanly readable
name for a field.

Change-Id: I524b6e017fb955146db81819270fe197f8f97980
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/239838
Reviewed-by: Herbie Ong <herbie@google.com>
diff --git a/encoding/protojson/decode.go b/encoding/protojson/decode.go
index 5ba9ebf..e68a268 100644
--- a/encoding/protojson/decode.go
+++ b/encoding/protojson/decode.go
@@ -184,17 +184,7 @@
 			// The name can either be the JSON name or the proto field name.
 			fd = fieldDescs.ByJSONName(name)
 			if fd == nil {
-				fd = fieldDescs.ByName(pref.Name(name))
-				if fd == nil {
-					// The proto name of a group field is in all lowercase,
-					// while the textual field name is the group message name.
-					gd := fieldDescs.ByName(pref.Name(strings.ToLower(name)))
-					if gd != nil && gd.Kind() == pref.GroupKind && gd.Message().Name() == pref.Name(name) {
-						fd = gd
-					}
-				} else if fd.Kind() == pref.GroupKind && fd.Message().Name() != pref.Name(name) {
-					fd = nil // reset since field name is actually the message name
-				}
+				fd = fieldDescs.ByTextName(name)
 			}
 		}
 		if flags.ProtoLegacy {
diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go
index 58bdebe..7dde32e 100644
--- a/encoding/protojson/encode.go
+++ b/encoding/protojson/encode.go
@@ -198,22 +198,9 @@
 
 	var err error
 	order.RangeFields(fields, order.IndexNameFieldOrder, func(fd pref.FieldDescriptor, v pref.Value) bool {
-		var name string
-		switch {
-		case fd.IsExtension():
-			if messageset.IsMessageSetExtension(fd) {
-				name = "[" + string(fd.FullName().Parent()) + "]"
-			} else {
-				name = "[" + string(fd.FullName()) + "]"
-			}
-		case e.opts.UseProtoNames:
-			if fd.Kind() == pref.GroupKind {
-				name = string(fd.Message().Name())
-			} else {
-				name = string(fd.Name())
-			}
-		default:
-			name = fd.JSONName()
+		name := fd.JSONName()
+		if e.opts.UseProtoNames {
+			name = fd.TextName()
 		}
 
 		if err = e.WriteName(name); err != nil {
diff --git a/encoding/prototext/decode.go b/encoding/prototext/decode.go
index 8cce1e0..e2bbf7c 100644
--- a/encoding/prototext/decode.go
+++ b/encoding/prototext/decode.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"strings"
 	"unicode/utf8"
 
 	"google.golang.org/protobuf/internal/encoding/messageset"
@@ -158,17 +157,7 @@
 		switch tok.NameKind() {
 		case text.IdentName:
 			name = pref.Name(tok.IdentName())
-			fd = fieldDescs.ByName(name)
-			if fd == nil {
-				// The proto name of a group field is in all lowercase,
-				// while the textproto field name is the group message name.
-				gd := fieldDescs.ByName(pref.Name(strings.ToLower(string(name))))
-				if gd != nil && gd.Kind() == pref.GroupKind && gd.Message().Name() == name {
-					fd = gd
-				}
-			} else if fd.Kind() == pref.GroupKind && fd.Message().Name() != name {
-				fd = nil // reset since field name is actually the message name
-			}
+			fd = fieldDescs.ByTextName(string(name))
 
 		case text.TypeName:
 			// Handle extensions only. This code path is not for Any.
diff --git a/encoding/prototext/encode.go b/encoding/prototext/encode.go
index 3171156..8d5304d 100644
--- a/encoding/prototext/encode.go
+++ b/encoding/prototext/encode.go
@@ -172,22 +172,7 @@
 	// Marshal fields.
 	var err error
 	order.RangeFields(m, order.IndexNameFieldOrder, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
-		var name string
-		if fd.IsExtension() {
-			if messageset.IsMessageSetExtension(fd) {
-				name = "[" + string(fd.FullName().Parent()) + "]"
-			} else {
-				name = "[" + string(fd.FullName()) + "]"
-			}
-		} else {
-			if fd.Kind() == pref.GroupKind {
-				name = string(fd.Message().Name())
-			} else {
-				name = string(fd.Name())
-			}
-		}
-
-		if err = e.marshalField(string(name), v, fd); err != nil {
+		if err = e.marshalField(fd.TextName(), v, fd); err != nil {
 			return false
 		}
 		return true
diff --git a/internal/cmd/generate-types/main.go b/internal/cmd/generate-types/main.go
index 0d3e731..d37bc82 100644
--- a/internal/cmd/generate-types/main.go
+++ b/internal/cmd/generate-types/main.go
@@ -100,6 +100,7 @@
 		byName map[protoreflect.Name]*{{$nameDesc}} // protected by once
 		{{- if (eq . "Field")}}
 		byJSON map[string]*{{$nameDesc}}            // protected by once
+		byText map[string]*{{$nameDesc}}            // protected by once
 		{{- end}}
 		{{- if .NumberExpr}}
 		byNum  map[{{.NumberExpr}}]*{{$nameDesc}}   // protected by once
@@ -125,6 +126,12 @@
 		}
 		return nil
 	}
+	func (p *{{$nameList}}) ByTextName(s string) {{.Expr}} {
+		if d := p.lazyInit().byText[s]; d != nil {
+			return d
+		}
+		return nil
+	}
 	{{- end}}
 	{{- if .NumberExpr}}
 	func (p *{{$nameList}}) ByNumber(n {{.NumberExpr}}) {{.Expr}} {
@@ -144,6 +151,7 @@
 				p.byName = make(map[protoreflect.Name]*{{$nameDesc}}, len(p.List))
 				{{- if (eq . "Field")}}
 				p.byJSON = make(map[string]*{{$nameDesc}}, len(p.List))
+				p.byText = make(map[string]*{{$nameDesc}}, len(p.List))
 				{{- end}}
 				{{- if .NumberExpr}}
 				p.byNum = make(map[{{.NumberExpr}}]*{{$nameDesc}}, len(p.List))
@@ -157,6 +165,9 @@
 					if _, ok := p.byJSON[d.JSONName()]; !ok {
 						p.byJSON[d.JSONName()] = d
 					}
+					if _, ok := p.byText[d.TextName()]; !ok {
+						p.byText[d.TextName()] = d
+					}
 					{{- end}}
 					{{- if .NumberExpr}}
 					if _, ok := p.byNum[d.Number()]; !ok {
diff --git a/internal/descfmt/desc_test.go b/internal/descfmt/desc_test.go
index 6d683b2..c9b92f5 100644
--- a/internal/descfmt/desc_test.go
+++ b/internal/descfmt/desc_test.go
@@ -22,6 +22,7 @@
 		"ProtoInternal": true,
 		"ProtoType":     true,
 
+		"TextName":           true, // derived from other fields
 		"HasOptionalKeyword": true, // captured by HasPresence
 		"IsSynthetic":        true, // captured by HasPresence
 
diff --git a/internal/encoding/tag/tag.go b/internal/encoding/tag/tag.go
index 16c02d7..38f1931 100644
--- a/internal/encoding/tag/tag.go
+++ b/internal/encoding/tag/tag.go
@@ -104,7 +104,7 @@
 		case strings.HasPrefix(s, "json="):
 			jsonName := s[len("json="):]
 			if jsonName != strs.JSONCamelCase(string(f.L0.FullName.Name())) {
-				f.L1.JSONName.Init(jsonName)
+				f.L1.StringName.InitJSON(jsonName)
 			}
 		case s == "packed":
 			f.L1.HasPacked = true
diff --git a/internal/filedesc/desc.go b/internal/filedesc/desc.go
index 9385126..12f65f3 100644
--- a/internal/filedesc/desc.go
+++ b/internal/filedesc/desc.go
@@ -13,6 +13,7 @@
 	"google.golang.org/protobuf/internal/descfmt"
 	"google.golang.org/protobuf/internal/descopts"
 	"google.golang.org/protobuf/internal/encoding/defval"
+	"google.golang.org/protobuf/internal/encoding/messageset"
 	"google.golang.org/protobuf/internal/genid"
 	"google.golang.org/protobuf/internal/pragma"
 	"google.golang.org/protobuf/internal/strs"
@@ -207,7 +208,7 @@
 		Number           pref.FieldNumber
 		Cardinality      pref.Cardinality // must be consistent with Message.RequiredNumbers
 		Kind             pref.Kind
-		JSONName         jsonName
+		StringName       stringName
 		IsProto3Optional bool // promoted from google.protobuf.FieldDescriptorProto
 		IsWeak           bool // promoted from google.protobuf.FieldOptions
 		HasPacked        bool // promoted from google.protobuf.FieldOptions
@@ -277,8 +278,9 @@
 func (fd *Field) Number() pref.FieldNumber      { return fd.L1.Number }
 func (fd *Field) Cardinality() pref.Cardinality { return fd.L1.Cardinality }
 func (fd *Field) Kind() pref.Kind               { return fd.L1.Kind }
-func (fd *Field) HasJSONName() bool             { return fd.L1.JSONName.has }
-func (fd *Field) JSONName() string              { return fd.L1.JSONName.get(fd) }
+func (fd *Field) HasJSONName() bool             { return fd.L1.StringName.hasJSON }
+func (fd *Field) JSONName() string              { return fd.L1.StringName.getJSON(fd) }
+func (fd *Field) TextName() string              { return fd.L1.StringName.getText(fd) }
 func (fd *Field) HasPresence() bool {
 	return fd.L1.Cardinality != pref.Repeated && (fd.L0.ParentFile.L1.Syntax == pref.Proto2 || fd.L1.Message != nil || fd.L1.ContainingOneof != nil)
 }
@@ -373,7 +375,7 @@
 	}
 	ExtensionL2 struct {
 		Options          func() pref.ProtoMessage
-		JSONName         jsonName
+		StringName       stringName
 		IsProto3Optional bool // promoted from google.protobuf.FieldDescriptorProto
 		IsPacked         bool // promoted from google.protobuf.FieldOptions
 		Default          defaultValue
@@ -391,8 +393,9 @@
 func (xd *Extension) Number() pref.FieldNumber      { return xd.L1.Number }
 func (xd *Extension) Cardinality() pref.Cardinality { return xd.L1.Cardinality }
 func (xd *Extension) Kind() pref.Kind               { return xd.L1.Kind }
-func (xd *Extension) HasJSONName() bool             { return xd.lazyInit().JSONName.has }
-func (xd *Extension) JSONName() string              { return xd.lazyInit().JSONName.get(xd) }
+func (xd *Extension) HasJSONName() bool             { return xd.lazyInit().StringName.hasJSON }
+func (xd *Extension) JSONName() string              { return xd.lazyInit().StringName.getJSON(xd) }
+func (xd *Extension) TextName() string              { return xd.lazyInit().StringName.getText(xd) }
 func (xd *Extension) HasPresence() bool             { return xd.L1.Cardinality != pref.Repeated }
 func (xd *Extension) HasOptionalKeyword() bool {
 	return (xd.L0.ParentFile.L1.Syntax == pref.Proto2 && xd.L1.Cardinality == pref.Optional) || xd.lazyInit().IsProto3Optional
@@ -506,27 +509,50 @@
 func (d *Base) IsPlaceholder() bool                 { return false }
 func (d *Base) ProtoInternal(pragma.DoNotImplement) {}
 
-type jsonName struct {
-	has  bool
-	once sync.Once
-	name string
+type stringName struct {
+	hasJSON  bool
+	once     sync.Once
+	nameJSON string
+	nameText string
 }
 
-// Init initializes the name. It is exported for use by other internal packages.
-func (js *jsonName) Init(s string) {
-	js.has = true
-	js.name = s
+// InitJSON initializes the name. It is exported for use by other internal packages.
+func (s *stringName) InitJSON(name string) {
+	s.hasJSON = true
+	s.nameJSON = name
 }
 
-func (js *jsonName) get(fd pref.FieldDescriptor) string {
-	if !js.has {
-		js.once.Do(func() {
-			js.name = strs.JSONCamelCase(string(fd.Name()))
-		})
-	}
-	return js.name
+func (s *stringName) lazyInit(fd pref.FieldDescriptor) *stringName {
+	s.once.Do(func() {
+		if fd.IsExtension() {
+			// For extensions, JSON and text are formatted the same way.
+			var name string
+			if messageset.IsMessageSetExtension(fd) {
+				name = string("[" + fd.FullName().Parent() + "]")
+			} else {
+				name = string("[" + fd.FullName() + "]")
+			}
+			s.nameJSON = name
+			s.nameText = name
+		} else {
+			// Format the JSON name.
+			if !s.hasJSON {
+				s.nameJSON = strs.JSONCamelCase(string(fd.Name()))
+			}
+
+			// Format the text name.
+			s.nameText = string(fd.Name())
+			if fd.Kind() == pref.GroupKind {
+				s.nameText = string(fd.Message().Name())
+			}
+		}
+	})
+	return s
 }
 
+func (s *stringName) getJSON(fd pref.FieldDescriptor) string { return s.lazyInit(fd).nameJSON }
+func (s *stringName) getText(fd pref.FieldDescriptor) string { return s.lazyInit(fd).nameText }
+
 func DefaultValue(v pref.Value, ev pref.EnumValueDescriptor) defaultValue {
 	dv := defaultValue{has: v.IsValid(), val: v, enum: ev}
 	if b, ok := v.Interface().([]byte); ok {
diff --git a/internal/filedesc/desc_lazy.go b/internal/filedesc/desc_lazy.go
index e672233..198451e 100644
--- a/internal/filedesc/desc_lazy.go
+++ b/internal/filedesc/desc_lazy.go
@@ -451,7 +451,7 @@
 			case genid.FieldDescriptorProto_Name_field_number:
 				fd.L0.FullName = appendFullName(sb, pd.FullName(), v)
 			case genid.FieldDescriptorProto_JsonName_field_number:
-				fd.L1.JSONName.Init(sb.MakeString(v))
+				fd.L1.StringName.InitJSON(sb.MakeString(v))
 			case genid.FieldDescriptorProto_DefaultValue_field_number:
 				fd.L1.Default.val = pref.ValueOfBytes(v) // temporarily store as bytes; later resolved in resolveMessages
 			case genid.FieldDescriptorProto_TypeName_field_number:
@@ -551,7 +551,7 @@
 			b = b[m:]
 			switch num {
 			case genid.FieldDescriptorProto_JsonName_field_number:
-				xd.L2.JSONName.Init(sb.MakeString(v))
+				xd.L2.StringName.InitJSON(sb.MakeString(v))
 			case genid.FieldDescriptorProto_DefaultValue_field_number:
 				xd.L2.Default.val = pref.ValueOfBytes(v) // temporarily store as bytes; later resolved in resolveExtensions
 			case genid.FieldDescriptorProto_TypeName_field_number:
diff --git a/internal/filedesc/desc_list.go b/internal/filedesc/desc_list.go
index c876cd3..4187d66 100644
--- a/internal/filedesc/desc_list.go
+++ b/internal/filedesc/desc_list.go
@@ -245,6 +245,7 @@
 	once   sync.Once
 	byName map[pref.Name]pref.FieldDescriptor        // protected by once
 	byJSON map[string]pref.FieldDescriptor           // protected by once
+	byText map[string]pref.FieldDescriptor           // protected by once
 	byNum  map[pref.FieldNumber]pref.FieldDescriptor // protected by once
 }
 
@@ -252,6 +253,7 @@
 func (p *OneofFields) Get(i int) pref.FieldDescriptor                   { return p.List[i] }
 func (p *OneofFields) ByName(s pref.Name) pref.FieldDescriptor          { return p.lazyInit().byName[s] }
 func (p *OneofFields) ByJSONName(s string) pref.FieldDescriptor         { return p.lazyInit().byJSON[s] }
+func (p *OneofFields) ByTextName(s string) pref.FieldDescriptor         { return p.lazyInit().byText[s] }
 func (p *OneofFields) ByNumber(n pref.FieldNumber) pref.FieldDescriptor { return p.lazyInit().byNum[n] }
 func (p *OneofFields) Format(s fmt.State, r rune)                       { descfmt.FormatList(s, r, p) }
 func (p *OneofFields) ProtoInternal(pragma.DoNotImplement)              {}
@@ -261,11 +263,13 @@
 		if len(p.List) > 0 {
 			p.byName = make(map[pref.Name]pref.FieldDescriptor, len(p.List))
 			p.byJSON = make(map[string]pref.FieldDescriptor, len(p.List))
+			p.byText = make(map[string]pref.FieldDescriptor, len(p.List))
 			p.byNum = make(map[pref.FieldNumber]pref.FieldDescriptor, len(p.List))
 			for _, f := range p.List {
 				// Field names and numbers are guaranteed to be unique.
 				p.byName[f.Name()] = f
 				p.byJSON[f.JSONName()] = f
+				p.byText[f.TextName()] = f
 				p.byNum[f.Number()] = f
 			}
 		}
diff --git a/internal/filedesc/desc_list_gen.go b/internal/filedesc/desc_list_gen.go
index 6a8825e..30db19f 100644
--- a/internal/filedesc/desc_list_gen.go
+++ b/internal/filedesc/desc_list_gen.go
@@ -142,6 +142,7 @@
 	once   sync.Once
 	byName map[protoreflect.Name]*Field        // protected by once
 	byJSON map[string]*Field                   // protected by once
+	byText map[string]*Field                   // protected by once
 	byNum  map[protoreflect.FieldNumber]*Field // protected by once
 }
 
@@ -163,6 +164,12 @@
 	}
 	return nil
 }
+func (p *Fields) ByTextName(s string) protoreflect.FieldDescriptor {
+	if d := p.lazyInit().byText[s]; d != nil {
+		return d
+	}
+	return nil
+}
 func (p *Fields) ByNumber(n protoreflect.FieldNumber) protoreflect.FieldDescriptor {
 	if d := p.lazyInit().byNum[n]; d != nil {
 		return d
@@ -178,6 +185,7 @@
 		if len(p.List) > 0 {
 			p.byName = make(map[protoreflect.Name]*Field, len(p.List))
 			p.byJSON = make(map[string]*Field, len(p.List))
+			p.byText = make(map[string]*Field, len(p.List))
 			p.byNum = make(map[protoreflect.FieldNumber]*Field, len(p.List))
 			for i := range p.List {
 				d := &p.List[i]
@@ -187,6 +195,9 @@
 				if _, ok := p.byJSON[d.JSONName()]; !ok {
 					p.byJSON[d.JSONName()] = d
 				}
+				if _, ok := p.byText[d.TextName()]; !ok {
+					p.byText[d.TextName()] = d
+				}
 				if _, ok := p.byNum[d.Number()]; !ok {
 					p.byNum[d.Number()] = d
 				}
diff --git a/internal/filedesc/desc_test.go b/internal/filedesc/desc_test.go
index 4aef0fb..e490446 100644
--- a/internal/filedesc/desc_test.go
+++ b/internal/filedesc/desc_test.go
@@ -724,7 +724,7 @@
 			Number:      1000
 			Cardinality: repeated
 			Kind:        message
-			JSONName:    "X"
+			JSONName:    "[test.C.X]"
 			IsExtension: true
 			IsList:      true
 			Extendee:    test.B
@@ -745,7 +745,7 @@
 		Number:      1000
 		Cardinality: repeated
 		Kind:        enum
-		JSONName:    "X"
+		JSONName:    "[test.X]"
 		IsExtension: true
 		IsPacked:    true
 		IsList:      true
diff --git a/internal/impl/legacy_extension.go b/internal/impl/legacy_extension.go
index 61757ce..49e7231 100644
--- a/internal/impl/legacy_extension.go
+++ b/internal/impl/legacy_extension.go
@@ -154,7 +154,8 @@
 func (x placeholderExtension) Cardinality() pref.Cardinality              { return 0 }
 func (x placeholderExtension) Kind() pref.Kind                            { return 0 }
 func (x placeholderExtension) HasJSONName() bool                          { return false }
-func (x placeholderExtension) JSONName() string                           { return "" }
+func (x placeholderExtension) JSONName() string                           { return "[" + string(x.name) + "]" }
+func (x placeholderExtension) TextName() string                           { return "[" + string(x.name) + "]" }
 func (x placeholderExtension) HasPresence() bool                          { return false }
 func (x placeholderExtension) HasOptionalKeyword() bool                   { return false }
 func (x placeholderExtension) IsExtension() bool                          { return true }
diff --git a/internal/msgfmt/format.go b/internal/msgfmt/format.go
index f01cf60..a8a8031 100644
--- a/internal/msgfmt/format.go
+++ b/internal/msgfmt/format.go
@@ -66,12 +66,7 @@
 
 	b = append(b, '{')
 	order.RangeFields(m, order.IndexNameFieldOrder, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
-		k := string(fd.Name())
-		if fd.IsExtension() {
-			k = string("[" + fd.FullName() + "]")
-		}
-
-		b = append(b, k...)
+		b = append(b, fd.TextName()...)
 		b = append(b, ':')
 		b = appendValue(b, v, fd)
 		b = append(b, delim()...)
diff --git a/reflect/protodesc/desc_init.go b/reflect/protodesc/desc_init.go
index 673a230..37efda1 100644
--- a/reflect/protodesc/desc_init.go
+++ b/reflect/protodesc/desc_init.go
@@ -135,7 +135,7 @@
 			f.L1.Kind = protoreflect.Kind(fd.GetType())
 		}
 		if fd.JsonName != nil {
-			f.L1.JSONName.Init(fd.GetJsonName())
+			f.L1.StringName.InitJSON(fd.GetJsonName())
 		}
 	}
 	return fs, nil
@@ -175,7 +175,7 @@
 			x.L1.Kind = protoreflect.Kind(xd.GetType())
 		}
 		if xd.JsonName != nil {
-			x.L2.JSONName.Init(xd.GetJsonName())
+			x.L2.StringName.InitJSON(xd.GetJsonName())
 		}
 	}
 	return xs, nil
diff --git a/reflect/protodesc/desc_validate.go b/reflect/protodesc/desc_validate.go
index 2d5fa99..9af1d56 100644
--- a/reflect/protodesc/desc_validate.go
+++ b/reflect/protodesc/desc_validate.go
@@ -239,6 +239,9 @@
 			return errors.New("extension field %q has an invalid cardinality: %d", x.FullName(), x.Cardinality())
 		}
 		if xd.JsonName != nil {
+			// A bug in older versions of protoc would always populate the
+			// "json_name" option for extensions when it is meaningless.
+			// When it did so, it would always use the camel-cased field name.
 			if xd.GetJsonName() != strs.JSONCamelCase(string(x.Name())) {
 				return errors.New("extension field %q may not have an explicitly set JSON name: %q", x.FullName(), xd.GetJsonName())
 			}
diff --git a/reflect/protodesc/proto.go b/reflect/protodesc/proto.go
index 00d35e0..a1b7c50 100644
--- a/reflect/protodesc/proto.go
+++ b/reflect/protodesc/proto.go
@@ -9,6 +9,7 @@
 	"strings"
 
 	"google.golang.org/protobuf/internal/encoding/defval"
+	"google.golang.org/protobuf/internal/strs"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 
@@ -138,7 +139,14 @@
 		p.TypeName = fullNameOf(field.Message())
 	}
 	if field.HasJSONName() {
-		p.JsonName = proto.String(field.JSONName())
+		// A bug in older versions of protoc would always populate the
+		// "json_name" option for extensions when it is meaningless.
+		// When it did so, it would always use the camel-cased field name.
+		if field.IsExtension() {
+			p.JsonName = proto.String(strs.JSONCamelCase(string(field.Name())))
+		} else {
+			p.JsonName = proto.String(field.JSONName())
+		}
 	}
 	if field.Syntax() == protoreflect.Proto3 && field.HasOptionalKeyword() {
 		p.Proto3Optional = proto.Bool(true)
diff --git a/reflect/protoreflect/type.go b/reflect/protoreflect/type.go
index 5be14a7..58034ef 100644
--- a/reflect/protoreflect/type.go
+++ b/reflect/protoreflect/type.go
@@ -279,8 +279,15 @@
 
 	// JSONName reports the name used for JSON serialization.
 	// It is usually the camel-cased form of the field name.
+	// Extension fields are represented by the full name surrounded by brackets.
 	JSONName() string
 
+	// TextName reports the name used for text serialization.
+	// It is usually the name of the field, except that groups use the name
+	// of the inlined message, and extension fields are represented by the
+	// full name surrounded by brackets.
+	TextName() string
+
 	// HasPresence reports whether the field distinguishes between unpopulated
 	// and default values.
 	HasPresence() bool
@@ -371,6 +378,9 @@
 	// ByJSONName returns the FieldDescriptor for a field with s as the JSON name.
 	// It returns nil if not found.
 	ByJSONName(s string) FieldDescriptor
+	// ByTextName returns the FieldDescriptor for a field with s as the text name.
+	// It returns nil if not found.
+	ByTextName(s string) FieldDescriptor
 	// ByNumber returns the FieldDescriptor for a field numbered n.
 	// It returns nil if not found.
 	ByNumber(n FieldNumber) FieldDescriptor
diff --git a/testing/protocmp/reflect.go b/testing/protocmp/reflect.go
index a4f9cac..5b92cb8 100644
--- a/testing/protocmp/reflect.go
+++ b/testing/protocmp/reflect.go
@@ -42,10 +42,7 @@
 	if m.Descriptor() != fd.ContainingMessage() {
 		panic("mismatching containing message")
 	}
-	if fd.IsExtension() {
-		return string("[" + fd.FullName() + "]")
-	}
-	return string(fd.Name())
+	return fd.TextName()
 }
 
 func (m reflectMessage) Descriptor() protoreflect.MessageDescriptor {
diff --git a/testing/protocmp/util.go b/testing/protocmp/util.go
index 668bb2e..f2175ad 100644
--- a/testing/protocmp/util.go
+++ b/testing/protocmp/util.go
@@ -167,7 +167,7 @@
 
 func mustFindFieldDescriptor(md protoreflect.MessageDescriptor, s protoreflect.Name) protoreflect.FieldDescriptor {
 	d := findDescriptor(md, s)
-	if fd, ok := d.(protoreflect.FieldDescriptor); ok && fd.Name() == s {
+	if fd, ok := d.(protoreflect.FieldDescriptor); ok && fd.TextName() == string(s) {
 		return fd
 	}
 
@@ -199,10 +199,10 @@
 
 func findDescriptor(md protoreflect.MessageDescriptor, s protoreflect.Name) protoreflect.Descriptor {
 	// Exact match.
-	if fd := md.Fields().ByName(s); fd != nil {
+	if fd := md.Fields().ByTextName(string(s)); fd != nil {
 		return fd
 	}
-	if od := md.Oneofs().ByName(s); od != nil {
+	if od := md.Oneofs().ByName(s); od != nil && !od.IsSynthetic() {
 		return od
 	}
 
@@ -293,13 +293,18 @@
 }
 
 func (f *nameFilters) filterFieldName(m Message, k string) bool {
-	if md := m.Descriptor(); md != nil {
-		switch {
-		case protoreflect.Name(k).IsValid():
-			return f.names[md.Fields().ByName(protoreflect.Name(k)).FullName()]
-		case strings.HasPrefix(k, "[") && strings.HasSuffix(k, "]"):
-			return f.names[protoreflect.FullName(k[1:len(k)-1])]
-		}
+	if _, ok := m[k]; !ok {
+		return true // treat missing fields as already filtered
+	}
+	var fd protoreflect.FieldDescriptor
+	switch mt := m[messageTypeKey].(messageType); {
+	case protoreflect.Name(k).IsValid():
+		fd = mt.md.Fields().ByTextName(k)
+	default:
+		fd = mt.xds[k]
+	}
+	if fd != nil {
+		return f.names[fd.FullName()]
 	}
 	return false
 }
@@ -373,9 +378,9 @@
 	var fd protoreflect.FieldDescriptor
 	switch mt := m[messageTypeKey].(messageType); {
 	case protoreflect.Name(k).IsValid():
-		fd = mt.md.Fields().ByName(protoreflect.Name(k))
-	case strings.HasPrefix(k, "[") && strings.HasSuffix(k, "]"):
-		fd = mt.xds[protoreflect.FullName(k[1:len(k)-1])]
+		fd = mt.md.Fields().ByTextName(k)
+	default:
+		fd = mt.xds[k]
 	}
 	if fd == nil || !fd.Default().IsValid() {
 		return false
diff --git a/testing/protocmp/xform.go b/testing/protocmp/xform.go
index 25d1302..d69bfb8 100644
--- a/testing/protocmp/xform.go
+++ b/testing/protocmp/xform.go
@@ -74,7 +74,7 @@
 
 type messageType struct {
 	md  protoreflect.MessageDescriptor
-	xds map[protoreflect.FullName]protoreflect.ExtensionDescriptor
+	xds map[string]protoreflect.ExtensionDescriptor
 }
 
 func (t messageType) String() string {
@@ -218,14 +218,13 @@
 
 func transformMessage(m protoreflect.Message) Message {
 	mx := Message{}
-	mt := messageType{md: m.Descriptor(), xds: make(map[protoreflect.FullName]protoreflect.FieldDescriptor)}
+	mt := messageType{md: m.Descriptor(), xds: make(map[string]protoreflect.FieldDescriptor)}
 
 	// Handle known and extension fields.
 	m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
-		s := string(fd.Name())
+		s := fd.TextName()
 		if fd.IsExtension() {
-			s = "[" + string(fd.FullName()) + "]"
-			mt.xds[fd.FullName()] = fd
+			mt.xds[s] = fd
 		}
 		switch {
 		case fd.IsList():