// 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>> runTest( MarkerOptions markerOptions, TestData testData, DataComputer dataComputer, List testedConfigs, { required bool testAfterFailures, bool forUserLibrariesOnly = true, Iterable globalIds = const [], required void Function(String message) onFailure, Map>? 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> 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 runTestFor( DataComputer dataComputer, List testedConfigs, ) { return ( MarkerOptions markerOptions, TestData testData, { required bool testAfterFailures, bool? verbose, bool? succinct, bool? printCode, Map>? 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> runTestForConfig( MarkerOptions markerOptions, TestData testData, DataComputer dataComputer, TestConfig config, { bool fatalErrors = true, required void Function(String message) onFailure, Map>? skipMap, }) async { MemberAnnotations 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>> actualMaps = >>{}; Map> globalData = >{}; Map> actualMapFor(Uri uri) { return actualMaps.putIfAbsent(uri, () => >{}); } var results = {}; 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 = >{}; 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] ??= []).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> actualMap = actualMapFor(testUri); actualMap[id] = ActualData(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.erroneous(); } } results[testUri] = result; } results.forEach((testUri, result) { dataComputer.computeUnitData( driver.testingData!, result.unit, actualMapFor(testUri), ); }); var compiledData = AnalyzerCompiledData( 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 extends CompiledData { // TODO(johnniwinther): . // TODO(paulberry): Maybe this should have access to the [ResolvedUnitResult] instead. final Map code; AnalyzerCompiledData( this.code, Uri mainUri, Map>> actualMaps, Map> 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 { const DataComputer(); DataInterpreter 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 diagnostics, ) => null; /// Computes a data mapping for [unit]. /// /// Fills [actualMap] with the data. void computeUnitData( TestingData testingData, CompilationUnit unit, Map> 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; }