| // 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 'annotated_code_helper.dart'; |
| |
| enum IdKind { |
| /// Id used for top level or class members. This is used in [MemberId]. |
| member, |
| |
| /// Id used for classes. This is used in [ClassId]. |
| cls, |
| |
| /// Id used for libraries. This is used in [LibraryId]. |
| library, |
| |
| /// Id used for a code point at certain offset. The id represents the default |
| /// use of the code point, often a read access. This is used in [NodeId]. |
| node, |
| |
| /// Id used for an invocation at certain offset. This is used in [NodeId]. |
| invoke, |
| |
| /// Id used for an assignment at certain offset. This is used in [NodeId]. |
| update, |
| |
| /// Id used for the iterator expression of a for-in at certain offset. This is |
| /// used in [NodeId]. |
| iterator, |
| |
| /// Id used for the implicit call to `Iterator.current` in a for-in at certain |
| /// offset. This is used in [NodeId]. |
| current, |
| |
| /// Id used for the implicit call to `Iterator.moveNext` in a for-in at |
| /// certain offset. This is used in [NodeId]. |
| moveNext, |
| |
| /// Id used for the implicit as expression inserted by the compiler. |
| implicitAs, |
| |
| /// Id used for the statement at certain offset. This is used in [NodeId]. |
| stmt, |
| |
| /// Id used for the error reported at certain offset. This is used in |
| /// [NodeId]. |
| error, |
| } |
| |
| /// Id for a code point or element. |
| abstract class Id { |
| IdKind get kind; |
| |
| /// Indicates whether the id refers to an element defined outside of the test |
| /// case itself (e.g. some tests may need to refer to properties of elements |
| /// in `dart:core`). |
| bool get isGlobal; |
| |
| /// Display name for this id. |
| String get descriptor; |
| } |
| |
| class IdValue { |
| final Id id; |
| final Annotation annotation; |
| final String value; |
| |
| const IdValue(this.id, this.annotation, this.value); |
| |
| @override |
| int get hashCode => id.hashCode * 13 + value.hashCode * 17; |
| |
| @override |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! IdValue) return false; |
| return id == other.id && value == other.value; |
| } |
| |
| @override |
| String toString() => idToString(id, value); |
| |
| static String idToString(Id id, String value) { |
| switch (id.kind) { |
| case IdKind.member: |
| MemberId elementId = id as MemberId; |
| return '$memberPrefix${elementId.name}:$value'; |
| case IdKind.cls: |
| ClassId classId = id as ClassId; |
| return '$classPrefix${classId.name}:$value'; |
| case IdKind.library: |
| return '$libraryPrefix$value'; |
| case IdKind.node: |
| return value; |
| case IdKind.invoke: |
| return '$invokePrefix$value'; |
| case IdKind.update: |
| return '$updatePrefix$value'; |
| case IdKind.iterator: |
| return '$iteratorPrefix$value'; |
| case IdKind.current: |
| return '$currentPrefix$value'; |
| case IdKind.moveNext: |
| return '$moveNextPrefix$value'; |
| case IdKind.implicitAs: |
| return '$implicitAsPrefix$value'; |
| case IdKind.stmt: |
| return '$stmtPrefix$value'; |
| case IdKind.error: |
| return '$errorPrefix$value'; |
| } |
| } |
| |
| static const String globalPrefix = "global#"; |
| static const String memberPrefix = "member: "; |
| static const String classPrefix = "class: "; |
| static const String libraryPrefix = "library: "; |
| static const String invokePrefix = "invoke: "; |
| static const String updatePrefix = "update: "; |
| static const String iteratorPrefix = "iterator: "; |
| static const String currentPrefix = "current: "; |
| static const String moveNextPrefix = "moveNext: "; |
| static const String implicitAsPrefix = "as: "; |
| static const String stmtPrefix = "stmt: "; |
| static const String errorPrefix = "error: "; |
| |
| static IdValue decode(Uri sourceUri, Annotation annotation, String text, |
| {bool preserveWhitespaceInAnnotations: false, |
| bool preserveInfixWhitespace: false}) { |
| int offset = annotation.offset; |
| Id id; |
| String expected; |
| if (text.startsWith(memberPrefix)) { |
| text = text.substring(memberPrefix.length); |
| int colonPos = text.indexOf(':'); |
| if (colonPos == -1) throw "Invalid member id: '$text'"; |
| String name = text.substring(0, colonPos); |
| bool isGlobal = name.startsWith(globalPrefix); |
| if (isGlobal) { |
| name = name.substring(globalPrefix.length); |
| } |
| id = new MemberId(name, isGlobal: isGlobal); |
| expected = text.substring(colonPos + 1); |
| } else if (text.startsWith(classPrefix)) { |
| text = text.substring(classPrefix.length); |
| int colonPos = text.indexOf(':'); |
| if (colonPos == -1) throw "Invalid class id: '$text'"; |
| String name = text.substring(0, colonPos); |
| bool isGlobal = name.startsWith(globalPrefix); |
| if (isGlobal) { |
| name = name.substring(globalPrefix.length); |
| } |
| id = new ClassId(name, isGlobal: isGlobal); |
| expected = text.substring(colonPos + 1); |
| } else if (text.startsWith(libraryPrefix)) { |
| id = new LibraryId(sourceUri); |
| expected = text.substring(libraryPrefix.length); |
| } else if (text.startsWith(invokePrefix)) { |
| id = new NodeId(offset, IdKind.invoke); |
| expected = text.substring(invokePrefix.length); |
| } else if (text.startsWith(updatePrefix)) { |
| id = new NodeId(offset, IdKind.update); |
| expected = text.substring(updatePrefix.length); |
| } else if (text.startsWith(iteratorPrefix)) { |
| id = new NodeId(offset, IdKind.iterator); |
| expected = text.substring(iteratorPrefix.length); |
| } else if (text.startsWith(currentPrefix)) { |
| id = new NodeId(offset, IdKind.current); |
| expected = text.substring(currentPrefix.length); |
| } else if (text.startsWith(moveNextPrefix)) { |
| id = new NodeId(offset, IdKind.moveNext); |
| expected = text.substring(moveNextPrefix.length); |
| } else if (text.startsWith(implicitAsPrefix)) { |
| id = new NodeId(offset, IdKind.implicitAs); |
| expected = text.substring(implicitAsPrefix.length); |
| } else if (text.startsWith(stmtPrefix)) { |
| id = new NodeId(offset, IdKind.stmt); |
| expected = text.substring(stmtPrefix.length); |
| } else if (text.startsWith(errorPrefix)) { |
| id = new NodeId(offset, IdKind.error); |
| expected = text.substring(errorPrefix.length); |
| } else { |
| id = new NodeId(offset, IdKind.node); |
| expected = text; |
| } |
| if (preserveWhitespaceInAnnotations) { |
| // Keep all whitespace. |
| } else if (preserveInfixWhitespace) { |
| // Remove heading and trailing whitespace. |
| expected = expected.trim(); |
| } else { |
| // Remove unneeded whitespace. |
| expected = expected.replaceAll(new RegExp(r'\s*(\n\s*)+\s*'), ''); |
| } |
| return new IdValue(id, annotation, expected); |
| } |
| } |
| |
| /// Id for an member element. |
| class MemberId implements Id { |
| final String? className; |
| final String memberName; |
| @override |
| final bool isGlobal; |
| |
| factory MemberId(String text, {bool isGlobal: false}) { |
| int dotPos = text.indexOf('.'); |
| if (dotPos != -1) { |
| return new MemberId.internal(text.substring(dotPos + 1), |
| className: text.substring(0, dotPos), isGlobal: isGlobal); |
| } else { |
| return new MemberId.internal(text, isGlobal: isGlobal); |
| } |
| } |
| |
| MemberId.internal(this.memberName, {this.className, this.isGlobal: false}); |
| |
| @override |
| int get hashCode => className.hashCode * 13 + memberName.hashCode * 17; |
| |
| @override |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! MemberId) return false; |
| return className == other.className && memberName == other.memberName; |
| } |
| |
| @override |
| IdKind get kind => IdKind.member; |
| |
| String get name => className != null ? '$className.$memberName' : memberName; |
| |
| @override |
| String get descriptor => 'member $name'; |
| |
| @override |
| String toString() => 'member:$name'; |
| } |
| |
| /// Id for a class. |
| class ClassId implements Id { |
| final String className; |
| @override |
| final bool isGlobal; |
| |
| ClassId(this.className, {this.isGlobal: false}); |
| |
| @override |
| int get hashCode => className.hashCode * 13; |
| |
| @override |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! ClassId) return false; |
| return className == other.className; |
| } |
| |
| @override |
| IdKind get kind => IdKind.cls; |
| |
| String get name => className; |
| |
| @override |
| String get descriptor => 'class $name'; |
| |
| @override |
| String toString() => 'class:$name'; |
| } |
| |
| /// Id for a library. |
| class LibraryId implements Id { |
| final Uri uri; |
| |
| LibraryId(this.uri); |
| |
| // TODO(johnniwinther): Support global library annotations. |
| @override |
| bool get isGlobal => false; |
| |
| @override |
| int get hashCode => uri.hashCode * 13; |
| |
| @override |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! LibraryId) return false; |
| return uri == other.uri; |
| } |
| |
| @override |
| IdKind get kind => IdKind.library; |
| |
| String get name => uri.toString(); |
| |
| @override |
| String get descriptor => 'library $name'; |
| |
| @override |
| String toString() => 'library:$name'; |
| } |
| |
| /// Id for a code point defined by a kind and a code offset. |
| class NodeId implements Id { |
| final int value; |
| @override |
| final IdKind kind; |
| |
| const NodeId(this.value, this.kind) |
| : |
| // ignore: unnecessary_null_comparison |
| assert(value != null), |
| assert(value >= 0); |
| |
| @override |
| bool get isGlobal => false; |
| |
| @override |
| int get hashCode => value.hashCode * 13 + kind.hashCode * 17; |
| |
| @override |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! NodeId) return false; |
| return value == other.value && kind == other.kind; |
| } |
| |
| @override |
| String get descriptor => 'offset $value ($kind)'; |
| |
| @override |
| String toString() => '$kind:$value'; |
| } |
| |
| class ActualData<T> { |
| final Id id; |
| final T value; |
| final Uri uri; |
| final int _offset; |
| final Object object; |
| |
| ActualData(this.id, this.value, this.uri, this._offset, this.object); |
| |
| int get offset { |
| if (id is NodeId) { |
| NodeId nodeId = id as NodeId; |
| return nodeId.value; |
| } else { |
| return _offset; |
| } |
| } |
| |
| String get objectText { |
| return 'object `${'$object'.replaceAll('\n', '')}` (${object.runtimeType})'; |
| } |
| |
| @override |
| String toString() => 'ActualData(id=$id,value=$value,uri=$uri,' |
| 'offset=$offset,object=$objectText)'; |
| } |
| |
| abstract class DataRegistry<T> { |
| Map<Id, ActualData<T>> get actualMap; |
| |
| /// Registers [value] with [id] in [actualMap]. |
| /// |
| /// Checks for duplicate data for [id]. |
| void registerValue(Uri uri, int offset, Id id, T? value, Object object) { |
| if (value != null) { |
| ActualData<T> newData = new ActualData<T>(id, value, uri, offset, object); |
| if (actualMap.containsKey(id)) { |
| ActualData<T> existingData = actualMap[id]!; |
| ActualData<T>? mergedData = mergeData(existingData, newData); |
| if (mergedData != null) { |
| actualMap[id] = mergedData; |
| } else { |
| report( |
| uri, |
| offset, |
| "Duplicate id ${id}, value=$value, object=$object " |
| "(${object.runtimeType})"); |
| report( |
| uri, |
| offset, |
| "Duplicate id ${id}, value=${existingData.value}, " |
| "object=${existingData.object} " |
| "(${existingData.object.runtimeType})"); |
| fail("Duplicate id $id."); |
| } |
| } else { |
| actualMap[id] = newData; |
| } |
| } |
| } |
| |
| ActualData<T>? mergeData(ActualData<T> value1, ActualData<T> value2) => null; |
| |
| /// Called to report duplicate errors. |
| void report(Uri uri, int offset, String message); |
| |
| /// Called to raise an exception on duplicate errors. |
| void fail(String message); |
| } |