26d9fbbd04
Change-Id: I0217fefb2747f2f9bd757bc041b53acaf3d1a8f1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195495 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
1661 lines
52 KiB
Dart
1661 lines
52 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:analyzer/dart/analysis/results.dart';
|
|
import 'package:analyzer/dart/ast/ast.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:nnbd_migration/nnbd_migration.dart';
|
|
import 'package:nnbd_migration/src/front_end/info_builder.dart';
|
|
import 'package:nnbd_migration/src/front_end/migration_info.dart';
|
|
import 'package:nnbd_migration/src/hint_action.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import 'analysis_abstract.dart';
|
|
import 'nnbd_migration_test_base.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(BuildEnclosingMemberDescriptionTest);
|
|
defineReflectiveTests(InfoBuilderTest);
|
|
});
|
|
}
|
|
|
|
@reflectiveTest
|
|
class BuildEnclosingMemberDescriptionTest extends AbstractAnalysisTest {
|
|
Future<ResolvedUnitResult> resolveTestFile() async {
|
|
return await session.getResolvedUnit2(testFile) as ResolvedUnitResult;
|
|
}
|
|
|
|
Future<void> test_classConstructor_named() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
C.aaa();
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var constructor = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(constructor),
|
|
equals("the constructor 'C.aaa'"));
|
|
}
|
|
|
|
Future<void> test_classConstructor_unnamed() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
C();
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var constructor = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(constructor),
|
|
equals("the default constructor of 'C'"));
|
|
}
|
|
|
|
Future<void> test_classField() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
int i;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
FieldDeclaration fieldDeclaration =
|
|
class_.members.single as FieldDeclaration;
|
|
var field = fieldDeclaration.fields.variables[0];
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(field),
|
|
equals("the field 'C.i'"));
|
|
}
|
|
|
|
Future<void> test_classField_from_type() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
int i;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
FieldDeclaration fieldDeclaration =
|
|
class_.members.single as FieldDeclaration;
|
|
var type = fieldDeclaration.fields.type;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(type),
|
|
equals("the field 'C.i'"));
|
|
}
|
|
|
|
Future<void> test_classGetter() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
int get aaa => 7;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var getter = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(getter),
|
|
equals("the getter 'C.aaa'"));
|
|
}
|
|
|
|
Future<void> test_classMethod() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
int aaa() => 7;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var method = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(method),
|
|
equals("the method 'C.aaa'"));
|
|
}
|
|
|
|
Future<void> test_classOperator() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
bool operator ==(Object other) => false;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var operator = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(operator),
|
|
equals("the operator 'C.=='"));
|
|
}
|
|
|
|
Future<void> test_classSetter() async {
|
|
addTestFile(r'''
|
|
class C {
|
|
void set aaa(value) {}
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ClassDeclaration class_ =
|
|
result.unit.declarations.single as ClassDeclaration;
|
|
var setter = class_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(setter),
|
|
equals("the setter 'C.aaa='"));
|
|
}
|
|
|
|
Future<void> test_extensionMethod() async {
|
|
addTestFile(r'''
|
|
extension E on List {
|
|
int aaa() => 7;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ExtensionDeclaration extension_ =
|
|
result.unit.declarations.single as ExtensionDeclaration;
|
|
var method = extension_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(method),
|
|
equals("the method 'E.aaa'"));
|
|
}
|
|
|
|
Future<void> test_extensionMethod_unnamed() async {
|
|
addTestFile(r'''
|
|
extension on List {
|
|
int aaa() => 7;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
ExtensionDeclaration extension_ =
|
|
result.unit.declarations.single as ExtensionDeclaration;
|
|
var method = extension_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(method),
|
|
equals("the method 'aaa' in unnamed extension on List<dynamic>"));
|
|
}
|
|
|
|
Future<void> test_mixinMethod() async {
|
|
addTestFile(r'''
|
|
mixin C {
|
|
int aaa() => 7;
|
|
}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
MixinDeclaration mixin_ =
|
|
result.unit.declarations.single as MixinDeclaration;
|
|
var method = mixin_.members.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(method),
|
|
equals("the method 'C.aaa'"));
|
|
}
|
|
|
|
Future<void> test_topLevelFunction() async {
|
|
addTestFile(r'''
|
|
void aaa(value) {}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
var function = result.unit.declarations.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(function),
|
|
equals("the function 'aaa'"));
|
|
}
|
|
|
|
Future<void> test_topLevelGetter() async {
|
|
addTestFile(r'''
|
|
int get aaa => 7;
|
|
''');
|
|
var result = await resolveTestFile();
|
|
var getter = result.unit.declarations.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(getter),
|
|
equals("the getter 'aaa'"));
|
|
}
|
|
|
|
Future<void> test_topLevelSetter() async {
|
|
addTestFile(r'''
|
|
void set aaa(value) {}
|
|
''');
|
|
var result = await resolveTestFile();
|
|
var setter = result.unit.declarations.single;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(setter),
|
|
equals("the setter 'aaa='"));
|
|
}
|
|
|
|
Future<void> test_topLevelVariable() async {
|
|
addTestFile(r'''
|
|
int i;
|
|
''');
|
|
var result = await resolveTestFile();
|
|
TopLevelVariableDeclaration topLevelVariableDeclaration =
|
|
result.unit.declarations.single as TopLevelVariableDeclaration;
|
|
var variable = topLevelVariableDeclaration.variables.variables[0];
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(variable),
|
|
equals("the variable 'i'"));
|
|
}
|
|
|
|
Future<void> test_topLevelVariable_from_type() async {
|
|
addTestFile(r'''
|
|
int i;
|
|
''');
|
|
var result = await resolveTestFile();
|
|
TopLevelVariableDeclaration topLevelVariableDeclaration =
|
|
result.unit.declarations.single as TopLevelVariableDeclaration;
|
|
var type = topLevelVariableDeclaration.variables.type;
|
|
expect(InfoBuilder.buildEnclosingMemberDescription(type),
|
|
equals("the variable 'i'"));
|
|
}
|
|
}
|
|
|
|
@reflectiveTest
|
|
class InfoBuilderTest extends NnbdMigrationTestBase {
|
|
/// Assert various properties of the given [edit].
|
|
bool assertEdit(
|
|
{@required EditDetail edit, int offset, int length, String replacement}) {
|
|
expect(edit, isNotNull);
|
|
if (offset != null) {
|
|
expect(edit.offset, offset);
|
|
}
|
|
if (length != null) {
|
|
expect(edit.length, length);
|
|
}
|
|
if (replacement != null) {
|
|
expect(edit.replacement, replacement);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
List<RegionInfo> getNonInformativeRegions(List<RegionInfo> regions) {
|
|
return regions
|
|
.where((region) =>
|
|
region.kind != NullabilityFixKind.typeNotMadeNullable &&
|
|
region.kind != NullabilityFixKind.typeNotMadeNullableDueToHint)
|
|
.toList();
|
|
}
|
|
|
|
Future<void> test_addLate() async {
|
|
var content = '''
|
|
f() {
|
|
String s;
|
|
if (1 == 2) s = "Hello";
|
|
g(s);
|
|
}
|
|
g(String /*!*/ s) {}
|
|
''';
|
|
var migratedContent = '''
|
|
f() {
|
|
late String s;
|
|
if (1 == 2) s = "Hello";
|
|
g(s);
|
|
}
|
|
g(String /*!*/ s) {}
|
|
''';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
var region = regions[0];
|
|
assertRegion(
|
|
region: region,
|
|
offset: 8,
|
|
length: 4,
|
|
explanation: 'Added a late keyword',
|
|
kind: NullabilityFixKind.addLate);
|
|
}
|
|
|
|
Future<void> test_addLate_dueToHint() async {
|
|
var content = '/*late*/ int x = 0;';
|
|
var migratedContent = '/*late*/ int x = 0;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
var textToRemove = '/*late*/ ';
|
|
assertRegionPair(regions, 0,
|
|
offset1: migratedContent.indexOf('/*'),
|
|
length1: 2,
|
|
offset2: migratedContent.indexOf('*/'),
|
|
length2: 2,
|
|
explanation: 'Added a late keyword, due to a hint',
|
|
kind: NullabilityFixKind.addLateDueToHint,
|
|
edits: (List<EditDetail> edits) => assertEdit(
|
|
edit: edits.single,
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: ''));
|
|
}
|
|
|
|
Future<void> test_addLate_dueToTestSetup() async {
|
|
addTestCorePackage();
|
|
var content = '''
|
|
import 'package:test/test.dart';
|
|
void main() {
|
|
int i;
|
|
setUp(() {
|
|
i = 1;
|
|
});
|
|
test('a', () {
|
|
f(i);
|
|
});
|
|
f(int /*?*/ i) {}
|
|
}
|
|
''';
|
|
var migratedContent = '''
|
|
import 'package:test/test.dart';
|
|
void main() {
|
|
late int i;
|
|
setUp(() {
|
|
i = 1;
|
|
});
|
|
test('a', () {
|
|
f(i);
|
|
});
|
|
f(int /*?*/ i) {}
|
|
}
|
|
''';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(3));
|
|
var region = regions[0];
|
|
assertRegion(
|
|
region: region,
|
|
offset: 49,
|
|
length: 4,
|
|
explanation: 'Added a late keyword, due to assignment in `setUp`',
|
|
kind: NullabilityFixKind.addLateDueToTestSetup);
|
|
}
|
|
|
|
Future<void> test_addLateFinal_dueToHint() async {
|
|
var content = '/*late final*/ int x = 0;';
|
|
var migratedContent = '/*late final*/ int x = 0;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
var textToRemove = '/*late final*/ ';
|
|
assertRegionPair(regions, 0,
|
|
offset1: migratedContent.indexOf('/*'),
|
|
length1: 2,
|
|
offset2: migratedContent.indexOf('*/'),
|
|
length2: 2,
|
|
explanation: 'Added late and final keywords, due to a hint',
|
|
kind: NullabilityFixKind.addLateFinalDueToHint,
|
|
edits: (List<EditDetail> edits) => assertEdit(
|
|
edit: edits.single,
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: ''));
|
|
}
|
|
|
|
Future<void> test_compound_assignment_nullable_result() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
abstract class C {
|
|
C/*?*/ operator+(int i);
|
|
}
|
|
void f(C/*!*/ a, int b) {
|
|
a += b;
|
|
}
|
|
''', migratedContent: '''
|
|
abstract class C {
|
|
C/*?*/ operator+(int i);
|
|
}
|
|
void f(C/*!*/ a, int b) {
|
|
a += b;
|
|
}
|
|
''');
|
|
var operator = '+=';
|
|
var operatorOffset = unit.content.indexOf(operator);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == operatorOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: operator.length,
|
|
explanation: 'Compound assignment has bad combined type',
|
|
kind: NullabilityFixKind.compoundAssignmentHasBadCombinedType,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_compound_assignment_nullable_source() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int/*?*/ a, int b) {
|
|
a += b;
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int/*?*/ a, int b) {
|
|
a += b;
|
|
}
|
|
''');
|
|
var operator = '+=';
|
|
var operatorOffset = unit.content.indexOf(operator);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == operatorOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: operator.length,
|
|
explanation: 'Compound assignment has nullable source',
|
|
kind: NullabilityFixKind.compoundAssignmentHasNullableSource,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_conditionFalseInStrongMode_expression() async {
|
|
var unit = await buildInfoForSingleTestFile(
|
|
'int f(String s) => s == null ? 0 : s.length;',
|
|
migratedContent:
|
|
'int f(String s) => s == null /* == false */ ? 0 : s.length;',
|
|
warnOnWeakCode: true);
|
|
var insertedComment = '/* == false */';
|
|
var insertedCommentOffset = unit.content.indexOf(insertedComment);
|
|
var region = unit.regions
|
|
.where((region) => region.offset == insertedCommentOffset)
|
|
.single;
|
|
assertRegion(
|
|
region: region,
|
|
length: insertedComment.length,
|
|
explanation: 'Condition will always be false in strong checking mode',
|
|
kind: NullabilityFixKind.conditionFalseInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_conditionFalseInStrongMode_if() async {
|
|
var unit = 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 insertedComment = '/* == false */';
|
|
var insertedCommentOffset = unit.content.indexOf(insertedComment);
|
|
var region = unit.regions
|
|
.where((region) => region.offset == insertedCommentOffset)
|
|
.single;
|
|
assertRegion(
|
|
region: region,
|
|
length: insertedComment.length,
|
|
explanation: 'Condition will always be false in strong checking mode',
|
|
kind: NullabilityFixKind.conditionFalseInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_conditionTrueInStrongMode_expression() async {
|
|
var unit = await buildInfoForSingleTestFile(
|
|
'int f(String s) => s != null ? s.length : 0;',
|
|
migratedContent:
|
|
'int f(String s) => s != null /* == true */ ? s.length : 0;',
|
|
warnOnWeakCode: true);
|
|
var insertedComment = '/* == true */';
|
|
var insertedCommentOffset = unit.content.indexOf(insertedComment);
|
|
var region = unit.regions
|
|
.where((region) => region.offset == insertedCommentOffset)
|
|
.single;
|
|
assertRegion(
|
|
region: region,
|
|
length: insertedComment.length,
|
|
explanation: 'Condition will always be true in strong checking mode',
|
|
kind: NullabilityFixKind.conditionTrueInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_conditionTrueInStrongMode_if() async {
|
|
var unit = 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 insertedComment = '/* == true */';
|
|
var insertedCommentOffset = unit.content.indexOf(insertedComment);
|
|
var region = unit.regions
|
|
.where((region) => region.offset == insertedCommentOffset)
|
|
.single;
|
|
assertRegion(
|
|
region: region,
|
|
length: insertedComment.length,
|
|
explanation: 'Condition will always be true in strong checking mode',
|
|
kind: NullabilityFixKind.conditionTrueInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_discardCondition() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void g(int i) {
|
|
print(i.isEven);
|
|
if (i != null) print('NULL');
|
|
}
|
|
''', migratedContent: '''
|
|
void g(int i) {
|
|
print(i.isEven);
|
|
/* if (i != null) */ print('NULL');
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: 38,
|
|
length: 3,
|
|
kind: NullabilityFixKind.removeDeadCode);
|
|
assertRegion(
|
|
region: regions[1],
|
|
offset: 56,
|
|
length: 3,
|
|
kind: NullabilityFixKind.removeDeadCode);
|
|
}
|
|
|
|
Future<void> test_downcast_nonNullable() async {
|
|
var content = 'int/*!*/ f(num/*!*/ n) => n;';
|
|
var migratedContent = 'int/*!*/ f(num/*!*/ n) => n as int;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = getNonInformativeRegions(unit.regions);
|
|
expect(regions, hasLength(1));
|
|
var region = regions.single;
|
|
var regionTarget = ' as int';
|
|
assertRegion(
|
|
region: region,
|
|
offset: migratedContent.indexOf(regionTarget),
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.downcastExpression,
|
|
edits: isEmpty,
|
|
traces: isEmpty);
|
|
}
|
|
|
|
Future<void> test_downcast_nonNullable_to_nullable() async {
|
|
var content = 'int/*?*/ f(num/*!*/ n) => n;';
|
|
// TODO(paulberry): we should actually cast to `int`, not `int?`, because we
|
|
// know `n` is non-nullable.
|
|
var migratedContent = 'int/*?*/ f(num/*!*/ n) => n as int?;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = getNonInformativeRegions(unit.regions);
|
|
var regionTarget = ' as int?';
|
|
var offset = migratedContent.indexOf(regionTarget);
|
|
var region = regions.where((region) => region.offset == offset).single;
|
|
// TODO(paulberry): once we are correctly casting to `int`, not `int?`, this
|
|
// should be classified as a downcast. Currently it's classified as a side
|
|
// cast.
|
|
assertRegion(
|
|
region: region,
|
|
offset: offset,
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.otherCastExpression,
|
|
edits: isEmpty,
|
|
traces: isEmpty);
|
|
}
|
|
|
|
Future<void> test_downcast_nullable() async {
|
|
var content = 'int/*?*/ f(num/*?*/ n) => n;';
|
|
var migratedContent = 'int/*?*/ f(num/*?*/ n) => n as int?;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = getNonInformativeRegions(unit.regions);
|
|
var regionTarget = ' as int?';
|
|
var offset = migratedContent.indexOf(regionTarget);
|
|
var region = regions.where((region) => region.offset == offset).single;
|
|
assertRegion(
|
|
region: region,
|
|
offset: offset,
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.downcastExpression,
|
|
edits: isEmpty,
|
|
traces: isEmpty);
|
|
}
|
|
|
|
Future<void> test_downcast_nullable_to_nonNullable() async {
|
|
var content = 'int/*!*/ f(num/*?*/ n) => n;';
|
|
var migratedContent = 'int/*!*/ f(num/*?*/ n) => n as int;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = getNonInformativeRegions(unit.regions);
|
|
var regionTarget = ' as int';
|
|
var offset = migratedContent.indexOf(regionTarget);
|
|
var region = regions.where((region) => region.offset == offset).single;
|
|
assertRegion(
|
|
region: region,
|
|
offset: offset,
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.downcastExpression,
|
|
edits: isEmpty,
|
|
traces: isNotEmpty);
|
|
}
|
|
|
|
Future<void> test_downcast_with_traces() async {
|
|
var content = 'List<int/*!*/>/*!*/ f(List<int/*?*/>/*?*/ x) => x;';
|
|
var migratedContent =
|
|
'List<int/*!*/>/*!*/ f(List<int/*?*/>/*?*/ x) => x as List<int>;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.regions.where(
|
|
(region) => region.kind == NullabilityFixKind.downcastExpression);
|
|
expect(regions, hasLength(1));
|
|
var region = regions.single;
|
|
var regionTarget = ' as List<int>';
|
|
assertRegion(
|
|
region: region,
|
|
offset: migratedContent.indexOf(regionTarget),
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.downcastExpression,
|
|
edits: isEmpty,
|
|
traces: isNotEmpty);
|
|
var traceDescriptionToOffset = {
|
|
for (var trace in region.traces)
|
|
trace.description: trace.entries[0].target.offset
|
|
};
|
|
expect(traceDescriptionToOffset, {
|
|
'Nullability reason': content.indexOf('List<int/*?*/>/*?*/'),
|
|
'Non-nullability reason': content.indexOf('List<int/*!*/>/*!*/'),
|
|
'Nullability reason for type argument 0': content.indexOf('int/*?*/'),
|
|
'Non-nullability reason for type argument 0': content.indexOf('int/*!*/')
|
|
});
|
|
}
|
|
|
|
Future<void> test_dynamicValueIsUsed() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
bool f(int i) {
|
|
if (i == null) return true;
|
|
else return false;
|
|
}
|
|
void g() {
|
|
dynamic i = null;
|
|
f(i);
|
|
}
|
|
''', migratedContent: '''
|
|
bool f(int? i) {
|
|
if (i == null) return true;
|
|
else return false;
|
|
}
|
|
void g() {
|
|
dynamic i = null;
|
|
f(i);
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: region,
|
|
offset: 11,
|
|
explanation: "Changed type 'int' to be nullable");
|
|
assertEdit(edit: edits[0], offset: 10, replacement: '/*!*/');
|
|
assertEdit(edit: edits[1], offset: 10, replacement: '/*?*/');
|
|
}
|
|
|
|
Future<void> test_expressionFunctionReturnTarget() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
String g() => 1 == 2 ? "Hello" : null;
|
|
''', migratedContent: '''
|
|
String? g() => 1 == 2 ? "Hello" : null;
|
|
''');
|
|
assertInTargets(targets: unit.targets, offset: 7, length: 1); // "g"
|
|
var regions = unit.regions;
|
|
expect(regions, hasLength(1));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: 6,
|
|
explanation: "Changed type 'String' to be nullable");
|
|
}
|
|
|
|
Future<void> test_function_typed_parameter_made_nullable_due_to_hint() async {
|
|
var content = 'f(void g(int i)/*?*/) {}';
|
|
var migratedContent = 'f(void g(int i)/*?*/) {}';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
var textToRemove = '/*?*/';
|
|
assertRegionPair(regions, 0,
|
|
offset1: migratedContent.indexOf(textToRemove),
|
|
length1: 2,
|
|
offset2: migratedContent.indexOf(textToRemove) + 3,
|
|
length2: 2,
|
|
explanation:
|
|
"Changed type 'void Function(int)' to be nullable, due to a "
|
|
'nullability hint',
|
|
kind: NullabilityFixKind.makeTypeNullableDueToHint,
|
|
traces: isNotNull, edits: (List<EditDetail> edits) {
|
|
expect(edits, hasLength(2));
|
|
var editsByDescription = {for (var edit in edits) edit.description: edit};
|
|
assertEdit(
|
|
edit: editsByDescription['Change to /*!*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '/*!*/');
|
|
assertEdit(
|
|
edit: editsByDescription['Remove /*?*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '');
|
|
return true;
|
|
});
|
|
}
|
|
|
|
Future<void> test_increment_nullable_result() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
abstract class C {
|
|
C/*?*/ operator+(int i);
|
|
}
|
|
void f(C/*!*/ a) {
|
|
a++;
|
|
}
|
|
''', migratedContent: '''
|
|
abstract class C {
|
|
C/*?*/ operator+(int i);
|
|
}
|
|
void f(C/*!*/ a) {
|
|
a++;
|
|
}
|
|
''');
|
|
var operator = '++';
|
|
var operatorOffset = unit.content.indexOf(operator);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == operatorOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: operator.length,
|
|
explanation: 'Compound assignment has bad combined type',
|
|
kind: NullabilityFixKind.compoundAssignmentHasBadCombinedType,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_increment_nullable_source() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int/*?*/ a) {
|
|
a++;
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int/*?*/ a) {
|
|
a++;
|
|
}
|
|
''');
|
|
var operator = '++';
|
|
var operatorOffset = unit.content.indexOf(operator);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == operatorOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: operator.length,
|
|
explanation: 'Compound assignment has nullable source',
|
|
kind: NullabilityFixKind.compoundAssignmentHasNullableSource,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_insertedRequired_fieldFormal_hint() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C {
|
|
int level;
|
|
int level2;
|
|
C({this.level}) : this.level2 = level + 1;
|
|
}
|
|
''', migratedContent: '''
|
|
class C {
|
|
int level;
|
|
int level2;
|
|
C({required this.level}) : this.level2 = level + 1;
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: region,
|
|
offset: 44,
|
|
length: 9,
|
|
explanation: "Add 'required' keyword to parameter 'level' in 'C.'",
|
|
kind: NullabilityFixKind.addRequired);
|
|
assertEdit(
|
|
edit: edits[0], offset: 42, length: 0, replacement: '/*required*/ ');
|
|
}
|
|
|
|
Future<void> test_insertedRequired_fieldFormal() async {
|
|
addMetaPackage();
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
import 'package:meta/meta.dart';
|
|
class C {
|
|
int level;
|
|
int level2;
|
|
C({this.level}) : this.level2 = level + 1;
|
|
}
|
|
''', migratedContent: '''
|
|
import 'package:meta/meta.dart';
|
|
class C {
|
|
int level;
|
|
int level2;
|
|
C({required this.level}) : this.level2 = level + 1;
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: region,
|
|
offset: 77,
|
|
length: 9,
|
|
explanation: "Add 'required' keyword to parameter 'level' in 'C.'",
|
|
kind: NullabilityFixKind.addRequired);
|
|
assertEdit(
|
|
edit: edits[0], offset: 75, length: 0, replacement: '@required ');
|
|
}
|
|
|
|
Future<void> test_insertedRequired_parameter_hint() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C {
|
|
int level = 0;
|
|
bool f({int lvl}) => lvl >= level;
|
|
}
|
|
''', migratedContent: '''
|
|
class C {
|
|
int level = 0;
|
|
bool f({required int lvl}) => lvl >= level;
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: region,
|
|
offset: 39,
|
|
length: 9,
|
|
explanation: "Add 'required' keyword to parameter 'lvl' in 'C.f'",
|
|
kind: NullabilityFixKind.addRequired);
|
|
assertEdit(
|
|
edit: edits[0], offset: 37, length: 0, replacement: '/*required*/ ');
|
|
}
|
|
|
|
Future<void> test_insertedRequired_parameter_metaPrefixed() async {
|
|
addMetaPackage();
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
import 'package:meta/meta.dart' as meta;
|
|
class C {
|
|
int level = 0;
|
|
bool f({int lvl}) => lvl >= level;
|
|
}
|
|
''', migratedContent: '''
|
|
import 'package:meta/meta.dart' as meta;
|
|
class C {
|
|
int level = 0;
|
|
bool f({required int lvl}) => lvl >= level;
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertEdit(
|
|
edit: edits[0], offset: 78, length: 0, replacement: '@meta.required ');
|
|
}
|
|
|
|
Future<void> test_insertedRequired_parameter() async {
|
|
addMetaPackage();
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
import 'package:meta/meta.dart';
|
|
class C {
|
|
int level = 0;
|
|
bool f({int lvl}) => lvl >= level;
|
|
}
|
|
''', migratedContent: '''
|
|
import 'package:meta/meta.dart';
|
|
class C {
|
|
int level = 0;
|
|
bool f({required int lvl}) => lvl >= level;
|
|
}
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(1));
|
|
var region = regions[0];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: region,
|
|
offset: 72,
|
|
length: 9,
|
|
explanation: "Add 'required' keyword to parameter 'lvl' in 'C.f'",
|
|
kind: NullabilityFixKind.addRequired);
|
|
assertEdit(
|
|
edit: edits[0], offset: 70, length: 0, replacement: '@required ');
|
|
}
|
|
|
|
Future<void> test_insertParens() async {
|
|
var originalContent = '''
|
|
class C {
|
|
C operator+(C c) => null;
|
|
}
|
|
C/*!*/ _f(C c) => c + c;
|
|
''';
|
|
var migratedContent = '''
|
|
class C {
|
|
C? operator+(C c) => null;
|
|
}
|
|
C/*!*/ _f(C c) => (c + c)!;
|
|
''';
|
|
var unit = await buildInfoForSingleTestFile(originalContent,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(3));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: migratedContent.indexOf('? operator'),
|
|
length: 1,
|
|
explanation: "Changed type 'C' to be nullable");
|
|
assertRegion(
|
|
region: regions[1],
|
|
offset: migratedContent.indexOf('/*!*/'),
|
|
length: 5,
|
|
explanation: "Type 'C' was not made nullable due to a hint",
|
|
kind: NullabilityFixKind.typeNotMadeNullableDueToHint);
|
|
assertRegion(
|
|
region: regions[2],
|
|
offset: migratedContent.indexOf('!;'),
|
|
length: 1,
|
|
explanation: 'Added a non-null assertion to nullable expression',
|
|
kind: NullabilityFixKind.checkExpression);
|
|
}
|
|
|
|
Future<void> test_method_name_change() async {
|
|
addPackageFile('collection', 'collection.dart', '');
|
|
var content = '''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int f(List<int> values, int/*?*/ x)
|
|
=> values.firstWhere((i) => (i + x).isEven,
|
|
orElse: () => null);
|
|
''';
|
|
var migratedContent = '''
|
|
import 'package:collection/collection.dart';
|
|
|
|
int? f(List<int > values, int/*?*/ x)
|
|
=> values.firstWherefirstWhereOrNull((i) => (i + x!).isEven,
|
|
orElse: () => null);
|
|
''';
|
|
await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent, removeViaComments: false);
|
|
}
|
|
|
|
void test_nullAwareAssignment_remove() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
int f(int/*!*/ x, int y) => x ??= y;
|
|
''', migratedContent: '''
|
|
int f(int/*!*/ x, int y) => x ??= y;
|
|
''', warnOnWeakCode: false, removeViaComments: false);
|
|
var codeToRemove = ' ??= y';
|
|
var removalOffset = unit.content.indexOf(codeToRemove);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == removalOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: codeToRemove.length,
|
|
explanation:
|
|
'Removed a null-aware assignment, because the target cannot be '
|
|
'null',
|
|
kind: NullabilityFixKind.removeDeadCode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
void test_nullAwareAssignment_unnecessaryInStrongMode() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
int f(int/*!*/ x, int y) => x ??= y;
|
|
''', migratedContent: '''
|
|
int f(int/*!*/ x, int y) => x ??= y;
|
|
''', warnOnWeakCode: true);
|
|
var operator = '??=';
|
|
var operatorOffset = unit.content.indexOf(operator);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == operatorOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: operator.length,
|
|
explanation:
|
|
'Null-aware assignment will be unnecessary in strong checking mode',
|
|
kind: NullabilityFixKind.nullAwareAssignmentUnnecessaryInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
void test_nullAwarenessUnnecessaryInStrongMode() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
int f(String s) => s?.length;
|
|
''', migratedContent: '''
|
|
int f(String s) => s?.length;
|
|
''', warnOnWeakCode: true);
|
|
var question = '?';
|
|
var questionOffset = unit.content.indexOf(question);
|
|
var region =
|
|
unit.regions.where((region) => region.offset == questionOffset).single;
|
|
assertRegion(
|
|
region: region,
|
|
length: question.length,
|
|
explanation:
|
|
'Null-aware access will be unnecessary in strong checking mode',
|
|
kind: NullabilityFixKind.nullAwarenessUnnecessaryInStrongMode,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_nullCheck_dueToHint() async {
|
|
var content = 'int f(int/*?*/ x) => x/*!*/;';
|
|
var migratedContent = 'int f(int/*?*/ x) => x/*!*/;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(4));
|
|
assertRegionPair(regions, 0,
|
|
kind: NullabilityFixKind.makeTypeNullableDueToHint);
|
|
var hintText = '/*!*/';
|
|
assertRegionPair(regions, 2,
|
|
offset1: migratedContent.indexOf(hintText),
|
|
length1: 2,
|
|
offset2: migratedContent.indexOf(hintText) + 3,
|
|
length2: 2,
|
|
explanation: 'Accepted a null check hint',
|
|
kind: NullabilityFixKind.checkExpressionDueToHint,
|
|
traces: isNotEmpty,
|
|
edits: ((List<EditDetail> edits) => assertEdit(
|
|
edit: edits.single,
|
|
offset: content.indexOf(hintText),
|
|
length: hintText.length,
|
|
replacement: '')));
|
|
}
|
|
|
|
Future<void> test_nullCheck_onMemberAccess() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C {
|
|
int value;
|
|
C([this.value]);
|
|
void f() {
|
|
value.sign;
|
|
}
|
|
}
|
|
''', migratedContent: '''
|
|
class C {
|
|
int? value;
|
|
C([this.value]);
|
|
void f() {
|
|
value!.sign;
|
|
}
|
|
}
|
|
''');
|
|
var regions = unit.regions;
|
|
expect(regions, hasLength(2));
|
|
// regions[0] is `int?`.
|
|
var region = regions[1];
|
|
var edits = region.edits;
|
|
assertRegion(
|
|
region: regions[1],
|
|
offset: 65,
|
|
explanation: 'Added a non-null assertion to nullable expression',
|
|
kind: NullabilityFixKind.checkExpression);
|
|
assertEdit(edit: edits[0], offset: 64, length: 0, replacement: '/*!*/');
|
|
}
|
|
|
|
Future<void> test_parameter_fromOverriddenField_explicit() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class A {
|
|
int m;
|
|
}
|
|
class B extends A {
|
|
void set m(Object p) {}
|
|
}
|
|
void f(A a) => a.m = null;
|
|
''', migratedContent: '''
|
|
class A {
|
|
int? m;
|
|
}
|
|
class B extends A {
|
|
void set m(Object? p) {}
|
|
}
|
|
void f(A a) => a.m = null;
|
|
''');
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: 15,
|
|
explanation: "Changed type 'int' to be nullable");
|
|
assertRegion(
|
|
region: regions[1],
|
|
offset: 61,
|
|
explanation: "Changed type 'Object' to be nullable");
|
|
|
|
expect(regions[0].traces, hasLength(1));
|
|
var trace = regions[0].traces.first;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(2));
|
|
assertTraceEntry(unit, entries[0], 'A.m', unit.content.indexOf('int?'),
|
|
contains('A.m (test.dart:2:3)'));
|
|
}
|
|
|
|
Future<void> test_removal_handles_offsets_correctly() async {
|
|
var originalContent = '''
|
|
void f(num n, int/*?*/ i) {
|
|
if (n is! int) return;
|
|
print((n as int).isEven);
|
|
print(i + 1);
|
|
}
|
|
''';
|
|
// Note: even though `as int` is removed, it still shows up in the
|
|
// preview, since we show deleted text.
|
|
var migratedContent = '''
|
|
void f(num n, int/*?*/ i) {
|
|
if (n is! int ) return;
|
|
print((n as int).isEven);
|
|
print(i! + 1);
|
|
}
|
|
''';
|
|
var unit = await buildInfoForSingleTestFile(originalContent,
|
|
migratedContent: migratedContent, removeViaComments: false);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(4));
|
|
assertRegionPair(regions, 0,
|
|
kind: NullabilityFixKind.makeTypeNullableDueToHint);
|
|
assertRegion(
|
|
region: regions[2],
|
|
offset: migratedContent.indexOf(' as int'),
|
|
length: ' as int'.length,
|
|
explanation: 'Discarded a downcast that is now unnecessary',
|
|
kind: NullabilityFixKind.removeAs);
|
|
assertRegion(
|
|
region: regions[3],
|
|
offset: migratedContent.indexOf('! + 1'),
|
|
explanation: 'Added a non-null assertion to nullable expression',
|
|
kind: NullabilityFixKind.checkExpression);
|
|
}
|
|
|
|
Future<void> test_returnDetailTarget() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
String g() {
|
|
return 1 == 2 ? "Hello" : null;
|
|
}
|
|
''', migratedContent: '''
|
|
String? g() {
|
|
return 1 == 2 ? "Hello" : null;
|
|
}
|
|
''');
|
|
assertInTargets(targets: unit.targets, offset: 7, length: 1); // "g"
|
|
var regions = unit.regions;
|
|
expect(regions, hasLength(1));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: 6,
|
|
explanation: "Changed type 'String' to be nullable");
|
|
}
|
|
|
|
Future<void> test_suspicious_cast() async {
|
|
var content = '''
|
|
int f(Object o) {
|
|
if (o is! String) return 0;
|
|
return o;
|
|
}
|
|
''';
|
|
var migratedContent = '''
|
|
int f(Object o) {
|
|
if (o is! String ) return 0;
|
|
return o as int;
|
|
}
|
|
''';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = getNonInformativeRegions(unit.regions);
|
|
expect(regions, hasLength(1));
|
|
var region = regions.single;
|
|
var regionTarget = ' as int';
|
|
assertRegion(
|
|
region: region,
|
|
offset: migratedContent.indexOf(regionTarget),
|
|
length: regionTarget.length,
|
|
kind: NullabilityFixKind.otherCastExpression,
|
|
edits: isEmpty);
|
|
}
|
|
|
|
Future<void> test_trace_constructor_named() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C {
|
|
C.named(int i) {}
|
|
}
|
|
void f() {
|
|
C.named(null);
|
|
}
|
|
''', migratedContent: '''
|
|
class C {
|
|
C.named(int? i) {}
|
|
}
|
|
void f() {
|
|
C.named(null);
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((info) => info.offset == unit.content.indexOf('? i) {}'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
assertTraceEntry(unit, entries[0], 'C.named',
|
|
unit.content.indexOf('int? i) {}'), contains('parameter 0 of C.named'));
|
|
}
|
|
|
|
Future<void> test_trace_constructor_unnamed() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C {
|
|
C(int i) {}
|
|
}
|
|
void f() {
|
|
C(null);
|
|
}
|
|
''', migratedContent: '''
|
|
class C {
|
|
C(int? i) {}
|
|
}
|
|
void f() {
|
|
C(null);
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((info) => info.offset == unit.content.indexOf('? i) {}'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
assertTraceEntry(
|
|
unit,
|
|
entries[0],
|
|
'C.<unnamed>',
|
|
unit.content.indexOf('int? i) {}'),
|
|
contains('parameter 0 of C.<unnamed>'));
|
|
}
|
|
|
|
Future<void> test_trace_deadCode() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int/*!*/ i) {
|
|
if (i == null) return;
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int/*!*/ i) {
|
|
/* if (i == null) return; */
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where(
|
|
(regionInfo) => regionInfo.offset == unit.content.indexOf('/* if'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Non-nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(2));
|
|
// Entry 0 is the nullability of f's argument
|
|
assertTraceEntry(unit, entries[0], 'f', unit.content.indexOf('int'),
|
|
contains('parameter 0 of f'));
|
|
// Entry 1 is the edge from f's argument to never, due to the `/*!*/` hint.
|
|
assertTraceEntry(unit, entries[1], 'f', unit.content.indexOf('int'),
|
|
'explicitly hinted to be non-nullable');
|
|
}
|
|
|
|
Future<void> test_trace_extension_unnamed() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
extension on String {
|
|
m(int i) {}
|
|
}
|
|
void f() {
|
|
"".m(null);
|
|
}
|
|
''', migratedContent: '''
|
|
extension on String {
|
|
m(int? i) {}
|
|
}
|
|
void f() {
|
|
"".m(null);
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((info) => info.offset == unit.content.indexOf('? i) {}'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
assertTraceEntry(
|
|
unit,
|
|
entries[0],
|
|
'<unnamed extension>.m',
|
|
unit.content.indexOf('int? i) {}'),
|
|
contains('parameter 0 of <unnamed>.m'));
|
|
}
|
|
|
|
Future<void> test_trace_nullableType() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int i) {} // f
|
|
void g(int i) { // g
|
|
f(i);
|
|
}
|
|
void h() {
|
|
g(null);
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int? i) {} // f
|
|
void g(int? i) { // g
|
|
f(i);
|
|
}
|
|
void h() {
|
|
g(null);
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((regionInfo) =>
|
|
regionInfo.offset == unit.content.indexOf('? i) {} // f'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(6));
|
|
// Entry 0 is the nullability of f's argument
|
|
assertTraceEntry(unit, entries[0], 'f',
|
|
unit.content.indexOf('int? i) {} // f'), contains('parameter 0 of f'),
|
|
hintActions: {
|
|
HintActionKind.addNullableHint,
|
|
HintActionKind.addNonNullableHint
|
|
});
|
|
|
|
// Entry 1 is the edge from g's argument to f's argument, due to g's call to
|
|
// f.
|
|
assertTraceEntry(
|
|
unit, entries[1], 'g', unit.content.indexOf('i);'), 'data flow');
|
|
// Entry 2 is the nullability of g's argument
|
|
assertTraceEntry(unit, entries[2], 'g',
|
|
unit.content.indexOf('int? i) { // g'), contains('parameter 0 of g'),
|
|
hintActions: {
|
|
HintActionKind.addNullableHint,
|
|
HintActionKind.addNonNullableHint
|
|
});
|
|
// Entry 3 is the edge from null to g's argument, due to h's call to g.
|
|
assertTraceEntry(
|
|
unit, entries[3], 'h', unit.content.indexOf('null'), 'data flow');
|
|
// Entry 4 is the nullability of the null literal.
|
|
assertTraceEntry(unit, entries[4], 'h', unit.content.indexOf('null'),
|
|
contains('null literal'));
|
|
// Entry 5 is the edge from always to null.
|
|
// TODO(paulberry): this edge provides no additional useful information and
|
|
// shouldn't be included in the trace.
|
|
assertTraceEntry(unit, entries[5], 'h', unit.content.indexOf('null'),
|
|
'literal expression');
|
|
}
|
|
|
|
Future<void> test_trace_nullCheck() async {
|
|
var unit = await buildInfoForSingleTestFile('int f(int/*?*/ i) => i + 1;',
|
|
migratedContent: 'int f(int/*?*/ i) => i! + 1;');
|
|
var region = unit.regions
|
|
.where((regionInfo) => regionInfo.offset == unit.content.indexOf('! +'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(2));
|
|
// Entry 0 is the nullability of the type of i.
|
|
assertTraceEntry(unit, entries[0], 'f', unit.content.indexOf('int/*?*/'),
|
|
contains('parameter 0 of f'));
|
|
// Entry 1 is the edge from always to the type of i.
|
|
// TODO(paulberry): this edge provides no additional useful information and
|
|
// shouldn't be included in the trace.
|
|
assertTraceEntry(unit, entries[1], 'f', unit.content.indexOf('int/*?*/'),
|
|
'explicitly hinted to be nullable');
|
|
}
|
|
|
|
Future<void> test_trace_nullCheck_notNullableReason() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int i) { // f
|
|
assert(i != null);
|
|
}
|
|
void g(int i) { // g
|
|
f(i); // call f
|
|
}
|
|
void h(int/*?*/ i) {
|
|
g(i);
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int i) { // f
|
|
assert(i != null);
|
|
}
|
|
void g(int i) { // g
|
|
f(i); // call f
|
|
}
|
|
void h(int/*?*/ i) {
|
|
g(i!);
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((regionInfo) => regionInfo.offset == unit.content.indexOf('!)'))
|
|
.single;
|
|
expect(region.traces, hasLength(2));
|
|
// Trace 0 is the nullability reason; we don't care about that right now.
|
|
// Trace 1 is the non-nullability reason.
|
|
var trace = region.traces[1];
|
|
expect(trace.description, 'Non-nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(4));
|
|
// Entry 0 is the nullability of g's argument
|
|
assertTraceEntry(unit, entries[0], 'g',
|
|
unit.content.indexOf('int i) { // g'), contains('parameter 0 of g'));
|
|
// Entry 1 is the edge from g's argument to f's argument, due to g's call to
|
|
// f.
|
|
assertTraceEntry(unit, entries[1], 'g',
|
|
unit.content.indexOf('i); // call f'), 'data flow');
|
|
// Entry 2 is the nullability of f's argument
|
|
assertTraceEntry(unit, entries[2], 'f',
|
|
unit.content.indexOf('int i) { // f'), contains('parameter 0 of f'));
|
|
// Entry 3 is the edge f's argument to never, due to the assert.
|
|
assertTraceEntry(unit, entries[3], 'f', unit.content.indexOf('assert'),
|
|
'value asserted to be non-null');
|
|
}
|
|
|
|
Future<void> test_trace_nullCheckHint() async {
|
|
var unit = await buildInfoForSingleTestFile('int f(int/*?*/ i) => i/*!*/;',
|
|
migratedContent: 'int f(int/*?*/ i) => i/*!*/;');
|
|
var region = unit.regions
|
|
.where(
|
|
(regionInfo) => regionInfo.offset == unit.content.indexOf('/*!*/'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Reason');
|
|
expect(trace.entries, hasLength(1));
|
|
assertTraceEntry(unit, trace.entries.single, 'f',
|
|
unit.content.indexOf('i/*!*/'), 'Null check hint');
|
|
}
|
|
|
|
Future<void> test_trace_refers_to_variable_initializer() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
void f(int/*?*/ i) {
|
|
var x = <int>[i];
|
|
int y = x[0];
|
|
}
|
|
''', migratedContent: '''
|
|
void f(int/*?*/ i) {
|
|
var x = <int?>[i];
|
|
int? y = x[0];
|
|
}
|
|
''');
|
|
var region = unit.regions
|
|
.where((regionInfo) => regionInfo.offset == unit.content.indexOf('? y'))
|
|
.single;
|
|
expect(region.traces, hasLength(1));
|
|
var trace = region.traces.single;
|
|
expect(trace.description, 'Nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(8));
|
|
// Entry 0 is the nullability of y
|
|
assertTraceEntry(unit, entries[0], 'f.y', unit.content.indexOf('int? y'),
|
|
contains('f.y'));
|
|
// Entry 1 is the edge from the list element type of x to y, due to array
|
|
// indexing.
|
|
assertTraceEntry(unit, entries[1], 'f.y', unit.content.indexOf('x[0]'),
|
|
contains('data flow'));
|
|
// Entry 2 is the nullability of the implicit list element type of x
|
|
assertTraceEntry(unit, entries[2], 'f.x', unit.content.indexOf('x ='),
|
|
contains('type argument 0 of f.x'));
|
|
// Entry 3 is the edge from the explicit list element type on the RHS of x
|
|
// to the implicit list element type on the LHS of x
|
|
assertTraceEntry(unit, entries[3], 'f.x', unit.content.indexOf('<int?>[i]'),
|
|
contains('data flow'));
|
|
// Entry 4 is the explicit list element type on the RHS of x
|
|
assertTraceEntry(unit, entries[4], 'f.x', unit.content.indexOf('int?>[i]'),
|
|
contains('list element type'));
|
|
// Entry 5 is the edge from the parameter i to the list literal
|
|
assertTraceEntry(unit, entries[5], 'f.x', unit.content.indexOf('i]'),
|
|
contains('data flow'));
|
|
// Entry 6 is the nullability of the parameter i
|
|
assertTraceEntry(unit, entries[6], 'f', unit.content.indexOf('int/*?*/'),
|
|
contains('parameter 0 of f'));
|
|
// Entry 7 is the edge due to the explicit /*?*/ hint
|
|
assertTraceEntry(unit, entries[7], 'f', unit.content.indexOf('int/*?*/'),
|
|
contains('explicitly hinted to be nullable'));
|
|
}
|
|
|
|
Future<void> test_trace_substitutionNode() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
class C<T extends Object/*!*/> {}
|
|
|
|
C<int /*?*/ > c;
|
|
|
|
Map<int, String> x = {};
|
|
String/*!*/ y = x[0];
|
|
''', migratedContent: '''
|
|
class C<T extends Object/*!*/> {}
|
|
|
|
C<int /*?*/ >? c;
|
|
|
|
Map<int , String > x = {};
|
|
String/*!*/ y = x[0]!;
|
|
''');
|
|
var region = unit.regions
|
|
.where((regionInfo) => regionInfo.offset == unit.content.indexOf('!;'))
|
|
.single;
|
|
// The "why nullable" node associated with adding the `!` is a substitution
|
|
// node, and we don't currently generate a trace for a substitution node.
|
|
// TODO(paulberry): fix this.
|
|
// We do, however, generate a trace for "why not nullable".
|
|
expect(region.traces, hasLength(1));
|
|
expect(region.traces[0].description, 'Non-nullability reason');
|
|
}
|
|
|
|
Future<void> test_type_made_nullable() async {
|
|
var unit = await buildInfoForSingleTestFile('''
|
|
String g() => 1 == 2 ? "Hello" : null;
|
|
''', migratedContent: '''
|
|
String? g() => 1 == 2 ? "Hello" : null;
|
|
''');
|
|
assertInTargets(targets: unit.targets, offset: 7, length: 1); // "g"
|
|
var regions = unit.regions;
|
|
expect(regions, hasLength(1));
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: 6,
|
|
explanation: "Changed type 'String' to be nullable");
|
|
}
|
|
|
|
Future<void> test_type_made_nullable_due_to_hint() async {
|
|
var content = 'int/*?*/ x = 0;';
|
|
var migratedContent = 'int/*?*/ x = 0;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.fixRegions;
|
|
expect(regions, hasLength(2));
|
|
var textToRemove = '/*?*/';
|
|
assertRegionPair(regions, 0,
|
|
offset1: migratedContent.indexOf(textToRemove),
|
|
length1: 2,
|
|
offset2: migratedContent.indexOf(textToRemove) + 3,
|
|
length2: 2,
|
|
explanation:
|
|
"Changed type 'int' to be nullable, due to a nullability hint",
|
|
kind: NullabilityFixKind.makeTypeNullableDueToHint,
|
|
traces: isNotNull, edits: (List<EditDetail> edits) {
|
|
expect(edits, hasLength(2));
|
|
var editsByDescription = {for (var edit in edits) edit.description: edit};
|
|
assertEdit(
|
|
edit: editsByDescription['Change to /*!*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '/*!*/');
|
|
assertEdit(
|
|
edit: editsByDescription['Remove /*?*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '');
|
|
return true;
|
|
});
|
|
}
|
|
|
|
Future<void> test_type_not_made_nullable() async {
|
|
var unit = await buildInfoForSingleTestFile('int i = 0;',
|
|
migratedContent: 'int i = 0;');
|
|
var region = unit.regions
|
|
.where((regionInfo) => regionInfo.offset == unit.content.indexOf(' i'))
|
|
.single;
|
|
expect(region.length, 1);
|
|
expect(region.lineNumber, 1);
|
|
expect(region.explanation, "Type 'int' was not made nullable");
|
|
expect(region.edits.map((edit) => edit.description).toSet(),
|
|
{'Add /*?*/ hint', 'Add /*!*/ hint'});
|
|
var trace = region.traces.first;
|
|
expect(trace.description, 'Non-nullability reason');
|
|
var entries = trace.entries;
|
|
expect(entries, hasLength(1));
|
|
assertTraceEntry(unit, entries[0], 'i', unit.content.indexOf('int'),
|
|
'No reason found to make nullable');
|
|
expect(region.kind, NullabilityFixKind.typeNotMadeNullable);
|
|
}
|
|
|
|
Future<void> test_type_not_made_nullable_due_to_hint() async {
|
|
var content = 'int/*!*/ i = 0;';
|
|
var migratedContent = 'int/*!*/ i = 0;';
|
|
var unit = await buildInfoForSingleTestFile(content,
|
|
migratedContent: migratedContent);
|
|
var regions = unit.regions;
|
|
expect(regions, hasLength(1));
|
|
var textToRemove = '/*!*/';
|
|
assertRegion(
|
|
region: regions[0],
|
|
offset: migratedContent.indexOf(textToRemove),
|
|
length: 5,
|
|
explanation: "Type 'int' was not made nullable due to a hint",
|
|
kind: NullabilityFixKind.typeNotMadeNullableDueToHint,
|
|
traces: isNotNull,
|
|
edits: (List<EditDetail> edits) {
|
|
expect(edits, hasLength(2));
|
|
var editsByDescription = {
|
|
for (var edit in edits) edit.description: edit
|
|
};
|
|
assertEdit(
|
|
edit: editsByDescription['Change to /*?*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '/*?*/');
|
|
assertEdit(
|
|
edit: editsByDescription['Remove /*!*/ hint'],
|
|
offset: content.indexOf(textToRemove),
|
|
length: textToRemove.length,
|
|
replacement: '');
|
|
return true;
|
|
});
|
|
}
|
|
}
|