blob: 99d666adfe01a3fb9cbb78e36f49e9009c73c0d7 [file] [log] [blame]
// 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 'dart:math' as math;
import 'package:protobuf/meta.dart';
import 'src/generated/dart_options.pb.dart';
import 'src/generated/descriptor.pb.dart';
class MemberNames {
List<FieldNames> fieldNames;
List<OneofNames> oneofNames;
MemberNames(this.fieldNames, this.oneofNames);
/// The Dart member names in a GeneratedMessage subclass for one protobuf field.
class FieldNames {
/// 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.
/// `null` for extensions.
final int? index;
/// The position of this field as it appeared in the original DescriptorProto.
/// Used to construct metadata.
/// `null` for extensions.
final int? sourcePosition;
/// 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;
// 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.ensureMethodName});
/// The Dart names associated with a oneof declaration.
class OneofNames {
final OneofDescriptorProto descriptor;
/// Index in the containing type's oneof_decl list.
final int index;
/// Identifier for the generated whichX() method, without braces.
final String whichOneofMethodName;
/// Identifier for the generated clearX() method, without braces.
final String clearMethodName;
/// Identifier for the generated enum definition.
final String oneofEnumName;
/// Identifier for the _XByTag map.
final String byTagMapName;
OneofNames(this.descriptor, this.index, this.clearMethodName,
this.whichOneofMethodName, this.oneofEnumName, this.byTagMapName);
// For performance reasons, use code units instead of Regex.
bool _startsWithDigit(String input) =>
input.isNotEmpty && (input.codeUnitAt(0) ^ 0x30) <= 9;
/// 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.
/// If [input] starts with a digit after transformation, prefix with an 'x'.
String avoidInitialUnderscore(String input) {
while (input.startsWith('_')) {
input = '${input.substring(1)}_';
if (_startsWithDigit(input)) {
input = 'x$input';
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, Set<String> usedNames) {
return _unusedMemberNames(descriptor, null, null, usedNames).fieldName;
Iterable<String> extensionSuffixes() sync* {
yield 'Ext';
var i = 2;
while (true) {
yield '$i';
/// Replaces all characters in [input] that are not valid in a dart identifier
/// with _.
/// This function does not take care of leading underscores.
String legalDartIdentifier(String input) {
return input.replaceAll(RegExp(r'[^a-zA-Z0-9$_]'), '_');
/// Chooses the name of the Dart class holding top-level extensions.
String extensionClassName(
FileDescriptorProto descriptor, Set<String> usedNames) {
final s = avoidInitialUnderscore(
final candidate = '${s[0].toUpperCase()}${s.substring(1)}';
return disambiguateName(candidate, usedNames, extensionSuffixes());
String _fileNameWithoutExtension(FileDescriptorProto descriptor) {
final path = Uri.file(;
final fileName = path.pathSegments.last;
final 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;
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 [generateVariants] 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));
var usedSuffix = '';
var candidateVariants = generateVariants(name);
if (!allVariantsAvailable(candidateVariants)) {
for (final suffix in suffixes) {
candidateVariants = generateVariants('$name$suffix');
if (allVariantsAvailable(candidateVariants)) {
usedSuffix = suffix;
return '$name$usedSuffix';
Iterable<String> defaultSuffixes() sync* {
yield '_';
var i = 0;
while (true) {
yield '_$i';
String oneofEnumClassName(
String descriptorName, Set<String> usedNames, String parentName) {
descriptorName = '${parentName}_${underscoresToCamelCase(descriptorName)}';
return disambiguateName(
avoidInitialUnderscore(descriptorName), usedNames, defaultSuffixes());
String oneofEnumMemberName(String fieldName) => disambiguateName(
fieldName, Set<String>.from(_oneofEnumMemberNames), defaultSuffixes());
/// 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, Set<String> usedNames,
{String parent = ''}) {
if (parent != '') {
descriptorName = '${parent}_$descriptorName';
return disambiguateName(
avoidInitialUnderscore(descriptorName), usedNames, defaultSuffixes());
/// Returns the set of names reserved by the ProtobufEnum class and its
/// generated subclasses.
Set<String> get reservedEnumNames => <String>{}
Iterable<String> enumSuffixes() sync* {
var s = '_';
while (true) {
yield s;
s += '_';
/// Chooses the GeneratedMessage member names for each field and names
/// associated with each oneof declaration.
/// Additional names to avoid can be supplied using [reserved].
/// (This should only be used for mixins.)
/// Returns [MemberNames] which holds a list with [FieldNames] and a list with [OneofNames].
/// Throws [DartNameOptionException] if a field has this option and
/// it's set to an invalid name.
MemberNames messageMemberNames(DescriptorProto descriptor,
String parentClassName, Set<String> usedTopLevelNames,
{Iterable<String> reserved = const []}) {
final fieldList = List<FieldDescriptorProto>.from(descriptor.field);
final sourcePositions =
fieldList.asMap().map((index, field) => MapEntry(, index));
final sorted = fieldList
..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 ${}';
// Choose indexes first, based on their position in the sorted list.
final indexes = <String, int>{};
for (final field in sorted) {
final index = indexes.length;
indexes[] = index;
final existingNames = <String>{...reservedMemberNames, ...reserved};
final fieldNames = List<FieldNames?>.filled(indexes.length, null);
void takeFieldNames(FieldNames chosen) {
fieldNames[chosen.index!] = chosen;
if (chosen.hasMethodName != null) {
if (chosen.clearMethodName != null) {
// 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 (final field in sorted) {
if (_nameOption(field)!.isNotEmpty) {
takeFieldNames(_memberNamesFromOption(descriptor, field,
indexes[]!, sourcePositions[]!, existingNames));
// Then do other fields.
// They are automatically renamed until we find something unused.
for (final field in sorted) {
if (_nameOption(field)!.isEmpty) {
final index = indexes[]!;
final sourcePosition = sourcePositions[];
_unusedMemberNames(field, index, sourcePosition, existingNames));
final oneofNames = <OneofNames>[];
void takeOneofNames(OneofNames chosen) {
List<String> oneofNameVariants(String name) {
return [_defaultWhichMethodName(name), _defaultClearMethodName(name)];
final realOneofCount = countRealOneofs(descriptor);
for (var i = 0; i < realOneofCount; i++) {
final oneof = descriptor.oneofDecl[i];
final oneofName = disambiguateName(
underscoresToCamelCase(, existingNames, defaultSuffixes(),
generateVariants: oneofNameVariants);
final oneofEnumName =
oneofEnumClassName(, usedTopLevelNames, parentClassName);
final enumMapName = disambiguateName(
'_${oneofEnumName}ByTag', existingNames, defaultSuffixes());
takeOneofNames(OneofNames(oneof, i, _defaultClearMethodName(oneofName),
_defaultWhichMethodName(oneofName), oneofEnumName, enumMapName));
return MemberNames(fieldNames.cast<FieldNames>(), oneofNames);
/// 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.)
FieldNames _memberNamesFromOption(
DescriptorProto message,
FieldDescriptorProto field,
int index,
int sourcePosition,
Set<String> existingNames) {
// TODO(skybrian): provide more context in errors (filename).
final where = '${}.${}';
void checkAvailable(String name) {
if (existingNames.contains(name)) {
throw DartNameOptionException(
"$where: dart_name option is invalid: '$name' is already used");
final name = _nameOption(field)!;
if (name.isEmpty) {
throw ArgumentError("field doesn't have dart_name option");
if (!_isDartFieldName(name)) {
throw DartNameOptionException('$where: dart_name option is invalid: '
"'$name' is not a valid Dart field name");
if (_isRepeated(field)) {
return FieldNames(field, index, sourcePosition, name);
final hasMethod = 'has${_capitalize(name)}';
final clearMethod = 'clear${_capitalize(name)}';
String? ensureMethod;
if (_isGroupOrMessage(field)) {
ensureMethod = 'ensure${_capitalize(name)}';
return FieldNames(field, index, sourcePosition, name,
hasMethodName: hasMethod,
clearMethodName: clearMethod,
ensureMethodName: ensureMethod);
Iterable<String> _memberNamesSuffix(int number) sync* {
var suffix = '_$number';
while (true) {
yield suffix;
suffix = '${suffix}_$number';
FieldNames _unusedMemberNames(FieldDescriptorProto field, int? index,
int? sourcePosition, Set<String> existingNames) {
if (_isRepeated(field)) {
return FieldNames(
existingNames, _memberNamesSuffix(field.number)));
List<String> generateNameVariants(String name) {
final result = <String>[
// TODO(zarah): Use 'collection if' when sdk dependency is updated.
if (_isGroupOrMessage(field)) result.add(_defaultEnsureMethodName(name));
return result;
final name = disambiguateName(_fieldMethodSuffix(field), existingNames,
generateVariants: generateNameVariants);
return FieldNames(field, index, sourcePosition,
hasMethodName: _defaultHasMethodName(name),
clearMethodName: _defaultClearMethodName(name),
_isGroupOrMessage(field) ? _defaultEnsureMethodName(name) : null);
/// The name to use by default for the Dart getter and setter.
/// (A suffix will be added if there is a conflict.)
String _defaultFieldName(String fieldMethodSuffix) =>
String _defaultHasMethodName(String fieldMethodSuffix) =>
String _defaultClearMethodName(String fieldMethodSuffix) =>
String _defaultWhichMethodName(String oneofMethodSuffix) =>
String _defaultEnsureMethodName(String 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) {
var name = _nameOption(field)!;
if (name.isNotEmpty) return _capitalize(name);
if (field.type != FieldDescriptorProto_Type.TYPE_GROUP) {
return underscoresToCamelCase(;
// For groups, use capitalization of 'typeName' rather than 'name'.
name = field.typeName;
final index = name.lastIndexOf('.');
if (index != -1) {
name = name.substring(index + 1);
return underscoresToCamelCase(name);
String underscoresToCamelCase(String s) =>
String _capitalize(String s) =>
s.isEmpty ? s : '${s[0].toUpperCase()}${s.substring(1)}';
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) as String?;
bool _isDartFieldName(String name) => name.startsWith(_dartFieldNameExpr);
final _dartFieldNameExpr = RegExp(r'^[a-z]\w+$');
/// Names that would collide as top-level identifiers.
final List<String> forbiddenTopLevelNames = <String>[
final List<String> reservedMemberNames = <String>[
final List<String> forbiddenExtensionNames = <String>[
// List of Dart language reserved words in names which cannot be used in a
// subclass of GeneratedMessage.
const List<String> _dartReservedWords = [
// 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 = <String>[
// 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 = <String>[
// List of names used in Dart enums, which can't be used as enum member names.
const _oneofEnumMemberNames = <String>['default', 'index', 'values'];
// Count the number of 'real' oneofs - that is oneofs not created for an
// optional proto3 field.
int countRealOneofs(DescriptorProto descriptor) {
var highestIndexSeen = -1;
for (final field in descriptor.field) {
if (field.hasOneofIndex() && !field.proto3Optional) {
highestIndexSeen = math.max(highestIndexSeen, field.oneofIndex);
// The number of entries is one higher than the highest seen index.
return highestIndexSeen + 1;
String lowerCaseFirstLetter(String input) =>
input[0].toLowerCase() + input.substring(1);