Skip to content

Commit 7411ddb

Browse files
stereotype441Commit Queue
authored andcommitted
[analysis server] Improve dead code removal to prepare for sound-flow-analysis.
The following improvements are made to the "remove dead code" quick fix: - Dead code removal can now replace conditional expressions like `condition ? live : dead` or `condition ? dead : live` with just the live subexpression, provided that the condition appears free of side effects. - Dead code removal can now replace `if` statements like `if (condition) live; else dead;` or `if (condition) dead; else live;` with just the live statement, provided that the condition appears free of side effects. - Dead code removal can now fully remove `if` statements like `if (condition) dead;`, provided that the condition appears free of side effects. Extra care is taken around opening and closing braces to try to preserve reasonable formatting as much as possible. These features were chosen by examining the set of new dead code warnings that pop up inside google3 if the sound-flow-analysis language feature is enabled. They are sufficient to allow 98% of those new warnings to be addressed by the quick fix. This should help customers adjust their code quickly when moving to a language version that enables this language feature. The notion that a condition "appears free of side effects" is defined to include casts, non-null assertions, and getter invocations. It is of course possible that a getter invocation, a cast, or a non-null assertion could have a side effect that is important, but it would be difficult to build the necessary static analysis to detect these cases, and in practice, they don't happen very often. Considering that the "remove dead code" quick fix is not usable in an automated fashion (it must be explicitly requested by the user in their editor for each piece of affected dead code), I believe this is a good tradeoff. Bug: #60438 Change-Id: Ied43500c4b7a729bbd686708ab10e80e72f22088 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425180 Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]>
1 parent cf717ac commit 7411ddb

File tree

2 files changed

+683
-6
lines changed

2 files changed

