Skip to content
Merged
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
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
## 0.5.9

* Update the minimum Dart SDK to `1.22.1`.
* Added `TypeChecker`, a high-level API for performing static type checks:

```dart
import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart';

void checkType(DartType dartType) {
// Checks compared to runtime type `SomeClass`.
print(const TypeChecker.forRuntime(SomeClass).isExactlyType(dartType));

// Checks compared to a known Url/Symbol:
const TypeChecker.forUrl('package:foo/foo.dart#SomeClass');

// Checks compared to another resolved `DartType`:
const TypeChecker.forStatic(anotherDartType);
}
```

## 0.5.8

* Add `formatOutput` optional parameter to the `GeneratorBuilder` constructor.
This is a lamda of the form `String formatOutput(String originalCode)` which
This is a lambda of the form `String formatOutput(String originalCode)` which
allows you do do custom formatting.

## 0.5.7
Expand Down
16 changes: 5 additions & 11 deletions lib/generators/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_gen/src/annotation.dart';
import 'package:source_gen/src/json_serializable/type_helper.dart';
import 'package:source_gen/src/utils.dart';

Expand Down Expand Up @@ -278,11 +277,10 @@ class JsonSerializableGenerator
/// [fieldName] is used, unless [field] is annotated with [JsonKey], in which
/// case [JsonKey.jsonName] is used.
String _fieldToJsonMapKey(String fieldName, FieldElement field) {
var metadata = field.metadata;
var jsonKey = metadata.firstWhere((m) => matchAnnotation(JsonKey, m),
orElse: () => null);
const $JsonKey = const TypeChecker.fromRuntime(JsonKey);
var jsonKey = $JsonKey.firstAnnotationOf(field);
if (jsonKey != null) {
var jsonName = jsonKey.constantValue.getField('jsonName').toStringValue();
var jsonName = jsonKey.getField('jsonName').toStringValue();
return jsonName;
}
return fieldName;
Expand Down Expand Up @@ -316,11 +314,7 @@ T _firstNotNull<T>(Iterable<T> values) =>
values.firstWhere((value) => value != null, orElse: () => null);

bool _isDartIterable(DartType type) =>
type.element.library != null &&
type.element.library.isDartCore &&
type.name == 'Iterable';
const TypeChecker.fromUrl('dart:core#Iterable').isExactlyType(type);

bool _isDartList(DartType type) =>
type.element.library != null &&
type.element.library.isDartCore &&
type.name == 'List';
const TypeChecker.fromUrl('dart:core#List').isExactlyType(type);
1 change: 1 addition & 0 deletions lib/source_gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ library source_gen;
export 'src/builder.dart';
export 'src/generator.dart';
export 'src/generator_for_annotation.dart';
export 'src/type_checker.dart' show TypeChecker;
174 changes: 174 additions & 0 deletions lib/src/type_checker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// 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.

import 'dart:mirrors';

import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

/// An abstraction around doing static type checking at compile/build time.
abstract class TypeChecker {
const TypeChecker._();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this constructor? You should be able to delete it and remove the super constructor calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could remove it, but then the other classes couldn't be const. Since I wanted that anyway, I figured making the TypeChecker class itself not extendable is probably an OK thing to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok just for the const, makes sense. We can always make this public later if somebody actually has a valid reason to extend it and files an issue.


/// Create a new [TypeChecker] backed by a runtime [type].
///
/// This implementation uses `dart:mirrors` (runtime reflection).
const factory TypeChecker.fromRuntime(Type type) = _MirrorTypeChecker;

/// Create a new [TypeChecker] backed by a static [type].
const factory TypeChecker.fromStatic(DartType type) = _LibraryTypeChecker;

/// Create a new [TypeChecker] backed by a library [url].
///
/// Example of referring to a `LinkedHashMap` from `dart:collection`:
/// ```dart
/// const linkedHashMap = const TypeChecker.fromUrl(
/// 'dart:collection#LinkedHashMap',
/// );
/// ```
const factory TypeChecker.fromUrl(dynamic url) = _UriTypeChecker;

/// Returns the first constant annotating [element] that is this type.
///
/// Otherwise returns `null`.
DartObject firstAnnotationOf(Element element) {
if (element.metadata.isEmpty) {
return null;
}
final results = annotationsOf(element);
return results.isEmpty ? null : results.first;
}

/// Returns every constant annotating [element] that is this type.
Iterable<DartObject> annotationsOf(Element element) => element.metadata
.map((a) => a.computeConstantValue())
.where((a) => isExactlyType(a.type));

/// Returns `true` if representing the exact same class as [element].
bool isExactly(Element element);

/// Returns `true` if representing the exact same type as [staticType].
bool isExactlyType(DartType staticType) => isExactly(staticType.element);

/// Returns `true` if representing a super class of [element].
bool isSuperOf(Element element) =>
element is ClassElement && element.allSupertypes.any(isExactlyType);

/// Returns `true` if representing a super type of [staticType].
bool isSuperTypeOf(DartType staticType) => isSuperOf(staticType.element);
}

Uri _normalizeUrl(Uri url) {
switch (url.scheme) {
case 'dart':
return _normalizeDartUrl(url);
case 'package':
return _packageToAssetUrl(url);
default:
return url;
}
}

/// Make `dart:`-type URLs look like a user-knowable path.
///
/// Some internal dart: URLs are something like `dart:core/map.dart`.
///
/// This isn't a user-knowable path, so we strip out extra path segments
/// and only expose `dart:core`.
Uri _normalizeDartUrl(Uri url) => url.pathSegments.isNotEmpty
? url.replace(pathSegments: url.pathSegments.take(1))
: url;

/// Returns a `package:` URL into a `asset:` URL.
///
/// This makes internal comparison logic much easier, but still allows users
/// to define assets in terms of `package:`, which is something that makes more
/// sense to most.
///
/// For example this transforms `package:source_gen/source_gen.dart` into:
/// `asset:source_gen/lib/source_gen.dart`.
Uri _packageToAssetUrl(Uri url) => url.scheme == 'package'
? url.replace(
scheme: 'asset',
pathSegments: <String>[]
..add(url.pathSegments.first)
..add('lib')
..addAll(url.pathSegments.skip(1)))
: url;

/// Returns
String _urlOfElement(Element element) => element.kind == ElementKind.DYNAMIC
? 'dart:core#dynmaic'
: _normalizeUrl(element.source.uri)
.replace(fragment: element.name)
.toString();

// Checks a static type against another static type;
class _LibraryTypeChecker extends TypeChecker {
final DartType _type;

const _LibraryTypeChecker(this._type) : super._();

@override
bool isExactly(Element element) =>
element is ClassElement && element == _type.element;

@override
String toString() => '${_urlOfElement(_type.element)}';
}

// Checks a runtime type against a static type.
class _MirrorTypeChecker extends TypeChecker {
static Uri _uriOf(ClassMirror mirror) =>
_normalizeUrl((mirror.owner as LibraryMirror).uri)
.replace(fragment: MirrorSystem.getName(mirror.simpleName));

// Precomputed type checker for types that already have been used.
static final _cache = new Expando<TypeChecker>();

final Type _type;

const _MirrorTypeChecker(this._type) : super._();

TypeChecker get _computed =>
_cache[this] ??= new TypeChecker.fromUrl(_uriOf(reflectClass(_type)));

@override
bool isExactly(Element element) => _computed.isExactly(element);

@override
String toString() => _computed.toString();
}

// Checks a runtime type against an Uri and Symbol.
class _UriTypeChecker extends TypeChecker {
final String _url;

// Precomputed cache of String --> Uri.
static final _cache = new Expando<Uri>();

const _UriTypeChecker(dynamic url)
: _url = '$url',
super._();

@override
bool operator ==(Object o) => o is _UriTypeChecker && o._url == _url;

@override
int get hashCode => _url.hashCode;

/// Url as a [Uri] object, lazily constructed.
Uri get uri => _cache[this] ??= Uri.parse(_url);

/// Returns whether this type represents the same as [url].
bool hasSameUrl(dynamic url) =>
uri.toString() == (url is String ? url : _normalizeUrl(url).toString());

@override
bool isExactly(Element element) => hasSameUrl(_urlOfElement(element));

@override
String toString() => '${uri}';
}
87 changes: 87 additions & 0 deletions test/type_checker_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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.

import 'dart:collection';

import 'package:analyzer/dart/element/type.dart';
import 'package:build_test/build_test.dart';
import 'package:meta/meta.dart';
import 'package:source_gen/source_gen.dart';
import 'package:test/test.dart';

void main() {
// Resolved top-level types from dart:core and dart:collection.
DartType staticMap;
DartType staticHashMap;
TypeChecker staticMapChecker;
TypeChecker staticHashMapChecker;

setUpAll(() async {
final resolver = await resolveSource('');

final core = resolver.getLibraryByName('dart.core');
staticMap = core.getType('Map').type;
staticMapChecker = new TypeChecker.fromStatic(staticMap);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well just create this inline in the test that uses them below like the others instead of creating them for all tests (same for staticHashMapChecker).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I did this is to avoid creating a Resolver multiple times per test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right, setUpAll just runs once doesn't it, sgtm


final collection = resolver.getLibraryByName('dart.collection');
staticHashMap = collection.getType('HashMap').type;
staticHashMapChecker = new TypeChecker.fromStatic(staticHashMap);
});

// Run a common set of type comparison checks with various implementations.
void commonTests({
@required TypeChecker checkMap(),
@required TypeChecker checkHashMap(),
}) {
group('(Map)', () {
test('should equal dart:core#Map', () {
expect(checkMap().isExactlyType(staticMap), isTrue,
reason: '${checkMap()} != $staticMap');
});

test('should not be a super type of dart:core#Map', () {
expect(checkMap().isSuperTypeOf(staticMap), isFalse);
});

test('should not equal dart:core#HashMap', () {
expect(checkMap().isExactlyType(staticHashMap), isFalse,
reason: '${checkMap()} == $staticHashMapChecker');
});

test('should be a super type of dart:collection#HashMap', () {
expect(checkMap().isSuperTypeOf(staticHashMap), isTrue);
});
});

group('(HashMap)', () {
test('should equal dart:collection#HashMap', () {
expect(checkHashMap().isExactlyType(staticHashMap), isTrue,
reason: '${checkHashMap()} != $staticHashMapChecker');
});

test('should not be a super type of dart:core#Map', () {
expect(checkHashMap().isSuperTypeOf(staticMap), isFalse);
});
});
}

group('TypeChecker.forRuntime', () {
commonTests(
checkMap: () => const TypeChecker.fromRuntime(Map),
checkHashMap: () => const TypeChecker.fromRuntime(HashMap));
});

group('TypeChecker.forStatic', () {
commonTests(
checkMap: () => staticMapChecker,
checkHashMap: () => staticHashMapChecker);
});

group('TypeChecker.fromUrl', () {
commonTests(
checkMap: () => const TypeChecker.fromUrl('dart:core#Map'),
checkHashMap: () =>
const TypeChecker.fromUrl('dart:collection#HashMap'));
});
}