Skip to content

Commit

Permalink
Elements. Support for generateForAnnotatedDirective() and related.
Browse files Browse the repository at this point in the history
  • Loading branch information
scheglov committed Jan 1, 2025
1 parent 0aa4656 commit c6f1dfb
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 28 deletions.
3 changes: 2 additions & 1 deletion source_gen/lib/source_gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export 'src/constants/revive.dart' show Revivable;
export 'src/generator.dart'
show Generator, InvalidGenerationSource, InvalidGenerationSourceError;
export 'src/generator_for_annotation.dart' show GeneratorForAnnotation;
export 'src/library.dart' show AnnotatedElement, LibraryReader;
export 'src/library.dart'
show AnnotatedDirective, AnnotatedElement, LibraryReader;
export 'src/span_for_element.dart' show spanForElement, spanForElement2;
export 'src/type_checker.dart' show TypeChecker, UnresolvedAnnotationException;
export 'src/utils.dart' show typeNameOf;
38 changes: 38 additions & 0 deletions source_gen/lib/src/generator_for_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ abstract class GeneratorForAnnotation<T> extends Generator {
FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
final values = <String>{};

for (var annotatedDirective in library.libraryDirectivesAnnotatedWith(
typeChecker,
throwOnUnresolved: throwOnUnresolved,
)) {
final generatedValue = generateForAnnotatedDirective(
annotatedDirective.directive,
annotatedDirective.annotation,
buildStep,
);
await for (var value in normalizeGeneratorOutput(generatedValue)) {
assert(value.length == value.trim().length);
values.add(value);
}
}

for (var annotatedElement in library.annotatedWith(
typeChecker,
throwOnUnresolved: throwOnUnresolved,
Expand Down Expand Up @@ -123,4 +138,27 @@ abstract class GeneratorForAnnotation<T> extends Generator {
ConstantReader annotation,
BuildStep buildStep,
) {}

/// Implement to return source code to generate for [directive].
///
/// This method is invoked based on finding directives annotated with an
/// instance of [T]. The [annotation] is provided as a [ConstantReader].
///
/// Supported return values include a single [String] or multiple [String]
/// instances within an [Iterable] or [Stream]. It is also valid to return a
/// [Future] of [String], [Iterable], or [Stream]. When multiple values are
/// returned through an iterable or stream they will be deduplicated.
/// Typically each value will be an independent unit of code and the
/// deduplication prevents re-defining the same member multiple times. For
/// example if multiple annotated elements may need a specific utility method
/// available it can be output for each one, and the single deduplicated
/// definition can be shared.
///
/// Implementations should return `null` when no content is generated. Empty
/// or whitespace-only [String] instances are also ignored.
dynamic generateForAnnotatedDirective(
ElementDirective directive,
ConstantReader annotation,
BuildStep buildStep,
) {}
}
34 changes: 34 additions & 0 deletions source_gen/lib/src/library.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import 'constants/reader.dart';
import 'type_checker.dart';
import 'utils.dart';

/// Result of finding an [annotation] on [directive] through [LibraryReader].
class AnnotatedDirective {
final ConstantReader annotation;
final ElementDirective directive;

const AnnotatedDirective(this.annotation, this.directive);

Metadata? get metadata2 => directive.metadata2;
}

/// Result of finding an [annotation] on [element] through [LibraryReader].
class AnnotatedElement {
final ConstantReader annotation;
Expand Down Expand Up @@ -87,6 +97,30 @@ class LibraryReader {
}
}

/// All of the directives in this library annotated with [checker].
Iterable<AnnotatedDirective> libraryDirectivesAnnotatedWith(
TypeChecker checker, {
bool throwOnUnresolved = true,
}) sync* {
final firstFragment = element2.firstFragment;
final directives = [
...firstFragment.libraryImports2,
...firstFragment.libraryExports2,
...firstFragment.partIncludes,
];

for (final directive in directives) {
final annotation = checker.firstAnnotationOf2(
directive,
throwOnUnresolved: throwOnUnresolved,
);

if (annotation != null) {
yield AnnotatedDirective(ConstantReader(annotation), directive);
}
}
}

/// All of the declarations in this library annotated with exactly [checker].
Iterable<AnnotatedElement> annotatedWithExact(
TypeChecker checker, {
Expand Down
10 changes: 5 additions & 5 deletions source_gen/lib/src/type_checker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ abstract class TypeChecker {
///
/// Throws on unresolved annotations unless [throwOnUnresolved] is `false`.
DartObject? firstAnnotationOf2(
Element2 element, {
Object element, {
bool throwOnUnresolved = true,
}) {
if (element case final Annotatable annotatable) {
Expand Down Expand Up @@ -188,13 +188,13 @@ abstract class TypeChecker {
}

DartObject? _computeConstantValue2(
Element2 element,
Object element,
ElementAnnotation annotation,
int annotationIndex, {
bool throwOnUnresolved = true,
}) {
final result = annotation.computeConstantValue();
if (result == null && throwOnUnresolved) {
if (result == null && throwOnUnresolved && element is Element2) {
throw UnresolvedAnnotationException._from(element, annotationIndex);
}
return result;
Expand All @@ -219,7 +219,7 @@ abstract class TypeChecker {
/// Throws [UnresolvedAnnotationException] on unresolved annotations unless
/// [throwOnUnresolved] is explicitly set to `false` (default is `true`).
Iterable<DartObject> annotationsOf2(
Element2 element, {
Object element, {
bool throwOnUnresolved = true,
}) =>
_annotationsWhere2(
Expand All @@ -246,7 +246,7 @@ abstract class TypeChecker {
}

Iterable<DartObject> _annotationsWhere2(
Element2 element,
Object element,
bool Function(DartType) predicate, {
bool throwOnUnresolved = true,
}) sync* {
Expand Down
3 changes: 2 additions & 1 deletion source_gen/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ dev_dependencies:
test: ^1.16.0

dependency_overrides:
analyzer: ^7.1.0
analyzer:
path: /Users/scheglov/Source/Dart/sdk.git/sdk/pkg/analyzer
build:
git:
url: https://github.com/dart-lang/build.git
Expand Down
116 changes: 95 additions & 21 deletions source_gen/test/generator_for_annotation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ library;
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:source_gen/source_gen.dart';
Expand All @@ -25,19 +26,24 @@ void main() {
'list with null, empty, and whitespace items': [null, '', '\n \t'],
}.entries) {
test(entry.key, () async {
final generator =
_StubGenerator<Deprecated>('Value', (_) => entry.value);
final generator = _StubGenerator<Deprecated>(
'Value',
elementBehavior: (_) => entry.value,
);
final builder = LibraryBuilder(generator);
await testBuilder(builder, _inputMap, outputs: {});
});
}
});

test('Supports and dedupes multiple return values', () async {
final generator = _StubGenerator<Deprecated>('Repeating', (element) sync* {
yield '// There are deprecated values in this library!';
yield '// ${element.name}';
});
final generator = _StubGenerator<Deprecated>(
'Repeating',
elementBehavior: (element) sync* {
yield '// There are deprecated values in this library!';
yield '// ${element.name}';
},
);
final builder = LibraryBuilder(generator);
await testBuilder(
builder,
Expand Down Expand Up @@ -65,13 +71,19 @@ $dartFormatWidth

group('handles errors correctly', () {
for (var entry in {
'sync errors': _StubGenerator<Deprecated>('Failing', (_) {
throw StateError('not supported!');
}),
'from iterable': _StubGenerator<Deprecated>('FailingIterable', (_) sync* {
yield '// There are deprecated values in this library!';
throw StateError('not supported!');
}),
'sync errors': _StubGenerator<Deprecated>(
'Failing',
elementBehavior: (_) {
throw StateError('not supported!');
},
),
'from iterable': _StubGenerator<Deprecated>(
'FailingIterable',
elementBehavior: (_) sync* {
yield '// There are deprecated values in this library!';
throw StateError('not supported!');
},
),
}.entries) {
test(entry.key, () async {
final builder = LibraryBuilder(entry.value);
Expand All @@ -92,8 +104,12 @@ $dartFormatWidth

test('Does not resolve the library if there are no top level annotations',
() async {
final builder =
LibraryBuilder(_StubGenerator<Deprecated>('Deprecated', (_) => null));
final builder = LibraryBuilder(
_StubGenerator<Deprecated>(
'Deprecated',
elementBehavior: (_) => null,
),
);
final input = AssetId('a', 'lib/a.dart');
final assets = {input: 'main() {}'};

Expand All @@ -116,7 +132,7 @@ $dartFormatWidth
final builder = LibraryBuilder(
_StubGenerator<Deprecated>(
'Deprecated',
(element) => '// ${element.displayName}',
elementBehavior: (element) => '// ${element.displayName}',
),
);
await testBuilder(
Expand All @@ -142,12 +158,54 @@ $dartFormatWidth
);
});

test('applies to annotated directives', () async {
final builder = LibraryBuilder(
_StubGenerator<Deprecated>(
'Deprecated',
directiveBehavior: (element) => '// ${element.runtimeType}',
elementBehavior: (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': '''
$dartFormatWidth
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// Generator: Deprecated
// **************************************************************************
// LibraryImportElementImpl
// LibraryExportElementImpl
// PartElementImpl
''',
},
);
});

group('Unresolved annotations', () {
test('cause an error by default', () async {
final builder = LibraryBuilder(
_StubGenerator<Deprecated>(
'Deprecated',
(element) => '// ${element.displayName}',
elementBehavior: (element) => '// ${element.displayName}',
),
);
expect(
Expand All @@ -169,7 +227,7 @@ $dartFormatWidth
final builder = LibraryBuilder(
_StubGenerator<Deprecated>(
'Deprecated',
(element) => '// ${element.displayName}',
elementBehavior: (element) => '// ${element.displayName}',
throwOnUnresolved: false,
),
);
Expand All @@ -192,20 +250,36 @@ $dartFormatWidth

class _StubGenerator<T> extends GeneratorForAnnotation<T> {
final String _name;
final Object? Function(Element) _behavior;
final Object? Function(ElementDirective) directiveBehavior;
final Object? Function(Element) elementBehavior;

const _StubGenerator(
this._name, {
this.directiveBehavior = _returnNull,
required this.elementBehavior,
super.throwOnUnresolved,
});

const _StubGenerator(this._name, this._behavior, {super.throwOnUnresolved});
@override
Object? generateForAnnotatedDirective(
ElementDirective directive,
ConstantReader annotation,
BuildStep buildStep,
) =>
directiveBehavior(directive);

@override
Object? generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) =>
_behavior(element);
elementBehavior(element);

@override
String toString() => _name;

static Null _returnNull(Object _) => null;
}

const _inputMap = {
Expand Down

0 comments on commit c6f1dfb

Please sign in to comment.