Skip to content

Commit 529077c

Browse files
authored
add basic checks extension macro (#3315)
This macro generates extensions for a list of types, for use with package:checks. It isn't yet runnable as I need to do a bit of upstream work, but should be a good start. I also updated the spec for extensions, allowing introspection on the on type as well as all generic type parameters.
1 parent feb15f5 commit 529077c

File tree

5 files changed

+193
-9
lines changed

5 files changed

+193
-9
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@ChecksExtensions([Person])
2+
library;
3+
4+
import 'package:checks/checks.dart';
5+
import 'package:test/test.dart';
6+
7+
import 'package:macro_proposal/checks_extensions.dart';
8+
9+
void main() {
10+
test('can use generated extensions', () {
11+
final draco = Person(name: 'Draco', age: 39);
12+
check(draco)
13+
..name.equals('Draco')
14+
..age.equals(39);
15+
});
16+
}
17+
18+
class Person {
19+
final String name;
20+
final int age;
21+
22+
Person({required this.name, required this.age});
23+
}

working/macros/example/bin/run.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ void main(List<String> args) async {
2727
var jsonSerializableUri =
2828
Uri.parse('package:macro_proposal/json_serializable.dart');
2929
var injectableUri = Uri.parse('package:macro_proposal/injectable.dart');
30+
var checksExtensionsUri =
31+
Uri.parse('package:macro_proposal/checks_extensions.dart');
3032
var bootstrapContent = bootstrapMacroIsolate({
3133
dataClassUri.toString(): {
3234
'AutoConstructor': [''],
@@ -51,7 +53,11 @@ void main(List<String> args) async {
5153
'Component': [''],
5254
'Injectable': [''],
5355
'Provides': [''],
54-
}
56+
},
57+
checksExtensionsUri.toString(): {
58+
'ChecksExtensions': [''],
59+
'ChecksExtension': [''],
60+
},
5561
}, SerializationMode.byteData);
5662
bootstrapFile.writeAsStringSync(bootstrapContent);
5763
var bootstrapKernelFile =
@@ -76,6 +82,7 @@ void main(List<String> args) async {
7682
bootstrapKernelFile.path,
7783
'--source=${bootstrapFile.path}',
7884
'--source=lib/auto_dispose.dart',
85+
'--source=lib/checks_extensions.dart',
7986
'--source=lib/data_class.dart',
8087
'--source=lib/functional_widget.dart',
8188
'--source=lib/injectable.dart',
@@ -119,6 +126,8 @@ void main(List<String> args) async {
119126
'$jsonSerializableUri;${bootstrapKernelFile.path}',
120127
'--precompiled-macro',
121128
'$injectableUri;${bootstrapKernelFile.path}',
129+
'--precompiled-macro',
130+
'$checksExtensionsUri;${bootstrapKernelFile.path}',
122131
'--macro-serialization-mode=bytedata',
123132
'--input-linked',
124133
bootstrapKernelFile.path,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
import 'dart:async';
6+
7+
// There is no public API exposed yet, the in-progress API lives here.
8+
import 'package:_fe_analyzer_shared/src/macros/api.dart';
9+
10+
import 'util.dart';
11+
12+
/// Generates extensions for the `checks` package for a list of types.
13+
///
14+
/// The extensions will be "on" the type `Subject<SomeType>`, for each given
15+
/// type.
16+
///
17+
/// Each extension will have a getter for each field in the type it is
18+
/// targetting, of the form `Subject<SomeFieldType>
19+
macro class ChecksExtensions implements LibraryTypesMacro {
20+
final List<NamedTypeAnnotation> types;
21+
22+
const ChecksExtensions(this.types);
23+
24+
@override
25+
Future<void> buildTypesForLibrary(
26+
Library library, TypeBuilder builder) async {
27+
// ignore: deprecated_member_use
28+
final subject = await builder.resolveIdentifier(
29+
Uri.parse('package:checks/checks.dart'), 'Subject');
30+
// ignore: deprecated_member_use
31+
final checksExtension = await builder.resolveIdentifier(
32+
Uri.parse('package:macro_proposal/checks_extensions.dart'),
33+
'ChecksExtension');
34+
for (final type in types) {
35+
if (type.typeArguments.isNotEmpty) {
36+
throw StateError('Cannot generate checks extensions for types with '
37+
'explicit generics');
38+
}
39+
final name = '${type.identifier.name}Checks';
40+
builder.declareType(
41+
name,
42+
DeclarationCode.fromParts([
43+
'@',
44+
checksExtension,
45+
'()',
46+
'extension $name on ',
47+
NamedTypeAnnotationCode(name: subject, typeArguments: [type.code]),
48+
'{}',
49+
]));
50+
}
51+
}
52+
}
53+
54+
/// Adds getters to an extension on a `Subject` type which abstract away the
55+
/// `has` calls for all the fields of the subject.
56+
macro class ChecksExtension implements ExtensionDeclarationsMacro {
57+
const ChecksExtension();
58+
59+
Future<void> buildDeclarationsForExtension(
60+
ExtensionDeclaration extension, MemberDeclarationBuilder builder) async {
61+
// ignore: deprecated_member_use
62+
final subject = await builder.resolveIdentifier(
63+
Uri.parse('package:checks/checks.dart'), 'Subject');
64+
final onType = extension.onType;
65+
if (onType is! NamedTypeAnnotation ||
66+
onType.identifier != subject ||
67+
onType.typeArguments.length != 1) {
68+
throw StateError(
69+
'The `on` type must be a Subject with an explicit type argument.');
70+
}
71+
72+
// Find the real named type declaration for our on type, and ensure its a
73+
// real named type (ie: not a function or record type, etc);
74+
final onTypeDeclaration = await _namedTypeDeclarationOrThrow(
75+
onType.typeArguments.single, builder);
76+
77+
// Ensure that our `on` type is coming from a null safe library, we don't
78+
// support legacy code.
79+
switch (onTypeDeclaration.library.languageVersion) {
80+
case LanguageVersion(:int major) when major < 2:
81+
case LanguageVersion(major: 2, :int minor) when minor < 12:
82+
throw InvalidCheckExtensions('must be imported in a null safe library');
83+
}
84+
85+
// Generate the getters
86+
final fields =
87+
await builder.fieldsOf(onTypeDeclaration as IntrospectableType);
88+
for (final field in fields) {
89+
if (_isCheckableField(field))
90+
await _declareHasGetter(field, builder, subject);
91+
}
92+
}
93+
94+
/// Find the named type declaration for [type], or throw if it doesn't refer
95+
/// to a named type.
96+
///
97+
/// Type aliases are followed to their underlying types.
98+
Future<TypeDeclaration> _namedTypeDeclarationOrThrow(
99+
TypeAnnotation type, DeclarationBuilder builder) async {
100+
if (type is! NamedTypeAnnotation) {
101+
throw StateError('Got a non interface type: ${type.code.debugString()}');
102+
}
103+
var onTypeDeclaration = await builder.typeDeclarationOf(type.identifier);
104+
while (onTypeDeclaration is TypeAliasDeclaration) {
105+
final aliasedTypeAnnotation = onTypeDeclaration.aliasedType;
106+
if (aliasedTypeAnnotation is! NamedTypeAnnotation) {
107+
throw StateError(
108+
'Got a non interface type: ${type.code.debugString()}');
109+
}
110+
onTypeDeclaration =
111+
await (builder.typeDeclarationOf(aliasedTypeAnnotation.identifier));
112+
}
113+
return onTypeDeclaration;
114+
}
115+
116+
/// Declares a getter for [field] that is a convenience method for calling
117+
/// `has` and extracting out the field.
118+
Future<void> _declareHasGetter(FieldDeclaration field,
119+
MemberDeclarationBuilder builder, Identifier subject) async {
120+
final name = field.identifier.name;
121+
builder.declareInType(DeclarationCode.fromParts([
122+
NamedTypeAnnotationCode(name: subject, typeArguments: [field.type.code]),
123+
// TODO: Use an identifier for `has`? It exists on `this` so it isn't
124+
// strictly necessary, this should always work.
125+
'get $name => has(',
126+
'(v) => v.$name,',
127+
"'$name'",
128+
');',
129+
]));
130+
}
131+
132+
bool _isCheckableField(FieldDeclaration field) =>
133+
field.identifier.name != 'hashCode' && !field.isStatic;
134+
}
135+
136+
class InvalidCheckExtensions extends Error {
137+
final String message;
138+
InvalidCheckExtensions(this.message);
139+
@override
140+
String toString() => 'Invalid `CheckExtensions` annotation: $message';
141+
}

working/macros/example/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ environment:
55
dev_dependencies:
66
args: ^2.3.0
77
benchmark_harness: ^2.2.1
8+
checks: ^0.2.0
9+
test: ^1.24.0
810
dart_style: ^2.2.1
911
_fe_analyzer_shared: any
1012
frontend_server: any

working/macros/feature-specification.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ There are three phases:
478478
### Phase 1: Types
479479

480480
Here, macros contribute new types to the program&mdash;classes, typedefs, enums,
481-
etc. This is the only phase where a macro can introduce a new visible name into
481+
etc. This is the only phase where a macro can introduce a new visible type into
482482
the top level scope.
483483

484484
**Note**: Macro classes _cannot_ be generated in this way, but they can rely on
@@ -501,13 +501,22 @@ about subtype relations.
501501
### Phase 2: Declarations
502502

503503
In this phase, macros declare functions, variables, and members. "Declaring"
504-
here means specifying the name and type signature, but not the body of a
505-
function or initializer for a variable. In other words, macros in this phase
506-
specify the declarative structure but no imperative code.
507-
508-
When applied to a class, a macro in this phase can introspect on all of the
509-
members of that class and its superclasses, but it cannot introspect on the
510-
members of other types.
504+
here means specifying the name and type signature, but not necessarily the body
505+
of a function or initializer for a variable. It is encouraged to provide a body
506+
(or initializer) if possible, but you can opt to wait until the definition phase
507+
if needed.
508+
509+
When applied to a class, enum, or mixin a macro in this phase can introspect on
510+
all of the members of that class and its superclasses, but it cannot introspect
511+
on the members of other types. For mixins, the `on` type is considered a
512+
superclass and is introspectable. Note that generic type arguments are not
513+
introspectable.
514+
515+
When applied to an extension, a macro in this phase can introspect on all of the
516+
members of the `on` type, as well as its generic type arguments and the bounds
517+
of any generic type parameters for the extension.
518+
519+
TODO: Define the introspection rules for extension types.
511520

512521
### Phase 3: Definitions
513522

0 commit comments

Comments
 (0)