@@ -21,37 +21,74 @@ class AddNullCheck extends ResolvedCorrectionProducer {
21
21
final bool skipAssignabilityCheck;
22
22
23
23
@override
24
- // This is a mutable field so it can be changed in `_replaceWithNullCheck`.
25
- // TODO(srawlins): This seems to violate a few assert statements around the
26
- // package that read:
27
- //
28
- // > Producers used in bulk fixes must not modify the FixKind during
29
- // > computation.
30
- FixKind fixKind = DartFixKind .ADD_NULL_CHECK ;
24
+ final FixKind fixKind;
31
25
32
26
@override
33
27
List <String >? fixArguments;
34
28
35
- AddNullCheck ({ required super .context})
36
- : skipAssignabilityCheck = false ,
37
- applicability = CorrectionApplicability .singleLocation ;
29
+ /// The real target on which we might produce a correction, derived from
30
+ /// [node] .
31
+ final Expression ? _target ;
38
32
39
- AddNullCheck .withoutAssignabilityCheck ({required super .context})
40
- : skipAssignabilityCheck = true ,
41
- applicability = CorrectionApplicability .automaticallyButOncePerFile;
33
+ /// A [Token] that, if non-`null` , will be transformed into a null-aware
34
+ /// operator token.
35
+ final Token ? _nullAwareToken;
36
+
37
+ factory AddNullCheck ({required CorrectionProducerContext context}) {
38
+ var (: target, : nullAwareToken) =
39
+ context is StubCorrectionProducerContext
40
+ ? (target: null , nullAwareToken: null )
41
+ : _computeTargetAndNullAwareToken (context.node);
42
+
43
+ return AddNullCheck ._(
44
+ context: context,
45
+ skipAssignabilityCheck: false ,
46
+ applicability: CorrectionApplicability .singleLocation,
47
+ target: target,
48
+ nullAwareToken: nullAwareToken,
49
+ );
50
+ }
51
+
52
+ factory AddNullCheck .withoutAssignabilityCheck ({
53
+ required CorrectionProducerContext context,
54
+ }) {
55
+ var (: target, : nullAwareToken) =
56
+ context is StubCorrectionProducerContext
57
+ ? (target: null , nullAwareToken: null )
58
+ : _computeTargetAndNullAwareToken (context.node);
59
+
60
+ return AddNullCheck ._(
61
+ context: context,
62
+ skipAssignabilityCheck: true ,
63
+ applicability: CorrectionApplicability .automaticallyButOncePerFile,
64
+ target: target,
65
+ nullAwareToken: nullAwareToken,
66
+ );
67
+ }
68
+
69
+ AddNullCheck ._({
70
+ required super .context,
71
+ required this .skipAssignabilityCheck,
72
+ required this .applicability,
73
+ required Expression ? target,
74
+ required Token ? nullAwareToken,
75
+ }) : _target = target,
76
+ _nullAwareToken = nullAwareToken,
77
+ fixKind =
78
+ nullAwareToken == null
79
+ ? DartFixKind .ADD_NULL_CHECK
80
+ : DartFixKind .REPLACE_WITH_NULL_AWARE ;
42
81
43
82
@override
44
83
Future <void > compute (ChangeBuilder builder) async {
45
- if (await _isNullAware (builder, coveringNode) ) {
46
- return ;
84
+ if (_nullAwareToken != null ) {
85
+ return _replaceWithNullCheck (builder, _nullAwareToken) ;
47
86
}
48
87
49
- var target = await _computeTarget (builder);
50
- if (target == null ) {
88
+ if (_target == null ) {
51
89
return ;
52
90
}
53
-
54
- var fromType = target.staticType;
91
+ var fromType = _target.staticType;
55
92
if (fromType == null ) {
56
93
return ;
57
94
}
@@ -61,19 +98,26 @@ class AddNullCheck extends ResolvedCorrectionProducer {
61
98
return ;
62
99
}
63
100
64
- if (await _isNullAware (builder, target)) {
65
- return ;
101
+ if (coveringNode case BinaryExpression binaryExpression) {
102
+ if (binaryExpression.operator .type == TokenType .QUESTION_QUESTION &&
103
+ _target == binaryExpression.rightOperand &&
104
+ ! _couldBeAssignableInNullAwareExpression (binaryExpression)) {
105
+ // Do not offer to add a null-check in the right side of `??` if that
106
+ // would not fix the issue.
107
+ return ;
108
+ }
66
109
}
110
+
67
111
DartType ? toType;
68
- var parent = target .parent;
69
- if (parent is AssignmentExpression && target == parent.rightHandSide) {
112
+ var parent = _target .parent;
113
+ if (parent is AssignmentExpression && _target == parent.rightHandSide) {
70
114
toType = parent.writeType;
71
115
} else if (parent is AsExpression ) {
72
116
toType = parent.staticType;
73
- } else if (parent is VariableDeclaration && target == parent.initializer) {
117
+ } else if (parent is VariableDeclaration && _target == parent.initializer) {
74
118
toType = parent.declaredFragment! .element.type;
75
119
} else if (parent is ArgumentList ) {
76
- toType = target .correspondingParameter? .type;
120
+ toType = _target .correspondingParameter? .type;
77
121
} else if (parent is IndexExpression ) {
78
122
toType = parent.realTarget.typeOrThrow;
79
123
} else if (parent is ForEachPartsWithDeclaration ) {
@@ -117,13 +161,14 @@ class AddNullCheck extends ResolvedCorrectionProducer {
117
161
)) {
118
162
return ;
119
163
}
120
- } else if ((parent is PrefixedIdentifier && target == parent.prefix) ||
164
+ } else if ((parent is PrefixedIdentifier && _target == parent.prefix) ||
121
165
parent is PostfixExpression ||
122
166
parent is PrefixExpression ||
123
- (parent is PropertyAccess && target == parent.target) ||
124
- (parent is CascadeExpression && target == parent.target) ||
125
- (parent is MethodInvocation && target == parent.target) ||
126
- (parent is FunctionExpressionInvocation && target == parent.function)) {
167
+ (parent is PropertyAccess && _target == parent.target) ||
168
+ (parent is CascadeExpression && _target == parent.target) ||
169
+ (parent is MethodInvocation && _target == parent.target) ||
170
+ (parent is FunctionExpressionInvocation &&
171
+ _target == parent.function)) {
127
172
// No need to set the `toType` because there isn't any need for a type
128
173
// check.
129
174
} else {
@@ -142,12 +187,12 @@ class AddNullCheck extends ResolvedCorrectionProducer {
142
187
return ;
143
188
}
144
189
145
- var needsParentheses = target .precedence < Precedence .postfix;
190
+ var needsParentheses = _target .precedence < Precedence .postfix;
146
191
await builder.addDartFileEdit (file, (builder) {
147
192
if (needsParentheses) {
148
- builder.addSimpleInsertion (target .offset, '(' );
193
+ builder.addSimpleInsertion (_target .offset, '(' );
149
194
}
150
- builder.addInsertion (target .end, (builder) {
195
+ builder.addInsertion (_target .end, (builder) {
151
196
if (needsParentheses) {
152
197
builder.write (')' );
153
198
}
@@ -156,103 +201,116 @@ class AddNullCheck extends ResolvedCorrectionProducer {
156
201
});
157
202
}
158
203
159
- /// Computes the target for which we need to add a null check.
160
- Future <Expression ?> _computeTarget (ChangeBuilder builder) async {
161
- var coveringNode = this .coveringNode;
162
- var coveringNodeParent = coveringNode? .parent;
204
+ /// Given that [_target] is the right operand in [binaryExpression] , returns
205
+ /// whether [_target] , if promoted to be non-`null` , could be assignable to
206
+ /// [binaryExpression] 's associated parameter.
207
+ bool _couldBeAssignableInNullAwareExpression (
208
+ BinaryExpression binaryExpression,
209
+ ) {
210
+ var expectedType = binaryExpression.correspondingParameter? .type;
211
+ if (expectedType == null ) {
212
+ return true ;
213
+ }
214
+ var leftType = binaryExpression.leftOperand.staticType;
215
+ return leftType != null &&
216
+ typeSystem.isAssignableTo (
217
+ typeSystem.promoteToNonNull (leftType),
218
+ expectedType,
219
+ strictCasts: analysisOptions.strictCasts,
220
+ );
221
+ }
163
222
223
+ /// Replaces the null-aware [token] with the null check operator.
224
+ Future <void > _replaceWithNullCheck (ChangeBuilder builder, Token token) {
225
+ var lexeme = token.lexeme;
226
+ var replacement = '!${lexeme .substring (1 )}' ;
227
+ fixArguments = [lexeme, replacement];
228
+ return builder.addDartFileEdit (file, (builder) {
229
+ builder.addSimpleReplacement (range.token (token), replacement);
230
+ });
231
+ }
232
+
233
+ /// Computes both the target in which we might make a correction, and a
234
+ /// null-aware [Token] that we might instead convert into a null-aware
235
+ /// operator token.
236
+ static ({Expression ? target, Token ? nullAwareToken})
237
+ _computeTargetAndNullAwareToken (AstNode ? coveringNode) {
238
+ var nullAwareToken = _hasNullAware (coveringNode);
239
+ if (coveringNode is Expression && nullAwareToken != null ) {
240
+ return (target: coveringNode, nullAwareToken: nullAwareToken);
241
+ }
242
+
243
+ var parent = coveringNode? .parent;
244
+ Expression ? target;
164
245
if (coveringNode is SimpleIdentifier ) {
165
- if (coveringNodeParent is MethodInvocation ) {
166
- return coveringNodeParent.realTarget;
167
- } else if (coveringNodeParent is PrefixedIdentifier ) {
168
- return coveringNodeParent.prefix;
169
- } else if (coveringNodeParent is PropertyAccess ) {
170
- return coveringNodeParent.realTarget;
171
- } else if (coveringNodeParent is CascadeExpression &&
172
- await _isNullAware (
173
- builder,
174
- coveringNodeParent.cascadeSections.first,
175
- )) {
176
- return null ;
246
+ if (parent is MethodInvocation ) {
247
+ target = parent.realTarget;
248
+ } else if (parent is PrefixedIdentifier ) {
249
+ target = parent.prefix;
250
+ } else if (parent is PropertyAccess ) {
251
+ target = parent.realTarget;
177
252
} else {
178
- return coveringNode;
253
+ target = coveringNode;
179
254
}
180
255
} else if (coveringNode is IndexExpression ) {
181
- var target = coveringNode.realTarget;
256
+ target = coveringNode.realTarget;
182
257
if (target.staticType? .nullabilitySuffix != NullabilitySuffix .question) {
183
258
target = coveringNode;
184
259
}
185
- return target;
186
260
} else if (coveringNode is Expression &&
187
- coveringNodeParent is FunctionExpressionInvocation ) {
188
- return coveringNode;
189
- } else if (coveringNodeParent is AssignmentExpression ) {
190
- return coveringNodeParent .rightHandSide;
261
+ parent is FunctionExpressionInvocation ) {
262
+ target = coveringNode;
263
+ } else if (parent is AssignmentExpression ) {
264
+ target = parent .rightHandSide;
191
265
} else if (coveringNode is PostfixExpression ) {
192
- return coveringNode.operand;
266
+ target = coveringNode.operand;
193
267
} else if (coveringNode is PrefixExpression ) {
194
- return coveringNode.operand;
268
+ target = coveringNode.operand;
195
269
} else if (coveringNode is BinaryExpression ) {
196
270
if (coveringNode.operator .type != TokenType .QUESTION_QUESTION ) {
197
- return coveringNode.leftOperand;
271
+ target = coveringNode.leftOperand;
198
272
} else {
199
273
var expectedType = coveringNode.correspondingParameter? .type;
200
- if (expectedType == null ) return null ;
201
-
202
- var leftType = coveringNode.leftOperand.staticType;
203
- var leftAssignable =
204
- leftType != null &&
205
- typeSystem.isAssignableTo (
206
- typeSystem.promoteToNonNull (leftType),
207
- expectedType,
208
- strictCasts: analysisOptions.strictCasts,
209
- );
210
- if (leftAssignable) {
211
- return coveringNode.rightOperand;
274
+ if (expectedType != null ) {
275
+ target = coveringNode.rightOperand;
212
276
}
213
277
}
214
278
} else if (coveringNode is AsExpression ) {
215
- return coveringNode.expression;
279
+ target = coveringNode.expression;
216
280
}
217
- return null ;
281
+
282
+ if (target == null ) {
283
+ return (target: null , nullAwareToken: null );
284
+ }
285
+
286
+ nullAwareToken = _hasNullAware (target);
287
+ return (target: target, nullAwareToken: nullAwareToken);
218
288
}
219
289
220
- /// Returns whether the specified [node] or, in some cases, a certain child
221
- /// node is null-aware, in which case the null-aware operator is replaced with
222
- /// a null check operator.
223
- Future <bool > _isNullAware (ChangeBuilder builder, AstNode ? node) async {
290
+ /// Adds an edit to a null-aware operation, replacing the `?` with a `!`
291
+ /// character, if applicable.
292
+ ///
293
+ /// Returns whether this edit was made.
294
+ static Token ? _hasNullAware (AstNode ? node) {
224
295
if (node is PropertyAccess ) {
225
296
if (node.isNullAware) {
226
- await _replaceWithNullCheck (builder, node.operator );
227
- return true ;
297
+ return node.operator ;
228
298
}
229
- return await _isNullAware (builder, node.target);
230
- } else if (node is MethodInvocation ) {
231
- var operator = node.operator ;
299
+ return _hasNullAware (node.target);
300
+ } else if (node case MethodInvocation (: var operator )) {
232
301
if (operator != null && node.isNullAware) {
233
- await _replaceWithNullCheck (builder, operator );
234
- return true ;
302
+ return operator ;
235
303
}
236
- return await _isNullAware (builder, node.target);
237
- } else if (node is IndexExpression ) {
238
- var question = node.question;
304
+ return _hasNullAware (node.target);
305
+ } else if (node case IndexExpression (: var question)) {
239
306
if (question != null ) {
240
- await _replaceWithNullCheck (builder, question);
241
- return true ;
307
+ return question;
242
308
}
243
- return await _isNullAware (builder, node.target);
309
+ return _hasNullAware (node.target);
310
+ } else if (node case SimpleIdentifier (: CascadeExpression parent)) {
311
+ return _hasNullAware (parent.cascadeSections.first);
244
312
}
245
- return false ;
246
- }
247
313
248
- /// Replaces the null-aware [token] with the null check operator.
249
- Future <void > _replaceWithNullCheck (ChangeBuilder builder, Token token) async {
250
- fixKind = DartFixKind .REPLACE_WITH_NULL_AWARE ;
251
- var lexeme = token.lexeme;
252
- var replacement = '!${lexeme .substring (1 )}' ;
253
- fixArguments = [lexeme, replacement];
254
- await builder.addDartFileEdit (file, (builder) {
255
- builder.addSimpleReplacement (range.token (token), replacement);
256
- });
314
+ return null ;
257
315
}
258
316
}
0 commit comments