-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Sema] More correct optional chaining vs. force unwrap fixits #15321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
d5ed794
e4da93d
4160528
df9bd22
606f009
e2802e8
f0cee60
029526a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3455,6 +3455,73 @@ performMemberLookup(ConstraintKind constraintKind, DeclName memberName, | |
return result; | ||
} | ||
|
||
static bool fixOptionalBaseMemberConstraint(ConstraintSystem &cs, | ||
Type unwrappedBaseObjTy, | ||
DeclName member, Type memberTy, | ||
DeclContext *useDC, | ||
FunctionRefKind functionRefKind, | ||
ArrayRef<OverloadChoice> outerAlternatives, | ||
ConstraintLocator *locator) { | ||
// Don't try to fixup multiple levels of optionality, if we unwrapped and | ||
// the result is still optional, fail. | ||
if (unwrappedBaseObjTy->getOptionalObjectType()) | ||
return false; | ||
|
||
if (!(cs.Options & ConstraintSystemFlags::AllowFixes)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was intentional, but maybe there's a better way to fix it? Very open to suggestions. The trouble is, that in order to get all the fixits at once in a multi-step dotted path that should be optional-chained, we have to be able to continue this code even while inside of a disjunction choice (the disjunction created for the first step in the dotted path). But trying a disjunction choice creates a new Stack frames:
How to get the information that recording fixes should be ok from So this code is hedging by ignoring There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow. |
||
return false; | ||
|
||
auto contextTy = cs.getContextualType(); | ||
bool forceUnwrap; | ||
if (contextTy) | ||
forceUnwrap = contextTy->getOptionalObjectType().isNull(); | ||
else | ||
forceUnwrap = | ||
cs.Options.contains(ConstraintSystemFlags::PreferForceUnwrapToOptional); | ||
|
||
if (forceUnwrap || functionRefKind != FunctionRefKind::Unapplied) { | ||
// Note the fix. | ||
auto fixKind = | ||
forceUnwrap ? FixKind::ForceOptional : FixKind::OptionalChaining; | ||
if (cs.recordFix(fixKind, locator)) | ||
return false; | ||
|
||
// The simple case - just look through one level of optional. | ||
cs.addValueMemberConstraint(unwrappedBaseObjTy, member, memberTy, useDC, | ||
functionRefKind, outerAlternatives, locator); | ||
return true; | ||
} | ||
|
||
auto innerTV = | ||
cs.createTypeVariable(locator, TVO_CanBindToLValue | TVO_CanBindToInOut); | ||
Type optTy = cs.getTypeChecker().getOptionalType(SourceLoc(), innerTV); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should pass a location, e.g. the one from |
||
if (!optTy) | ||
return false; | ||
|
||
// Add a disjunction with the three possibilities: | ||
// 1) member is non-optional, result of expression adds optional to it ("?") | ||
// 2) member is non-optional, result of expression is non-optional ("!") | ||
// 3) member is optional, result of expression matches it ("?") | ||
SmallVector<Constraint *, 3> optionalities; | ||
auto optionalResult = Constraint::createFixed(cs, ConstraintKind::Conversion, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear to me why you are using Wouldn't the expectation be that the types are equal? |
||
FixKind::OptionalChaining, | ||
optTy, memberTy, locator); | ||
auto nonoptionalResult = | ||
Constraint::createFixed(cs, ConstraintKind::Conversion, | ||
FixKind::ForceOptional, optTy, memberTy, locator); | ||
auto optionalMember = Constraint::createFixed(cs, ConstraintKind::Conversion, | ||
FixKind::OptionalMember, | ||
innerTV, memberTy, locator); | ||
optionalMember->setFavored(); | ||
optionalities.push_back(optionalResult); | ||
optionalities.push_back(nonoptionalResult); | ||
optionalities.push_back(optionalMember); | ||
cs.addDisjunctionConstraint(optionalities, locator); | ||
|
||
cs.addValueMemberConstraint(unwrappedBaseObjTy, member, innerTV, useDC, | ||
functionRefKind, outerAlternatives, locator); | ||
return true; | ||
} | ||
|
||
ConstraintSystem::SolutionKind ConstraintSystem::simplifyMemberConstraint( | ||
ConstraintKind kind, Type baseTy, DeclName member, Type memberTy, | ||
DeclContext *useDC, FunctionRefKind functionRefKind, | ||
|
@@ -3504,38 +3571,23 @@ ConstraintSystem::SolutionKind ConstraintSystem::simplifyMemberConstraint( | |
if (!result.UnviableCandidates.empty()) | ||
return SolutionKind::Error; | ||
|
||
|
||
// If the lookup found no hits at all (either viable or unviable), diagnose it | ||
// as such and try to recover in various ways. | ||
|
||
auto instanceTy = baseObjTy; | ||
if (auto MTT = instanceTy->getAs<MetatypeType>()) | ||
instanceTy = MTT->getInstanceType(); | ||
|
||
// Value member lookup has some hacks too. | ||
if (shouldAttemptFixes() && baseObjTy->getOptionalObjectType()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd really appreciate if the whole original block of code was removed from the I'd be great if you could overload |
||
// If the base type was an optional, look through it. | ||
|
||
// Determine whether or not we want to provide an optional chaining fixit or | ||
// a force unwrap fixit. | ||
bool optionalChain; | ||
if (!getContextualType()) | ||
optionalChain = !(Options & ConstraintSystemFlags::PreferForceUnwrapToOptional); | ||
else | ||
optionalChain = !getContextualType()->getOptionalObjectType().isNull(); | ||
auto fixKind = optionalChain ? FixKind::OptionalChaining : FixKind::ForceOptional; | ||
|
||
// Note the fix. | ||
if (recordFix(fixKind, locator)) | ||
return SolutionKind::Error; | ||
|
||
// Look through one level of optional. | ||
addValueMemberConstraint(baseObjTy->getOptionalObjectType(), | ||
member, memberTy, useDC, functionRefKind, | ||
outerAlternatives, locator); | ||
// If the base type was an optional, look through it and try to generate | ||
// fixes to make it valid. | ||
auto unwrappedBaseObjTy = baseObjTy->getOptionalObjectType(); | ||
if (unwrappedBaseObjTy && fixOptionalBaseMemberConstraint( | ||
*this, unwrappedBaseObjTy, member, memberTy, | ||
useDC, functionRefKind, outerAlternatives, | ||
locator)) | ||
return SolutionKind::Solved; | ||
} | ||
return SolutionKind::Error; | ||
else | ||
return SolutionKind::Error; | ||
} | ||
|
||
ConstraintSystem::SolutionKind | ||
|
@@ -4814,7 +4866,7 @@ bool ConstraintSystem::recordFix(Fix fix, ConstraintLocatorBuilder locator) { | |
|
||
// Increase the score. If this would make the current solution worse than | ||
// the best solution we've seen already, stop now. | ||
increaseScore(SK_Fix); | ||
increaseScore(SK_Fix, fix.scoreWeight()); | ||
if (worseThanBestSolution()) | ||
return true; | ||
|
||
|
@@ -4842,8 +4894,7 @@ ConstraintSystem::simplifyFixConstraint(Fix fix, Type type1, Type type2, | |
TypeMatchOptions subflags = | ||
getDefaultDecompositionOptions(flags) | TMF_ApplyingFix; | ||
switch (fix.getKind()) { | ||
case FixKind::ForceOptional: | ||
case FixKind::OptionalChaining: { | ||
case FixKind::ForceOptional: { | ||
// Assume that '!' was applied to the first type. | ||
auto result = | ||
matchTypes(type1->getRValueObjectType()->getOptionalObjectType(), type2, | ||
|
@@ -4854,6 +4905,38 @@ ConstraintSystem::simplifyFixConstraint(Fix fix, Type type1, Type type2, | |
|
||
return result; | ||
} | ||
|
||
case FixKind::OptionalChaining: { | ||
// Types have already been munged, we just want to apply the fix or not | ||
// based on them being solved. | ||
auto result = | ||
matchTypes(type1, type2, matchKind, subflags, locator); | ||
if (result == SolutionKind::Solved) | ||
if (recordFix(fix, locator)) | ||
return SolutionKind::Error; | ||
|
||
return result; | ||
} | ||
|
||
case FixKind::OptionalMember: { | ||
// The second type must result in an optional, and then can be matched | ||
// to the first. | ||
auto fixedType = | ||
getFixedTypeRecursive(type2, flags, /* wantRValue= */ false, | ||
/* retainParens= */ false); | ||
if (fixedType->hasTypeVariable()) | ||
return SolutionKind::Unsolved; | ||
if (!fixedType->getOptionalObjectType()) | ||
return SolutionKind::Error; | ||
|
||
auto result = matchTypes(type1, type2, matchKind, subflags, locator); | ||
if (result == SolutionKind::Solved) | ||
if (recordFix(fix, locator)) | ||
return SolutionKind::Error; | ||
|
||
return result; | ||
} | ||
|
||
case FixKind::ForceDowncast: | ||
// These work whenever they are suggested. | ||
if (recordFix(fix, locator)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,3 +144,86 @@ struct S1116 { | |
|
||
let a1116: [S1116] = [] | ||
var s1116 = Set(1...10).subtracting(a1116.map({ $0.s })) // expected-error {{cannot convert value of type '[Int?]' to expected argument type 'Set<Int>'}} | ||
|
||
func moreComplexUnwrapFixes() { | ||
struct S { let value: Int } | ||
struct T { | ||
let s: S | ||
let optS: S? | ||
} | ||
struct U { | ||
let t: T | ||
} | ||
func takeNon(_ x: Int) -> Void {} | ||
func takeOpt(_ x: Int?) -> Void {} | ||
|
||
let s = S(value: 0) | ||
let t: T? = T(s: s, optS: nil) | ||
let u: U? = U(t: t!) | ||
let os: S? = s | ||
|
||
takeOpt(os.value) // expected-error{{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}} {{13-13=?}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. None of these tests are going to do nasty things like introduce disjuncts or throw an unbound-type-variable-wrench into your logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add some! |
||
takeNon(os.value) // expected-error{{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}} {{13-13=!}} | ||
|
||
takeOpt(t.s.value) // expected-error{{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=?}} | ||
// expected-error@-1 {{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}} {{14-14=?}} | ||
takeNon(t.s.value) // expected-error{{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=!}} | ||
takeOpt(t.optS.value) // expected-error{{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=?}} | ||
// expected-error@-1 {{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}} {{17-17=?}} | ||
takeNon(t.optS.value) // expected-error{{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=?}} | ||
// expected-error@-1 {{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}}{{17-17=!}} | ||
|
||
takeOpt(u.t.s.value) // expected-error{{value of optional type 'U?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=?}} | ||
// expected-error@-1 {{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}}{{14-14=?}} | ||
// expected-error@-2 {{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}}{{16-16=?}} | ||
|
||
takeNon(u.t.s.value) // expected-error{{value of optional type 'U?' not unwrapped; did you mean to use '!' or '?'?}} {{12-12=!}} | ||
|
||
t.s.value = 2 // expected-error{{value of optional type 'T?' not unwrapped; did you mean to use '!' or '?'?}} {{4-4=?}} | ||
// expected-error@-1 {{value of optional type 'S?' not unwrapped; did you mean to use '!' or '?'?}}{{6-6=?}} | ||
|
||
struct A { | ||
var n: Int = 0 | ||
var ns: [Int] = [] | ||
} | ||
struct B { | ||
var a: A? = nil | ||
var r: [A] = [] | ||
var n: Int? = nil | ||
} | ||
struct C { | ||
var b: B | ||
func thing() -> A? { return nil } // expected-note{{found this candidate}} | ||
func thing() -> B? { return nil } // expected-note{{found this candidate}} | ||
} | ||
|
||
func moreTest(c: C?) { | ||
takeOpt(c.thing().n) // expected-error{{ambiguous reference to member 'thing()'}} | ||
// Not ambigious because we can prefer A.n over B.n | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: ambiguous |
||
takeNon(c.thing().n) // expected-error{{value of optional type 'C?' not unwrapped; did you mean to use '!' or '?'?}}{{14-14=?}} | ||
// expected-error@-1 {{value of optional type 'A?' not unwrapped; did you mean to use '!' or '?'?}}{{22-22=!}} | ||
|
||
takeOpt(c.b.r[0].n) // expected-error{{value of optional type 'C?' not unwrapped; did you mean to use '!' or '?'?}}{{14-14=!}} | ||
takeNon(c.b.r[0].n) // expected-error{{value of optional type 'C?' not unwrapped; did you mean to use '!' or '?'?}}{{14-14=!}} | ||
takeOpt(c?.b.a.ns.first) // expected-error{{value of optional type 'A?' not unwrapped; did you mean to use '!' or '?'?}}{{19-19=?}} | ||
// expected-error@-1 {{value of optional type '[Int]?' not unwrapped; did you mean to use '!' or '?'?}}{{22-22=?}} | ||
takeNon(c!.b.a.ns.first) // expected-error{{value of optional type 'A?' not unwrapped; did you mean to use '!' or '?'?}}{{19-19=?}} | ||
// expected-error@-1 {{value of optional type '[Int]?' not unwrapped; did you mean to use '!' or '?'?}}{{22-22=?}} | ||
// expected-error@-2 {{value of optional type 'Int?' not unwrapped; did you mean to use '!' or '?'?}}{{28-28=!}} | ||
} | ||
} | ||
|
||
func moreComplexUnwrapFixes2() { | ||
class C { | ||
var i: Int = 0 | ||
} | ||
|
||
class D { | ||
var c: C? = nil | ||
} | ||
|
||
func test(d: D?) -> Int? { | ||
return d.c.i // expected-error{{value of optional type 'D?' not unwrapped; did you mean to use '!' or '?'?}}{{13-13=?}} | ||
// expected-error@-1 {{value of optional type 'C?' not unwrapped; did you mean to use '!' or '?'?}}{{15-15=?}} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need the LLVM_FALLTHROUGH here.