Skip to content
12 changes: 12 additions & 0 deletions pkgs/swift2objc/lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:path/path.dart' as path;

import 'ast/_core/interfaces/declaration.dart';

const defaultTempDirPrefix = 'swift2objc_temp_';
const symbolgraphFileSuffix = '.symbols.json';

Expand Down Expand Up @@ -32,11 +34,21 @@ class Config {
/// intermediate files after generating the wrapper
final Uri? tempDir;

/// Filter function to filter APIs
///
/// APIs can be filtered by name
///
/// Includes all declarations by default
final bool Function(Declaration declaration)? include;

static bool _defaultInclude(_) => true;

const Config({
required this.input,
required this.outputFile,
this.tempDir,
this.preamble,
this.include = Config._defaultInclude
});
}

Expand Down
5 changes: 3 additions & 2 deletions pkgs/swift2objc/lib/src/generate_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ Future<void> generateWrapper(Config config) async {
JsonFileInputConfig() => parseModuleName(symbolgraphJson),
};


final declarations = parseAst(symbolgraphJson);
final transformedDeclarations = transform(declarations);

final transformedDeclarations = transform(declarations,
filter: config.include);
final wrapperCode = generate(
transformedDeclarations,
moduleName: sourceModule,
Expand Down
16 changes: 11 additions & 5 deletions pkgs/swift2objc/lib/src/transformer/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ import 'transformers/transform_globals.dart';

typedef TransformationMap = Map<Declaration, Declaration>;

List<Declaration> transform(List<Declaration> declarations) {
/// Transforms the given declarations into the desired ObjC wrapped declarations
List<Declaration> transform(List<Declaration> declarations, {
bool Function(Declaration)? filter
}) {
final TransformationMap transformationMap;
final _filter = filter ?? (declaration) => true;

final _declarations = declarations.where((d) => _filter(d));

transformationMap = {};

final globalNamer = UniqueNamer(
declarations.map((declaration) => declaration.name),
_declarations.map((declaration) => declaration.name),
);

final globals = Globals(
functions: declarations.whereType<GlobalFunctionDeclaration>().toList(),
variables: declarations.whereType<GlobalVariableDeclaration>().toList(),
functions: _declarations.whereType<GlobalFunctionDeclaration>().toList(),
variables: _declarations.whereType<GlobalVariableDeclaration>().toList(),
);
final nonGlobals = declarations
final nonGlobals = _declarations
.where(
(declaration) =>
declaration is! GlobalFunctionDeclaration &&
Expand Down
110 changes: 110 additions & 0 deletions pkgs/swift2objc/test/unit/filter_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 'dart:io';

import 'package:path/path.dart' as path;
import 'package:swift2objc/src/ast/declarations/compounds/class_declaration.dart';
import 'package:swift2objc/swift2objc.dart';
import 'package:test/test.dart';

void main() {
group('Unit test for filter', () {
final thisDir = path.join(Directory.current.path, 'test/unit');

final file = path.join(thisDir, 'filter_test_input.swift');
test('A: Specific Files', () async {
final output = path.join(thisDir, 'filter_test_output_a.swift');
final actualOutputFile = path.join(thisDir, '${
path.basenameWithoutExtension(output)}.test${path.extension(output)
}');

await generateWrapper(Config(
input: FilesInputConfig(
files: [Uri.file(file)],
),
outputFile: Uri.file(actualOutputFile),
tempDir: Directory(thisDir).uri,
preamble: '// Test preamble text',
include: (declaration) => declaration.name == 'Engine',
));

final actualOutput = await File(actualOutputFile).readAsString();
final expectedOutput = File(output).readAsStringSync();

expect(actualOutput, expectedOutput);
});

test('B: Declarations of a specific type', () async {
final output = path.join(thisDir, 'filter_test_output_b.swift');
final actualOutputFile = path.join(thisDir, '${
path.basenameWithoutExtension(output)}.test${path.extension(output)
}');

await generateWrapper(Config(
input: FilesInputConfig(
files: [Uri.file(file)],
),
outputFile: Uri.file(actualOutputFile),
tempDir: Directory(thisDir).uri,
preamble: '// Test preamble text',
include: (declaration) => declaration is ClassDeclaration,
));

final actualOutput = await File(actualOutputFile).readAsString();
final expectedOutput = File(output).readAsStringSync();

expect(actualOutput, expectedOutput);
});

test('C: Nonexistent declaration', () async {
final output = path.join(thisDir, 'filter_test_output_c.swift');
final actualOutputFile = path.join(thisDir, '${
path.basenameWithoutExtension(output)}.test${path.extension(output)
}');

await generateWrapper(Config(
input: FilesInputConfig(
files: [Uri.file(file)],
),
outputFile: Uri.file(actualOutputFile),
tempDir: Directory(thisDir).uri,
preamble: '// Test preamble text',
// The following declaration does not exist,
// so none are produced in output
include: (declaration) => declaration.name == 'Ship',
));

final actualOutput = await File(actualOutputFile).readAsString();
final expectedOutput = File(output).readAsStringSync();

expect(actualOutput, expectedOutput);
});

tearDown(() {
if (File(path.join(thisDir, 'symbolgraph_module.abi.json')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.abi.json')).deleteSync();
}
if (File(path.join(thisDir, 'symbolgraph_module.swiftdoc')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.swiftdoc')).deleteSync();
}
if (File(path.join(thisDir, 'symbolgraph_module.swiftmodule')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.swiftmodule')).deleteSync();
}
if (File(path.join(thisDir, 'symbolgraph_module.swiftsource')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.swiftsource')).deleteSync();
}
if (File(path.join(thisDir, 'symbolgraph_module.symbols.json')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.symbols.json')).deleteSync();
}
if (File(path.join(thisDir, 'symbolgraph_module.swiftsourceinfo')).existsSync()) {
File(path.join(thisDir, 'symbolgraph_module.swiftsourceinfo')).deleteSync();
}

for (final file in Directory(thisDir).listSync().where((t) => path.extension(t.path, 2) == '.test.swift')) {
if (file is File) file.deleteSync();
}
});
});
}
135 changes: 135 additions & 0 deletions pkgs/swift2objc/test/unit/filter_test_input.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import Foundation

public struct Engine {
public let type: String
public let horsepower: Int

public init(type: String, horsepower: Int) {
self.type = type
self.horsepower = horsepower
}

public func displaySpecs() {
print("Engine: \(type), \(horsepower) HP")
}
}


public struct Tire {
public let brand: String
public let size: Int

public init(brand: String, size: Int) {
self.brand = brand
self.size = size
}

public func displayInfo() {
print("Tire: \(brand), size \(size)")
}
}


public struct Dimensions {
public let length: Double
public let width: Double
public let height: Double

public init(length: Double, width: Double, height: Double) {
self.length = length
self.width = width
self.height = height
}

public func displayDimensions() {
print("Dimensions (LxWxH): \(length) x \(width) x \(height) meters")
}
}


public class Vehicle {
public var make: String
public var model: String
public var engine: Engine
public var dimensions: Dimensions

public init(make: String, model: String, engine: Engine, dimensions: Dimensions) {
self.make = make
self.model = model
self.engine = engine
self.dimensions = dimensions
}

public func displayInfo() {
print("Vehicle: \(make) \(model)")
engine.displaySpecs()
dimensions.displayDimensions()
}
}


public class Car: Vehicle {
public var numberOfDoors: Int
public var tires: [Tire]

public init(make: String, model: String, engine: Engine, dimensions: Dimensions, numberOfDoors: Int, tires: [Tire]) {
self.numberOfDoors = numberOfDoors
self.tires = tires
super.init(make: make, model: model, engine: engine, dimensions: dimensions)
}

public func honk() {
print("Car \(make) \(model) goes 'Beep Beep!'")
}
}


public class ElectricCar: Car {
public var batteryCapacity: Int // in kWh

public init(make: String, model: String, dimensions: Dimensions, numberOfDoors: Int, tires: [Tire], batteryCapacity: Int) {
self.batteryCapacity = batteryCapacity
let electricEngine = Engine(type: "Electric", horsepower: batteryCapacity * 3) // Example calculation
super.init(make: make, model: model, engine: electricEngine, dimensions: dimensions, numberOfDoors: numberOfDoors, tires: tires)
}

public func chargeBattery() {
print("Charging \(make) \(model)... Battery capacity: \(batteryCapacity) kWh")
}
}

public class Bicycle {
public var brand: String
public var gearCount: Int
public var dimensions: Dimensions

public init(brand: String, gearCount: Int, dimensions: Dimensions) {
self.brand = brand
self.gearCount = gearCount
self.dimensions = dimensions
}

public func pedal() {
print("\(brand) bicycle is pedaling with \(gearCount) gears.")
dimensions.displayDimensions()
}
}


public class Garage {
private var vehicles: [Vehicle] = []

public init() {}

public func addVehicle(_ vehicle: Vehicle) {
vehicles.append(vehicle)
print("Added \(vehicle.make) \(vehicle.model) to the garage.")
}

public func listVehicles() {
print("Garage contains:")
for vehicle in vehicles {
print("- \(vehicle.make) \(vehicle.model)")
}
}
}
31 changes: 31 additions & 0 deletions pkgs/swift2objc/test/unit/filter_test_output_a.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Test preamble text

import Foundation

@objc public class EngineWrapper: NSObject {
var wrappedInstance: Engine

@objc public var horsepower: Int {
get {
wrappedInstance.horsepower
}
}

@objc public var type: String {
get {
wrappedInstance.type
}
}

init(_ wrappedInstance: Engine) {
self.wrappedInstance = wrappedInstance
}

@objc init(type: String, horsepower: Int) {
wrappedInstance = Engine(type: type, horsepower: horsepower)
}

@objc public func displaySpecs() {
wrappedInstance.displaySpecs()
}
}
Loading
Loading