Skip to content

Commit 8e7fa2a

Browse files
srawlinscommit-bot@chromium.org
authored andcommitted
NNBD i13n: Proper messages for nullable values in collection literals
Change-Id: I5eccfeb7d88f3f4adb57f3734f170dce82c1fd2d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/121906 Commit-Queue: Samuel Rawlins <[email protected]> Reviewed-by: Paul Berry <[email protected]>
1 parent c52ac7b commit 8e7fa2a

File tree

2 files changed

+178
-29
lines changed

2 files changed

+178
-29
lines changed

pkg/analysis_server/lib/src/edit/nnbd_migration/info_builder.dart

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,17 @@ class InfoBuilder {
104104
return "This field is initialized by a field formal parameter and a "
105105
"nullable value is passed as an argument";
106106
}
107+
String nullableValue =
108+
node is NullLiteral ? "an explicit 'null'" : "a nullable value";
109+
String nullableValue2 =
110+
node is NullLiteral ? "An explicit 'null'" : "A nullable value";
107111
AstNode parent = node.parent;
108112
if (parent is ArgumentList) {
109-
if (node is NullLiteral) {
110-
return "An explicit 'null' is passed as an argument";
111-
}
112-
return "A nullable value is explicitly passed as an argument";
113+
return "$nullableValue2 is passed as an argument";
113114
}
114115

115-
/// If the [node] is the return expression for a function body, return the
116-
/// function body. Otherwise return `null`.
116+
/// If the [node] is inside the return expression for a function body,
117+
/// return the function body. Otherwise return `null`.
117118
FunctionBody findFunctionBody() {
118119
if (parent is ExpressionFunctionBody) {
119120
return parent;
@@ -126,37 +127,51 @@ class InfoBuilder {
126127
}
127128
}
128129

130+
/// If the [node] is inside a collection literal, return it. Otherwise
131+
/// return `null`.
132+
TypedLiteral findCollectionLiteral() {
133+
AstNode ancestor = parent;
134+
// Walk up collection elements, except for collection literals.
135+
while (ancestor is CollectionElement && ancestor is! TypedLiteral) {
136+
ancestor = ancestor.parent;
137+
}
138+
return (ancestor is TypedLiteral) ? ancestor : null;
139+
}
140+
141+
CompilationUnit unit = node.thisOrAncestorOfType<CompilationUnit>();
142+
int lineNumber = unit.lineInfo.getLocation(node.offset).lineNumber;
129143
FunctionBody functionBody = findFunctionBody();
130144
if (functionBody != null) {
131-
CompilationUnit unit = node.thisOrAncestorOfType<CompilationUnit>();
132-
int lineNumber = unit.lineInfo.getLocation(node.offset).lineNumber;
133145
AstNode function = functionBody.parent;
134146
if (function is MethodDeclaration) {
135147
if (function.isGetter) {
136-
return "This getter returns a nullable value on line $lineNumber";
148+
return "This getter returns $nullableValue on line $lineNumber";
137149
}
138-
return "This method returns a nullable value on line $lineNumber";
150+
return "This method returns $nullableValue on line $lineNumber";
151+
}
152+
return "This function returns $nullableValue on line $lineNumber";
153+
}
154+
155+
TypedLiteral collectionLiteral = findCollectionLiteral();
156+
if (collectionLiteral != null) {
157+
if (collectionLiteral is ListLiteral) {
158+
return "This list is initialized with $nullableValue on line "
159+
"$lineNumber";
160+
} else if (collectionLiteral is SetOrMapLiteral) {
161+
var mapOrSet = collectionLiteral.isMap ? 'map' : 'set';
162+
return "This $mapOrSet is initialized with $nullableValue on line "
163+
"$lineNumber";
139164
}
140-
return "This function returns a nullable value on line $lineNumber";
141165
} else if (parent is VariableDeclaration) {
142166
AstNode grandparent = parent.parent?.parent;
143167
if (grandparent is FieldDeclaration) {
144-
if (node is NullLiteral) {
145-
return "This field is initialized to null";
146-
}
147-
return "This field is initialized to a nullable value";
168+
return "This field is initialized to $nullableValue";
148169
}
149-
if (node is NullLiteral) {
150-
return "This variable is initialized to null";
151-
}
152-
return "This variable is initialized to a nullable value";
170+
return "This variable is initialized to $nullableValue";
153171
} else if (parent is AsExpression) {
154172
return "The value of the expression is nullable";
155173
}
156-
if (node is NullLiteral) {
157-
return "An explicit 'null' is assigned";
158-
}
159-
return "A nullable value is assigned";
174+
return "$nullableValue2 is assigned";
160175
}
161176

