Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generic type arguments in annotations #283

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

jacksonjude
Copy link

@jacksonjude jacksonjude commented Jul 20, 2022

Overview

Adds support for generic annotations, a feature added in Dart SDK 2.14.

Take this class for example:

@reflector
class Person {
  @TypeHelper<Soul>(42, "The meaning")
  String? life;
}

class TypeHelper<T> {
  const TypeHelper(int var1, String var2);
}

class Soul { }

The annotation would have previously been identified as having the type argument dynamic:

ClassMirror personClassMirror = reflector.reflectType(Person) as ClassMirror;
VariableMirror field = personClassMirror.declarations["life"] as VariableMirror;
var annotation = field.metadata[0];
print(annotation.runtimeType); // TypeHelper<dynamic>

It will now be correctly identified as having the type argument Soul:

print(annotation.runtimeType); // TypeHelper<Soul>

This is done by appending the type arguments (if any exist) after the annotation name during the _extractMetadataCode function of builder_implementation.dart.

@google-cla
Copy link

google-cla bot commented Jul 20, 2022

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@eernstg
Copy link
Collaborator

eernstg commented Jul 21, 2022

Hello @jacksonjude, thanks for the PR! This is something reflectable should support. I'm really busy now, but I'll take a look as soon as possible.

@jacksonjude
Copy link
Author

Hey there, sorry if you are still busy, but can you take another look at this?

@eernstg
Copy link
Collaborator

eernstg commented Nov 6, 2022

Sorry about the delay — yep, I'm still busy, but I will take a look at it tomorrow.

Copy link
Collaborator

@eernstg eernstg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the input! We need to make the code a bit more robust. Here is the beginnings of a test that is intended to verify that the new feature is working:

@c
library test_reflectable.test.metadata_test;

import 'package:reflectable/reflectable.dart';
import 'package:test/test.dart';
import 'metadata_test.reflectable.dart';

class MyReflectable extends Reflectable {
  const MyReflectable()
      : super(metadataCapability, instanceInvokeCapability,
            staticInvokeCapability, declarationsCapability, libraryCapability);
}

const myReflectable = MyReflectable();

const c = [
  Bar<num>({'a': 14})
];

class K {
  static const p = 2;
}

@myReflectable
@Bar<Object?>({
  b: deprecated,
  c: Deprecated('tomorrow'),
  1 + 2: (d ? 3 : 4),
  identical(1, 2): 's',
  K.p: 6
})
@Bar<Never>({})
@Bar<void>.namedConstructor({})
@c
class Foo {
  @Bar<bool>({})
  @Bar<Bar<Bar>>({})
  @Bar<MyReflectable>.namedConstructor({})
  @Bar<void Function()>.namedConstructor({})
  @c
  void foo() {}
  var x = 10;
}

class Bar<X> {
  final Map<Object, X> m;
  const Bar(this.m);
  const Bar.namedConstructor(this.m);

  @override
  String toString() => 'Bar<$X>($m)';
}

