blob: 6d2ad691ab831d3359a306d8b3b5b3966c2aaaac [file] [log] [blame] [edit]
// Copyright (c) 2019, 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.
// TODO(johnniwinther): .
// TODO(paulberry): Use the code for extraction of test data from
// annotated code from CFE.
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';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart' hide Annotation;
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/testing_data.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
import 'package:analyzer/src/utilities/extensions/diagnostic.dart';
import 'package:analyzer_testing/utilities/extensions/resource_provider.dart';
/// Test configuration used for testing the analyzer without experiments.
final TestConfig analyzerDefaultConfig = TestConfig(
analyzerMarker,
'analyzer without experiments',
featureSet: FeatureSet.latestLanguageVersion(),
);
/// A fake absolute directory used as the root of a memory-file system in ID
/// tests.
Uri _defaultDir = Uri.parse('file:///a/b/c/');
/// Creates the testing URI used for [fileName] in annotated tests.
Uri createUriForFileName(String fileName) => _toTestUri(fileName);
void onFailure(String message) {
throw StateError(message);
}
/// Runs [dataComputer] on [testData] for all [testedConfigs].
///
/// Returns `true` if an error was encountered.
Future<Map<String, TestResult<T>>> runTest<T>(
MarkerOptions markerOptions,
TestData testData,
DataComputer<T> dataComputer,
List<TestConfig> testedConfigs, {
required bool testAfterFailures,
bool forUserLibrariesOnly = true,
Iterable<Id> globalIds = const <Id>[],
required void Function(String message) onFailure,
Map<String, List<String>>? skipMap,
}) async {
for (TestConfig config in testedConfigs) {
if (!testData.expectedMaps.containsKey(config.marker)) {
throw ArgumentError(
"Unexpected test marker '${config.marker}'. "
"Supported markers: ${testData.expectedMaps.keys}.",
);
}
}
Map<String, TestResult<T>> results = {};
for (TestConfig config in testedConfigs) {
if (skipForConfig(testData.name, config.marker, skipMap)) {
continue;
}
results[config.marker] = await runTestForConfig(
markerOptions,
testData,
dataComputer,
config,
fatalErrors: !testAfterFailures,
onFailure: onFailure,
);
}
return results;
}
/// Creates a test runner for [dataComputer] on [testedConfigs].
RunTestFunction<T> runTestFor<T>(
DataComputer<T> dataComputer,
List<TestConfig> testedConfigs,
) {
return (
MarkerOptions markerOptions,
TestData testData, {
required bool testAfterFailures,
bool? verbose,
bool? succinct,
bool? printCode,
Map<String, List<String>>? skipMap,
Uri? nullUri,
}) {
return runTest(
markerOptions,
testData,
dataComputer,
testedConfigs,
testAfterFailures: testAfterFailures,
onFailure: onFailure,
skipMap: skipMap,
);
};
}
/// Runs [dataComputer] on [testData] for [config].
///
/// Returns `true` if an error was encountered.
Future<TestResult<T>> runTestForConfig<T>(
MarkerOptions markerOptions,
TestData testData,
DataComputer<T> dataComputer,
TestConfig config, {
bool fatalErrors = true,
required void Function(String message) onFailure,
Map<String, List<String>>? skipMap,
}) async {
MemberAnnotations<IdValue> memberAnnotations =
testData.expectedMaps[config.marker]!;
var resourceProvider = MemoryResourceProvider();
var testFiles = <_TestFile>[];
for (var entry in testData.memorySourceFiles.entries) {
var uri = _toTestUri(entry.key);
var path = ResourceProviderExtension(
resourceProvider,
).convertPath(uri.path);
var file = resourceProvider.getFile(path);
testFiles.add(_TestFile(uri: uri, file: file));
file.writeAsStringSync(entry.value);
}
var sdkRoot = resourceProvider.newFolder(
ResourceProviderExtension(resourceProvider).convertPath('/sdk'),
);
createMockSdk(resourceProvider: resourceProvider, root: sdkRoot);
var contextCollection = AnalysisContextCollectionImpl(
includedPaths: testFiles.map((e) => e.path).toList(),
resourceProvider: resourceProvider,
retainDataForTesting: true,
sdkPath: sdkRoot.path,
updateAnalysisOptions4: ({required analysisOptions}) {
analysisOptions.contextFeatures = config.featureSet;
},
);
var analysisContext = contextCollection.contexts.single;
var analysisSession = analysisContext.currentSession;
var driver = analysisContext.driver;
Map<Uri, Map<Id, ActualData<T>>> actualMaps = <Uri, Map<Id, ActualData<T>>>{};
Map<Id, ActualData<T>> globalData = <Id, ActualData<T>>{};
Map<Id, ActualData<T>> actualMapFor(Uri uri) {
return actualMaps.putIfAbsent(uri, () => <Id, ActualData<T>>{});
}
var results = <Uri, ResolvedUnitResult>{};
for (var testFile in testFiles) {
var testUri = testFile.uri;
var result = await analysisSession.getResolvedUnit(testFile.path);
result as ResolvedUnitResult;
var errors = result.diagnostics.errors;
if (errors.isNotEmpty) {
if (dataComputer.supportsErrors) {
var diagnosticMap = <int, List<Diagnostic>>{};
for (var error in errors) {
var offset = error.offset;
if (offset == 0 || offset < 0) {
// Position errors without offset in the begin of the file.
offset = 0;
}
(diagnosticMap[offset] ??= <Diagnostic>[]).add(error);
}
diagnosticMap.forEach((offset, errors) {
var id = NodeId(offset, IdKind.error);
var data = dataComputer.computeErrorData(
config,
driver.testingData!,
id,
errors,
);
if (data != null) {
Map<Id, ActualData<T>> actualMap = actualMapFor(testUri);
actualMap[id] = ActualData<T>(id, data, testUri, offset, errors);
}
});
} else {
String formatError(Diagnostic e) {
var locationInfo = result.unit.lineInfo.getLocation(e.offset);
return '$locationInfo: ${e.diagnosticCode}: ${e.message}';
}
onFailure('Errors found:\n ${errors.map(formatError).join('\n ')}');
return TestResult<T>.erroneous();
}
}
results[testUri] = result;
}
results.forEach((testUri, result) {
dataComputer.computeUnitData(
driver.testingData!,
result.unit,
actualMapFor(testUri),
);
});
var compiledData = AnalyzerCompiledData<T>(
testData.code,
testData.entryPoint,
actualMaps,
globalData,
);
return checkCode(
markerOptions,
config.marker,
config.name,
testData,
memberAnnotations,
compiledData,
dataComputer.dataValidator,
fatalErrors: fatalErrors,
onFailure: onFailure,
);
}
/// Convert relative file paths into an absolute Uri as expected by the test
/// helpers.
Uri _toTestUri(String relativePath) => _defaultDir.resolve(relativePath);
class AnalyzerCompiledData<T> extends CompiledData<T> {
// TODO(johnniwinther): .
// TODO(paulberry): Maybe this should have access to the [ResolvedUnitResult] instead.
final Map<Uri, AnnotatedCode> code;
AnalyzerCompiledData(
this.code,
Uri mainUri,
Map<Uri, Map<Id, ActualData<T>>> actualMaps,
Map<Id, ActualData<T>> globalData,
) : super(mainUri, actualMaps, globalData);
@override
int getOffsetFromId(Id id, Uri uri) {
if (id is NodeId) {
return id.value;
} else if (id is MemberId) {
var className = id.className;
var name = id.memberName;
var unit = parseString(
content: code[uri]!.sourceCode,
throwIfDiagnostics: false,
).unit;
if (className != null) {
for (var declaration in unit.declarations) {
if (declaration is ClassDeclaration &&
declaration.name.lexeme == className) {
for (var member in declaration.members) {
if (member is ConstructorDeclaration) {
if (member.name!.lexeme == name) {
return member.offset;
}
} else if (member is FieldDeclaration) {
for (var variable in member.fields.variables) {
if (variable.name.lexeme == name) {
return variable.offset;
}
}
} else if (member is MethodDeclaration) {
if (member.name.lexeme == name) {
return member.offset;
}
}
}
// Use class offset for members not declared in the class.
return declaration.offset;
}
}
return 0;
}
for (var declaration in unit.declarations) {
if (declaration is FunctionDeclaration) {
if (declaration.name.lexeme == name) {
return declaration.offset;
}
} else if (declaration is TopLevelVariableDeclaration) {
for (var variable in declaration.variables.variables) {
if (variable.name.lexeme == name) {
return variable.offset;
}
}
}
}
return 0;
} else if (id is ClassId) {
var className = id.className;
var unit = parseString(
content: code[uri]!.sourceCode,
throwIfDiagnostics: false,
).unit;
for (var declaration in unit.declarations) {
if (declaration is ClassDeclaration &&
declaration.name.lexeme == className) {
return declaration.offset;
}
}
return 0;
} else {
throw StateError('Unexpected id ${id.runtimeType}');
}
}
@override
void reportError(
Uri uri,
int offset,
String message, {
bool succinct = false,
}) {
print('$offset: $message');
}
}
abstract class DataComputer<T> {
const DataComputer();
DataInterpreter<T> get dataValidator;
/// 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 [diagnostics].
T? computeErrorData(
TestConfig config,
TestingData testingData,
Id id,
List<Diagnostic> diagnostics,
) => null;
/// Computes a data mapping for [unit].
///
/// Fills [actualMap] with the data.
void computeUnitData(
TestingData testingData,
CompilationUnit unit,
Map<Id, ActualData<T>> actualMap,
);
}
class TestConfig {
final String marker;
final String name;
final FeatureSet featureSet;
TestConfig(this.marker, this.name, {FeatureSet? featureSet})
: featureSet = featureSet ?? FeatureSet.latestLanguageVersion();
}
class _TestFile {
final Uri uri;
final File file;
_TestFile({required this.uri, required this.file});
String get path => file.path;
}