Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions NativeScript/runtime/ArgConverter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -883,16 +883,18 @@

ObjCDataWrapper* objcDataWrapper = static_cast<ObjCDataWrapper*>(wrapper);
id target = objcDataWrapper->Data();
if (![target isKindOfClass:[NSArray class]]) {
// Treat anything that can report a count and return elements by index as
// indexable, not just NSArray (e.g. PHFetchResult, NSOrderedSet).
if (![target respondsToSelector:@selector(count)] ||
![target respondsToSelector:@selector(objectAtIndex:)]) {
return;
}

NSArray* array = (NSArray*)target;
if (index >= [array count]) {
if (index >= [target count]) {
return;
}

id obj = [array objectAtIndex:index];
id obj = [target objectAtIndex:index];

std::shared_ptr<Caches> cache = Caches::Get(isolate);
auto it = cache->Instances.find(obj);
Expand Down Expand Up @@ -959,6 +961,16 @@
ObjCDataWrapper* objcDataWrapper = static_cast<ObjCDataWrapper*>(wrapper);
id target = objcDataWrapper->Data();
if (![target isKindOfClass:[NSMutableArray class]]) {
// Indexed assignment is only supported for NSMutableArray. Other indexable
// collections (e.g. NSOrderedSet, PHFetchResult) reject the write so it can't be
// silently dropped into an unreachable JS property on the wrapper. NSArray is
// excluded from the throw and falls through to a silent no-op.
if ([target respondsToSelector:@selector(count)] &&
[target respondsToSelector:@selector(objectAtIndex:)] &&
![target isKindOfClass:[NSArray class]]) {
isolate->ThrowException(Exception::TypeError(
tns::ToV8String(isolate, "Indexed assignment is only supported for NSMutableArray")));
}
return;
}

Expand Down
39 changes: 39 additions & 0 deletions TestRunner/app/tests/ApiTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,45 @@ describe(module.id, function () {
expect(res[0].constructor.name).toEqual("NSURL");
});

it("indexed access works for non-NSArray indexable collections", function () {
// NSOrderedSet is not an NSArray but responds to count + objectAtIndex:,
// so obj[i] should resolve the same elements as objectAtIndex(i).
const set = NSOrderedSet.orderedSetWithArray(['a', 'b', 'c']);
expect(set.count).toBe(3);
expect(set[0]).toBe(set.objectAtIndex(0));
expect(set[1]).toBe(set.objectAtIndex(1));
expect(set[2]).toBe(set.objectAtIndex(2));
expect(set[0]).toBe('a');
expect(set[2]).toBe('c');
// Out-of-range index returns undefined instead of throwing.
expect(set[3]).toBeUndefined();
});

it("indexed assignment throws for read-only indexable collections", function () {
const set = NSOrderedSet.orderedSetWithArray(['a', 'b', 'c']);
// NSOrderedSet is indexable but not writable, so indexed assignment throws.
// The index is irrelevant; the whole collection rejects writes.
expect(function () { set[0] = 'ghost'; }).toThrowError(TypeError, /Indexed assignment is only supported for NSMutableArray/);
expect(function () { set[5] = 'ghost'; }).toThrowError(TypeError);
// The rejected out-of-range write left no stray JS property behind, the native
// collection is untouched, and reads still resolve native elements.
expect(Object.prototype.hasOwnProperty.call(set, '5')).toBe(false);
expect(set.count).toBe(3);
expect(set[0]).toBe('a');
expect(set[5]).toBeUndefined();
});

it("indexed assignment still works for NSMutableArray", function () {
const first = NSObject.new();
const arr = NSMutableArray.arrayWithArray([first, NSObject.new()]);
const replacement = NSObject.new();

arr[0] = replacement;
expect(arr.count).toBe(2);
expect(arr.objectAtIndex(0)).toBe(replacement);
expect(arr[0]).toBe(replacement);
});

it("MethodCalledInDealloc", function () {
expect(function () {
(function () {
Expand Down
Loading