Skip to content

[Async Refactoring] Support Obj-C style bool flag checks #37629

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

Merged
merged 5 commits into from
May 28, 2021

Conversation

hamishknight
Copy link
Contributor

Add an additional case to CallbackCondition to support boolean checks, and add the necessary classification logic to determine whether a bool flag check is for a success or failure path.

The logic currently first checks to see if the async alternative has a convention that specifies which parameter the flag is, and whether it's a success or failure flag. If so, it classifies the flag check accordingly. If there's no async convention specified, we use a heuristic to see if the error parameter is force unwrapped in the failure block. If so, we treat that as the error path. If there's no force unwrap of the error, we leave the condition unhandled.

rdar://74063899

Copy link
Contributor

@bnbarham bnbarham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the cleanup as usual 🙇. No major comments

// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 -I %S/Inputs -sdk %clang-importer-sdk-path | %FileCheck -check-prefix=BOOL-DONT-HANDLE2 %s
boolWithErr { success, err in
if !success {
func doThings() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above - is there a good reason not to handle this case? Ie. if the force unwrap is used at all in the scope (nested or not), treat it as the error condition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that would count for things like this:

if !success {
  func doThings() {
    print(err!)
  }
  if err != nil {
    doThings()
  }
}

Where the !success condition doesn't appear to be relevant to the force unwrap. We could probably perform a more in-depth heuristic that also looks for other references to err, but I'm not sure whether or not that's worth it. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I wouldn't expect that case to be super common though. Another would be returning multiples bools and checking each both (nested). Though again, I assume that's uncommon. Erring on the side of leaving it and adding placeholders is probably okay for now though 👍


// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 -I %S/Inputs -sdk %clang-importer-sdk-path | %FileCheck -check-prefix=BOOL-WITH-ERR-SILLY %s
boolWithErr { success, err in
if success == false && err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree kind of silly, but this sort of implies that success is actually error. We could handle it as such... Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the problem is that we're only classifying it as a success flag to begin with because of the force unwrap of the error in the body, so even if we classify both checks as success paths, the code still doesn't make much sense. We could apply a more general heuristic that allows for bool condition checks if they're also with other conditions that are able to classify as success or failure, but I don't really have a good sense of how often that comes up.

On a related note, I've just noticed that if the force unwrap of the error in this example was in an else block here, we would still classify the condition as the same, which doesn't seem right. It seems we should adjust the classification logic so we make the determination of whether the flag is a success check or failure check based on the branch in which the force unwrap of the error occurs rather than the other way around, which would let us classify a flag as an error check if we then force unwrap the error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a couple of problems here really. Yes, ideally if the force unwrap was in the else we would then consider success to be a flag. Could get a bit tricky when the else can itself be another if though.

Another problem is that if we don't classify the success as being a flag then there are unhandled conditions and so we don't remove the if at all. Ideally we'd classify the whole if as being in the success block and then remove the && err == nil. But that's an existing issue so... 🤷

@hamishknight
Copy link
Contributor Author

hamishknight commented May 26, 2021

Updated to handle the unwrap case, and changed the flag classification logic to consider the branch the force unwrap happens in, rather than assuming the flag always denotes success (and with that change, it didn't seem like we really needed the check to discard the flag if it disagrees with the rest of the conditions). Also noticed we were missing a couple of getSemanticsProvidingExpr calls, added those in.

@swiftlang swiftlang deleted a comment from swift-ci May 26, 2021
@swiftlang swiftlang deleted a comment from swift-ci May 27, 2021
@swiftlang swiftlang deleted a comment from swift-ci May 27, 2021
Copy link
Contributor

@bnbarham bnbarham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One final comment, but we can do that in another PR if we want it.

Comment on lines +354 to +356
// It's a little unfortunate that print("a \(<#err#>!)") gets classified in the success
// block below, but it doesn't seem like a case that's likely to come up, as optBool
// would then be inaccessible in the error block.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For both a and f we could just assume that if there's a force unwrap, that == the error branch (only where the conditions aren't explicitly success/error and involve known params). I'm happy with this as is though, just thought I'd mention it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's possible we'll want to generalise the heuristic to better handle cases like these, though I'm not really sure how often they'll come up in practice as it seems a little odd to e.g check a success param for non-nil and then assume you're on an error path without looking at the success param again. I think for now at least I'd rather tend on the side on keeping the heuristic a little more limited and seeing what kind of feedback we get from users.

This will make it a little nicer to switch on
ConditionType.
- Expand ConditionType to include `.success` and
`.failure` patterns.
- Introduce ConditionPath to represent whether a
classified condition represents a success or
failure path.
- Add methods to CallbackClassifier that deal with
deciding whether a given condition is a success or
failure path.
- Use these methods to simplify `classifyConditional`.
Add an additional case to `CallbackCondition` to
support boolean checks, and add the necessary
classification logic to determine whether a bool
flag check is for a success or failure path.

The logic currently first checks to see if the
async alternative has a convention that specifies
which parameter the flag is, and whether it's a
success or failure flag. If so, it classifies the
flag check accordingly. If there's no async
convention specified, we use a heuristic to see if
the error parameter is force unwrapped in the
failure block. If so, we treat that as the error
path. If there's no force unwrap of the error, we
leave the condition unhandled.

rdar://74063899
@hamishknight
Copy link
Contributor Author

@swift-ci please smoke test

@hamishknight hamishknight merged commit c8a28f4 into swiftlang:main May 28, 2021
@hamishknight hamishknight deleted the ghost-of-objc-past branch May 28, 2021 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants