| // Copyright (c) 2020, 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 'package:_fe_analyzer_shared/src/testing/annotated_code_helper.dart'; | |
| import 'package:_fe_analyzer_shared/src/testing/id.dart'; | |
| import 'package:_fe_analyzer_shared/src/testing/id_testing.dart'; | |
| Map<Uri, List<Annotation>> computeAnnotationsPerUri<T>( | |
| Map<Uri, AnnotatedCode> annotatedCode, | |
| Map<String, MemberAnnotations<IdValue>> expectedMaps, | |
| Uri mainUri, | |
| Map<String, Map<Uri, Map<Id, ActualData<T>>>> actualData, | |
| DataInterpreter<T> dataInterpreter, { | |
| Annotation? Function(Annotation? expected, Annotation? actual)? createDiff, | |
| bool forceUpdate = false, | |
| }) { | |
| Set<Uri> uriSet = {}; | |
| Set<String> actualMarkers = actualData.keys.toSet(); | |
| Map<Uri, Map<Id, Map<String, IdValue>>> idValuePerUri = {}; | |
| Map<Uri, Map<Id, Map<String, ActualData<T>>>> actualDataPerUri = {}; | |
| void addData(String marker, Uri? uri, Map<Id, IdValue> data) { | |
| if (uri == null) { | |
| // TODO(johnniwinther): Avoid `null` URIs. | |
| assert(data.isEmpty, "Non-empty data without uri: $data"); | |
| return; | |
| } | |
| uriSet.add(uri); | |
| Map<Id, Map<String, IdValue>> idValuePerId = idValuePerUri[uri] ??= {}; | |
| data.forEach((Id id, IdValue value) { | |
| Map<String, IdValue> idValuePerMarker = idValuePerId[id] ??= {}; | |
| idValuePerMarker[marker] = value; | |
| }); | |
| } | |
| expectedMaps.forEach((String marker, MemberAnnotations<IdValue> annotations) { | |
| annotations.forEach((Uri uri, Map<Id, IdValue> data) { | |
| addData(marker, uri, data); | |
| }); | |
| addData(marker, mainUri, annotations.globalData); | |
| }); | |
| actualData.forEach(( | |
| String marker, | |
| Map<Uri, Map<Id, ActualData<T>>> dataPerUri, | |
| ) { | |
| dataPerUri.forEach((Uri uri, Map<Id, ActualData<T>> dataMap) { | |
| uriSet.add(uri); | |
| dataMap.forEach((Id id, ActualData<T> data) { | |
| Map<Id, Map<String, ActualData<T>>> actualDataPerId = | |
| actualDataPerUri[uri] ??= {}; | |
| Map<String, ActualData<T>> actualDataPerMarker = actualDataPerId[id] ??= | |
| {}; | |
| actualDataPerMarker[marker] = data; | |
| }); | |
| }); | |
| }); | |
| Map<Uri, List<Annotation>> result = {}; | |
| for (Uri uri in uriSet) { | |
| Map<Id, Map<String, IdValue>> idValuePerId = idValuePerUri[uri] ?? {}; | |
| Map<Id, Map<String, ActualData<T>>> actualDataPerId = | |
| actualDataPerUri[uri] ?? {}; | |
| AnnotatedCode? code = annotatedCode[uri]; | |
| if (code != null) { | |
| // Annotations are not computed from synthesized code. | |
| result[uri] = _computeAnnotations( | |
| code, | |
| expectedMaps.keys, | |
| actualMarkers, | |
| idValuePerId, | |
| actualDataPerId, | |
| dataInterpreter, | |
| sortMarkers: false, | |
| createDiff: createDiff, | |
| forceUpdate: forceUpdate, | |
| ); | |
| } | |
| } | |
| return result; | |
| } | |
| List<Annotation> _computeAnnotations<T>( | |
| AnnotatedCode annotatedCode, | |
| Iterable<String> supportedMarkers, | |
| Set<String> actualMarkers, | |
| Map<Id, Map<String, IdValue>> idValuePerId, | |
| Map<Id, Map<String, ActualData<T>>> actualDataPerId, | |
| DataInterpreter<T> dataInterpreter, { | |
| String defaultPrefix = '/*', | |
| String defaultSuffix = '*/', | |
| bool sortMarkers = true, | |
| Annotation? Function(Annotation? expected, Annotation? actual)? createDiff, | |
| bool forceUpdate = false, | |
| }) { | |
| Annotation createAnnotationFromData( | |
| ActualData<T> actualData, | |
| Annotation? annotation, | |
| ) { | |
| String getIndentationFromOffset(int offset) { | |
| int lineIndex = annotatedCode.getLineIndex(offset); | |
| String line = annotatedCode.getLine(lineIndex); | |
| String trimmed = line.trimLeft(); | |
| return line.substring(0, line.length - trimmed.length); | |
| } | |
| int offset; | |
| String prefix; | |
| String suffix; | |
| String indentation; | |
| if (annotation != null) { | |
| offset = annotation.offset; | |
| prefix = annotation.prefix; | |
| suffix = annotation.suffix; | |
| indentation = getIndentationFromOffset(offset); | |
| } else { | |
| Id id = actualData.id; | |
| if (id is NodeId) { | |
| offset = id.value; | |
| prefix = defaultPrefix; | |
| suffix = defaultSuffix; | |
| indentation = getIndentationFromOffset(offset); | |
| } else if (id is ClassId || id is MemberId) { | |
| // Place the annotation at the line above at the indentation level of | |
| // the class/member. | |
| int lineIndex = annotatedCode.getLineIndex(actualData.offset); | |
| String line = annotatedCode.getLine(lineIndex); | |
| String trimmed = line.trimLeft(); | |
| indentation = line.substring(0, line.length - trimmed.length); | |
| offset = annotatedCode.getLineStart(lineIndex); | |
| prefix = '$indentation$defaultPrefix'; | |
| suffix = '$defaultSuffix\n'; | |
| } else if (id is LibraryId) { | |
| // Place the annotation on its own line after the copyright comments. | |
| int lineIndex = 0; | |
| while (lineIndex < annotatedCode.lineCount) { | |
| String line = annotatedCode.getLine(lineIndex); | |
| if (!line.startsWith('//')) { | |
| break; | |
| } | |
| lineIndex++; | |
| } | |
| offset = annotatedCode.getLineStart(lineIndex); | |
| prefix = '\n$defaultPrefix'; | |
| suffix = '$defaultSuffix\n'; | |
| indentation = ''; | |
| } else { | |
| throw 'Unexpected id $id (${id.runtimeType})'; | |
| } | |
| } | |
| return new Annotation( | |
| annotation?.index, | |
| annotation?.lineNo ?? -1, | |
| annotation?.columnNo ?? -1, | |
| offset, | |
| prefix, | |
| IdValue.idToString( | |
| actualData.id, | |
| dataInterpreter.getText(actualData.value, indentation), | |
| ), | |
| suffix, | |
| ); | |
| } | |
| Set<Id> idSet = {} | |
| ..addAll(idValuePerId.keys) | |
| ..addAll(actualDataPerId.keys); | |
| List<Annotation> result = <Annotation>[]; | |
| for (Id id in idSet) { | |
| Map<String, IdValue> idValuePerMarker = idValuePerId[id] ?? {}; | |
| Map<String, ActualData<T>> actualDataPerMarker = actualDataPerId[id] ?? {}; | |
| Map<String, Annotation> newAnnotationsPerMarker = {}; | |
| for (String marker in supportedMarkers) { | |
| IdValue? idValue = idValuePerMarker[marker]; | |
| ActualData<T>? actualData = actualDataPerMarker[marker]; | |
| Annotation? expectedAnnotation; | |
| Annotation? actualAnnotation; | |
| if (idValue != null && actualData != null) { | |
| if (dataInterpreter.isAsExpected(actualData.value, idValue.value) == | |
| null && | |
| !forceUpdate) { | |
| // Use existing annotation. | |
| expectedAnnotation = actualAnnotation = idValue.annotation; | |
| } else { | |
| expectedAnnotation = idValue.annotation; | |
| actualAnnotation = createAnnotationFromData( | |
| actualData, | |
| idValue.annotation, | |
| ); | |
| } | |
| } else if (idValue != null && !actualMarkers.contains(marker)) { | |
| // Use existing annotation if no actual data is provided for this | |
| // marker. | |
| expectedAnnotation = actualAnnotation = idValue.annotation; | |
| } else if (actualData != null) { | |
| if (dataInterpreter.isAsExpected(actualData.value, null) != null) { | |
| // Insert annotation if the actual value is not equivalent to an | |
| // empty value. | |
| actualAnnotation = createAnnotationFromData(actualData, null); | |
| } | |
| } | |
| Annotation? annotation = createDiff != null | |
| ? createDiff(expectedAnnotation, actualAnnotation) | |
| : actualAnnotation; | |
| if (annotation != null) { | |
| newAnnotationsPerMarker[marker] = annotation; | |
| } | |
| } | |
| Map<String, Map<String, Annotation>> groupedByText = {}; | |
| newAnnotationsPerMarker.forEach((String marker, Annotation annotation) { | |
| Map<String, Annotation> byText = groupedByText[annotation.text] ??= {}; | |
| byText[marker] = annotation; | |
| }); | |
| groupedByText.forEach((String text, Map<String, Annotation> annotations) { | |
| Set<String> markers = annotations.keys.toSet(); | |
| if (markers.isNotEmpty) { | |
| String prefix; | |
| if (markers.length == supportedMarkers.length) { | |
| // Don't use prefix for annotations that match all markers. | |
| prefix = ''; | |
| } else { | |
| Iterable<String> usedMarkers = markers; | |
| if (sortMarkers) { | |
| usedMarkers = usedMarkers.toList()..sort(); | |
| } | |
| prefix = '${usedMarkers.join('|')}.'; | |
| } | |
| Annotation firstAnnotation = annotations.values.first; | |
| result.add( | |
| new Annotation( | |
| firstAnnotation.index, | |
| firstAnnotation.lineNo, | |
| firstAnnotation.columnNo, | |
| firstAnnotation.offset, | |
| firstAnnotation.prefix, | |
| '$prefix$text', | |
| firstAnnotation.suffix, | |
| ), | |
| ); | |
| } | |
| }); | |
| } | |
| return result; | |
| } | |
| bool setEquals<E>(Set<E> a, Set<E> b) { | |
| return a.length == b.length && a.containsAll(b); | |
| } |