TheSpixxyQ
TheSpixxyQ

Reputation: 1045

Dart source generator: Is there any way to find all object instantiations?

I am trying to make a source generator that would mimic C# anonymous objects, because they are great for when you are manipulating with collections (Select, GroupBy, etc.).

Imagine this code:

class Person {
  final String firstName;
  final String lastName;
  final int age;

  Person(this.firstName, this.age, this.lastName);
}

class TestClass {
  final _data = [
    Person('John', 'Doe', 51),
    Person('Jane', 'Doe', 50),
    Person('John', 'Smith', 40),
  ];

  void testMethod() {
    final map1 = _data.map((p) => _$$1(name: p.firstName, age: p.age));
    final map2 = _data.map((p) => _$$2(fullName: '${p.firstName} ${p.lastName}', age: p.age));
  }
}

Those _$$x objects are what I want to generate now. I need to somehow find them and find what is being passed into them, so my code generator would generate this:

class _$$1 {
  final String name;
  final int age;

  const _$$1({required this.name, required this.age});
}

class _$$2 {
  final String fullName;
  final int age;

  const _$$1({required this.fullName, required this.age});
}

but I cannot seem to even find method content:

FutureOr<String?> generate(LibraryReader library, BuildStep buildStep) {
  for (final clazz in library.classes) {
    final method = clazz.methods.first;
    method.visitChildren(RecursiveElementVisitor<dynamic>());
  }
}

it looks like the MethodElement doesn't have any children? so this doesn't look like the right way.

Is there any other way to find what I need?

Upvotes: 1

Views: 136

Answers (1)

hacker1024
hacker1024

Reputation: 3678

A visitor can be used at the lower-level Abstract Syntax Tree to find the _$$x constructor invocations.

The visitor should also visit the whole library rather than just classes as is done in your example, in order to locate top-level usages as well.

The AST does not distinguish between constructor and method invocations, but we can use a series of checks to make sure that the invocation in question is an appropriate target for code generation nonetheless. In a similar fashion, checks can also be put in place to ensure that the invocation is done in a closure.

The following example implements this approach, and leaves you with a map of '_$$x' to MethodInvocations to work with:

FutureOr<String?> generate(LibraryReader library, BuildStep buildStep) {
  final libraryElement = libraryReader.element;
  final parsedLibraryResult = libraryElement.session
      .getParsedLibraryByElement(libraryElement) as ParsedLibraryResult;
  final libraryCompilationUnit = parsedLibraryResult.units[0].unit;

  final selectorInstantiationLocator = SelectorInstantiationLocator();
  libraryCompilationUnit.visitChildren(selectorInstantiationLocator);
  final selectorInstantiations =
      selectorInstantiationLocator.selectorInstantiations;

  // ...
}

class SelectorInstantiationLocator extends RecursiveAstVisitor<void> {
  final selectorInstantiations = <String, MethodInvocation>{};

  @override
  void visitMethodInvocation(MethodInvocation node) {
    // Ensure that the invocation is an appropriate target for code generation.
    // &= is not used in favour of the short-circuit && operator (https://github.com/dart-lang/language/issues/23).

    // Stop if the invocation doesn't match the required prefix.
    final className = node.methodName.name;
    var isSelectorInstantiation = className.startsWith(r'_$$');
    final classIndex = int.tryParse(className.substring(3));
    isSelectorInstantiation =
        isSelectorInstantiation && (classIndex != null && classIndex >= 0);
    // No target will exist for a constructor invocation.
    isSelectorInstantiation =
        isSelectorInstantiation && node.realTarget == null;
    // The selector instantiation should be done in an expression function body (=>).
    isSelectorInstantiation =
        isSelectorInstantiation && node.parent is ExpressionFunctionBody;
    // The function body should be part of a function expression (rather than a method declaration)
    isSelectorInstantiation =
        isSelectorInstantiation && node.parent!.parent is FunctionExpression;
    // The function expression should be inside an argument list.
    isSelectorInstantiation =
        isSelectorInstantiation && node.parent!.parent!.parent is ArgumentList;

    if (isSelectorInstantiation) selectorInstantiations[className] = node;

    return super.visitMethodInvocation(node);
  }
}

Upvotes: 1

Related Questions