@@ -256,3 +256,76 @@ internal func expressionType(_ type: String) -> String {
256256 }
257257 return type. dropLast ( ) + " ? "
258258}
259+
260+ // MARK: - @JS property collection
261+
262+ /// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
263+ /// Shared between `@ExpoModule` and `@SharedObject` — the resulting properties are receiver-agnostic;
264+ /// the decorator that emits them picks the receiver (module `self` vs. shared-object `_self`).
265+ internal func collectProperties(
266+ varDecl: VariableDeclSyntax ,
267+ attribute: AttributeSyntax
268+ ) -> [ JSProperty ] {
269+ let jsNameOverride = jsNameArgument ( of: attribute)
270+ // A `let` is never settable; only `var` bindings can carry a setter.
271+ let isVar = varDecl. bindingSpecifier. tokenKind == . keyword( . var)
272+
273+ return varDecl. bindings. compactMap { binding in
274+ guard let ident = binding. pattern. as ( IdentifierPatternSyntax . self) else {
275+ return nil
276+ }
277+ let swiftName = ident. identifier. text
278+ // Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
279+ // when there's none. `nil` falls back to inference at the use site.
280+ let valueType = binding. typeAnnotation? . type. trimmedDescription
281+ ?? binding. initializer. flatMap { inferredLiteralType ( of: $0. value) }
282+ return JSProperty (
283+ swiftName: swiftName,
284+ jsName: jsNameOverride ?? swiftName,
285+ valueType: valueType,
286+ isSettable: isVar && bindingIsSettable ( binding)
287+ )
288+ }
289+ }
290+
291+ /// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
292+ /// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
293+ /// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
294+ /// observers imply stored storage, which is also settable.
295+ private func bindingIsSettable( _ binding: PatternBindingSyntax ) -> Bool {
296+ guard let accessorBlock = binding. accessorBlock else {
297+ return true
298+ }
299+ switch accessorBlock. accessors {
300+ case . accessors( let accessors) :
301+ return accessors. contains { accessor in
302+ switch accessor. accessorSpecifier. tokenKind {
303+ case . keyword( . set) , . keyword( . willSet) , . keyword( . didSet) :
304+ return true
305+ default :
306+ return false
307+ }
308+ }
309+ case . getter:
310+ return false
311+ }
312+ }
313+
314+ /// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly
315+ /// (`asDouble()` for `Double`, etc.), bypassing the dynamic-type converter. Returns `nil` for types
316+ /// without a dedicated accessor (arrays, records, optionals, shared objects, other numeric widths),
317+ /// which decode through `getDynamicType().cast(...)`.
318+ func fastDecodeAccessor( for type: String ) -> String ? {
319+ switch type {
320+ case " Bool " :
321+ return " asBool "
322+ case " Int " :
323+ return " asInt "
324+ case " Double " :
325+ return " asDouble "
326+ case " String " :
327+ return " asString "
328+ default :
329+ return nil
330+ }
331+ }
0 commit comments