diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index 58969b4b30..e660168560 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -449,7 +449,9 @@ ${modifier}final $classRef = $_jni.JClass.forName(r'$internalName'); ), ); final implementsClause = {superName, ...interfaces}.join(', '); - final implClassName = '\$$name'; + final implClassName = node.declKind == DeclKind.interfaceKind + ? node.finalInterfaceMixinName + : ''; final typeParamsDef = node.allTypeParams .accept(const _TypeParamDef()) .join(', ') diff --git a/pkgs/jnigen/lib/src/bindings/renamer.dart b/pkgs/jnigen/lib/src/bindings/renamer.dart index e4e19f08b0..dfc1ca276d 100644 --- a/pkgs/jnigen/lib/src/bindings/renamer.dart +++ b/pkgs/jnigen/lib/src/bindings/renamer.dart @@ -181,16 +181,23 @@ class Renamer extends Visitor with TopLevelVisitor { class _ClassRenamer implements Visitor { final Config config; final Set renamed; - final Map topLevelNameCounts = { - ..._definedSyms, - ..._reservedTopLevelNames, - }; + final Map> topLevelNameCounts = {}; final Map> nameCounts = {}; _ClassRenamer( this.config, ) : renamed = {...config.importedClasses.values}; + Map _getTopLevelNameCounts(ClassDecl node) { + return topLevelNameCounts.putIfAbsent( + node.path, + () => { + ..._definedSyms, + ..._reservedTopLevelNames, + }, + ); + } + @override void visit(ClassDecl node) { if (renamed.contains(node)) return; @@ -217,13 +224,25 @@ class _ClassRenamer implements Visitor { 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 @@ class _ClassRenamer implements Visitor { // 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 @@ class _ClassRenamer implements Visitor { 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 6fc037fdb5..5250d1df22 100644 --- a/pkgs/jnigen/lib/src/elements/elements.dart +++ b/pkgs/jnigen/lib/src/elements/elements.dart @@ -108,6 +108,9 @@ class ClassDecl with ClassMember, Annotated implements Element { @JsonKey(includeFromJson: false) String? userDefinedName; + @JsonKey(includeFromJson: false) + String? userDefinedInterfaceMixinName; + @override final Set modifiers; @@ -149,6 +152,9 @@ class ClassDecl with ClassMember, Annotated implements Element { @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 49cccedd53..31d9dbd828 100644 --- a/pkgs/jnigen/lib/src/elements/j_elements.dart +++ b/pkgs/jnigen/lib/src/elements/j_elements.dart @@ -110,6 +110,15 @@ class ClassDecl implements _Element { /// 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 8ebbee6220..4fe63f2fde 100644 --- a/pkgs/jnigen/test/renamer_test.dart +++ b/pkgs/jnigen/test/renamer_test.dart @@ -21,13 +21,18 @@ extension on Iterable { }).toList(); } -Future rename(Classes classes) async { +Future 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 @@ void main() { 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 583538ec34..3f3088c59a 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 @@ void main() { 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')), + ); + }); }