3103e1e0f6
Fixes #44174. Bug: https://github.com/dart-lang/sdk/issues/44174 Change-Id: Ida53a11afc2e265cd510b0ac63c50f6b5ac93fdd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/171962 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
511 lines
16 KiB
Dart
511 lines
16 KiB
Dart
// 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.
|
|
|
|
import 'package:nnbd_migration/nnbd_migration.dart';
|
|
import 'package:nnbd_migration/src/front_end/migration_info.dart';
|
|
import 'package:nnbd_migration/src/front_end/path_mapper.dart';
|
|
import 'package:nnbd_migration/src/front_end/unit_renderer.dart';
|
|
import 'package:nnbd_migration/src/front_end/web/file_details.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import 'nnbd_migration_test_base.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(UnitRendererTest);
|
|
});
|
|
}
|
|
|
|
@reflectiveTest
|
|
class UnitRendererTest extends NnbdMigrationTestBase {
|
|
/// Render [libraryInfo], using a [MigrationInfo] which knows only about this
|
|
/// library.
|
|
List<FileDetails> renderUnits() {
|
|
var packageRoot = convertPath('/package');
|
|
var migrationInfo =
|
|
MigrationInfo(infos, {}, resourceProvider.pathContext, packageRoot);
|
|
|
|
var contents = <FileDetails>[];
|
|
for (var unitInfo in infos) {
|
|
contents.add(UnitRenderer(unitInfo, migrationInfo,
|
|
PathMapper(resourceProvider), 'AUTH_TOKEN')
|
|
.render());
|
|
}
|
|
return contents;
|
|
}
|
|
|
|
void test_conditionFalseInStrongMode() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int f(String s) {
|
|
if (s == null) {
|
|
return 0;
|
|
} else {
|
|
return s.length;
|
|
}
|
|
}
|
|
''', migratedContent: '''
|
|
int f(String s) {
|
|
if (s == null /* == false */) {
|
|
return 0;
|
|
} else {
|
|
return s.length;
|
|
}
|
|
}
|
|
''', warnOnWeakCode: true);
|
|
var output = renderUnits()[0];
|
|
expect(
|
|
_stripDataAttributes(output.regions),
|
|
contains(
|
|
'<span class="region informative-region">/* == false */</span>'));
|
|
expect(output.edits.keys,
|
|
contains('1 condition will be false in strong checking mode'));
|
|
}
|
|
|
|
void test_conditionTrueInStrongMode() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int f(String s) {
|
|
if (s != null) {
|
|
return s.length;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
''', migratedContent: '''
|
|
int f(String s) {
|
|
if (s != null /* == true */) {
|
|
return s.length;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
''', warnOnWeakCode: true);
|
|
var output = renderUnits()[0];
|
|
expect(
|
|
_stripDataAttributes(output.regions),
|
|
contains(
|
|
'<span class="region informative-region">/* == true */</span>'));
|
|
expect(output.edits.keys,
|
|
contains('1 condition will be true in strong checking mode'));
|
|
}
|
|
|
|
Future<void> test_editList_containsCount() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int a = null;
|
|
bool b = a.isEven;
|
|
''', migratedContent: '''
|
|
int? a = null;
|
|
bool b = a!.isEven;
|
|
''');
|
|
var output = renderUnits()[0];
|
|
var editList = output.edits;
|
|
expect(editList, hasLength(2));
|
|
}
|
|
|
|
Future<void> test_editList_containsEdits() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int a = null;
|
|
bool b = a.isEven;
|
|
''', migratedContent: '''
|
|
int? a = null;
|
|
bool b = a!.isEven;
|
|
''');
|
|
var output = renderUnits()[0];
|
|
// The null checks are higher priority than the assertions.
|
|
expect(output.edits.keys,
|
|
orderedEquals(['1 null check added', '1 type made nullable']));
|
|
var typesMadeNullable = output.edits['1 type made nullable'];
|
|
expect(typesMadeNullable, hasLength(1));
|
|
var typeMadeNullable = typesMadeNullable.single;
|
|
expect(typeMadeNullable.line, equals(1));
|
|
expect(typeMadeNullable.offset, equals(3));
|
|
expect(typeMadeNullable.explanation,
|
|
equals("Changed type 'int' to be nullable"));
|
|
var nullChecks = output.edits['1 null check added'];
|
|
expect(nullChecks, hasLength(1));
|
|
var nullCheck = nullChecks.single;
|
|
expect(nullCheck.line, equals(2));
|
|
expect(nullCheck.offset, equals(26));
|
|
expect(nullCheck.explanation,
|
|
equals('Added a non-null assertion to nullable expression'));
|
|
}
|
|
|
|
Future<void> test_editList_countsHintAcceptanceSingly() async {
|
|
await buildInfoForSingleTestFile('int f(int/*?*/ x) => x/*!*/;',
|
|
migratedContent: 'int f(int/*?*/ x) => x/*!*/;');
|
|
var output = renderUnits()[0];
|
|
expect(
|
|
output.edits.keys,
|
|
unorderedEquals([
|
|
'1 null check hint converted to null check',
|
|
'1 nullability hint converted to ?'
|
|
]));
|
|
}
|
|
|
|
Future<void> test_editList_countsHintAcceptanceSingly_late() async {
|
|
await buildInfoForSingleTestFile('/*late*/ int x = 0;',
|
|
migratedContent: '/*late*/ int x = 0;');
|
|
var output = renderUnits()[0];
|
|
expect(output.edits.keys,
|
|
unorderedEquals(['1 late hint converted to late keyword']));
|
|
}
|
|
|
|
Future<void> test_editList_countsHintAcceptanceSingly_lateFinal() async {
|
|
await buildInfoForSingleTestFile('/*late final*/ int x = 0;',
|
|
migratedContent: '/*late final*/ int x = 0;');
|
|
var output = renderUnits()[0];
|
|
expect(
|
|
output.edits.keys,
|
|
unorderedEquals(
|
|
['1 late final hint converted to late and final keywords']));
|
|
}
|
|
|
|
Future<void> test_editList_countsWhereOrNullSingly() async {
|
|
await buildInfoForSingleTestFile(
|
|
'''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int firstEven(Iterable<int> x)
|
|
=> x.firstWhere((x) => x.isEven, orElse: () => null);
|
|
''',
|
|
removeViaComments: false,
|
|
migratedContent: '''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int? firstEven(Iterable<int > x)
|
|
=> x.firstWherefirstWhereOrNull((x) => x.isEven, orElse: () => null);
|
|
''');
|
|
var output = renderUnits()[0];
|
|
expect(output.edits.keys,
|
|
unorderedEquals(['1 method name changed', '1 type made nullable']));
|
|
}
|
|
|
|
Future<void> test_editList_pluralHeader() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int a = null;
|
|
int b = null;
|
|
''', migratedContent: '''
|
|
int? a = null;
|
|
int? b = null;
|
|
''');
|
|
var output = renderUnits()[0];
|
|
expect(output.edits.keys.toList(), ['2 types made nullable']);
|
|
}
|
|
|
|
Future<void> test_handle_large_deleted_region_near_top_of_file() async {
|
|
await buildInfoForSingleTestFile('''
|
|
class C {
|
|
int hash(Iterable<int> elements) {
|
|
if (elements == null) {
|
|
return null.hashCode;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
List<int> x = [null];
|
|
''', migratedContent: '''
|
|
class C {
|
|
int hash(Iterable<int > elements) {
|
|
if (elements == null) {
|
|
return null.hashCode;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
List<int?> x = [null];
|
|
''', removeViaComments: false);
|
|
renderUnits();
|
|
// No assertions necessary; we are checking to make sure there is no crash.
|
|
}
|
|
|
|
Future<void> test_info_within_deleted_code() async {
|
|
await buildInfoForSingleTestFile('''
|
|
class C {
|
|
int hash(Iterable<int> elements) {
|
|
if (elements == null) {
|
|
return null.hashCode;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
List<int> x = [null];
|
|
''', migratedContent: '''
|
|
class C {
|
|
int hash(Iterable<int > elements) {
|
|
if (elements == null) {
|
|
return null.hashCode;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
List<int?> x = [null];
|
|
''', removeViaComments: false);
|
|
var output = renderUnits()[0];
|
|
// Strip out URLs and span IDs; they're not being tested here.
|
|
var navContent = output.navigationContent
|
|
.replaceAll(RegExp('href="[^"]*"'), 'href="..."')
|
|
.replaceAll(RegExp('id="[^"]*"'), 'id="..."');
|
|
expect(navContent, '''
|
|
class <span id="...">C</span> {
|
|
<a href="..." class="nav-link">int</a> <span id="...">hash</span>(<a href="..." class="nav-link">Iterable</a><<a href="..." class="nav-link">int</a> > <span id="...">elements</span>) {
|
|
if (<a href="..." class="nav-link">elements</a> <a href="..." class="nav-link">==</a> null) {
|
|
return null.<a href="..." class="nav-link">hashCode</a>;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
<a href="..." class="nav-link">List</a><<a href="..." class="nav-link">int</a>?> <span id="...">x</span> = [null];
|
|
''');
|
|
}
|
|
|
|
Future<void> test_inserted_lines() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int f(List<int> values)
|
|
=> values.firstWhere((i) => i.isEven, orElse: () => null);
|
|
''', migratedContent: '''
|
|
import 'package:collection/collection.dart' show IterableExtension;
|
|
|
|
int? f(List<int > values)
|
|
=> values.firstWherefirstWhereOrNull((i) => i.isEven/* , orElse: () => null */);
|
|
''');
|
|
var output = renderUnits()[0];
|
|
var regions = output.regions;
|
|
// We're not testing the correctness of the data path, so drop it.
|
|
regions = regions.replaceAll(RegExp(' data-path="[^"]*"'), '');
|
|
// Split the output into table rows.
|
|
var rows = regions.split(RegExp('(?=<tr)'));
|
|
// The table should begin with an added region for the added import; since
|
|
// adding the import involves adding 2 lines, the second and third lines
|
|
// should have a line number of "(new)", and then lines should resume
|
|
// counting at 2 after that.
|
|
expect(rows[0], '<table><tbody>');
|
|
expect(
|
|
rows[1],
|
|
'<tr><td class="line-no">1</td>'
|
|
'<td class="line-1">'
|
|
'<span class="region added-region" data-offset="0" data-line="1">'
|
|
"import 'package:collection/collection.dart' show IterableExtension;"
|
|
'</span></td></tr>');
|
|
expect(rows[2], '<tr><td class="line-no">(new)</td><td></td></tr>');
|
|
expect(rows[3], startsWith('<tr><td class="line-no">(new)</td><td>int'));
|
|
expect(rows[4],
|
|
startsWith('<tr><td class="line-no">2</td><td class="line-2">'));
|
|
}
|
|
|
|
void test_kindPriorityOrder() {
|
|
var nonDisplayedKinds = NullabilityFixKind.values.toSet();
|
|
for (var kind in UnitRenderer.kindPriorityOrder) {
|
|
expect(nonDisplayedKinds.remove(kind), isTrue);
|
|
}
|
|
// The only kinds that should not be displayed are those associated with a
|
|
// place where nothing interesting occurred.
|
|
expect(nonDisplayedKinds, {
|
|
NullabilityFixKind.typeNotMadeNullable,
|
|
NullabilityFixKind.typeNotMadeNullableDueToHint
|
|
});
|
|
}
|
|
|
|
Future<void> test_method_name_change() async {
|
|
addPackageFile('collection', 'collection.dart', '');
|
|
await buildInfoForSingleTestFile(
|
|
'''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int f(List<int> values)
|
|
=> values.firstWhere((i) => i.isEven, orElse: () => null);
|
|
''',
|
|
removeViaComments: false,
|
|
migratedContent: '''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int? f(List<int > values)
|
|
=> values.firstWherefirstWhereOrNull((i) => i.isEven, orElse: () => null);
|
|
''');
|
|
var output = renderUnits()[0];
|
|
var regions = output.regions;
|
|
// We aren't testing data-offset or data-line behaviors.
|
|
regions = regions.replaceAll(RegExp(' data-offset="[^"]*"'), '');
|
|
regions = regions.replaceAll(RegExp(' data-line="[^"]*"'), '');
|
|
expect(
|
|
regions,
|
|
contains('<span class="region removed-region">firstWhere</span>'
|
|
'<span class="region added-region">firstWhereOrNull</span>('));
|
|
}
|
|
|
|
Future<void> test_navContentContainsEscapedHtml() async {
|
|
await buildInfoForSingleTestFile('List<String> a = null;',
|
|
migratedContent: 'List<String >? a = null;');
|
|
var output = renderUnits()[0];
|
|
// Strip out URLs which will change; not being tested here.
|
|
var navContent =
|
|
output.navigationContent.replaceAll(RegExp('href=".*?"'), 'href="..."');
|
|
expect(
|
|
navContent,
|
|
contains(r'<a href="..." class="nav-link">List</a>'
|
|
r'<<a href="..." class="nav-link">String</a> >? '
|
|
r'<span id="o13">a</span> = null;'));
|
|
}
|
|
|
|
void test_nullAwarenessUnnecessaryInStrongMode() async {
|
|
await buildInfoForSingleTestFile('''
|
|
int f(String s) => s?.length;
|
|
''', migratedContent: '''
|
|
int f(String s) => s?.length;
|
|
''', warnOnWeakCode: true);
|
|
var output = renderUnits()[0];
|
|
expect(_stripDataAttributes(output.regions),
|
|
contains('s<span class="region informative-region">?</span>.length'));
|
|
expect(
|
|
output.edits.keys,
|
|
contains(
|
|
'1 null-aware access will be unnecessary in strong checking mode'));
|
|
}
|
|
|
|
Future<void> test_outputContains_addedType() async {
|
|
await buildInfoForSingleTestFile('''
|
|
void f() {
|
|
final a = <List<int>>[];
|
|
a.add([null]);
|
|
}
|
|
''', migratedContent: '''
|
|
void f() {
|
|
final List<List<int?>> a = <List<int > >[];
|
|
a.add([null]);
|
|
}
|
|
''');
|
|
var output = renderUnits()[0];
|
|
var regions = _stripDataAttributes(output.regions);
|
|
expect(
|
|
regions,
|
|
contains('final '
|
|
'<span class="region added-region">List<List<int?>></span>'
|
|
' a = <List<int'
|
|
'<span class="region informative-region"> </span>'
|
|
'>'
|
|
'<span class="region informative-region"> </span>'
|
|
'>[];'));
|
|
}
|
|
|
|
Future<void> test_outputContains_replacedVar() async {
|
|
await buildInfoForSingleTestFile('''
|
|
void f() {
|
|
var a = <List<int>>[];
|
|
a.add([null]);
|
|
}
|
|
''', migratedContent: '''
|
|
void f() {
|
|
varList<List<int?>> a = <List<int > >[];
|
|
a.add([null]);
|
|
}
|
|
''');
|
|
var output = renderUnits()[0];
|
|
var regions = _stripDataAttributes(output.regions);
|
|
expect(
|
|
regions,
|
|
contains('<span class="region removed-region">var</span>'
|
|
'<span class="region added-region">List<List<int?>></span>'
|
|
' a = <List<int'
|
|
'<span class="region informative-region"> </span>'
|
|
'>'
|
|
'<span class="region informative-region"> </span>'
|
|
'>[];'));
|
|
}
|
|
|
|
Future<void> test_outputContainsModifiedAndUnmodifiedRegions() async {
|
|
await buildInfoForSingleTestFile('int a = null;',
|
|
migratedContent: 'int? a = null;');
|
|
var output = renderUnits()[0];
|
|
var regions = _stripDataAttributes(output.regions);
|
|
expect(regions,
|
|
contains('int<span class="region added-region">?</span> a = null;'));
|
|
}
|
|
|
|
Future<void> test_project_with_parts() async {
|
|
// In this test, we migrate a library and its part file. Both files require
|
|
// addition of a `?`, but at different offsets. We make sure the `?`s get
|
|
// added at the correct locations in each file.
|
|
var files = {
|
|
convertPath('$projectPath/lib/a.dart'): '''
|
|
part 'b.dart';
|
|
|
|
int f() => null;
|
|
''',
|
|
convertPath('$projectPath/lib/b.dart'): '''
|
|
part of 'a.dart';
|
|
|
|
int g() => null;
|
|
''',
|
|
};
|
|
await buildInfoForTestFiles(files, includedRoot: projectPath);
|
|
var output = renderUnits();
|
|
expect(output[0].sourceCode, contains('int?'));
|
|
expect(output[1].sourceCode, contains('int?'));
|
|
}
|
|
|
|
Future<void> test_reference_to_sdk_file_with_parts() async {
|
|
await buildInfoForSingleTestFile('''
|
|
import 'dart:async';
|
|
Future<int> f(Future<int> x) {
|
|
return x.whenComplete(() {});
|
|
}
|
|
''', migratedContent: '''
|
|
import 'dart:async';
|
|
Future<int > f(Future<int > x) {
|
|
return x.whenComplete(() {});
|
|
}
|
|
''');
|
|
renderUnits();
|
|
// No assertions; we're just making sure there's no crash.
|
|
}
|
|
|
|
Future<void> test_regionsContainsEscapedHtml_ampersand() async {
|
|
await buildInfoForSingleTestFile('bool a = true && false;',
|
|
migratedContent: 'bool a = true && false;');
|
|
var output = renderUnits()[0];
|
|
expect(
|
|
output.regions,
|
|
contains('bool<span class="region informative-region" data-offset="4" '
|
|
'data-line="1"> </span> a = true && false;'));
|
|
}
|
|
|
|
Future<void> test_regionsContainsEscapedHtml_betweenRegions() async {
|
|
await buildInfoForSingleTestFile('List<String> a = null;',
|
|
migratedContent: 'List<String >? a = null;');
|
|
var output = renderUnits()[0];
|
|
var regions = _stripDataAttributes(output.regions);
|
|
expect(
|
|
regions,
|
|
contains(
|
|
'List<String<span class="region informative-region"> </span>>'
|
|
'<span class="region added-region">?</span> a = null;'));
|
|
}
|
|
|
|
Future<void> test_regionsContainsEscapedHtml_region() async {
|
|
await buildInfoForSingleTestFile('f(List<String> a) => a.join(",");',
|
|
migratedContent: 'f(List<String > a) => a.join(",");');
|
|
var output = renderUnits()[0];
|
|
var regions = _stripDataAttributes(output.regions);
|
|
expect(
|
|
regions,
|
|
contains(
|
|
'List<String<span class="region informative-region"> </span>'
|
|
'><span class="region informative-region"> </span>'));
|
|
}
|
|
|
|
UnitInfo unit(String path, String content, {List<RegionInfo> regions}) {
|
|
return UnitInfo(convertPath(path))
|
|
..content = content
|
|
..regions.addAll(regions);
|
|
}
|
|
|
|
/// Strip out data attributes which are not being tested here.
|
|
String _stripDataAttributes(String html) =>
|
|
html.replaceAll(RegExp(' data-[^=]+="[^"]+"'), '');
|
|
}
|