blob: 896c67080684428b8cd519bcf97a56355b9fd0b0 [file] [log] [blame]
// 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) {
// ignore: unnecessary_null_comparison
if (uri == null) {
// TODO(johnniwinther): Avoid `null` URIs.
assert(dataMap.isEmpty, "Non-empty data for `null` uri: $dataMap");
return;
}
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]!;
assert(
// ignore: unnecessary_null_comparison
code != null, "No annotated code for ${uri} in ${annotatedCode.keys}");
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}) {
// ignore: unnecessary_null_comparison
assert(annotatedCode != null);
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);
}