+683
-6
lines changed

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

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,67 @@ class RemoveDeadCode extends ResolvedCorrectionProducer {
160160
await _addEdit((builder) {
161161
builder.addDeletion(deletionRange);
162162
});
163+
case Expression(
164+
parent: ConditionalExpression(
165+
parent: var grandParent,
166+
:var condition,
167+
:var thenExpression,
168+
:var elseExpression,
169+
) &&
170+
var parent,
171+
)
172+
when (node == thenExpression || node == elseExpression) &&
173+
_looksSideEffectFree(condition):
174+
// Then expression is `condition ? live : dead` or
175+
// `condition ? dead : live`, with a condition that is free of side
176+
// effects (or so we presume--see `_looksSideEffectFree` for details).
177+
// So the conditional expression can be replaced with the `live`
178+
// subexpression.
179+
var nodeToKeep =
180+
node == thenExpression ? elseExpression : thenExpression;
181+
if (nodeToKeep is ThrowExpression &&
182+
grandParent is CascadeExpression &&
183+
grandParent.target == parent) {
184+
// It's not safe to transform something like
185+
// `a ? b : throw c..cascadeSection` into `throw c..cascadeSection`,
186+
// because the former parses as `(a ? b : throw c)..cascadeSection`,
187+
// and the latter parses as `throw (c..cascadeSection)`. This is
188+
// unlikely to arise in practice, so just bail out and don't generate
189+
// a correction.
190+
return;
191+
}
192+
await _addEdit((builder) {
193+
builder.addDeletion(range.startStart(parent, nodeToKeep));
194+
builder.addDeletion(range.endEnd(nodeToKeep, parent));
195+
});
196+
case Statement(
197+
parent: IfStatement(
198+
:var expression,
199+
caseClause: null,
200+
:var thenStatement,
201+
:var elseStatement,
202+
) &&
203+
var ifStatement,
204+
)
205+
when _looksSideEffectFree(expression):
206+
if (node == thenStatement) {
207+
if (elseStatement == null) {
208+
// The "if" statement is `if (expression) dead;`, so it can be
209+
// removed in its entirety.
210+
await _removeStatement(ifStatement);
211+
} else {
212+
// The "if" statement is `if (expression) dead; else live;`, so it
213+
// can be replaced with `live;`.
214+
await _replaceStatementWithInnerStatement(
215+
ifStatement,
216+
elseStatement,
217+
);
218+
}
219+
} else if (node == elseStatement) {
220+
// The "if" statement is `if (expression) live; else dead;`, so it can
221+
// be replaced with `live;`.
222+
await _replaceStatementWithInnerStatement(ifStatement, thenStatement);
223+
}
163224
case Statement(:Block parent):
164225
var lastStatementInBlock = parent.statements.last;
165226
assert(
@@ -189,6 +250,64 @@ class RemoveDeadCode extends ResolvedCorrectionProducer {
189250
}
190251
}
191252

253+
/// Determines whether [expression] is side effect free, under certain
254+
/// presumptions.
255+
///
256+
/// The presumptions are:
257+
/// - A getter invocation is presumed to be side effect free, because (a) in
258+
/// practice, most getter invocations are side effect free, and (b) it would
259+
/// take a lot of analysis to establish with certainty whether a particular
260+
/// getter invocation is side effect free.
261+
/// - A use of a non-null assertion (`!`) or cast (`as`) is presumed to be
262+
/// side effect free, because in practice these constructs are only used
263+
/// when their operand is already known by the user to be of the appropriate
264+
/// type. This enables dead code like `if (a.b!.c == null) { ... }` to be
265+
/// eliminated (assuming `c` has a non-null type).
266+
///
267+
/// These assumptions aren't necessarily going to be true of all code. We rely
268+
/// on the user to verify these assumptions by inspecting the results of dead
269+
/// code removal.
270+
bool _looksSideEffectFree(Expression expression) {
271+
switch (expression) {
272+
case AsExpression(:var expression):
273+
// A cast is presumed to be side effect free (provided that its operand
274+
// is).
275+
return _looksSideEffectFree(expression);
276+
case BinaryExpression(:var leftOperand, :var rightOperand):
277+
return _looksSideEffectFree(leftOperand) &&
278+
_looksSideEffectFree(rightOperand);
279+
case SimpleIdentifier():
280+
// A simple identifier might be a local variable reference or a method
281+
// tear-off, in which case it's definitely side effect free. Or it might
282+
// be a call to a getter, in which case we assume it's side effect free.
283+
return true;
284+
case Literal():
285+
return true;
286+
case ParenthesizedExpression(:var expression):
287+
return _looksSideEffectFree(expression);
288+
case PostfixExpression(
289+
:var operand,
290+
operator: Token(type: TokenType.BANG),
291+
):
292+
// A non-null assertion is presumed to be side effect free (provided
293+
// that its operand is).
294+
return _looksSideEffectFree(operand);
295+
case PrefixedIdentifier(prefix: Expression? target):
296+
case PropertyAccess(:var target):
297+
// If the target isn't side effect free, then the property access isn't.
298+
if (target != null && !_looksSideEffectFree(target)) return false;
299+
// A property access might be a method tear-off, in which case it's
300+
// definitely side effect free. Or it might be a call to a getter, in
301+
// which case we assume it's side effect free.
302+
return true;
303+
case SuperExpression():
304+
return true;
305+
default:
306+
// Anything else is conservatively assumed to have side effects.
307+
return false;
308+
}
309+
}
310+
192311
/// In the case where the dead code range starts somewhere inside
193312
/// [partiallyDeadNode], and may continue past it, removes any code after
194313
/// [partiallyDeadNode] that is included in the dead code range.
@@ -244,4 +363,63 @@ class RemoveDeadCode extends ResolvedCorrectionProducer {
244363
}
245364
}
246365
}
366+
367+
Future<void> _removeStatement(Statement statement) async {
368+
var parent = statement.parent;
369+
switch (parent) {
370+
case Block():
371+
await _addEdit((builder) {
372+
_deleteLineRange(builder, range.entity(statement));
373+
});
374+
case IfStatement(:var thenStatement, :var elseStatement)
375+
when statement == elseStatement:
376+
// The code looks something like:
377+
// if (condition) live; else if (false) dead;
378+
// thenStatement-^^^^^ ^^^^^^^^^^^^^^^^-elseStatement
379+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-parent
380+
// So drop the `else` and the `elseStatement` to produce:
381+
// if (condition) live;
382+
await _addEdit((builder) {
383+
builder.addDeletion(range.endEnd(thenStatement, parent));
384+
});
385+
default:
386+
// In general it's not safe to just remove a statement, since it affects
387+
// how the surrounding code will be parsed. So don't generate a
388+
// correction.
389+
break;
390+
}
391+
}
392+
393+
Future<void> _replaceStatementWithInnerStatement(
394+
Statement statement,
395+
Statement innerStatement,
396+
) async {
397+
var parent = statement.parent;
398+
if (parent is Block && innerStatement is Block) {
399+
// The code looks something like:
400+
// { ... if (condition) { live; } ... }
401+
// ^^^^^^^^^-innerStatement
402+
// ^^^^^^^^^^^^^^^^^^^^^^^^-statement
403+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-parent
404+
// So drop the opening and closing brace of `innerStatement` to produce:
405+
// { ... live; ... }
406+
await _addEdit((builder) {
407+
_deleteLineRange(
408+
builder,
409+
range.startEnd(statement, innerStatement.leftBracket),
410+
);
411+
_deleteLineRange(
412+
builder,
413+
range.startEnd(innerStatement.rightBracket, statement),
414+
);
415+
});
416+
} else {
417+
// Don't do anything special with braces. Just replace `statement` with
418+
// `innerStatement`.
419+
await _addEdit((builder) {
420+
builder.addDeletion(range.startStart(statement, innerStatement));
421+
builder.addDeletion(range.endEnd(innerStatement, statement));
422+
});
423+
}
424+
}
247425
}

0 commit comments

Comments
 (0)