// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:nnbd_migration/nnbd_migration.dart'; import 'package:nnbd_migration/src/front_end/migration_info.dart'; import 'package:nnbd_migration/src/front_end/path_mapper.dart'; import 'package:nnbd_migration/src/front_end/unit_renderer.dart'; import 'package:nnbd_migration/src/front_end/web/file_details.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'nnbd_migration_test_base.dart'; void main() { defineReflectiveSuite(() { defineReflectiveTests(UnitRendererTest); }); } @reflectiveTest class UnitRendererTest extends NnbdMigrationTestBase { /// Render [libraryInfo], using a [MigrationInfo] which knows only about this /// library. List renderUnits() { var packageRoot = convertPath('/package'); var migrationInfo = MigrationInfo(infos, {}, resourceProvider.pathContext, packageRoot); var contents = []; for (var unitInfo in infos) { contents.add(UnitRenderer(unitInfo, migrationInfo, PathMapper(resourceProvider), 'AUTH_TOKEN') .render()); } return contents; } void test_conditionFalseInStrongMode() async { await buildInfoForSingleTestFile(''' int f(String s) { if (s == null) { return 0; } else { return s.length; } } ''', migratedContent: ''' int f(String s) { if (s == null /* == false */) { return 0; } else { return s.length; } } ''', warnOnWeakCode: true); var output = renderUnits()[0]; expect( _stripDataAttributes(output.regions), contains( '/* == false */')); expect(output.edits.keys, contains('1 condition will be false in strong checking mode')); } void test_conditionTrueInStrongMode() async { await buildInfoForSingleTestFile(''' int f(String s) { if (s != null) { return s.length; } else { return 0; } } ''', migratedContent: ''' int f(String s) { if (s != null /* == true */) { return s.length; } else { return 0; } } ''', warnOnWeakCode: true); var output = renderUnits()[0]; expect( _stripDataAttributes(output.regions), contains( '/* == true */')); expect(output.edits.keys, contains('1 condition will be true in strong checking mode')); } Future test_editList_containsCount() async { await buildInfoForSingleTestFile(''' int a = null; bool b = a.isEven; ''', migratedContent: ''' int? a = null; bool b = a!.isEven; '''); var output = renderUnits()[0]; var editList = output.edits; expect(editList, hasLength(2)); } Future test_editList_containsEdits() async { await buildInfoForSingleTestFile(''' int a = null; bool b = a.isEven; ''', migratedContent: ''' int? a = null; bool b = a!.isEven; '''); var output = renderUnits()[0]; // The null checks are higher priority than the assertions. expect(output.edits.keys, orderedEquals(['1 null check added', '1 type made nullable'])); var typesMadeNullable = output.edits['1 type made nullable']; expect(typesMadeNullable, hasLength(1)); var typeMadeNullable = typesMadeNullable.single; expect(typeMadeNullable.line, equals(1)); expect(typeMadeNullable.offset, equals(3)); expect(typeMadeNullable.explanation, equals("Changed type 'int' to be nullable")); var nullChecks = output.edits['1 null check added']; expect(nullChecks, hasLength(1)); var nullCheck = nullChecks.single; expect(nullCheck.line, equals(2)); expect(nullCheck.offset, equals(26)); expect(nullCheck.explanation, equals('Added a non-null assertion to nullable expression')); } Future test_editList_countsHintAcceptanceSingly() async { await buildInfoForSingleTestFile('int f(int/*?*/ x) => x/*!*/;', migratedContent: 'int f(int/*?*/ x) => x/*!*/;'); var output = renderUnits()[0]; expect( output.edits.keys, unorderedEquals([ '1 null check hint converted to null check', '1 nullability hint converted to ?' ])); } Future test_editList_countsHintAcceptanceSingly_late() async { await buildInfoForSingleTestFile('/*late*/ int x = 0;', migratedContent: '/*late*/ int x = 0;'); var output = renderUnits()[0]; expect(output.edits.keys, unorderedEquals(['1 late hint converted to late keyword'])); } Future test_editList_countsHintAcceptanceSingly_lateFinal() async { await buildInfoForSingleTestFile('/*late final*/ int x = 0;', migratedContent: '/*late final*/ int x = 0;'); var output = renderUnits()[0]; expect( output.edits.keys, unorderedEquals( ['1 late final hint converted to late and final keywords'])); } Future test_editList_countsWhereOrNullSingly() async { await buildInfoForSingleTestFile( ''' import 'package:collection/collection.dart'; int firstEven(Iterable x) => x.firstWhere((x) => x.isEven, orElse: () => null); ''', removeViaComments: false, migratedContent: ''' import 'package:collection/collection.dart'; int? firstEven(Iterable x) => x.firstWherefirstWhereOrNull((x) => x.isEven, orElse: () => null); '''); var output = renderUnits()[0]; expect(output.edits.keys, unorderedEquals(['1 method name changed', '1 type made nullable'])); } Future test_editList_pluralHeader() async { await buildInfoForSingleTestFile(''' int a = null; int b = null; ''', migratedContent: ''' int? a = null; int? b = null; '''); var output = renderUnits()[0]; expect(output.edits.keys.toList(), ['2 types made nullable']); } Future test_handle_large_deleted_region_near_top_of_file() async { await buildInfoForSingleTestFile(''' class C { int hash(Iterable elements) { if (elements == null) { return null.hashCode; } return 0; } } List x = [null]; ''', migratedContent: ''' class C { int hash(Iterable elements) { if (elements == null) { return null.hashCode; } return 0; } } List x = [null]; ''', removeViaComments: false); renderUnits(); // No assertions necessary; we are checking to make sure there is no crash. } Future test_info_within_deleted_code() async { await buildInfoForSingleTestFile(''' class C { int hash(Iterable elements) { if (elements == null) { return null.hashCode; } return 0; } } List x = [null]; ''', migratedContent: ''' class C { int hash(Iterable elements) { if (elements == null) { return null.hashCode; } return 0; } } List x = [null]; ''', removeViaComments: false); var output = renderUnits()[0]; // Strip out URLs and span IDs; they're not being tested here. var navContent = output.navigationContent .replaceAll(RegExp('href="[^"]*"'), 'href="..."') .replaceAll(RegExp('id="[^"]*"'), 'id="..."'); expect(navContent, ''' class C { int hash(Iterable<int > elements) { if (elements == null) { return null.hashCode; } return 0; } } List<int?> x = [null]; '''); } Future test_inserted_lines() async { await buildInfoForSingleTestFile(''' int f(List values) => values.firstWhere((i) => i.isEven, orElse: () => null); ''', migratedContent: ''' import 'package:collection/collection.dart' show IterableExtension; int? f(List values) => values.firstWherefirstWhereOrNull((i) => i.isEven/* , orElse: () => null */); '''); var output = renderUnits()[0]; var regions = output.regions; // We're not testing the correctness of the data path, so drop it. regions = regions.replaceAll(RegExp(' data-path="[^"]*"'), ''); // Split the output into table rows. var rows = regions.split(RegExp('(?='); expect( rows[1], '1' '' '' "import 'package:collection/collection.dart' show IterableExtension;" ''); expect(rows[2], '(new)'); expect(rows[3], startsWith('(new)int')); expect(rows[4], startsWith('2')); } void test_kindPriorityOrder() { var nonDisplayedKinds = NullabilityFixKind.values.toSet(); for (var kind in UnitRenderer.kindPriorityOrder) { expect(nonDisplayedKinds.remove(kind), isTrue); } // The only kinds that should not be displayed are those associated with a // place where nothing interesting occurred. expect(nonDisplayedKinds, { NullabilityFixKind.typeNotMadeNullable, NullabilityFixKind.typeNotMadeNullableDueToHint }); } Future test_method_name_change() async { addPackageFile('collection', 'collection.dart', ''); await buildInfoForSingleTestFile( ''' import 'package:collection/collection.dart'; int f(List values) => values.firstWhere((i) => i.isEven, orElse: () => null); ''', removeViaComments: false, migratedContent: ''' import 'package:collection/collection.dart'; int? f(List values) => values.firstWherefirstWhereOrNull((i) => i.isEven, orElse: () => null); '''); var output = renderUnits()[0]; var regions = output.regions; // We aren't testing data-offset or data-line behaviors. regions = regions.replaceAll(RegExp(' data-offset="[^"]*"'), ''); regions = regions.replaceAll(RegExp(' data-line="[^"]*"'), ''); expect( regions, contains('firstWhere' 'firstWhereOrNull(')); } Future test_navContentContainsEscapedHtml() async { await buildInfoForSingleTestFile('List a = null;', migratedContent: 'List? a = null;'); var output = renderUnits()[0]; // Strip out URLs which will change; not being tested here. var navContent = output.navigationContent.replaceAll(RegExp('href=".*?"'), 'href="..."'); expect( navContent, contains(r'List' r'<String >? ' r'a = null;')); } void test_nullAwarenessUnnecessaryInStrongMode() async { await buildInfoForSingleTestFile(''' int f(String s) => s?.length; ''', migratedContent: ''' int f(String s) => s?.length; ''', warnOnWeakCode: true); var output = renderUnits()[0]; expect(_stripDataAttributes(output.regions), contains('s?.length')); expect( output.edits.keys, contains( '1 null-aware access will be unnecessary in strong checking mode')); } Future test_outputContains_addedType() async { await buildInfoForSingleTestFile(''' void f() { final a = >[]; a.add([null]); } ''', migratedContent: ''' void f() { final List> a = >[]; a.add([null]); } '''); var output = renderUnits()[0]; var regions = _stripDataAttributes(output.regions); expect( regions, contains('final ' 'List<List<int?>>' ' a = <List<int' ' ' '>' ' ' '>[];')); } Future test_outputContains_replacedVar() async { await buildInfoForSingleTestFile(''' void f() { var a = >[]; a.add([null]); } ''', migratedContent: ''' void f() { varList> a = >[]; a.add([null]); } '''); var output = renderUnits()[0]; var regions = _stripDataAttributes(output.regions); expect( regions, contains('var' 'List<List<int?>>' ' a = <List<int' ' ' '>' ' ' '>[];')); } Future test_outputContainsModifiedAndUnmodifiedRegions() async { await buildInfoForSingleTestFile('int a = null;', migratedContent: 'int? a = null;'); var output = renderUnits()[0]; var regions = _stripDataAttributes(output.regions); expect(regions, contains('int? a = null;')); } Future test_project_with_parts() async { // In this test, we migrate a library and its part file. Both files require // addition of a `?`, but at different offsets. We make sure the `?`s get // added at the correct locations in each file. var files = { convertPath('$projectPath/lib/a.dart'): ''' part 'b.dart'; int f() => null; ''', convertPath('$projectPath/lib/b.dart'): ''' part of 'a.dart'; int g() => null; ''', }; await buildInfoForTestFiles(files, includedRoot: projectPath); var output = renderUnits(); expect(output[0].sourceCode, contains('int?')); expect(output[1].sourceCode, contains('int?')); } Future test_reference_to_sdk_file_with_parts() async { await buildInfoForSingleTestFile(''' import 'dart:async'; Future f(Future x) { return x.whenComplete(() {}); } ''', migratedContent: ''' import 'dart:async'; Future f(Future x) { return x.whenComplete(() {}); } '''); renderUnits(); // No assertions; we're just making sure there's no crash. } Future test_regionsContainsEscapedHtml_ampersand() async { await buildInfoForSingleTestFile('bool a = true && false;', migratedContent: 'bool a = true && false;'); var output = renderUnits()[0]; expect( output.regions, contains('bool a = true && false;')); } Future test_regionsContainsEscapedHtml_betweenRegions() async { await buildInfoForSingleTestFile('List a = null;', migratedContent: 'List? a = null;'); var output = renderUnits()[0]; var regions = _stripDataAttributes(output.regions); expect( regions, contains( 'List<String >' '? a = null;')); } Future test_regionsContainsEscapedHtml_region() async { await buildInfoForSingleTestFile('f(List a) => a.join(",");', migratedContent: 'f(List a) => a.join(",");'); var output = renderUnits()[0]; var regions = _stripDataAttributes(output.regions); expect( regions, contains( 'List<String ' '> ')); } UnitInfo unit(String path, String content, {List regions}) { return UnitInfo(convertPath(path)) ..content = content ..regions.addAll(regions); } /// Strip out data attributes which are not being tested here. String _stripDataAttributes(String html) => html.replaceAll(RegExp(' data-[^=]+="[^"]+"'), ''); }