Add ensureX for each message field (#290)


diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md
index 4797d80..4ff3e89 100644
--- a/protobuf/CHANGELOG.md
+++ b/protobuf/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 0.14.4
 
+* Added 'ensureX' methods on GeneratedMessage classes for each message field X.
+
+ The method `ensureX()` will set X to an empty instance if `hasX()` returns false and then returns the value of X.
+
 * Add specialized getters for `String`, `int`, and `bool` with usual default values.
 * Shrink dart2js generated code for `getDefault()`.
 
diff --git a/protobuf/lib/meta.dart b/protobuf/lib/meta.dart
index d8fbf3d..9ccf448 100644
--- a/protobuf/lib/meta.dart
+++ b/protobuf/lib/meta.dart
@@ -59,6 +59,7 @@
   'writeToJson',
   'writeToJsonMap',
   r'$_defaultFor',
+  r'$_ensure',
   r'$_get',
   r'$_getI64',
   r'$_getList',
diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart
index 2e2b12e..d45a97e 100644
--- a/protobuf/lib/src/protobuf/field_set.dart
+++ b/protobuf/lib/src/protobuf/field_set.dart
@@ -379,6 +379,17 @@
     return _getDefault(_nonExtensionInfoByIndex(index));
   }
 
+  T _$ensure<T>(int index) {
+    if (!_$has(index)) {
+      dynamic value = _nonExtensionInfoByIndex(index).subBuilder();
+      _$set(index, value);
+      return value;
+    }
+    // The implicit downcast at the return is always correct by construction
+    // from the protoc generator. See `GeneratedMessage.$_getN` for details.
+    return _$getND(index);
+  }
+
   /// The implementation of a generated getter for repeated fields.
   List<T> _$getList<T>(int index) {
     var value = _values[index];
diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart
index f2f0026..88b54ae 100644
--- a/protobuf/lib/src/protobuf/generated_message.dart
+++ b/protobuf/lib/src/protobuf/generated_message.dart
@@ -402,6 +402,11 @@
   }
 
   /// For generated code only.
+  T $_ensure<T>(int index) {
+    return _fieldSet._$ensure<T>(index);
+  }
+
+  /// For generated code only.
   List<T> $_getList<T>(int index) => _fieldSet._$getList<T>(index);
 
   /// For generated code only.
diff --git a/protobuf/pubspec.yaml b/protobuf/pubspec.yaml
index d3ced8c..4cd2d7e 100644
--- a/protobuf/pubspec.yaml
+++ b/protobuf/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protobuf
-version: 0.14.4
+version: 0.14.4-dev
 author: Dart Team <misc@dartlang.org>
 description: >
   Runtime library for protocol buffers support.
diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md
index 9c631ae..56960e1 100644
--- a/protoc_plugin/CHANGELOG.md
+++ b/protoc_plugin/CHANGELOG.md
@@ -1,7 +1,7 @@
 ## 19.0.0-dev
-
-* Breaking: Add specialized getters for `String`, `int`, and `bool` with usual default values.
-* Breaking: Shrink dart2js generated code for `getDefault()`.
+* Breaking: Generates code that requires at least `protobuf` 0.14.4.
+  - GeneratedMessage classes now have methods `ensureX` for each message field X.
+  - Add specialized getters for `String`, `int`, and `bool` with usual default values.
 
 ## 18.0.2
 
diff --git a/protoc_plugin/lib/message_generator.dart b/protoc_plugin/lib/message_generator.dart
index 8ed90e0..d38f7d8 100644
--- a/protoc_plugin/lib/message_generator.dart
+++ b/protoc_plugin/lib/message_generator.dart
@@ -527,6 +527,17 @@
                 fieldPathSegment: memberFieldPath,
                 start: 'void '.length)
           ]);
+      if (field.baseType.isMessage) {
+        out.printlnAnnotated(
+            '${fieldTypeString} ${names.ensureMethodName}() => '
+            '\$_ensure(${field.index});',
+            <NamedLocation>[
+              NamedLocation(
+                  name: names.ensureMethodName,
+                  fieldPathSegment: memberFieldPath,
+                  start: '${fieldTypeString} '.length)
+            ]);
+      }
     }
   }
 
diff --git a/protoc_plugin/lib/names.dart b/protoc_plugin/lib/names.dart
index 0373187..56bbf74 100644
--- a/protoc_plugin/lib/names.dart
+++ b/protoc_plugin/lib/names.dart
@@ -38,8 +38,13 @@
   /// `null` for repeated fields.
   final String clearMethodName;
 
+  // Identifier for the generated ensureX() method, without braces.
+  //
+  //'null' for scalar, repeated, and map fields.
+  final String ensureMethodName;
+
   FieldNames(this.descriptor, this.index, this.sourcePosition, this.fieldName,
-      {this.hasMethodName, this.clearMethodName});
+      {this.hasMethodName, this.clearMethodName, this.ensureMethodName});
 }
 
 /// The Dart names associated with a oneof declaration.
@@ -359,8 +364,16 @@
   String clearMethod = "clear${_capitalize(name)}";
   checkAvailable(clearMethod);
 
