diff --git a/CHANGELOG.md b/CHANGELOG.md index e427857e..45800f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/generators/json_serializable_generator.dart b/lib/generators/json_serializable_generator.dart index 057dcd0b..36f7a18c 100644 --- a/lib/generators/json_serializable_generator.dart +++ b/lib/generators/json_serializable_generator.dart @@ -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'; @@ -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; @@ -316,11 +314,7 @@ T _firstNotNull(Iterable 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); diff --git a/lib/source_gen.dart b/lib/source_gen.dart index 0019fc43..dcc3ceba 100644 --- a/lib/source_gen.dart +++ b/lib/source_gen.dart @@ -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; diff --git a/lib/src/type_checker.dart b/lib/src/type_checker.dart new file mode 100644 index 00000000..794ddbde --- /dev/null +++ b/lib/src/type_checker.dart @@ -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._(); + + /// 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 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: [] + ..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(); + + 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(); + + 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}'; +} diff --git a/test/type_checker_test.dart b/test/type_checker_test.dart new file mode 100644 index 00000000..e5c95364 --- /dev/null +++ b/test/type_checker_test.dart @@ -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); + + 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')); + }); +}