Resolving names with underscores (#139)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 650143a..3f25578 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+## 0.12.0
+
+* Breaking change: Handle identifiers starting with a leading underscore.
+  This covers message names, enum names, enum value identifiers and file names.
+
+  Before, these would appear in the generated Dart code as private identifiers.
+  Now the underscore is moved to the end.
+
+  Field names and extension field names already had all underscores removed, and these are not
+  affected by this change.
+
+  If there is a conflicting name with a trailing underscore defined later in the same scope, a
+  disambiguation will happen that can potentially lead to existing identifiers getting a new name in
+  the generated Dart.
+
+  For example:
+
+  ```
+  message _Foo {}
+  message Foo_ {}
+  ```
+
+  `_Foo` will get the name `Foo_` and `Foo_` will now end up being called `Foo__`.
+
 ## 0.11.0
 
 * Breaking change: Support for [map fields](https://developers.google.com/protocol-buffers/docs/proto3#maps)
diff --git a/Makefile b/Makefile
index e75116c..6cc1bf2 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,7 @@
 BENCHMARK_PROTOS = $(wildcard benchmark/protos/*.proto)
 
 TEST_PROTO_LIST = \
+	_leading_underscores \
 	google/protobuf/any \
 	google/protobuf/unittest_import \
 	google/protobuf/unittest_optimize_for \
diff --git a/lib/client_generator.dart b/lib/client_generator.dart
index 63376de..309b121 100644
--- a/lib/client_generator.dart
+++ b/lib/client_generator.dart
@@ -7,14 +7,20 @@
 class ClientApiGenerator {
   // The service that this Client API connects to.
   final ServiceGenerator service;
+  final String className;
+  final Set<String> usedMethodNames = new Set<String>()
+    ..addAll(reservedMemberNames);
 
-  ClientApiGenerator(this.service);
+  ClientApiGenerator(this.service, Set<String> usedNames)
+      : className = disambiguateName(
+            avoidInitialUnderscore(service._descriptor.name),
+            usedNames,
+            defaultSuffixes());
 
   // Subclasses can override this.
   String get _clientType => '$_protobufImportPrefix.RpcClient';
 
   void generate(IndentingWriter out) {
-    var className = service._descriptor.name;
     out.addBlock('class ${className}Api {', '}', () {
       out.println('$_clientType _client;');
       out.println('${className}Api(this._client);');
@@ -29,7 +35,10 @@
 
   // Subclasses can override this.
   void generateMethod(IndentingWriter out, MethodDescriptorProto m) {
-    var methodName = service._methodName(m.name);
+    var methodName = disambiguateName(
+        avoidInitialUnderscore(service._methodName(m.name)),
+        usedMethodNames,
+        defaultSuffixes());
     var inputType = service._getDartClassName(m.inputType);
     var outputType = service._getDartClassName(m.outputType);
     out.addBlock(
@@ -37,8 +46,7 @@
         '$_protobufImportPrefix.ClientContext ctx, $inputType request) {',
         '}', () {
       out.println('var emptyResponse = new $outputType();');
-      out.println(
-          'return _client.invoke<$outputType>(ctx, \'${service._descriptor.name}\', '
+      out.println('return _client.invoke<$outputType>(ctx, \'${className}\', '
           '\'${m.name}\', request, emptyResponse);');
     });
   }
diff --git a/lib/enum_generator.dart b/lib/enum_generator.dart
index 225cae6..2ff355c 100644
--- a/lib/enum_generator.dart
+++ b/lib/enum_generator.dart
@@ -19,15 +19,20 @@
       <EnumValueDescriptorProto>[];
   final List<EnumAlias> _aliases = <EnumAlias>[];
 
-  EnumGenerator(EnumDescriptorProto descriptor, ProtobufContainer parent)
+  /// Maps the name of an enum value to the Dart name we will use for it.
+  final Map<String, String> dartNames = <String, String>{};
+
+  EnumGenerator(EnumDescriptorProto descriptor, ProtobufContainer parent,
+      Set<String> usedClassNames)
       : assert(parent != null),
         _parent = parent,
-        classname = messageOrEnumClassName(descriptor.name,
+        classname = messageOrEnumClassName(descriptor.name, usedClassNames,
             parent: parent?.classname ?? ''),
         fullName = parent.fullName == ''
             ? descriptor.name
             : '${parent.fullName}.${descriptor.name}',
         _descriptor = descriptor {
+    final usedNames = reservedEnumNames;
     for (EnumValueDescriptorProto value in descriptor.value) {
       EnumValueDescriptorProto canonicalValue =
           descriptor.value.firstWhere((v) => v.number == value.number);
@@ -36,6 +41,8 @@
       } else {
         _aliases.add(new EnumAlias(value, canonicalValue));
       }
+      dartNames[value.name] = disambiguateName(
+          avoidInitialUnderscore(value.name), usedNames, enumSuffixes());
     }
   }
 
@@ -63,27 +70,25 @@
         '}\n', () {
       // -----------------------------------------------------------------
       // Define enum types.
-      var reservedNames = reservedEnumNames;
       for (EnumValueDescriptorProto val in _canonicalValues) {
-        final name = unusedEnumNames(val.name, reservedNames);
+        final name = dartNames[val.name];
         out.println('static const ${classname} $name = '
-            "const ${classname}._(${val.number}, '$name');");
+            "const ${classname}._(${val.number}, ${singleQuote(name)});");
       }
       if (_aliases.isNotEmpty) {
         out.println();
         for (EnumAlias alias in _aliases) {
-          final name = unusedEnumNames(alias.value.name, reservedNames);
+          final name = dartNames[alias.value.name];
           out.println('static const ${classname} $name ='
-              ' ${alias.canonicalValue.name};');
+              ' ${dartNames[alias.canonicalValue.name]};');
         }
       }
       out.println();
 
       out.println('static const List<${classname}> values ='
           ' const <${classname}> [');
-      reservedNames = reservedEnumNames;
       for (EnumValueDescriptorProto val in _canonicalValues) {
-        final name = unusedEnumNames(val.name, reservedNames);
+        final name = dartNames[val.name];
         out.println('  $name,');
       }
       out.println('];');
@@ -95,7 +100,7 @@
           ' _byValue[value];');
       out.addBlock('static void $checkItem($classname v) {', '}', () {
         out.println('if (v is! $classname)'
-            " $_protobufImportPrefix.checkItemFailed(v, '$classname');");
+            " $_protobufImportPrefix.checkItemFailed(v, ${singleQuote(classname)});");
       });
       out.println();
 
diff --git a/lib/extension_generator.dart b/lib/extension_generator.dart
index 352e0f4..aca8255 100644
--- a/lib/extension_generator.dart
+++ b/lib/extension_generator.dart
@@ -10,13 +10,13 @@
 
   // populated by resolve()
   ProtobufField _field;
-  String _extensionName;
+  final String _extensionName;
   String _extendedFullName = "";
 
-  ExtensionGenerator(this._descriptor, this._parent);
+  ExtensionGenerator(this._descriptor, this._parent, Set<String> usedNames)
+      : _extensionName = extensionName(_descriptor, usedNames);
 
   void resolve(GenerationContext ctx) {
-    _extensionName = extensionName(_descriptor);
     _field = new ProtobufField.extension(_descriptor, _parent, ctx);
 
     ProtobufContainer extendedType = ctx.getFieldType(_descriptor.extendee);
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index 0b9d2a3..c9940c6 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -104,6 +104,19 @@
   final serviceGenerators = <ServiceGenerator>[];
   final grpcGenerators = <GrpcServiceGenerator>[];
 
+  /// Used to avoid collisions after names have been mangled to match the Dart
+  /// style.
+  final Set<String> usedTopLevelNames = Set<String>()
+    ..addAll(toplevelReservedCapitalizedNames);
+
+  /// Used to avoid collisions in the service file after names have been mangled
+  /// to match the dart style.
+  final Set<String> usedTopLevelServiceNames = Set<String>()
+    ..addAll(toplevelReservedCapitalizedNames);
+
+  final Set<String> usedExtensionNames = Set<String>()
+    ..addAll(forbiddenExtensionNames);
+
   /// True if cross-references have been resolved.
   bool _linked = false;
 
@@ -126,22 +139,25 @@
 
     // Load and register all enum and message types.
     for (EnumDescriptorProto enumType in descriptor.enumType) {
-      enumGenerators.add(new EnumGenerator(enumType, this));
+      enumGenerators.add(new EnumGenerator(enumType, this, usedTopLevelNames));
     }
     for (DescriptorProto messageType in descriptor.messageType) {
       messageGenerators.add(new MessageGenerator(
-          messageType, this, declaredMixins, defaultMixin));
+          messageType, this, declaredMixins, defaultMixin, usedTopLevelNames));
     }
     for (FieldDescriptorProto extension in descriptor.extension) {
-      extensionGenerators.add(new ExtensionGenerator(extension, this));
+      extensionGenerators
+          .add(new ExtensionGenerator(extension, this, usedExtensionNames));
     }
     for (ServiceDescriptorProto service in descriptor.service) {
       if (options.useGrpc) {
         grpcGenerators.add(new GrpcServiceGenerator(service, this));
       } else {
-        var serviceGen = new ServiceGenerator(service, this);
+        var serviceGen =
+            new ServiceGenerator(service, this, usedTopLevelServiceNames);
         serviceGenerators.add(serviceGen);
-        clientApiGenerators.add(new ClientApiGenerator(serviceGen));
+        clientApiGenerators
+            .add(new ClientApiGenerator(serviceGen, usedTopLevelNames));
       }
     }
   }
@@ -210,7 +226,7 @@
     // name derived from the file name.
     if (extensionGenerators.isNotEmpty) {
       // TODO(antonm): do not generate a class.
-      String className = extensionClassName(descriptor);
+      String className = extensionClassName(descriptor, usedTopLevelNames);
       out.addBlock('class $className {', '}\n', () {
         for (ExtensionGenerator x in extensionGenerators) {
           x.generate(out);
diff --git a/lib/message_generator.dart b/lib/message_generator.dart
index 365953a..5c9c072 100644
--- a/lib/message_generator.dart
+++ b/lib/message_generator.dart
@@ -60,11 +60,15 @@
   // populated by resolve()
   List<ProtobufField> _fieldList;
 
-  MessageGenerator(DescriptorProto descriptor, ProtobufContainer parent,
-      Map<String, PbMixin> declaredMixins, PbMixin defaultMixin)
+  MessageGenerator(
+      DescriptorProto descriptor,
+      ProtobufContainer parent,
+      Map<String, PbMixin> declaredMixins,
+      PbMixin defaultMixin,
+      Set<String> usedNames)
       : _descriptor = descriptor,
         _parent = parent,
-        classname = messageOrEnumClassName(descriptor.name,
+        classname = messageOrEnumClassName(descriptor.name, usedNames,
             parent: parent?.classname ?? ''),
         assert(parent != null),
         fullName = parent.fullName == ''
@@ -73,16 +77,16 @@
         mixin = _getMixin(descriptor, parent.fileGen.descriptor, declaredMixins,
             defaultMixin) {
     for (EnumDescriptorProto e in _descriptor.enumType) {
-      _enumGenerators.add(new EnumGenerator(e, this));
+      _enumGenerators.add(new EnumGenerator(e, this, usedNames));
     }
 
     for (DescriptorProto n in _descriptor.nestedType) {
-      _messageGenerators
-          .add(new MessageGenerator(n, this, declaredMixins, defaultMixin));
+      _messageGenerators.add(new MessageGenerator(
+          n, this, declaredMixins, defaultMixin, usedNames));
     }
 
     for (FieldDescriptorProto x in _descriptor.extension) {
-      _extensionGenerators.add(new ExtensionGenerator(x, this));
+      _extensionGenerators.add(new ExtensionGenerator(x, this, usedNames));
     }
   }
 
diff --git a/lib/names.dart b/lib/names.dart
index e5dab99..a974bec 100644
--- a/lib/names.dart
+++ b/lib/names.dart
@@ -32,52 +32,57 @@
   /// `null` for repeated fields.
   final String clearMethodName;
 
-  MemberNames(this.descriptor, this.index, this.fieldName, this.hasMethodName,
-      this.clearMethodName);
+  MemberNames(this.descriptor, this.index, this.fieldName,
+      {this.hasMethodName, this.clearMethodName});
+}
 
-  MemberNames.forRepeatedField(this.descriptor, this.index, this.fieldName)
-      : hasMethodName = null,
-        clearMethodName = null;
+/// Move any initial underscores in [input] to the end.
+///
+/// According to the spec identifiers cannot start with _, but it seems to be
+/// accepted by protoc.
+///
+/// These identifiers are private in Dart, so they have to be transformed.
+String avoidInitialUnderscore(String input) {
+  while (input.startsWith('_')) {
+    input = '${input.substring(1)}_';
+  }
+  return input;
+}
+
+/// Returns [input] surrounded by single quotes and with all '$'s escaped.
+String singleQuote(String input) {
+  return "'${input.replaceAll(r'$', r'\$')}'";
 }
 
 /// Chooses the Dart name of an extension.
-String extensionName(FieldDescriptorProto descriptor) {
-  var existingNames = new Set<String>()
-    ..addAll(_dartReservedWords)
-    ..addAll(GeneratedMessage_reservedNames)
-    ..addAll(_generatedMessageNames);
-  return _unusedMemberNames(descriptor, null, existingNames).fieldName;
+String extensionName(FieldDescriptorProto descriptor, Set<String> usedNames) {
+  return _unusedMemberNames(descriptor, null, usedNames).fieldName;
+}
+
+Iterable<String> extensionSuffixes() sync* {
+  yield "Ext";
+  int i = 2;
+  while (true) {
+    yield '$i';
+    i++;
+  }
+}
+
+/// Replaces all characters in [imput] that are not valid in a dart identifier
+/// with _.
+///
+/// This function does not take care of leading underscores.
+String legalDartIdentifier(String imput) {
+  return imput.replaceAll(new RegExp(r'[^a-zA-Z0-9$_]'), '_');
 }
 
 /// Chooses the name of the Dart class holding top-level extensions.
-String extensionClassName(FileDescriptorProto descriptor) {
-  var taken = new Set<String>();
-  for (var messageType in descriptor.messageType) {
-    taken.add(messageOrEnumClassName(messageType.name));
-  }
-  for (var enumType in descriptor.enumType) {
-    taken.add(enumType.name);
-  }
-
-  String s = _fileNameWithoutExtension(descriptor).replaceAll('-', '_');
+String extensionClassName(
+    FileDescriptorProto descriptor, Set<String> usedNames) {
+  String s = avoidInitialUnderscore(
+      legalDartIdentifier(_fileNameWithoutExtension(descriptor)));
   String candidate = '${s[0].toUpperCase()}${s.substring(1)}';
-
-  if (!taken.contains(candidate)) {
-    return candidate;
-  }
-
-  // Found a conflict; try again.
-  candidate = "${candidate}Ext";
-  if (!taken.contains(candidate)) {
-    return candidate;
-  }
-
-  // Next, try numbers.
-  int suffix = 2;
-  while (taken.contains("$candidate$suffix")) {
-    suffix++;
-  }
-  return "$candidate$suffix";
+  return disambiguateName(candidate, usedNames, extensionSuffixes());
 }
 
 String _fileNameWithoutExtension(FileDescriptorProto descriptor) {
@@ -94,24 +99,61 @@
   String toString() => "$message";
 }
 
+/// Returns a [name] that is not contained in [usedNames] by suffixing it with
+/// the first possible suffix from [suffixes].
+///
+/// The chosen name is added to [usedNames].
+///
+/// If [variants] is given, all the variants of a name must be available before
+/// that name is chosen, and all the chosen variants will be added to
+/// [usedNames].
+/// The returned name is that, which will generate the accepted variants.
+String disambiguateName(
+    String name, Set<String> usedNames, Iterable<String> suffixes,
+    {List<String> Function(String candidate) generateVariants}) {
+  generateVariants ??= (String name) => <String>[name];
+
+  bool allVariantsAvailable(List<String> variants) {
+    return variants.every((String variant) => !usedNames.contains(variant));
+  }
+
+  String usedSuffix = '';
+  List<String> candidateVariants = generateVariants(name);
+
+  if (!allVariantsAvailable(candidateVariants)) {
+    for (String suffix in suffixes) {
+      candidateVariants = generateVariants('$name$suffix');
+      if (allVariantsAvailable(candidateVariants)) {
+        usedSuffix = suffix;
+        break;
+      }
+    }
+  }
+
+  usedNames.addAll(candidateVariants);
+  return '$name$usedSuffix';
+}
+
+Iterable<String> defaultSuffixes() sync* {
+  yield '_';
+  int i = 0;
+  while (true) {
+    yield ('_$i');
+    i++;
+  }
+}
+
 /// Chooses the name of the Dart class to generate for a proto message or enum.
 ///
 /// For a nested message or enum, [parent] should be provided
 /// with the name of the Dart class for the immediate parent.
-String messageOrEnumClassName(String descriptorName, {String parent = ''}) {
-  var name = descriptorName;
+String messageOrEnumClassName(String descriptorName, Set<String> usedNames,
+    {String parent = ''}) {
   if (parent != '') {
-    name = '${parent}_${descriptorName}';
+    descriptorName = '${parent}_${descriptorName}';
   }
-  if (name == 'Function') {
-    name = 'Function_'; // Avoid reserved word.
-  } else if (name == 'List') {
-    name = 'List_';
-  } else if (name.startsWith('Function_')) {
-    // Avoid any further name conflicts due to 'Function' rename (unlikely).
-    name = name + '_';
-  }
-  return name;
+  return disambiguateName(
+      avoidInitialUnderscore(descriptorName), usedNames, defaultSuffixes());
 }
 
 /// Returns the set of names reserved by the ProtobufEnum class and its
@@ -120,18 +162,12 @@
   ..addAll(ProtobufEnum_reservedNames)
   ..addAll(_protobufEnumNames);
 
-/// Chooses the ProtobufEnum names for each value.
-///
-/// Since the values all have the same type as the containing enum class, it
-/// does not require all the same checks as for message member names. Still,
-/// it needs to be checked against a list of reserved names to avoid collisions.
-String unusedEnumNames(String name, Set<String> existingNames) {
-  final suffix = '_';
-  while (existingNames.contains(name)) {
-    name += suffix;
+Iterable<String> enumSuffixes() sync* {
+  String s = '_';
+  while (true) {
+    yield s;
+    s += '_';
   }
-  existingNames.add(name);
-  return name;
 }
 
 /// Chooses the GeneratedMessage member names for each field.
@@ -161,9 +197,7 @@
   }
 
   var existingNames = new Set<String>()
-    ..addAll(_dartReservedWords)
-    ..addAll(GeneratedMessage_reservedNames)
-    ..addAll(_generatedMessageNames)
+    ..addAll(reservedMemberNames)
     ..addAll(reserved);
 
   var memberNames = <String, MemberNames>{};
@@ -234,7 +268,7 @@
   checkAvailable(name);
 
   if (_isRepeated(field)) {
-    return new MemberNames.forRepeatedField(field, index, name);
+    return new MemberNames(field, index, name);
   }
 
   String hasMethod = "has${_capitalize(name)}";
@@ -243,47 +277,55 @@
   String clearMethod = "clear${_capitalize(name)}";
   checkAvailable(clearMethod);
 
-  return new MemberNames(field, index, name, hasMethod, clearMethod);
+  return new MemberNames(field, index, name,
+      hasMethodName: hasMethod, clearMethodName: clearMethod);
+}
+
+Iterable<String> _memberNamesSuffix(int number) sync* {
+  String suffix = '_$number';
+  while (true) {
+    yield suffix;
+    suffix = '${suffix}_$number';
+  }
 }
 
 MemberNames _unusedMemberNames(
     FieldDescriptorProto field, int index, Set<String> existingNames) {
-  var suffix = '_' + field.number.toString();
-
   if (_isRepeated(field)) {
-    var name = _defaultFieldName(field);
-    while (existingNames.contains(name)) {
-      name += suffix;
-    }
-    return new MemberNames.forRepeatedField(field, index, name);
+    return new MemberNames(
+        field,
+        index,
+        disambiguateName(_defaultFieldName(_fieldMethodSuffix(field)),
+            existingNames, _memberNamesSuffix(field.number)));
   }
 
-  String name = _defaultFieldName(field);
-  String hasMethod = _defaultHasMethodName(field);
-  String clearMethod = _defaultClearMethodName(field);
-
-  while (existingNames.contains(name) ||
-      existingNames.contains(hasMethod) ||
-      existingNames.contains(clearMethod)) {
-    name += suffix;
-    hasMethod += suffix;
-    clearMethod += suffix;
+  List<String> generateNameVariants(String name) {
+    return [
+      _defaultFieldName(name),
+      _defaultHasMethodName(name),
+      _defaultClearMethodName(name)
+    ];
   }
-  return new MemberNames(field, index, name, hasMethod, clearMethod);
+
+  String name = disambiguateName(_fieldMethodSuffix(field), existingNames,
+      _memberNamesSuffix(field.number),
+      generateVariants: generateNameVariants);
+  return new MemberNames(field, index, _defaultFieldName(name),
+      hasMethodName: _defaultHasMethodName(name),
+      clearMethodName: _defaultClearMethodName(name));
 }
 
 /// The name to use by default for the Dart getter and setter.
 /// (A suffix will be added if there is a conflict.)
-String _defaultFieldName(FieldDescriptorProto field) {
-  String name = _fieldMethodSuffix(field);
-  return '${name[0].toLowerCase()}${name.substring(1)}';
+String _defaultFieldName(String fieldMethodSuffix) {
+  return '${fieldMethodSuffix[0].toLowerCase()}${fieldMethodSuffix.substring(1)}';
 }
 
-String _defaultHasMethodName(FieldDescriptorProto field) =>
-    'has${_fieldMethodSuffix(field)}';
+String _defaultHasMethodName(String fieldMethodSuffix) =>
+    'has$fieldMethodSuffix';
 
-String _defaultClearMethodName(FieldDescriptorProto field) =>
-    'clear${_fieldMethodSuffix(field)}';
+String _defaultClearMethodName(String fieldMethodSuffix) =>
+    'clear$fieldMethodSuffix';
 
 /// The suffix to use for this field in Dart method names.
 /// (It should be camelcase and begin with an uppercase letter.)
@@ -319,6 +361,24 @@
 
 final _dartFieldNameExpr = new RegExp(r'^[a-z]\w+$');
 
+/// Names that would collide with capitalized core Dart names as top-level
+/// identifiers.
+final List<String> toplevelReservedCapitalizedNames = const <String>[
+  'List',
+  'Function',
+  'Map',
+];
+
+final List<String> reservedMemberNames = <String>[]
+  ..addAll(_dartReservedWords)
+  ..addAll(GeneratedMessage_reservedNames)
+  ..addAll(_generatedMessageNames);
+
+final List<String> forbiddenExtensionNames = <String>[]
+  ..addAll(_dartReservedWords)
+  ..addAll(GeneratedMessage_reservedNames)
+  ..addAll(_generatedMessageNames);
+
 // List of Dart language reserved words in names which cannot be used in a
 // subclass of GeneratedMessage.
 const List<String> _dartReservedWords = const [
diff --git a/lib/protobuf_field.dart b/lib/protobuf_field.dart
index ce53c7d..83eca16 100644
--- a/lib/protobuf_field.dart
+++ b/lib/protobuf_field.dart
@@ -291,7 +291,7 @@
             descriptor.defaultValue.isNotEmpty) {
           return '$className.${descriptor.defaultValue}';
         } else if (gen._canonicalValues.isNotEmpty) {
-          return '$className.${gen._canonicalValues[0].name}';
+          return '$className.${gen.dartNames[gen._canonicalValues[0].name]}';
         }
         return null;
       default:
diff --git a/lib/service_generator.dart b/lib/service_generator.dart
index 4f9f183..e06d5fc 100644
--- a/lib/service_generator.dart
+++ b/lib/service_generator.dart
@@ -27,16 +27,22 @@
   /// Populated by [resolve].
   final _undefinedDeps = <String, String>{};
 
-  ServiceGenerator(this._descriptor, this.fileGen);
+  final String classname;
 
-  String get classname {
-    if (_descriptor.name.endsWith("Service")) {
-      return _descriptor.name + "Base"; // avoid: ServiceServiceBase
+  static String serviceBaseName(String originalName) {
+    if (originalName.endsWith("Service")) {
+      return originalName + "Base"; // avoid: ServiceServiceBase
     } else {
-      return _descriptor.name + "ServiceBase";
+      return originalName + "ServiceBase";
     }
   }
 
+  ServiceGenerator(this._descriptor, this.fileGen, Set<String> usedNames)
+      : classname = disambiguateName(
+            serviceBaseName(avoidInitialUnderscore(_descriptor.name)),
+            usedNames,
+            defaultSuffixes());
+
   /// Finds all message types used by this service.
   ///
   /// Puts the types found in [_deps] and [_transitiveDeps].
@@ -194,8 +200,8 @@
     out.println();
   }
 
-  String get jsonConstant => "${_descriptor.name}\$json";
-  String get messageJsonConstant => "${_descriptor.name}\$messageJson";
+  String get jsonConstant => "$classname\$json";
+  String get messageJsonConstant => "$classname\$messageJson";
 
   /// Writes Dart constants for the service and message descriptors.
   ///
diff --git a/pubspec.yaml b/pubspec.yaml
index dac6d95..7d7f545 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protoc_plugin
-version: 11.0.0
+version: 12.0.0
 author: Dart Team <misc@dartlang.org>
 description: Protoc compiler plugin to generate Dart code
 homepage: https://github.com/dart-lang/dart-protoc-plugin
diff --git a/test/all_tests.dart b/test/all_tests.dart
index e9e159b..fec7447 100755
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -17,6 +17,7 @@
 import 'indenting_writer_test.dart' as indenting_writer;
 import 'import_test.dart' as import_prefix;
 import 'json_test.dart' as json;
+import 'leading_underscores_test.dart' as leading_underscores;
 import 'map_test.dart' as map;
 import 'message_generator_test.dart' as message_generator;
 import 'message_test.dart' as message;
@@ -43,6 +44,7 @@
   indenting_writer.main();
   import_prefix.main();
   json.main();
+  leading_underscores.main();
   map.main();
   message_generator.main();
   message.main();
diff --git a/test/enum_generator_test.dart b/test/enum_generator_test.dart
index a5f8fc5..d1cd2f1 100755
--- a/test/enum_generator_test.dart
+++ b/test/enum_generator_test.dart
@@ -33,7 +33,7 @@
     IndentingWriter writer = new IndentingWriter();
     FileGenerator fg =
         new FileGenerator(new FileDescriptorProto(), new GenerationOptions());
-    EnumGenerator eg = new EnumGenerator(ed, fg);
+    EnumGenerator eg = new EnumGenerator(ed, fg, new Set<String>());
     eg.generate(writer);
     expectMatchesGoldenFile(writer.toString(), 'test/goldens/enum');
   });
diff --git a/test/goldens/service.pbserver b/test/goldens/service.pbserver
index f50d95b..cf1be0c 100644
--- a/test/goldens/service.pbserver
+++ b/test/goldens/service.pbserver
@@ -30,7 +30,7 @@
     }
   }
 
-  Map<String, dynamic> get $json => Test$json;
-  Map<String, Map<String, dynamic>> get $messageJson => Test$messageJson;
+  Map<String, dynamic> get $json => TestServiceBase$json;
+  Map<String, Map<String, dynamic>> get $messageJson => TestServiceBase$messageJson;
 }
 
diff --git a/test/goldens/serviceGenerator b/test/goldens/serviceGenerator
index 00f9075..f5eb2e9 100644
--- a/test/goldens/serviceGenerator
+++ b/test/goldens/serviceGenerator
@@ -18,7 +18,7 @@
     }
   }
 
-  Map<String, dynamic> get $json => Test$json;
-  Map<String, Map<String, dynamic>> get $messageJson => Test$messageJson;
+  Map<String, dynamic> get $json => TestServiceBase$json;
+  Map<String, Map<String, dynamic>> get $messageJson => TestServiceBase$messageJson;
 }
 
diff --git a/test/goldens/serviceGenerator.pb.json b/test/goldens/serviceGenerator.pb.json
new file mode 100644
index 0000000..96cdc7a
--- /dev/null
+++ b/test/goldens/serviceGenerator.pb.json
@@ -0,0 +1,31 @@
+///
+//  Generated code. Do not modify.
+//  source: testpkg.proto
+///
+// ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
+
+import 'foobar.pbjson.dart' as $0;
+
+const SomeRequest$json = const {
+  '1': 'SomeRequest',
+};
+
+const SomeReply$json = const {
+  '1': 'SomeReply',
+};
+
+const TestServiceBase$json = const {
+  '1': 'Test',
+  '2': const [
+    const {'1': 'AMethod', '2': '.testpkg.SomeRequest', '3': '.testpkg.SomeReply'},
+    const {'1': 'AnotherMethod', '2': '.foo.bar.EmptyMessage', '3': '.foo.bar.AnotherReply'},
+  ],
+};
+
+const TestServiceBase$messageJson = const {
+  '.testpkg.SomeRequest': SomeRequest$json,
+  '.testpkg.SomeReply': SomeReply$json,
+  '.foo.bar.EmptyMessage': $0.EmptyMessage$json,
+  '.foo.bar.AnotherReply': $0.AnotherReply$json,
+};
+
diff --git a/test/leading_underscores_test.dart b/test/leading_underscores_test.dart
new file mode 100644
index 0000000..8227542
--- /dev/null
+++ b/test/leading_underscores_test.dart
@@ -0,0 +1,45 @@
+#!/usr/bin/env dart
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:fixnum/fixnum.dart';
+
+import '../out/protos/_leading_underscores.pb.dart';
+
+void main() {
+  test('can set, read and clear all fields and refer to types', () {
+    A_ message = new A_();
+    message.setExtension(Leading_underscores_.p, Int64(99));
+    expect(message.getExtension(Leading_underscores_.p), Int64(99));
+    message.f = 'foo';
+    message.f_2 = 'foo2';
+    expect(message.f, 'foo');
+    expect(message.f_2, 'foo2');
+    message.clearF();
+    message.clearF_2();
+    expect(message.hasF(), false);
+    expect(message.hasF_2(), false);
+    expect(message.f, '');
+    expect(message.f_2, '');
+    A messageA = new A();
+    messageA.b = message;
+    messageA.b_6 = message;
+    expect(messageA.b_6, message);
+    messageA.amap['foo'] = message;
+    expect(messageA.amap['foo'], message);
+
+    messageA.e = Enum_.constant;
+    expect(messageA.e, Enum_.constant);
+    messageA.clearE();
+    expect(messageA.e, Enum_.constant_);
+    messageA.r.add(message);
+    expect(messageA.r, [message]);
+    messageA.setExtension(Leading_underscores_.q, Int64(100));
+    expect(messageA.getExtension(Leading_underscores_.q), Int64(100));
+
+    A__ a = A__()..foo = 'hi';
+    expect(a.foo, 'hi');
+  });
+}
diff --git a/test/message_generator_test.dart b/test/message_generator_test.dart
index 1187cd1..70b0341 100755
--- a/test/message_generator_test.dart
+++ b/test/message_generator_test.dart
@@ -61,7 +61,8 @@
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
     FileGenerator fg = new FileGenerator(fd, options);
-    MessageGenerator mg = new MessageGenerator(md, fg, {}, null);
+    MessageGenerator mg =
+        new MessageGenerator(md, fg, {}, null, new Set<String>());
 
     var ctx = new GenerationContext(options);
     mg.register(ctx);
diff --git a/test/names_test.dart b/test/names_test.dart
index 95906c0..667d11d 100644
--- a/test/names_test.dart
+++ b/test/names_test.dart
@@ -86,6 +86,61 @@
     new pb.Function_()..fun = 'renamed';
     new pb.Function__()..fun1 = 'also renamed';
   });
+
+  test('disambiguateName', () {
+    Iterable<String> oneTwoThree() sync* {
+      yield* ['_one', '_two', '_three'];
+    }
+
+    {
+      final used = Set<String>.from(['moo']);
+      expect(names.disambiguateName('foo', used, oneTwoThree()), 'foo');
+      expect(used, Set<String>.from(['moo', 'foo']));
+    }
+    {
+      final used = Set<String>.from(['foo']);
+      expect(names.disambiguateName('foo', used, oneTwoThree()), 'foo_one');
+      expect(used, Set<String>.from(['foo', 'foo_one']));
+    }
+    {
+      final used = Set<String>.from(['foo', 'foo_one']);
+      expect(names.disambiguateName('foo', used, oneTwoThree()), 'foo_two');
+      expect(used, Set<String>.from(['foo', 'foo_one', 'foo_two']));
+    }
+
+    {
+      List<String> variants(String s) {
+        return ['a_' + s, 'b_' + s];
+      }
+
+      final used = Set<String>.from(['a_foo', 'b_foo_one']);
+      expect(
+          names.disambiguateName('foo', used, oneTwoThree(),
+              generateVariants: variants),
+          'foo_two');
+      expect(used,
+          Set<String>.from(['a_foo', 'b_foo_one', 'a_foo_two', 'b_foo_two']));
+    }
+  });
+
+  test('avoidInitialUnderscore', () {
+    expect(names.avoidInitialUnderscore('foo'), 'foo');
+    expect(names.avoidInitialUnderscore('foo_'), 'foo_');
+    expect(names.avoidInitialUnderscore('_foo'), 'foo_');
+    expect(names.avoidInitialUnderscore('__foo'), 'foo__');
+  });
+
+  test('legalDartIdentifier', () {
+    expect(names.legalDartIdentifier("foo"), "foo");
+    expect(names.legalDartIdentifier("_foo"), "_foo");
+    expect(names.legalDartIdentifier("-foo"), "_foo");
+    expect(names.legalDartIdentifier("foo.\$a{b}c(d)e_"), "foo_\$a_b_c_d_e_");
+  });
+
+  test('defaultSuffixes', () {
+    expect(names.defaultSuffixes().take(5).toList(),
+        ['_', '_0', '_1', '_2', '_3']);
+  });
 }
 
 FieldDescriptorProto stringField(String name, int number, String dartName) {
diff --git a/test/protos/_leading_underscores.proto b/test/protos/_leading_underscores.proto
new file mode 100644
index 0000000..35d7688
--- /dev/null
+++ b/test/protos/_leading_underscores.proto
@@ -0,0 +1,51 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+syntax = "proto2";
+
+package _leading_underscores;
+
+message _A {
+  optional string _f = 1;
+  optional string f = 2;
+  extensions 3 to 4;
+  extend A {
+    optional int64 _q = 3;
+  }
+}
+
+message A_ {
+  optional string foo = 1;
+}
+
+message A {
+  optional string _f = 1;
+  optional string f = 2;
+  extensions 3 to 4;
+  optional _A b = 5;
+  optional _A _b = 6;
+  optional _A _c = 7;
+  optional _Enum _e = 8;
+  map<string, _A> _amap = 9;
+  repeated _A _r = 10;
+}
+
+extend _A {
+  optional int64 _p = 3;
+  optional int64 p = 4;
+}
+
+extend A {
+  optional int64 _q = 4;
+}
+
+enum _Enum {
+  _constant = 0;
+  constant = 1;
+}
+
+service _service {
+  rpc _search (_A) returns (_A);
+  rpc search (_A) returns (_A);
+}
diff --git a/test/service_generator_test.dart b/test/service_generator_test.dart
index 3c4ef2a..ca15923 100644
--- a/test/service_generator_test.dart
+++ b/test/service_generator_test.dart
@@ -26,8 +26,11 @@
 
     link(new GenerationOptions(), [fg, fg2]);
 
-    var writer = new IndentingWriter();
-    fg.serviceGenerators[0].generate(writer);
-    expectMatchesGoldenFile(writer.toString(), 'test/goldens/serviceGenerator');
+    var serviceWriter = new IndentingWriter();
+    fg.serviceGenerators[0].generate(serviceWriter);
+    expectMatchesGoldenFile(
+        serviceWriter.toString(), 'test/goldens/serviceGenerator');
+    expectMatchesGoldenFile(
+        fg.generateJsonFile(), 'test/goldens/serviceGenerator.pb.json');
   });
 }