+  String ensureMethod;
+
+  if (_isGroupOrMessage(field)) {
+    ensureMethod = 'ensure${_capitalize(name)}';
+    checkAvailable(ensureMethod);
+  }
   return FieldNames(field, index, sourcePosition, name,
-      hasMethodName: hasMethod, clearMethodName: clearMethod);
+      hasMethodName: hasMethod,
+      clearMethodName: clearMethod,
+      ensureMethodName: ensureMethod);
 }
 
 Iterable<String> _memberNamesSuffix(int number) sync* {
@@ -383,19 +396,27 @@
   }
 
   List<String> generateNameVariants(String name) {
-    return [
+    List<String> result = [
       _defaultFieldName(name),
       _defaultHasMethodName(name),
-      _defaultClearMethodName(name)
+      _defaultClearMethodName(name),
     ];
+
+    // TODO(zarah): Use 'collection if' when sdk dependency is updated.
+    if (_isGroupOrMessage(field)) result.add(_defaultEnsureMethodName(name));
+
+    return result;
   }
 
   String name = disambiguateName(_fieldMethodSuffix(field), existingNames,
       _memberNamesSuffix(field.number),
       generateVariants: generateNameVariants);
+
   return FieldNames(field, index, sourcePosition, _defaultFieldName(name),
       hasMethodName: _defaultHasMethodName(name),
-      clearMethodName: _defaultClearMethodName(name));
+      clearMethodName: _defaultClearMethodName(name),
+      ensureMethodName:
+          _isGroupOrMessage(field) ? _defaultEnsureMethodName(name) : null);
 }
 
 /// The name to use by default for the Dart getter and setter.
@@ -413,6 +434,9 @@
 String _defaultWhichMethodName(String oneofMethodSuffix) =>
     'which$oneofMethodSuffix';
 
+String _defaultEnsureMethodName(String fieldMethodSuffix) =>
+    'ensure$fieldMethodSuffix';
+
 /// The suffix to use for this field in Dart method names.
 /// (It should be camelcase and begin with an uppercase letter.)
 String _fieldMethodSuffix(FieldDescriptorProto field) {
@@ -440,6 +464,10 @@
 bool _isRepeated(FieldDescriptorProto field) =>
     field.label == FieldDescriptorProto_Label.LABEL_REPEATED;
 
+bool _isGroupOrMessage(FieldDescriptorProto field) =>
+    field.type == FieldDescriptorProto_Type.TYPE_MESSAGE ||
+    field.type == FieldDescriptorProto_Type.TYPE_GROUP;
+
 String _nameOption(FieldDescriptorProto field) =>
     field.options.getExtension(Dart_options.dartName);
 
diff --git a/protoc_plugin/test/generated_message_test.dart b/protoc_plugin/test/generated_message_test.dart
index c0db210..d9d0426 100755
--- a/protoc_plugin/test/generated_message_test.dart
+++ b/protoc_plugin/test/generated_message_test.dart
@@ -215,6 +215,13 @@
     assertClear(message);
   });
 
+  test('test ensure method', () {
+    TestAllTypes message = TestAllTypes();
+    expect(message.hasOptionalNestedMessage(), isFalse);
+    expect(message.ensureOptionalNestedMessage(), TestAllTypes_NestedMessage());
+    expect(message.hasOptionalNestedMessage(), isTrue);
+  });
+
   // void testReflectionGetters() {} // UNSUPPORTED -- until reflection
   // void testReflectionSetters() {} // UNSUPPORTED -- until reflection
   // void testReflectionSettersRejectNull() {} // UNSUPPORTED - reflection
diff --git a/protoc_plugin/test/goldens/imports.pb b/protoc_plugin/test/goldens/imports.pb
index e038ee8..00eb417 100644
--- a/protoc_plugin/test/goldens/imports.pb
+++ b/protoc_plugin/test/goldens/imports.pb
@@ -39,15 +39,18 @@
   set m(M v) { setField(1, v); }
   $core.bool hasM() => $_has(0);
   void clearM() => clearField(1);
+  M ensureM() => $_ensure(0);
 
   $1.M get m1 => $_getN(1);
   set m1($1.M v) { setField(2, v); }
   $core.bool hasM1() => $_has(1);
   void clearM1() => clearField(2);
+  $1.M ensureM1() => $_ensure(1);
 
   $2.M get m2 => $_getN(2);
   set m2($2.M v) { setField(3, v); }
   $core.bool hasM2() => $_has(2);
   void clearM2() => clearField(3);
+  $2.M ensureM2() => $_ensure(2);
 }
 
diff --git a/protoc_plugin/test/oneof_test.dart b/protoc_plugin/test/oneof_test.dart
index d59a1cb..f6659cf 100644
--- a/protoc_plugin/test/oneof_test.dart
+++ b/protoc_plugin/test/oneof_test.dart
@@ -171,6 +171,18 @@
     Foo copy2 = foo.copyWith((_) {});
     expectFirstSet(copy2);
   });
+
+  test('oneof semantics is preserved when using ensure method', () {
+    Foo foo = Foo();
+    foo.first = 'oneof';
+    expectFirstSet(foo);
+    foo.ensureIndex();
+    expect(foo.hasFirst(), false);
+    expect(foo.first, '');
+    expect(foo.whichOneofField(), Foo_OneofField.index_);
+    expect(foo.hasIndex(), true);
+    expect(foo.index, Bar());
+  });
 }
 
 void expectSecondSet(Foo foo) {