43db6c6ba8
This prepares for adding id-test support in ddc. Change-Id: I96d35b06e5e32f464cd487dc852ec114064afc9d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/334161 Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Jens Johansen <jensj@google.com>
417 lines
14 KiB
Dart
417 lines
14 KiB
Dart
// 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/testing/id.dart';
|
|
import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
|
|
import 'package:kernel/ast.dart';
|
|
import '../fasta/kernel/macro/macro.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);
|
|
}
|
|
}
|
|
|
|
void printMessageInLocation(
|
|
Map<Uri, Source> uriToSource, Uri? uri, int offset, String message,
|
|
{bool succinct = false}) {
|
|
if (uri == null) {
|
|
print("(null uri)@$offset: $message");
|
|
} else {
|
|
Source? source = uriToSource[uri];
|
|
if (source == null) {
|
|
print('$uri@$offset: $message');
|
|
} else {
|
|
if (offset >= 1) {
|
|
Location location = source.getLocation(uri, offset);
|
|
print('$location: $message');
|
|
if (!succinct) {
|
|
print(source.getTextLine(location.line));
|
|
print(' ' * (location.column - 1) + '^');
|
|
}
|
|
} else {
|
|
print('$uri: $message');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
if (uri?.scheme == augmentationScheme) {
|
|
throw new UnsupportedError(
|
|
"Annotations are not support on augmentation uris.");
|
|
}
|
|
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 isAugmentation = uri?.scheme == augmentationScheme;
|
|
if (isAugmentation) {
|
|
uri = testData.entryPoint;
|
|
}
|
|
Map<int, List<FormattedMessage>> map =
|
|
errorMap.putIfAbsent(uri ?? nullUri, () => {});
|
|
List<FormattedMessage> list =
|
|
map.putIfAbsent(isAugmentation ? -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 (uri.scheme == augmentationScheme) {
|
|
TreeNode library = node;
|
|
while (library is! Library) {
|
|
library = library.parent!;
|
|
}
|
|
uri = library.fileUri;
|
|
}
|
|
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);
|
|
}
|