generateForAnnotatedElement method

  1. @override
Stream<String> generateForAnnotatedElement(
  1. Element element,
  2. ConstantReader annotation,
  3. BuildStep buildStep
)

Implement to return source code to generate for element.

This method is invoked based on finding elements annotated with an instance of T. The annotation is provided as a ConstantReader.

Supported return values include a single String or multiple String instances within an Iterable or Stream. It is also valid to return a Future of String, Iterable, or Stream.

Implementations should return null when no content is generated. Empty or whitespace-only String instances are also ignored.

Implementation

@override
Stream<String> generateForAnnotatedElement(
  Element element,
  ConstantReader annotation,
  BuildStep buildStep,
) async* {
  await validateSdkConstraint(buildStep);

  if (element is! ClassElement) {
    final friendlyName = element.displayName;
    throw InvalidGenerationSourceError(
      'Generator cannot target `$friendlyName`. '
      '`@CliOptions` can only be applied to a class.',
      todo: 'Remove the `@CliOptions` annotation from `$friendlyName`.',
      element: element,
    );
  }

  // Get all of the fields that need to be assigned
  // TODO: We only care about constructor things + writable fields, right?
  final fieldsList = createSortedFieldSet(element);

  // Explicitly using `LinkedHashMap` – we want these ordered.
  final fields = LinkedHashMap<String, FieldElement>.fromIterable(
    fieldsList,
    key: (f) => (f as FieldElement).name,
  );

  // Get the constructor to use for the factory

  final populateParserName = '_\$populate${element.name}Parser';
  final parserFieldName = '_\$parserFor${element.name}';
  final resultParserName = '_\$parse${element.name}Result';

  if (fieldsList.any((fe) => fe.type.isEnum)) {
    yield enumValueHelper;

    if (fieldsList.any(
      (fe) =>
          fe.type.isEnum &&
          fe.type.nullabilitySuffix != NullabilitySuffix.none,
    )) {
      yield nullableEnumValueHelper;
    }
  }

  if (fieldsList.any((fe) => numChecker.isAssignableFromType(fe.type))) {
    yield r'''
T _$badNumberFormat<T extends num>(
String source,
String type,
String argName,
) =>
throw FormatException(
  'Cannot parse "$source" into `$type` for option "$argName".',
);
''';
  }

  var buffer = StringBuffer()
    ..write(
      '''
${element.name} $resultParserName(ArgResults result) =>''',
    );

  String deserializeForField(
    String fieldName, {
    ParameterElement? ctorParam,
  }) =>
      _deserializeForField(fields[fieldName]!, ctorParam, fields);

  final usedFields = writeConstructorInvocation(
    buffer,
    element,
    fields.keys,
    fields.values.where((fe) => !fe.isFinal).map((fe) => fe.name),
    {},
    deserializeForField,
  );

  final unusedFields = fields.keys.toSet()..removeAll(usedFields);

  if (unusedFields.isNotEmpty) {
    final fieldsString = unusedFields.map((f) => '`$f`').join(', ');
    log.warning('Skipping unassignable fields on `$element`: $fieldsString');

    unusedFields.forEach(fields.remove);
  }
  yield buffer.toString();

  final provideOverrides =
      fields.map((k, v) => MapEntry(k, ArgInfo.fromField(v)))
        ..removeWhere(
          (k, v) => !(v.optionData?.provideDefaultToOverride ?? false),
        );

  final fyis = <String>[];

  /// If an override has a converter, then we don't support passing the
  /// override in via the source type. You have to pass it in as a [String].
  String typeForOverride(String fieldName, ArgInfo info) {
    assert(info.optionData!.provideDefaultToOverride);
    String typeInfo;
    if (converterDataFromOptions(info.optionData!) == null) {
      typeInfo = info.dartType.getDisplayString(withNullability: false);
    } else {
      fyis.add(
        'The value for [${_overrideParamName(fieldName)}] must be a '
        '[String] that is convertible to '
        '[${info.dartType.getDisplayString(withNullability: false)}].',
      );
      typeInfo = 'String';
    }

    typeInfo = '$typeInfo?';

    return '$typeInfo ${_overrideParamName(fieldName)},';
  }

  var overrideArgs = '';
  if (provideOverrides.isNotEmpty) {
    overrideArgs = provideOverrides.entries
        .map((e) => typeForOverride(e.key, e.value))
        .join('\n');
    overrideArgs = ',{$overrideArgs}';
  }

  buffer = StringBuffer()
    ..writeAll(fyis.map((e) => '/// $e\n'))
    ..write(
      'ArgParser $populateParserName(ArgParser parser$overrideArgs) => '
      'parser',
    );
  for (var f in fields.values) {
    if (f.type.isEnum) {
      yield enumValueMapFromType(f.type)!;
    }

    _parserOptionFor(buffer, f);
  }
  buffer.write(';');
  yield buffer.toString();

  yield 'final $parserFieldName = $populateParserName(ArgParser());';

  yield '''
${element.name} parse${element.name}(List<String> args) {
final result = $parserFieldName.parse(args);
return $resultParserName(result);
}
''';

  final createCommand = annotation.read('createCommand').boolValue;
  if (createCommand) {
    yield '''
abstract class _\$${element.name}Command<T> extends Command<T> {
_\$${element.name}Command() {
  $populateParserName(argParser);
}

late final _options = $resultParserName(argResults!);
}
    ''';
  }
}