|
| 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 | +} |
0 commit comments