blob: 3ae79b36f5b24ee62482d067ce69fa5bfb1412f4 [file] [log] [blame]
// Copyright (c) 2020, 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.
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/context_root.dart';
import 'package:analyzer/dart/sdk/build_sdk_summary.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/results.dart';
import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/summary2/kernel_compilation_service.dart';
import 'package:analyzer/src/summary2/macro.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
import 'package:analyzer/src/test_utilities/package_config_file_builder.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/workspace/basic.dart';
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/gn.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer_utilities/test/experiments/experiments.dart';
import 'package:analyzer_utilities/test/mock_packages/mock_packages.dart';
import 'package:analyzer_utilities/testing/tree_string_sink.dart';
import 'package:linter/src/rules.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import '../../../generated/test_support.dart';
import '../../summary/macros_environment.dart';
import '../analysis/analyzer_state_printer.dart';
import 'node_text_expectations.dart';
import 'resolution.dart';
export 'package:analyzer/src/test_utilities/package_config_file_builder.dart';
// TODO(srawlins): This is duplicate with pkg/linter/test/rule_test_support.dart
// and pkg/analysis_server/test/analysis_server_base.dart.
// Keep them as consistent with each other as they are today. Ultimately combine
// them in a shared analyzer test utilities package (e.g. the analyzer_utilities
// package).
String analysisOptionsContent({
List<String> experiments = const [],
List<String> plugins = const [],
List<String> rules = const [],
bool strictCasts = false,
bool strictInference = false,
bool strictRawTypes = false,
List<String> unignorableNames = const [],
}) {
var buffer = StringBuffer();
buffer.writeln('analyzer:');
if (experiments.isNotEmpty) {
buffer.writeln(' enable-experiment:');
for (var experiment in experiments) {
buffer.writeln(' - $experiment');
}
}
buffer.writeln(' language:');
buffer.writeln(' strict-casts: $strictCasts');
buffer.writeln(' strict-inference: $strictInference');
buffer.writeln(' strict-raw-types: $strictRawTypes');
buffer.writeln(' cannot-ignore:');
for (var name in unignorableNames) {
buffer.writeln(' - $name');
}
if (plugins.isNotEmpty) {
buffer.writeln(' plugins:');
for (var plugin in plugins) {
buffer.writeln(' - $plugin');
}
}
buffer.writeln('linter:');
buffer.writeln(' rules:');
for (var rule in rules) {
buffer.writeln(' - $rule');
}
return buffer.toString();
}
// TODO(scheglov): This is duplicate with
// pkg/linter/test/rule_test_support.dart. Keep them as consistent with each
// other as they are today. Ultimately combine them in a shared analyzer test
// utilities package.
String pubspecYamlContent({
String? name,
String? sdkVersion,
List<PubspecYamlFileDependency> dependencies = const [],
}) {
var buffer = StringBuffer();
if (name != null) {
buffer.writeln('name: $name');
}
if (sdkVersion != null) {
buffer.writeln('environment:');
buffer.writeln(" sdk: '$sdkVersion'");
}
if (dependencies.isNotEmpty) {
buffer.writeln('dependencies:');
for (var dependency in dependencies) {
buffer.writeln(' ${dependency.name}: ${dependency.version}');
}
}
return buffer.toString();
}
class BlazeWorkspaceResolutionTest extends ContextResolutionTest {
@override
List<String> get collectionIncludedPaths => [workspaceRootPath];
String get myPackageLibPath => '$myPackageRootPath/lib';
String get myPackageRootPath => '$workspaceRootPath/dart/my';
@override
File get testFile => getFile('$myPackageLibPath/my.dart');
String get workspaceRootPath => '/workspace';
String get workspaceThirdPartyDartPath {
return '$workspaceRootPath/third_party/dart';
}
@override
void setUp() {
super.setUp();
newFile('$workspaceRootPath/${file_paths.blazeWorkspaceMarker}', '');
newFile('$myPackageRootPath/BUILD', '');
}
@override
void verifyCreatedCollection() {
super.verifyCreatedCollection();
assertBlazeWorkspaceFor(testFile);
}
}
/// [AnalysisContextCollection] based implementation of [ResolutionTest].
abstract class ContextResolutionTest
with ResourceProviderMixin, ResolutionTest {
static bool _lintRulesAreRegistered = false;
/// The byte store that is reused between tests. This allows reusing all
/// unlinked and linked summaries for SDK, so that tests run much faster.
/// However nothing is preserved between Dart VM runs, so changes to the
/// implementation are still fully verified.
static final MemoryByteStore _sharedByteStore = MemoryByteStore();
MemoryByteStore _byteStore = _sharedByteStore;
Map<String, String> _declaredVariables = {};
AnalysisContextCollectionImpl? _analysisContextCollection;
/// If not `null`, [resolveFile] will use the context that corresponds
/// to this file, instead of the given file.
File? fileForContextSelection;
/// Optional Dart SDK summary file, to be used instead of [sdkRoot].
File? sdkSummaryFile;
/// Optional summaries to provide for the collection.
List<File>? librarySummaryFiles;
/// By default the kernel implementation is used, this can override it.
MacroSupportFactory? macroSupportFactory;
AnalyzerStatePrinterConfiguration analyzerStatePrinterConfiguration =
AnalyzerStatePrinterConfiguration();
final IdProvider _idProvider = IdProvider();
List<MockSdkLibrary> get additionalMockSdkLibraries => [];
AnalysisContextCollectionImpl get analysisContextCollection {
var collection = _analysisContextCollection;
if (collection != null) {
return collection;
}
createMockSdk(
resourceProvider: resourceProvider,
root: sdkRoot,
additionalLibraries: additionalMockSdkLibraries,
);
collection = AnalysisContextCollectionImpl(
byteStore: _byteStore,
declaredVariables: _declaredVariables,
enableIndex: true,
includedPaths: collectionIncludedPaths.map(convertPath).toList(),
resourceProvider: resourceProvider,
retainDataForTesting: retainDataForTesting,
sdkPath: sdkRoot.path,
sdkSummaryPath: sdkSummaryFile?.path,
librarySummaryPaths: librarySummaryFiles?.map((e) => e.path).toList(),
updateAnalysisOptions2: updateAnalysisOptions,
macroSupportFactory: macroSupportFactory,
drainStreams: false,
);
_analysisContextCollection = collection;
verifyCreatedCollection();
return collection;
}
List<String> get collectionIncludedPaths;
set declaredVariables(Map<String, String> map) {
if (_analysisContextCollection != null) {
throw StateError('Declared variables cannot be changed after analysis.');
}
_declaredVariables = map;
}
bool get retainDataForTesting => false;
Folder get sdkRoot => newFolder('/sdk');
void assertBasicWorkspaceFor(File file) {
var workspace = contextFor(file).contextRoot.workspace;
expect(workspace, TypeMatcher<BasicWorkspace>());
}
void assertBlazeWorkspaceFor(File file) {
var workspace = contextFor(file).contextRoot.workspace;
expect(workspace, TypeMatcher<BlazeWorkspace>());
}
void assertDriverStateString(File file, String expected) {
var analysisDriver = driverFor(file);
var buffer = StringBuffer();
AnalyzerStatePrinter(
byteStore: _byteStore,
unlinkedUnitStore:
analysisDriver.fsState.unlinkedUnitStore as UnlinkedUnitStoreImpl,
idProvider: _idProvider,
libraryContext: analysisDriver.libraryContext,
configuration: analyzerStatePrinterConfiguration,
resourceProvider: resourceProvider,
sink: TreeStringSink(
sink: buffer,
indent: '',
),
withKeysGetPut: false,
).writeAnalysisDriver(analysisDriver.testView!);
var actual = buffer.toString();
if (actual != expected) {
print(actual);
NodeTextExpectationsCollector.add(actual);
}
expect(actual, expected);
}
void assertGnWorkspaceFor(File file) {
var workspace = contextFor(file).contextRoot.workspace;
expect(workspace, TypeMatcher<GnWorkspace>());
}
void assertPackageConfigWorkspaceFor(File file) {
var workspace = contextFor(file).contextRoot.workspace;
expect(workspace, TypeMatcher<PackageConfigWorkspace>());
}
AnalysisContext contextFor(File file) {
return _contextFor(file);
}
Future<void> disposeAnalysisContextCollection() async {
var analysisContextCollection = _analysisContextCollection;
if (analysisContextCollection != null) {
await analysisContextCollection.dispose(
forTesting: true,
);
_analysisContextCollection = null;
}
}
AnalysisDriver driverFor(File file) {
return _contextFor(file).driver;
}
Future<LibraryElementImpl> libraryElementForFile(File file) async {
var analysisContext = contextFor(file);
var analysisSession = analysisContext.currentSession;
var uri = analysisSession.uriConverter.pathToUri(file.path);
var uriStr = uri.toString();
var libraryResult = await analysisSession.getLibraryByUri(uriStr);
libraryResult as LibraryElementResultImpl;
return libraryResult.element2;
}
void makeFilePriority(File file) {
driverFor(file).priorityFiles2 = [file];
}
@override
File newFile(String path, String content) {
if (_analysisContextCollection != null && !path.endsWith('.dart')) {
throw StateError('Only dart files can be changed after analysis.');
}
return super.newFile(path, content);
}
@override
Future<ResolvedUnitResultImpl> resolveFile(File file) async {
var analysisContext = contextFor(fileForContextSelection ?? file);
var session = analysisContext.currentSession;
var result = await session.getResolvedUnit(file.path);
return result as ResolvedUnitResultImpl;
}
@mustCallSuper
void setUp() {
if (!_lintRulesAreRegistered) {
registerLintRules();
_lintRulesAreRegistered = true;
}
}
@mustCallSuper
Future<void> tearDown() async {
await disposeAnalysisContextCollection();
KernelCompilationService.disposeDelayed(
const Duration(milliseconds: 500),
);
}
/// Override this method to update [analysisOptions] for every context root,
/// the default or already updated with `analysis_options.yaml` file.
void updateAnalysisOptions({
required AnalysisOptionsImpl analysisOptions,
required ContextRoot contextRoot,
required DartSdk sdk,
}) {}
/// Call this method if the test needs to use the empty byte store, without
/// any information cached.
void useEmptyByteStore() {
_byteStore = MemoryByteStore();
}
void verifyCreatedCollection() {}
DriverBasedAnalysisContext _contextFor(File file) {
return analysisContextCollection.contextFor(file.path);
}
}
class PubPackageResolutionTest extends ContextResolutionTest
with MockPackagesMixin {
AnalysisOptionsImpl get analysisOptions {
return contextFor(testFile).getAnalysisOptionsForFile(testFile)
as AnalysisOptionsImpl;
}
@override
List<String> get collectionIncludedPaths => [workspaceRootPath];
List<String> get experiments => experimentsForTests;
@override
String get packagesRootPath => '/packages';
@override
File get testFile => getFile('$testPackageLibPath/test.dart');
/// The language version to use by default for `package:test`.
String? get testPackageLanguageVersion => null;
String get testPackageLibPath => '$testPackageRootPath/lib';
String get testPackageRootPath => '$workspaceRootPath/test';
String get workspaceRootPath => '/home';
/// Creates `package:macro` and `package:_macro` files, adds to [config].
void addMacrosEnvironment(
PackageConfigFileBuilder config,
MacrosEnvironment macrosEnvironment,
) {
var packagesRootFolder = getFolder(packagesRootPath);
macrosEnvironment.publicMacrosFolder.copyTo(packagesRootFolder);
macrosEnvironment.privateMacrosFolder.copyTo(packagesRootFolder);
config.add(
name: '_macros',
rootPath: getFolder('$packagesRootPath/_macros').path,
);
config.add(
name: 'macros',
rootPath: getFolder('$packagesRootPath/macros').path,
);
}
/// Build summary bundle for a single URI `package:foo/foo.dart`.
Future<File> buildPackageFooSummary({
required Map<String, String> files,
}) async {
var rootFolder = getFolder('$workspaceRootPath/foo');
writePackageConfig(
rootFolder.path,
PackageConfigFileBuilder()..add(name: 'foo', rootPath: rootFolder.path),
);
for (var entry in files.entries) {
newFile('${rootFolder.path}/${entry.key}', entry.value);
}
var targetFile = getFile(rootFolder.path);
var analysisDriver = driverFor(targetFile);
var bundleBytes = await analysisDriver.buildPackageBundle(
uriList: [
Uri.parse('package:foo/foo.dart'),
],
);
var bundleFile = getFile('/home/summaries/packages.sum');
bundleFile.writeAsBytesSync(bundleBytes);
// Delete, so it is not available as a file.
// We don't have a package config for it anyway, but just to be sure.
rootFolder.delete();
await disposeAnalysisContextCollection();
return bundleFile;
}
bool configureWithCommonMacros() {
try {
writeTestPackageConfig(
PackageConfigFileBuilder(),
macrosEnvironment: MacrosEnvironment.instance,
);
newFile(
'$testPackageLibPath/append.dart',
getMacroCode('append.dart'),
);
return true;
} catch (_) {
markTestSkipped('Cannot initialize macro environment.');
return false;
}
}
@override
void setUp() {
super.setUp();
writeTestPackageAnalysisOptionsFile(
analysisOptionsContent(experiments: experiments),
);
writeTestPackageConfig(
PackageConfigFileBuilder(),
);
}
void writePackageConfig(
String directoryPath,
PackageConfigFileBuilder config,
) {
var content = config.toContent(
toUriStr: toUriStr,
);
newPackageConfigJsonFile(directoryPath, content);
}
Future<File> writeSdkSummary() async {
var file = getFile('/home/summaries/sdk.sum');
var bytes = await buildSdkSummary(
resourceProvider: resourceProvider,
sdkPath: sdkRoot.path,
);
file.writeAsBytesSync(bytes);
return file;
}
void writeTestPackageAnalysisOptionsFile(String content) {
newAnalysisOptionsYamlFile(testPackageRootPath, content);
}
void writeTestPackageConfig(
PackageConfigFileBuilder config, {
String? languageVersion,
bool angularMeta = false,
bool ffi = false,
bool flutter = false,
bool js = false,
bool meta = false,
MacrosEnvironment? macrosEnvironment,
}) {
config = config.copy();
config.add(
name: 'test',
rootPath: testPackageRootPath,
languageVersion: languageVersion ?? testPackageLanguageVersion,
);
if (angularMeta) {
var angularMetaPath = addAngularMeta().parent.path;
config.add(name: 'angular_meta', rootPath: angularMetaPath);
}
if (ffi) {
var ffiPath = addFfi().parent.path;
config.add(name: 'ffi', rootPath: ffiPath);
}
if (flutter) {
var uiPath = addUI().parent.path;
config.add(name: 'ui', rootPath: uiPath);
var flutterPath = addFlutter().parent.path;
config.add(name: 'flutter', rootPath: flutterPath);
}
if (js) {
var jsPath = addJs().parent.path;
config.add(name: 'js', rootPath: jsPath);
}
if (meta || flutter) {
var metaPath = addMeta().parent.path;
config.add(name: 'meta', rootPath: metaPath);
}
if (macrosEnvironment != null) {
addMacrosEnvironment(config, macrosEnvironment);
}
writePackageConfig(testPackageRootPath, config);
}
void writeTestPackageConfigWithMeta() {
writeTestPackageConfig(PackageConfigFileBuilder(), meta: true);
}
void writeTestPackagePubspecYamlFile(String content) {
newPubspecYamlFile(testPackageRootPath, content);
}
}
class PubspecYamlFileDependency {
final String name;
final String version;
PubspecYamlFileDependency({
required this.name,
this.version = 'any',
});
}
mixin WithLanguage219Mixin on PubPackageResolutionTest {
@override
String? get testPackageLanguageVersion => '2.19';
}
mixin WithoutConstructorTearoffsMixin on PubPackageResolutionTest {
@override
String? get testPackageLanguageVersion => '2.14';
}
mixin WithoutEnhancedEnumsMixin on PubPackageResolutionTest {
@override
String? get testPackageLanguageVersion => '2.16';
}
mixin WithStrictCastsMixin on PubPackageResolutionTest {
/// Asserts that no errors are reported in [code] when implicit casts are
/// allowed, and that [expectedErrors] are reported for the same [code] when
/// implicit casts are not allowed.
Future<void> assertErrorsWithStrictCasts(
String code,
List<ExpectedError> expectedErrors,
) async {
await resolveTestCode(code);
assertNoErrorsInResult();
await disposeAnalysisContextCollection();
writeTestPackageAnalysisOptionsFile(
analysisOptionsContent(experiments: experiments, strictCasts: true),
);
await resolveTestFile();
assertErrorsInResult(expectedErrors);
}
/// Asserts that no errors are reported in [code], both when implicit casts
/// are allowed and when implicit casts are not allowed.
Future<void> assertNoErrorsWithStrictCasts(String code) async =>
assertErrorsWithStrictCasts(code, []);
}