createMatcherString method

String? createMatcherString({
  1. Map<String, String> propNameOverrides = const {},
  2. String? imports,
  3. bool filter(
    1. DiagnosticsNode node
    )?,
})

Generates matchers for the properties of W.

Implementation

String? createMatcherString({
  Map<String, String> propNameOverrides = const {},
  String? imports,
  bool Function(DiagnosticsNode node)? filter,
}) {
  final s = snapshot()..existsAtLeastOnce();
  final anyElement = s.discoveredElements.first;

  final elementProps = anyElement.toDiagnosticsNode().getProperties();
  final widgetProps =
      mapElementToWidget(anyElement).toDiagnosticsNode().getProperties();

  String widgetType = _typeOf<W>().toString().capitalize();
  if (widgetType.contains('<')) {
    widgetType = widgetType.substring(0, widgetType.indexOf('<'));
  }
  bool addedMethods = false;

  final matcherSb = StringBuffer();
  matcherSb.writeln('''
/// Matchers for the properties of [$widgetType] provided via [Diagnosticable.debugFillProperties]
extension ${widgetType}Matcher on WidgetMatcher<$widgetType> {
''');

  final selectorSb = StringBuffer();
  selectorSb.writeln(
    '''
/// Allows filtering [$widgetType] by the properties provided via [Diagnosticable.debugFillProperties]
extension ${widgetType}Selector on WidgetSelector<$widgetType> {
''',
  );

  final getterSb = StringBuffer();
  getterSb.writeln(
    '''
/// Retrieves the [DiagnosticsProperty] of the matched widget with [propName] of type [T]
extension ${widgetType}Getter on WidgetMatcher<$widgetType> {
''',
  );

  final distinctProps =
      [...widgetProps, ...elementProps].distinctBy((it) => it.name).toList();
  for (final DiagnosticsNode prop in distinctProps) {
    if (filter != null && !filter(prop)) {
      continue;
    }
    final String diagnosticPropName = prop.name!;
    final String methodPropName = () {
      final String name = prop.name!;
      final parts = name.split(RegExp('[^a-zA-Z]'));
      if (parts.length == 1) {
        return name;
      }

      // camel case
      return parts
          .mapIndexed((index, it) => index == 0 ? it : it.capitalize())
          .join();
    }();
    final humanPropName = propNameOverrides[methodPropName] ?? methodPropName;
    String propType = prop.getType();
    if (prop is ObjectFlagProperty &&
        (propType == 'Widget' || propType == 'Widget?')) {
      // matchers on widgets are not supported, use .spot() to check the tree further down
      continue;
    }
    if (prop is FlagProperty && methodPropName == 'dirty') {
      // dirty flags are irrelevant for assertions (and always false)
      continue;
    }
    if (propType.contains('=>')) {
      // ignore lambda properties
      continue;
    }
    if (prop.name == 'depth' || prop.name == 'key') {
      // ignore default properties that are covered by general Wiget selectors
      continue;
    }
    if (prop.name == 'dependencies') {
      if (propType == 'List<DiagnosticsNode>' ||
          propType == 'Set<InheritedElement>') {
        // Widget dependencies are only indirect properties
        continue;
      }
    }
    if (prop.name == 'renderObject' && propType == 'RenderObject') {
      final propValueRuntimeType = prop.value.runtimeType.toString();
      if (!propValueRuntimeType.startsWith('_')) {
        propType = propValueRuntimeType;
      }
    }

    if (prop.name == 'state' && propType.contains('State<StatefulWidget>')) {
      final propValueRuntimeType = prop.value.runtimeType.toString();
      if (propValueRuntimeType.startsWith('_')) {
        // this is not useful without type
        continue;
      }
      propType = propValueRuntimeType;
      continue;
    }
    final propTypeNullable = propType.endsWith('?') ? propType : '$propType?';

    String matcherVerb = 'has';
    if (humanPropName == 'enabled') {
      matcherVerb = 'is';
    }

    addedMethods = true;
    final widgetMatcherWithValueName =
        '$matcherVerb${humanPropName.capitalize()}';
    final widgetMatcherWithPropName = '${widgetMatcherWithValueName}Where';

    final String valueExample = _getExampleValue(node: prop);
    final String matcherExample = _getExampleValue(node: prop, matcher: true);

    getterSb.writeln('''
/// Returns the $humanPropName of the matched [$widgetType] via [Widget.toDiagnosticsNode]
$propType get${humanPropName.capitalize()}() {
  return getDiagnosticProp<$propType>('$diagnosticPropName');
}
''');

    matcherSb.writeln('''
/// Expects that $humanPropName of [$widgetType] matches the condition in [match].
///
/// #### Example usage:
/// ```dart
/// spot<$widgetType>().existsOnce().$widgetMatcherWithPropName($matcherExample);
/// ```
WidgetMatcher<$widgetType> $widgetMatcherWithPropName(MatchProp<$propType> match) {
  return hasDiagnosticProp<$propType>('$diagnosticPropName', match);
}

/// Expects that $humanPropName of [$widgetType] equals (==) [value].
///
/// #### Example usage:
/// ```dart
/// spot<$widgetType>().existsOnce().$widgetMatcherWithValueName($valueExample);
/// ```
WidgetMatcher<$widgetType> $widgetMatcherWithValueName($propTypeNullable value) {
  return hasDiagnosticProp<$propType>('$diagnosticPropName', (it) => value == null ? it.isNull() : it.equals(value));
}
''');

    final propPart = humanPropName.capitalize();

    selectorSb.writeln('''
/// Creates a [WidgetSelector] that finds all [$widgetType] where $humanPropName matches the condition.
///
/// #### Example usage:
/// ```dart
/// spot<$widgetType>().where$propPart($matcherExample).existsOnce();
/// ```
@useResult
WidgetSelector<$widgetType> where$propPart(MatchProp<$propType> match) {
  return withDiagnosticProp<$propType>('$diagnosticPropName', match);
}

/// Creates a [WidgetSelector] that finds all [$widgetType] where $humanPropName equals (==) [value].
///
/// #### Example usage:
/// ```dart
/// spot<$widgetType>().with$propPart($valueExample).existsOnce();
/// ```
@useResult
WidgetSelector<$widgetType> with$propPart($propTypeNullable value) {
  return withDiagnosticProp<$propType>('$diagnosticPropName', (it) => value == null ? it.isNull() : it.equals(value));
}
''');
  }

  matcherSb.writeln('}');
  selectorSb.writeln('}');
  getterSb.writeln('}');

  if (addedMethods == false) {
    // nothing added, don't generate the file at all
    return null;
  }

  final overridesParam = propNameOverrides.isEmpty
      ? ''
      : () {
          final map = propNameOverrides
              .mapEntries((it) => MapEntry("'${it.key}'", "'${it.value}'"))
              .map((it) => '${it.key}: ${it.value}')
              .joinToString(separator: ', ', prefix: '{', postfix: '}');
          return 'propNameOverrides: $map';
        }();

  return '''
// ignore_for_file: require_trailing_commas, directives_ordering
\/\/ coverage:ignore-file

/// Matchers for [$widgetType] auto-generated by spot
///
/// Can be generated with:
/// ```dart
/// spot<$widgetType>().printMatchers($overridesParam);
/// ```
library;

import 'package:flutter/foundation.dart';
${imports ?? "import 'package:flutter/widgets.dart';"}
import 'package:spot/spot.dart';

$selectorSb

$matcherSb

$getterSb
  ''';
}