extractBridgedArg<T> static method
Extract a typed value from a BridgedInstance or native object.
Handles both wrapped (BridgedInstance) and unwrapped (native) objects. Throws ArgumentError if the type doesn't match.
If visitor is provided, interface proxy creation is enabled for
InterpretedInstance values that implement bridged abstract types.
INTER-003: Supports int→double promotion INTER-004: Supports collection type casting (List, Set, Map)
Implementation
static T extractBridgedArg<T>(
Object? arg,
String paramName, [
InterpreterVisitor? visitor,
]) {
// Unwrap BridgedInstance or BridgedEnumValue if needed
final unwrapped = arg is BridgedInstance
? arg.nativeObject
: arg is BridgedEnumValue
? arg.nativeValue
: arg;
if (unwrapped is T) {
// GEN-C3: Deep-unwrap container contents for unbounded T.
// When the bridge adapter requests `dynamic`, `Object`, or `Object?`
// (typically because the underlying parameter is declared as such —
// e.g. `MessageCodec.encodeMessage(dynamic message)`), nested
// BridgedInstance/BridgedEnumValue values inside Map/List/Set
// containers must be unwrapped before reaching native code. Native
// receivers (StandardMessageCodec, JSON codecs, …) cannot interpret
// BridgedInstance wrappers and reject them with `Invalid argument`.
// Typed parameters (e.g. `Map<String, Widget>`) take a different
// path via `coerceMap` and are unaffected by this branch.
final tName = typeName(T);
if (tName == 'dynamic' || tName == 'Object' || tName == 'Object?') {
final deep = _deepUnwrap(unwrapped);
if (deep is T) return deep;
}
return unwrapped;
}
// ENG-007: Nullable type fallback.
// When T is nullable (e.g., TextStyle?) and unwrapped is the non-nullable
// base type (TextStyle), the `is T` check should succeed in Dart.
// However, cross-package or reified generics edge cases may cause `is T`
// to fail. This fallback uses a dynamic cast as a safety net.
if (null is T && unwrapped != null) {
try {
return unwrapped as T;
} catch (_) {
// Fall through to subsequent checks
}
}
// GEN-100: String-based nullable type check fallback.
// In some Flutter test environments, `v is T?` can incorrectly return
// false even when v's runtimeType matches the non-nullable base of T.
// This check compares type names as strings as a last resort.
if (null is T && unwrapped != null) {
final tStr = typeName(T);
final unwrappedTypeStr = typeName(unwrapped.runtimeType);
// Check if T is "SomeType?" and unwrapped is "SomeType"
if (tStr.endsWith('?') &&
tStr.substring(0, tStr.length - 1) == unwrappedTypeStr) {
// The types match semantically, force the return through dynamic.
// GEN-100b (C07): When two classes share a simple name but live in
// different libraries (e.g. painting.StrutStyle vs dart:ui.StrutStyle),
// the `as T` cast still throws TypeError even though the simple-name
// match succeeded. Wrap in try/catch so the subsequent RC-3
// cross-package type-coercion path can fire on the same argument
// instead of surfacing a raw TypeError out of the bridge.
try {
final dynamic temp = unwrapped;
return temp as T;
} catch (_) {
// Fall through to RC-3 coercion or final ArgumentD4rtException.
}
}
}
// ENG-002: BridgedClass → Type conversion.
// When a class name appears in expression position (e.g., as a map key
// like `{ActivateIntent: ...}`), the interpreter resolves it to a
// BridgedClass object. Convert to the native Type for bridges expecting Type.
if (arg is BridgedClass && arg.nativeType is T) {
return arg.nativeType as T;
}
// RC-6c: Callable → native function wrapping.
// When T is a function type (e.g., void Function(), GestureTapCallback)
// and the value is a Callable (InterpretedFunction or NativeFunction),
// wrap it in a native closure so it can be assigned to typed function
// properties. Uses the same pattern as _wrapCallableForMap.
if (unwrapped is Callable) {
final tStr = typeName(T);
if (tStr.contains('Function')) {
final effectiveVisitor = visitor ?? _activeVisitor;
if (effectiveVisitor != null) {
final wrapped = _wrapCallableForMap<T>(unwrapped, effectiveVisitor);
if (wrapped is T) return wrapped;
}
}
}
// GEN-079: Generic type wrapper resolution.
// When T is a complex generic (e.g., WidgetStateProperty<Color?>?),
// the `is T` check fails if the value was created with `<dynamic>`
// (e.g., WidgetStatePropertyAll<dynamic>) because Dart's reified generics
// require exact type argument matching for subtype checks.
// Use registered wrapper factories to create properly typed proxy objects.
// Factories are checked additively — each module contributes cases for
// its own types, and the first factory to return non-null wins.
//
// IMPORTANT: Inlined (not via _tryGenericWrapperResolution helper) so that
// when a factory returns null for an unrecognised innerTypeArg and T is
// nullable, `null is T` returns the null directly — which is valid for
// optional bridge parameters. The helper route checked `wrapperResult !=
// null`, masking valid null-for-nullable returns and falling through to a
// spurious ArgumentD4rtException.
if (unwrapped != null && _genericTypeWrappers.isNotEmpty) {
final tStr = typeName(T);
// Strip trailing '?' for nullable generic types
String baseT = tStr;
while (baseT.endsWith('?')) {
baseT = baseT.substring(0, baseT.length - 1);
}
if (baseT.contains('<')) {
final baseTypeName = baseT.substring(0, baseT.indexOf('<'));
final innerTypeArg = baseT.substring(
baseT.indexOf('<') + 1,
baseT.lastIndexOf('>'),
);
// Try with the target base type name first, then with the value's
// runtime base type name (e.g., WidgetStatePropertyAll when target is
// WidgetStateProperty).
final valueName = typeName(unwrapped.runtimeType);
final valueBaseName = valueName.contains('<')
? valueName.substring(0, valueName.indexOf('<'))
: valueName;
final typeNamesToTry = <String>{baseTypeName, valueBaseName};
for (final typeName in typeNamesToTry) {
final factories = _genericTypeWrappers[typeName];
if (factories == null) continue;
for (final factory in factories) {
// Only accept non-null results: null means "not handled by this
// factory, keep trying the next one".
final wrapped = factory(unwrapped, innerTypeArg);
if (wrapped != null && wrapped is T) {
if (usageLogEnabled) {
recordUsageHit('relaxer', typeName, innerTypeArg);
}
return wrapped as T;
}
// GEN-079b: If innerTypeArg is nullable (e.g., 'Color?'), also try
// the non-nullable form. The wrapper created with non-nullable T
// will still be assignable to the nullable target.
if (innerTypeArg.endsWith('?')) {
final nonNullableArg = innerTypeArg.substring(
0,
innerTypeArg.length - 1,
);
final wrapped2 = factory(unwrapped, nonNullableArg);
if (wrapped2 != null && wrapped2 is T) {
if (usageLogEnabled) {
recordUsageHit('relaxer', typeName, nonNullableArg);
}
return wrapped2 as T;
}
}
}
}
}
}
// INTER-003: int→double promotion (handles both double and double?)
if (_isDoubleType<T>() && unwrapped is int) {
return unwrapped.toDouble() as T;
}
// INTER-003b: int→num promotion (handles both num and num?)
if (_isNumType<T>() && unwrapped is int) {
return unwrapped as T;
}
// INTER-004: Collection type casting
// GEN-075: Unwrap BridgedInstance/BridgedEnumValue elements first
final tStr = typeName(T);
// List<Object?> → List<T> (also handles Iterable<T>)
if (unwrapped is List &&
(tStr.startsWith('List<') || tStr.startsWith('Iterable<'))) {
try {
// Unwrap any BridgedInstance/BridgedEnumValue elements
final unwrappedList = unwrapped.map(_unwrapElement).toList();
// Extract element type from T string
final prefixLen = tStr.startsWith('List<') ? 5 : 9;
final elementType = tStr.substring(prefixLen, tStr.length - 1);
final result = switch (elementType) {
'int' => unwrappedList.cast<int>().toList(),
'double' =>
unwrappedList
.map((e) => e is int ? e.toDouble() : e)
.cast<double>()
.toList(),
'String' => unwrappedList.cast<String>().toList(),
'num' => unwrappedList.cast<num>().toList(),
'bool' => unwrappedList.cast<bool>().toList(),
'Object' || 'dynamic' => unwrappedList.cast<Object>().toList(),
// ENG-001: For non-primitive types, use coerceList which handles
// BridgedInstance/InterpretedInstance/BridgedEnumValue unwrapping
// and produces a properly typed List<T> via element casting.
_ => unwrappedList,
};
// ENG-001: Try typed cast; if it fails, try coerceList which creates
// a properly-typed list using per-element casting.
try {
return result as T;
} catch (_) {
// Fall through — collection is right shape but wrong generic type
}
} catch (_) {
// Fall through to error
}
}
// C35: Iterator<Object?> → Iterator<T>.
// Typed list literals (e.g. `<int>[1, 2, 3]`) lose their element type
// inside the interpreter and produce a `List<Object?>`, whose `.iterator`
// is `ListIterator<Object?>`. Bridge constructors that declare an
// `Iterator<T>` parameter (e.g. `CachingIterable<T>(Iterator<T> source)`)
// then reject the value via reified-generics TypeError. Wrap the source
// iterator in a typed cast iterator that lazily casts each `current`
// element, mirroring `Iterable.cast<T>().iterator`.
if (unwrapped is Iterator && tStr.startsWith('Iterator<')) {
final elementType = tStr.substring(9, tStr.length - 1);
final source = unwrapped;
final Iterator<Object?>? result = switch (elementType) {
'int' => _CastIterator<int>(source),
'double' => _PromotingDoubleIterator(source),
'String' => _CastIterator<String>(source),
'num' => _CastIterator<num>(source),
'bool' => _CastIterator<bool>(source),
'Object' ||
'dynamic' ||
'Object?' =>
_CastIterator<Object?>(source),
_ => null,
};
if (result != null) {
try {
return result as T;
} catch (_) {
// Fall through to error.
}
}
}
// Set<Object?> → Set<T>
if ((unwrapped is Set || (unwrapped is Map && tStr.startsWith('Set<'))) &&
tStr.startsWith('Set<')) {
try {
// D4rt may produce Maps for set literals (e.g., `{}` defaults to Map).
// Coerce Map keys → Set elements.
final source = unwrapped is Map ? unwrapped.keys : (unwrapped as Set);
final unwrappedSet = source.map(_unwrapElement).toSet();
final elementType = tStr.substring(4, tStr.length - 1);
final result = switch (elementType) {
'int' => unwrappedSet.cast<int>().toSet(),
'double' =>
unwrappedSet
.map((e) => e is int ? e.toDouble() : e)
.cast<double>()
.toSet(),
'String' => unwrappedSet.cast<String>().toSet(),
'num' => unwrappedSet.cast<num>().toSet(),
'bool' => unwrappedSet.cast<bool>().toSet(),
'Object' || 'dynamic' => unwrappedSet.cast<Object>().toSet(),
// RC-7c: For non-primitive element types (e.g., Set<WidgetState>),
// attempt Set.from() which uses runtime type coercion. Elements
// are already unwrapped to native values, so the typed set
// constructor can succeed if all elements are the correct type.
_ => unwrappedSet,
};
try {
return result as T;
} catch (_) {
// RC-7c: Direct cast failed (e.g., Set<Object?> as Set<EnumType>).
// Non-primitive Set coercion requires coerceSet<ElementType>() at
// the call site (bridge adapters). extractBridgedArg cannot create
// typed sets without compile-time type parameters.
// Fall through to error.
}
} catch (_) {
// Fall through to error
}
}
// Map casting support
// When T is Map<K,V> and the value is Map<Object?, Object?>, unwrap
// BridgedInstance/BridgedEnumValue keys and values, then try to cast
// the resulting map. Uses coerceMap for proper typed map creation
// when the basic unwrap+cast approach fails.
if (unwrapped is Map && tStr.startsWith('Map<')) {
try {
// ENG-001: Try unwrapping map keys and values first
final unwrappedMap = <Object?, Object?>{};
for (final entry in unwrapped.entries) {
unwrappedMap[_unwrapElement(entry.key)] = _unwrapElement(entry.value);
}
try {
return unwrappedMap as T;
} catch (_) {}
// Fall back to original map
try {
return unwrapped as T;
} catch (_) {}
// MAP-COERCE: Rebuild as a typed Map<K, V> for common primitive
// combinations. Reified generics defeat the bare casts above
// (a Map<Object?, Object?> cannot be cast to Map<String, String>),
// so parse K and V from the type string and dispatch through
// _buildTypedMap. Mirrors the Set branch above and unblocks the
// dcli `withEnvironmentAsync(environment: <String, String>{...})`
// path (Cluster MAP-COERCE).
final inner = tStr.substring(4, tStr.length - 1);
final commaIdx = _splitTopLevelComma(inner);
if (commaIdx > 0) {
final keyType = inner.substring(0, commaIdx).trim();
final valueType = inner.substring(commaIdx + 1).trim();
final typed = _buildTypedMap(unwrappedMap, keyType, valueType);
if (typed != null) {
try {
return typed as T;
} catch (_) {}
}
}
// GEN-079: Generic wrapper resolution for map values.
// Try wrapping individual values through registered factories.
final rewrappedMap = <Object?, Object?>{};
for (final entry in unwrappedMap.entries) {
rewrappedMap[entry.key] = entry.value;
}
return rewrappedMap as T;
} catch (_) {
// Fall through to error
}
}
// RC-1: InterpretedInstance → bridgedSuperObject unwrapping.
// When a D4rt script class extends a bridged class (e.g.,
// `class _StatefulDemo extends StatefulWidget`), the InterpretedInstance
// holds the native object in bridgedSuperObject.
if (arg is InterpretedInstance) {
// RC-5: Check nativeProxy first. When a native proxy (e.g.,
// _InterpretedTickerProviderState) wraps the InterpretedInstance,
// return it directly if it satisfies T. This avoids creating a new
// delegation wrapper when the proxy already implements the interface.
final proxy = arg.nativeProxy;
if (proxy != null && proxy is T) {
return proxy as T;
}
final superObj = arg.bridgedSuperObject;
// RC-7: Check superObj != null before returning, otherwise a null
// bridgedSuperObject (from abstract classes like CustomPainter) would
// match nullable T (e.g., CustomPainter?) and skip proxy resolution.
if (superObj != null && superObj is T) {
// 1401-TODO #7 (F9): record the native↔interpreted mapping so
// later property assignments on this native object can route
// back to script-defined fields/setters. See
// [_nativeToInterpreted].
registerInterpretedForNative(superObj, arg);
return superObj as T;
}
// RC-6b: After superObj fails the `is T` check (e.g., Tween<dynamic>
// is not Animatable<double> due to reified generics), try the generic
// wrapper resolution on superObj. The relaxer factory can wrap the
// native object in a properly typed proxy.
if (superObj != null && _genericTypeWrappers.isNotEmpty) {
final wrapped = _tryGenericWrapperResolution<T>(superObj);
if (wrapped != null) return wrapped;
}
// RC-1: Try registered interface proxy factories.
// For abstract classes/interfaces (CustomClipper, TickerProvider),
// bridgedSuperObject may be null. Use a proxy factory to create a
// native delegate that calls back into the interpreter.
final effectiveVisitor = visitor ?? _activeVisitor;
if (_interfaceProxies.isNotEmpty && effectiveVisitor != null) {
final proxyResult = tryCreateInterfaceProxyWithVisitor<T>(
arg,
effectiveVisitor,
);
if (proxyResult != null) {
if (usageLogEnabled) {
recordUsageHit('proxy', _baseTypeName(T.toString()), arg.klass.name);
}
return proxyResult;
}
}
}
// RC-3: Cross-package type coercion.
// When the unwrapped value has the same conceptual type but from a
// different package (e.g., painting.TextStyle vs dart:ui.TextStyle),
// use a registered coercion to convert.
if (unwrapped != null && _typeCoercionsByType.isNotEmpty) {
final sourceType = unwrapped.runtimeType;
for (final entry in _typeCoercionsByType.entries) {
if (entry.key.sourceType == sourceType) {
final coerced = entry.value(unwrapped);
if (coerced is T) {
if (usageLogEnabled) {
recordUsageHit(
'coercion',
_baseTypeName(T.toString()),
sourceType.toString(),
);
}
return coerced;
}
}
}
}
// P&R#3: last-resort lookup against user-registered relaxer factories,
// covering non-generic target types the inlined relaxer path skips. Runs
// only here on the about-to-throw path, so it is strictly additive.
final userResolved = _tryUserFactoryResolution<T>(unwrapped);
if (userResolved != null) return userResolved;
if (usageLogEnabled) {
final tStr = T.toString();
recordUsageMiss(_baseTypeName(tStr), _innerTypeArg(tStr));
}
final actualType = arg is BridgedInstance
? arg.nativeObject.runtimeType
: arg is InterpretedInstance
? 'InterpretedInstance(${arg.klass.name})'
: arg.runtimeType;
throw ArgumentD4rtException(
_missingBridgeResolutionMessage(paramName, T.toString(), actualType),
);
}