blob: 6cc60d64c23ac69258271fe11b9e043af1f85e25 [file]
// Copyright (c) 2026, 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:ffigen/src/code_generator.dart';
import 'package:ffigen/src/config_provider/config.dart';
import 'package:ffigen/src/config_provider/config_types.dart';
import 'package:ffigen/src/context.dart';
import 'package:ffigen/src/header_parser/sub_parsers/api_availability.dart';
import 'package:test/test.dart';
import '../test_utils.dart';
void main() {
group('ObjC method clone', () {
final availability = ApiAvailability(
externalVersions: const ExternalVersions(),
);
final config = FfiGenerator(
output: Output(dartFile: Uri.file('unused')),
objectiveC: const ObjectiveC(),
);
late Context context;
final voidType = NativeType(SupportedNativeType.voidType);
final intType = NativeType(SupportedNativeType.int32);
void resetContext() {
context = testContext(config);
}
setUp(resetContext);
ObjCInterface makeInterface(
String name,
ObjCInterface? superType,
List<ObjCMethod> methods,
) {
final itf = ObjCInterface(
context: context,
usr: name,
originalName: name,
apiAvailability: availability,
);
if (superType != null) {
itf.superType = superType;
superType.subtypes.add(itf);
}
for (final m in methods) {
itf.addMethod(m);
}
itf.filled = true;
return itf;
}
ObjCCategory makeCategory(
String name,
ObjCInterface parent,
List<ObjCMethod> methods, {
bool isIncluded = true,
}) {
final category = ObjCCategory(
context: context,
usr: name,
originalName: name,
parent: parent,
apiAvailability: availability,
)..isIncluded = isIncluded;
parent.categories.add(category);
for (final m in methods) {
category.addMethod(m);
}
return category;
}
ObjCMethod makeMethod(
String name,
Type returnType,
List<Parameter> params, {
bool isClassMethod = false,
}) => ObjCMethod(
context: context,
originalName: name,
name: name,
kind: ObjCMethodKind.method,
isClassMethod: isClassMethod,
isOptional: false,
returnType: returnType,
family: null,
apiAvailability: availability,
params: params,
ownershipAttribute: null,
consumesSelfAttribute: false,
);
Parameter makeParam(String name, Type type) =>
Parameter(name: name, type: type, objCConsumed: false);
(ObjCMethod, ObjCMethod) makeProperty(String name, Type type) {
final getter = ObjCMethod(
context: context,
originalName: name,
name: name,
kind: ObjCMethodKind.propertyGetter,
isClassMethod: false,
isOptional: false,
returnType: type,
family: null,
apiAvailability: availability,
params: const [],
ownershipAttribute: null,
consumesSelfAttribute: false,
);
final setter = ObjCMethod.withSymbol(
context: context,
originalName: 'set${name[0].toUpperCase()}${name.substring(1)}:',
symbol: getter.symbol,
protocolMethodName: 'set${name[0].toUpperCase()}${name.substring(1)}:',
kind: ObjCMethodKind.propertySetter,
isClassMethod: false,
isOptional: false,
returnType: voidType,
params: [Parameter(name: 'value', type: type, objCConsumed: false)],
family: null,
apiAvailability: availability,
ownershipAttribute: null,
consumesSelfAttribute: false,
);
getter.setter = setter;
return (getter, setter);
}
test(
'copyMethod creates deep clone with parent set to destination container',
() {
final param = makeParam('p1', intType);
final method = makeMethod('foo:', voidType, [param]);
final source = makeInterface('Source', null, [method]);
final dest = makeInterface('Destination', null, []);
dest.copyMethod(method);
expect(dest.methods.length, 1);
final clonedMethod = dest.methods.single;
expect(clonedMethod, isNot(same(method)));
expect(clonedMethod.originalName, method.originalName);
expect(clonedMethod.parent, dest);
expect(method.parent, source);
expect(clonedMethod.params.length, 1);
final clonedParam = clonedMethod.params.single;
expect(clonedParam, isNot(same(param)));
expect(clonedParam.originalName, param.originalName);
},
);
test('clone getter with linked setter', () {
final (getter, setter) = makeProperty('foo', intType);
final dest = makeInterface('Destination', null, []);
final clonedGetter = getter.clone(parent: dest);
final clonedSetter = clonedGetter.setter;
expect(clonedSetter, isNotNull);
expect(clonedGetter.symbol, clonedSetter!.symbol);
expect(clonedGetter.symbol, isNot(getter.symbol));
expect(clonedGetter.parent, dest);
expect(clonedSetter.parent, dest);
expect(clonedGetter.setter, clonedSetter);
});
test(
'copying a property (getter + setter) to child interface via copyMethod',
() {
final (getter, setter) = makeProperty('foo', intType);
final source = makeInterface('Source', null, [getter, setter]);
final dest = makeInterface('Destination', null, []);
dest.copyMethod(getter);
expect(dest.methods.length, 2);
final clonedGetter = dest.methods.firstWhere(
(m) => m.kind == ObjCMethodKind.propertyGetter,
);
final clonedSetter = dest.methods.firstWhere(
(m) => m.kind == ObjCMethodKind.propertySetter,
);
expect(clonedGetter.parent, dest);
expect(clonedSetter.parent, dest);
expect(clonedGetter.setter, clonedSetter);
expect(clonedGetter.symbol, clonedSetter.symbol);
expect(clonedGetter.symbol, isNot(getter.symbol));
expect(source.methods.length, 2);
},
);
test('copyMethod with originCategory sets originCategory', () {
final method = makeMethod('catMethod', voidType, []);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, [method]);
final dest = makeInterface('Destination', null, []);
dest.copyMethod(method, originCategory: category);
expect(dest.methods.length, 1);
final clonedMethod = dest.methods.single;
expect(clonedMethod.originCategory, category);
});
test('copying a property with originCategory sets originCategory on getter '
'and setter', () {
final (getter, setter) = makeProperty('prop', intType);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, [getter, setter]);
final dest = makeInterface('Destination', null, []);
dest.copyMethod(getter, originCategory: category);
expect(dest.methods.length, 2);
final clonedGetter = dest.methods.firstWhere(
(m) => m.kind == ObjCMethodKind.propertyGetter,
);
final clonedSetter = dest.methods.firstWhere(
(m) => m.kind == ObjCMethodKind.propertySetter,
);
expect(clonedGetter.originCategory, category);
expect(clonedSetter.originCategory, category);
});
test('copyMethod preserves originCategory across multiple copies', () {
final method = makeMethod('catMethod', voidType, []);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, [method]);
final mid = makeInterface('Mid', null, []);
final dest = makeInterface('Destination', null, []);
mid.copyMethod(method, originCategory: category);
final midMethod = mid.methods.single;
expect(midMethod.originCategory, category);
dest.copyMethod(midMethod);
final destMethod = dest.methods.single;
expect(destMethod.originCategory, category);
});
test('clone with originCategory sets originCategory', () {
final method = makeMethod('catMethod', voidType, []);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, []);
final cloned = method.clone(originCategory: category);
expect(cloned.originCategory, category);
});
test('clone with originCategory overrides existing originCategory', () {
final method = makeMethod('catMethod', voidType, []);
final parent = makeInterface('Parent', null, []);
final category1 = makeCategory('Category1', parent, []);
final category2 = makeCategory('Category2', parent, []);
method.originCategory = category1;
final cloned = method.clone(originCategory: category2);
expect(cloned.originCategory, category2);
});
test('clone property with originCategory sets originCategory on getter and '
'setter', () {
final (getter, setter) = makeProperty('prop', intType);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, []);
final clonedGetter = getter.clone(originCategory: category);
expect(clonedGetter.originCategory, category);
expect(clonedGetter.setter?.originCategory, category);
});
test(
'clone property with originCategory overrides existing originCategory on '
'getter and setter',
() {
final (getter, setter) = makeProperty('prop', intType);
final parent = makeInterface('Parent', null, []);
final category1 = makeCategory('Category1', parent, []);
final category2 = makeCategory('Category2', parent, []);
getter.originCategory = category1;
setter.originCategory = category1;
final clonedGetter = getter.clone(originCategory: category2);
expect(clonedGetter.originCategory, category2);
expect(clonedGetter.setter?.originCategory, category2);
},
);
test('clone preserves originCategory', () {
final method = makeMethod('catMethod', voidType, []);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, [method]);
final dest1 = makeInterface('Dest1', null, []);
final dest2 = makeInterface('Dest2', null, []);
dest1.copyMethod(method, originCategory: category);
final dest1Method = dest1.methods.single;
final cloned = dest1Method.clone(parent: dest2);
expect(cloned.originCategory, category);
});
test('clone property preserves originCategory on getter and setter', () {
final (getter, setter) = makeProperty('prop', intType);
final parent = makeInterface('Parent', null, []);
final category = makeCategory('Category', parent, []);
getter.originCategory = category;
setter.originCategory = category;
final clonedGetter = getter.clone();
expect(clonedGetter.originCategory, category);
expect(clonedGetter.setter?.originCategory, category);
});
});
}