| // 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, | |
| configureAnalysisOptionsBuilder: ({required analysisOptionsBuilder}) { | |
| analysisOptionsBuilder.contextFeatures = config.featureSet; | |
| }, | |
| withFineDependencies: true, | |
| ); | |
| var analysisContext = contextCollection.contexts.single; | |
| var analysisSession = analysisContext.currentSession; | |
| var driver = analysisContext.driver; | |
| Map<Uri, ActualDataMap<T>> actualMaps = <Uri, ActualDataMap<T>>{}; | |
| ActualDataMap<T> globalData = ActualDataMap(); | |
| ActualDataMap<T> actualMapFor(Uri uri) { | |
| return actualMaps.putIfAbsent(uri, () => ActualDataMap()); | |
| } | |
| 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) { | |
| ActualDataMap<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, | |
| await actualMaps.getData(), | |
| await globalData.getData(), | |
| ); | |
| 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.declarations2) { | |
| if (declaration is ClassDeclaration && | |
| declaration.namePart.typeName.lexeme == className) { | |
| if (declaration.body case BlockClassBody body) { | |
| for (var member in body.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.declarations2) { | |
| if (declaration is FunctionDeclaration) { | |
| if (declaration.name.lexeme == name) { | |
| return declaration.offset; | |
| } | |
| } else if (declaration is TopLevelGetterDeclaration) { | |
| 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.declarations2) { | |
| if (declaration is ClassDeclaration && | |
| declaration.namePart.typeName.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, | |
| ActualDataMap<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; | |
| } |