Skip to content

Commit 2de0a6f

Browse files
committed
fix <rdar://25178926> QoI: Warn about cases where switch statement "ignores" where clause
It is a common point of confusion that code like: switch value { case .Foo, .Bar where someNumber != 100: Only applies the where clause to the second pattern, not every pattern in the case. Resolve this by warning about the ambiguity, providing two notes (with fixits) that resolve the issue in different ways: t.swift:25:17: warning: 'where' only applies to the second pattern match in this case case .Foo, .Bar where someNumber != 100: ~~~~ ^ ~~~~~~~~~~~~~~~~~ t.swift:25:12: note: disambiguate by adding a line break between them if this is desired case .Foo, .Bar where someNumber != 100: ^ t.swift:25:6: note: duplicate the 'where' on both patterns to check both patterns case .Foo, .Bar where someNumber != 100: ^~~~ where someNumber != 100
1 parent 6778c88 commit 2de0a6f

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,15 @@ ERROR(unnecessary_cast_over_optionset,none,
21762176
ERROR(type_mismatch_multiple_pattern_list,none,
21772177
"pattern variable bound to type %0, expected type %1", (Type, Type))
21782178

2179+
WARNING(where_on_one_item, none,
2180+
"'where' only applies to the second pattern match in this case", ())
2181+
2182+
NOTE(add_where_newline, none,
2183+
"disambiguate by adding a line break between them if this is desired", ())
2184+
2185+
NOTE(duplicate_where, none,
2186+
"duplicate the 'where' on both patterns to check both patterns",())
2187+
21792188
//------------------------------------------------------------------------------
21802189
// Type Check Patterns
21812190
//------------------------------------------------------------------------------

lib/Sema/MiscDiagnostics.cpp

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,7 +1825,8 @@ static bool plusEqualOneIncrementForConvertingCStyleForLoop(TypeChecker &TC, con
18251825
}
18261826

18271827
static void checkCStyleForLoop(TypeChecker &TC, const ForStmt *FS) {
1828-
// If we're missing semi-colons we'll already be erroring out, and this may not even have been intended as C-style.
1828+
// If we're missing semi-colons we'll already be erroring out, and this may
1829+
// not even have been intended as C-style.
18291830
if (FS->getFirstSemicolonLoc().isInvalid() || FS->getSecondSemicolonLoc().isInvalid())
18301831
return;
18311832

@@ -1835,15 +1836,16 @@ static void checkCStyleForLoop(TypeChecker &TC, const ForStmt *FS) {
18351836

18361837
// Verify that there is only one loop variable, and it is declared here.
18371838
auto initializers = FS->getInitializerVarDecls();
1838-
PatternBindingDecl *loopVarDecl = initializers.size() == 2 ? dyn_cast<PatternBindingDecl>(initializers[0]) : nullptr;
1839+
PatternBindingDecl *loopVarDecl = initializers.size() == 2 ?
1840+
dyn_cast<PatternBindingDecl>(initializers[0]) : nullptr;
18391841
if (!loopVarDecl || loopVarDecl->getNumPatternEntries() != 1)
18401842
return;
18411843

18421844
VarDecl *loopVar = dyn_cast<VarDecl>(initializers[1]);
18431845
Expr *startValue = loopVarDecl->getInit(0);
18441846
Expr *endValue = endConditionValueForConvertingCStyleForLoop(FS, loopVar);
18451847
bool strideByOne = unaryIncrementForConvertingCStyleForLoop(FS, loopVar) ||
1846-
plusEqualOneIncrementForConvertingCStyleForLoop(TC, FS, loopVar);
1848+
plusEqualOneIncrementForConvertingCStyleForLoop(TC, FS, loopVar);
18471849

18481850
if (!loopVar || !startValue || !endValue || !strideByOne)
18491851
return;
@@ -1859,16 +1861,78 @@ static void checkCStyleForLoop(TypeChecker &TC, const ForStmt *FS) {
18591861
return;
18601862
}
18611863

1862-
SourceLoc loopPatternEnd = Lexer::getLocForEndOfToken(TC.Context.SourceMgr, loopVarDecl->getPattern(0)->getEndLoc());
1863-
SourceLoc endOfIncrementLoc = Lexer::getLocForEndOfToken(TC.Context.SourceMgr, FS->getIncrement().getPtrOrNull()->getEndLoc());
1864+
SourceLoc loopPatternEnd =
1865+
Lexer::getLocForEndOfToken(TC.Context.SourceMgr,
1866+
loopVarDecl->getPattern(0)->getEndLoc());
1867+
SourceLoc endOfIncrementLoc =
1868+
Lexer::getLocForEndOfToken(TC.Context.SourceMgr,
1869+
FS->getIncrement().getPtrOrNull()->getEndLoc());
18641870

18651871
diagnostic
18661872
.fixItRemoveChars(loopVarDecl->getLoc(), loopVar->getLoc())
18671873
.fixItReplaceChars(loopPatternEnd, startValue->getStartLoc(), " in ")
1868-
.fixItReplaceChars(FS->getFirstSemicolonLoc(), endValue->getStartLoc(), " ..< ")
1874+
.fixItReplaceChars(FS->getFirstSemicolonLoc(), endValue->getStartLoc(),
1875+
" ..< ")
18691876
.fixItRemoveChars(FS->getSecondSemicolonLoc(), endOfIncrementLoc);
18701877
}
18711878

1879+
1880+
// Perform MiscDiagnostics on Switch Statements.
1881+
static void checkSwitch(TypeChecker &TC, const SwitchStmt *stmt) {
1882+
// We want to warn about "case .Foo, .Bar where 1 != 100:" since the where
1883+
// clause only applies to the second case, and this is surprising.
1884+
for (auto cs : stmt->getCases()) {
1885+
// The case statement can have multiple case items, each can have a where.
1886+
// If we find a "where", and there is a preceding item without a where, and
1887+
// if they are on the same source line, then warn.
1888+
auto items = cs->getCaseLabelItems();
1889+
1890+
// Don't do any work for the vastly most common case.
1891+
if (items.size() == 1) continue;
1892+
1893+
// Ignore the first item, since it can't have preceding ones.
1894+
for (unsigned i = 1, e = items.size(); i != e; ++i) {
1895+
// Must have a where clause.
1896+
auto where = items[i].getGuardExpr();
1897+
if (!where)
1898+
continue;
1899+
1900+
// Preceding item must not.
1901+
if (items[i-1].getGuardExpr())
1902+
continue;
1903+
1904+
// Must be on the same source line.
1905+
auto prevLoc = items[i-1].getStartLoc();
1906+
auto thisLoc = items[i].getStartLoc();
1907+
if (prevLoc.isInvalid() || thisLoc.isInvalid())
1908+
continue;
1909+
1910+
auto &SM = TC.Context.SourceMgr;
1911+
auto prevLineCol = SM.getLineAndColumn(prevLoc);
1912+
if (SM.getLineNumber(thisLoc) != prevLineCol.first)
1913+
continue;
1914+
1915+
TC.diagnose(items[i].getWhereLoc(), diag::where_on_one_item)
1916+
.highlight(items[i].getPattern()->getSourceRange())
1917+
.highlight(where->getSourceRange());
1918+
1919+
// Whitespace it out to the same column as the previous item.
1920+
std::string whitespace(prevLineCol.second-1, ' ');
1921+
TC.diagnose(thisLoc, diag::add_where_newline)
1922+
.fixItInsert(thisLoc, "\n"+whitespace);
1923+
1924+
auto whereRange = SourceRange(items[i].getWhereLoc(),
1925+
where->getEndLoc());
1926+
auto charRange = Lexer::getCharSourceRangeFromSourceRange(SM, whereRange);
1927+
auto whereText = SM.extractText(charRange);
1928+
TC.diagnose(prevLoc, diag::duplicate_where)
1929+
.fixItInsertAfter(items[i-1].getEndLoc(), " " + whereText.str())
1930+
.highlight(items[i-1].getSourceRange());
1931+
}
1932+
}
1933+
}
1934+
1935+
18721936
static Optional<ObjCSelector>
18731937
parseObjCSelector(ASTContext &ctx, StringRef string) {
18741938
// Find the first colon.
@@ -2250,6 +2314,9 @@ void swift::performStmtDiagnostics(TypeChecker &TC, const Stmt *S) {
22502314

22512315
if (auto forStmt = dyn_cast<ForStmt>(S))
22522316
checkCStyleForLoop(TC, forStmt);
2317+
2318+
if (auto switchStmt = dyn_cast<SwitchStmt>(S))
2319+
checkSwitch(TC, switchStmt);
22532320
}
22542321

22552322
//===----------------------------------------------------------------------===//

test/stmt/statements.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,37 @@ func f(x : Int, y : Int) {
480480

481481

482482

483+
// <rdar://problem/25178926> QoI: Warn about cases where switch statement "ignores" where clause
484+
enum Type {
485+
case Foo
486+
case Bar
487+
}
488+
func r25178926(a : Type) {
489+
switch a {
490+
case .Foo, .Bar where 1 != 100:
491+
// expected-warning @-1 {{'where' only applies to the second pattern match in this case}}
492+
// expected-note @-2 {{disambiguate by adding a line break between them if this is desired}} {{14-14=\n }}
493+
// expected-note @-3 {{duplicate the 'where' on both patterns to check both patterns}} {{12-12= where 1 != 100}}
494+
break
495+
}
496+
497+
switch a {
498+
case .Foo: break
499+
case .Bar where 1 != 100: break
500+
}
501+
502+
switch a {
503+
case .Foo, // no warn
504+
.Bar where 1 != 100:
505+
break
506+
}
507+
508+
switch a {
509+
case .Foo where 1 != 100, .Bar where 1 != 100:
510+
break
511+
}
512+
}
513+
483514

484515

485516
// Errors in case syntax

0 commit comments

Comments
 (0)