generateForAnnotatedElement method

  1. @override
Stream<String> generateForAnnotatedElement(
  1. Element factoryElement,
  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 factoryElement,
  ConstantReader annotation,
  BuildStep buildStep,
) async* {
  if (factoryElement is ClassElement) {
    final target = annotation.read('target').typeValue;
    final targetElement = target.element;
    if (targetElement is! ClassElement) {
      throw StateError('Factory target should be valid class element');
    }

    final hasAnnotatedConstructor = targetElement.constructors.any(
      _constructorChecker.hasAnnotationOf,
    );
    final targetConstructor = targetElement.constructors.firstWhere(
      (element) {
        if (hasAnnotatedConstructor) {
          return _constructorChecker.hasAnnotationOf(element);
        }
        return !element.isSynthetic &&
            !element.isPrivate &&
            element.isAccessibleIn(factoryElement.library);
      },
    );
    final targetParameters = targetConstructor.parameters;
    final parametersDocumentation = Map.fromEntries(
      await Future.wait(
        targetParameters.map(
          (e) async => MapEntry(
            e.name,
            await getDocumentation(e, buildStep),
          ),
        ),
      ),
    );
    final hasValueProvider = checkValueProviderAssigned(factoryElement);
    for (final targetParameter in targetParameters) {
      final element = targetParameter.type.element;
      if (element?.isAccessibleIn(factoryElement.library) != true) {
        logWarning(
          '${targetParameter.type.getDisplayString(withNullability: false)} needed by ${targetConstructor.displayName}\n'
          'but not accessible in ${factoryElement.library.displayName} file.',
        );
      }
    }

    final defaultValues = Map.fromEntries(
      targetParameters.map(
        (targetParameter) => MapEntry(
          targetParameter.name,
          getDefaultValueCode(targetParameter),
        ),
      ),
    );

    bool checkDefaultValue(ParameterElement element) =>
        defaultValues[element.name] != null;

    final factoryImplementation = Class((it) {
      it.name = '_\$${factoryElement.name}';
      it.types.addAll(
          factoryElement.typeParameters.map((type) => Reference('$type')));
      it.extend = Reference('ObjectFactory<${targetElement.thisType}>');
      it.abstract = true;
      it.fields.addAll([
        Field(
          (it) => it
            ..name = 'isRoot'
            ..type = Reference('bool')
            ..modifier = FieldModifier.final$,
        ),
        Field(
          (it) => it
            ..name = 'context'
            ..type = Reference('FactoryContext')
            ..modifier = FieldModifier.final$,
        ),
        Field(
          (it) => it
            ..name = 'key'
            ..type = Reference('ContextKey')
            ..modifier = FieldModifier.final$,
        ),
      ]);

      it.constructors.add(Constructor((it) {
        it.optionalParameters.addAll([
          Parameter((it) => it
            ..name = 'context'
            ..type = Reference('FactoryContext?')),
          Parameter(
            (it) => it
              ..name = 'key'
              ..defaultTo = Code('defaultKey')
              ..toThis = true,
          ),
        ]);
        it.initializers.addAll([
          Code('isRoot = context == null'),
          Code('context = context ?? FactoryContext()'),
        ]);
      }));

      it.methods.addAll(targetParameters.map((targetParameter) {
        return Method((it) {
          final docs = parametersDocumentation[targetParameter.name];
          if (docs?.isNotEmpty == true) {
            it.docs.add(docs!);
          }
          it.name = 'get${targetParameter.name.capitalize()}';
          it.returns = Reference('${targetParameter.type}');
          it.requiredParameters.addAll([
            Parameter((it) {
              it.type = Reference('FactoryContext');
              it.name = 'context';
            }),
            Parameter((it) {
              it.type = Reference('ContextKey');
              it.name = 'key';
            })
          ]);

          final hasDefaultValueCode =
              defaultValues[targetParameter.name] != null;
          final isOptional = checkOptionalRecursive(targetParameter);
          final providerAssignment =
              getProviderCallCode(targetParameter.type);

          if (hasDefaultValueCode) {
            it.body = Code('return ${defaultValues[targetParameter.name]};');
          } else if (targetParameter.hasDefaultValue) {
            it.body = Code('return ${targetParameter.defaultValueCode};');
          } else if (hasValueProvider && providerAssignment != null) {
            it.body = Code(providerAssignment);
          } else if (isOptional) {
            it.body = Code('return null;');
          }
        });
      }));

      final valueBuilders = targetParameters.map(
        (e) => Parameter((it) {
          it.name = e.name;
          it.named = true;
          it.type = Reference('ValueBuilder<${e.type}>?');
        }),
      );

      it.methods.add(Method((it) {
        it.name = 'create';
        it.returns = Reference('${targetElement.thisType}');
        it.optionalParameters.addAll(valueBuilders);

        final fieldsSetters = targetParameters.map(
          (e) => '_\$objectBuilder.${e.name} = '
              '(${e.name} ?? get${e.name.capitalize()})(context, key + \'${e.name}\');',
        );

        final builderTypeParameters = targetElement.thisType.typeArguments;
        final builderTypeParametersString = builderTypeParameters.isNotEmpty
            ? '<${builderTypeParameters.join(',')}>'
            : '';

        it.body = Code('''
          final _\$objectBuilder = _${targetElement.thisType.plainName}Builder$builderTypeParametersString();
          this.context.add(_\$objectBuilder.toReadOnly(), this.key);

          ${fieldsSetters.join('\n')}

          {
            final object = _\$objectBuilder.build();
            this.context.clear();
            return object;
          }
        ''');
      }));

      it.methods.add(Method((it) {
        it.name = 'batch';
        it.returns = Reference('List<${targetElement.thisType}>');
        it.requiredParameters.add(Parameter((it) {
          it.name = 'length';
          it.type = Reference('int');
        }));

        it.optionalParameters.addAll(valueBuilders);
        it.body = Code('''
        return List.generate(
          length,
          (index) => create(
            ${targetParameters.map((e) => '${e.name}: ${e.name}').join(',\n')}
          ),
        );
        ''');
      }));
    });

    final readonlyBulder = Class((it) {
      final typeName = targetElement.thisType.plainName;
      final genericTypes = targetElement.thisType.typeArguments;

      it.name = '${typeName}ReadonlyBuilder';
      it.types.addAll(genericTypes.map((type) => Reference('$type')));
      it.extend = Reference(
        'ObjectReadonlyBuilder<${targetElement.thisType}>',
      );

      it.fields.addAll(
        targetParameters.map(
          (e) => Field(
            (it) {
              final hasDefaultValue = checkDefaultValue(e);
              final shouldBeNullable =
                  !hasDefaultValue && !e.type.toString().endsWith('?');
              it
                ..type = Reference(
                  'ValueGetter<${e.type}${shouldBeNullable ? '?' : ''}>',
                )
                ..name = 'get${e.name.capitalize()}'
                ..modifier = FieldModifier.final$;
            },
          ),
        ),
      );

      it.constructors.add(Constructor((it) {
        it.constant = true;
        it.requiredParameters.addAll(
          targetParameters.map(
            (e) => Parameter(
              (it) => it
                ..name = 'get${e.name.capitalize()}'
                ..toThis = true,
            ),
          ),
        );
      }));
    });

    final builder = Class((it) {
      final typeName = targetElement.thisType.plainName;
      final genericTypes = targetElement.thisType.typeArguments;

      it.name = '_${typeName}Builder';
      it.types.addAll(genericTypes.map((type) => Reference('$type')));
      it.extend = Reference('ObjectBuilder<${targetElement.thisType}>');

      it.fields.addAll(
        targetParameters.map(
          (targetParameter) => Field(
            (it) {
              final hasDefaultValue =
                  defaultValues[targetParameter.name] != null;
              final shouldBeNullable = !hasDefaultValue &&
                  !targetParameter.type.toString().endsWith('?');

              it
                ..name = targetParameter.name
                ..type = Reference(
                    '${targetParameter.type}${shouldBeNullable ? '?' : ''}');

              final parameterDefaultAssignment =
                  defaultValues[targetParameter.name];

              if (parameterDefaultAssignment?.isNotEmpty == true) {
                it.assignment = Code(parameterDefaultAssignment!);
              } else if (targetParameter.hasDefaultValue) {
                it.assignment = Code(targetParameter.defaultValueCode!);
              }
            },
          ),
        ),
      );

      it.methods.add(
        Method(
          (it) {
            it
              ..name = 'toReadOnly'
              ..returns = Reference(readonlyBulder.name);

            it.body = Code('''
            return ${readonlyBulder.name}(
              ${targetParameters.map((e) => '() => ${e.name}').join(',\n')}
            );
            ''');
          },
        ),
      );

      it.methods.add(
        Method(
          (it) {
            final parameters = targetParameters.map((e) {
              if (e.isNamed) {
                return '${e.name}: ${e.name}';
              }
              return '${e.name}';
            });
            final constructorHasName =
                targetConstructor.name.isNotEmpty == true;
            final constructor =
                '${targetElement.thisType}${constructorHasName ? '.' + targetConstructor.name : ''}';
            it
              ..name = 'build'
              ..returns = Reference('${targetElement.thisType}')
              ..body = Code('''
          try {
            ${targetParameters.map((targetParameter) {
                final isOptional = checkOptionalRecursive(targetParameter);
                return 'final ${targetParameter.name} = this.${targetParameter.name}${!isOptional ? '!' : ''};';
              }).join('\n')}

            return $constructor(
              ${parameters.join(',\n')}
            );
          } on Object {
            throw InvalidBuilderStateException();
          }
          ''');
          },
        ),
      );
    });

    final library = Library(
      (b) => b.body.addAll(
        [
          factoryImplementation,
          readonlyBulder,
          builder,
        ],
      ),
    );
    final code = '${library.accept(DartEmitter.scoped())}';
    yield _dartfmt.format(code);
  }
}