proto: add TestNil test

TestNil checks for panic behavior for all top-level functions that
accept messages to ensure that we can detect when behavior changes
whether accidentally or deliberately.

This test discovered that the pure protobuf reflect behavior
differs with the fast-path implementation for a few cases.

For the protobuf reflection implementation, we explicitly check
for invalid messages in Merge and Reset. Previously, these would
not panic if the message was already empty (i.e., had no need to
actually set/clear any fields in the receiver message).

Change-Id: I571c0a819366bae993ce7c99b05fb4707e55cd3e
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/220958
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/proto/merge.go b/proto/merge.go
index 16a79d6..df72f98 100644
--- a/proto/merge.go
+++ b/proto/merge.go
@@ -68,6 +68,10 @@
 		}
 	}
 
+	if !dst.IsValid() {
+		panic("cannot merge into invalid destination message")
+	}
+
 	src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
 		switch {
 		case fd.IsList():
diff --git a/proto/nil_test.go b/proto/nil_test.go
new file mode 100644
index 0000000..06e76ae
--- /dev/null
+++ b/proto/nil_test.go
@@ -0,0 +1,168 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package proto_test
+
+import (
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+
+	testpb "google.golang.org/protobuf/internal/testprotos/test"
+)
+
+// TestNil tests for boundary conditions when nil and typed-nil messages
+// are passed to various top-level functions.
+// These tests are not necessarily a statement of proper behavior,
+// but exist to detect accidental changes in behavior.
+func TestNil(t *testing.T) {
+	nilMsg := (*testpb.TestAllExtensions)(nil)
+	extType := testpb.E_OptionalBool
+
+	tests := []struct {
+		label string
+		test  func()
+		panic bool
+	}{{
+		label: "Size",
+		test:  func() { proto.Size(nil) },
+		panic: true,
+	}, {
+		label: "Size",
+		test:  func() { proto.Size(nilMsg) },
+	}, {
+		label: "Marshal",
+		test:  func() { proto.Marshal(nil) },
+		panic: true,
+	}, {
+		label: "Marshal",
+		test:  func() { proto.Marshal(nilMsg) },
+	}, {
+		label: "Unmarshal",
+		test:  func() { proto.Unmarshal(nil, nil) },
+		panic: true,
+	}, {
+		label: "Unmarshal",
+		test:  func() { proto.Unmarshal(nil, nilMsg) },
+		panic: true,
+	}, {
+		label: "Merge",
+		test:  func() { proto.Merge(nil, nil) },
+		panic: true,
+	}, {
+		label: "Merge",
+		test:  func() { proto.Merge(nil, nilMsg) },
+		panic: true,
+	}, {
+		label: "Merge",
+		test:  func() { proto.Merge(nilMsg, nil) },
+		panic: true,
+	}, {
+		label: "Merge",
+		test:  func() { proto.Merge(nilMsg, nilMsg) },
+		panic: true,
+	}, {
+		label: "Clone",
+		test:  func() { proto.Clone(nil) },
+	}, {
+		label: "Clone",
+		test:  func() { proto.Clone(nilMsg) },
+	}, {
+		label: "Equal",
+		test:  func() { proto.Equal(nil, nil) },
+	}, {
+		label: "Equal",
+		test:  func() { proto.Equal(nil, nilMsg) },
+	}, {
+		label: "Equal",
+		test:  func() { proto.Equal(nilMsg, nil) },
+	}, {
+		label: "Equal",
+		test:  func() { proto.Equal(nilMsg, nilMsg) },
+	}, {
+		label: "Reset",
+		test:  func() { proto.Reset(nil) },
+		panic: true,
+	}, {
+		label: "Reset",
+		test:  func() { proto.Reset(nilMsg) },
+		panic: true,
+	}, {
+		label: "HasExtension",
+		test:  func() { proto.HasExtension(nil, nil) },
+		panic: true,
+	}, {
+		label: "HasExtension",
+		test:  func() { proto.HasExtension(nil, extType) },
+		panic: true,
+	}, {
+		label: "HasExtension",
+		test:  func() { proto.HasExtension(nilMsg, nil) },
+		panic: true,
+	}, {
+		label: "HasExtension",
+		test:  func() { proto.HasExtension(nilMsg, extType) },
+	}, {
+		label: "GetExtension",
+		test:  func() { proto.GetExtension(nil, nil) },
+		panic: true,
+	}, {
+		label: "GetExtension",
+		test:  func() { proto.GetExtension(nil, extType) },
+		panic: true,
+	}, {
+		label: "GetExtension",
+		test:  func() { proto.GetExtension(nilMsg, nil) },
+		panic: true,
+	}, {
+		label: "GetExtension",
+		test:  func() { proto.GetExtension(nilMsg, extType) },
+	}, {
+		label: "SetExtension",
+		test:  func() { proto.SetExtension(nil, nil, true) },
+		panic: true,
+	}, {
+		label: "SetExtension",
+		test:  func() { proto.SetExtension(nil, extType, true) },
+		panic: true,
+	}, {
+		label: "SetExtension",
+		test:  func() { proto.SetExtension(nilMsg, nil, true) },
+		panic: true,
+	}, {
+		label: "SetExtension",
+		test:  func() { proto.SetExtension(nilMsg, extType, true) },
+		panic: true,
+	}, {
+		label: "ClearExtension",
+		test:  func() { proto.ClearExtension(nil, nil) },
+		panic: true,
+	}, {
+		label: "ClearExtension",
+		test:  func() { proto.ClearExtension(nil, extType) },
+		panic: true,
+	}, {
+		label: "ClearExtension",
+		test:  func() { proto.ClearExtension(nilMsg, nil) },
+		panic: true,
+	}, {
+		label: "ClearExtension",
+		test:  func() { proto.ClearExtension(nilMsg, extType) },
+		panic: true,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.label, func(t *testing.T) {
+			defer func() {
+				switch gotPanic := recover() != nil; {
+				case gotPanic && !tt.panic:
+					t.Errorf("unexpected panic")
+				case !gotPanic && tt.panic:
+					t.Errorf("expected panic")
+				}
+			}()
+			tt.test()
+		})
+	}
+}
diff --git a/proto/reset.go b/proto/reset.go
index 57bbc3a..22048bc 100644
--- a/proto/reset.go
+++ b/proto/reset.go
@@ -18,6 +18,10 @@
 }
 
 func resetMessage(m protoreflect.Message) {
+	if !m.IsValid() {
+		panic("cannot reset invalid message")
+	}
+
 	// Clear all known fields.
 	fds := m.Descriptor().Fields()
 	for i := 0; i < fds.Len(); i++ {