runWithReporter method

  1. @override
void runWithReporter(
  1. SaropaDiagnosticReporter reporter,
  2. SaropaContext context
)
override

Override this method to implement your lint rule.

Use context to register callbacks for AST node types:

context.addMethodInvocation((node) {
  if (condition) {
    reporter.atNode(node);
  }
});

Implementation

@override
void runWithReporter(
  SaropaDiagnosticReporter reporter,
  SaropaContext context,
) {
  context.addMethodInvocation((MethodInvocation node) {
    // Identify a real dialog launch, not any identifier containing "Dialog".
    // A bare `name.contains('Dialog')` flags l10n string getters such as
    // `emergencyDirectoryDialogHeader(...)` (returns String) as dialog
    // launches, demanding a meaningless pumpAndSettle. A genuine dialog API
    // is either a known launcher OR a "*Dialog*" call that returns an
    // awaitable (Future/void) — a String/int/bool getter never shows a dialog.
    final String invokedName = node.methodName.name;
    const Set<String> dialogLaunchers = <String>{
      'showDialog',
      'showGeneralDialog',
      'showAdaptiveDialog',
      'showDialogCommon',
      'showModalBottomSheet',
      'showCupertinoDialog',
      'showCupertinoModalPopup',
    };
    if (!dialogLaunchers.contains(invokedName)) {
      if (!invokedName.contains('Dialog')) return;
      // A "*Dialog*" call is a real dialog launch only when its return type is
      // a CONFIRMED awaitable (Future/void). A String/int/bool getter such as
      // the l10n `emergencyDirectoryDialogHeader(...)` is not. Unresolved
      // (null) type is NOT treated as awaitable: genuine launchers are already
      // matched by name above, so the type-fallback path only needs to admit
      // confirmed-awaitable custom helpers, never guess on unresolved types.
      final DartType? returnType = node.staticType;
      final bool awaitable =
          returnType != null &&
          (returnType.isDartAsyncFuture || returnType is VoidType);
      if (!awaitable) return;
    }

    // Check if inside a test function
    if (!_isInsideTestForDialog(node)) return;

    // Look for pumpAndSettle after the dialog
    final AstNode? block = _findContainingBlockForDialog(node);
    if (block == null) return;

    final String blockSource = block.toSource();
    final int dialogIndex = blockSource.indexOf(node.toSource());

    // Check if pumpAndSettle comes after showDialog
    final String afterDialog = blockSource.substring(dialogIndex);
    if (!afterDialog.contains('pumpAndSettle')) {
      reporter.atNode(node);
    }
  });
}