Skip to content

Commit 09f1bf8

Browse files
robinkundejrose-apple
authored andcommitted
[Diagnostics] Diagnose unresolved and typoed labels when using labeled break or continue statements inside loops (#21915)
Adds proper diagnostics when using unresolved labels on break statements inside loops. In case of simple typos, also provides suggestions and fixits. Right now, the misleading "error: 'break' is only allowed inside a loop, if, do, or switch" is emitted in 4.2 and master. Resolves SR-9677.
1 parent d95861b commit 09f1bf8

File tree

5 files changed

+163
-40
lines changed

5 files changed

+163
-40
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3172,6 +3172,12 @@ ERROR(continue_outside_loop,none,
31723172
ERROR(continue_not_in_this_stmt,none,
31733173
"'continue' cannot be used with %0 statements", (StringRef))
31743174

3175+
ERROR(unresolved_label,none,
3176+
"use of unresolved label %0", (Identifier))
3177+
ERROR(unresolved_label_corrected,none,
3178+
"use of unresolved label %0; did you mean %1?",
3179+
(Identifier, Identifier))
3180+
31753181
// Switch Stmt
31763182
ERROR(no_match_operator,none,
31773183
"no binary '~=' operator available for 'switch' statement", ())

lib/Sema/TypeCheckNameLookup.cpp

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -574,18 +574,9 @@ LookupResult TypeChecker::lookupConstructors(DeclContext *dc, Type type,
574574
return lookupMember(dc, type, DeclBaseName::createConstructor(), options);
575575
}
576576

577-
enum : unsigned {
578-
/// Never consider a candidate that's this distance away or worse.
579-
UnreasonableCallEditDistance = 8,
580-
581-
/// Don't consider candidates that score worse than the given distance
582-
/// from the best candidate.
583-
MaxCallEditDistanceFromBestCandidate = 1
584-
};
585-
586-
static unsigned getCallEditDistance(DeclName writtenName,
587-
DeclName correctedName,
588-
unsigned maxEditDistance) {
577+
unsigned TypeChecker::getCallEditDistance(DeclName writtenName,
578+
DeclName correctedName,
579+
unsigned maxEditDistance) {
589580
// TODO: consider arguments.
590581
// TODO: maybe ignore certain kinds of missing / present labels for the
591582
// first argument label?

lib/Sema/TypeCheckStmt.cpp

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "swift/Basic/STLExtras.h"
3232
#include "swift/Basic/SourceManager.h"
3333
#include "swift/Basic/Statistic.h"
34+
#include "swift/Basic/TopCollection.h"
3435
#include "swift/Parse/Lexer.h"
3536
#include "swift/Parse/LocalContext.h"
3637
#include "swift/Syntax/TokenKinds.h"
@@ -787,6 +788,7 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
787788

788789
Stmt *visitBreakStmt(BreakStmt *S) {
789790
LabeledStmt *Target = nullptr;
791+
TopCollection<unsigned, LabeledStmt *> labelCorrections(3);
790792
// Pick the nearest break target that matches the specified name.
791793
if (S->getTargetName().empty()) {
792794
for (auto I = ActiveLabeledStmts.rbegin(), E = ActiveLabeledStmts.rend();
@@ -806,31 +808,39 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
806808
if (S->getTargetName() == (*I)->getLabelInfo().Name) {
807809
Target = *I;
808810
break;
811+
} else {
812+
unsigned distance =
813+
TC.getCallEditDistance(S->getTargetName(), (*I)->getLabelInfo().Name,
814+
TypeChecker::UnreasonableCallEditDistance);
815+
if (distance < TypeChecker::UnreasonableCallEditDistance)
816+
labelCorrections.insert(distance, std::move(*I));
809817
}
810818
}
819+
labelCorrections.filterMaxScoreRange(
820+
TypeChecker::MaxCallEditDistanceFromBestCandidate);
811821
}
812822

813823
if (!Target) {
814824
// If we're in a defer, produce a tailored diagnostic.
815825
if (isInDefer()) {
816826
TC.diagnose(S->getLoc(), diag::jump_out_of_defer, "break");
817-
return nullptr;
818-
}
819-
820-
auto diagid = diag::break_outside_loop;
821-
822-
// If someone is using an unlabeled break inside of an 'if' or 'do'
823-
// statement, produce a more specific error.
824-
if (S->getTargetName().empty() &&
825-
std::any_of(ActiveLabeledStmts.rbegin(),
826-
ActiveLabeledStmts.rend(),
827-
[&](Stmt *S) -> bool {
828-
return isa<IfStmt>(S) || isa<DoStmt>(S);
829-
})) {
830-
diagid = diag::unlabeled_break_outside_loop;
827+
} else if (S->getTargetName().empty()) {
828+
// If we're dealing with an unlabeled break inside of an 'if' or 'do'
829+
// statement, produce a more specific error.
830+
if (std::any_of(ActiveLabeledStmts.rbegin(),
831+
ActiveLabeledStmts.rend(),
832+
[&](Stmt *S) -> bool {
833+
return isa<IfStmt>(S) || isa<DoStmt>(S);
834+
})) {
835+
TC.diagnose(S->getLoc(), diag::unlabeled_break_outside_loop);
836+
} else {
837+
// Otherwise produce a generic error.
838+
TC.diagnose(S->getLoc(), diag::break_outside_loop);
839+
}
840+
} else {
841+
emitUnresolvedLabelDiagnostics(TC, S->getTargetLoc(), S->getTargetName(),
842+
labelCorrections);
831843
}
832-
833-
TC.diagnose(S->getLoc(), diagid);
834844
return nullptr;
835845
}
836846
S->setTarget(Target);
@@ -839,6 +849,7 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
839849

840850
Stmt *visitContinueStmt(ContinueStmt *S) {
841851
LabeledStmt *Target = nullptr;
852+
TopCollection<unsigned, LabeledStmt *> labelCorrections(3);
842853
// Scan to see if we are in any non-switch labeled statements (loops). Scan
843854
// inside out.
844855
if (S->getTargetName().empty()) {
@@ -858,31 +869,68 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
858869
if (S->getTargetName() == (*I)->getLabelInfo().Name) {
859870
Target = *I;
860871
break;
872+
} else {
873+
unsigned distance =
874+
TC.getCallEditDistance(S->getTargetName(), (*I)->getLabelInfo().Name,
875+
TypeChecker::UnreasonableCallEditDistance);
876+
if (distance < TypeChecker::UnreasonableCallEditDistance)
877+
labelCorrections.insert(distance, std::move(*I));
861878
}
862879
}
880+
labelCorrections.filterMaxScoreRange(
881+
TypeChecker::MaxCallEditDistanceFromBestCandidate);
863882
}
864883

865-
if (!Target) {
884+
if (Target) {
885+
// Continue cannot be used to repeat switches, use fallthrough instead.
886+
if (!Target->isPossibleContinueTarget()) {
887+
TC.diagnose(S->getLoc(), diag::continue_not_in_this_stmt,
888+
isa<SwitchStmt>(Target) ? "switch" : "if");
889+
return nullptr;
890+
}
891+
} else {
866892
// If we're in a defer, produce a tailored diagnostic.
867893
if (isInDefer()) {
868894
TC.diagnose(S->getLoc(), diag::jump_out_of_defer, "break");
869-
return nullptr;
895+
} else if (S->getTargetName().empty()) {
896+
// If we're dealing with an unlabeled continue, produce a generic error.
897+
TC.diagnose(S->getLoc(), diag::continue_outside_loop);
898+
} else {
899+
emitUnresolvedLabelDiagnostics(TC, S->getTargetLoc(), S->getTargetName(),
900+
labelCorrections);
870901
}
871-
872-
TC.diagnose(S->getLoc(), diag::continue_outside_loop);
873-
return nullptr;
874-
}
875-
876-
// Continue cannot be used to repeat switches, use fallthrough instead.
877-
if (!Target->isPossibleContinueTarget()) {
878-
TC.diagnose(S->getLoc(), diag::continue_not_in_this_stmt,
879-
isa<SwitchStmt>(Target) ? "switch" : "if");
880902
return nullptr;
881903
}
882-
883904
S->setTarget(Target);
884905
return S;
885906
}
907+
908+
static void
909+
emitUnresolvedLabelDiagnostics(TypeChecker &tc, SourceLoc targetLoc, Identifier targetName,
910+
TopCollection<unsigned, LabeledStmt *> corrections) {
911+
// If an unresolved label was used, but we have a single correction,
912+
// produce the specific diagnostic and fixit.
913+
if (corrections.size() == 1) {
914+
tc.diagnose(targetLoc, diag::unresolved_label_corrected,
915+
targetName, corrections.begin()->Value->getLabelInfo().Name)
916+
.highlight(SourceRange(targetLoc))
917+
.fixItReplace(SourceRange(targetLoc),
918+
corrections.begin()->Value->getLabelInfo().Name.str());
919+
tc.diagnose(corrections.begin()->Value->getLabelInfo().Loc,
920+
diag::identifier_declared_here,
921+
corrections.begin()->Value->getLabelInfo().Name);
922+
} else {
923+
// If we have multiple corrections or none, produce a generic diagnostic
924+
// and all corrections available.
925+
tc.diagnose(targetLoc, diag::unresolved_label, targetName)
926+
.highlight(SourceRange(targetLoc));
927+
for (auto &entry : corrections)
928+
tc.diagnose(entry.Value->getLabelInfo().Loc, diag::note_typo_candidate,
929+
entry.Value->getLabelInfo().Name.str())
930+
.fixItReplace(SourceRange(targetLoc),
931+
entry.Value->getLabelInfo().Name.str());
932+
}
933+
}
886934

887935
Stmt *visitFallthroughStmt(FallthroughStmt *S) {
888936
if (!SwitchLevel) {

lib/Sema/TypeChecker.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,20 @@ class TypeChecker final : public LazyResolver {
21282128
/// Attempt to omit needless words from the name of the given declaration.
21292129
Optional<Identifier> omitNeedlessWords(VarDecl *var);
21302130

2131+
/// Calculate edit distance between declaration names.
2132+
static unsigned getCallEditDistance(DeclName writtenName,
2133+
DeclName correctedName,
2134+
unsigned maxEditDistance);
2135+
2136+
enum : unsigned {
2137+
/// Never consider a candidate that's this distance away or worse.
2138+
UnreasonableCallEditDistance = 8,
2139+
2140+
/// Don't consider candidates that score worse than the given distance
2141+
/// from the best candidate.
2142+
MaxCallEditDistanceFromBestCandidate = 1
2143+
};
2144+
21312145
/// Check for a typo correction.
21322146
void performTypoCorrection(DeclContext *DC,
21332147
DeclRefKind refKind,

test/stmt/statements.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,70 @@ func bad_if() {
535535
if (x: 1) {} // expected-error {{'(x: Int)' is not convertible to 'Bool'}}
536536
}
537537

538+
// Typo correction for loop labels
539+
for _ in [1] {
540+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
541+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
542+
}
543+
while true {
544+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
545+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
546+
}
547+
repeat {
548+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
549+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
550+
} while true
551+
552+
outerLoop: for _ in [1] { // expected-note {{'outerLoop' declared here}}
553+
break outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{9-18=outerLoop}}
554+
}
555+
outerLoop: for _ in [1] { // expected-note {{'outerLoop' declared here}}
556+
continue outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{12-21=outerLoop}}
557+
}
558+
outerLoop: while true { // expected-note {{'outerLoop' declared here}}
559+
break outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{9-18=outerLoop}}
560+
}
561+
outerLoop: while true { // expected-note {{'outerLoop' declared here}}
562+
continue outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{12-21=outerLoop}}
563+
}
564+
outerLoop: repeat { // expected-note {{'outerLoop' declared here}}
565+
break outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{9-18=outerLoop}}
566+
} while true
567+
outerLoop: repeat { // expected-note {{'outerLoop' declared here}}
568+
continue outerloop // expected-error {{use of unresolved label 'outerloop'; did you mean 'outerLoop'?}} {{12-21=outerLoop}}
569+
} while true
570+
571+
outerLoop1: for _ in [1] { // expected-note {{did you mean 'outerLoop1'?}} {{11-20=outerLoop1}}
572+
outerLoop2: for _ in [1] { // expected-note {{did you mean 'outerLoop2'?}} {{11-20=outerLoop2}}
573+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
574+
}
575+
}
576+
outerLoop1: for _ in [1] { // expected-note {{did you mean 'outerLoop1'?}} {{14-23=outerLoop1}}
577+
outerLoop2: for _ in [1] { // expected-note {{did you mean 'outerLoop2'?}} {{14-23=outerLoop2}}
578+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
579+
}
580+
}
581+
outerLoop1: while true { // expected-note {{did you mean 'outerLoop1'?}} {{11-20=outerLoop1}}
582+
outerLoop2: while true { // expected-note {{did you mean 'outerLoop2'?}} {{11-20=outerLoop2}}
583+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
584+
}
585+
}
586+
outerLoop1: while true { // expected-note {{did you mean 'outerLoop1'?}} {{14-23=outerLoop1}}
587+
outerLoop2: while true { // expected-note {{did you mean 'outerLoop2'?}} {{14-23=outerLoop2}}
588+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
589+
}
590+
}
591+
outerLoop1: repeat { // expected-note {{did you mean 'outerLoop1'?}} {{11-20=outerLoop1}}
592+
outerLoop2: repeat { // expected-note {{did you mean 'outerLoop2'?}} {{11-20=outerLoop2}}
593+
break outerloop // expected-error {{use of unresolved label 'outerloop'}}
594+
} while true
595+
} while true
596+
outerLoop1: repeat { // expected-note {{did you mean 'outerLoop1'?}} {{14-23=outerLoop1}}
597+
outerLoop2: repeat { // expected-note {{did you mean 'outerLoop2'?}} {{14-23=outerLoop2}}
598+
continue outerloop // expected-error {{use of unresolved label 'outerloop'}}
599+
} while true
600+
} while true
601+
538602
// Errors in case syntax
539603
class
540604
case, // expected-error {{expected identifier in enum 'case' declaration}} expected-error {{expected pattern}}

0 commit comments

Comments
 (0)