Index: lib/names.dart |
diff --git a/lib/names.dart b/lib/names.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..24a9e66bb1d9432e4cbc53ded2872fe89c6d5e91 |
--- /dev/null |
+++ b/lib/names.dart |
@@ -0,0 +1,288 @@ |
+// Copyright (c) 2016, 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:protobuf/meta.dart'; |
+import 'package:protoc_plugin/src/dart_options.pb.dart'; |
+import 'package:protoc_plugin/src/descriptor.pb.dart'; |
+ |
+/// A Dart function called on each item added to a repeated list |
+/// to check its type and range. |
+const checkItem = '\$checkItem'; |
+ |
+/// The Dart member names in a GeneratedMessage subclass for one protobuf field. |
+class MemberNames { |
+ /// The descriptor of the field these member names apply to. |
+ final FieldDescriptorProto descriptor; |
+ |
+ /// The index of this field in MessageGenerator.fieldList. |
+ /// The same index will be stored in FieldInfo.index. |
+ final int index; |
+ |
+ /// Identifier for generated getters/setters. |
+ final String fieldName; |
+ |
+ /// Identifier for the generated hasX() method, without braces. |
+ /// |
+ /// `null` for repeated fields. |
+ final String hasMethodName; |
+ |
+ /// Identifier for the generated clearX() method, without braces. |
+ /// |
+ /// `null` for repeated fields. |
+ final String clearMethodName; |
+ |
+ MemberNames(this.descriptor, this.index, this.fieldName, this.hasMethodName, |
+ this.clearMethodName); |
+ |
+ MemberNames.forRepeatedField(this.descriptor, this.index, this.fieldName) |
+ : hasMethodName = null, |
+ clearMethodName = null; |
+} |
+ |
+/// Chooses the Dart name of an extension. |
+String extensionName(FieldDescriptorProto descriptor) { |
+ var existingNames = new Set<String>() |
+ ..addAll(_dartReservedWords) |
+ ..addAll(GeneratedMessage_reservedNames) |
+ ..addAll(_generatedNames); |
+ return _unusedMemberNames(descriptor, null, existingNames).fieldName; |
+} |
+ |
+// Exception thrown when a field has an invalid 'dart_name' option. |
+class DartNameOptionException implements Exception { |
+ final String message; |
+ DartNameOptionException(this.message); |
+ String toString() => "$message"; |
+} |
+ |
+/// Chooses the GeneratedMessage member names for each field. |
+/// |
+/// Additional names to avoid can be supplied using [reserved]. |
+/// (This should only be used for mixins.) |
+/// |
+/// Returns a map from the field name in the .proto file to its |
+/// corresponding MemberNames. |
+/// |
+/// Throws [DartNameOptionException] if a field has this option and |
+/// it's set to an invalid name. |
+Map<String, MemberNames> messageFieldNames(DescriptorProto descriptor, |
+ {Iterable<String> reserved: const []}) { |
+ var sorted = new List<FieldDescriptorProto>.from(descriptor.field) |
+ ..sort((FieldDescriptorProto a, FieldDescriptorProto b) { |
+ if (a.number < b.number) return -1; |
+ if (a.number > b.number) return 1; |
+ throw "multiple fields defined for tag ${a.number} in ${descriptor.name}"; |
+ }); |
+ |
+ // Choose indexes first, based on their position in the sorted list. |
+ var indexes = <String, int>{}; |
+ for (var field in sorted) { |
+ var index = indexes.length; |
+ indexes[field.name] = index; |
+ } |
+ |
+ var existingNames = new Set<String>() |
+ ..addAll(_dartReservedWords) |
+ ..addAll(GeneratedMessage_reservedNames) |
+ ..addAll(_generatedNames) |
+ ..addAll(reserved); |
+ |
+ var memberNames = <String, MemberNames>{}; |
+ |
+ void takeNames(MemberNames chosen) { |
+ memberNames[chosen.descriptor.name] = chosen; |
+ |
+ existingNames.add(chosen.fieldName); |
+ if (chosen.hasMethodName != null) { |
+ existingNames.add(chosen.hasMethodName); |
+ } |
+ if (chosen.clearMethodName != null) { |
+ existingNames.add(chosen.clearMethodName); |
+ } |
+ } |
+ |
+ // Handle fields with a dart_name option. |
+ // They have higher priority than automatically chosen names. |
+ // Explicitly setting a name that's already taken is a build error. |
+ for (var field in sorted) { |
+ if (_nameOption(field).isNotEmpty) { |
+ takeNames(_memberNamesFromOption( |
+ descriptor, field, indexes[field.name], existingNames)); |
+ } |
+ } |
+ |
+ // Then do other fields. |
+ // They are automatically renamed until we find something unused. |
+ for (var field in sorted) { |
+ if (_nameOption(field).isEmpty) { |
+ var index = indexes[field.name]; |
+ takeNames(_unusedMemberNames(field, index, existingNames)); |
+ } |
+ } |
+ |
+ // Return a map with entries in sorted order. |
+ var result = <String, MemberNames>{}; |
+ for (var field in sorted) { |
+ result[field.name] = memberNames[field.name]; |
+ } |
+ return result; |
+} |
+ |
+/// Chooses the member names for a field that has the 'dart_name' option. |
+/// |
+/// If the explicitly-set Dart name is already taken, throw an exception. |
+/// (Fails the build.) |
+MemberNames _memberNamesFromOption(DescriptorProto message, |
+ FieldDescriptorProto field, int index, Set<String> existingNames) { |
+ // TODO(skybrian): provide more context in errors (filename). |
+ var where = "${message.name}.${field.name}"; |
+ |
+ void checkAvailable(String name) { |
+ if (existingNames.contains(name)) { |
+ throw throw new DartNameOptionException( |
frederikmutzel
2016/06/09 07:50:28
double throw
skybrian
2016/06/09 19:10:47
Done.
|
+ "$where: dart_name option is invalid: '$name' is already used"); |
+ } |
+ } |
+ |
+ var name = _nameOption(field); |
+ if (name.isEmpty) { |
+ throw new ArgumentError("field doesn't have dart_name option"); |
+ } |
+ if (!_isDartFieldName(name)) { |
+ throw new DartNameOptionException("$where: dart_name option is invalid: " |
+ "'$name' is not a valid Dart field name"); |
+ } |
+ checkAvailable(name); |
+ |
+ if (_isRepeated(field)) { |
+ return new MemberNames.forRepeatedField(field, index, name); |
+ } |
+ |
+ String hasMethod = "has${_capitalize(name)}"; |
+ checkAvailable(hasMethod); |
+ |
+ String clearMethod = "clear${_capitalize(name)}"; |
+ checkAvailable(clearMethod); |
+ |
+ return new MemberNames(field, index, name, hasMethod, clearMethod); |
+} |
+ |
+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); |
+ } |
+ |
+ 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; |
+ } |
+ return new MemberNames(field, index, name, hasMethod, clearMethod); |
+} |
+ |
+/// 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 _defaultHasMethodName(FieldDescriptorProto field) => |
+ 'has${_fieldMethodSuffix(field)}'; |
+ |
+String _defaultClearMethodName(FieldDescriptorProto field) => |
+ 'clear${_fieldMethodSuffix(field)}'; |
+ |
+/// 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) { |
+ var name = _nameOption(field); |
+ if (name.isNotEmpty) return _capitalize(name); |
+ |
+ if (field.type != FieldDescriptorProto_Type.TYPE_GROUP) { |
+ return _underscoresToCamelCase(field.name); |
+ } |
+ |
+ // For groups, use capitalization of 'typeName' rather than 'name'. |
+ name = field.typeName; |
+ int index = name.lastIndexOf('.'); |
+ if (index != -1) { |
+ name = name.substring(index + 1); |
+ } |
+ return _underscoresToCamelCase(name); |
+} |
+ |
+String _underscoresToCamelCase(s) => s.split('_').map(_capitalize).join(''); |
+ |
+String _capitalize(s) => |
+ s.isEmpty ? s : '${s[0].toUpperCase()}${s.substring(1)}'; |
+ |
+bool _isRepeated(FieldDescriptorProto field) => |
+ field.label == FieldDescriptorProto_Label.LABEL_REPEATED; |
+ |
+String _nameOption(FieldDescriptorProto field) => |
+ field.options.getExtension(Dart_options.dartName); |
+ |
+bool _isDartFieldName(name) => name.startsWith(_dartFieldNameExpr); |
+ |
+final _dartFieldNameExpr = new RegExp(r'^[a-z]\w+$'); |
+ |
+// List of Dart language reserved words in names which cannot be used in a |
+// subclass of GeneratedMessage. |
+const List<String> _dartReservedWords = const [ |
+ 'assert', |
+ 'break', |
+ 'case', |
+ 'catch', |
+ 'class', |
+ 'const', |
+ 'continue', |
+ 'default', |
+ 'do', |
+ 'else', |
+ 'enum', |
+ 'extends', |
+ 'false', |
+ 'final', |
+ 'finally', |
+ 'for', |
+ 'if', |
+ 'in', |
+ 'is', |
+ 'new', |
+ 'null', |
+ 'rethrow', |
+ 'return', |
+ 'super', |
+ 'switch', |
+ 'this', |
+ 'throw', |
+ 'true', |
+ 'try', |
+ 'var', |
+ 'void', |
+ 'while', |
+ 'with' |
+]; |
+ |
+// List of names used in the generated class itself. |
+const List<String> _generatedNames = const [ |
+ 'create', |
+ 'createRepeated', |
+ 'getDefault', |
+ checkItem |
+]; |