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 InheritedWidget macro example and fix up FunctionalWidget #3256

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion working/macros/example/benchmark/src/functional_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Future<void> runBenchmarks(MacroExecutor executor, Uri macroUri) async {
'int': intIdentifier,
'String': stringIdentifier,
},
Uri.parse('package:flutter/flutter.dart'): {
Uri.parse('package:flutter/widgets.dart'): {
'BuildContext': buildContextIdentifier,
'StatelessWidget': statelessWidgetIdentifier,
'Widget': widgetIdentifier,
}
});
Expand Down Expand Up @@ -67,6 +68,8 @@ class FunctionalWidgetTypesPhaseBenchmark extends AsyncBenchmarkBase {

final buildContextIdentifier =
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'BuildContext');
final statelessWidgetIdentifier =
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'StatelessWidget');
final buildContextType = NamedTypeAnnotationImpl(
id: RemoteInstance.uniqueId,
isNullable: false,
Expand Down
36 changes: 27 additions & 9 deletions working/macros/example/lib/functional_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
// There is no public API exposed yet, the in-progress API lives here.
import 'package:_fe_analyzer_shared/src/macros/api.dart';

/// A macro that annotates a function, which becomes the build method for a
/// generated stateless widget.
///
/// The function must have at least one positional parameter, which is of type
/// BuildContext (and this must be the first parameter).
///
/// Any additional function parameters are turned into fields on the stateless
/// widget.
macro class FunctionalWidget implements FunctionTypesMacro {
final Identifier? widgetIdentifier;

Expand All @@ -15,8 +23,8 @@ macro class FunctionalWidget implements FunctionTypesMacro {
this.widgetIdentifier});

@override
void buildTypesForFunction(
FunctionDeclaration function, TypeBuilder builder) {
Future<void> buildTypesForFunction(
FunctionDeclaration function, TypeBuilder builder) async {
if (!function.identifier.name.startsWith('_')) {
throw ArgumentError(
'FunctionalWidget should only be used on private declarations');
Expand All @@ -36,10 +44,19 @@ macro class FunctionalWidget implements FunctionTypesMacro {
function.identifier.name
.replaceRange(0, 2, function.identifier.name[1].toUpperCase());
var positionalFieldParams = function.positionalParameters.skip(1);
// ignore: deprecated_member_use
var statelessWidget = await builder.resolveIdentifier(
Uri.parse('package:flutter/widgets.dart'), 'StatelessWidget');
// ignore: deprecated_member_use
var buildContext = await builder.resolveIdentifier(
Uri.parse('package:flutter/widgets.dart'), 'BuildContext');
// ignore: deprecated_member_use
var widget = await builder.resolveIdentifier(
Uri.parse('package:flutter/widgets.dart'), 'Widget');
builder.declareType(
widgetName,
DeclarationCode.fromParts([
'class $widgetName extends StatelessWidget {',
'class $widgetName extends ', statelessWidget, ' {',
// Fields
for (var param
in positionalFieldParams.followedBy(function.namedParameters))
Expand All @@ -57,13 +74,14 @@ macro class FunctionalWidget implements FunctionTypesMacro {
'{',
for (var param in function.namedParameters)
'${param.isRequired ? 'required ' : ''}this.${param.identifier.name}, ',
'Key? key,',
'}',
') : super(key: key);',
'super.key,',
'});',
// Build method
'''
@override
Widget build(BuildContext context) => ''',
'@override ',
widget,
' build(',
buildContext,
' context) => ',
function.identifier,
'(context, ',
for (var param in positionalFieldParams) '${param.identifier.name}, ',
Expand Down
97 changes: 97 additions & 0 deletions working/macros/example/lib/inherited_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// There is no public API exposed yet, the in-progress API lives here.
import 'package:_fe_analyzer_shared/src/macros/api.dart';

/// A macro that annotates a class and turns it into an inherited widget.
///
/// This will fill in any "holes" that do not have custom implementations,
/// specifically the following items will be added if they don't exist:
///
/// - Make the class extend `InheritedWidget`.
/// - Add a constructor that will initialize any fields that are defined, and
/// take `key` and `child` parameters which it forwards to the super
/// constructor.
/// - Add static `of` and `maybeOf` methods which take a build context and
/// return an instance of this class using `dependOnIheritedWidgetOfExactType`.
/// - Add an `updateShouldNotify` method which does checks for equality of all
/// fields.
macro class InheritedWidget implements ClassTypesMacro, ClassDeclarationsMacro {
const InheritedWidget();

@override
void buildTypesForClass(
ClassDeclaration clazz, ClassTypeBuilder builder) {
if (clazz.superclass != null) return;
// TODO: Add `extends InheritedWidget` once we have an API for that.
}

@override
Future<void> buildDeclarationsForClass(IntrospectableClassDeclaration clazz, MemberDeclarationBuilder builder) async {
final fields = await builder.fieldsOf(clazz);
final methods = await builder.methodsOf(clazz);
if (!methods.any((method) => method is ConstructorDeclaration)) {
builder.declareInType(DeclarationCode.fromParts([
'const ${clazz.identifier.name}(',
'{',
for (var field in fields)...[
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Looks like this hasn't been run through dart format?

(I noticed the lack of space before .... This is what working on the formatter has done to me. :) )

...Oh, I just realized you can't because of the macro keywords. :(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah because dart format doesn't allow the macro keyword so its a pain to use it lol.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fwiw, we should have a larger discussion, but the current way the formatter works with experiments is causing other problems with deprecating old experiments. We should re-evaluate the whole thing and come up with a way to explicitly enable any experiments imo.

field.type.isNullable ? '' : 'required ',
field.identifier,
',',
],
'super.key,',
'required super.child,',
'});',
]));
}

final buildContext =
// ignore: deprecated_member_use
await builder.resolveIdentifier(Uri.parse('package:flutter/widgets.dart'), 'BuildContext');
if (!methods.any((method) => method.identifier.name == "maybeOf")) {
builder.declareInType(DeclarationCode.fromParts([
'static ',
clazz.identifier,
'? maybeOf(',
buildContext,
' context) => context.dependOnInheritedWidgetOfExactType<',
clazz.identifier,
'>();',
]));
}

if (!methods.any((method) => method.identifier.name == "of")) {
builder.declareInType(DeclarationCode.fromParts([
'static ',
clazz.identifier,
' of(',
buildContext,
''' context) {
final result = this.maybeOf(context);
assert(result != null, 'No ${clazz.identifier.name} found in context');
return result!;
}''',
]));
}

if (!methods.any((method) => method.identifier.name == 'updateShouldNotify')) {
// ignore: deprecated_member_use
final override = await builder.resolveIdentifier(
Uri.parse('package:meta/meta.dart'), 'override');
builder.declareInType(DeclarationCode.fromParts([
'@',
override,
' bool updateShouldNotify(',
clazz.identifier,
' oldWidget) =>',
...[
for (var field in fields)
'oldWidget.${field.identifier.name} != this.${field.identifier.name}',
].joinAsCode(' || '),
';',
]));
}
}
}