guardSync static method

void guardSync()

Verifies that there are no guarded methods currently pending (see guard).

If a guarded method is currently pending, and this is not a call nested from inside that method's body (directly or indirectly), then this method will throw a detailed exception.

Implementation

static void guardSync() {
  if (_scopeStack.isEmpty) {
    // No scopes open, so we must be fine.
    return;
  }
  // Find the current TestAsyncUtils scope zone so we can see if it's the one we expect.
  final Zone? zone = _currentScopeZone;
  if (zone == _scopeStack.last.zone) {
    // We're still in the current scope zone. All good.
    return;
  }
  // If we get here, we know we've got a conflict on our hands.
  // We got an async barrier, but the current zone isn't the last scope that
  // we pushed on the stack.
  // Find which scope the conflict happened in, so that we know
  // which stack trace to report the conflict as starting from.
  //
  // For example, if we called an async method A, which ran its body in a
  // guarded block, and in its body it ran an async method B, which ran its
  // body in a guarded block, but we didn't await B, then in A's block we ran
  // an async method C, which ran its body in a guarded block, then we should
  // complain about the call to B then the call to C. BUT. If we called an async
  // method A, which ran its body in a guarded block, and in its body it ran
  // an async method B, which ran its body in a guarded block, but we didn't
  // await A, and then at the top level we called a method D, then we should
  // complain about the call to A then the call to D.
  //
  // In both examples, the scope stack would have two scopes. In the first
  // example, the current zone would be the zone of the _scopeStack[0] scope,
  // and we would want to show _scopeStack[1]'s creationStack. In the second
  // example, the current zone would not be in the _scopeStack, and we would
  // want to show _scopeStack[0]'s creationStack.
  int skipCount = 0;
  _AsyncScope candidateScope = _scopeStack.last;
  _AsyncScope scope;
  do {
    skipCount += 1;
    scope = candidateScope;
    if (skipCount >= _scopeStack.length) {
      if (zone == null) {
        break;
      }
      // Some people have reported reaching this point, but it's not clear
      // why. For now, just silently return.
      // TODO(ianh): If we ever get a test case that shows how we reach
      // this point, reduce it and report the error if there is one.
      return;
    }
    candidateScope = _scopeStack[_scopeStack.length - skipCount - 1];
  } while (candidateScope.zone != zone);
  final List<DiagnosticsNode> information = <DiagnosticsNode>[
    ErrorSummary('Guarded function conflict.'),
    ErrorHint('You must use "await" with all Future-returning test APIs.'),
  ];
  final _StackEntry? originalGuarder = _findResponsibleMethod(scope.creationStack, 'guard', information);
  final _StackEntry? collidingGuarder = _findResponsibleMethod(StackTrace.current, 'guardSync', information);
  if (originalGuarder != null && collidingGuarder != null) {
    final String originalKind = originalGuarder.className == null ? 'function' : 'method';
    String originalName;
    if (originalGuarder.className == null) {
      originalName = '$originalKind (${originalGuarder.methodName})';
      information.add(ErrorDescription(
        'The guarded "${originalGuarder.methodName}" function '
        'was called from ${originalGuarder.callerFile} '
        'on line ${originalGuarder.callerLine}.'
      ));
    } else {
      originalName = '$originalKind (${originalGuarder.className}.${originalGuarder.methodName})';
      information.add(ErrorDescription(
        'The guarded method "${originalGuarder.methodName}" '
        'from class ${originalGuarder.className} '
        'was called from ${originalGuarder.callerFile} '
        'on line ${originalGuarder.callerLine}.'
      ));
    }
    final String again = (originalGuarder.callerFile == collidingGuarder.callerFile) &&
                         (originalGuarder.callerLine == collidingGuarder.callerLine) ?
                         'again ' : '';
    final String collidingKind = collidingGuarder.className == null ? 'function' : 'method';
    String collidingName;
    if ((originalGuarder.className == collidingGuarder.className) &&
        (originalGuarder.methodName == collidingGuarder.methodName)) {
      originalName = originalKind;
      collidingName = collidingKind;
      information.add(ErrorDescription(
        'Then, it '
        'was called ${again}from ${collidingGuarder.callerFile} '
        'on line ${collidingGuarder.callerLine}.'
      ));
    } else if (collidingGuarder.className == null) {
      collidingName = '$collidingKind (${collidingGuarder.methodName})';
      information.add(ErrorDescription(
        'Then, the "${collidingGuarder.methodName}" function '
        'was called ${again}from ${collidingGuarder.callerFile} '
        'on line ${collidingGuarder.callerLine}.'
      ));
    } else {
      collidingName = '$collidingKind (${collidingGuarder.className}.${collidingGuarder.methodName})';
      information.add(ErrorDescription(
        'Then, the "${collidingGuarder.methodName}" method '
        '${originalGuarder.className == collidingGuarder.className ? "(also from class ${collidingGuarder.className})"
                                                                   : "from class ${collidingGuarder.className}"} '
        'was called ${again}from ${collidingGuarder.callerFile} '
        'on line ${collidingGuarder.callerLine}.'
      ));
    }
    information.add(ErrorDescription(
      'The first $originalName '
      'had not yet finished executing at the time that '
      'the second $collidingName '
      'was called. Since both are guarded, and the second was not a nested call inside the first, the '
      'first must complete its execution before the second can be called. Typically, this is achieved by '
      'putting an "await" statement in front of the call to the first.'
    ));
    if (collidingGuarder.className == null && collidingGuarder.methodName == 'expect') {
      information.add(ErrorHint(
        'If you are confident that all test APIs are being called using "await", and '
        'this expect() call is not being called at the top level but is itself being '
        'called from some sort of callback registered before the ${originalGuarder.methodName} '
        'method was called, then consider using expectSync() instead.'
      ));
    }
    information.add(DiagnosticsStackTrace(
      '\nWhen the first $originalName was called, this was the stack',
      scope.creationStack,
    ));
  } else {
    information.add(DiagnosticsStackTrace(
      '\nWhen the first function was called, this was the stack',
      scope.creationStack,
    ));
  }
  throw FlutterError.fromParts(information);
}