mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
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:
parent
843cd8e43f
commit
c3fb570105
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue