Skip to content

Commit 2379928

Browse files
committed
Fix <rdar://22774938> QoI: "never used" in an "if let" should rewrite expression to use != nil
When we see an unused variable in a simple-enough "if/let" (also guard and while of course), fixit it into a comparison against nil instead of replacing the name of the variable with "_". Also special case initialization with an as? expression, since we can transform that into an "is" boolean test.
1 parent 5f3f1a3 commit 2379928

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2612,10 +2612,17 @@ NOTE(availability_conformance_introduced_here, sema_avail, none,
26122612
// Variable usage diagnostics
26132613
//------------------------------------------------------------------------------
26142614

2615+
WARNING(pbd_never_used_stmtcond, sema_varusage, none,
2616+
"value %0 was defined but never used; consider replacing "
2617+
"with boolean test",
2618+
(Identifier))
2619+
26152620
WARNING(pbd_never_used, sema_varusage, none,
26162621
"initialization of %select{variable|immutable value}1 %0 was never used"
26172622
"; consider replacing with assignment to '_' or removing it",
26182623
(Identifier, unsigned))
2624+
2625+
26192626
WARNING(capture_never_used, sema_varusage, none,
26202627
"capture %0 was never used",
26212628
(Identifier))

lib/Sema/MiscDiagnostics.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,11 @@ class VarDeclUsageChecker : public ASTWalker {
12161216
/// This is a mapping from an OpaqueValue to the expression that initialized
12171217
/// it.
12181218
llvm::SmallDenseMap<OpaqueValueExpr*, Expr*> OpaqueValueMap;
1219+
1220+
/// This is a mapping from VarDecls to the if/while/guard statement that they
1221+
/// occur in, when they are in a pattern in a StmtCondition.
1222+
llvm::SmallDenseMap<VarDecl*, LabeledConditionalStmt*> StmtConditionForVD;
1223+
12191224
bool sawError = false;
12201225

12211226
VarDeclUsageChecker(const VarDeclUsageChecker &) = delete;
@@ -1340,6 +1345,18 @@ class VarDeclUsageChecker : public ASTWalker {
13401345
// for them.
13411346
if (auto *ICS = dyn_cast<IfConfigStmt>(S))
13421347
handleIfConfig(ICS);
1348+
1349+
// Keep track of an association between vardecls and the StmtCondition that
1350+
// they are bound in for IfStmt, GuardStmt, WhileStmt, etc.
1351+
if (auto LCS = dyn_cast<LabeledConditionalStmt>(S)) {
1352+
for (auto &cond : LCS->getCond())
1353+
if (auto pat = cond.getPatternOrNull()) {
1354+
pat->forEachVariable([&](VarDecl *VD) {
1355+
StmtConditionForVD[VD] = LCS;
1356+
});
1357+
}
1358+
}
1359+
13431360
return { true, S };
13441361
}
13451362

@@ -1409,6 +1426,60 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
14091426
continue;
14101427
}
14111428

1429+
// If the variable is defined in a pattern in an if/while/guard statement,
1430+
// see if we can produce a tuned fixit. When we have something like:
1431+
//
1432+
// if let x = <expr> {
1433+
//
1434+
// we prefer to rewrite it to:
1435+
//
1436+
// if <expr> != nil {
1437+
//
1438+
if (auto SC = StmtConditionForVD[var]) {
1439+
// We only handle the "if let" case right now, since it is vastly the
1440+
// most common situation that people run into.
1441+
if (SC->getCond().size() == 1) {
1442+
auto pattern = SC->getCond()[0].getPattern();
1443+
if (auto OSP = dyn_cast<OptionalSomePattern>(pattern))
1444+
if (auto LP = dyn_cast<VarPattern>(OSP->getSubPattern()))
1445+
if (isa<NamedPattern>(LP->getSubPattern())) {
1446+
auto initExpr = SC->getCond()[0].getInitializer();
1447+
auto beforeExprLoc =
1448+
initExpr->getStartLoc().getAdvancedLocOrInvalid(-1);
1449+
if (beforeExprLoc.isValid()) {
1450+
unsigned noParens = initExpr->canAppendCallParentheses();
1451+
1452+
// If the subexpr is an "as?" cast, we can rewrite it to
1453+
// be an "is" test.
1454+
bool isIsTest = false;
1455+
if (isa<ConditionalCheckedCastExpr>(initExpr) &&
1456+
!initExpr->isImplicit()) {
1457+
noParens = isIsTest = true;
1458+
}
1459+
1460+
auto diagIF = TC.diagnose(var->getLoc(),
1461+
diag::pbd_never_used_stmtcond,
1462+
var->getName());
1463+
auto introducerLoc = SC->getCond()[0].getIntroducerLoc();
1464+
diagIF.fixItReplace(SourceRange(introducerLoc, beforeExprLoc),
1465+
&"("[noParens]);
1466+
1467+
if (isIsTest) {
1468+
// If this was an "x as? T" check, rewrite it to "x is T".
1469+
auto CCE = cast<ConditionalCheckedCastExpr>(initExpr);
1470+
diagIF.fixItReplace(SourceRange(CCE->getLoc(),
1471+
CCE->getQuestionLoc()),
1472+
"is");
1473+
} else {
1474+
diagIF.fixItInsertAfter(initExpr->getEndLoc(),
1475+
&") != nil"[noParens]);
1476+
}
1477+
continue;
1478+
}
1479+
}
1480+
}
1481+
}
1482+
14121483
// Otherwise, this is something more complex, perhaps
14131484
// let (a,b) = foo()
14141485
// Just rewrite the one variable with a _.

test/decl/var/usage.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,28 @@ func testFixitsInStatementsWithPatterns(a : Int?) {
219219
_ = b
220220
}
221221
}
222+
223+
224+
// <rdar://22774938> QoI: "never used" in an "if let" should rewrite expression to use != nil
225+
func test(a : Int?, b : Any) {
226+
if true == true, let x = a { // expected-warning {{immutable value 'x' was never used; consider replacing with '_' or removing it}} {{24-25=_}}
227+
}
228+
if let x = a, y = a { // expected-warning {{immutable value 'x' was never used; consider replacing with '_' or removing it}} {{10-11=_}}
229+
_ = y
230+
}
231+
232+
// Simple case, insert a comparison with nil.
233+
if let x = a { // expected-warning {{value 'x' was defined but never used; consider replacing with boolean test}} {{6-14=}} {{15-15= != nil}}
234+
}
235+
236+
// General case, need to insert parentheses.
237+
if let x = a ?? a {} // expected-warning {{value 'x' was defined but never used; consider replacing with boolean test}} {{6-14=(}} {{20-20=) != nil}}
238+
239+
// Special case, we can turn this into an 'is' test.
240+
if let x = b as? Int { // expected-warning {{value 'x' was defined but never used; consider replacing with boolean test}} {{6-14=}} {{16-19=is}}
241+
}
242+
243+
244+
}
245+
246+

0 commit comments

Comments
 (0)