blob: a61a7ed1ca148c9505194d379a4dad6ef5631414 [file] [log] [blame]
// Copyright (c) 2023, 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/macros/uri.dart';
import 'package:_fe_analyzer_shared/src/testing/id.dart';
import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
import 'package:kernel/ast.dart';
import '../fasta/messages.dart';
import 'id_testing_utils.dart';
/// Identifier for a test configuration.
class TestConfig {
/// The id for the test config. This is used in test annotations to
/// distinguish between expectations that differ between configs.
///
/// For instance if config 'a' expects 'A' and config 'b' expects 'B' on 'foo'
/// then the annotation in the data file would be
///
/// /*a:A*/ /*b:B*/ foo
///
final String marker;
/// The descriptive name of the config, used on error reporting.
final String name;
const TestConfig(this.marker, this.name);
}
/// The compilation result of an id-test for given test [config].
abstract class TestResultData<C extends TestConfig, R> {
/// The test config used to run the test.
final C config;
/// The compiler result from running the test, include access to the used
/// compiler.
final R compilerResult;
TestResultData(this.config, this.compilerResult);
Component get component;
}
/// Abstract base class used for computing the id-test annotation data for
/// different parts of a [Component].
abstract class DataComputer<T, C extends TestConfig, R,
D extends TestResultData<C, R>> {
const DataComputer();
/// Called before testing to setup flags needed for data collection.
void setup() {}
/// Called to allow for (awaited) inspection of the [testResultData] from
/// running the test.
Future<void> inspectTestResultData(D testResultData) {
return new Future.value(null);
}
/// Function that computes a data mapping for [member].
///
/// Fills [actualMap] with the data.
void computeMemberData(
D testResultData, Member member, Map<Id, ActualData<T>> actualMap,
{bool? verbose}) {}
/// Function that computes a data mapping for [cls].
///
/// Fills [actualMap] with the data.
void computeClassData(
D testResultData, Class cls, Map<Id, ActualData<T>> actualMap,
{bool? verbose}) {}
/// Function that computes a data mapping for [extension].
///
/// Fills [actualMap] with the data.
void computeExtensionData(
D testResultData, Extension extension, Map<Id, ActualData<T>> actualMap,
{bool? verbose}) {}
/// Function that computes a data mapping for [extensionTypeDeclaration].
///
/// Fills [actualMap] with the data.
void computeExtensionTypeDeclarationData(
D testResultData,
ExtensionTypeDeclaration extensionTypeDeclaration,
Map<Id, ActualData<T>> actualMap,
{bool? verbose}) {}
/// Function that computes a data mapping for [library].
///
/// Fills [actualMap] with the data.
void computeLibraryData(
D testResultData, Library library, Map<Id, ActualData<T>> actualMap,
{bool? verbose}) {}
/// Returns `true` if this data computer supports tests with compile-time
/// errors.
///
/// Unsuccessful compilation might leave the compiler in an inconsistent
/// state, so this testing feature is opt-in.
bool get supportsErrors => false;
/// Returns data corresponding to [error].
T? computeErrorData(D testResultData, Id id, List<FormattedMessage> errors) =>
null;
/// Returns the [DataInterpreter] used to check the actual data with the
/// expected data.
DataInterpreter<T> get dataValidator;
/// Returns `true` if data should be collected for member signatures.
bool get includeMemberSignatures => false;
}
/// [CompiledData] that includes [Component] and the [compilerResult].
class KernelCompiledData<T, R> extends CompiledData<T> {
final R compilerResult;
final Component component;
KernelCompiledData(this.compilerResult, this.component, super.mainUri,
super.actualMaps, super.globalData);
@override
int getOffsetFromId(Id id, Uri uri) {
if (id is NodeId) return id.value;
if (id is MemberId) {
Library library = lookupLibrary(component, uri)!;
Member? member;
int offset = -1;
if (id.className != null) {
Class? cls = lookupClass(library, id.className!, required: false);
if (cls != null) {
member = lookupClassMember(cls, id.memberName, required: false);
if (member != null) {
offset = member.fileOffset;
if (offset == -1) {
offset = cls.fileOffset;
}
} else {
offset = cls.fileOffset;
}
}
} else {
member = lookupLibraryMember(library, id.memberName, required: false);
offset = member?.fileOffset ?? 0;
}
if (offset == -1) {
offset = 0;
}
return offset;
} else if (id is ClassId) {
Library library = lookupLibrary(component, uri)!;
Extension? extension =
lookupExtension(library, id.className, required: false);
if (extension != null) {
return extension.fileOffset;
}
Class? cls = lookupClass(library, id.className, required: false);
return cls?.fileOffset ?? 0;
}
return 0;
}
@override
void reportError(Uri uri, int offset, String message,
{bool succinct = false}) {
printMessageInLocation(component.uriToSource, uri, offset, message,
succinct: succinct);
}
}
String createMessageInLocation(
Map<Uri, Source> uriToSource, Uri? uri, int offset, String message,
{bool succinct = false}) {
StringBuffer sb = new StringBuffer();
if (uri == null) {
sb.write("(null uri)@$offset: $message");
} else {
Source? source = uriToSource[uri];
if (source == null) {
sb.write('$uri@$offset: $message');
} else {
if (offset >= 1) {
Location location = source.getLocation(uri, offset);
sb.write('$location: $message');
if (!succinct) {
sb.writeln('');
sb.writeln(source.getTextLine(location.line));
sb.write(' ' * (location.column - 1) + '^');
}
} else {
sb.write('$uri: $message');
}
}
}
return sb.toString();
}
void printMessageInLocation(
Map<Uri, Source> uriToSource, Uri? uri, int offset, String message,
{bool succinct = false}) {
print(createMessageInLocation(uriToSource, uri, offset, message,
succinct: succinct));
}
/// Computes the [TestResult] for an id-test [testData] using the result of
/// the compilation in [testResultData].
Future<TestResult<T>> processCompiledResult<T, C extends TestConfig, R,
D extends TestResultData<C, R>>(
MarkerOptions markerOptions,
TestData testData,
DataComputer<T, C, R, D> dataComputer,
D testResultData,
List<FormattedMessage> errors,
{required bool fatalErrors,
required bool verbose,
required bool succinct,
bool forUserLibrariesOnly = true,
required void onFailure(String message),
required Uri nullUri}) async {
C config = testResultData.config;
R compilerResult = testResultData.compilerResult;
Component component = testResultData.component;
Map<Uri, Map<Id, ActualData<T>>> actualMaps = <Uri, Map<Id, ActualData<T>>>{};
Map<Id, ActualData<T>> globalData = <Id, ActualData<T>>{};
Map<Id, ActualData<T>> actualMapForUri(Uri? uri) {
return actualMaps.putIfAbsent(uri ?? nullUri, () => <Id, ActualData<T>>{});
}
if (errors.isNotEmpty) {
if (!dataComputer.supportsErrors) {
onFailure("Compilation with compile-time errors not supported for this "
"testing setup.");
}
Map<Uri, Map<int, List<FormattedMessage>>> errorMap = {};
for (FormattedMessage error in errors) {
Uri? uri = error.uri;
bool isMacroLibrary = false;
if (uri != null && isMacroLibraryUri(uri)) {
isMacroLibrary = true;
uri = toOriginLibraryUri(uri);
}
Map<int, List<FormattedMessage>> map =
errorMap.putIfAbsent(uri ?? nullUri, () => {});
List<FormattedMessage> list =
map.putIfAbsent(isMacroLibrary ? -1 : error.charOffset, () => []);
list.add(error);
}
errorMap.forEach((Uri uri, Map<int, List<FormattedMessage>> map) {
map.forEach((int offset, List<FormattedMessage> list) {
if (offset < 0) {
// Position errors without offset in the begin of the file.
offset = 0;
}
NodeId id = new NodeId(offset, IdKind.error);
T? data = dataComputer.computeErrorData(testResultData, id, list);
if (data != null) {
Map<Id, ActualData<T>> actualMap = actualMapForUri(uri);
actualMap[id] = new ActualData<T>(id, data, uri, offset, list);
}
});
});
}
Map<Id, ActualData<T>> actualMapFor(TreeNode node) {
Uri uri = node is Library
? node.fileUri
: (node is Member ? node.fileUri : node.location!.file);
if (isMacroLibraryUri(uri)) {
uri = toOriginLibraryUri(uri);
}
return actualMapForUri(uri);
}
void processMember(Member member, Map<Id, ActualData<T>> actualMap) {
if (!dataComputer.includeMemberSignatures && member is Procedure) {
if (member.isMemberSignature ||
(member.isForwardingStub && !member.isForwardingSemiStub)) {
return;
}
}
if (member.enclosingClass != null) {
if (member.enclosingClass!.isEnum) {
if (member is Constructor ||
member.isInstanceMember ||
member.name.text == 'values') {
return;
}
}
if (member is Constructor && member.enclosingClass.isMixinApplication) {
return;
}
}
dataComputer.computeMemberData(testResultData, member, actualMap,
verbose: verbose);
}
void processClass(Class cls, Map<Id, ActualData<T>> actualMap) {
dataComputer.computeClassData(testResultData, cls, actualMap,
verbose: verbose);
}
void processExtension(Extension extension, Map<Id, ActualData<T>> actualMap) {
dataComputer.computeExtensionData(testResultData, extension, actualMap,
verbose: verbose);
}
void processExtensionTypeDeclaration(
ExtensionTypeDeclaration extensionTypeDeclaration,
Map<Id, ActualData<T>> actualMap) {
dataComputer.computeExtensionTypeDeclarationData(
testResultData, extensionTypeDeclaration, actualMap,
verbose: verbose);
}
bool excludeLibrary(Library library) {
return forUserLibrariesOnly &&
(library.importUri.isScheme('dart') ||
library.importUri.isScheme('package'));
}
await dataComputer.inspectTestResultData(testResultData);
for (Library library in component.libraries) {
if (excludeLibrary(library) &&
!testData.memorySourceFiles.containsKey(library.fileUri.path)) {
continue;
}
dataComputer.computeLibraryData(
testResultData, library, actualMapFor(library));
for (Class cls in library.classes) {
processClass(cls, actualMapFor(cls));
for (Member member in cls.members) {
processMember(member, actualMapFor(member));
}
}
for (Member member in library.members) {
processMember(member, actualMapFor(member));
}
for (Extension extension in library.extensions) {
processExtension(extension, actualMapFor(extension));
}
for (ExtensionTypeDeclaration extensionTypeDeclaration
in library.extensionTypeDeclarations) {
processExtensionTypeDeclaration(
extensionTypeDeclaration, actualMapFor(extensionTypeDeclaration));
}
}
List<Uri> globalLibraries = <Uri>[
Uri.parse('dart:core'),
Uri.parse('dart:collection'),
Uri.parse('dart:async'),
];
Class getGlobalClass(String className) {
Class? cls;
for (Uri uri in globalLibraries) {
Library? library = lookupLibrary(component, uri);
if (library != null) {
cls ??= lookupClass(library, className);
}
}
if (cls == null) {
throw "Global class '$className' not found in the global "
"libraries: ${globalLibraries.join(', ')}";
}
return cls;
}
Member getGlobalMember(String memberName) {
Member? member;
for (Uri uri in globalLibraries) {
Library? library = lookupLibrary(component, uri);
if (library != null) {
member ??= lookupLibraryMember(library, memberName);
}
}
if (member == null) {
throw "Global member '$memberName' not found in the global "
"libraries: ${globalLibraries.join(', ')}";
}
return member;
}
MemberAnnotations<IdValue> memberAnnotations =
testData.expectedMaps[config.marker]!;
Iterable<Id> globalIds = memberAnnotations.globalData.keys;
for (Id id in globalIds) {
if (id is MemberId) {
Member? member;
if (id.className != null) {
Class? cls = getGlobalClass(id.className!);
member = lookupClassMember(cls, id.memberName);
if (member == null) {
throw "Global member '${id.memberName}' not found in class $cls.";
}
} else {
member = getGlobalMember(id.memberName);
}
processMember(member, globalData);
} else if (id is ClassId) {
Class cls = getGlobalClass(id.className);
processClass(cls, globalData);
} else {
throw new UnsupportedError("Unexpected global id: $id");
}
}
KernelCompiledData<T, R> compiledData = new KernelCompiledData<T, R>(
compilerResult, component, testData.entryPoint, actualMaps, globalData);
return checkCode(markerOptions, config.marker, config.name, testData,
memberAnnotations, compiledData, dataComputer.dataValidator,
fatalErrors: fatalErrors, succinct: succinct, onFailure: onFailure);
}