blob: 2ec57e51f1e6652f608166532520f19790466ab6 [file]
// 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.
import 'dart:async';
import 'package:analyzer/dart/ast/ast.dart' as ast;
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart' hide FunctionType;
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:source_gen/source_gen.dart' as source_gen show LibraryBuilder;
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
import 'annotation.dart';
Builder checksBuilder(BuilderOptions? _) => source_gen.LibraryBuilder(
const ChecksGenerator(),
generatedExtension: '.checks.dart',
);
final class ChecksGenerator extends GeneratorForAnnotation<CheckExtensions> {
const ChecksGenerator();
@override
Future<String> generateForAnnotatedDirective(
ElementDirective directive,
ConstantReader annotation,
BuildStep buildStep,
) async {
final basename = p.url.basenameWithoutExtension(buildStep.inputId.path);
final expectedImport = '$basename.checks.dart';
if (directive
case LibraryImport(:final DirectiveUriWithRelativeUri uri) ||
LibraryExport(:final DirectiveUriWithRelativeUri uri)
when uri.relativeUriString == expectedImport) {
// Annotation is on the correct import or export
} else {
throw InvalidGenerationSourceError(
'must annotate an import or export of $expectedImport',
);
}
final compilationUnit =
await buildStep.resolver.astNodeFor(directive.libraryFragment)
as ast.CompilationUnit?;
if (compilationUnit == null) {
throw InvalidGenerationSourceError('Could not find AST for library.');
}
final typeNames = await _extractTypeNamesFromAst(
compilationUnit,
expectedImport,
);
if (typeNames.isEmpty) {
throw InvalidGenerationSourceError(
'Could not find @CheckExtensions annotation or it was empty.',
);
}
final currentLibrary = directive.libraryFragment.element;
final imports = directive.libraryFragment.importedLibraries;
final targetElements = <Element>[];
for (final name in typeNames) {
final element = _findElementByName(currentLibrary, imports, name);
if (element != null) {
targetElements.add(element);
} else {
throw InvalidGenerationSourceError('Could not resolve type: $name');
}
}
final extensions = await Future.wait([
for (final element in targetElements)
_createExtension(
directive.libraryFragment.importedLibraries,
element,
buildStep.resolver,
buildStep.inputId.path,
),
]);
final library = Library(
(b) => b
..body.addAll(extensions)
..directives.add(
Directive(
(b) => b
..type = DirectiveType.import
..url = 'package:checks/checks.dart',
),
),
);
final emitter = DartEmitter.scoped(
useNullSafetySyntax: true,
orderDirectives: true,
);
return library.accept(emitter).toString();
}
@override
dynamic generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
final basename = p.url.basenameWithoutExtension(buildStep.inputId.path);
throw InvalidGenerationSourceError(
'must annotate an import or export of $basename.checks.dart',
element: element,
);
}
Future<Extension> _createExtension(
List<LibraryElement> imports,
Element element,
Resolver resolver,
String entryAssetPath,
) async {
final import = await _findImportFor(
imports,
element,
resolver,
entryAssetPath,
);
final checkableProperties = _getCheckableProperties(element);
final hasGetters = await Future.wait([
for (final property in checkableProperties)
_createHasGetter(imports, property, resolver, entryAssetPath),
]);
return Extension(
(b) => b
..name = '${element.displayName}Checks'
..on = TypeReference(
(b) => b
..symbol = 'Subject'
..url = 'package:checks/context.dart'
..types.add(refer(element.displayName, import)),
)
..methods.addAll(hasGetters),
);
}
bool _isCheckableField(FieldElement field) =>
field.name != 'hashCode' &&
!field.isStatic &&
field.type is! FunctionType;
Future<Method> _createHasGetter(
List<LibraryElement> imports,
_CheckableProperty property,
Resolver resolver,
String entryAssetPath,
) async {
final typeElement = property.element;
String? import;
if (typeElement != null) {
import = await _findImportFor(
imports,
typeElement,
resolver,
entryAssetPath,
);
}
final name = property.name;
return Method(
(b) => b
..name = name
..type = MethodType.getter
..returns = TypeReference(
(b) => b
..symbol = 'Subject'
..url = 'package:checks/context.dart'
..types.add(refer(property.type.getDisplayString(), import)),
)
..lambda = true
..body = refer('has').call([
Method(
(b) => b
..lambda = true
..requiredParameters.add(Parameter((b) => b..name = 'v'))
..body = refer('v').property(name).code,
).closure,
literalString(name),
]).code,
);
}
static Future<String?> _findImportFor(
Iterable<LibraryElement> imports,
Element element,
Resolver resolver,
String entryAssetPath,
) async {
final elementLibrary = element.library!;
if (elementLibrary.isInSdk && !elementLibrary.name!.startsWith('dart._')) {
// For public SDK libraries, just use the source URI.
return elementLibrary.uri.toString();
}
final elementName = element.name;
if (elementName == null) {
return elementLibrary.uri.toString();
}
final exported = imports.firstWhereOrNull(
(l) => l.exportNamespace.get2(elementName) == element,
);
final exportingLibrary = exported ?? elementLibrary;
try {
final typeAssetId = await resolver.assetIdForElement(exportingLibrary);
if (typeAssetId.path.startsWith('lib/')) {
return typeAssetId.uri.toString();
} else {
return p.url.relative(
typeAssetId.path,
from: p.dirname(entryAssetPath),
);
}
} on UnresolvableAssetException {
// Asset may be in a summary.
return exportingLibrary.uri.toString();
}
}
List<_CheckableProperty> _getCheckableProperties(Element element) {
final properties = <_CheckableProperty>[];
if (element is InterfaceElement) {
for (final field in element.fields) {
if (_isCheckableField(field)) {
final type = field.type;
Element? typeElement;
if (type is InterfaceType) {
typeElement = type.element;
}
final name = field.name;
if (name != null) {
properties.add(_CheckableProperty(name, type, typeElement));
}
}
}
} else if (element is ExtensionTypeElement) {
for (final getter in element.getters) {
if (!getter.isStatic && getter.name != 'hashCode') {
final type = getter.returnType;
if (type is! FunctionType) {
Element? typeElement;
if (type is InterfaceType) {
typeElement = type.element;
}
final name = getter.name;
if (name != null) {
properties.add(_CheckableProperty(name, type, typeElement));
}
}
}
}
}
return properties;
}
Future<List<String>> _extractTypeNamesFromAst(
ast.CompilationUnit compilationUnit,
String expectedImport,
) async {
for (final directive in compilationUnit.directives) {
bool isTargetDirective = false;
if (directive is ast.ImportDirective) {
isTargetDirective = directive.uri.stringValue == expectedImport;
} else if (directive is ast.ExportDirective) {
isTargetDirective = directive.uri.stringValue == expectedImport;
}
if (isTargetDirective) {
for (final annotation in directive.metadata) {
if (annotation.name.name == 'CheckExtensions') {
final arguments = annotation.arguments?.arguments;
if (arguments != null && arguments.isNotEmpty) {
final typesArg = arguments.first;
if (typesArg is ast.ListLiteral) {
return typesArg.elements
.whereType<ast.Identifier>()
.map((e) => e.name)
.toList();
}
}
}
}
}
}
return [];
}
Element? _findElementByName(
LibraryElement currentLibrary,
List<LibraryElement> imports,
String name,
) {
final element = currentLibrary.exportNamespace.get2(name);
if (element != null) return element;
for (final import in imports) {
final element = import.exportNamespace.get2(name);
if (element != null) {
return element;
}
}
return null;
}
}
class _CheckableProperty {
final String name;
final DartType type;
final Element? element;
_CheckableProperty(this.name, this.type, this.element);
}