162177
/// Return details for a fix built from the given [edge], or `null` if the

pkg/analysis_server/test/src/edit/nnbd_migration/info_builder_test.dart

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,79 @@ class A {
221221
assertRegion(
222222
region: regions[0],
223223
offset: 15,
224-
details: ["This field is initialized to null"]);
224+
details: ["This field is initialized to an explicit 'null'"]);
225225
assertRegion(
226226
region: regions[1],
227227
offset: 33,
228228
details: ["This field is initialized to a nullable value"]);
229229
}
230230

231+
test_listAndSetLiteralTypeArgument() async {
232+
// TODO(srawlins): Simplify this test with `var x` once #38341 is fixed.
233+
addTestFile('''
234+
void f() {
235+
String s = null;
236+
List<String> x = <String>["hello", s];
237+
Set<String> y = <String>{"hello", s};
238+
''');
239+
await buildInfo();
240+
expect(infos, hasLength(1));
241+
UnitInfo unit = infos[0];
242+
expect(unit.content, '''
243+
void f() {
244+
String? s = null;
245+
List<String?> x = <String?>["hello", s];
246+
Set<String?> y = <String?>{"hello", s};
247+
''');
248+
List<RegionInfo> regions = unit.regions;
249+
expect(regions, hasLength(5));
250+
// regions[0] is the `String? s` fix.
251+
// regions[1] is the `List<String?> x` fix.
252+
assertRegion(
253+
region: regions[2],
254+
offset: 58,
255+
details: ["This list is initialized with a nullable value on line 3"]);
256+
assertDetail(detail: regions[2].details[0], offset: 67, length: 1);
257+
// regions[3] is the `Set<String?> y` fix.
258+
assertRegion(
259+
region: regions[4],
260+
offset: 100,
261+
details: ["This set is initialized with a nullable value on line 4"]);
262+
assertDetail(detail: regions[4].details[0], offset: 107, length: 1);
263+
}
264+
265+
test_listLiteralTypeArgument_collectionIf() async {
266+
// TODO(srawlins): Simplify this test with `var x` once #38341 is fixed.
267+
addTestFile('''
268+
void f() {
269+
String s = null;
270+
List<String> x = <String>[
271+
"hello",
272+
if (1 == 2) s
273+
];
274+
''');
275+
await buildInfo();
276+
expect(infos, hasLength(1));
277+
UnitInfo unit = infos[0];
278+
expect(unit.content, '''
279+
void f() {
280+
String? s = null;
281+
List<String?> x = <String?>[
282+
"hello",
283+
if (1 == 2) s
284+
];
285+
''');
286+
List<RegionInfo> regions = unit.regions;
287+
expect(regions, hasLength(3));
288+
// regions[0] is the `String? s` fix.
289+
// regions[1] is the `List<String?> x` fix.
290+
assertRegion(
291+
region: regions[2],
292+
offset: 58,
293+
details: ["This list is initialized with a nullable value on line 5"]);
294+
assertDetail(detail: regions[2].details[0], offset: 88, length: 1);
295+
}
296+
231297
test_localVariable() async {
232298
addTestFile('''
233299
void f() {
@@ -250,13 +316,47 @@ void f() {
250316
assertRegion(
251317
region: regions[0],
252318
offset: 16,
253-
details: ["This variable is initialized to null"]);
319+
details: ["This variable is initialized to an explicit 'null'"]);
254320
assertRegion(
255321
region: regions[1],
256322
offset: 35,
257323
details: ["This variable is initialized to a nullable value"]);
258324
}
259325

326+
test_mapLiteralTypeArgument() async {
327+
// TODO(srawlins): Simplify this test with `var x` once #38341 is fixed.
328+
addTestFile('''
329+
void f() {
330+
String s = null;
331+
Map<String, bool> x = <String, bool>{"hello": false, s: true};
332+
Map<bool, String> y = <bool, String>{false: "hello", true: s};
333+
''');
334+
await buildInfo();
335+
expect(infos, hasLength(1));
336+
UnitInfo unit = infos[0];
337+
expect(unit.content, '''
338+
void f() {
339+
String? s = null;
340+
Map<String?, bool> x = <String?, bool>{"hello": false, s: true};
341+
Map<bool, String?> y = <bool, String?>{false: "hello", true: s};
342+
''');
343+
List<RegionInfo> regions = unit.regions;
344+
expect(regions, hasLength(5));
345+
// regions[0] is the `String? s` fix.
346+
// regions[1] is the `Map<String?, bool> x` fix.
347+
assertRegion(
348+
region: regions[2],
349+
offset: 63,
350+
details: ["This map is initialized with a nullable value on line 3"]);
351+
assertDetail(detail: regions[2].details[0], offset: 85, length: 1);
352+
// regions[3] is the `Map<bool, String?> y` fix.
353+
assertRegion(
354+
region: regions[4],
355+
offset: 136,
356+
details: ["This map is initialized with a nullable value on line 4"]);
357+
assertDetail(detail: regions[4].details[0], offset: 156, length: 1);
358+
}
359+
260360
test_parameter_fromInvocation_explicit() async {
261361
addTestFile('''
262362
void f(String s) {}
@@ -562,7 +662,7 @@ int? f() => _f;
562662
assertRegion(
563663
region: regions[0],
564664
offset: 3,
565-
details: ["This variable is initialized to null"]);
665+
details: ["This variable is initialized to an explicit 'null'"]);
566666
assertRegion(
567667
region: regions[1],
568668
offset: 19,
@@ -595,7 +695,7 @@ class A {
595695
assertRegion(
596696
region: regions[0],
597697
offset: 15,
598-
details: ["This field is initialized to null"]);
698+
details: ["This field is initialized to an explicit 'null'"]);
599699
assertRegion(
600700
region: regions[1],
601701
offset: 33,
@@ -624,13 +724,47 @@ class A {
624724
assertRegion(
625725
region: regions[0],
626726
offset: 15,
627-
details: ["This field is initialized to null"]);
727+
details: ["This field is initialized to an explicit 'null'"]);
628728
assertRegion(
629729
region: regions[1],
630730
offset: 33,
631731
details: ["This getter returns a nullable value on line 3"]);
632732
}
633733

734+
test_setLiteralTypeArgument_nestedList() async {
735+
// TODO(srawlins): Simplify this test with `var x` once #38341 is fixed.
736+
addTestFile('''
737+
void f() {
738+
String s = null;
739+
Set<List<String>> x = <List<String>>{
740+
["hello"],
741+
if (1 == 2) [s]
742+
};
743+
''');
744+
await buildInfo();
745+
expect(infos, hasLength(1));
746+
UnitInfo unit = infos[0];
747+
expect(unit.content, '''
748+
void f() {
749+
String? s = null;
750+
Set<List<String?>> x = <List<String?>>{
751+
["hello"],
752+
if (1 == 2) [s]
753+
};
754+
''');
755+
List<RegionInfo> regions = unit.regions;
756+
expect(regions, hasLength(3));
757+
// regions[0] is the `String? s` fix.
758+
// regions[1] is the `Set<List<String?>> x` fix.
759+
assertRegion(
760+
region: regions[2],
761+
offset: 68,
762+
details: ["This set is initialized with a nullable value on line 5"]);
763+
// TODO(srawlins): Actually, this is marking the `[s]`, but I think only
764+
// `s` should be marked. Minor bug for now.
765+
assertDetail(detail: regions[2].details[0], offset: 101, length: 3);
766+
}
767+
634768
test_topLevelVariable() async {
635769
addTestFile('''
636770
int _f = null;
@@ -649,7 +783,7 @@ int? _f2 = _f;
649783
assertRegion(
650784
region: regions[0],
651785
offset: 3,
652-
details: ["This variable is initialized to null"]);
786+
details: ["This variable is initialized to an explicit 'null'"]);
653787
assertRegion(
654788
region: regions[1],
655789
offset: 19,

0 commit comments

Comments
 (0)