Chromium Code Reviews| 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 |
| +]; |