Skip to content

Commit d138290

Browse files
committed
Produce warnings when implicit optional promotions are introduced in some
common standard library operators. This is progress towards: <rdar://problem/27457457> [Type checker] Diagnose unsavory optional injections but there is more work to be done here.
1 parent b07fd4b commit d138290

File tree

5 files changed

+95
-11
lines changed

5 files changed

+95
-11
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,6 +2214,13 @@ NOTE(fix_unqualified_access_top_level_multi,none,
22142214
"use '%0' to reference the %1 in module %2",
22152215
(StringRef, DescriptiveDeclKind, Identifier))
22162216

2217+
WARNING(use_of_qq_on_non_optional_value,none,
2218+
"left side of nil coalescing operator '?""?' is non-optional, "
2219+
"so the right side is never used", ())
2220+
WARNING(nonoptional_compare_to_nil,none,
2221+
"comparing non-optional value to nil always returns"
2222+
" %select{false|true}0", (bool))
2223+
22172224
ERROR(type_of_metatype,none,
22182225
"'.dynamicType' is not allowed after a type name", ())
22192226
ERROR(invalid_noescape_use,none,

lib/Sema/MiscDiagnostics.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ static void diagSelfAssignment(TypeChecker &TC, const Expr *E) {
8888
/// argument lists.
8989
/// - 'self.init' and 'super.init' cannot be wrapped in a larger expression
9090
/// or statement.
91+
/// - Warn about promotions to optional in specific syntactic forms.
9192
///
9293
static void diagSyntacticUseRestrictions(TypeChecker &TC, const Expr *E,
9394
const DeclContext *DC,
@@ -268,6 +269,9 @@ static void diagSyntacticUseRestrictions(TypeChecker &TC, const Expr *E,
268269
// Check function calls, looking through implicit conversions on the
269270
// function and inspecting the arguments directly.
270271
if (auto *Call = dyn_cast<ApplyExpr>(E)) {
272+
// Warn about surprising implicit optional promotions.
273+
checkOptionalPromotions(Call);
274+
271275
// Check for tuple splat.
272276
checkTupleSplat(Call);
273277

@@ -594,6 +598,72 @@ static void diagSyntacticUseRestrictions(TypeChecker &TC, const Expr *E,
594598
.fixItInsert(DRE->getStartLoc(), namePlusDot);
595599
}
596600
}
601+
602+
/// Return true if this expression is an implicit promotion from T to T?.
603+
static bool isImplicitPromotionToOptional(Expr *E) {
604+
return E->isImplicit() && isa<InjectIntoOptionalExpr>(E);
605+
}
606+
607+
/// Return true if this is 'nil' type checked as an Optional. This looks
608+
/// like this:
609+
/// (call_expr implicit type='Int?'
610+
/// (constructor_ref_call_expr implicit
611+
/// (declref_expr implicit decl=Optional.init(nilLiteral:)
612+
static bool isTypeCheckedOptionalNil(Expr *E) {
613+
auto CE = dyn_cast<CallExpr>(E->getSemanticsProvidingExpr());
614+
if (!CE || !CE->isImplicit() ||
615+
!CE->getType()->getAnyOptionalObjectType())
616+
return false;
617+
618+
auto CRCE = dyn_cast<ConstructorRefCallExpr>(CE->getSemanticFn());
619+
if (!CRCE || !CRCE->isImplicit()) return false;
620+
621+
auto DRE = dyn_cast<DeclRefExpr>(CRCE->getSemanticFn());
622+
623+
SmallString<32> NameBuffer;
624+
auto name = DRE->getDecl()->getFullName().getString(NameBuffer);
625+
return name == "init(nilLiteral:)";
626+
}
627+
628+
629+
/// Warn about surprising implicit optional promotions involving operands to
630+
/// calls. Specifically, we warn about these expressions when the 'x'
631+
/// operand is implicitly promoted to optional:
632+
///
633+
/// x ?? y
634+
/// x == nil // also !=
635+
///
636+
void checkOptionalPromotions(ApplyExpr *call) {
637+
auto DRE = dyn_cast<DeclRefExpr>(call->getSemanticFn());
638+
auto args = dyn_cast<TupleExpr>(call->getArg());
639+
if (!DRE || !DRE->getDecl()->isOperator() ||
640+
!args || args->getNumElements() != 2)
641+
return;
642+
643+
auto lhs = args->getElement(0);
644+
auto rhs = args->getElement(1);
645+
auto calleeName = DRE->getDecl()->getName().str();
646+
647+
if (calleeName == "??" && isImplicitPromotionToOptional(lhs)) {
648+
TC.diagnose(DRE->getLoc(), diag::use_of_qq_on_non_optional_value)
649+
.highlight(lhs->getSourceRange())
650+
.fixItRemove(SourceRange(DRE->getLoc(), rhs->getEndLoc()));
651+
return;
652+
}
653+
654+
if (calleeName == "==" || calleeName == "!=") {
655+
if ((isImplicitPromotionToOptional(lhs) &&
656+
isTypeCheckedOptionalNil(rhs)) ||
657+
(isTypeCheckedOptionalNil(lhs) &&
658+
isImplicitPromotionToOptional(rhs))) {
659+
TC.diagnose(DRE->getLoc(), diag::nonoptional_compare_to_nil,
660+
calleeName == "!=")
661+
.highlight(lhs->getSourceRange())
662+
.highlight(rhs->getSourceRange());
663+
return;
664+
}
665+
}
666+
}
597667
};
598668

599669
DiagnoseWalker Walker(TC, DC, isExprStmt);

test/Constraints/diagnostics.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ class r20201968C {
214214
// <rdar://problem/21459429> QoI: Poor compilation error calling assert
215215
func r21459429(_ a : Int) {
216216
assert(a != nil, "ASSERT COMPILATION ERROR")
217+
// expected-warning @-1 {{comparing non-optional value to nil always returns true}}
217218
}
218219

219220

@@ -781,10 +782,10 @@ class SR1594 {
781782
}
782783

783784
func nilComparison(i: Int, o: AnyObject) {
784-
_ = i == nil
785-
_ = nil == i
786-
_ = i != nil
787-
_ = nil != i
785+
_ = i == nil // expected-warning {{comparing non-optional value to nil always returns false}}
786+
_ = nil == i // expected-warning {{comparing non-optional value to nil always returns false}}
787+
_ = i != nil // expected-warning {{comparing non-optional value to nil always returns true}}
788+
_ = nil != i // expected-warning {{comparing non-optional value to nil always returns true}}
788789
_ = i < nil
789790
_ = nil < i
790791
_ = i <= nil

test/expr/cast/nil_value_to_optional.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ var f = false
55

66
func markUsed<T>(_ t: T) {}
77

8-
markUsed(t != nil)
9-
markUsed(f != nil)
8+
markUsed(t != nil) // expected-warning {{comparing non-optional value to nil always returns true}}
9+
markUsed(f != nil) // expected-warning {{comparing non-optional value to nil always returns true}}
1010

1111
class C : Equatable {}
1212

@@ -15,7 +15,7 @@ func == (lhs: C, rhs: C) -> Bool {
1515
}
1616

1717
func test(_ c: C) {
18-
if c == nil {}
18+
if c == nil {} // expected-warning {{comparing non-optional value to nil always returns false}}
1919
}
2020

2121
class D {}

test/expr/expressions.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -705,10 +705,10 @@ func invalidDictionaryLiteral() {
705705
//===----------------------------------------------------------------------===//
706706
// nil/metatype comparisons
707707
//===----------------------------------------------------------------------===//
708-
_ = Int.self == nil
709-
_ = nil == Int.self
710-
_ = Int.self != nil
711-
_ = nil != Int.self
708+
_ = Int.self == nil // expected-warning {{comparing non-optional value to nil always returns false}}
709+
_ = nil == Int.self // expected-warning {{comparing non-optional value to nil always returns false}}
710+
_ = Int.self != nil // expected-warning {{comparing non-optional value to nil always returns true}}
711+
_ = nil != Int.self // expected-warning {{comparing non-optional value to nil always returns true}}
712712

713713
// <rdar://problem/19032294> Disallow postfix ? when not chaining
714714
func testOptionalChaining(_ a : Int?, b : Int!, c : Int??) {
@@ -737,6 +737,12 @@ func testNilCoalescePrecedence(cond: Bool, a: Int?, r: CountableClosedRange<Int>
737737
let r2 = (r ?? 0)...42 // not ok: expected-error {{binary operator '??' cannot be applied to operands of type 'CountableClosedRange<Int>?' and 'Int'}}
738738
// expected-note @-1 {{overloads for '??' exist with these partially matching parameter lists:}}
739739
let r3 = r ?? 0...42 // parses as the first one, not the second.
740+
741+
742+
// <rdar://problem/27457457> [Type checker] Diagnose unsavory optional injections
743+
// Accidental optional injection for ??.
744+
let i = 42
745+
_ = i ?? 17 // expected-warning {{left side of nil coalescing operator '??' is non-optional, so the right side is never used}} {{9-15=}}
740746
}
741747

742748
// <rdar://problem/19772570> Parsing of as and ?? regressed

0 commit comments

Comments
 (0)