Skip to content

Commit 241c994

Browse files
committed
fix: prefer JS subclass property setters
1 parent 2295794 commit 241c994

3 files changed

Lines changed: 86 additions & 2 deletions

File tree

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,6 +1880,21 @@ NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value
18801880
throw JSError(runtime, "Cannot set property on nil object.");
18811881
}
18821882

1883+
bool isEngineExtendedInstance =
1884+
class_conformsToProtocol(object_getClass(object_),
1885+
@protocol(NativeApiClassBuilderProtocol));
1886+
if (isEngineExtendedInstance) {
1887+
#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE
1888+
if (invokeEnginePrototypeSetter(runtime, property, value)) {
1889+
NATIVE_API_SET_RETURN(true);
1890+
}
1891+
#else
1892+
if (enginePrototypeHasSetter(runtime, property)) {
1893+
NATIVE_API_SET_RETURN(false);
1894+
}
1895+
#endif
1896+
}
1897+
18831898
if (Class appearanceClass =
18841899
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
18851900
if (const NativeApiSymbol* symbol =
@@ -1950,8 +1965,7 @@ throw JSError(
19501965
// For JS-subclassed instances, an unknown property is owned by the JS
19511966
// prototype (e.g. a JS-defined accessor); defer so the engine runs it instead of
19521967
// shadowing it with a bridge expando.
1953-
if (class_conformsToProtocol(object_getClass(object_),
1954-
@protocol(NativeApiClassBuilderProtocol))) {
1968+
if (isEngineExtendedInstance) {
19551969
#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE
19561970
// Engines whose exotic property storage doesn't fall back to own
19571971
// properties need the JS-owned set resolved here: invoke a JS-prototype

PROGRESS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,42 @@ TypeScript/UI worklets.
1717

1818
## Latest Update - 2026-06-30
1919

20+
### 2026-06-30 02:10 EDT - JS subclass setters prefer prototype accessors before native setters
21+
22+
- Scope check:
23+
- Still generic NativeScript runtime behavior on the RN-module branch. No RNS
24+
native implementation and no direct-engine backend refactor work.
25+
- Simulator-only rule remains active; no physical devices were used.
26+
- CI finding:
27+
- GitHub Actions run `28423162506` on `2295794f` kept setup, FFI boundary
28+
check, native build, CLI build, and macOS tests green.
29+
- iOS still completed with one failure:
30+
`ApiTests.js SpecialCaseProperty_When_CustomSelector_ImplementedInJS`.
31+
- `ApiTests.js NSMutableArrayMethods` still passed, so the construction-state
32+
fix remains valid.
33+
- The repeated `getter` output persisted because writes to a JS-overridden
34+
native property still hit the native setter before the JS prototype setter.
35+
- Change:
36+
- JS-extended object property writes now prefer JS prototype setters before
37+
native metadata/runtime setters on every backend.
38+
- The existing unknown-property expando fallback remains in place for JS
39+
fields without a prototype setter.
40+
- Source coverage now guards both getter and setter prototype precedence in
41+
the tracked runtime bridge and RN mirror.
42+
- Verification:
43+
- Focused construction/property guard test passed:
44+
`node packages/react-native/test/runtime-instance-selector-base-dispatch.test.js`.
45+
- Runtime RN JS tests passed:
46+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
47+
- `git diff --check` passed.
48+
- `npm run check:ffi-boundaries` passed.
49+
- Existing generated macOS project compile/link passed with
50+
`xcodebuild -project dist/intermediates/macos/NativeScript.xcodeproj ...`.
51+
- Still next:
52+
- Commit/push this setter-precedence fix and watch fresh PR CI. Expected
53+
result: macOS remains green and iOS clears the remaining
54+
`SpecialCaseProperty_When_CustomSelector_ImplementedInJS` failure.
55+
2056
### 2026-06-30 01:44 EDT - JS subclass getters prefer prototype accessors before ObjC runtime getters
2157

2258
- Scope check:

packages/react-native/test/runtime-instance-selector-base-dispatch.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,40 @@ for (const relativePath of [
8484
guardedIndex < branchIndex,
8585
`${relativePath}: JS-subclassed property reads should prefer prototype getters before native runtime getters on every backend`,
8686
);
87+
88+
const nilObjectSetIndex = source.indexOf(
89+
'throw JSError(runtime, "Cannot set property on nil object.");',
90+
);
91+
const setIndex = source.lastIndexOf(
92+
"NativeApiHostSetResult set(",
93+
nilObjectSetIndex,
94+
);
95+
const writableIndex = source.indexOf(
96+
"selectWritablePropertyMember(members, property, false)",
97+
setIndex,
98+
);
99+
const explicitSetterIndex = source.indexOf(
100+
"invokeEnginePrototypeSetter(runtime, property, value)",
101+
setIndex,
102+
);
103+
const fallbackSetterIndex = source.indexOf(
104+
"enginePrototypeHasSetter(runtime, property)",
105+
setIndex,
106+
);
107+
108+
assert(
109+
setIndex !== -1 &&
110+
nilObjectSetIndex !== -1 &&
111+
setIndex < nilObjectSetIndex &&
112+
writableIndex !== -1 &&
113+
explicitSetterIndex !== -1 &&
114+
fallbackSetterIndex !== -1 &&
115+
setIndex < explicitSetterIndex &&
116+
explicitSetterIndex < writableIndex &&
117+
setIndex < fallbackSetterIndex &&
118+
fallbackSetterIndex < writableIndex,
119+
`${relativePath}: JS-subclassed property writes should prefer prototype setters before native runtime setters on every backend`,
120+
);
87121
}
88122

89123
for (const relativePath of [

0 commit comments

Comments
 (0)