dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.25k stars 1.58k forks source link

Macro with field annotations doesn't work properly #56410

Open Sadhorsephile opened 3 months ago

Sadhorsephile commented 3 months ago

Problem

I wrote macro to automatically generate constructors for classes. It uses annotations to determine which fields should be named, positional and required.

Macro code ```dart import 'dart:async'; import 'package:collection/collection.dart'; import 'package:macros/macros.dart'; macro class AutoConstructor implements ClassDeclarationsMacro { const AutoConstructor(); @override FutureOr buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder) async { final fields = await builder.fieldsOf(clazz); final code = [ '\t${clazz.identifier.name}(\n', ]; final positionalParams = []; final namedParams = []; for (final field in fields) { final annotationsOfField = field.metadata; final namedParam = annotationsOfField.firstWhereOrNull( (element) => element is ConstructorMetadataAnnotation && element.type.identifier.name == 'NamedParam', ) as ConstructorMetadataAnnotation?; if (namedParam != null) { final defaultValue = namedParam.namedArguments['defaultValue']; final isRequired = annotationsOfField.any( (element) => element is IdentifierMetadataAnnotation && element.identifier.name == 'requiredField', ); namedParams.addAll( [ '\t\t', if (isRequired && defaultValue == null) ...[ 'required ', ], 'this.${field.identifier.name}', if (defaultValue != null) ...[ ' = ', defaultValue, ], ',\n', ], ); } else { positionalParams.add('\t\tthis.${field.identifier.name},\n'); } } code.addAll([ ...positionalParams, if (namedParams.isNotEmpty) ...['\t\t{\n', ...namedParams, '\t\t}',], '\n\t);', ]); builder.declareInType(DeclarationCode.fromParts(code)); } } ```
Annotations code ```dart class NamedParam { final T? defaultValue; const NamedParam({this.defaultValue}); } const requiredField = Required(); class Required { const Required(); } ```
Example ```dart import 'package:test_macros/1.%20auto_constructor/annotations.dart'; import 'package:test_macros/1.%20auto_constructor/auto_constructor.dart'; @AutoConstructor() class AnotherComplicatedClass { final int a; @requiredField @NamedParam() final String b; @NamedParam(defaultValue: 3.14) final double c; @NamedParam() final bool? d; @requiredField @NamedParam() final bool? e; final List f; } void main() async { final instance = AnotherComplicatedClass( 1, [], b: 'a', c: 2, d: false, e: false, ); } ```
Example (augmentation) ```dart augment library 'package:test_macros/1.%20auto_constructor/example.dart'; augment class AnotherComplicatedClass { AnotherComplicatedClass( this.a, this.f, { required this.b, this.c = 3.14, this.d, required this.e, } ); } ```

There are no errors from the analyzer, but I can't launch this code due to the following error:

lib/1.%20auto_constructor/example.dart:26:36: Error: Too few positional arguments: 6 required, 2 given.
  final instance = AnotherComplicatedClass(
                                   ^
org-dartlang-augmentation:/_/test_macros/lib/1.%20auto_constructor/example.dart-0:4:2: Context: Found this candidate, but the arguments don't match.
        AnotherComplicatedClass(
        ^^^^^^^^^^^^^^^^^^^^^^^

At the same time, this code works if I remove all the field annotations from the class.

Additional info

dart info output:

#### General info

- Dart 3.5.0-180.3.beta (beta) (Wed Jun 5 15:06:15 2024 +0000) on "macos_arm64"
- on macos / Version 14.4.1 (Build 23E224)
- locale is en-RU

#### Project info

- sdk constraint: '^3.5.0-180'
- dependencies: collection, dio, flutter, json, macros, provider
- dev_dependencies: flutter_lints, flutter_test
dart-github-bot commented 3 months ago

Summary: The user's macro generates constructors based on field annotations, but it fails to generate a constructor with the correct number of positional arguments. The macro incorrectly identifies the number of required positional arguments, leading to an error when attempting to instantiate the class.

Sadhorsephile commented 2 months ago

Annotations also don't work in such cases (when used on function parameters):

  external Future<PostEntity> createPost(@Body() String post);