Skip to content

Commit ccadba7

Browse files
authored
Add functional widget macro example (#3176)
This adds a new macro `@FunctionalWidget(<identifier>)`. It generates a widget with the given name (from the identifier) from a function. Fields are inferred from the parameters, for example: ```dart @FunctionalWidget(MyApp) Widget _buildApp(BuildContext context, ValueNotifier<int> counter, {String? appTitle, String? homePageTitle}) { return MaterialApp( title: appTitle ?? 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue), home: MyHomePage(counter, title: homePageTitle ?? 'Flutter Demo Home Page')); } ``` Would generate this widget class: ```dart class MyApp extends StatelessWidget { final ValueNotifier<int> counter; final String? appTitle; final String? homePageTitle; const MyApp(this.counter, {this.appTitle, this.homePageTitle, Key? key}) : super(key: key); @OverRide Widget build(BuildContext context) => _buildApp(context, counter, appTitle: appTitle, homePageTitle: homePageTitle); } ``` I also applied some fixes to other macros to update them to the latest apis.
1 parent d6b50f1 commit ccadba7

File tree

10 files changed

+231
-10
lines changed

10 files changed

+231
-10
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
include: ../../../analysis_options.yaml
2+
3+
analyzer:
4+
enable-experiment:
5+
- macros
6+
exclude:
7+
- bin/**

working/macros/example/benchmark/simple.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import 'package:_fe_analyzer_shared/src/macros/executor/multi_executor.dart'
2929
as multiExecutor;
3030

3131
import 'src/data_class.dart' as data_class;
32+
import 'src/functional_widget.dart' as functional_widget;
3233
import 'src/injectable.dart' as injectable;
3334

3435
final _watch = Stopwatch()..start();
@@ -50,7 +51,8 @@ final argParser = ArgParser()
5051
defaultsTo: 'stdio',
5152
help: 'The communication channel to use when running as a separate'
5253
' process.')
53-
..addOption('macro', allowed: ['DataClass', 'Injectable'], mandatory: true)
54+
..addOption('macro',
55+
allowed: ['DataClass', 'Injectable', 'FunctionalWidget'], mandatory: true)
5456
..addFlag('help', negatable: false, hide: true);
5557

5658
// Run this script to print out the generated augmentation library for an example class.
@@ -102,6 +104,7 @@ Macro: $macro
102104
var macroFile = switch (macro) {
103105
'DataClass' => File('lib/data_class.dart'),
104106
'Injectable' => File('lib/injectable.dart'),
107+
'FunctionalWidget' => File('lib/functional_widget.dart'),
105108
_ => throw UnsupportedError('Unrecognized macro $macro'),
106109
};
107110
if (!macroFile.existsSync()) {
@@ -113,6 +116,8 @@ Macro: $macro
113116
var macroUri = switch (macro) {
114117
'DataClass' => Uri.parse('package:macro_proposal/data_class.dart'),
115118
'Injectable' => Uri.parse('package:macro_proposal/injectable.dart'),
119+
'FunctionalWidget' =>
120+
Uri.parse('package:macro_proposal/functional_widget.dart'),
116121
_ => throw UnsupportedError('Unrecognized macro $macro'),
117122
};
118123
var macroConstructors = switch (macro) {
@@ -124,6 +129,9 @@ Macro: $macro
124129
'Provides': [''],
125130
'Component': [''],
126131
},
132+
'FunctionalWidget' => {
133+
'FunctionalWidget': [''],
134+
},
127135
_ => throw UnsupportedError('Unrecognized macro $macro'),
128136
};
129137

@@ -165,6 +173,7 @@ Macro: $macro
165173
await switch (macro) {
166174
'DataClass' => data_class.runBenchmarks(executor, macroUri),
167175
'Injectable' => injectable.runBenchmarks(executor, macroUri),
176+
'FunctionalWidget' => functional_widget.runBenchmarks(executor, macroUri),
168177
_ => throw UnsupportedError('Unrecognized macro $macro'),
169178
};
170179
await executor.close();
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import 'package:_fe_analyzer_shared/src/macros/api.dart';
2+
import 'package:_fe_analyzer_shared/src/macros/executor.dart';
3+
import 'package:_fe_analyzer_shared/src/macros/executor/introspection_impls.dart';
4+
import 'package:_fe_analyzer_shared/src/macros/executor/remote_instance.dart';
5+
import 'package:benchmark_harness/benchmark_harness.dart';
6+
7+
import 'shared.dart';
8+
9+
Future<void> runBenchmarks(MacroExecutor executor, Uri macroUri) async {
10+
final identifierResolver = SimpleIdentifierResolver({
11+
Uri.parse('dart:core'): {
12+
'int': intIdentifier,
13+
'String': stringIdentifier,
14+
},
15+
Uri.parse('package:flutter/flutter.dart'): {
16+
'BuildContext': buildContextIdentifier,
17+
'Widget': widgetIdentifier,
18+
}
19+
});
20+
final identifierDeclarations = <Identifier, Declaration>{};
21+
final instantiateBenchmark =
22+
FunctionalWidgetInstantiateBenchmark(executor, macroUri);
23+
await instantiateBenchmark.report();
24+
final instanceId = instantiateBenchmark.instanceIdentifier;
25+
final typesBenchmark = FunctionalWidgetTypesPhaseBenchmark(
26+
executor, macroUri, identifierResolver, instanceId);
27+
await typesBenchmark.report();
28+
BuildAugmentationLibraryBenchmark.reportAndPrint(
29+
executor,
30+
[if (typesBenchmark.result != null) typesBenchmark.result!],
31+
identifierDeclarations);
32+
}
33+
34+
class FunctionalWidgetInstantiateBenchmark extends AsyncBenchmarkBase {
35+
final MacroExecutor executor;
36+
final Uri macroUri;
37+
late MacroInstanceIdentifier instanceIdentifier;
38+
39+
FunctionalWidgetInstantiateBenchmark(this.executor, this.macroUri)
40+
: super('FunctionalWidgetInstantiate');
41+
42+
Future<void> run() async {
43+
instanceIdentifier = await executor.instantiateMacro(
44+
macroUri, 'FunctionalWidget', '', Arguments([], {}));
45+
}
46+
}
47+
48+
class FunctionalWidgetTypesPhaseBenchmark extends AsyncBenchmarkBase {
49+
final MacroExecutor executor;
50+
final Uri macroUri;
51+
final IdentifierResolver identifierResolver;
52+
final MacroInstanceIdentifier instanceIdentifier;
53+
MacroExecutionResult? result;
54+
55+
FunctionalWidgetTypesPhaseBenchmark(this.executor, this.macroUri,
56+
this.identifierResolver, this.instanceIdentifier)
57+
: super('FunctionalWidgetTypesPhase');
58+
59+
Future<void> run() async {
60+
if (instanceIdentifier.shouldExecute(
61+
DeclarationKind.function, Phase.types)) {
62+
result = await executor.executeTypesPhase(
63+
instanceIdentifier, myFunction, identifierResolver);
64+
}
65+
}
66+
}
67+
68+
final buildContextIdentifier =
69+
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'BuildContext');
70+
final buildContextType = NamedTypeAnnotationImpl(
71+
id: RemoteInstance.uniqueId,
72+
isNullable: false,
73+
identifier: buildContextIdentifier,
74+
typeArguments: []);
75+
final widgetIdentifier =
76+
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Widget');
77+
final widgetType = NamedTypeAnnotationImpl(
78+
id: RemoteInstance.uniqueId,
79+
isNullable: false,
80+
identifier: widgetIdentifier,
81+
typeArguments: []);
82+
final myFunction = FunctionDeclarationImpl(
83+
id: RemoteInstance.uniqueId,
84+
identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: '_myWidget'),
85+
library: fooLibrary,
86+
isAbstract: false,
87+
isExternal: false,
88+
isGetter: false,
89+
isOperator: false,
90+
isSetter: false,
91+
namedParameters: [
92+
ParameterDeclarationImpl(
93+
id: RemoteInstance.uniqueId,
94+
identifier:
95+
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'title'),
96+
isNamed: true,
97+
isRequired: true,
98+
library: fooLibrary,
99+
type: stringType),
100+
],
101+
positionalParameters: [
102+
ParameterDeclarationImpl(
103+
id: RemoteInstance.uniqueId,
104+
identifier:
105+
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'context'),
106+
isNamed: false,
107+
isRequired: true,
108+
library: fooLibrary,
109+
type: buildContextType),
110+
ParameterDeclarationImpl(
111+
id: RemoteInstance.uniqueId,
112+
identifier:
113+
IdentifierImpl(id: RemoteInstance.uniqueId, name: 'count'),
114+
isNamed: false,
115+
isRequired: true,
116+
library: fooLibrary,
117+
type: intType),
118+
],
119+
returnType: widgetType,
120+
typeParameters: []);

working/macros/example/bin/run.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ void main(List<String> args) async {
2020
File(dartToolDir.uri.resolve('bootstrap.dart').toFilePath());
2121
log('Bootstrapping macro program (${bootstrapFile.path}).');
2222
var dataClassUri = Uri.parse('package:macro_proposal/data_class.dart');
23+
var functionalWidgetUri =
24+
Uri.parse('package:macro_proposal/functional_widget.dart');
2325
var observableUri = Uri.parse('package:macro_proposal/observable.dart');
2426
var autoDisposableUri = Uri.parse('package:macro_proposal/auto_dispose.dart');
2527
var jsonSerializableUri =
@@ -33,6 +35,9 @@ void main(List<String> args) async {
3335
'HashCode': [''],
3436
'ToString': [''],
3537
},
38+
functionalWidgetUri.toString(): {
39+
'FunctionalWidget': [''],
40+
},
3641
observableUri.toString(): {
3742
'Observable': [''],
3843
},
@@ -72,6 +77,7 @@ void main(List<String> args) async {
7277
'--source=${bootstrapFile.path}',
7378
'--source=lib/auto_dispose.dart',
7479
'--source=lib/data_class.dart',
80+
'--source=lib/functional_widget.dart',
7581
'--source=lib/injectable.dart',
7682
'--source=lib/json_serializable.dart',
7783
'--source=lib/observable.dart',

working/macros/example/lib/auto_dispose.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

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

88
// Interface for disposable things.
@@ -43,7 +43,7 @@ macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro
4343
for (var field in fields) {
4444
var type = await builder.resolve(field.type.code);
4545
if (!await type.isSubtypeOf(disposableType)) continue;
46-
disposeCalls.add(Code.fromParts([
46+
disposeCalls.add(RawCode.fromParts([
4747
'\n',
4848
field.identifier,
4949
if (field.type.isNullable) '?',

working/macros/example/lib/data_class.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

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

88
macro class DataClass
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// There is no public API exposed yet, the in-progress API lives here.
6+
import 'package:_fe_analyzer_shared/src/macros/api.dart';
7+
8+
macro class FunctionalWidget implements FunctionTypesMacro {
9+
final Identifier? widgetIdentifier;
10+
11+
const FunctionalWidget(
12+
{
13+
// Defaults to removing the leading `_` from the function name and calling
14+
// `toUpperCase` on the next character.
15+
this.widgetIdentifier});
16+
17+
@override
18+
void buildTypesForFunction(
19+
FunctionDeclaration function, TypeBuilder builder) {
20+
if (!function.identifier.name.startsWith('_')) {
21+
throw ArgumentError(
22+
'FunctionalWidget should only be used on private declarations');
23+
}
24+
if (function.positionalParameters.isEmpty ||
25+
// TODO: A proper type check here.
26+
(function.positionalParameters.first.type as NamedTypeAnnotation)
27+
.identifier
28+
.name !=
29+
'BuildContext') {
30+
throw ArgumentError(
31+
'FunctionalWidget functions must have a BuildContext argument as the '
32+
'first positional argument');
33+
}
34+
35+
var widgetName = widgetIdentifier?.name ??
36+
function.identifier.name
37+
.replaceRange(0, 2, function.identifier.name[1].toUpperCase());
38+
var positionalFieldParams = function.positionalParameters.skip(1);
39+
builder.declareType(
40+
widgetName,
41+
DeclarationCode.fromParts([
42+
'class $widgetName extends StatelessWidget {',
43+
// Fields
44+
for (var param
45+
in positionalFieldParams.followedBy(function.namedParameters))
46+
DeclarationCode.fromParts([
47+
'final ',
48+
param.type.code,
49+
' ',
50+
param.identifier.name,
51+
';',
52+
]),
53+
// Constructor
54+
'const $widgetName(',
55+
for (var param in positionalFieldParams)
56+
'this.${param.identifier.name}, ',
57+
'{',
58+
for (var param in function.namedParameters)
59+
'${param.isRequired ? 'required ' : ''}this.${param.identifier.name}, ',
60+
'Key? key,',
61+
'}',
62+
') : super(key: key);',
63+
// Build method
64+
'''
65+
@override
66+
Widget build(BuildContext context) => ''',
67+
function.identifier,
68+
'(context, ',
69+
for (var param in positionalFieldParams) '${param.identifier.name}, ',
70+
for (var param in function.namedParameters)
71+
'${param.identifier.name}: ${param.identifier.name}, ',
72+
');',
73+
'}',
74+
]));
75+
}
76+
}

working/macros/example/lib/injectable.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'dart:async';
66

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

1010
import 'util.dart';
@@ -113,6 +113,7 @@ macro class Provides implements MethodDeclarationsMacro {
113113
@override
114114
FutureOr<void> buildDeclarationsForMethod(
115115
MethodDeclaration method, MemberDeclarationBuilder builder) async {
116+
// ignore: deprecated_member_use
116117
final providerIdentifier = await builder.resolveIdentifier(
117118
Uri.parse('package:macro_proposal/injectable.dart'), 'Provider');
118119
if (method.namedParameters.isNotEmpty) {
@@ -194,6 +195,7 @@ macro class Component implements ClassDeclarationsMacro, ClassDefinitionMacro {
194195
@override
195196
FutureOr<void> buildDeclarationsForClass(IntrospectableClassDeclaration clazz,
196197
MemberDeclarationBuilder builder) async {
198+
// ignore: deprecated_member_use
197199
final providerIdentifier = await builder.resolveIdentifier(
198200
Uri.parse('package:macro_proposal/injectable.dart'), 'Provider');
199201
final methods = await builder.methodsOf(clazz);
@@ -243,6 +245,7 @@ macro class Component implements ClassDeclarationsMacro, ClassDefinitionMacro {
243245
@override
244246
FutureOr<void> buildDefinitionForClass(IntrospectableClassDeclaration clazz,
245247
TypeDefinitionBuilder builder) async {
248+
// ignore: deprecated_member_use
246249
final providerIdentifier = await builder.resolveIdentifier(
247250
Uri.parse('package:macro_proposal/injectable.dart'), 'Provider');
248251
final methods = await builder.methodsOf(clazz);

working/macros/example/lib/json_serializable.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//
55
// ignore_for_file: deprecated_member_use
66

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

1010
final dartCore = Uri.parse('dart:core');
@@ -56,7 +56,7 @@ macro class JsonSerializable
5656
var jsonParam = fromJson.positionalParameters.single.identifier;
5757
fromJsonBuilder.augment(initializers: [
5858
for (var field in fields)
59-
Code.fromParts([
59+
RawCode.fromParts([
6060
field.identifier,
6161
' = ',
6262
await _convertField(field, jsonParam, builder),
@@ -94,14 +94,14 @@ macro class JsonSerializable
9494
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
9595
?.identifier;
9696
if (fieldTypeFromJson != null) {
97-
return Code.fromParts([
97+
return RawCode.fromParts([
9898
fieldTypeFromJson,
9999
'(',
100100
jsonParam,
101101
'["${field.identifier.name}"])',
102102
]);
103103
} else {
104-
return Code.fromParts([
104+
return RawCode.fromParts([
105105
jsonParam,
106106
// TODO: support nested serializable types.
107107
'["${field.identifier.name}"] as ',

working/macros/example/lib/observable.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// There is no public API exposed yet, the in progress api lives here.
5+
// There is no public API exposed yet, the in-progress API lives here.
66
import 'dart:async';
77

88
import 'package:_fe_analyzer_shared/src/macros/api.dart';

0 commit comments

Comments
 (0)