blob: 4a1ca15e12accf16587aa7ae206042e59ebafcc4 [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) {
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);
}