diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md index 504601c6..9dd89140 100644 --- a/protoc_plugin/CHANGELOG.md +++ b/protoc_plugin/CHANGELOG.md @@ -2,6 +2,7 @@ Note: this version requires protobuf 5.0.0. +* Support protobuf editions. ([#1052]) * Update generated code for protobuf 5.0.0. * Update generated `clone` members to take advantage of faster `deepCopy` implementation in protobuf 5.0.0. ([#742]) @@ -9,6 +10,7 @@ Note: this version requires protobuf 5.0.0. [#742]: https://github.com/google/protobuf.dart/pull/742 [#1047]: https://github.com/google/protobuf.dart/pull/1047 +[#1052]: https://github.com/google/protobuf.dart/pull/1052 ## 22.5.0 diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile index 18aaa658..b3121ff2 100644 --- a/protoc_plugin/Makefile +++ b/protoc_plugin/Makefile @@ -39,6 +39,7 @@ TEST_PROTO_LIST = \ foo \ high_tagnumber \ import_clash \ + import_option \ import_public \ json_name \ map_api \ diff --git a/protoc_plugin/lib/names.dart b/protoc_plugin/lib/names.dart index cf176a70..e14495d4 100644 --- a/protoc_plugin/lib/names.dart +++ b/protoc_plugin/lib/names.dart @@ -117,8 +117,18 @@ String singleQuote(String input) { } /// Chooses the Dart name of an extension. -String extensionName(FieldDescriptorProto descriptor, Set usedNames) { - return _unusedMemberNames(descriptor, null, null, usedNames).fieldName; +String extensionName( + FieldDescriptorProto descriptor, + Set usedNames, + bool lowercaseGroupNames, +) { + return _unusedMemberNames( + descriptor, + null, + null, + usedNames, + lowercaseGroupNames, + ).fieldName; } Iterable extensionSuffixes() sync* { @@ -281,6 +291,7 @@ MemberNames messageMemberNames( String parentClassName, Set usedTopLevelNames, { Iterable reserved = const [], + bool lowercaseGroupNames = false, }) { final fieldList = List.from(descriptor.field); final sourcePositions = fieldList.asMap().map( @@ -340,7 +351,13 @@ MemberNames messageMemberNames( final index = indexes[field.name]!; final sourcePosition = sourcePositions[field.name]; takeFieldNames( - _unusedMemberNames(field, index, sourcePosition, existingNames), + _unusedMemberNames( + field, + index, + sourcePosition, + existingNames, + lowercaseGroupNames, + ), ); } } @@ -470,6 +487,7 @@ FieldNames _unusedMemberNames( int? index, int? sourcePosition, Set existingNames, + bool lowercaseGroupNames, ) { if (_isRepeated(field)) { return FieldNames( @@ -477,7 +495,7 @@ FieldNames _unusedMemberNames( index, sourcePosition, disambiguateName( - _defaultFieldName(_fieldMethodSuffix(field)), + _defaultFieldName(_fieldMethodSuffix(field, lowercaseGroupNames)), existingNames, _memberNamesSuffix(field.number), ), @@ -498,7 +516,7 @@ FieldNames _unusedMemberNames( } final name = disambiguateName( - _fieldMethodSuffix(field), + _fieldMethodSuffix(field, lowercaseGroupNames), existingNames, _memberNamesSuffix(field.number), generateVariants: generateNameVariants, @@ -535,11 +553,15 @@ 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) { +String _fieldMethodSuffix( + FieldDescriptorProto field, + bool lowercaseGroupNames, +) { var name = _nameOption(field)!; if (name.isNotEmpty) return _capitalize(name); - if (field.type != FieldDescriptorProto_Type.TYPE_GROUP) { + if (field.type != FieldDescriptorProto_Type.TYPE_GROUP || + lowercaseGroupNames) { return underscoresToCamelCase(field.name); } diff --git a/protoc_plugin/lib/protoc.dart b/protoc_plugin/lib/protoc.dart index 36731a50..88413523 100644 --- a/protoc_plugin/lib/protoc.dart +++ b/protoc_plugin/lib/protoc.dart @@ -15,6 +15,7 @@ import 'src/gen/dart_options.pb.dart'; import 'src/gen/google/api/client.pb.dart'; import 'src/gen/google/protobuf/compiler/plugin.pb.dart'; import 'src/gen/google/protobuf/descriptor.pb.dart'; +import 'src/gen/google/protobuf/dart_edition_defaults.pb.dart'; import 'src/linker.dart'; import 'src/options.dart'; import 'src/output_config.dart'; @@ -34,3 +35,12 @@ part 'src/paths.dart'; part 'src/protobuf_field.dart'; part 'src/service_generator.dart'; part 'src/well_known_types.dart'; + +final FeatureSetDefaults pluginFeatureSetDefaults = + FeatureSetDefaults.fromBuffer( + base64Decode(ProtobufInternalDartEditionDefaults), + ); + +const Edition pluginMinSupportedEdition = Edition.EDITION_PROTO2; + +const Edition pluginMaxSupportedEdition = Edition.EDITION_2024; diff --git a/protoc_plugin/lib/src/base_type.dart b/protoc_plugin/lib/src/base_type.dart index 0f5d08c6..fc1d33c3 100644 --- a/protoc_plugin/lib/src/base_type.dart +++ b/protoc_plugin/lib/src/base_type.dart @@ -65,8 +65,13 @@ class BaseType { String getRepeatedDartTypeIterable(FileGenerator fileGen) => '$coreImportPrefix.Iterable<${getDartType(fileGen)}>'; - factory BaseType(FieldDescriptorProto field, GenerationContext ctx) { + factory BaseType( + FieldDescriptorProto field, + FeatureSet features, + GenerationContext ctx, + ) { String constSuffix; + FieldDescriptorProto_Type type; switch (field.type) { case FieldDescriptorProto_Type.TYPE_BOOL: @@ -191,14 +196,17 @@ class BaseType { ); case FieldDescriptorProto_Type.TYPE_GROUP: - constSuffix = 'G'; - break; case FieldDescriptorProto_Type.TYPE_MESSAGE: - constSuffix = 'M'; - break; + if (features.messageEncoding == FeatureSet_MessageEncoding.DELIMITED) { + constSuffix = 'G'; + type = FieldDescriptorProto_Type.TYPE_GROUP; + } else { + constSuffix = 'M'; + type = FieldDescriptorProto_Type.TYPE_MESSAGE; + } case FieldDescriptorProto_Type.TYPE_ENUM: constSuffix = 'E'; - break; + type = FieldDescriptorProto_Type.TYPE_ENUM; default: throw ArgumentError('unimplemented type: ${field.type.name}'); @@ -210,7 +218,7 @@ class BaseType { } return BaseType._raw( - field.type, + type, constSuffix, generator.classname!, null, diff --git a/protoc_plugin/lib/src/code_generator.dart b/protoc_plugin/lib/src/code_generator.dart index 8d4acad0..fbcbefc3 100644 --- a/protoc_plugin/lib/src/code_generator.dart +++ b/protoc_plugin/lib/src/code_generator.dart @@ -10,10 +10,16 @@ import 'package:fixnum/fixnum.dart'; import 'package:protobuf/protobuf.dart'; import '../names.dart' show lowerCaseFirstLetter; -import '../protoc.dart' show FileGenerator; +import '../protoc.dart' + show + FileGenerator, + pluginFeatureSetDefaults, + pluginMinSupportedEdition, + pluginMaxSupportedEdition; import 'gen/dart_options.pb.dart'; import 'gen/google/api/client.pb.dart'; import 'gen/google/protobuf/compiler/plugin.pb.dart'; +import 'gen/google/protobuf/descriptor.pb.dart'; import 'linker.dart'; import 'options.dart'; import 'output_config.dart'; @@ -58,6 +64,8 @@ abstract class ProtobufContainer { // The generator containing this entity. ProtobufContainer? get parent; + FeatureSet get features; + /// The top-level parent of this entity, or itself if it is a top-level /// entity. ProtobufContainer? get toplevelParent { @@ -86,8 +94,8 @@ class CodeGenerator { Map? optionParsers, OutputConfiguration config = const DefaultOutputConfiguration(), }) async { + final editionDefaults = pluginFeatureSetDefaults; final extensions = ExtensionRegistry(); - Dart_options.registerAllExtensions(extensions); Client.registerAllExtensions(extensions); @@ -118,7 +126,7 @@ class CodeGenerator { // (We may import it even if we don't generate the .pb.dart file.) final generators = []; for (final file in request.protoFile) { - generators.add(FileGenerator(file, options)); + generators.add(FileGenerator(editionDefaults, file, options)); } // Collect field types and importable files. @@ -131,9 +139,12 @@ class CodeGenerator { response.file.addAll(gen.generateFiles(config)); } } - response.supportedFeatures = Int64( - CodeGeneratorResponse_Feature.FEATURE_PROTO3_OPTIONAL.value, - ); + response.supportedFeatures = + Int64(CodeGeneratorResponse_Feature.FEATURE_PROTO3_OPTIONAL.value) | + Int64(CodeGeneratorResponse_Feature.FEATURE_SUPPORTS_EDITIONS.value); + response.minimumEdition = pluginMinSupportedEdition.value; + response.maximumEdition = pluginMaxSupportedEdition.value; + _streamOut.add(response.writeToBuffer()); } } diff --git a/protoc_plugin/lib/src/enum_generator.dart b/protoc_plugin/lib/src/enum_generator.dart index d7e992a8..d6c0f88d 100644 --- a/protoc_plugin/lib/src/enum_generator.dart +++ b/protoc_plugin/lib/src/enum_generator.dart @@ -14,6 +14,9 @@ class EnumGenerator extends ProtobufContainer { @override final ProtobufContainer parent; + @override + final FeatureSet features; + @override final String classname; @@ -50,7 +53,8 @@ class EnumGenerator extends ProtobufContainer { parent.fullName == '' ? descriptor.name : '${parent.fullName}.${descriptor.name}', - _descriptor = descriptor { + _descriptor = descriptor, + features = resolveFeatures(parent.features, descriptor.options.features) { final usedNames = {...reservedEnumNames}; for (var i = 0; i < descriptor.value.length; i++) { final value = descriptor.value[i]; diff --git a/protoc_plugin/lib/src/extension_generator.dart b/protoc_plugin/lib/src/extension_generator.dart index 229d1052..2f95fa62 100644 --- a/protoc_plugin/lib/src/extension_generator.dart +++ b/protoc_plugin/lib/src/extension_generator.dart @@ -24,7 +24,7 @@ class ExtensionGenerator { Set usedNames, int repeatedFieldIndex, int fieldIdTag, - ) : _extensionName = extensionName(_descriptor, usedNames), + ) : _extensionName = extensionName(_descriptor, usedNames, false), _fieldPathSegment = [fieldIdTag, repeatedFieldIndex]; static const _topLevelFieldTag = 7; @@ -71,6 +71,8 @@ class ExtensionGenerator { /// The generator of the .pb.dart file where this extension will be defined. FileGenerator? get fileGen => _parent.fileGen; + FeatureSet get features => _field.features; + String get name { if (!_resolved) throw StateError('resolve not called'); final name = _extensionName; diff --git a/protoc_plugin/lib/src/file_generator.dart b/protoc_plugin/lib/src/file_generator.dart index 63674687..261cf6aa 100644 --- a/protoc_plugin/lib/src/file_generator.dart +++ b/protoc_plugin/lib/src/file_generator.dart @@ -18,8 +18,6 @@ const String _protobufImportUrl = 'package:protobuf/protobuf.dart'; const String _typedDataImportPrefix = r'$typed_data'; const String _typedDataImportUrl = 'dart:typed_data'; -enum ProtoSyntax { proto2, proto3 } - /// Generates the Dart output files for one .proto input file. /// /// Outputs include .pb.dart, pbenum.dart, and .pbjson.dart. @@ -140,14 +138,21 @@ class FileGenerator extends ProtobufContainer { /// Whether cross-references have been resolved. bool _linked = false; - final ProtoSyntax syntax; + final Edition edition; - FileGenerator(this.descriptor, this.options) - : protoFileUri = Uri.file(descriptor.name), - syntax = - descriptor.syntax == 'proto3' - ? ProtoSyntax.proto3 - : ProtoSyntax.proto2 { + @override + final FeatureSet features; + + FileGenerator( + FeatureSetDefaults editionDefaults, + this.descriptor, + this.options, + ) : protoFileUri = Uri.file(descriptor.name), + edition = _getEdition(descriptor), + features = resolveFeatures( + _getEditionDefaults(editionDefaults, _getEdition(descriptor)), + descriptor.options.features, + ) { if (protoFileUri.isAbsolute) { // protoc should never generate an import with an absolute path. throw 'FAILURE: Import with absolute path is not supported'; @@ -825,6 +830,51 @@ class ConditionalConstDefinition { } } +Edition _getEdition(FileDescriptorProto file) { + if (file.edition != Edition.EDITION_UNKNOWN) { + return file.edition; + } + if (file.syntax == 'proto3') { + return Edition.EDITION_PROTO3; + } + return Edition.EDITION_PROTO2; +} + +FeatureSet resolveFeatures(FeatureSet parent, FeatureSet child) { + final result = parent.deepCopy(); + result.mergeFromMessage(child); + return result; +} + +FeatureSet _getEditionDefaults( + FeatureSetDefaults editionDefaults, + Edition edition, +) { + if (edition.value < editionDefaults.minimumEdition.value) { + throw ArgumentError( + 'Edition $edition is earlier than the minimum supported edition ${editionDefaults.minimumEdition}!', + ); + } + if (edition.value > editionDefaults.maximumEdition.value) { + throw ArgumentError( + 'Edition $edition is later than the maximum supported edition ${editionDefaults.maximumEdition}!', + ); + } + FeatureSetDefaults_FeatureSetEditionDefault? found; + for (final d in editionDefaults.defaults) { + if (d.edition.value > edition.value) { + break; + } + found = d; + } + if (found == null) { + throw ArgumentError('No default found for edition $edition!'); + } + final defaults = found.fixedFeatures.deepCopy(); + defaults.mergeFromMessage(found.overridableFeatures); + return defaults; +} + const _fileIgnores = { 'annotate_overrides', 'camel_case_types', diff --git a/protoc_plugin/lib/src/gen/google/protobuf/dart_edition_defaults.pb.dart b/protoc_plugin/lib/src/gen/google/protobuf/dart_edition_defaults.pb.dart new file mode 100755 index 00000000..25306d19 --- /dev/null +++ b/protoc_plugin/lib/src/gen/google/protobuf/dart_edition_defaults.pb.dart @@ -0,0 +1,9 @@ +// Generated with third_party/dart/protoc_plugin/tool/regenerate.sh. +// Copyright (c) 2013, 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. + +// ignore_for_file: constant_identifier_names + +const ProtobufInternalDartEditionDefaults = + 'ChcYhAciACoQCAEQAhgCIAMoATACOAJAAQoXGOcHIgAqEAgCEAEYASACKAEwATgCQAEKFxjoByIMCAEQARgBIAIoATABKgQ4AkABChcY6QciEAgBEAEYASACKAEwATgBQAIqACDmByjpBw=='; diff --git a/protoc_plugin/lib/src/message_generator.dart b/protoc_plugin/lib/src/message_generator.dart index 9345584d..1ff7bea5 100644 --- a/protoc_plugin/lib/src/message_generator.dart +++ b/protoc_plugin/lib/src/message_generator.dart @@ -76,6 +76,9 @@ class MessageGenerator extends ProtobufContainer { @override final ProtobufContainer parent; + @override + final FeatureSet features; + final DescriptorProto _descriptor; final List _enumGenerators = []; final List _messageGenerators = []; @@ -85,13 +88,14 @@ class MessageGenerator extends ProtobufContainer { /// by the index in the containing types's oneof_decl list. /// Only contains the 'real' oneofs. final List> _oneofFields; + final List _oneofFeatures; late List _oneofNames; @override final List fieldPath; // populated by resolve() - late List _fieldList; + late List fieldList; bool _resolved = false; Set _usedTopLevelNames; @@ -116,7 +120,12 @@ class MessageGenerator extends ProtobufContainer { _oneofFields = List.generate( countRealOneofs(descriptor), (int index) => [], - ) { + ), + _oneofFeatures = List.generate( + countRealOneofs(descriptor), + (int index) => FeatureSet(), + ), + features = resolveFeatures(parent.features, descriptor.options.features) { mixin = _getMixin(declaredMixins, defaultMixin); for (var i = 0; i < _descriptor.enumType.length; i++) { final e = _descriptor.enumType[i]; @@ -137,6 +146,13 @@ class MessageGenerator extends ProtobufContainer { ); } + for (var oneof = 0; oneof < _oneofFeatures.length; oneof++) { + _oneofFeatures[oneof] = resolveFeatures( + features, + descriptor.oneofDecl[oneof].options.features, + ); + } + // Extensions within messages won't create top-level classes and don't need // to check against / be added to top-level reserved names. final usedExtensionNames = {...forbiddenExtensionNames}; @@ -237,16 +253,25 @@ class MessageGenerator extends ProtobufContainer { classname, _usedTopLevelNames, reserved: reserved, + lowercaseGroupNames: false, ); - _fieldList = []; + fieldList = []; for (final names in members.fieldNames) { - final field = ProtobufField.message(names, this, ctx); - if (field.descriptor.hasOneofIndex() && - !field.descriptor.proto3Optional) { + final descriptor = names.descriptor; + ProtobufField field; + if (descriptor.hasOneofIndex() && !descriptor.proto3Optional) { + field = ProtobufField.message( + names, + this, + _oneofFeatures[descriptor.oneofIndex], + ctx, + ); _oneofFields[field.descriptor.oneofIndex].add(field); + } else { + field = ProtobufField.message(names, this, features, ctx); } - _fieldList.add(field); + fieldList.add(field); } _oneofNames = members.oneofNames; @@ -260,7 +285,7 @@ class MessageGenerator extends ProtobufContainer { bool get needsFixnumImport { checkResolved(); - for (final field in _fieldList) { + for (final field in fieldList) { if (field.needsFixnumImport) return true; } for (final m in _messageGenerators) { @@ -281,7 +306,7 @@ class MessageGenerator extends ProtobufContainer { Set enumImports, ) { checkResolved(); - for (final field in _fieldList) { + for (final field in fieldList) { final typeGen = field.baseType.generator; if (typeGen is EnumGenerator) { enumImports.add(typeGen.fileGen!); @@ -455,7 +480,7 @@ class MessageGenerator extends ProtobufContainer { out.println('..oo($oneof, $tags)'); } - for (final field in _fieldList) { + for (final field in fieldList) { field.generateBuilderInfoCall(out, package); } @@ -520,9 +545,9 @@ class MessageGenerator extends ProtobufContainer { } void _generateFactory(IndentingWriter out) { - if (!fileGen.options.disableConstructorArgs && _fieldList.isNotEmpty) { + if (!fileGen.options.disableConstructorArgs && fieldList.isNotEmpty) { out.println('factory $classname({'); - for (final field in _fieldList) { + for (final field in fieldList) { _emitDeprecatedIf(field.isDeprecated, out); if (field.isRepeated && !field.isMapField) { out.println( @@ -543,14 +568,14 @@ class MessageGenerator extends ProtobufContainer { } out.print('}) '); - final names = _fieldList.map((f) => f.memberNames!.fieldName).toSet(); + final names = fieldList.map((f) => f.memberNames!.fieldName).toSet(); var result = 'result'; if (names.contains(result)) { result += r'$'; } out.addBlock('{', '}', () { out.println('final $result = create();'); - for (final field in _fieldList) { + for (final field in fieldList) { out.print('if (${field.memberNames!.fieldName} != null) '); if (field.isRepeated && !field.isMapField) { out.println( @@ -601,7 +626,7 @@ class MessageGenerator extends ProtobufContainer { return true; } - for (final field in type._fieldList) { + for (final field in type.fieldList) { if (field.isRequired) { return true; } @@ -620,7 +645,7 @@ class MessageGenerator extends ProtobufContainer { generateOneofAccessors(out, oneof); } - for (final field in _fieldList) { + for (final field in fieldList) { out.println(); generateFieldAccessorsMutators( field, diff --git a/protoc_plugin/lib/src/protobuf_field.dart b/protoc_plugin/lib/src/protobuf_field.dart index 8e90c810..d365d4b7 100644 --- a/protoc_plugin/lib/src/protobuf_field.dart +++ b/protoc_plugin/lib/src/protobuf_field.dart @@ -30,27 +30,47 @@ class ProtobufField { final String fullName; final BaseType baseType; final ProtobufContainer parent; + final FeatureSet features; ProtobufField.message( FieldNames names, ProtobufContainer parent, + FeatureSet inheritFeatures, GenerationContext ctx, - ) : this._(names.descriptor, names, parent, ctx); + ) : this._(names.descriptor, names, parent, inheritFeatures, ctx); ProtobufField.extension( FieldDescriptorProto descriptor, ProtobufContainer parent, GenerationContext ctx, - ) : this._(descriptor, null, parent, ctx); + ) : this._(descriptor, null, parent, parent.features, ctx); ProtobufField._( + FieldDescriptorProto descriptor, + FieldNames? dartNames, + ProtobufContainer parent, + FeatureSet inheritFeatures, + GenerationContext ctx, + ) : this._features( + descriptor, + resolveFeatures( + inheritFeatures, + _inferLegacyProtoFeatures(descriptor, parent.fileGen!.edition), + ), + dartNames, + parent, + ctx, + ); + + ProtobufField._features( this.descriptor, + this.features, FieldNames? dartNames, this.parent, GenerationContext ctx, ) : memberNames = dartNames, fullName = '${parent.fullName}.${descriptor.name}', - baseType = BaseType(descriptor, ctx); + baseType = BaseType(descriptor, features, ctx); /// The index of this field in MessageGenerator.fieldList. /// @@ -71,8 +91,9 @@ class ProtobufField { /// Whether the field is to be encoded with [deprecated = true] encoding. bool get isDeprecated => descriptor.options.deprecated; - bool get isRequired => - descriptor.label == FieldDescriptorProto_Label.LABEL_REQUIRED; + bool get isRequired { + return features.fieldPresence == FeatureSet_FieldPresence.LEGACY_REQUIRED; + } bool get isRepeated => descriptor.label == FieldDescriptorProto_Label.LABEL_REPEATED; @@ -91,20 +112,8 @@ class ProtobufField { return false; } - switch (parent.fileGen!.syntax) { - case ProtoSyntax.proto3: - if (!descriptor.hasOptions()) { - return true; // packed by default in proto3 - } else { - return !descriptor.options.hasPacked() || descriptor.options.packed; - } - case ProtoSyntax.proto2: - if (!descriptor.hasOptions()) { - return false; // not packed by default in proto3 - } else { - return descriptor.options.packed; - } - } + return features.repeatedFieldEncoding == + FeatureSet_RepeatedFieldEncoding.PACKED; } /// Whether the field has the `overrideGetter` annotation set to true. @@ -166,7 +175,7 @@ class ProtobufField { /// Only for map fields: returns the type to use for Dart map field key type. String getDartMapKeyType() { assert(isMapField); - return (baseType.generator as MessageGenerator)._fieldList[0].baseType + return (baseType.generator as MessageGenerator).fieldList[0].baseType .getDartType(parent.fileGen!); } @@ -174,7 +183,7 @@ class ProtobufField { /// type. String getDartMapValueType() { assert(isMapField); - return (baseType.generator as MessageGenerator)._fieldList[1].baseType + return (baseType.generator as MessageGenerator).fieldList[1].baseType .getDartType(parent.fileGen!); } @@ -230,8 +239,8 @@ class ProtobufField { if (isMapField) { final generator = baseType.generator as MessageGenerator; - final key = generator._fieldList[0]; - final value = generator._fieldList[1]; + final key = generator.fieldList[0]; + final value = generator.fieldList[1]; // Key type is an integer type or string. No need to specify the default // value as the library knows the defaults for integer and string fields. @@ -498,3 +507,28 @@ class ProtobufField { ); } } + +FeatureSet _inferLegacyProtoFeatures( + FieldDescriptorProto descriptor, + Edition edition, +) { + if (edition.value >= Edition.EDITION_2023.value) { + return descriptor.options.features; + } + final features = FeatureSet(); + if (descriptor.label == FieldDescriptorProto_Label.LABEL_REQUIRED) { + features.fieldPresence = FeatureSet_FieldPresence.LEGACY_REQUIRED; + } + if (descriptor.type == FieldDescriptorProto_Type.TYPE_GROUP) { + features.messageEncoding = FeatureSet_MessageEncoding.DELIMITED; + } + if (descriptor.options.packed) { + features.repeatedFieldEncoding = FeatureSet_RepeatedFieldEncoding.PACKED; + } + if (edition.value == Edition.EDITION_PROTO3.value && + descriptor.options.hasPacked() && + !descriptor.options.packed) { + features.repeatedFieldEncoding = FeatureSet_RepeatedFieldEncoding.EXPANDED; + } + return features; +} diff --git a/protoc_plugin/lib/src/service_generator.dart b/protoc_plugin/lib/src/service_generator.dart index bc26c115..7e37c217 100644 --- a/protoc_plugin/lib/src/service_generator.dart +++ b/protoc_plugin/lib/src/service_generator.dart @@ -84,7 +84,7 @@ class ServiceGenerator { mg.checkResolved(); if (depth == 0) _deps[mg.dottedName] = mg; _transitiveDeps[mg.dottedName] = mg; - for (final field in mg._fieldList) { + for (final field in mg.fieldList) { if (field.baseType.isGroup || field.baseType.isMessage) { _addDepsRecursively( field.baseType.generator as MessageGenerator, diff --git a/protoc_plugin/test/client_generator_test.dart b/protoc_plugin/test/client_generator_test.dart index 43956643..a8f27113 100644 --- a/protoc_plugin/test/client_generator_test.dart +++ b/protoc_plugin/test/client_generator_test.dart @@ -13,6 +13,7 @@ import 'package:test/test.dart'; import 'src/golden_file.dart'; import 'src/service_util.dart'; +import 'src/test_features.dart'; void main() { test('testClientGenerator', () { @@ -22,13 +23,13 @@ void main() { 'SomeReply', ]); fd.service.add(buildServiceDescriptor()); - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); final fd2 = buildFileDescriptor('foo.bar', 'foobar.proto', [ 'EmptyMessage', 'AnotherReply', ]); - final fg2 = FileGenerator(fd2, options); + final fg2 = FileGenerator(testEditionDefaults, fd2, options); link(GenerationOptions(), [fg, fg2]); diff --git a/protoc_plugin/test/enum_generator_test.dart b/protoc_plugin/test/enum_generator_test.dart index f41bd0a2..894e0ece 100644 --- a/protoc_plugin/test/enum_generator_test.dart +++ b/protoc_plugin/test/enum_generator_test.dart @@ -12,10 +12,16 @@ import 'package:protoc_plugin/src/options.dart'; import 'package:test/test.dart'; import 'src/golden_file.dart'; +import 'src/test_features.dart'; void main() { - test('testEnumGenerator', () { - final ed = + late FileDescriptorProto fd; + late EnumDescriptorProto ed; + late DescriptorProto md; + + setUp(() async { + fd = FileDescriptorProto(); + ed = EnumDescriptorProto() ..name = 'PhoneType' ..value.addAll([ @@ -32,14 +38,58 @@ void main() { ..name = 'BUSINESS' ..number = 2, ]); + md = DescriptorProto()..enumType.add(ed); + }); + + test('testEnumGenerator', () { final writer = IndentingWriter( generateMetadata: true, fileName: 'sample.proto', ); - final fg = FileGenerator(FileDescriptorProto(), GenerationOptions()); + final fg = FileGenerator( + testEditionDefaults, + FileDescriptorProto(), + GenerationOptions(), + ); final eg = EnumGenerator.topLevel(ed, fg, {}, 0); eg.generate(writer); expectGolden(writer.emitSource(format: false), 'enum.pbenum.dart'); expectGolden(writer.sourceLocationInfo.toString(), 'enum.pbenum.dart.meta'); }); + + test('EnumGenerator inherits from a parent file', () { + setTestFeature(fd, 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final eg = EnumGenerator.topLevel(ed, fg, {}, 0); + + expect(getTestFeature(eg.features), 1); + }); + + test('EnumGenerator inherits from a parent message', () { + setTestFeature(md, 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.nested(md, fg, {}, null, {}, 0); + final eg = EnumGenerator.nested(ed, mg, {}, 0); + + expect(getTestFeature(eg.features), 1); + }); + + test('EnumGenerator can override parent file features', () { + setTestFeature(fd, 1); + setTestFeature(ed, 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final eg = EnumGenerator.topLevel(ed, fg, {}, 0); + + expect(getTestFeature(eg.features), 2); + }); + + test('EnumGenerator can override parent message features', () { + setTestFeature(md, 1); + setTestFeature(ed, 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.nested(md, fg, {}, null, {}, 0); + final eg = EnumGenerator.nested(ed, mg, {}, 0); + + expect(getTestFeature(eg.features), 2); + }); } diff --git a/protoc_plugin/test/extension_generator_test.dart b/protoc_plugin/test/extension_generator_test.dart index bd3775aa..1070d7e7 100644 --- a/protoc_plugin/test/extension_generator_test.dart +++ b/protoc_plugin/test/extension_generator_test.dart @@ -15,17 +15,21 @@ import 'package:protoc_plugin/src/options.dart'; import 'package:test/test.dart'; import 'src/golden_file.dart'; +import 'src/test_features.dart'; + +pb.FieldDescriptorProto makeExtension() { + return pb.FieldDescriptorProto() + ..name = 'client_info' + ..jsonName = 'clientInfo' + ..number = 261486461 + ..label = pb.FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = pb.FieldDescriptorProto_Type.TYPE_STRING + ..extendee = '.Card'; +} void main() { test('testExtensionGenerator', () { - final extensionFieldDescriptor = - pb.FieldDescriptorProto() - ..name = 'client_info' - ..jsonName = 'clientInfo' - ..number = 261486461 - ..label = pb.FieldDescriptorProto_Label.LABEL_OPTIONAL - ..type = pb.FieldDescriptorProto_Type.TYPE_STRING - ..extendee = '.Card'; + final extensionFieldDescriptor = makeExtension(); final messageDescriptor = pb.DescriptorProto() ..name = 'Card' @@ -35,7 +39,11 @@ void main() { ..messageType.add(messageDescriptor) ..extension.add(extensionFieldDescriptor); - final fileGenerator = FileGenerator(fileDescriptor, GenerationOptions()); + final fileGenerator = FileGenerator( + testEditionDefaults, + fileDescriptor, + GenerationOptions(), + ); final options = parseGenerationOptions( pb.CodeGeneratorRequest(), pb.CodeGeneratorResponse(), @@ -47,11 +55,68 @@ void main() { ); fileGenerator.extensionGenerators.single.generate(writer); - final actual = writer.emitSource(format: false); - expectGolden(actual, 'extension.pb.dart'); + expectGolden(writer.emitSource(format: false), 'extension.pb.dart'); expectGolden( writer.sourceLocationInfo.toString(), 'extension.pb.dart.meta', ); }); + + test('ExtensionGenerator inherits from a parent file', () { + final ed = makeExtension(); + final fd = setTestFeature( + pb.FileDescriptorProto() + ..edition = pb.Edition.EDITION_2023 + ..extension.add(ed), + 1, + ); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + fg.resolve(GenerationContext(GenerationOptions())); + + expect(getTestFeature(fg.extensionGenerators.single.features), 1); + }); + + test('ExtensionGenerator can override parent file features', () { + final ed = setTestFeature(makeExtension(), 2); + final fd = setTestFeature( + pb.FileDescriptorProto() + ..edition = pb.Edition.EDITION_2023 + ..extension.add(ed), + 1, + ); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + fg.resolve(GenerationContext(GenerationOptions())); + + expect(getTestFeature(fg.extensionGenerators.single.features), 2); + }); + + test('ExtensionGenerator inherits from a parent message', () { + final ed = makeExtension(); + final md = setTestFeature(pb.DescriptorProto()..extension.add(ed), 1); + final fd = + pb.FileDescriptorProto() + ..edition = pb.Edition.EDITION_2023 + ..messageType.add(md); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + final eg = ExtensionGenerator.nested(ed, mg, {}, 0); + eg.resolve(GenerationContext(GenerationOptions())); + + expect(getTestFeature(eg.features), 1); + }); + + test('ExtensionGenerator can override parent message features', () { + final ed = setTestFeature(makeExtension(), 2); + final md = setTestFeature(pb.DescriptorProto()..extension.add(ed), 1); + final fd = + pb.FileDescriptorProto() + ..edition = pb.Edition.EDITION_2023 + ..messageType.add(md); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + final eg = ExtensionGenerator.nested(ed, mg, {}, 0); + eg.resolve(GenerationContext(GenerationOptions())); + + expect(getTestFeature(eg.features), 2); + }); } diff --git a/protoc_plugin/test/feature_set_defaults_test.dart b/protoc_plugin/test/feature_set_defaults_test.dart new file mode 100644 index 00000000..29a6a13b --- /dev/null +++ b/protoc_plugin/test/feature_set_defaults_test.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2025, 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:protoc_plugin/protoc.dart'; +import 'package:test/test.dart'; + +void main() { + // The edition defaults should always stay synchronized with the supported + // edition range we report to protoc. It's not clear that the BUILD file + // definitions are load-bearing though, so we test them here. + test('Plugin supported editions are in sync with the defaults constant', () { + expect(pluginMinSupportedEdition, pluginFeatureSetDefaults.minimumEdition); + expect(pluginMaxSupportedEdition, pluginFeatureSetDefaults.maximumEdition); + }); +} diff --git a/protoc_plugin/test/file_generator_test.dart b/protoc_plugin/test/file_generator_test.dart index 13b931d7..618209a7 100644 --- a/protoc_plugin/test/file_generator_test.dart +++ b/protoc_plugin/test/file_generator_test.dart @@ -15,6 +15,7 @@ import 'package:protoc_plugin/src/options.dart'; import 'package:test/test.dart'; import 'src/golden_file.dart'; +import 'src/test_features.dart'; FileDescriptorProto buildFileDescriptor({ bool phoneNumber = true, @@ -111,7 +112,7 @@ void main() { CodeGeneratorRequest()..parameter = 'disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden( fg.generateMainFile().emitSource(format: true), @@ -127,7 +128,7 @@ void main() { CodeGeneratorRequest()..parameter = 'disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden( fg.generateMainFile().emitSource(format: true), @@ -145,7 +146,7 @@ void main() { ..parameter = 'generate_kythe_info,disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden( fg.generateMainFile().sourceLocationInfo.toString(), @@ -163,7 +164,7 @@ void main() { CodeGeneratorRequest()..parameter = 'disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden(fg.generateJsonFile(), 'oneMessage.pbjson.dart'); }, @@ -177,7 +178,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden( fg.generateMainFile().emitSource(format: true), @@ -197,7 +198,7 @@ void main() { ..parameter = 'generate_kythe_info,disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden( @@ -218,7 +219,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); expectGolden(fg.generateJsonFile(), 'topLevelEnum.pbjson.dart'); }); @@ -232,7 +233,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); final writer = IndentingWriter(); @@ -262,7 +263,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); final writer = IndentingWriter(); @@ -295,7 +296,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); final writer = IndentingWriter(); @@ -330,7 +331,7 @@ void main() { final options = GenerationOptions(useGrpc: true); - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); final writer = IndentingWriter(); @@ -427,7 +428,7 @@ void main() { final options = GenerationOptions(useGrpc: true); - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [fg]); final writer = IndentingWriter(); @@ -547,11 +548,11 @@ void main() { final response = CodeGeneratorResponse(); final options = parseGenerationOptions(request, response)!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); link(options, [ fg, - FileGenerator(fd1, options), - FileGenerator(fd2, options), + FileGenerator(testEditionDefaults, fd1, options), + FileGenerator(testEditionDefaults, fd2, options), ]); expectGolden( fg.generateMainFile().emitSource(format: true), @@ -562,4 +563,114 @@ void main() { 'imports.pbjson.dart', ); }); + + test('FileGenerator rejects files without valid edition defaults', () { + final fd = buildFileDescriptor(); + final editionDefaults = + FeatureSetDefaults() + ..defaults.add( + FeatureSetDefaults_FeatureSetEditionDefault() + ..edition = Edition.EDITION_2023 + ..overridableFeatures = + testEditionDefaults.defaults[0].overridableFeatures, + ) + ..minimumEdition = Edition.EDITION_PROTO2 + ..maximumEdition = Edition.EDITION_2023; + + expect( + () => FileGenerator(editionDefaults, fd, GenerationOptions()), + throwsA( + const TypeMatcher().having( + (e) => e.message, + 'message', + allOf(contains('No default found'), contains('EDITION_PROTO2')), + ), + ), + ); + }); + + test('FileGenerator rejects files before the minimum supported edition', () { + final fd = buildFileDescriptor(); + final editionDefaults = + FeatureSetDefaults() + ..defaults.addAll(testEditionDefaults.defaults.sublist(1)) + ..minimumEdition = Edition.EDITION_PROTO3 + ..maximumEdition = Edition.EDITION_2023; + + expect( + () => FileGenerator(editionDefaults, fd, GenerationOptions()), + throwsA( + const TypeMatcher().having( + (e) => e.message, + 'message', + contains('earlier than the minimum'), + ), + ), + ); + }); + + test('FileGenerator rejects files after the maximum supported edition', () { + final fd = buildFileDescriptor()..edition = Edition.EDITION_2023; + final editionDefaults = + FeatureSetDefaults() + ..defaults.addAll(testEditionDefaults.defaults) + ..minimumEdition = Edition.EDITION_PROTO2 + ..maximumEdition = Edition.EDITION_PROTO3; + + expect( + () => FileGenerator(editionDefaults, fd, GenerationOptions()), + throwsA( + const TypeMatcher().having( + (e) => e.message, + 'message', + contains('later than the maximum'), + ), + ), + ); + }); + + test('FileGenerator initializes the file-level edition defaults', () { + final fd = buildFileDescriptor(); + final editionDefaults = testEditionDefaults.deepCopy(); + setTestFeature(editionDefaults.defaults[0].overridableFeatures, 1); + + final fg = FileGenerator(editionDefaults, fd, GenerationOptions()); + expect(fg.features.enumType, FeatureSet_EnumType.CLOSED); + expect(fg.features.fieldPresence, FeatureSet_FieldPresence.EXPLICIT); + expect( + fg.features.messageEncoding, + FeatureSet_MessageEncoding.LENGTH_PREFIXED, + ); + expect(fg.features.utf8Validation, FeatureSet_Utf8Validation.NONE); + expect( + fg.features.repeatedFieldEncoding, + FeatureSet_RepeatedFieldEncoding.EXPANDED, + ); + expect(fg.features.jsonFormat, FeatureSet_JsonFormat.LEGACY_BEST_EFFORT); + expect(getTestFeature(fg.features), 1); + }); + + test('FileGenerator uses file-level overrides', () { + final fd = setTestFeature( + buildFileDescriptor()..edition = Edition.EDITION_2023, + 2, + ); + final editionDefaults = testEditionDefaults.deepCopy(); + setTestFeature(editionDefaults.defaults[0].overridableFeatures, 1); + + final fg = FileGenerator(editionDefaults, fd, GenerationOptions()); + expect(fg.features.enumType, FeatureSet_EnumType.OPEN); + expect(fg.features.fieldPresence, FeatureSet_FieldPresence.EXPLICIT); + expect( + fg.features.messageEncoding, + FeatureSet_MessageEncoding.LENGTH_PREFIXED, + ); + expect(fg.features.utf8Validation, FeatureSet_Utf8Validation.VERIFY); + expect( + fg.features.repeatedFieldEncoding, + FeatureSet_RepeatedFieldEncoding.PACKED, + ); + expect(fg.features.jsonFormat, FeatureSet_JsonFormat.ALLOW); + expect(getTestFeature(fg.features), 2); + }); } diff --git a/protoc_plugin/test/import_option_test.dart b/protoc_plugin/test/import_option_test.dart new file mode 100644 index 00000000..fd7324d5 --- /dev/null +++ b/protoc_plugin/test/import_option_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2019, 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 'gen/custom_option.pb.dart'; +import 'gen/import_option.pbjson.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:protoc_plugin/src/gen/google/protobuf/descriptor.pb.dart'; +import 'package:test/test.dart'; + +void main() { + test('can read custom options from linked import option', () { + final registry = ExtensionRegistry()..add(Custom_option.myOption); + final descriptor = DescriptorProto.fromBuffer( + messageWithOptionsDescriptor, + registry, + ); + final option = descriptor.options.getExtension(Custom_option.myOption); + expect(option, 'Hello world!'); + }); + + test('unlinked options are in unknown fields', () { + final registry = ExtensionRegistry()..add(Custom_option.myOption); + final descriptor = DescriptorProto.fromBuffer( + messageWithOptionsDescriptor, + registry, + ); + final ufs = descriptor.options.unknownFields; + expect(ufs.hasField(51235), true); + expect(ufs.getField(51235)!.varints.length, 1); + expect(ufs.getField(51235)!.varints.first, Int64(99)); + }); +} diff --git a/protoc_plugin/test/message_generator_test.dart b/protoc_plugin/test/message_generator_test.dart index 5585e5c3..782cb786 100644 --- a/protoc_plugin/test/message_generator_test.dart +++ b/protoc_plugin/test/message_generator_test.dart @@ -17,6 +17,7 @@ import 'package:protoc_plugin/src/options.dart'; import 'package:test/test.dart'; import 'src/golden_file.dart'; +import 'src/test_features.dart'; void main() { late FileDescriptorProto fd; @@ -86,7 +87,7 @@ void main() { CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); final ctx = GenerationContext(options); @@ -119,7 +120,7 @@ void main() { CodeGeneratorRequest()..parameter = 'disable_constructor_args', CodeGeneratorResponse(), )!; - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); final ctx = GenerationContext(options); @@ -162,4 +163,191 @@ void main() { expect(annotatedName, isIn(expectedStrings)); } }); + + test('MessageGenerator inherits from a parent file', () { + setTestFeature(fd, 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + expect(getTestFeature(mg.features), 1); + }); + + test('MessageGenerator can override parent file features', () { + setTestFeature(fd, 1); + setTestFeature(md, 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + expect(getTestFeature(mg.features), 2); + }); + + test('MessageGenerator inherits from a parent message', () { + final mdParent = setTestFeature(md.deepCopy(), 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mgParent = MessageGenerator.topLevel( + mdParent, + fg, + {}, + null, + {}, + 0, + ); + final mg = MessageGenerator.nested(md, mgParent, {}, null, {}, 0); + + expect(getTestFeature(mg.features), 1); + }); + + test('MessageGenerator can override parent message features', () { + final mdParent = setTestFeature(md.deepCopy(), 1); + setTestFeature(md, 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mgParent = MessageGenerator.topLevel( + mdParent, + fg, + {}, + null, + {}, + 0, + ); + final mg = MessageGenerator.nested(md, mgParent, {}, null, {}, 0); + + expect(getTestFeature(mg.features), 2); + }); + + test('MessageGenerator fields inherit from a parent message', () { + fd.edition = Edition.EDITION_2023; + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md, 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 1); + }); + + test('MessageGenerator fields can override parent message features', () { + fd.edition = Edition.EDITION_2023; + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md, 1); + setTestFeature(md.field[0], 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 2); + }); + + test('MessageGenerator fields inherit from a parent oneof', () { + fd.edition = Edition.EDITION_2023; + md.oneofDecl.add(OneofDescriptorProto()..name = 'oneof'); + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..oneofIndex = 0 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md.oneofDecl[0], 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 1); + }); + + test('MessageGenerator fields can override parent oneof', () { + fd.edition = Edition.EDITION_2023; + md.oneofDecl.add(OneofDescriptorProto()..name = 'oneof'); + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..oneofIndex = 0 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md.oneofDecl[0], 1); + setTestFeature(md.field[0], 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 2); + }); + + test('MessageGenerator oneof inherits from a parent message', () { + fd.edition = Edition.EDITION_2023; + md.oneofDecl.add(OneofDescriptorProto()..name = 'oneof'); + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..oneofIndex = 0 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md, 1); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 1); + }); + + test('MessageGenerator oneof can override parent message', () { + fd.edition = Edition.EDITION_2023; + md.oneofDecl.add(OneofDescriptorProto()..name = 'oneof'); + (md.field..clear()).add( + FieldDescriptorProto() + ..name = 'number' + ..jsonName = 'number' + ..number = 1 + ..oneofIndex = 0 + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_STRING, + ); + setTestFeature(md, 1); + setTestFeature(md.oneofDecl[0], 2); + final fg = FileGenerator(testEditionDefaults, fd, GenerationOptions()); + final mg = MessageGenerator.topLevel(md, fg, {}, null, {}, 0); + + final ctx = GenerationContext(GenerationOptions()); + mg.register(ctx); + mg.resolve(ctx); + + expect(getTestFeature(mg.fieldList[0].features), 2); + }); } diff --git a/protoc_plugin/test/protos/custom_option_unlinked.proto b/protoc_plugin/test/protos/custom_option_unlinked.proto new file mode 100644 index 00000000..2ab39f6e --- /dev/null +++ b/protoc_plugin/test/protos/custom_option_unlinked.proto @@ -0,0 +1,15 @@ +// Copyright (c) 2020, 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. + +// This file defines an option that is not linked into the test, to verify that +// it ends up in unknown fields as expected. +syntax = "proto2"; + +package custom_option; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.MessageOptions { + optional int32 unlinked_option = 51235; +} diff --git a/protoc_plugin/test/protos/import_option.proto b/protoc_plugin/test/protos/import_option.proto new file mode 100644 index 00000000..b2c1d402 --- /dev/null +++ b/protoc_plugin/test/protos/import_option.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package import_option; + +import option "custom_option.proto"; +import option "custom_option_unlinked.proto"; + +message MessageWithOptions { + option (custom_option.my_option) = "Hello world!"; + option (custom_option.unlinked_option) = 99; + + string a = 1; +} diff --git a/protoc_plugin/test/service_generator_test.dart b/protoc_plugin/test/service_generator_test.dart index 52c2401c..ca3b8a1a 100644 --- a/protoc_plugin/test/service_generator_test.dart +++ b/protoc_plugin/test/service_generator_test.dart @@ -13,6 +13,7 @@ import 'package:test/test.dart'; import 'src/golden_file.dart'; import 'src/service_util.dart'; +import 'src/test_features.dart'; void main() { test('testServiceGenerator', () { @@ -22,13 +23,13 @@ void main() { 'SomeReply', ]); fd.service.add(buildServiceDescriptor()); - final fg = FileGenerator(fd, options); + final fg = FileGenerator(testEditionDefaults, fd, options); final fd2 = buildFileDescriptor('foo.bar', 'foobar.proto', [ 'EmptyMessage', 'AnotherReply', ]); - final fg2 = FileGenerator(fd2, options); + final fg2 = FileGenerator(testEditionDefaults, fd2, options); link(GenerationOptions(), [fg, fg2]); diff --git a/protoc_plugin/test/src/test_features.dart b/protoc_plugin/test/src/test_features.dart new file mode 100644 index 00000000..71062586 --- /dev/null +++ b/protoc_plugin/test/src/test_features.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2025, 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:protoc_plugin/protoc.dart'; +import 'package:protoc_plugin/src/gen/google/protobuf/descriptor.pb.dart'; +import 'package:protoc_plugin/src/gen/google/protobuf/unittest_features.pb.dart'; + +final testEditionDefaults = pluginFeatureSetDefaults; + +// Sets a test-only feature extension that can be applied to any type of descriptor. +dynamic setTestFeature(dynamic descriptor, int value) { + var features = descriptor; + if (descriptor is! FeatureSet) { + descriptor.ensureOptions(); + descriptor.options.ensureFeatures(); + features = descriptor.options.features; + } + features.setExtension( + Unittest_features.test, + TestFeatures()..multipleFeature = EnumFeature.valueOf(value)!, + ); + return descriptor; +} + +// Retrieves a test-only feature extension on an arbitrary descriptor. +int getTestFeature(FeatureSet features) { + return features.getExtension(Unittest_features.test).multipleFeature.value; +} diff --git a/tool/setup.sh b/tool/setup.sh index 16e7b9cd..b20a1491 100755 --- a/tool/setup.sh +++ b/tool/setup.sh @@ -1,7 +1,7 @@ #!/bin/bash wget -O protoc.zip \ - https://github.com/protocolbuffers/protobuf/releases/download/v31.0/protoc-31.0-linux-x86_64.zip + https://github.com/protocolbuffers/protobuf/releases/download/v32.1/protoc-32.1-linux-x86_64.zip unzip -d protoc protoc.zip if [[ -z "${GITHUB_ENV}" ]]; then