| // 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(_generatedMessageNames); |
| return _unusedMemberNames(descriptor, null, existingNames).fieldName; |
| } |
| |
| /// 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 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"; |
| } |
| |
| String _fileNameWithoutExtension(FileDescriptorProto descriptor) { |
| Uri path = new Uri.file(descriptor.name); |
| String fileName = path.pathSegments.last; |
| int dot = fileName.lastIndexOf("."); |
| return dot == -1 ? fileName : fileName.substring(0, dot); |
| } |
| |
| // 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 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; |
| if (parent != '') { |
| name = '${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; |
| } |
| |
| /// Returns the set of names reserved by the ProtobufEnum class and its |
| /// generated subclasses. |
| Set<String> get reservedEnumNames => new Set<String>() |
| ..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; |
| } |
| existingNames.add(name); |
| return name; |
| } |
| |
| /// 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(_generatedMessageNames) |
| ..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 new DartNameOptionException( |
| "$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 message classes. |
| // |
| // This is in addition to GeneratedMessage_reservedNames, which are names from |
| // the base GeneratedMessage class determined by reflection. |
| const _generatedMessageNames = const <String>[ |
| 'create', |
| 'createRepeated', |
| 'getDefault', |
| 'List', |
| checkItem |
| ]; |
| |
| // List of names used in the generated enum classes. |
| // |
| // This is in addition to ProtobufEnum_reservedNames, which are names from the |
| // base ProtobufEnum class determined by reflection. |
| const _protobufEnumNames = const <String>[ |
| 'List', |
| 'valueOf', |
| 'values', |
| checkItem |
| ]; |