| // 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' hide SourceLocation; |
| |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/constant/value.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:build/build.dart'; |
| import 'package:source_span/source_span.dart'; |
| |
| import 'utils.dart'; |
| |
| /// An abstraction around doing static type checking at compile/build time. |
| abstract class TypeChecker { |
| const TypeChecker._(); |
| |
| /// Creates a new [TypeChecker] that delegates to other [checkers]. |
| /// |
| /// This implementation will return `true` for type checks if _any_ of the |
| /// provided type checkers return true, which is useful for deprecating an |
| /// API: |
| /// ```dart |
| /// const $Foo = const TypeChecker.fromRuntime(Foo); |
| /// const $Bar = const TypeChecker.fromRuntime(Bar); |
| /// |
| /// // Used until $Foo is deleted. |
| /// const $FooOrBar = const TypeChecker.forAny(const [$Foo, $Bar]); |
| /// ``` |
| const factory TypeChecker.any(Iterable<TypeChecker> checkers) = _AnyChecker; |
| |
| /// 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', |
| /// ); |
| /// ``` |
| /// |
| /// **NOTE**: This is considered a more _brittle_ way of determining the type |
| /// because it relies on knowing the _absolute_ path (i.e. after resolved |
| /// `export` directives). You should ideally only use `fromUrl` when you know |
| /// the full path (likely you own/control the package) or it is in a stable |
| /// package like in the `dart:` SDK. |
| const factory TypeChecker.fromUrl(dynamic url) = _UriTypeChecker; |
| |
| /// Returns the first constant annotating [element] assignable to this type. |
| /// |
| /// Otherwise returns `null`. |
| /// |
| /// Throws on unresolved annotations unless [throwOnUnresolved] is `false`. |
| DartObject? firstAnnotationOf( |
| Element element, { |
| bool throwOnUnresolved = true, |
| }) { |
| if (element.metadata.isEmpty) { |
| return null; |
| } |
| final results = |
| annotationsOf(element, throwOnUnresolved: throwOnUnresolved); |
| return results.isEmpty ? null : results.first; |
| } |
| |
| /// Returns if a constant annotating [element] is assignable to this type. |
| /// |
| /// Throws on unresolved annotations unless [throwOnUnresolved] is `false`. |
| bool hasAnnotationOf(Element element, {bool throwOnUnresolved = true}) => |
| firstAnnotationOf(element, throwOnUnresolved: throwOnUnresolved) != null; |
| |
| /// Returns the first constant annotating [element] that is exactly this type. |
| /// |
| /// Throws [UnresolvedAnnotationException] on unresolved annotations unless |
| /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). |
| DartObject? firstAnnotationOfExact( |
| Element element, { |
| bool throwOnUnresolved = true, |
| }) { |
| if (element.metadata.isEmpty) { |
| return null; |
| } |
| final results = |
| annotationsOfExact(element, throwOnUnresolved: throwOnUnresolved); |
| return results.isEmpty ? null : results.first; |
| } |
| |
| /// Returns if a constant annotating [element] is exactly this type. |
| /// |
| /// Throws [UnresolvedAnnotationException] on unresolved annotations unless |
| /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). |
| bool hasAnnotationOfExact(Element element, {bool throwOnUnresolved = true}) => |
| firstAnnotationOfExact(element, throwOnUnresolved: throwOnUnresolved) != |
| null; |
| |
| DartObject? _computeConstantValue( |
| Element element, |
| int annotationIndex, { |
| bool throwOnUnresolved = true, |
| }) { |
| final annotation = element.metadata[annotationIndex]; |
| final result = annotation.computeConstantValue(); |
| if (result == null && throwOnUnresolved) { |
| throw UnresolvedAnnotationException._from(element, annotationIndex); |
| } |
| return result; |
| } |
| |
| /// Returns annotating constants on [element] assignable to this type. |
| /// |
| /// Throws [UnresolvedAnnotationException] on unresolved annotations unless |
| /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). |
| Iterable<DartObject> annotationsOf( |
| Element element, { |
| bool throwOnUnresolved = true, |
| }) => |
| _annotationsWhere( |
| element, |
| isAssignableFromType, |
| throwOnUnresolved: throwOnUnresolved, |
| ); |
| |
| Iterable<DartObject> _annotationsWhere( |
| Element element, |
| bool Function(DartType) predicate, { |
| bool throwOnUnresolved = true, |
| }) sync* { |
| for (var i = 0; i < element.metadata.length; i++) { |
| final value = _computeConstantValue( |
| element, |
| i, |
| throwOnUnresolved: throwOnUnresolved, |
| ); |
| if (value?.type != null && predicate(value!.type!)) { |
| yield value; |
| } |
| } |
| } |
| |
| /// Returns annotating constants on [element] of exactly this type. |
| /// |
| /// Throws [UnresolvedAnnotationException] on unresolved annotations unless |
| /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). |
| Iterable<DartObject> annotationsOfExact( |
| Element element, { |
| bool throwOnUnresolved = true, |
| }) => |
| _annotationsWhere( |
| element, |
| isExactlyType, |
| throwOnUnresolved: throwOnUnresolved, |
| ); |
| |
| /// Returns `true` if the type of [element] can be assigned to this type. |
| bool isAssignableFrom(Element element) => |
| isExactly(element) || |
| (element is InterfaceElement && element.allSupertypes.any(isExactlyType)); |
| |
| /// Returns `true` if [staticType] can be assigned to this type. |
| bool isAssignableFromType(DartType staticType) { |
| final element = staticType.element; |
| return element != null && isAssignableFrom(element); |
| } |
| |
| /// Returns `true` if representing the exact same class as [element]. |
| bool isExactly(Element element); |
| |
| /// Returns `true` if representing the exact same type as [staticType]. |
| /// |
| /// This will always return false for types without a backingclass such as |
| /// `void` or function types. |
| bool isExactlyType(DartType staticType) { |
| final element = staticType.element; |
| if (element != null) { |
| return isExactly(element); |
| } else { |
| return false; |
| } |
| } |
| |
| /// Returns `true` if representing a super class of [element]. |
| /// |
| /// This check only takes into account the *extends* hierarchy. If you wish |
| /// to check mixins and interfaces, use [isAssignableFrom]. |
| bool isSuperOf(Element element) { |
| if (element is InterfaceElement) { |
| var theSuper = element.supertype; |
| |
| do { |
| if (isExactlyType(theSuper!)) { |
| return true; |
| } |
| |
| theSuper = theSuper.superclass; |
| } while (theSuper != null); |
| } |
| |
| return false; |
| } |
| |
| /// Returns `true` if representing a super type of [staticType]. |
| /// |
| /// This only takes into account the *extends* hierarchy. If you wish |
| /// to check mixins and interfaces, use [isAssignableFromType]. |
| bool isSuperTypeOf(DartType staticType) => isSuperOf(staticType.element!); |
| } |
| |
| // 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 InterfaceElement && 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 = Expando<TypeChecker>(); |
| |
| final Type _type; |
| |
| const _MirrorTypeChecker(this._type) : super._(); |
| |
| TypeChecker get _computed => |
| _cache[this] ??= 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 = 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] ??= normalizeUrl(Uri.parse(_url)); |
| |
| /// Returns whether this type represents the same as [url]. |
| bool hasSameUrl(dynamic url) => |
| uri.toString() == |
| (url is String ? url : normalizeUrl(url as Uri).toString()); |
| |
| @override |
| bool isExactly(Element element) => hasSameUrl(urlOfElement(element)); |
| |
| @override |
| String toString() => '$uri'; |
| } |
| |
| class _AnyChecker extends TypeChecker { |
| final Iterable<TypeChecker> _checkers; |
| |
| const _AnyChecker(this._checkers) : super._(); |
| |
| @override |
| bool isExactly(Element element) => _checkers.any((c) => c.isExactly(element)); |
| } |
| |
| /// Exception thrown when [TypeChecker] fails to resolve a metadata annotation. |
| /// |
| /// Methods such as [TypeChecker.firstAnnotationOf] may throw this exception |
| /// when one or more annotations are not resolvable. This is usually a sign that |
| /// something was misspelled, an import is missing, or a dependency was not |
| /// defined (for build systems such as Bazel). |
| class UnresolvedAnnotationException implements Exception { |
| /// Element that was annotated with something we could not resolve. |
| final Element annotatedElement; |
| |
| /// Source span of the annotation that was not resolved. |
| /// |
| /// May be `null` if the import library was not found. |
| final SourceSpan? annotationSource; |
| |
| static SourceSpan? _findSpan( |
| Element annotatedElement, |
| int annotationIndex, |
| ) { |
| try { |
| final parsedLibrary = annotatedElement.session! |
| .getParsedLibraryByElement(annotatedElement.library!) |
| as ParsedLibraryResult; |
| final declaration = parsedLibrary.getElementDeclaration(annotatedElement); |
| if (declaration == null) { |
| return null; |
| } |
| final node = declaration.node; |
| final List<Annotation> metadata; |
| if (node is AnnotatedNode) { |
| metadata = node.metadata; |
| } else if (node is FormalParameter) { |
| metadata = node.metadata; |
| } else { |
| throw StateError( |
| 'Unhandled Annotated AST node type: ${node.runtimeType}', |
| ); |
| } |
| final annotation = metadata[annotationIndex]; |
| final start = annotation.offset; |
| final end = start + annotation.length; |
| final parsedUnit = declaration.parsedUnit!; |
| return SourceSpan( |
| SourceLocation(start, sourceUrl: parsedUnit.uri), |
| SourceLocation(end, sourceUrl: parsedUnit.uri), |
| parsedUnit.content.substring(start, end), |
| ); |
| } catch (e, stack) { |
| // Trying to get more information on https://github.com/dart-lang/sdk/issues/45127 |
| log.warning( |
| ''' |
| An unexpected error was thrown trying to get location information on `$annotatedElement` (${annotatedElement.runtimeType}). |
| |
| Please file an issue at https://github.com/dart-lang/source_gen/issues/new |
| Include the contents of this warning and the stack trace along with |
| the version of `package:source_gen`, `package:analyzer` from `pubspec.lock`. |
| ''', |
| e, |
| stack, |
| ); |
| return null; |
| } |
| } |
| |
| /// Creates an exception from an annotation ([annotationIndex]) that was not |
| /// resolvable while traversing [Element.metadata] on [annotatedElement]. |
| factory UnresolvedAnnotationException._from( |
| Element annotatedElement, |
| int annotationIndex, |
| ) { |
| final sourceSpan = _findSpan(annotatedElement, annotationIndex); |
| return UnresolvedAnnotationException._(annotatedElement, sourceSpan); |
| } |
| |
| const UnresolvedAnnotationException._( |
| this.annotatedElement, |
| this.annotationSource, |
| ); |
| |
| @override |
| String toString() { |
| final message = 'Could not resolve annotation for `$annotatedElement`.'; |
| if (annotationSource != null) { |
| return annotationSource!.message(message); |
| } |
| return message; |
| } |
| } |