void main() {
  initializeReflectable();

  test('metadata on class', () {
    expect(myReflectable.reflectType(Foo).metadata, const [
      MyReflectable(),
      Bar<Object?>({
        b: deprecated,
        c: Deprecated('tomorrow'),
        3: 3,
        false: 's',
        2: 6,
      }),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    var fooMirror = myReflectable.reflectType(Foo) as ClassMirror;
    expect(fooMirror.declarations['foo']!.metadata, const [
      Bar<bool>({}),
      Bar<Bar<Bar<dynamic>>>({}),
      Bar<MyReflectable>({}),
      Bar<void Function()>({}),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    // The synthetic accessors do not have metadata.
    expect(fooMirror.instanceMembers['x']!.metadata, []);
    expect(fooMirror.instanceMembers['x=']!.metadata, []);

    // Test metadata on libraries
    expect(myReflectable.reflectType(Foo).owner!.metadata, [c]);
  });
}

The test probably needs to be corrected here and there (but it already shows that the new code gets a null reference error, so it's a useful starting point).

The test should be added to the repository at test_reflectable/test/generic_metadata_test.dart. The lower bound of the SDK constraint in test_reflectable/pubspec.yaml` should be changed to 2.17 (such that the generic-metadata feature is included in the language).

reflectable/lib/src/builder_implementation.dart Outdated Show resolved Hide resolved
reflectable/lib/src/builder_implementation.dart Outdated Show resolved Hide resolved
reflectable/lib/src/builder_implementation.dart Outdated Show resolved Hide resolved
String typeArguments = annotationNode.typeArguments!.arguments
.map((TypeAnnotation typeArgument) {
LibraryElement library = typeArgument.type!.element!.library!;
String prefix = importCollector._getPrefix(library);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the prefix we need to check that the given library is importable (_isImportable). For instance, dart:_internal is not importable by user code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've resolved this, I currently have the type argument default to 'dynamic' if the library can't be imported, though I'm not sure if this is the correct behavior.

.map((TypeAnnotation typeArgument) {
LibraryElement library = typeArgument.type!.element!.library!;
String prefix = importCollector._getPrefix(library);
return '$prefix$typeArgument';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this won't work in the case where the type argument is a parameterized type of the form C<D> where C is imported from prefix1 and D is imported from prefix2: We should get prefix1.C<prefix2.D>, but the code above simply uses toString() on the given TypeAnnotation, and that will yield syntax which is only valid for the location where that type annotation actually occurs in the program (and at this point we are writing some code that should denote the same type, but in a different library).

Check out _typeCodeOfTypeArgument, you might be able to call that function in order to get the right string.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having some trouble calling _typeCodeOfTypeArgument since it is a function of _ReflectorDomain, which I can't seem to access from within _extractMetadataCode. However, it is very possible I may have missed something given that I'm not super familiar with the layout of the library.

@jacksonjude
Copy link
Author

jacksonjude commented Nov 8, 2022

Thanks again for the input! We need to make the code a bit more robust. Here is the beginnings of a test that is intended to verify that the new feature is working:

@c
library test_reflectable.test.metadata_test;

import 'package:reflectable/reflectable.dart';
import 'package:test/test.dart';
import 'metadata_test.reflectable.dart';

class MyReflectable extends Reflectable {
  const MyReflectable()
      : super(metadataCapability, instanceInvokeCapability,
            staticInvokeCapability, declarationsCapability, libraryCapability);
}

const myReflectable = MyReflectable();

const c = [
  Bar<num>({'a': 14})
];

class K {
  static const p = 2;
}

@myReflectable
@Bar<Object?>({
  b: deprecated,
  c: Deprecated('tomorrow'),
  1 + 2: (d ? 3 : 4),
  identical(1, 2): 's',
  K.p: 6
})
@Bar<Never>({})
@Bar<void>.namedConstructor({})
@c
class Foo {
  @Bar<bool>({})
  @Bar<Bar<Bar>>({})
  @Bar<MyReflectable>.namedConstructor({})
  @Bar<void Function()>.namedConstructor({})
  @c
  void foo() {}
  var x = 10;
}

class Bar<X> {
  final Map<Object, X> m;
  const Bar(this.m);
  const Bar.namedConstructor(this.m);

  @override
  String toString() => 'Bar<$X>($m)';
}

void main() {
  initializeReflectable();

  test('metadata on class', () {
    expect(myReflectable.reflectType(Foo).metadata, const [
      MyReflectable(),
      Bar<Object?>({
        b: deprecated,
        c: Deprecated('tomorrow'),
        3: 3,
        false: 's',
        2: 6,
      }),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    var fooMirror = myReflectable.reflectType(Foo) as ClassMirror;
    expect(fooMirror.declarations['foo']!.metadata, const [
      Bar<bool>({}),
      Bar<Bar<Bar<dynamic>>>({}),
      Bar<MyReflectable>({}),
      Bar<void Function()>({}),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    // The synthetic accessors do not have metadata.
    expect(fooMirror.instanceMembers['x']!.metadata, []);
    expect(fooMirror.instanceMembers['x=']!.metadata, []);

    // Test metadata on libraries
    expect(myReflectable.reflectType(Foo).owner!.metadata, [c]);
  });
}

The test probably needs to be corrected here and there (but it already shows that the new code gets a null reference error, so it's a useful starting point).

The test should be added to the repository at test_reflectable/test/generic_metadata_test.dart. The lower bound of the SDK constraint in test_reflectable/pubspec.yaml` should be changed to 2.17 (such that the generic-metadata feature is included in the language).

One more question: the variables b and d don't seem to be defined in the sample test code you have provided. Is this intentional?

@eernstg
Copy link
Collaborator

eernstg commented Dec 7, 2022

(No, I haven't forgotten this. Just busy again. Will try to get to it real soon now. ;-)

@eernstg
Copy link
Collaborator

eernstg commented Jan 30, 2023

(Busy, busy, busy, but I will take a look later today.)

@codekeyz
Copy link

@eernstg

@Sten435
Copy link
Contributor

Sten435 commented Feb 7, 2024

@codekeyz @eernstg Any updates on this ?

It has been a year ?

@jacksonjude
Copy link
Author

Hi again @eernstg, what changes need to be made for this PR to be ready to merge?

@eernstg
Copy link
Collaborator

eernstg commented Aug 22, 2024

Hi @jacksonjude, thanks for contributing! I'm sincerely sorry about not being able to get around to this for so long.

I took another look at it today, and I can see that it generates code where a prefix is missing when this test is executed:

@c
library test_reflectable.test.generic_metadata_test;

import 'package:reflectable/reflectable.dart';
import 'package:test/test.dart';
import 'metadata_test.reflectable.dart';

class MyReflectable extends Reflectable {
  const MyReflectable()
      : super(metadataCapability, instanceInvokeCapability,
            staticInvokeCapability, declarationsCapability, libraryCapability);
}

const myReflectable = MyReflectable();

const b = 0;

const c = [
  Bar<num>({'a': 14})
];

const d = true;

class K {
  static const p = 2;
}

@myReflectable
@Bar<Object?>({
  b: deprecated,
  c: Deprecated('tomorrow'),
  1 + 2: (d ? 3 : 4),
  identical(1, 2): 's',
  K.p: 6
})
@Bar<Never>({})
@Bar<void>.namedConstructor({})
@c
class Foo {
  @Bar<bool>({})
  @Bar<Bar<Bar>>({})
  @Bar<MyReflectable>.namedConstructor({})
  @Bar<void Function()>.namedConstructor({})
  @c
  void foo() {}
  var x = 10;
}

class Bar<X> {
  final Map<Object, X> m;
  const Bar(this.m);
  const Bar.namedConstructor(this.m);

  @override
  String toString() => 'Bar<$X>($m)';
}

void main() {
  initializeReflectable();

  test('metadata on class', () {
    expect(myReflectable.reflectType(Foo).metadata, const [
      MyReflectable(),
      Bar<Object?>({
        b: deprecated,
        c: Deprecated('tomorrow'),
        3: 3,
        false: 's',
        2: 6,
      }),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    var fooMirror = myReflectable.reflectType(Foo) as ClassMirror;
    expect(fooMirror.declarations['foo']!.metadata, const [
      Bar<bool>({}),
      Bar<Bar<Bar<dynamic>>>({}),
      Bar<MyReflectable>({}),
      Bar<void Function()>({}),
      [
        Bar<num>({'a': 14})
      ],
    ]);

    // The synthetic accessors do not have metadata.
    expect(fooMirror.instanceMembers['x']!.metadata, []);
    expect(fooMirror.instanceMembers['x=']!.metadata, []);

    // Test metadata on libraries
    expect(myReflectable.reflectType(Foo).owner!.metadata, [c]);
  });
}

The error from the analyzer looks as follows:

  error • test/generic_metadata_test.reflectable.dart:66:45 • The name 'Bar' isn't a type, so it can't be used as a type argument. Try correcting the name to an existing type, or defining a type named 'Bar'. • non_type_as_type_argument

If I edit the generated code to add the missing prefix then I get 'Null check operator used on a null value'.

I think it shouldn't be hard to fix. It would be great to get it landed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants