generateForAnnotatedElement method
Stream<String>
generateForAnnotatedElement(
- Element element,
- ConstantReader annotation,
- 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!);
}
''';
}
}