// 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 resolveTestFile() async { return await session.getResolvedUnit2(testFile) as ResolvedUnitResult; } Future 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 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 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 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 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 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 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 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 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 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")); } Future 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 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 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 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 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 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 getNonInformativeRegions(List regions) { return regions .where((region) => region.kind != NullabilityFixKind.typeNotMadeNullable && region.kind != NullabilityFixKind.typeNotMadeNullableDueToHint) .toList(); } Future 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 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 edits) => assertEdit( edit: edits.single, offset: content.indexOf(textToRemove), length: textToRemove.length, replacement: '')); } Future 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 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 edits) => assertEdit( edit: edits.single, offset: content.indexOf(textToRemove), length: textToRemove.length, replacement: '')); } Future 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 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 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 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 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 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 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 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 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 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 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 test_downcast_with_traces() async { var content = 'List/*!*/ f(List/*?*/ x) => x;'; var migratedContent = 'List/*!*/ f(List/*?*/ x) => x as List;'; 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'; 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/*?*/'), 'Non-nullability reason': content.indexOf('List/*!*/'), 'Nullability reason for type argument 0': content.indexOf('int/*?*/'), 'Non-nullability reason for type argument 0': content.indexOf('int/*!*/') }); } Future 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 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 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 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 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 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 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 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 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 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 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 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 test_method_name_change() async { addPackageFile('collection', 'collection.dart', ''); var content = ''' import 'package:collection/collection.dart'; int f(List values, int/*?*/ x) => values.firstWhere((i) => (i + x).isEven, orElse: () => null); '''; var migratedContent = ''' import 'package:collection/collection.dart'; int? f(List 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 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 edits) => assertEdit( edit: edits.single, offset: content.indexOf(hintText), length: hintText.length, replacement: ''))); } Future 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 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 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 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 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 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 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.', unit.content.indexOf('int? i) {}'), contains('parameter 0 of C.')); } Future 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 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], '.m', unit.content.indexOf('int? i) {}'), contains('parameter 0 of .m')); } Future 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 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 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 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 test_trace_refers_to_variable_initializer() async { var unit = await buildInfoForSingleTestFile(''' void f(int/*?*/ i) { var x = [i]; int y = x[0]; } ''', migratedContent: ''' void f(int/*?*/ i) { var x = [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('[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 test_trace_substitutionNode() async { var unit = await buildInfoForSingleTestFile(''' class C {} C c; Map x = {}; String/*!*/ y = x[0]; ''', migratedContent: ''' class C {} C? c; Map 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 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 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 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 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 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 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; }); } }