Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a note about not importable annotations #576

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions source_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion source_gen/lib/source_gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion source_gen/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 60 additions & 1 deletion source_gen/lib/src/generator_for_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends Generator {
const GeneratorForAnnotation();

Expand Down Expand Up @@ -81,3 +88,55 @@ abstract class GeneratorForAnnotation<T> 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<String> generate(LibraryReader library, BuildStep buildStep) async {
final values = <String>{};

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');
}
}
2 changes: 1 addition & 1 deletion source_gen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
249 changes: 249 additions & 0 deletions source_gen/test/generator_for_matching_annotation_test.dart
Original file line number Diff line number Diff line change
@@ -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': <Object>[],
'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<StateError>().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<AssetId, String> assets;
final parsedUnits = <AssetId>{};
final resolvedLibs = <AssetId>{};

_TestingResolver(this.assets);

@override
Future<CompilationUnit> compilationUnitFor(
AssetId assetId, {
bool allowSyntaxErrors = false,
}) async {
parsedUnits.add(assetId);
return parseString(content: assets[assetId]!).unit;
}

@override
Future<bool> isLibrary(AssetId assetId) async {
final unit = await compilationUnitFor(assetId);
return unit.directives.every((d) => d is! PartOfDirective);
}

@override
Future<LibraryElement> 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<ReleasableResolver> get(BuildStep buildStep) =>
Future.value(_resolver);

@override
void reset() {}
}