|  | // 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; | 
|  | } |