Skip to content

Commit

Permalink
[jnigen] User-defined visitors (#1755)
Browse files Browse the repository at this point in the history
Config now accepts a list of user-defined visitors. For now the only the they do is excluding classes, methods, and fields.
  • Loading branch information
marshelino-maged authored Dec 8, 2024
1 parent 3aba894 commit acb973c
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 22 deletions.
2 changes: 2 additions & 0 deletions pkgs/jnigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 10 additions & 4 deletions pkgs/jnigen/lib/src/bindings/excluder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Excluder extends Visitor<Classes, void> {
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}');
Expand All @@ -61,13 +62,17 @@ class _ClassExcluder extends Visitor<ClassDecl, void> {
@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}');
}
Expand All @@ -80,8 +85,9 @@ class _ClassExcluder extends Visitor<ClassDecl, void> {
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}');
}
Expand Down
40 changes: 22 additions & 18 deletions pkgs/jnigen/lib/src/config/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -349,6 +350,9 @@ class Config {
/// Used for testing package:jnigen.
final Map<String, String>? customClassBody;

// User custom visitors.
List<j_ast.Visitor>? visitors;

Future<void> importClasses() async {
importedClasses = {};
for (final import in [
Expand Down
9 changes: 9 additions & 0 deletions pkgs/jnigen/lib/src/elements/elements.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Classes implements Element<Classes> {
@JsonSerializable(createToJson: false)
class ClassDecl with ClassMember, Annotated implements Element<ClassDecl> {
ClassDecl({
this.isExcluded = false,
this.annotations,
this.javadoc,
required this.declKind,
Expand All @@ -77,6 +78,8 @@ class ClassDecl with ClassMember, Annotated implements Element<ClassDecl> {
this.kotlinPackage,
});

bool isExcluded;

@override
final Set<String> modifiers;

Expand Down Expand Up @@ -602,6 +605,7 @@ mixin ClassMember {
@JsonSerializable(createToJson: false)
class Method with ClassMember, Annotated implements Element<Method> {
Method({
this.isExcluded = false,
this.annotations,
this.javadoc,
this.modifiers = const {},
Expand All @@ -612,6 +616,8 @@ class Method with ClassMember, Annotated implements Element<Method> {
required this.returnType,
});

bool isExcluded;

@override
final String name;
@override
Expand Down Expand Up @@ -704,6 +710,7 @@ class Param with Annotated implements Element<Param> {
@JsonSerializable(createToJson: false)
class Field with ClassMember, Annotated implements Element<Field> {
Field({
this.isExcluded = false,
this.annotations,
this.javadoc,
this.modifiers = const {},
Expand All @@ -712,6 +719,8 @@ class Field with ClassMember, Annotated implements Element<Field> {
this.defaultValue,
});

bool isExcluded;

@override
final String name;
@override
Expand Down
85 changes: 85 additions & 0 deletions pkgs/jnigen/lib/src/elements/j_elements.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 4 additions & 0 deletions pkgs/jnigen/lib/src/generate_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,6 +39,9 @@ Future<void> 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));
Expand Down
86 changes: 86 additions & 0 deletions pkgs/jnigen/test/user_excluder.dart
Original file line number Diff line number Diff line change
@@ -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<ast.Method> {
List<bool> get isExcludedValues => map((c) => c.isExcluded).toList();
}

extension on Iterable<ast.Field> {
List<bool> 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]);
});
}

0 comments on commit acb973c

Please sign in to comment.