blob: 6d75b6f1433c772a3c63a93ba0a98326ec678bef [file] [log] [blame]
// 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(paulberry,johnniwinther): 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/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/testing_data.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
/// Test configuration used for testing the analyzer with constant evaluation.
final TestConfig analyzerConstantUpdate2018Config = TestConfig(
analyzerMarker, 'analyzer with constant-update-2018',
featureSet: FeatureSet.forTesting(
sdkVersion: '2.2.2',
additionalFeatures: [Feature.constant_update_2018]));
/// Test configuration used for testing the analyzer with NNBD.
final TestConfig analyzerNnbdConfig = TestConfig(
analyzerMarker, 'analyzer with NNBD',
featureSet: FeatureSet.forTesting(
sdkVersion: '2.2.2', additionalFeatures: [Feature.non_nullable]));
/// A fake absolute directory used as the root of a memory-file system in ID
/// tests.
Uri _defaultDir = Uri.parse('file:///a/b/c/');
Future<TestResult<T>> checkTests<T>(
String rawCode, DataComputer<T> dataComputer, FeatureSet featureSet) async {
AnnotatedCode code =
AnnotatedCode.fromText(rawCode, commentStart, commentEnd);
String testFileName = 'test.dart';
var testFileUri = _toTestUri(testFileName);
var memorySourceFiles = {testFileName: code.sourceCode};
var marker = 'analyzer';
Map<String, MemberAnnotations<IdValue>> expectedMaps = {
marker: MemberAnnotations<IdValue>(),
};
computeExpectedMap(testFileUri, testFileName, code, expectedMaps,
onFailure: onFailure);
Map<Uri, AnnotatedCode> codeMap = {testFileUri: code};
var testData = TestData(testFileName, testFileUri, testFileUri,
memorySourceFiles, codeMap, expectedMaps);
var config =
TestConfig(marker, 'provisional test config', featureSet: featureSet);
return runTestForConfig<T>(testData, dataComputer, config,
onFailure: onFailure);
}
/// 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>(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(
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 (TestData testData,
{required bool testAfterFailures,
bool? verbose,
bool? succinct,
bool? printCode,
Map<String, List<String>>? skipMap,
Uri? nullUri}) {
return runTest(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>(
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 testUris = <Uri>[];
for (var entry in testData.memorySourceFiles.entries) {
var testUri = _toTestUri(entry.key);
testUris.add(testUri);
resourceProvider.newFile(
resourceProvider.convertPath(testUri.path), entry.value);
}
var sdk = MockSdk(resourceProvider: resourceProvider);
var logBuffer = StringBuffer();
var logger = PerformanceLog(logBuffer);
var scheduler = AnalysisDriverScheduler(logger);
// TODO(paulberry): Do we need a non-empty package map for any of these tests?
var packageMap = <String, List<Folder>>{};
var byteStore = MemoryByteStore();
var analysisOptions = AnalysisOptionsImpl()
..contextFeatures = config.featureSet;
var driver = AnalysisDriver.tmp1(
scheduler: scheduler,
logger: logger,
resourceProvider: resourceProvider,
byteStore: byteStore,
sourceFactory: SourceFactory([
DartUriResolver(sdk),
PackageMapUriResolver(resourceProvider, packageMap),
ResourceUriResolver(resourceProvider)
]),
analysisOptions: analysisOptions,
packages: Packages.empty,
retainDataForTesting: true,
);
scheduler.start();
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 testUri in testUris) {
var path = resourceProvider.convertPath(testUri.path);
var result = await driver.getResult2(path) as ResolvedUnitResult;
var errors =
result.errors.where((e) => e.severity == Severity.error).toList();
if (errors.isNotEmpty) {
if (dataComputer.supportsErrors) {
var errorMap = <int, List<AnalysisError>>{};
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;
}
(errorMap[offset] ??= <AnalysisError>[]).add(error);
}
errorMap.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(AnalysisError e) {
var locationInfo = result.unit!.lineInfo!.getLocation(e.offset);
return '$locationInfo: ${e.errorCode}: ${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(config.name, testData.testFileUri, testData.code,
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,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.name == className) {
for (var member in declaration.members) {
if (member is ConstructorDeclaration) {
if (member.name!.name == name) {
return member.offset;
}
} else if (member is FieldDeclaration) {
for (var variable in member.fields.variables) {
if (variable.name.name == name) {
return variable.offset;
}
}
} else if (member is MethodDeclaration) {
if (member.name.name == 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.name == name) {
return declaration.offset;
}
} else if (declaration is TopLevelVariableDeclaration) {
for (var variable in declaration.variables.variables) {
if (variable.name.name == 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.name == className) {
return declaration.offset;
}
}
return 0;
} else if (id is LibraryId) {
var unit =
parseString(content: code[uri]!.sourceCode, throwIfDiagnostics: false)
.unit;
var offset = unit.declaredElement?.library.nameOffset ?? -1;
return offset >= 0 ? offset : 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 [error].
T? computeErrorData(TestConfig config, TestingData testingData, Id id,
List<AnalysisError> errors) =>
null;
/// Function that computes a data mapping for [unit].
///
/// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
/// for the data origin.
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();
}