Add "Add 'late' hint" feature

Tested with:

* top-level variables
* local variables
* instance and static fields declared in classes and mixins (enhanced enums are not supported pre-null safety)

Fixes https://github.com/dart-lang/sdk/issues/41389

Change-Id: I8a89668fffe64fff508d0aeb36aaebef5c84f223
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/263460
Reviewed-by: Paul Berry <paulberry@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This commit is contained in:
Sam Rawlins 2022-10-17 15:24:57 +00:00 committed by Commit Queue
parent 843cd8e43f
commit c3fb570105
2 changed files with 235 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show SourceFileEdit;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
@ -225,6 +226,13 @@ class InfoBuilder {
case NullabilityFixKind.typeNotMadeNullable:
edits.add(EditDetail('Add /*!*/ hint', offset, 0, '/*!*/'));
edits.add(EditDetail('Add /*?*/ hint', offset, 0, '/*?*/'));
var declarationList = _findVariableDeclaration(result.unit, offset);
if (declarationList != null) {
var lateOffset = _offsetForPossibleLateModifier(declarationList);
if (lateOffset != null) {
edits.add(EditDetail('Add late hint', lateOffset, 0, '/*late*/'));
}
}
break;
case NullabilityFixKind.makeTypeNullableDueToHint:
edits.add(changeHint('Change to /*!*/ hint', '/*!*/'));
@ -504,6 +512,18 @@ class InfoBuilder {
return null;
}
/// Returns the variable declaration which covers [offset], or `null` if none
/// does.
VariableDeclarationList? _findVariableDeclaration(
CompilationUnit unit, int offset) {
var nodeLocator = NodeLocator2(offset);
var node = nodeLocator.searchWithin(unit);
if (node == null) {
return null;
}
return node.thisOrAncestorOfType<VariableDeclarationList>();
}
TraceEntryInfo _makeTraceEntry(
String description, CodeReference? codeReference,
{List<HintAction> hintActions = const []}) {
@ -527,6 +547,34 @@ class InfoBuilder {
.toList());
}
/// Returns the offset for a possible `late` modifier which could be inserted
/// into [declarationList], or `null` if none is possible.
int? _offsetForPossibleLateModifier(VariableDeclarationList declarationList) {
if (declarationList.isLate || declarationList.isConst) {
// Don't offer an ofset.
return null;
}
var keyword = declarationList.keyword;
if (keyword != null) {
// Offset for possible `late` is before `var`, `const`, or `final`.
return keyword.offset;
}
var typeAnnotation = declarationList.type;
if (typeAnnotation != null) {
// Without a `keyword`, offset for possible `late` is before the type
// annotation.
return typeAnnotation.offset;
}
assert(
false,
'In this VariableDeclarationList, there is no `var`, '
'`const`, or `final` keyword, nor any type annotation. This variable '
'declaration list is not valid: $declarationList');
return null;
}
TraceEntryInfo _stepToTraceEntry(PropagationStepInfo step) {
var description = step.edge?.description;
description ??= step.toString(); // TODO(paulberry): improve this message.

View file

@ -384,6 +384,192 @@ void main() {
replacement: ''));
}
Future<void> test_addLateHint_classMultipleTypedInstanceVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
class C {
int f, g;
C() {
f = 1;
}
}
''', migratedContent: '''
class C {
int? f, g;
C() {
f = 1;
}
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 15,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 12, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_classTypedFinalInstanceVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
class C {
final int f;
C({this.f});
}
''', migratedContent: '''
class C {
final int? f;
C({this.f});
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 21,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 12, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_classTypedInstanceVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
class C {
int f;
C() {
f = 1;
}
}
''', migratedContent: '''
class C {
int? f;
C() {
f = 1;
}
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 15,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 12, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_classTypedStaticVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
class C {
static int x;
void f() => x = null;
}
''', migratedContent: '''
class C {
static int? x;
void f() => x = null;
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 22,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 19, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_mixinVarInstanceVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
mixin M {
int f;
void m() => f = null;
}
''', migratedContent: '''
mixin M {
int? f;
void m() => f = null;
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 15,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 12, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_typedLocalVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
void f() {
int x;
void g() => x = null;
}
''', migratedContent: '''
void f() {
int? x;
void g() => x = null;
}
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 16,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 13, length: 0, replacement: '/*late*/');
}
Future<void> test_addLateHint_typedTopLevelVariable() async {
addMetaPackage();
var unit = await buildInfoForSingleTestFile('''
int x;
void f() => x = null;
''', migratedContent: '''
int? x;
void f() => x = null;
''');
var regions = unit.fixRegions;
expect(regions, hasLength(1));
var region = regions[0];
var edits = region.edits;
assertRegion(
region: region,
offset: 3,
length: 1,
explanation: "Changed type 'int' to be nullable",
kind: NullabilityFixKind.makeTypeNullable);
assertEdit(edit: edits[2], offset: 0, length: 0, replacement: '/*late*/');
}
Future<void> test_compound_assignment_nullable_result() async {
var unit = await buildInfoForSingleTestFile('''
abstract class C {
@ -1623,7 +1809,7 @@ String? g() => 1 == 2 ? "Hello" : null;
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'});
{'Add /*?*/ hint', 'Add /*!*/ hint', 'Add late hint'});
var trace = region.traces.first;
expect(trace.description, 'Non-nullability reason');
var entries = trace.entries;