diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md index 82735ab5..853b018c 100644 --- a/source_gen/CHANGELOG.md +++ b/source_gen/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.4.0-wip + +* Add `GeneratorForMatchingAnnotation` which explicitly takes the generating + behavior and `TypeChecker` as separate constructor arguments instead of using + an extending class. + ## 1.3.0 * Add support for `build_extensions` configuration of builders producing diff --git a/source_gen/lib/source_gen.dart b/source_gen/lib/source_gen.dart index b695732e..6971544a 100644 --- a/source_gen/lib/source_gen.dart +++ b/source_gen/lib/source_gen.dart @@ -7,7 +7,8 @@ export 'src/builder.dart' export 'src/constants/reader.dart' show ConstantReader; export 'src/constants/revive.dart' show Revivable; export 'src/generator.dart' show Generator, InvalidGenerationSourceError; -export 'src/generator_for_annotation.dart' show GeneratorForAnnotation; +export 'src/generator_for_annotation.dart' + show GeneratorForAnnotation, GeneratorForMatchingAnnotation; export 'src/library.dart' show AnnotatedElement, LibraryReader; export 'src/span_for_element.dart' show spanForElement; export 'src/type_checker.dart' show TypeChecker, UnresolvedAnnotationException; diff --git a/source_gen/lib/src/builder.dart b/source_gen/lib/src/builder.dart index 6c505419..b668e9d2 100644 --- a/source_gen/lib/src/builder.dart +++ b/source_gen/lib/src/builder.dart @@ -88,7 +88,9 @@ class _Builder extends Builder { if (!await resolver.isLibrary(buildStep.inputId)) return; - if (_generators.every((g) => g is GeneratorForAnnotation) && + if (_generators.every((g) => + g is GeneratorForAnnotation || + g is GeneratorForMatchingAnnotation) && !(await _hasAnyTopLevelAnnotations( buildStep.inputId, resolver, diff --git a/source_gen/lib/src/generator_for_annotation.dart b/source_gen/lib/src/generator_for_annotation.dart index 4d53fcf9..14fd1343 100644 --- a/source_gen/lib/src/generator_for_annotation.dart +++ b/source_gen/lib/src/generator_for_annotation.dart @@ -39,7 +39,14 @@ import 'type_checker.dart'; /// extension, are not searched for annotations. To operate on, for instance, /// annotated fields of a class ensure that the class itself is annotated with /// [T] and use the [Element] to iterate over fields. The [TypeChecker] utility -/// may be helpful to check which elements have a given annotation. +/// may be helpful to check which elements have a given annotation if the +/// generator should further filter it's target based on annotations. +/// +/// If the annotation type cannot be imported on the Dart VM, for example if it +/// imports `dart:html` or `dart:ui`, then the default behavior of using +/// `TypeChecker.fromRuntime` is not feasible. In these cases use +/// `GeneratorForMatchingAnnotation` and pass an appropriate [TypeChecker] to +/// the constructor. abstract class GeneratorForAnnotation extends Generator { const GeneratorForAnnotation(); @@ -81,3 +88,55 @@ abstract class GeneratorForAnnotation extends Generator { BuildStep buildStep, ); } + +/// A [Generator] that takes a [TypeChecker] to match an annotation, and a +/// callback to generate the output for each annotated element. +/// +/// When all annotated elements have been processed, the results will be +/// combined into a single output with duplicate items collapsed. +/// +/// For example, this will allow code generated for all top level elements which +/// are annotated with `@Deprecated`: +/// +/// ```dart +/// Builder createBuilder(BuilderOptions _) => LibraryBuilder( +/// GeneratorForMatchingAnnotation(TypeChecker.fromUrl( +/// 'dart:core#Deprecated')), +/// (element, annotation, buildStep) { +/// // Return the generated content for this element. +/// }); +/// ``` +/// +/// Elements which are not at the top level, such as the members of a class or +/// extension, are not searched for annotations. To operate on, for instance, +/// annotated fields of a class ensure that the class itself is annotated with +/// a matching annotation and use the [Element] to iterate over fields. The +/// [TypeChecker] utility may be helpful to check which elements have a given +/// annotation if the generator should further filter it's target based on +/// annotations. +class GeneratorForMatchingAnnotation extends Generator { + final TypeChecker _typeChecker; + final dynamic Function(Element, ConstantReader annotation, BuildStep) + _generateForAnnotatedElement; + GeneratorForMatchingAnnotation( + this._typeChecker, this._generateForAnnotatedElement, {String? generatorName}); + + @override + FutureOr generate(LibraryReader library, BuildStep buildStep) async { + final values = {}; + + for (var annotatedElement in library.annotatedWith(_typeChecker)) { + final generatedValue = _generateForAnnotatedElement( + annotatedElement.element, + annotatedElement.annotation, + buildStep, + ); + await for (var value in normalizeGeneratorOutput(generatedValue)) { + assert(value.length == value.trim().length); + values.add(value); + } + } + + return values.join('\n\n'); + } +} diff --git a/source_gen/pubspec.yaml b/source_gen/pubspec.yaml index b841ab86..3a285bf1 100644 --- a/source_gen/pubspec.yaml +++ b/source_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: source_gen -version: 1.3.0 +version: 1.4.0-wip description: >- Source code generation builders and utilities for the Dart build system repository: https://github.com/dart-lang/source_gen/tree/master/source_gen diff --git a/source_gen/test/generator_for_matching_annotation_test.dart b/source_gen/test/generator_for_matching_annotation_test.dart new file mode 100644 index 00000000..39b38e4c --- /dev/null +++ b/source_gen/test/generator_for_matching_annotation_test.dart @@ -0,0 +1,249 @@ +// Copyright (c) 2017, 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. + +// The first test that runs `testBuilder` takes a LOT longer than the rest. +@Timeout.factor(3) +library test; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:test/test.dart'; + +const _deprecatedTypeChecker = TypeChecker.fromUrl('dart:core#Deprecated'); +void main() { + group('skips output if per-annotation output is', () { + for (var entry in { + '`null`': null, + 'empty string': '', + 'only whitespace': '\n \t', + 'empty list': [], + 'list with null, empty, and whitespace items': [null, '', '\n \t'] + }.entries) { + test(entry.key, () async { + final generator = GeneratorForMatchingAnnotation( + _deprecatedTypeChecker, (_, __, ___) => entry.value); + final builder = LibraryBuilder(generator); + await testBuilder(builder, _inputMap, outputs: {}); + }); + } + }); + + test('Supports and dedupes multiple return values', () async { + final generator = GeneratorForMatchingAnnotation(_deprecatedTypeChecker, + (element, _, __) sync* { + yield '// There are deprecated values in this library!'; + yield '// ${element.name}'; + }); + final builder = LibraryBuilder(generator); + await testBuilder( + builder, + _inputMap, + outputs: { + 'a|lib/file.g.dart': r''' +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: GeneratorForMatchingAnnotation +// ************************************************************************** + +// There are deprecated values in this library! + +// foo + +// bar + +// baz +''' + }, + ); + }); + + group('handles errors correctly', () { + for (var entry in { + 'sync errors': + GeneratorForMatchingAnnotation(_deprecatedTypeChecker, (_, __, ___) { + throw StateError('not supported!'); + }), + 'from iterable': GeneratorForMatchingAnnotation(_deprecatedTypeChecker, + (_, __, ___) sync* { + yield '// There are deprecated values in this library!'; + throw StateError('not supported!'); + }) + }.entries) { + test(entry.key, () async { + final builder = LibraryBuilder(entry.value); + + await expectLater( + () => testBuilder(builder, _inputMap), + throwsA( + isA().having( + (source) => source.message, + 'message', + 'not supported!', + ), + ), + ); + }); + } + }); + + test('Does not resolve the library if there are no top level annotations', + () async { + final builder = LibraryBuilder(GeneratorForMatchingAnnotation( + _deprecatedTypeChecker, (_, __, ___) => null)); + final input = AssetId('a', 'lib/a.dart'); + final assets = {input: 'main() {}'}; + + final reader = InMemoryAssetReader(sourceAssets: assets); + final resolver = _TestingResolver(assets); + + await runBuilder( + builder, + [input], + reader, + InMemoryAssetWriter(), + _FixedResolvers(resolver), + ); + + expect(resolver.parsedUnits, {input}); + expect(resolver.resolvedLibs, isEmpty); + }); + + test('applies to annotated libraries', () async { + final builder = LibraryBuilder( + GeneratorForMatchingAnnotation( + _deprecatedTypeChecker, + (element, __, ___) => '// ${element.displayName}', + ), + ); + await testBuilder( + builder, + { + 'a|lib/file.dart': ''' + @deprecated + library foo; + ''' + }, + outputs: { + 'a|lib/file.g.dart': ''' +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: GeneratorForMatchingAnnotation +// ************************************************************************** + +// foo +''' + }, + ); + }); + + test('applies to annotated directives', () async { + final builder = LibraryBuilder( + GeneratorForMatchingAnnotation( + _deprecatedTypeChecker, + (element, _, __) => '// ${element.runtimeType}', + ), + ); + await testBuilder( + builder, + { + 'a|lib/imported.dart': '', + 'a|lib/part.dart': 'part of \'file.dart\';', + 'a|lib/file.dart': ''' + library; + @deprecated + import 'imported.dart'; + @deprecated + export 'imported.dart'; + @deprecated + part 'part.dart'; + ''' + }, + outputs: { + 'a|lib/file.g.dart': ''' +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: GeneratorForMatchingAnnotation +// ************************************************************************** + +// LibraryImportElementImpl + +// LibraryExportElementImpl + +// PartElementImpl +''' + }, + ); + }); +} + +const _inputMap = { + 'a|lib/file.dart': ''' + @deprecated + final foo = 'foo'; + + @deprecated + final bar = 'bar'; + + @deprecated + final baz = 'baz'; + ''' +}; + +class _TestingResolver implements ReleasableResolver { + final Map assets; + final parsedUnits = {}; + final resolvedLibs = {}; + + _TestingResolver(this.assets); + + @override + Future compilationUnitFor( + AssetId assetId, { + bool allowSyntaxErrors = false, + }) async { + parsedUnits.add(assetId); + return parseString(content: assets[assetId]!).unit; + } + + @override + Future isLibrary(AssetId assetId) async { + final unit = await compilationUnitFor(assetId); + return unit.directives.every((d) => d is! PartOfDirective); + } + + @override + Future libraryFor( + AssetId assetId, { + bool allowSyntaxErrors = false, + }) async { + resolvedLibs.add(assetId); + throw StateError('This method intentionally throws'); + } + + @override + void release() {} + + @override + void noSuchMethod(_) => throw UnimplementedError(); +} + +class _FixedResolvers implements Resolvers { + final ReleasableResolver _resolver; + + _FixedResolvers(this._resolver); + + @override + Future get(BuildStep buildStep) => + Future.value(_resolver); + + @override + void reset() {} +}