diff --git a/protoc_plugin/lib/file_generator.dart b/protoc_plugin/lib/file_generator.dart index 91c357c25..a1e6f3bd2 100644 --- a/protoc_plugin/lib/file_generator.dart +++ b/protoc_plugin/lib/file_generator.dart @@ -104,7 +104,9 @@ class FileGenerator extends ProtobufContainer { final FileDescriptorProto descriptor; final GenerationOptions options; - // The relative path used to import the .proto file, as a URI. + // The path used to import the .proto file, as a URI. + // This is a package: URI if a `dart_package` option is given. + // Otherwise this is a relative path. final Uri protoFileUri; final enumGenerators = []; @@ -130,13 +132,22 @@ class FileGenerator extends ProtobufContainer { /// True if cross-references have been resolved. bool _linked = false; - FileGenerator(this.descriptor, this.options) - : protoFileUri = Uri.file(descriptor.name) { - if (protoFileUri.isAbsolute) { - // protoc should never generate an import with an absolute path. - throw "FAILURE: Import with absolute path is not supported"; + static Uri calculateUri(FileDescriptorProto descriptor) { + // protoc should never generate an import with an absolute path. + assert(!Uri.file(descriptor.name).isAbsolute, + "Import with absolute path is not supported"); + + if (descriptor.options.hasExtension(Dart_options.dartPackage)) { + String dartPackage = + descriptor.options.getExtension(Dart_options.dartPackage); + return Uri(scheme: 'package', path: '${dartPackage}/${descriptor.name}'); + } else { + return Uri.file(descriptor.name); } + } + FileGenerator(this.descriptor, this.options) + : protoFileUri = calculateUri(descriptor) { var declaredMixins = _getDeclaredMixins(descriptor); var defaultMixinName = descriptor.options?.getExtension(Dart_options.defaultMixin) ?? ''; @@ -194,15 +205,18 @@ class FileGenerator extends ProtobufContainer { FileGenerator get fileGen => this; List get fieldPath => []; + Uri outputFile(OutputConfiguration config, String extension) { + Uri protoUrl = Uri.file(descriptor.name); + return config.outputPathFor(protoUrl, extension); + } + /// Generates all the Dart files for this .proto file. List generateFiles(OutputConfiguration config) { if (!_linked) throw StateError("not linked"); makeFile(String extension, String content) { - Uri protoUrl = Uri.file(descriptor.name); - Uri dartUrl = config.outputPathFor(protoUrl, extension); return CodeGeneratorResponse_File() - ..name = dartUrl.path + ..name = outputFile(config, extension).path ..content = content; } diff --git a/protoc_plugin/lib/src/dart_options.pb.dart b/protoc_plugin/lib/src/dart_options.pb.dart index ad2b620df..977301514 100644 --- a/protoc_plugin/lib/src/dart_options.pb.dart +++ b/protoc_plugin/lib/src/dart_options.pb.dart @@ -110,6 +110,11 @@ class Imports extends $pb.GeneratedMessage { } class Dart_options { + static final $pb.Extension dartPackage = $pb.Extension<$core.String>( + 'google.protobuf.FileOptions', + 'dartPackage', + 28205290, + $pb.PbFieldType.OS); static final $pb.Extension imports = $pb.Extension( 'google.protobuf.FileOptions', 'imports', 28125061, $pb.PbFieldType.OM, defaultOrMaker: Imports.getDefault, subBuilder: Imports.create); @@ -143,6 +148,7 @@ class Dart_options { static final $pb.Extension dartName = $pb.Extension<$core.String>( 'google.protobuf.FieldOptions', 'dartName', 28700919, $pb.PbFieldType.OS); static void registerAllExtensions($pb.ExtensionRegistry registry) { + registry.add(dartPackage); registry.add(imports); registry.add(defaultMixin); registry.add(mixin); diff --git a/protoc_plugin/protos/dart_options.proto b/protoc_plugin/protos/dart_options.proto index 507c7f3b7..ffdffefa0 100644 --- a/protoc_plugin/protos/dart_options.proto +++ b/protoc_plugin/protos/dart_options.proto @@ -41,6 +41,9 @@ message Imports { } extend google.protobuf.FileOptions { + // The Dart package that should be used when importing code generated from + // this file. + optional string dart_package = 1073; optional Imports imports = 28125061; diff --git a/protoc_plugin/test/file_generator_test.dart b/protoc_plugin/test/file_generator_test.dart index 12f0a946f..c43cfb9b8 100644 --- a/protoc_plugin/test/file_generator_test.dart +++ b/protoc_plugin/test/file_generator_test.dart @@ -6,6 +6,7 @@ library file_generator_test; import 'package:protoc_plugin/indenting_writer.dart'; +import 'package:protoc_plugin/src/dart_options.pb.dart'; import 'package:protoc_plugin/src/descriptor.pb.dart'; import 'package:protoc_plugin/src/plugin.pb.dart'; import 'package:protoc_plugin/protoc.dart'; @@ -405,4 +406,127 @@ void main() { expectMatchesGoldenFile( fg.generateEnumFile().toString(), 'test/goldens/imports.pbjson'); }); + + test('FileGenerator generates correct dart package imports', () { + // This defines a.proto, same_package.proto, other_package.proto, + // no_package.proto + // + // a.proto: + // --------------- + // import "dart_options.proto"; + // option (dart_options.dart_package) = 'abc'; + // message A { + // } + // + // same_package.proto: + // --------------- + // import "a.proto"; + // import "dart_options.proto"; + // option (dart_options.dart_package) = 'abc'; + // + // message B { + // optional A a = 1; + // } + // + // other_package.proto: + // --------------- + // import "a.proto"; + // import "dart_options.proto"; + // option (dart_options.dart_package) = 'def'; + // message C { + // optional A a = 1; + // } + // + // no_package.proto: + // --------------- + // import "a.proto"; + // message D { + // optional A a = 1; + // } + // + // import_no_package.proto + // --------------- + // import "no_package.proto"; + // import "dart_options.proto"; + // option (dart_options.dart_package) = 'abc'; + // + // message E { + // optional D d = 1; + // } + + FieldDescriptorProto makeField(String name, String typeName, + {int number = 1}) { + return FieldDescriptorProto() + ..name = name + ..jsonName = name + ..number = number + ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL + ..type = FieldDescriptorProto_Type.TYPE_MESSAGE + ..typeName = typeName; + } + + DescriptorProto mA = DescriptorProto()..name = 'A'; + FileDescriptorProto fda = FileDescriptorProto() + ..name = 'a.proto' + ..messageType.add(mA) + ..options = + (FileOptions()..setExtension(Dart_options.dartPackage, 'abc')); + + DescriptorProto mB = DescriptorProto() + ..name = 'B' + ..field.addAll([makeField('a', '.A')]); + FileDescriptorProto fdSamePackage = FileDescriptorProto() + ..name = 'same_package.proto' + ..messageType.add(mB) + ..dependency.add('a.proto') + ..options = + (FileOptions()..setExtension(Dart_options.dartPackage, 'abc')); + + DescriptorProto mC = DescriptorProto() + ..name = 'C' + ..field.add(makeField('a', '.A')); + FileDescriptorProto fdOtherPackage = FileDescriptorProto() + ..name = 'other_package.proto' + ..messageType.add(mC) + ..dependency.add('a.proto') + ..options = + (FileOptions()..setExtension(Dart_options.dartPackage, 'def')); + + DescriptorProto mD = DescriptorProto() + ..name = 'D' + ..field.add(makeField('a', '.A')); + FileDescriptorProto fdNoPackage = FileDescriptorProto() + ..name = 'no_package.proto' + ..dependency.add('a.proto') + ..messageType.add(mD); + + DescriptorProto mE = DescriptorProto() + ..name = 'E' + ..field.add(makeField('d', '.D')); + FileDescriptorProto fdImportNoPackage = FileDescriptorProto() + ..name = 'import_no_package.proto' + ..dependency.add('no_package.proto') + ..messageType.add(mE); + + void testFile(FileDescriptorProto fileDescriptor, + List dependencies, Uri expectedFilePath) { + final options = parseGenerationOptions( + CodeGeneratorRequest(), CodeGeneratorResponse()); + final fileGenerators = + dependencies.map((dep) => FileGenerator(dep, options)).toList(); + final fg = FileGenerator(fileDescriptor, options); + fileGenerators.add(fg); + link(options, fileGenerators); + final file = fg.outputFile(DefaultOutputConfiguration(), '.pb.dart'); + expectMatchesGoldenFile( + fg.generateMainFile().toString(), 'test/goldens/${file.path}.golden'); + expect(file, expectedFilePath); + } + + testFile(fdSamePackage, [fda], Uri.file('same_package.pb.dart')); + testFile(fdOtherPackage, [fda], Uri.file('other_package.pb.dart')); + testFile(fdNoPackage, [fda], Uri.file('no_package.pb.dart')); + testFile(fdImportNoPackage, [fdNoPackage, fda], + Uri.file('import_no_package.pb.dart')); + }); } diff --git a/protoc_plugin/test/goldens/no_package.pb.dart b/protoc_plugin/test/goldens/no_package.pb.dart new file mode 100644 index 000000000..a2f41788b --- /dev/null +++ b/protoc_plugin/test/goldens/no_package.pb.dart @@ -0,0 +1,46 @@ +/// +// Generated code. Do not modify. +// source: no_package.proto +// +// @dart = 2.3 +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:abc/a.pb.dart' as $3; + +class D extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo('D', createEmptyInstance: create) + ..aOM<$3.A>(1, 'a', subBuilder: $3.A.create) + ..hasRequiredFields = false + ; + + D._() : super(); + factory D() => create(); + factory D.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory D.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + D clone() => D()..mergeFromMessage(this); + D copyWith(void Function(D) updates) => super.copyWith((message) => updates(message as D)); + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static D create() => D._(); + D createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static D getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static D _defaultInstance; + + @$pb.TagNumber(1) + $3.A get a => $_getN(0); + @$pb.TagNumber(1) + set a($3.A v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasA() => $_has(0); + @$pb.TagNumber(1) + void clearA() => clearField(1); + @$pb.TagNumber(1) + $3.A ensureA() => $_ensure(0); +} + diff --git a/protoc_plugin/test/goldens/other_package.pb.dart b/protoc_plugin/test/goldens/other_package.pb.dart new file mode 100644 index 000000000..0723ebfe9 --- /dev/null +++ b/protoc_plugin/test/goldens/other_package.pb.dart @@ -0,0 +1,46 @@ +/// +// Generated code. Do not modify. +// source: other_package.proto +// +// @dart = 2.3 +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:abc/a.pb.dart' as $3; + +class C extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo('C', createEmptyInstance: create) + ..aOM<$3.A>(1, 'a', subBuilder: $3.A.create) + ..hasRequiredFields = false + ; + + C._() : super(); + factory C() => create(); + factory C.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory C.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + C clone() => C()..mergeFromMessage(this); + C copyWith(void Function(C) updates) => super.copyWith((message) => updates(message as C)); + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static C create() => C._(); + C createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static C getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static C _defaultInstance; + + @$pb.TagNumber(1) + $3.A get a => $_getN(0); + @$pb.TagNumber(1) + set a($3.A v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasA() => $_has(0); + @$pb.TagNumber(1) + void clearA() => clearField(1); + @$pb.TagNumber(1) + $3.A ensureA() => $_ensure(0); +} + diff --git a/protoc_plugin/test/goldens/same_package.pb.dart b/protoc_plugin/test/goldens/same_package.pb.dart new file mode 100644 index 000000000..ba81546ae --- /dev/null +++ b/protoc_plugin/test/goldens/same_package.pb.dart @@ -0,0 +1,46 @@ +/// +// Generated code. Do not modify. +// source: same_package.proto +// +// @dart = 2.3 +// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'a.pb.dart' as $3; + +class B extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo('B', createEmptyInstance: create) + ..aOM<$3.A>(1, 'a', subBuilder: $3.A.create) + ..hasRequiredFields = false + ; + + B._() : super(); + factory B() => create(); + factory B.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory B.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + B clone() => B()..mergeFromMessage(this); + B copyWith(void Function(B) updates) => super.copyWith((message) => updates(message as B)); + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static B create() => B._(); + B createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static B getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static B _defaultInstance; + + @$pb.TagNumber(1) + $3.A get a => $_getN(0); + @$pb.TagNumber(1) + set a($3.A v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasA() => $_has(0); + @$pb.TagNumber(1) + void clearA() => clearField(1); + @$pb.TagNumber(1) + $3.A ensureA() => $_ensure(0); +} +