Skip to content

Commit c706117

Browse files
chloestefantsovaCommit Queue
authored andcommitted
[analyzer] Add rewrite of null checks into null-aware elements
The rewrite is done upon the lint `use_null_aware_elements`. Part of #56989 Change-Id: I58e47385089ec04513b0aacd5f486fe719943c51 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412160 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Chloe Stefantsova <[email protected]>
1 parent c083f5a commit c706117

File tree

6 files changed

+458
-1
lines changed

6 files changed

+458
-1
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/services/correction/fix.dart';
6+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
7+
import 'package:analyzer/dart/element/element2.dart';
8+
import 'package:analyzer/src/dart/ast/ast.dart';
9+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
10+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
11+
import 'package:analyzer_plugin/utilities/range_factory.dart';
12+
13+
class ConvertNullCheckToNullAwareElementOrEntry
14+
extends ResolvedCorrectionProducer {
15+
ConvertNullCheckToNullAwareElementOrEntry({required super.context});
16+
17+
@override
18+
CorrectionApplicability get applicability =>
19+
CorrectionApplicability.automatically;
20+
21+
@override
22+
FixKind get fixKind =>
23+
DartFixKind.CONVERT_NULL_CHECK_TO_NULL_AWARE_ELEMENT_OR_ENTRY;
24+
25+
@override
26+
FixKind get multiFixKind =>
27+
DartFixKind.CONVERT_NULL_CHECK_TO_NULL_AWARE_ELEMENT_OR_ENTRY_MULTI;
28+
29+
@override
30+
Future<void> compute(ChangeBuilder builder) async {
31+
var node = coveringNode;
32+
if (node case IfElement(
33+
expression: var condition,
34+
:var thenElement,
35+
elseKeyword: null,
36+
)) {
37+
if (node.caseClause == null) {
38+
// An element or entry of the form `if (x != null) ...`.
39+
if (thenElement is! MapLiteralEntry) {
40+
// In case of a list or set element, we simply replace the entire
41+
// element with the then-element prefixed by '?'.
42+
//
43+
// `[if (x != null) x]` ==> `[?x]`
44+
// `{if (x != null) x]` ==> `{?x}`
45+
await builder.addDartFileEdit(file, (builder) {
46+
builder.addSimpleReplacement(
47+
range.startOffsetEndOffset(
48+
node.ifKeyword.offset,
49+
thenElement.offset,
50+
),
51+
'?',
52+
);
53+
});
54+
} else {
55+
// In case of a map entry we need to check if it's the key that's
56+
// promoted to non-nullable or the value.
57+
var binaryCondition = condition as BinaryExpression;
58+
var keyCanonicalElement = thenElement.key.canonicalElement;
59+
if (keyCanonicalElement != null &&
60+
(binaryCondition.leftOperand.canonicalElement ==
61+
keyCanonicalElement ||
62+
binaryCondition.rightOperand.canonicalElement ==
63+
keyCanonicalElement)) {
64+
// In case the key is promoted, we simply replace everything before
65+
// the key with '?'.
66+
//
67+
// `{if (x != null) x: "value"}` ==> `{?x: "value"}`
68+
await builder.addDartFileEdit(file, (builder) {
69+
builder.addSimpleReplacement(
70+
range.startOffsetEndOffset(node.offset, thenElement.key.offset),
71+
'?',
72+
);
73+
});
74+
} else {
75+
// In case the value is promoted, we remove everything before the
76+
// key and insert '?' before the value.
77+
//
78+
// `{if (x != null) "key": x}` ==> `{"key": ?x}`
79+
await builder.addDartFileEdit(file, (builder) {
80+
builder.addDeletion(
81+
range.startOffsetEndOffset(node.offset, thenElement.key.offset),
82+
);
83+
builder.addSimpleInsertion(thenElement.value.offset, '?');
84+
});
85+
}
86+
}
87+
} else {
88+
// An element or entry of the form `if (x case var y?) ...`.
89+
if (thenElement is! MapLiteralEntry) {
90+
// In case of a list or set element, we replace the entire element
91+
// with the expression to the left of 'case', prefixed by '?'.
92+
//
93+
// `[if (x case var y?) y]` ==> `[?x]`
94+
// `{if (x case var y?) y]` ==> `{?x}`
95+
await builder.addDartFileEdit(file, (builder) {
96+
builder.addSimpleReplacement(
97+
range.startOffsetEndOffset(node.offset, node.end),
98+
'?${condition.toSource()}',
99+
);
100+
});
101+
} else {
102+
// In case of a map entry we need to check if it's the key that's
103+
// promoted to non-nullable or the value.
104+
var caseVariable =
105+
((node.caseClause?.guardedPattern.pattern as NullCheckPattern)
106+
.pattern
107+
as DeclaredVariablePattern)
108+
.declaredElement2;
109+
if (caseVariable == thenElement.key.canonicalElement) {
110+
// In case the key is promoted, replace everything before ':' with
111+
// the expression before 'case', prefixed by '?'.
112+
//
113+
// `{if (x case var y?) y: "value"}` ==> `{?x: "value"}`
114+
await builder.addDartFileEdit(file, (builder) {
115+
builder.addSimpleReplacement(
116+
range.startOffsetEndOffset(node.offset, thenElement.key.end),
117+
'?${condition.toSource()}',
118+
);
119+
});
120+
} else {
121+
// In case the value is promoted, delete everything before the key
122+
// and replace the value with the expression to the left of 'case',
123+
// prefixed by '?'.
124+
//
125+
// `{if (x case var y?) "key": y}` ==> `{"key": ?x}`
126+
await builder.addDartFileEdit(file, (builder) {
127+
builder.addDeletion(
128+
range.startOffsetEndOffset(node.offset, thenElement.key.offset),
129+
);
130+
builder.addSimpleReplacement(
131+
range.startOffsetEndOffset(
132+
thenElement.value.offset,
133+
thenElement.value.end,
134+
),
135+
'?${condition.toSource()}',
136+
);
137+
});
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
145+
extension AstNodeNullableExtension on AstNode? {
146+
Element2? get canonicalElement {
147+
var self = this;
148+
if (self is Expression) {
149+
var node = self.unParenthesized;
150+
if (node is Identifier) {
151+
return node.element;
152+
} else if (node is PropertyAccess) {
153+
return node.propertyName.element;
154+
}
155+
}
156+
return null;
157+
}
158+
}

pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2513,7 +2513,7 @@ LintCode.use_late_for_private_fields_and_variables:
25132513
LintCode.use_named_constants:
25142514
status: hasFix
25152515
LintCode.use_null_aware_elements:
2516-
status: needsFix
2516+
status: hasFix
25172517
LintCode.use_raw_strings:
25182518
status: hasFix
25192519
LintCode.use_rethrow_when_possible:

pkg/analysis_server/lib/src/services/correction/fix.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,17 @@ abstract final class DartFixKind {
407407
DartFixKindPriority.inFile,
408408
'Convert to expression bodies everywhere in file',
409409
);
410+
static const CONVERT_NULL_CHECK_TO_NULL_AWARE_ELEMENT_OR_ENTRY = FixKind(
411+
'dart.fix.convert.nullCheckToNullAwareElement',
412+
DartFixKindPriority.standard,
413+
'Convert null check to null-aware element',
414+
);
415+
static const CONVERT_NULL_CHECK_TO_NULL_AWARE_ELEMENT_OR_ENTRY_MULTI =
416+
FixKind(
417+
'dart.fix.convert.nullCheckToNullAwareElement.multi',
418+
DartFixKindPriority.inFile,
419+
'Convert null check to null-aware element in file',
420+
);
410421
static const CONVERT_QUOTES = FixKind(
411422
'dart.fix.convert.quotes',
412423
DartFixKindPriority.standard,

pkg/analysis_server/lib/src/services/correction/fix_internal.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import 'package:analysis_server/src/services/correction/dart/convert_for_each_to
5454
import 'package:analysis_server/src/services/correction/dart/convert_into_block_body.dart';
5555
import 'package:analysis_server/src/services/correction/dart/convert_into_is_not.dart';
5656
import 'package:analysis_server/src/services/correction/dart/convert_map_from_iterable_to_for_literal.dart';
57+
import 'package:analysis_server/src/services/correction/dart/convert_null_check_to_null_aware_element_or_entry.dart';
5758
import 'package:analysis_server/src/services/correction/dart/convert_quotes.dart';
5859
import 'package:analysis_server/src/services/correction/dart/convert_related_to_cascade.dart';
5960
import 'package:analysis_server/src/services/correction/dart/convert_to_boolean_expression.dart';
@@ -535,6 +536,9 @@ final _builtInLintProducers = <LintCode, List<ProducerGenerator>>{
535536
],
536537
LinterLintCode.use_key_in_widget_constructors: [AddKeyToConstructors.new],
537538
LinterLintCode.use_named_constants: [ReplaceWithNamedConstant.new],
539+
LinterLintCode.use_null_aware_elements: [
540+
ConvertNullCheckToNullAwareElementOrEntry.new,
541+
],
538542
LinterLintCode.use_raw_strings: [ConvertToRawString.new],
539543
LinterLintCode.use_rethrow_when_possible: [UseRethrow.new],
540544
LinterLintCode.use_string_in_part_of_directives: [

0 commit comments

Comments
 (0)