-
Notifications
You must be signed in to change notification settings - Fork 110
Add TypeChecker class. #153
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
Changes from all commits
07ebca8
bbde90b
c11b270
da74b88
71333fb
b72548d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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._(); | ||
|
|
||
| /// 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}'; | ||
| } | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only reason I did this is to avoid creating a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh right, |
||
|
|
||
| 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')); | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 theTypeCheckerclass itself not extendable is probably an OK thing to do.There was a problem hiding this comment.
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.