Skip to content

Commit cc4d6e1

Browse files
authored
add throwOnUnresolved to TypeChecker methods to allow matching annotations in a non-fully resolved context (#249)
1 parent 7fbcacc commit cc4d6e1

File tree

3 files changed

+103
-27
lines changed

3 files changed

+103
-27
lines changed

CHANGELOG.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
`package:json_serializable`.
1515
* Removed `lib/builder.dart`. Import through `source_gen.dart` instead.
1616
* Removed `OutputFormatter` typedef.
17-
17+
1818
* Add `LibraryReader.allElements` - a utility to iterate across all `Element`
1919
instances contained in Dart library.
2020
* Add `LibraryReader.element` to get back to the `LibraryElement` instance.
@@ -29,6 +29,12 @@ findTokenField(DartObject o) {
2929
}
3030
```
3131

32+
* Add `throwOnUnresolved` optional parameter to `TypeChecker.annotationsOf`,
33+
`TypeChecker.annotationsOfExact`, `TypeChecker.firstAnnotationOf`, and
34+
`TypeChecker.firstAnnotationOfExact`. Setting this to `false` will enable you
35+
to check for matching annotations with incomplete type information (at your
36+
own risk).
37+
3238
## 0.6.1+1
3339

3440
* Tighten constraint on `source_span`.
@@ -70,14 +76,14 @@ findTokenField(DartObject o) {
7076
```dart
7177
import 'package:analyzer/dart/element/type.dart';
7278
import 'package:source_gen/source_gen.dart';
73-
79+
7480
void checkType(DartType dartType) {
7581
// Checks compared to runtime type `SomeClass`.
7682
print(const TypeChecker.forRuntime(SomeClass).isExactlyType(dartType));
77-
83+
7884
// Checks compared to a known Url/Symbol:
7985
const TypeChecker.forUrl('package:foo/foo.dart#SomeClass');
80-
86+
8187
// Checks compared to another resolved `DartType`:
8288
const TypeChecker.forStatic(anotherDartType);
8389
}
@@ -86,19 +92,19 @@ findTokenField(DartObject o) {
8692
* Failing to add a `library` directive to a library that is being used as a
8793
generator target that generates partial files (`part of`) is now an explicit
8894
error that gives a hint on how to name and fix your library:
89-
95+
9096
```bash
9197
> Could not find library identifier so a "part of" cannot be built.
9298
>
9399
> Consider adding the following to your source file:
94100
>
95101
> "library foo.bar;"
96102
```
97-
103+
98104
In Dart SDK `>=1.25.0` this can be relaxed as `part of` can refer to a path.
99105
To opt-in, `GeneratorBuilder` now has a new flag, `requireLibraryDirective`.
100106
Set it to `false`, and also set your `sdk` constraint appropriately:
101-
107+
102108
```yaml
103109
sdk: '>=1.25.0 <2.0.0'
104110
```
@@ -107,11 +113,11 @@ findTokenField(DartObject o) {
107113
high-level APIs, including `findType`, which traverses `export` directives
108114
for publicly exported types. For example, to find `Generator` from
109115
`package:source_gen/source_gen.dart`:
110-
116+
111117
```dart
112118
void example(LibraryElement pkgSourceGen) {
113119
var library = new LibraryReader(pkgSourceGen);
114-
120+
115121
// Instead of pkgSourceGen.getType('Generator'), which is null.
116122
library.findType('Generator');
117123
}
@@ -120,26 +126,26 @@ findTokenField(DartObject o) {
120126
* Added `ConstantReader`, a high-level API for reading from constant (static)
121127
values from Dart source code (usually represented by `DartObject` from the
122128
`analyzer` package):
123-
129+
124130
```dart
125131
abstract class ConstantReader {
126132
factory ConstantReader(DartObject object) => ...
127-
133+
128134
// Other methods and properties also exist.
129-
135+
130136
/// Reads[ field] from the constant as another constant value.
131137
ConstantReader read(String field);
132-
138+
133139
/// Reads [field] from the constant as a boolean.
134140
///
135141
/// If the resulting value is `null`, uses [defaultTo] if defined.
136142
bool readBool(String field, {bool defaultTo()});
137-
143+
138144
/// Reads [field] from the constant as an int.
139145
///
140146
/// If the resulting value is `null`, uses [defaultTo] if defined.
141147
int readInt(String field, {int defaultTo()});
142-
148+
143149
/// Reads [field] from the constant as a string.
144150
///
145151
/// If the resulting value is `null`, uses [defaultTo] if defined.

lib/src/type_checker.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,34 @@ abstract class TypeChecker {
5555
/// Returns the first constant annotating [element] assignable to this type.
5656
///
5757
/// Otherwise returns `null`.
58-
DartObject firstAnnotationOf(Element element) {
58+
///
59+
/// Throws on unresolved annotations unless [throwOnUnresolved] is `false`.
60+
DartObject firstAnnotationOf(Element element, {bool throwOnUnresolved}) {
5961
if (element.metadata.isEmpty) {
6062
return null;
6163
}
62-
final results = annotationsOf(element);
64+
final results =
65+
annotationsOf(element, throwOnUnresolved: throwOnUnresolved);
6366
return results.isEmpty ? null : results.first;
6467
}
6568

6669
/// Returns the first constant annotating [element] that is exactly this type.
67-
DartObject firstAnnotationOfExact(Element element) {
70+
///
71+
/// Throws on unresolved annotations unless [throwOnUnresolved] is `false`.
72+
DartObject firstAnnotationOfExact(Element element, {bool throwOnUnresolved}) {
6873
if (element.metadata.isEmpty) {
6974
return null;
7075
}
71-
final results = annotationsOfExact(element);
76+
final results =
77+
annotationsOfExact(element, throwOnUnresolved: throwOnUnresolved);
7278
return results.isEmpty ? null : results.first;
7379
}
7480

75-
DartObject _checkedConstantValue(ElementAnnotation annotation) {
81+
DartObject _computeConstantValue(ElementAnnotation annotation,
82+
{bool throwOnUnresolved}) {
83+
throwOnUnresolved ??= true;
7684
final result = annotation.computeConstantValue();
77-
if (result == null) {
85+
if (result == null && throwOnUnresolved) {
7886
throw new StateError(
7987
'Could not resolve $annotation. An import or dependency may be '
8088
'missing or invalid.');
@@ -83,14 +91,24 @@ abstract class TypeChecker {
8391
}
8492

8593
/// Returns annotating constants on [element] assignable to this type.
86-
Iterable<DartObject> annotationsOf(Element element) => element.metadata
87-
.map(_checkedConstantValue)
88-
.where((a) => a?.type != null && isAssignableFromType(a.type));
94+
///
95+
/// Throws on unresolved annotations unless [throwOnUnresolved] is `false`.
96+
Iterable<DartObject> annotationsOf(Element element,
97+
{bool throwOnUnresolved}) =>
98+
element.metadata
99+
.map((annotation) => _computeConstantValue(annotation,
100+
throwOnUnresolved: throwOnUnresolved))
101+
.where((a) => a?.type != null && isAssignableFromType(a.type));
89102

90103
/// Returns annotating constants on [element] of exactly this type.
91-
Iterable<DartObject> annotationsOfExact(Element element) => element.metadata
92-
.map(_checkedConstantValue)
93-
.where((a) => a?.type != null && isExactlyType(a.type));
104+
///
105+
/// Throws on unresolved annotations unless [throwOnUnresolved] is `false`.
106+
Iterable<DartObject> annotationsOfExact(Element element,
107+
{bool throwOnUnresolved}) =>
108+
element.metadata
109+
.map((annotation) => _computeConstantValue(annotation,
110+
throwOnUnresolved: throwOnUnresolved))
111+
.where((a) => a?.type != null && isExactlyType(a.type));
94112

95113
/// Returns `true` if the type of [element] can be assigned to this type.
96114
bool isAssignableFrom(Element element) =>

test/type_checker_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,56 @@ void main() {
286286
expect(bExact.map((a) => a.type.name), ['B']);
287287
});
288288
});
289+
290+
group('unresolved annotations', () {
291+
TypeChecker $A;
292+
ClassElement $ExampleOfA;
293+
294+
setUpAll(() async {
295+
final resolver = await resolveSource(r'''
296+
library _test;
297+
298+
// Put the missing annotation first so it throws.
299+
@B()
300+
@A()
301+
class ExampleOfA {}
302+
303+
class A {
304+
const A();
305+
}
306+
''');
307+
final library = resolver.getLibraryByName('_test');
308+
$A = new TypeChecker.fromStatic(library.getType('A').type);
309+
$ExampleOfA = library.getType('ExampleOfA');
310+
});
311+
312+
test('should throw by default', () {
313+
expect(() => $A.firstAnnotationOf($ExampleOfA), throwsStateError);
314+
expect(() => $A.annotationsOf($ExampleOfA), throwsStateError);
315+
expect(() => $A.firstAnnotationOfExact($ExampleOfA), throwsStateError);
316+
expect(() => $A.annotationsOfExact($ExampleOfA), throwsStateError);
317+
});
318+
319+
test('should not throw if `throwOnUnresolved` == false', () {
320+
expect(
321+
$A.firstAnnotationOf($ExampleOfA, throwOnUnresolved: false).type.name,
322+
'A');
323+
expect(
324+
$A
325+
.annotationsOf($ExampleOfA, throwOnUnresolved: false)
326+
.map((a) => a.type.name),
327+
['A']);
328+
expect(
329+
$A
330+
.firstAnnotationOfExact($ExampleOfA, throwOnUnresolved: false)
331+
.type
332+
.name,
333+
'A');
334+
expect(
335+
$A
336+
.annotationsOfExact($ExampleOfA, throwOnUnresolved: false)
337+
.map((a) => a.type.name),
338+
['A']);
339+
});
340+
});
289341
}

0 commit comments

Comments
 (0)