-
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 1 commit
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 |
---|---|---|
|
@@ -7785,6 +7785,21 @@ bool ConstraintSystem::applySolutionFix(Expr *expr, | |
} | ||
|
||
switch (fix.first.getKind()) { | ||
case FixKind::OptionalChaining: { | ||
if (!solution.DisjunctionChoices.count(locator) || | ||
!solution.getDisjunctionChoice(fix.second)) { | ||
auto type = solution.simplifyType(getType(affected)) | ||
->getRValueObjectType(); | ||
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. Nit: Formatting changed when you moved this case |
||
auto diag = TC.diagnose(affected->getLoc(), | ||
diag::missing_unwrap_optional, type); | ||
diag.fixItInsertAfter(affected->getEndLoc(), "?"); | ||
return true; | ||
} | ||
// Making the result optional failed, but non-optional succeeded, so | ||
// we can't optional chain after all: fallthrough into forcing. | ||
LLVM_FALLTHROUGH; | ||
} | ||
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. Is there an actual change here, or were these just reordered? If the latter, is there a reason to do so? |
||
|
||
case FixKind::ForceOptional: { | ||
const Expr *unwrapped = affected->getValueProvidingExpr(); | ||
auto type = solution.simplifyType(getType(affected)) | ||
|
@@ -7808,15 +7823,6 @@ bool ConstraintSystem::applySolutionFix(Expr *expr, | |
} | ||
return true; | ||
} | ||
|
||
case FixKind::OptionalChaining: { | ||
auto type = solution.simplifyType(getType(affected)) | ||
->getRValueObjectType(); | ||
auto diag = TC.diagnose(affected->getLoc(), | ||
diag::missing_unwrap_optional, type); | ||
diag.fixItInsertAfter(affected->getEndLoc(), "?"); | ||
return true; | ||
} | ||
|
||
case FixKind::ForceDowncast: { | ||
if (auto *paren = dyn_cast<ParenExpr>(affected)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3440,7 +3440,6 @@ ConstraintSystem::simplifyMemberConstraint(ConstraintKind kind, | |
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. | ||
|
||
|
@@ -3449,24 +3448,95 @@ ConstraintSystem::simplifyMemberConstraint(ConstraintKind kind, | |
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 |
||
auto unwrappedBaseObjTy = baseObjTy->getOptionalObjectType(); | ||
if (unwrappedBaseObjTy) { | ||
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. Can you split all of this out into a separate function? It's gone from ~20 lines to ~100 lines. Also, please run |
||
// 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; | ||
|
||
|
||
// Figure out if we have an existing optional chaining fix done earlier in | ||
// this dotted expression chain. | ||
bool continuingChain = false; | ||
if (auto dotExpr = dyn_cast<UnresolvedDotExpr>(locator->getAnchor())) { | ||
for (auto pair : Fixes) { | ||
if (pair.first.getKind() == FixKind::OptionalChaining) { | ||
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. CSSimplify is too early to be examining the Fixes generated by a particular constraint system. This should be done in CSApply. 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 is to avoid creating disjunctions at every step in the chain, which seems worthwhile to avoid 2^n attempted solves. It's true that if we wanted to let that happen we could add a fix at each dot expr and then check fixes in CSApply to make all but the first one just produce no fixit. 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. My thinking is that potentially doing n^2~ish work on every simplification isn't that great either. If the constraint system doesn't look right, perhaps we should look into changing its morphology or emitting a "compound fix" like you mention here. Besides, I suspect 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. Now that I've slept, let me try to talk about this better: Given an example like If this was done in a symmetrical, consistent way instead of the admittedly hacky look-to-see-if-a-chain-fix-already-exists way, then it'd have two major impacts. First, we'd need to allow fixes during evaluation of disjunction candidates instead of just at the top level which is an algorithmic change I don't know enough to judge. This may or may not be a good idea in general? Second, it always type checks in this sort of expression to force the optional at any of the steps and then rewrap at the end, and with multiple independent chaining fixes, a solution of a single force fix at the beginning will get the best score. So taking a symmetrical approach would require additional state on the Disjunction or perhaps on each Constraint inside that would be examined by the scorer to make it so that a forcing fix was scored as some multiple N-times worse than a step of chaining fix. If this ability to make lopsided disjunction scoring seems like it'd be useful elsewhere, maybe that's the way to go? |
||
auto innerExpr = dotExpr->getBase(); | ||
while (innerExpr) { | ||
if (pair.second->getAnchor() == innerExpr) { | ||
continuingChain = true; | ||
break; | ||
} | ||
if (auto innerDot = dyn_cast<UnresolvedDotExpr>(innerExpr)) | ||
innerExpr = innerDot->getBase(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// If we aren't continuing an existing chain, do we want to add a new fixit? | ||
if (!continuingChain) { | ||
if (!shouldAttemptFixes()) | ||
return SolutionKind::Error; | ||
|
||
auto contextTy = getContextualType(); | ||
bool forceUnwrap; | ||
if (contextTy) | ||
forceUnwrap = contextTy->getOptionalObjectType().isNull(); | ||
else | ||
forceUnwrap = Options.contains(ConstraintSystemFlags::PreferForceUnwrapToOptional); | ||
|
||
// Note the fix. | ||
auto fixKind = forceUnwrap ? FixKind::ForceOptional : FixKind::OptionalChaining; | ||
if (recordFix(fixKind, locator)) | ||
return SolutionKind::Error; | ||
|
||
if (forceUnwrap) { | ||
// The simple case - just look through one level of optional. | ||
addValueMemberConstraint(unwrappedBaseObjTy, | ||
member, memberTy, useDC, functionRefKind, locator); | ||
return SolutionKind::Solved; | ||
} | ||
} | ||
|
||
// See if there is a matching member with an optional type. | ||
// If so, then inserting an optional chaining fix won't produce a MORE optional result, | ||
// and it is safe to just fix with chaining. | ||
MemberLookupResult result = | ||
performMemberLookup(kind, member, unwrappedBaseObjTy, functionRefKind, locator, | ||
/*includeInaccessibleMembers*/false); | ||
bool memberIsOptional = false; | ||
for (auto candidate : result.ViableCandidates) { | ||
if (candidate.isDecl()) | ||
memberIsOptional |= !candidate.getDecl()->getInterfaceType()-> | ||
getOptionalObjectType().isNull(); | ||
} | ||
|
||
// If the member isn't already optional, we need to make the result optional. | ||
if (!memberIsOptional) { | ||
auto innerTV = createTypeVariable(locator, TVO_CanBindToLValue | TVO_CanBindToInOut); | ||
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. Can this really bind to 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 is cribbed from how the type variables for non-implicit optional chaining are created in CSGen. I can look at whether it's necessary or not. 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. “Can bind to inout” used to be the default. When I made an explicit flag for it, I went and added it in every place we were creating a type variable. Most of them can lose the flag - that was the original motivation for adding it - but nobody got around to cleaning that up yet. Maybe @CodaFi can do it :) |
||
Type optTy = getTypeChecker().getOptionalType(SourceLoc(), innerTV); | ||
if (!optTy) | ||
return SolutionKind::Error; | ||
|
||
if (continuingChain) { | ||
addConstraint(ConstraintKind::Conversion, optTy, memberTy, locator); | ||
} else { | ||
// If we are starting a new chain, it may or may not be valid. Add a disjunction | ||
// with the optional and non-optional result. Later when we generate the fixit | ||
// we'll check the disjunction and fall back on force unwrapping if the | ||
// non-optional result is the solution. | ||
SmallVector<Constraint *, 4> optionalities; | ||
auto optionalResult = Constraint::create(*this, ConstraintKind::Conversion, | ||
optTy, memberTy, locator); | ||
auto nonoptionalResult = Constraint::create(*this, ConstraintKind::Conversion, | ||
innerTV, memberTy, locator); | ||
optionalities.push_back(optionalResult); | ||
optionalities.push_back(nonoptionalResult); | ||
addDisjunctionConstraint(optionalities, locator, RememberChoice); | ||
} | ||
memberTy = innerTV; | ||
} | ||
|
||
// Look through one level of optional. | ||
addValueMemberConstraint(baseObjTy->getOptionalObjectType(), | ||
addValueMemberConstraint(unwrappedBaseObjTy, | ||
member, memberTy, useDC, functionRefKind, locator); | ||
return SolutionKind::Solved; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,3 +144,35 @@ 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=?}} | ||
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=?}} | ||
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=?}} | ||
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=?}} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,11 @@ import Foundation | |
|
||
func iuo_error(prop: IUOProperty) { | ||
let _: Coat? = prop.iuo.optional() | ||
// expected-error@-1 {{value of optional type '(() -> Coat?)?' not unwrapped; did you mean to use '!' or '?'?}} | ||
// expected-error@-1 {{value of optional type 'OptionalMethods?' not unwrapped; did you mean to use '!' or '?'?}} | ||
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. Is the promotion of the chain from 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. Sorry is this comment pointed at the wrong place? Not sure what's intended. |
||
// expected-error@-2 {{value of optional type '(() -> Coat?)?' not unwrapped; did you mean to use '!' or '?'?}} | ||
let _: Coat? = prop.iuo.optional()! | ||
// expected-error@-1 {{cannot invoke 'optional' with no arguments}} | ||
// expected-error@-1 {{value of optional type 'OptionalMethods?' not unwrapped; did you mean to use '!' or '?'?}} | ||
// expected-error@-2 {{value of optional type '(() -> Coat?)?' not unwrapped; did you mean to use '!' or '?'?}} | ||
let _: Coat? = prop.iuo.optional!() | ||
let _: Coat? = prop.iuo.optional!()! | ||
let _: Coat? = prop.iuo!.optional() | ||
|
Uh oh!
There was an error while loading. Please reload this page.
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.
Examining disjuncts is a brittle way to propagate this through the constraint system. I wonder if you couldn't implement this with another
FixKind
.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.
It's the way that implicit unwrapping or not with IUOs gets propagated. I know IUOs isn't exactly the best part of the implementation to emulate, but it is existing practice... If you have another suggestion I'd be glad to try it out. Another
FixKind
wouldn't work by itself because there is no way to know at the time the fix is added which of the two possibilities will type check.Uh oh!
There was an error while loading. Please reload this page.
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.
It is existing practice in the context of the implementation of IUOs because there we put up a front to maintain the illusion that IUOs aren't a modality of
T?
. This uses disjunctions as pieces of state and I suspect we have better ways of handling that.