diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index f8408ec7c..3c074d4aa 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -2,6 +2,8 @@ - **Breaking Change**([#1644](https://github.com/dart-lang/native/issues/1644)): Generate null-safe Dart bindings for Java and Kotlin. +- Add a simple AST and update `excluder.dart` to support user-defined exclusions for classes, methods, and fields. + ## 0.12.2 diff --git a/pkgs/jnigen/lib/src/bindings/excluder.dart b/pkgs/jnigen/lib/src/bindings/excluder.dart index c9fba9ab5..85f4a681c 100644 --- a/pkgs/jnigen/lib/src/bindings/excluder.dart +++ b/pkgs/jnigen/lib/src/bindings/excluder.dart @@ -35,6 +35,7 @@ class Excluder extends Visitor { void visit(Classes node) { node.decls.removeWhere((_, classDecl) { final excluded = classDecl.isPrivate || + classDecl.isExcluded || !(config.exclude?.classes?.included(classDecl) ?? true); if (excluded) { log.fine('Excluded class ${classDecl.binaryName}'); @@ -61,13 +62,17 @@ class _ClassExcluder extends Visitor { @override void visit(ClassDecl node) { node.methods = node.methods.where((method) { + final isExcluded = method.isExcluded; final isPrivate = method.isPrivate; final isAbstractCtor = method.isConstructor && node.isAbstract; final isBridgeMethod = method.isSynthetic && method.isBridge; final isExcludedInConfig = config.exclude?.methods?.included(node, method) ?? false; - final excluded = - isPrivate || isAbstractCtor || isBridgeMethod || isExcludedInConfig; + final excluded = isPrivate || + isAbstractCtor || + isBridgeMethod || + isExcludedInConfig || + isExcluded; if (excluded) { log.fine('Excluded method ${node.binaryName}#${method.name}'); } @@ -80,8 +85,9 @@ class _ClassExcluder extends Visitor { return !excluded; }).toList(); node.fields = node.fields.where((field) { - final excluded = field.isPrivate && - (config.exclude?.fields?.included(node, field) ?? true); + final excluded = field.isExcluded || + (field.isPrivate && + (config.exclude?.fields?.included(node, field) ?? true)); if (excluded) { log.fine('Excluded field ${node.binaryName}#${field.name}'); } diff --git a/pkgs/jnigen/lib/src/config/config_types.dart b/pkgs/jnigen/lib/src/config/config_types.dart index f5bd848fd..bead7f371 100644 --- a/pkgs/jnigen/lib/src/config/config_types.dart +++ b/pkgs/jnigen/lib/src/config/config_types.dart @@ -10,6 +10,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; import '../elements/elements.dart'; +import '../elements/j_elements.dart' as j_ast; import '../logging/logging.dart'; import '../util/find_package.dart'; import 'config_exception.dart'; @@ -265,24 +266,24 @@ void _validateClassName(String className) { /// Configuration for jnigen binding generation. class Config { - Config({ - required this.outputConfig, - required this.classes, - this.experiments, - this.exclude, - this.sourcePath, - this.classPath, - this.preamble, - this.customClassBody, - this.androidSdkConfig, - this.mavenDownloads, - this.summarizerOptions, - this.nonNullAnnotations, - this.nullableAnnotations, - this.logLevel = Level.INFO, - this.dumpJsonTo, - this.imports, - }) { + Config( + {required this.outputConfig, + required this.classes, + this.experiments, + this.exclude, + this.sourcePath, + this.classPath, + this.preamble, + this.customClassBody, + this.androidSdkConfig, + this.mavenDownloads, + this.summarizerOptions, + this.nonNullAnnotations, + this.nullableAnnotations, + this.logLevel = Level.INFO, + this.dumpJsonTo, + this.imports, + this.visitors}) { for (final className in classes) { _validateClassName(className); } @@ -349,6 +350,9 @@ class Config { /// Used for testing package:jnigen. final Map? customClassBody; + // User custom visitors. + List? visitors; + Future importClasses() async { importedClasses = {}; for (final import in [ diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart index cbdbea653..970c6bfa3 100644 --- a/pkgs/jnigen/lib/src/elements/elements.dart +++ b/pkgs/jnigen/lib/src/elements/elements.dart @@ -59,6 +59,7 @@ class Classes implements Element { @JsonSerializable(createToJson: false) class ClassDecl with ClassMember, Annotated implements Element { ClassDecl({ + this.isExcluded = false, this.annotations, this.javadoc, required this.declKind, @@ -77,6 +78,8 @@ class ClassDecl with ClassMember, Annotated implements Element { this.kotlinPackage, }); + bool isExcluded; + @override final Set modifiers; @@ -602,6 +605,7 @@ mixin ClassMember { @JsonSerializable(createToJson: false) class Method with ClassMember, Annotated implements Element { Method({ + this.isExcluded = false, this.annotations, this.javadoc, this.modifiers = const {}, @@ -612,6 +616,8 @@ class Method with ClassMember, Annotated implements Element { required this.returnType, }); + bool isExcluded; + @override final String name; @override @@ -704,6 +710,7 @@ class Param with Annotated implements Element { @JsonSerializable(createToJson: false) class Field with ClassMember, Annotated implements Element { Field({ + this.isExcluded = false, this.annotations, this.javadoc, this.modifiers = const {}, @@ -712,6 +719,8 @@ class Field with ClassMember, Annotated implements Element { this.defaultValue, }); + bool isExcluded; + @override final String name; @override diff --git a/pkgs/jnigen/lib/src/elements/j_elements.dart b/pkgs/jnigen/lib/src/elements/j_elements.dart new file mode 100644 index 000000000..883786aea --- /dev/null +++ b/pkgs/jnigen/lib/src/elements/j_elements.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2024, 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 'elements.dart' as ast; + +abstract class Element { + void accept(Visitor visitor); +} + +abstract class Visitor { + void visitClass(ClassDecl c) {} + void visitMethod(Method method) {} + void visitField(Field field) {} +} + +class Classes implements Element { + Classes(this._classes); + final ast.Classes _classes; + + @override + void accept(Visitor visitor) { + for (final value in _classes.decls.values) { + final classDecl = ClassDecl(value); + classDecl.accept(visitor); + } + } + + void let(void Function(dynamic userClasses) param0) {} +} + +class ClassDecl implements Element { + ClassDecl(this._classDecl) : binaryName = _classDecl.binaryName; + final ast.ClassDecl _classDecl; + + // Ex: com.x.Foo. + final String binaryName; + + bool get isExcluded => _classDecl.isExcluded; + set isExcluded(bool value) => _classDecl.isExcluded = value; + + @override + void accept(Visitor visitor) { + visitor.visitClass(this); + if (_classDecl.isExcluded) return; + for (final method in _classDecl.methods) { + Method(method).accept(visitor); + } + for (var field in _classDecl.fields) { + Field(field).accept(visitor); + } + } +} + +class Method implements Element { + Method(this._method); + + final ast.Method _method; + + String get name => _method.name; + + bool get isExcluded => _method.isExcluded; + set isExcluded(bool value) => _method.isExcluded = value; + + @override + void accept(Visitor visitor) { + visitor.visitMethod(this); + } +} + +class Field implements Element { + Field(this._field); + + final ast.Field _field; + + String get name => _field.name; + + bool get isExcluded => _field.isExcluded; + set isExcluded(bool value) => _field.isExcluded = value; + + @override + void accept(Visitor visitor) { + visitor.visitField(this); + } +} diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart index 2e237f211..6d185f242 100644 --- a/pkgs/jnigen/lib/src/generate_bindings.dart +++ b/pkgs/jnigen/lib/src/generate_bindings.dart @@ -13,6 +13,7 @@ import 'bindings/linker.dart'; import 'bindings/renamer.dart'; import 'config/config.dart'; import 'elements/elements.dart'; +import 'elements/j_elements.dart' as j_ast; import 'logging/logging.dart'; import 'summary/summary.dart'; import 'tools/tools.dart'; @@ -38,6 +39,9 @@ Future generateJniBindings(Config config) async { log.fatal(e.message); } + final userClasses = j_ast.Classes(classes); + config.visitors?.forEach(userClasses.accept); + classes.accept(Excluder(config)); classes.accept(KotlinProcessor()); await classes.accept(Linker(config)); diff --git a/pkgs/jnigen/test/user_excluder.dart b/pkgs/jnigen/test/user_excluder.dart new file mode 100644 index 000000000..f13adcda4 --- /dev/null +++ b/pkgs/jnigen/test/user_excluder.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2024, 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:jnigen/src/elements/elements.dart' as ast; +import 'package:jnigen/src/elements/j_elements.dart'; +import 'package:test/test.dart'; + +extension on Iterable { + List get isExcludedValues => map((c) => c.isExcluded).toList(); +} + +extension on Iterable { + List get isExcludedValues => map((c) => c.isExcluded).toList(); +} + +// This is customizable by the user +class UserExcluder extends Visitor { + @override + void visitClass(ClassDecl c) { + if (c.binaryName.contains('y')) { + c.isExcluded = true; + } + } + + @override + void visitMethod(Method method) { + if (method.name == 'Bar') { + method.isExcluded = true; + } + } + + @override + void visitField(Field field) { + if (field.name == 'Bar') { + field.isExcluded = true; + } + } +} + +void main() { + test('Exclude something using the user excluder, Simple AST', () async { + final classes = ast.Classes({ + 'Foo': ast.ClassDecl( + binaryName: 'Foo', + declKind: ast.DeclKind.classKind, + superclass: ast.TypeUsage.object, + methods: [ + ast.Method(name: 'foo', returnType: ast.TypeUsage.object), + ast.Method(name: 'Bar', returnType: ast.TypeUsage.object), + ast.Method(name: 'foo1', returnType: ast.TypeUsage.object), + ast.Method(name: 'Bar', returnType: ast.TypeUsage.object), + ], + fields: [ + ast.Field(name: 'foo', type: ast.TypeUsage.object), + ast.Field(name: 'Bar', type: ast.TypeUsage.object), + ast.Field(name: 'foo1', type: ast.TypeUsage.object), + ast.Field(name: 'Bar', type: ast.TypeUsage.object), + ], + ), + 'y.Foo': ast.ClassDecl( + binaryName: 'y.Foo', + declKind: ast.DeclKind.classKind, + superclass: ast.TypeUsage.object, + methods: [ + ast.Method(name: 'foo', returnType: ast.TypeUsage.object), + ast.Method(name: 'Bar', returnType: ast.TypeUsage.object), + ], + fields: [ + ast.Field(name: 'foo', type: ast.TypeUsage.object), + ast.Field(name: 'Bar', type: ast.TypeUsage.object), + ]), + }); + + final simpleClasses = Classes(classes); + simpleClasses.accept(UserExcluder()); + + expect(classes.decls['y.Foo']?.isExcluded, true); + expect(classes.decls['Foo']?.isExcluded, false); + + expect(classes.decls['Foo']?.fields.isExcludedValues, + [false, true, false, true]); + expect(classes.decls['Foo']?.methods.isExcludedValues, + [false, true, false, true]); + }); +}