resolveBinding static method

ResolvedBinding? resolveBinding(
  1. Environment env,
  2. STypeAnnotation? typeNode, {
  3. String? describedAs,
})

The value-independent half of a binding check, resolved once.

Returns null when the annotation admits everything, so the caller can skip the check entirely rather than call a predicate that always says yes.

Permissive wherever it cannot be sure. A false positive rejects a correct program, which is worse than the silent pass these checks replace, so every case the resolver cannot answer confidently returns null here: unannotated bindings, dynamic / void, annotations that fail to resolve, type parameters (which resolve to a placeholder rather than to the type actually supplied), and the structural function/record annotations whose comparison is a larger question than this check.

Split out from checkBindingType by SCD63 so a for-each loop can resolve its annotation once and check every element against the result. That is not a micro-optimisation: on a 200 000-element typed loop the resolution measured ~86% of the check's total cost (+16% over an unchecked loop, of which the subtype test itself was ~2%), because resolving a name walks the environment chain while comparing two resolved types does not.

Implementation

static ResolvedBinding? resolveBinding(
  Environment env,
  STypeAnnotation? typeNode, {
  String? describedAs,
}) {
  // An unannotated binding admits anything.
  if (typeNode == null) return null;
  // Only nominal annotations are checked. `int Function(String)` and
  // `(int, String)` need a structural comparison against a callable or a
  // record, and getting that wrong rejects working callbacks.
  if (typeNode is! SNamedType) return null;

  // Cheap spelling-level exit, before the environment lookup below. The same
  // test is repeated on the RESOLVED type, which is the one that matters: a
  // type parameter left unbound by a raw generic (`Box()` rather than
  // `Box<int>()`) is spelled `T` but resolves to `dynamic`.
  if (_isUncheckableTypeName(typeNode.name?.name ?? '')) return null;

  final RuntimeType declaredType;
  try {
    declaredType = _resolveTypeAnnotationDynamic(typeNode, env);
  } catch (_) {
    // Unresolvable annotation — stay permissive, as the return-type check
    // does.
    return null;
  }
  // A type parameter stands for a type that is not bound at this point, so a
  // comparison would be against the placeholder rather than against what the
  // caller actually supplied.
  if (declaredType is TypeParameter) return null;

  final declaredName = declaredType.name;
  // SCD90 removed a by-name repeat of the top-type test that used to stand
  // here (`declaredName == 'dynamic' || declaredName == 'void'`). It was a
  // workaround: `BridgedClass.isSubtypeOf` did not treat `dynamic` as a top
  // type, so a raw generic's unbound `T` — which is spelled `T` and RESOLVES
  // to `dynamic` — rejected a correct argument. The predicate answers that
  // question itself now, for every kind of target, so repeating it here would
  // be a spelling test standing in for a type question, which is the shape
  // SCC28 spent its whole budget removing. The cheap pre-resolution exit on
  // the ANNOTATION's lexeme stays: it skips an environment walk, and a
  // spelling test is the only thing available before resolution.

  return ResolvedBinding._(
    declaredType,
    declaredName,
    typeNode.isNullable,
    describedAs == null ? '' : " of '$describedAs'",
    _appliedDeclaredType(env, typeNode, declaredType),
  );
}