blob: b941ca3182b8214c53dbc0dfa1f610a74b51ae70 [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/results.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:meta/meta.dart';
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/nnbd_migration.dart';
import 'package:nnbd_migration/src/front_end/dartfix_listener.dart';
import 'package:nnbd_migration/src/front_end/driver_provider_impl.dart';
import 'package:nnbd_migration/src/front_end/info_builder.dart';
import 'package:nnbd_migration/src/front_end/instrumentation_listener.dart';
import 'package:nnbd_migration/src/front_end/migration_info.dart';
import 'package:nnbd_migration/src/front_end/non_nullable_fix.dart';
import 'package:nnbd_migration/src/front_end/offset_mapper.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utilities/test_logger.dart';
import 'analysis_abstract.dart';
class ListenerClient implements DartFixListenerClient {
@override
void onException(String detail) {
fail('Unexpected call to onException($detail)');
}
@override
void onFatalError(String detail) {
fail('Unexpected call to onFatalError($detail)');
}
@override
void onMessage(String detail) {
fail('Unexpected call to onMessage($detail)');
}
}
@reflectiveTest
class NnbdMigrationTestBase extends AbstractAnalysisTest {
/// The information produced by the InfoBuilder, or `null` if [buildInfo] has
/// not yet completed.
Set<UnitInfo> infos;
NodeMapper nodeMapper;
/// Assert that some target in [targets] has various properties.
void assertInTargets(
{@required Iterable<NavigationTarget> targets,
int offset,
int length,
OffsetMapper offsetMapper}) {
var failureReasons = [
if (offset != null) 'offset: $offset',
if (length != null) 'length: $length',
if (offsetMapper != null) 'match a custom offset mapper',
].join(' and ');
offsetMapper ??= OffsetMapper.identity;
expect(targets.any((t) {
return (offset == null || offset == offsetMapper.map(t.offset)) &&
(length == null || length == t.length);
}), isTrue, reason: 'Expected one of $targets to contain $failureReasons');
}
/// Assert various properties of the given [region]. If an [offset] is
/// provided but no [length] is provided, a default length of `1` will be
/// used.
void assertRegion(
{@required RegionInfo region,
int offset,
int length,
Object explanation = anything,
Object edits = anything,
Object traces = anything,
Object kind = NullabilityFixKind.makeTypeNullable}) {
if (offset != null) {
expect(region.offset, offset);
expect(region.length, length ?? 1);
} else if (length != null) {
expect(region.length, length);
}
expect(region.kind, kind);
expect(region.edits, edits);
expect(region.explanation, explanation);
expect(region.traces, traces);
}
/// Asserts various properties of the pair of [regions], `regions[index]` and
/// `regions[index + 1]`. The expected offsets and lengths are specified
/// separately; everything else is asserted using the same matcher.
void assertRegionPair(List<RegionInfo> regions, int index,
{int offset1,
int length1,
int offset2,
int length2,
Object explanation = anything,
Object edits = anything,
Object traces = anything,
Object kind = anything}) {
assertRegion(
region: regions[index],
offset: offset1,
length: length1,
explanation: explanation,
edits: edits,
traces: traces,
kind: kind);
assertRegion(
region: regions[index + 1],
offset: offset2,
length: length2,
explanation: explanation,
edits: edits,
traces: traces,
kind: kind);
}
void assertTraceEntry(UnitInfo unit, TraceEntryInfo entryInfo,
String function, int offset, Object descriptionMatcher,
{Set<HintActionKind> hintActions}) {
if (offset == null) {
expect(entryInfo.target, isNull);
} else {
assert(offset >= 0);
var lineInfo = LineInfo.fromContent(unit.content);
var expectedLocation = lineInfo.getLocation(offset);
expect(entryInfo.target.filePath, unit.path);
expect(entryInfo.target.line, expectedLocation.lineNumber);
expect(unit.offsetMapper.map(entryInfo.target.offset), offset);
}
expect(entryInfo.function, function);
expect(entryInfo.description, descriptionMatcher);
if (hintActions != null) {
assertTraceHintActions(unit, entryInfo, hintActions, offset);
}
}
void assertTraceHintActions(UnitInfo unit, TraceEntryInfo traceEntry,
Set<HintActionKind> expectedHints, int nodeOffset) {
final actionsByKind = Map<HintActionKind, HintAction>.fromIterables(
traceEntry.hintActions.map((action) => action.kind),
traceEntry.hintActions);
expect(actionsByKind, hasLength(expectedHints.length));
for (final expectedHint in expectedHints) {
final action = actionsByKind[expectedHint];
expect(action, isNotNull);
final node = nodeMapper.nodeForId(action.nodeId);
expect(node, isNotNull);
expect(unit.offsetMapper.map(node.codeReference.offset), nodeOffset);
}
}
/// Uses the InfoBuilder to build information for [testFile].
///
/// The information is stored in [infos].
Future<void> buildInfo(
{bool removeViaComments = true, bool warnOnWeakCode = false}) async {
var includedRoot = resourceProvider.pathContext.dirname(testFile);
await _buildMigrationInfo([testFile],
includedRoot: includedRoot,
removeViaComments: removeViaComments,
warnOnWeakCode: warnOnWeakCode);
}
/// Uses the InfoBuilder to build information for a single test file.
///
/// Asserts that [originalContent] is migrated to [migratedContent]. Returns
/// the singular UnitInfo which was built.
Future<UnitInfo> buildInfoForSingleTestFile(String originalContent,
{@required String migratedContent,
bool removeViaComments = true,
bool warnOnWeakCode = false}) async {
addTestFile(originalContent);
await buildInfo(
removeViaComments: removeViaComments, warnOnWeakCode: warnOnWeakCode);
// Ignore info for dart:core.
var filteredInfos = [
for (var info in infos)
if (!info.path.contains('core.dart')) info
];
expect(filteredInfos, hasLength(1));
var unit = filteredInfos[0];
expect(unit.path, testFile);
expect(unit.content, migratedContent);
return unit;
}
/// Uses the [InfoBuilder] to build information for test files.
///
/// Returns the singular [UnitInfo] which was built.
Future<List<UnitInfo>> buildInfoForTestFiles(Map<String, String> files,
{String includedRoot}) async {
var testPaths = <String>[];
files.forEach((String path, String content) {
newFile(path, content: content);
testPaths.add(path);
});
await _buildMigrationInfo(testPaths, includedRoot: includedRoot);
// Ignore info for dart:core.
var filteredInfos = [
for (var info in infos)
if (!info.path.contains('core.dart')) info
];
return filteredInfos;
}
void setUp() {
super.setUp();
nodeMapper = SimpleNodeMapper();
}
/// Uses the InfoBuilder to build information for files at [testPaths], which
/// should all share a common parent directory, [includedRoot].
Future<void> _buildMigrationInfo(List<String> testPaths,
{String includedRoot,
bool removeViaComments = true,
bool warnOnWeakCode = false}) async {
// Compute the analysis results.
var server = DriverProviderImpl(resourceProvider, driver.analysisContext);
// Run the migration engine.
var listener = DartFixListener(server, ListenerClient());
var instrumentationListener = InstrumentationListener();
var adapter = NullabilityMigrationAdapter(listener);
var migration = NullabilityMigration(adapter, getLineInfo,
permissive: false,
instrumentation: instrumentationListener,
removeViaComments: removeViaComments,
warnOnWeakCode: warnOnWeakCode);
Future<void> _forEachPath(
void Function(ResolvedUnitResult) callback) async {
for (var testPath in testPaths) {
var result = await driver.currentSession.getResolvedUnit(testPath);
callback(result);
}
}
await _forEachPath(migration.prepareInput);
expect(migration.unmigratedDependencies, isEmpty);
await _forEachPath(migration.processInput);
await _forEachPath(migration.finalizeInput);
migration.finish();
// Build the migration info.
var info = instrumentationListener.data;
var logger = TestLogger(false);
var builder = InfoBuilder(resourceProvider, includedRoot, info, listener,
migration, nodeMapper, logger);
infos = await builder.explainMigration();
}
}