toBridgedInstance method

BridgedInstance<Object>? toBridgedInstance(
  1. Object? nativeObject
)

Converts a native object to a bridged instance if a bridge exists.

nativeObject The native object to convert.

Returns a BridgedInstance if a bridge is found for the object's type, otherwise throws a D4rtException (caller should handle).

Resolution order:

  1. Direct lookup by Type (most specific — bridges with explicit BridgedClass.nativeType).
  2. BridgedClass.isAssignable iteration — keeps the LAST match across all enclosing environments. Bridges register general→specific, so the last match is the most specific (e.g., CupertinoTextThemeData wins over Diagnosticable). Required to disambiguate native types whose name happens to be a prefix of another bridge's name — e.g., StringCharacters implements Characters: without this step the name-prefix fallback in toBridgedClass would wrap it as String. Bucket #11 fix.
  3. toBridgedClass name-based fallbacks (private impl types, generic suffix matching, *Impl prefix matching). Used only when neither direct type lookup nor isAssignable found a bridge.

Implementation

BridgedInstance? toBridgedInstance(Object? nativeObject) {
  if (nativeObject == null) {
    return null;
  }
  final runtimeType = nativeObject.runtimeType;

  // 1) Direct type lookup.
  Environment? current = this;
  while (current != null) {
    final direct = current._bridgedClassesLookupByType[runtimeType];
    if (direct != null) {
      return BridgedInstance(direct, nativeObject);
    }
    current = current._enclosing;
  }

  // 1b) Resolution cache. GEN-115 Phase 2: a previous call already paid
  //     the full step-2 / step-3 cost for this runtimeType; reuse it.
  //     Walks the env chain so a hit in any enclosing scope short-circuits.
  current = this;
  while (current != null) {
    final cached = current._resolvedTypeCache[runtimeType];
    if (cached != null) {
      return BridgedInstance(cached, nativeObject);
    }
    // T4 (perf, F3): negative-cache hit — a previous call already proved this
    // runtimeType has no bridge. Re-throw the same miss without re-walking the
    // chain, iterating every bridge, or running the name-fallback toString.
    if (current._unbridgedTypeCache.contains(runtimeType)) {
      throw RuntimeD4rtException(
        'Cannot bridge native object: No registered bridged class found '
        'for native type $runtimeType.',
      );
    }
    current = current._enclosing;
  }

  // 2) isAssignable iteration. Bridges may register in any order, so we
  //    collect ALL matches and then drop those that are supertypes of
  //    another match using [BridgedClass.transitiveSupertypeNames]. The
  //    remaining set is "leaf" matches; we pick the last one (preserves
  //    legacy LAST-wins behaviour when the registry doesn't disambiguate).
  //
  //    D2 fix: A native object whose runtimeType is a private impl of
  //    BoxConstraints (e.g. `_BodyBoxConstraints`) was wrapped as
  //    `Constraints` because the LAST-match-wins iteration picked the
  //    abstract base. With `BoxConstraints: [Constraints, ...]` registered
  //    in the supertype registry, the filter drops `Constraints` and
  //    keeps `BoxConstraints`, so `.maxWidth` resolves correctly.
  final allMatches = <BridgedClass>[];
  current = this;
  while (current != null) {
    for (final entry in current._bridgedClassesLookupByType.entries) {
      final bridge = entry.value;
      if (bridge.isAssignable != null && bridge.isAssignable!(nativeObject)) {
        allMatches.add(bridge);
      }
    }
    current = current._enclosing;
  }
  if (allMatches.isNotEmpty) {
    final filtered = _filterToMostSpecific(allMatches);
    final picked = filtered.isNotEmpty ? filtered.last : allMatches.last;
    _resolvedTypeCacheOrNew[runtimeType] = picked;
    return BridgedInstance(picked, nativeObject);
  }

  // 3) Name-based fallbacks (private impl, generic suffix, *Impl prefix).
  //    [toBridgedClass] will throw if no bridge matches — try the structural
  //    fallback (step 4) before giving up, and negative-cache the miss so
  //    repeats short-circuit (T4, F3).
  final BridgedClass bridgedClass;
  try {
    bridgedClass = toBridgedClass(runtimeType);
  } on RuntimeD4rtException {
    // 4) Structural suffix fallback (SCC49). Dart names implementation types
    //    after the interface they implement — `_CompactIterator`,
    //    `_SplayTreeKeyIterator`, `_HashMapKeyIterator`, `RuneIterator`,
    //    `_CompactKeysIterable` — and step 3 already exploits that, but only
    //    for names that are BOTH public AND generic: the suffix rule sits in
    //    the `else if (name.contains('<'))` arm of an
    //    `if (name starts with '_') … else if …` chain, so it is unreachable
    //    for private names (they take the first arm) and for non-generic
    //    public names (they enter neither). Those two shapes are the entire
    //    reason the stdlib bridges carry hand-maintained `nativeNames`
    //    allowlists: the `Iterator` bridge alone enumerates seventeen private
    //    SDK types, and the eighteenth an SDK release introduces surfaces to
    //    the script author as "Undefined property or method 'moveNext' on
    //    _CompactIterator".
    //
    //    WHY IT LIVES HERE AND NOT IN `toBridgedClass`. It was implemented
    //    there first, as a fourth pass past the loose prefix fallback, on the
    //    reasoning that a pass firing only where an exception is already
    //    thrown cannot regress a working case. That reasoning is wrong, and
    //    the suite said so: 43 tests failed, all of them enum dispatch. The
    //    throw is not merely a failure report — callers USE it as a control
    //    flow signal, catching it and falling through to the bridged-ENUM
    //    registry, which is a different registry from bridged classes. A
    //    bridged enum named `SimpleEnum` suffix-matches the `Enum` bridge, so
    //    widening `toBridgedClass` silently claimed it and the fallthrough
    //    never ran. Resolving here instead keeps `toBridgedClass` — which
    //    `getRuntimeType` and others also call — exactly as it was, and
    //    confines the widening to the one path that actually wraps a native
    //    object.
    //
    //    INTERPRETER-OWNED VALUES ARE EXCLUDED for the same reason, and the
    //    exclusion is wider than it first looks. The obvious guard —
    //    "skip native Dart enum values" — was not enough: the object that
    //    actually reached here in the enum tests was d4rt's own
    //    `BridgedEnum`, whose NAME ends with `Enum`, so it was claimed by the
    //    `Enum` bridge. Interpreter runtime objects (`RuntimeType`,
    //    `RuntimeValue`, `Callable`) are not native objects awaiting a
    //    bridge; they are the interpreter's own representation, and a
    //    name-shaped guess about them is never meaningful. Nothing about a
    //    suffix can distinguish them, so the guess must not be attempted.
    //
    //    `nativeNames` stays the fast path and the explicit-ownership
    //    override: an entry there matches in step 3 and never reaches here,
    //    so a bridge can still claim a type whose name points elsewhere —
    //    which is required, because the SDK abbreviates often enough
    //    (`_StreamSinkWrapper` for `StreamSink`, `_ControllerSubscription`
    //    for `StreamSubscription`) that the naming convention alone is not
    //    sufficient.
    final BridgedClass? structural = _isInterpreterOwned(nativeObject)
        ? null
        : _structuralSuffixBridge(runtimeType);
    if (structural != null) {
      Logger.debug(
        "[Environment] Matched native type '$runtimeType' to bridge "
        "'${structural.name}' via structural suffix matching",
      );
      _resolvedTypeCacheOrNew[runtimeType] = structural;
      return BridgedInstance(structural, nativeObject);
    }
    _unbridgedTypeCacheOrNew.add(runtimeType);
    rethrow;
  }
  _resolvedTypeCacheOrNew[runtimeType] = bridgedClass;
  return BridgedInstance(bridgedClass, nativeObject);
}