diff --git a/analysis_options.yaml b/analysis_options.yaml index 4c558d34f3..579974b8a8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,6 +2,7 @@ analyzer: exclude: - accepted/2.3/spread-collections/examples/** # TODO: remove these when the analyzer supports macros + - working/macros/example/lib/auto_dispose.dart - working/macros/example/lib/data_class.dart - working/macros/example/lib/observable.dart - working/macros/example/bin/user_main.dart diff --git a/working/macros/example/bin/run.dart b/working/macros/example/bin/run.dart index 0c4fa51904..7ae89c6da2 100644 --- a/working/macros/example/bin/run.dart +++ b/working/macros/example/bin/run.dart @@ -20,13 +20,21 @@ void main() async { log('Bootstrapping macro program (${bootstrapFile.path}).'); var dataClassUri = Uri.parse('package:macro_proposal/data_class.dart'); var observableUri = Uri.parse('package:macro_proposal/observable.dart'); + var autoDisposableUri = Uri.parse('package:macro_proposal/auto_dispose.dart'); var bootstrapContent = bootstrapMacroIsolate({ dataClassUri.toString(): { + 'AutoConstructor': [''], + 'CopyWith': [''], 'DataClass': [''], + 'HashCode': [''], + 'ToString': [''], }, observableUri.toString(): { 'Observable': [''], }, + autoDisposableUri.toString(): { + 'AutoDispose': [''], + }, }, SerializationMode.byteDataClient); bootstrapFile.writeAsStringSync(bootstrapContent); var bootstrapKernelFile = @@ -85,6 +93,8 @@ void main() async { '$dataClassUri;${bootstrapKernelFile.path}', '--precompiled-macro', '$observableUri;${bootstrapKernelFile.path}', + '--precompiled-macro', + '$autoDisposableUri;${bootstrapKernelFile.path}', '--macro-serialization-mode=bytedata', '--input-linked', bootstrapKernelFile.path, diff --git a/working/macros/example/bin/user_main.dart b/working/macros/example/bin/user_main.dart index 8c492e8057..91957ec9fe 100644 --- a/working/macros/example/bin/user_main.dart +++ b/working/macros/example/bin/user_main.dart @@ -4,6 +4,7 @@ import 'dart:math'; +import 'package:macro_proposal/auto_dispose.dart'; import 'package:macro_proposal/data_class.dart'; import 'package:macro_proposal/observable.dart'; @@ -27,6 +28,9 @@ void main() { observableUser ..age = 11 ..name = 'Greg'; + + var state = MyState.gen(a: ADisposable(), b: BDisposable(), c: 'hello world'); + state.dispose(); } @DataClass() @@ -54,3 +58,32 @@ class ObservableUser { }) : _age = age, _name = name; } + +// TODO: remove @AutoConstructor once we can, today it is required. +@AutoConstructor() +class State { + void dispose() { + print('disposing of State $this'); + } +} + +@AutoDispose() +@AutoConstructor() +class MyState extends State { + final ADisposable a; + final ADisposable? a2; + final BDisposable b; + final String c; +} + +class ADisposable implements Disposable { + void dispose() { + print('disposing of ADisposable'); + } +} + +class BDisposable implements Disposable { + void dispose() { + print('disposing of BDisposable'); + } +} diff --git a/working/macros/example/lib/auto_dispose.dart b/working/macros/example/lib/auto_dispose.dart new file mode 100644 index 0000000000..394b40f942 --- /dev/null +++ b/working/macros/example/lib/auto_dispose.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2022, 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'; + +// Interface for disposable things. +abstract class Disposable { + void dispose(); +} + +macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro { + const AutoDispose(); + + @override + void buildDeclarationsForClass( + ClassDeclaration clazz, ClassMemberDeclarationBuilder builder) async { + var methods = await builder.methodsOf(clazz); + if (methods.any((d) => d.identifier.name == 'dispose')) { + // Don't need to add the dispose method, it already exists. + return; + } + + builder.declareInClass(DeclarationCode.fromParts([ + 'external void dispose();', + ])); + } + + @override + Future buildDefinitionForClass( + ClassDeclaration clazz, ClassDefinitionBuilder builder) async { + var disposableIdentifier = + // ignore: deprecated_member_use + await builder.resolveIdentifier( + Uri.parse('package:macro_proposal/auto_dispose.dart'), + 'Disposable'); + var disposableType = await builder + .resolve(NamedTypeAnnotationCode(name: disposableIdentifier)); + + var disposeCalls = []; + var fields = await builder.fieldsOf(clazz); + for (var field in fields) { + var type = await builder.resolve(field.type.code); + if (!await type.isSubtypeOf(disposableType)) continue; + disposeCalls.add(Code.fromParts([ + '\n', + field.identifier, + if (field.type.isNullable) '?', + '.dispose();', + ])); + } + // Augment the dispose method by injecting all the new dispose calls after + // the call to `augment super()`, which should be calling `super.dispose()` + // already. + var disposeMethod = (await builder.methodsOf(clazz)) + .firstWhere((method) => method.identifier.name == 'dispose'); + var disposeBuilder = await builder.buildMethod(disposeMethod.identifier); + disposeBuilder.augment(FunctionBodyCode.fromParts([ + '{\n', + if (disposeMethod.isExternal) 'super.dispose();' else 'augment super();', + ...disposeCalls, + '}', + ])); + } +} diff --git a/working/macros/example/lib/data_class.dart b/working/macros/example/lib/data_class.dart index fdfcd72120..8c89872d17 100644 --- a/working/macros/example/lib/data_class.dart +++ b/working/macros/example/lib/data_class.dart @@ -13,11 +13,11 @@ macro class DataClass Future buildDeclarationsForClass( ClassDeclaration clazz, ClassMemberDeclarationBuilder context) async { await Future.wait([ - autoConstructor.buildDeclarationsForClass(clazz, context), - copyWith.buildDeclarationsForClass(clazz, context), - hashCode.buildDeclarationsForClass(clazz, context), - equality.buildDeclarationsForClass(clazz, context), - toString.buildDeclarationsForClass(clazz, context), + AutoConstructor().buildDeclarationsForClass(clazz, context), + CopyWith().buildDeclarationsForClass(clazz, context), + HashCode().buildDeclarationsForClass(clazz, context), + Equality().buildDeclarationsForClass(clazz, context), + ToString().buildDeclarationsForClass(clazz, context), ]); } @@ -25,17 +25,15 @@ macro class DataClass Future buildDefinitionForClass( ClassDeclaration clazz, ClassDefinitionBuilder builder) async { await Future.wait([ - hashCode.buildDefinitionForClass(clazz, builder), - equality.buildDefinitionForClass(clazz, builder), - toString.buildDefinitionForClass(clazz, builder), + HashCode().buildDefinitionForClass(clazz, builder), + Equality().buildDefinitionForClass(clazz, builder), + ToString().buildDefinitionForClass(clazz, builder), ]); } } -const autoConstructor = _AutoConstructor(); - -macro class _AutoConstructor implements ClassDeclarationsMacro { - const _AutoConstructor(); +macro class AutoConstructor implements ClassDeclarationsMacro { + const AutoConstructor(); @override Future buildDeclarationsForClass( @@ -46,13 +44,14 @@ macro class _AutoConstructor implements ClassDeclarationsMacro { 'Cannot generate an unnamed constructor because one already exists'); } - // Don't use the identifier here because it should just be the raw name. - var parts = [clazz.identifier.name, '.gen({']; + var params = []; // Add all the fields of `declaration` as named parameters. var fields = await builder.fieldsOf(clazz); - for (var field in fields) { - var requiredKeyword = field.type.isNullable ? '' : 'required '; - parts.addAll(['\n${requiredKeyword}', field.identifier, ',']); + if (fields.isNotEmpty) { + for (var field in fields) { + var requiredKeyword = field.type.isNullable ? '' : 'required '; + params.addAll(['\n${requiredKeyword}', field.identifier, ',']); + } } // The object type from dart:core. @@ -78,7 +77,7 @@ macro class _AutoConstructor implements ClassDeclarationsMacro { // parameters in this constructor. for (var param in superconstructor.positionalParameters) { var requiredKeyword = param.isRequired ? 'required' : ''; - parts.addAll([ + params.addAll([ '\n$requiredKeyword', param.type.code, ' ${param.identifier.name},', @@ -86,14 +85,24 @@ macro class _AutoConstructor implements ClassDeclarationsMacro { } for (var param in superconstructor.namedParameters) { var requiredKeyword = param.isRequired ? '' : 'required '; - parts.addAll([ + params.addAll([ '\n$requiredKeyword', param.type.code, ' ${param.identifier.name},', ]); } } - parts.add('\n})'); + + bool hasParams = params.isNotEmpty; + List parts = [ + // Don't use the identifier here because it should just be the raw name. + clazz.identifier.name, + '.gen(', + if (hasParams) '{', + ...params, + if (hasParams) '}', + ')', + ]; if (superconstructor != null) { parts.addAll([' : super.', superconstructor.identifier.name, '(']); for (var param in superconstructor.positionalParameters) { @@ -111,11 +120,9 @@ macro class _AutoConstructor implements ClassDeclarationsMacro { } } -const copyWith = _CopyWith(); - // TODO: How to deal with overriding nullable fields to `null`? -macro class _CopyWith implements ClassDeclarationsMacro { - const _CopyWith(); +macro class CopyWith implements ClassDeclarationsMacro { + const CopyWith(); @override Future buildDeclarationsForClass( @@ -141,24 +148,25 @@ macro class _CopyWith implements ClassDeclarationsMacro { field.identifier, ]), ]; + var hasParams = namedParams.isNotEmpty; builder.declareInClass(DeclarationCode.fromParts([ clazz.identifier, - ' copyWith({', + ' copyWith(', + if (hasParams) '{', ...namedParams.joinAsCode(', '), - ',})', + if (hasParams) '}', + ')', // TODO: We assume this constructor exists, but should check '=> ', clazz.identifier, '.gen(', ...args.joinAsCode(', '), - ', );', + ');', ])); } } -const hashCode = _HashCode(); - -macro class _HashCode +macro class HashCode implements ClassDeclarationsMacro, ClassDefinitionMacro { - const _HashCode(); + const HashCode(); @override Future buildDeclarationsForClass( @@ -189,11 +197,9 @@ macro class _HashCode } } -const equality = _Equality(); - -macro class _Equality +macro class Equality implements ClassDeclarationsMacro, ClassDefinitionMacro { - const _Equality(); + const Equality(); @override Future buildDeclarationsForClass( @@ -234,11 +240,10 @@ macro class _Equality } } -const toString = _ToString(); -macro class _ToString +macro class ToString implements ClassDeclarationsMacro, ClassDefinitionMacro { - const _ToString(); + const ToString(); @override Future buildDeclarationsForClass(