Skip to content

Commit 6838dab

Browse files
srawlinsCommit Queue
authored and
Commit Queue
committed
DAS: make add_null_check fixKind final
A few statements around the package read: > Producers used in bulk fixes must not modify the FixKind during > computation. In fact, this `add_null_check` correction producer seems to be the _only_ remaining producer that violates this statement. In order to fix it, such that `fixKind` is set during constructor initialization, we must do some poking around the AST. So we convert `_computeTarget` to be static (and change it to also compute and return any possible null-aware token, renaming it to `_computeTargetAndNullAwareToken`). We also convert `_isNullAware` to be static. Then the two public constructors are converted into factory constructors that determine the fix kind, and store the `_target` and `_nullAwareToken` in fields, so as to avoid re-computing them during `compute()`. Change-Id: Ic66319ef764dd9bd69f0e1059347acf174debb25 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/419420 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]>
1 parent ff09699 commit 6838dab

File tree

2 files changed

+168
-101
lines changed

2 files changed

+168
-101
lines changed

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

Lines changed: 159 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,74 @@ class AddNullCheck extends ResolvedCorrectionProducer {
2121
final bool skipAssignabilityCheck;
2222

2323
@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;
3125

3226
@override
3327
List<String>? fixArguments;
3428

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;
3832

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;
4281

4382
@override
4483
Future<void> compute(ChangeBuilder builder) async {
45-
if (await _isNullAware(builder, coveringNode)) {
46-
return;
84+
if (_nullAwareToken != null) {
85+
return _replaceWithNullCheck(builder, _nullAwareToken);
4786
}
4887

49-
var target = await _computeTarget(builder);
50-
if (target == null) {
88+
if (_target == null) {
5189
return;
5290
}
53-
54-
var fromType = target.staticType;
91+
var fromType = _target.staticType;
5592
if (fromType == null) {
5693
return;
5794
}
@@ -61,19 +98,26 @@ class AddNullCheck extends ResolvedCorrectionProducer {
6198
return;
6299
}
63100

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+
}
66109
}
110+
67111
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) {
70114
toType = parent.writeType;
71115
} else if (parent is AsExpression) {
72116
toType = parent.staticType;
73-
} else if (parent is VariableDeclaration && target == parent.initializer) {
117+
} else if (parent is VariableDeclaration && _target == parent.initializer) {
74118
toType = parent.declaredFragment!.element.type;
75119
} else if (parent is ArgumentList) {
76-
toType = target.correspondingParameter?.type;
120+
toType = _target.correspondingParameter?.type;
77121
} else if (parent is IndexExpression) {
78122
toType = parent.realTarget.typeOrThrow;
79123
} else if (parent is ForEachPartsWithDeclaration) {
@@ -117,13 +161,14 @@ class AddNullCheck extends ResolvedCorrectionProducer {
117161
)) {
118162
return;
119163
}
120-
} else if ((parent is PrefixedIdentifier && target == parent.prefix) ||
164+
} else if ((parent is PrefixedIdentifier && _target == parent.prefix) ||
121165
parent is PostfixExpression ||
122166
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)) {
127172
// No need to set the `toType` because there isn't any need for a type
128173
// check.
129174
} else {
@@ -142,12 +187,12 @@ class AddNullCheck extends ResolvedCorrectionProducer {
142187
return;
143188
}
144189

145-
var needsParentheses = target.precedence < Precedence.postfix;
190+
var needsParentheses = _target.precedence < Precedence.postfix;
146191
await builder.addDartFileEdit(file, (builder) {
147192
if (needsParentheses) {
148-
builder.addSimpleInsertion(target.offset, '(');
193+
builder.addSimpleInsertion(_target.offset, '(');
149194
}
150-
builder.addInsertion(target.end, (builder) {
195+
builder.addInsertion(_target.end, (builder) {
151196
if (needsParentheses) {
152197
builder.write(')');
153198
}
@@ -156,103 +201,116 @@ class AddNullCheck extends ResolvedCorrectionProducer {
156201
});
157202
}
158203

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+
}
163222

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;
164245
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;
177252
} else {
178-
return coveringNode;
253+
target = coveringNode;
179254
}
180255
} else if (coveringNode is IndexExpression) {
181-
var target = coveringNode.realTarget;
256+
target = coveringNode.realTarget;
182257
if (target.staticType?.nullabilitySuffix != NullabilitySuffix.question) {
183258
target = coveringNode;
184259
}
185-
return target;
186260
} 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;
191265
} else if (coveringNode is PostfixExpression) {
192-
return coveringNode.operand;
266+
target = coveringNode.operand;
193267
} else if (coveringNode is PrefixExpression) {
194-
return coveringNode.operand;
268+
target = coveringNode.operand;
195269
} else if (coveringNode is BinaryExpression) {
196270
if (coveringNode.operator.type != TokenType.QUESTION_QUESTION) {
197-
return coveringNode.leftOperand;
271+
target = coveringNode.leftOperand;
198272
} else {
199273
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;
212276
}
213277
}
214278
} else if (coveringNode is AsExpression) {
215-
return coveringNode.expression;
279+
target = coveringNode.expression;
216280
}
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);
218288
}
219289

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) {
224295
if (node is PropertyAccess) {
225296
if (node.isNullAware) {
226-
await _replaceWithNullCheck(builder, node.operator);
227-
return true;
297+
return node.operator;
228298
}
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)) {
232301
if (operator != null && node.isNullAware) {
233-
await _replaceWithNullCheck(builder, operator);
234-
return true;
302+
return operator;
235303
}
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)) {
239306
if (question != null) {
240-
await _replaceWithNullCheck(builder, question);
241-
return true;
307+
return question;
242308
}
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);
244312
}
245-
return false;
246-
}
247313

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;
257315
}
258316
}

pkg/analysis_server_plugin/lib/edit/dart/correction_producer.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,15 @@ abstract class ResolvedCorrectionProducer
660660
}
661661
}
662662

663+
/// A stub implementation of [CorrectionProducerContext], which can be used to
664+
/// instantiate a correction producer for the purpose of examining fixed
665+
/// properties on it.
666+
///
667+
/// This has several acceptable use cases:
668+
/// * checking the applicability of a correction producer,
669+
/// * testing purposes,
670+
/// * short-circuiting logic in a correction producer factory constructor that
671+
/// relies on the context's `node` property.
663672
final class StubCorrectionProducerContext implements CorrectionProducerContext {
664673
static final instance = StubCorrectionProducerContext._();
665674

0 commit comments

Comments
 (0)