[jnigen] Support renaming interface mixins (#3528)
diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md
index c84c5e9..8a704fe 100644
--- a/pkgs/jnigen/CHANGELOG.md
+++ b/pkgs/jnigen/CHANGELOG.md
@@ -14,6 +14,7 @@
- Flip `isExcluded` to `isIncluded`.
- Make a bunch of nullable fields non-null, if null was functionally
identical to a default value.
+ - Allow interface mixin names to be customized using the visitor API.
## 0.17.0
diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
index 58969b4..32b6502 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
@@ -449,7 +449,6 @@
),
);
final implementsClause = {superName, ...interfaces}.join(', ');
- final implClassName = '\$$name';
final typeParamsDef = node.allTypeParams
.accept(const _TypeParamDef())
.join(', ')
@@ -493,9 +492,10 @@
node.compareTo?.accept(_ComparatorGenerator(resolver, instanceSink));
if (node.declKind == DeclKind.interfaceKind) {
+ final interfaceMixinName = node.finalInterfaceMixinName;
s.write('''
/// Maps a specific port to the implemented interface.
- static final $_core.Map<$_core.int, $implClassName> _\$impls = {};
+ static final $_core.Map<$_core.int, $interfaceMixinName> _\$impls = {};
''');
s.write('''
static $_jni.JObjectPtr _\$invoke(
@@ -540,7 +540,7 @@
static void implementIn$typeParamsDef(
$_jni.JImplementer implementer,
- $implClassName$typeParamsCall \$impl,
+ $interfaceMixinName$typeParamsCall \$impl,
) {
late final $_jni.RawReceivePort \$p;
\$p = $_jni.RawReceivePort((\$m) {
@@ -564,7 +564,7 @@
final interfaceAsyncMethod = _InterfaceIfAsyncMethod(
resolver,
s,
- implClassName: implClassName,
+ implClassName: interfaceMixinName,
);
for (final method in node.methods) {
method.accept(interfaceAsyncMethod);
@@ -577,7 +577,7 @@
}
factory $name.implement(
- $implClassName$typeParamsCall \$impl,
+ $interfaceMixinName$typeParamsCall \$impl,
) {
final \$i = $_jni.JImplementer();
implementIn(\$i, \$impl);
@@ -608,16 +608,17 @@
// Abstract and concrete Impl class definition.
// Used for interface implementation.
if (node.declKind == DeclKind.interfaceKind) {
+ final interfaceMixinName = node.finalInterfaceMixinName;
// Abstract Impl class.
final abstractFactoryArgs = node.methods
.accept(_AbstractImplFactoryArg(resolver))
.join(_newLine(depth: 2))
.encloseIfNotEmpty('{', '}');
s.write('''
-abstract base mixin class $implClassName$typeParamsDef {
- factory $implClassName(
+abstract base mixin class $interfaceMixinName$typeParamsDef {
+ factory $interfaceMixinName(
$abstractFactoryArgs
- ) = _$implClassName$typeParamsCall;
+ ) = _$interfaceMixinName$typeParamsCall;
''');
final abstractImplMethod = _AbstractImplMethod(resolver, s);
@@ -638,8 +639,8 @@
.encloseIfNotEmpty(' : ', '');
s.write('''
-final class _$implClassName$typeParamsDef with $implClassName$typeParamsCall {
- _$implClassName(
+final class _$interfaceMixinName$typeParamsDef with $interfaceMixinName$typeParamsCall {
+ _$interfaceMixinName(
$concreteCtorArgs
)$setClosures;
diff --git a/pkgs/jnigen/lib/src/bindings/renamer.dart b/pkgs/jnigen/lib/src/bindings/renamer.dart
index e4e19f0..dfc1ca2 100644
--- a/pkgs/jnigen/lib/src/bindings/renamer.dart
+++ b/pkgs/jnigen/lib/src/bindings/renamer.dart
@@ -181,16 +181,23 @@
class _ClassRenamer implements Visitor<ClassDecl, void> {
final Config config;
final Set<ClassDecl> renamed;
- final Map<String, int> topLevelNameCounts = {
- ..._definedSyms,
- ..._reservedTopLevelNames,
- };
+ final Map<String, Map<String, int>> topLevelNameCounts = {};
final Map<ClassDecl, Map<String, int>> nameCounts = {};
_ClassRenamer(
this.config,
) : renamed = {...config.importedClasses.values};
+ Map<String, int> _getTopLevelNameCounts(ClassDecl node) {
+ return topLevelNameCounts.putIfAbsent(
+ node.path,
+ () => {
+ ..._definedSyms,
+ ..._reservedTopLevelNames,
+ },
+ );
+ }
+
@override
void visit(ClassDecl node) {
if (renamed.contains(node)) return;
@@ -217,13 +224,25 @@
final className =
'$outerClassName${_preprocess(node.userDefinedName ?? node.name)}';
- // When generating all the classes in a single file
- // the names need to be unique.
- final uniquifyName =
- config.output.dart.structure == OutputStructure.singleFile;
- node.finalName = uniquifyName
- ? _renameConflict(topLevelNameCounts, className, _ElementKind.klass)
- : className;
+ final generatedFileNameCounts = _getTopLevelNameCounts(node);
+
+ node.finalName = _renameConflict(
+ generatedFileNameCounts,
+ className,
+ _ElementKind.klass,
+ );
+
+ if (node.declKind == DeclKind.interfaceKind) {
+ final interfaceMixinName = node.userDefinedInterfaceMixinName == null
+ ? '\$${node.finalName}'
+ : _preprocess(node.userDefinedInterfaceMixinName!);
+
+ node.finalInterfaceMixinName = _renameConflict(
+ generatedFileNameCounts,
+ interfaceMixinName,
+ _ElementKind.klass,
+ );
+ }
if (node.userDefinedName == null ||
node.userDefinedName == node.finalName) {
@@ -238,7 +257,7 @@
// method will be renamed.
final fieldRenamer = _FieldRenamer(
config,
- uniquifyName && node.isTopLevel ? topLevelNameCounts : nameCounts[node]!,
+ node.isTopLevel ? generatedFileNameCounts : nameCounts[node]!,
);
for (final field in node.fields) {
field.accept(fieldRenamer);
@@ -246,7 +265,7 @@
final methodRenamer = _MethodRenamer(
config,
- uniquifyName && node.isTopLevel ? topLevelNameCounts : nameCounts[node]!,
+ node.isTopLevel ? generatedFileNameCounts : nameCounts[node]!,
node.declKind == DeclKind.interfaceKind,
);
for (final method in node.methods) {
diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart
index 6fc037f..5250d1d 100644
--- a/pkgs/jnigen/lib/src/elements/elements.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.dart
@@ -108,6 +108,9 @@
@JsonKey(includeFromJson: false)
String? userDefinedName;
+ @JsonKey(includeFromJson: false)
+ String? userDefinedInterfaceMixinName;
+
@override
final Set<String> modifiers;
@@ -149,6 +152,9 @@
@override
late String finalName;
+ @JsonKey(includeFromJson: false)
+ late String finalInterfaceMixinName;
+
/// Name of the type class.
@JsonKey(includeFromJson: false)
String get typeClassName => '\$$finalName\$Type\$';
diff --git a/pkgs/jnigen/lib/src/elements/j_elements.dart b/pkgs/jnigen/lib/src/elements/j_elements.dart
index 49ccced..31d9dbd 100644
--- a/pkgs/jnigen/lib/src/elements/j_elements.dart
+++ b/pkgs/jnigen/lib/src/elements/j_elements.dart
@@ -110,6 +110,15 @@
/// The original name of the class in Java.
String get originalName => _classDecl.name;
+ /// The custom name of the mixin generated for implementing this Java
+ /// interface
+ ///
+ /// If null, the default generated name is used.
+ String? get interfaceMixinName => _classDecl.userDefinedInterfaceMixinName;
+
+ set interfaceMixinName(String? newName) =>
+ _classDecl.userDefinedInterfaceMixinName = newName;
+
@override
void accept(Visitor visitor) {
visitor.visitClass(this);
diff --git a/pkgs/jnigen/test/renamer_test.dart b/pkgs/jnigen/test/renamer_test.dart
index 8ebbee6..4fe63f2 100644
--- a/pkgs/jnigen/test/renamer_test.dart
+++ b/pkgs/jnigen/test/renamer_test.dart
@@ -21,13 +21,18 @@
}).toList();
}
-Future<void> rename(Classes classes) async {
+Future<void> rename(
+ Classes classes, {
+ OutputStructure structure = OutputStructure.singleFile,
+}) async {
final config = Config(
input: Input(classes: []),
output: Output(
dart: DartCodeOutput(
- path: Uri.file('test.dart'),
- structure: OutputStructure.singleFile,
+ path: structure == OutputStructure.singleFile
+ ? Uri.file('test.dart')
+ : Uri.directory('test_output/'),
+ structure: structure,
),
),
);
@@ -303,6 +308,67 @@
expect(classRenamedMethods, [r'implement', r'implementIn']);
});
+ test('Interface mixin names', () async {
+ final classes = Classes({
+ 'Foo': ClassDecl(
+ binaryName: 'Foo',
+ declKind: DeclKind.interfaceKind,
+ superclass: DeclaredType.object,
+ ),
+ 'Bar': ClassDecl(
+ binaryName: 'Bar',
+ declKind: DeclKind.interfaceKind,
+ superclass: DeclaredType.object,
+ )..userDefinedInterfaceMixinName = 'Foo',
+ 'Baz': ClassDecl(
+ binaryName: 'Baz',
+ declKind: DeclKind.interfaceKind,
+ superclass: DeclaredType.object,
+ )..userDefinedInterfaceMixinName = 'class',
+ });
+
+ await rename(classes);
+
+ expect(classes.decls['Foo']!.finalInterfaceMixinName, r'$Foo');
+ expect(classes.decls['Bar']!.finalInterfaceMixinName, r'Foo$1');
+ expect(classes.decls['Baz']!.finalInterfaceMixinName, r'class$');
+ });
+
+ test('Interface mixin name preprocessing', () async {
+ final classes = Classes({
+ 'Foo': ClassDecl(
+ binaryName: 'Foo',
+ declKind: DeclKind.interfaceKind,
+ superclass: DeclaredType.object,
+ )..userDefinedInterfaceMixinName = r'_Foo$',
+ });
+
+ await rename(classes);
+
+ expect(
+ classes.decls['Foo']!.finalInterfaceMixinName,
+ r'$_Foo$$',
+ );
+ });
+
+ test('Interface mixin name conflicts in package structure', () async {
+ final classes = Classes({
+ 'Foo': ClassDecl(
+ binaryName: 'Foo',
+ declKind: DeclKind.interfaceKind,
+ superclass: DeclaredType.object,
+ )..userDefinedInterfaceMixinName = 'Foo',
+ });
+
+ await rename(
+ classes,
+ structure: OutputStructure.packageStructure,
+ );
+
+ expect(classes.decls['Foo']!.finalName, 'Foo');
+ expect(classes.decls['Foo']!.finalInterfaceMixinName, r'Foo$1');
+ });
+
test('Inner classes vs classes with dollar signs', () async {
final classes = Classes({
'Outer': ClassDecl(
diff --git a/pkgs/jnigen/test/user_visitor_test.dart b/pkgs/jnigen/test/user_visitor_test.dart
index 583538e..3f3088c 100644
--- a/pkgs/jnigen/test/user_visitor_test.dart
+++ b/pkgs/jnigen/test/user_visitor_test.dart
@@ -2,7 +2,10 @@
// 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:io';
+
import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/src/bindings/dart_generator.dart';
import 'package:jnigen/src/bindings/linker.dart';
import 'package:jnigen/src/bindings/renamer.dart';
import 'package:jnigen/src/elements/elements.dart' as ast;
@@ -255,4 +258,101 @@
expect(classes.decls['y.Foo']?.methods.first.params.finalNames,
['Bar', 'Bar1']);
});
+
+ test('Rename interface mixin using the user visitor', () async {
+ final classes = ast.Classes({
+ 'Foo': ast.ClassDecl(
+ binaryName: 'Foo',
+ declKind: ast.DeclKind.interfaceKind,
+ superclass: ast.DeclaredType.object,
+ ),
+ });
+
+ Classes(classes).accept(
+ Visitor(
+ visitClass: (c) {
+ if (c.originalName == 'Foo') {
+ c.interfaceMixinName = 'FooInterface';
+ }
+ },
+ ),
+ );
+
+ expect(
+ classes.decls['Foo']!.userDefinedInterfaceMixinName,
+ 'FooInterface',
+ );
+
+ await rename(classes);
+
+ expect(
+ classes.decls['Foo']!.finalInterfaceMixinName,
+ 'FooInterface',
+ );
+ });
+
+ test('Use the renamed interface mixin in generated bindings', () async {
+ final tempDirectory = Directory.systemTemp.createTempSync(
+ 'jnigen_interface_mixin_test_',
+ );
+ addTearDown(() => tempDirectory.deleteSync(recursive: true));
+
+ final output = tempDirectory.uri.resolve('bindings.dart');
+ final config = Config(
+ input: Input(classes: []),
+ output: Output(
+ dart: DartCodeOutput(
+ path: output,
+ structure: OutputStructure.singleFile,
+ ),
+ ),
+ );
+
+ final classes = ast.Classes({
+ 'Foo': ast.ClassDecl(
+ binaryName: 'Foo',
+ declKind: ast.DeclKind.interfaceKind,
+ superclass: ast.DeclaredType.object,
+ methods: [
+ ast.Method(
+ name: 'run',
+ returnType: ast.PrimitiveType.fromJson({'name': 'void'}),
+ ),
+ ],
+ ),
+ });
+
+ Classes(classes).accept(
+ Visitor(
+ visitClass: (c) {
+ if (c.originalName == 'Foo') {
+ c.interfaceMixinName = 'FooInterface';
+ }
+ },
+ ),
+ );
+
+ await classes.accept(Linker(config));
+ classes.accept(Renamer(config));
+ await classes.accept(DartGenerator(config));
+
+ final content = File.fromUri(output).readAsStringSync();
+
+ expect(
+ content,
+ contains('abstract base mixin class FooInterface'),
+ );
+ expect(
+ content,
+ contains('final class _FooInterface with FooInterface'),
+ );
+ expect(
+ content,
+ contains(r'FooInterface $impl'),
+ );
+ expect(
+ content,
+ isNot(contains(r'abstract base mixin class $Foo')),
+ );
+ });
}