From c7b6e6ab4a847a15d43647a076f02c06576aea28 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 3 Dec 2021 13:12:18 -0800 Subject: [PATCH 1/2] Add a note about not importable annotations The build systems run on the Dart VM only, so there are limitations on what imports can be used by builders. Add documentation describing how to work around this limitation by overriding the `TypeChecker` with something other than `fromRuntime`. --- source_gen/CHANGELOG.md | 2 ++ source_gen/lib/src/generator_for_annotation.dart | 9 ++++++++- source_gen/pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md index ac952d6b..fbdadda5 100644 --- a/source_gen/CHANGELOG.md +++ b/source_gen/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.2.1-dev + ## 1.2.0 - Include the `LibraryElement` in `LibraryReader.allElements`, diff --git a/source_gen/lib/src/generator_for_annotation.dart b/source_gen/lib/src/generator_for_annotation.dart index 6edf426f..69e7e09b 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 extend +/// `GeneratorForAnnotation` and override the [typeChecker] member with a +/// checker matching the annotation type. abstract class GeneratorForAnnotation extends Generator { const GeneratorForAnnotation(); diff --git a/source_gen/pubspec.yaml b/source_gen/pubspec.yaml index 70a64ae3..995b45b7 100644 --- a/source_gen/pubspec.yaml +++ b/source_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: source_gen -version: 1.2.0 +version: 1.2.1-dev description: >- Source code generation builders and utilities for the Dart build system repository: https://github.com/dart-lang/source_gen From f1219b3c8cb49b2e37de47f9fd0160fa46a9ad97 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 9 May 2023 22:24:58 +0000 Subject: [PATCH 2/2] Add a GeneratorForMatchingAnnotation Copy the implementation and test for `GeneratorForAnnotation.` --- source_gen/CHANGELOG.md | 6 +- source_gen/lib/source_gen.dart | 3 +- source_gen/lib/src/builder.dart | 4 +- .../lib/src/generator_for_annotation.dart | 58 +++- source_gen/pubspec.yaml | 2 +- ...enerator_for_matching_annotation_test.dart | 249 ++++++++++++++++++ 6 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 source_gen/test/generator_for_matching_annotation_test.dart diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md index 396ff1fd..853b018c 100644 --- a/source_gen/CHANGELOG.md +++ b/source_gen/CHANGELOG.md @@ -1,4 +1,8 @@ -## 1.3.1-dev +## 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 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 15f893ca..14fd1343 100644 --- a/source_gen/lib/src/generator_for_annotation.dart +++ b/source_gen/lib/src/generator_for_annotation.dart @@ -44,9 +44,9 @@ import 'type_checker.dart'; /// /// 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 extend -/// `GeneratorForAnnotation` and override the [typeChecker] member with a -/// checker matching the annotation type. +/// `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(); @@ -88,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 fdeb851a..3a285bf1 100644 --- a/source_gen/pubspec.yaml +++ b/source_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: source_gen -version: 1.3.1-dev +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() {} +}