Skip to content

[TypeChecker] SR-7307 : Warn when a block contains defer as last statement #17967

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 8 commits into from
Jul 27, 2018

Conversation

vguerra
Copy link
Contributor

@vguerra vguerra commented Jul 15, 2018

This PR adds a diagnostic (NOTE WARNING ) when a defer statement is the last statement within a block of code. The diagnostic is implemented as follows:

  • During type-checking a BraceStmt simply check if last statement of it's elements is a DeferStmt. If that is the case, emit diagnostic.
  • A flag (IsBraceStmtFromTopLevelDecl) has been added to the StmtChecker in order to keep track of whether the BraceStmt being checked belongs to a TopLevelCodeDecl or not. This is needed to avoid diagnosing every defer statement found at the top level. (check discussion in : SR-7307)
  • As a consequence of prev. point, this solution is not able to diagnose a defer statement that appears last at the top level ( not sure if this is worth adding ).

Resolves SR-7307.

Copy link
Collaborator

@xwu xwu left a comment

Choose a reason for hiding this comment

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

Mostly some formatting nits. My two substantive pieces of feedback are:

The diagnostic should, I think, be more descriptive; a user who has the misunderstanding described in the linked bug won't be educated by seeing a diagnostic that says "pointless." Maybe something along the lines of saying that the body will not be deferred because it's already at the end of the block.

My second concern is how this interacts with initializers. As you know, setting a property inside an initializer doesn't invoke any didSet or willSet routines. However, whether a bug or no, I recall there's been some issue where a defer statement can be used inside an initializer to get around that. I don't recall if that's still the case. But, if so, a defer statement at the end of an initializer isn't actually pointless as removing it would change the observed behavior.

@@ -54,6 +54,9 @@ NOTE(previous_decldef,none,
NOTE(brace_stmt_suggest_do,none,
"did you mean to use a 'do' statement?", ())

NOTE(defer_stmt_at_blockend,none,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: "block_end" (two words).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in next commit

@@ -54,6 +54,9 @@ NOTE(previous_decldef,none,
NOTE(brace_stmt_suggest_do,none,
"did you mean to use a 'do' statement?", ())

NOTE(defer_stmt_at_blockend,none,
"defer statement at the end of block is pointless", ())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pointless because...? It's important to give a little more explanation here, I think. Also, stylistically, "defer" should be surrounded by single quotes, as shown above with "a 'do' statement".

Choose a reason for hiding this comment

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

I'd suggest something like:

'defer' at the end of its scope is redundant and will execute immediately

Probably best for a follow up PR, but it would be nice to have a fixit that simply removes the defer and braces.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems it'd be simpler just to suggest do in place of defer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks @xwu and @jszumski for the reviews. I improved the note message as you suggested in the following commit.

I thought as well about the fix-it from the beginning but did not have the time to see how to implement it, I am still getting familiar with the code base :). As @xwu, probably the easier/simpler thing would be to suggest a do. Will look into 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.

hi @xwu, @jszumski
I pushed a 3rd commit that simply adds a fix-it to the diagnostic. The fix-it just suggest to replace the defer with a do statement.

@@ -352,13 +355,13 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
};

StmtChecker(TypeChecker &TC, AbstractFunctionDecl *AFD)
: TC(TC), TheFunc(AFD), DC(AFD), IsREPL(false) { }
: TC(TC), TheFunc(AFD), DC(AFD), IsREPL(false), IsBraceStmtFromTopLevelDecl(false) { }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Formatting nit: please wrap to 80 characters per Clang style.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in next commit.

@@ -1333,6 +1336,19 @@ void TypeChecker::checkIgnoredExpr(Expr *E) {

Stmt *StmtChecker::visitBraceStmt(BraceStmt *BS) {
const SourceManager &SM = TC.Context.SourceMgr;

// Diagnose defer statement being last one in block ( Only if
// BraceStmt does not start a TopLevelDecl )
Copy link
Collaborator

Choose a reason for hiding this comment

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

Formatting nit: please end comments with a period here and elsewhere; also, no spaces or capitalization inside parentheses.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in next commit.


StmtChecker(TypeChecker &TC, DeclContext *DC)
: TC(TC), TheFunc(), DC(DC), IsREPL(false) {
: TC(TC), TheFunc(), DC(DC), IsREPL(false), IsBraceStmtFromTopLevelDecl(true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not necessarily true. You'll want to ask isa<TopLevelCodeDecl>(DC).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately I can not relay on isa<TopLevelCodeDecl>(DC) because in the following code snippet, it would return true when visiting the { that opens the ifs then block:

let x = 1
let y = 3

if (x > y) {
    defer {
        print("not so useful defer stmt.")
    }
}

and I would like to diagnose this block.

@vguerra vguerra changed the title [TypeChecker] SR-7307 : Warn when a block contains nothing but a defer statement [TypeChecker] SR-7307 : Warn when a block contains defer as last statement Jul 17, 2018
@vguerra
Copy link
Contributor Author

vguerra commented Jul 18, 2018

@xwu regarding using defer within initializers, I'll double check if that is still needed or not. Thank you for bringing that up.

@rudkx
Copy link
Contributor

rudkx commented Jul 20, 2018

I'm not aware of other cases where we emit a note in isolation as opposed to being attached to a warning or error.

Do you know if there is precedence for this?

Copy link
Contributor

@rudkx rudkx left a comment

Choose a reason for hiding this comment

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

Please add a test that the fix-it is applied correctly.

@CodaFi
Copy link
Contributor

CodaFi commented Jul 20, 2018

I'm not aware of other cases where we emit a note in isolation

Oh, good catch. No, that is never correct - and I thought the diagnostic engine would trap on that. This should be a warning.

@@ -54,6 +54,10 @@ NOTE(previous_decldef,none,
NOTE(brace_stmt_suggest_do,none,
"did you mean to use a 'do' statement?", ())

NOTE(defer_stmt_at_block_end,none,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be a warning. Notes are always attached to warnings or errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is fixed in the next commit :)

@vguerra
Copy link
Contributor Author

vguerra commented Jul 22, 2018

hi @rudkx! .. thank you for catching this. In my last commit I converted the NOTE into a WARNING and updated the test file to check for the fix-it!

@rudkx
Copy link
Contributor

rudkx commented Jul 23, 2018

@swift-ci Please smoke test

1 similar comment
@rudkx
Copy link
Contributor

rudkx commented Jul 24, 2018

@swift-ci Please smoke test

@vguerra
Copy link
Contributor Author

vguerra commented Jul 24, 2018

a test that has been added recently fails this time :( ( https://ci.swift.org/job/swift-PR-Linux-smoke-test/8474/testReport/junit/Swift(linux-x86_64)/stmt/yield_swift/ ). My bad, I did not rebase locally before running tests.

fix is coming :)

vguerra added 5 commits July 24, 2018 23:15
Updating test as well to check that the fix-it is applied correctly.
Usage of 'defer' now emits a warning in some places in the tests,
so adding the corresponding 'expected-warning'.
@@ -54,6 +54,10 @@ NOTE(previous_decldef,none,
NOTE(brace_stmt_suggest_do,none,
"did you mean to use a 'do' statement?", ())

WARNING(defer_stmt_at_block_end,none,
"'defer' at the end of its scope is redundant and will execute immediately; "
Copy link
Collaborator

@xwu xwu Jul 25, 2018

Choose a reason for hiding this comment

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

@jrose-apple wrote an excellent guide on Swift diagnostic messages. Some points to consider here include using a terse style (known as reduced written register), phrasing as a rule, and matching the tone of other diagnostics. So perhaps:

'defer' statement immediately before end of scope always executes immediately; replace with 'do' statement to silence this warning

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thank you for the pointer to the Diagnostics guide... useful stuff 👍 .. and yes the message you suggest definitely reads better. Will update it in next commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

warning message improved in ffd1366

@@ -54,6 +54,10 @@ NOTE(previous_decldef,none,
NOTE(brace_stmt_suggest_do,none,
"did you mean to use a 'do' statement?", ())

WARNING(defer_stmt_at_block_end,none,
"'defer' at the end of its scope is redundant and will execute immediately; "
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: The diagnostic definition is better placed within DiagnosticsSema.def as it's only emitted from Sema. DiagnosticsCommon is for common diagnostics that are emitted from separate parts of the compiler (e.g Parse and Sema).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, good catch. Yes, this belongs in DiagnosticsSema.def.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thank you for catching this ... fixed in ffd1366

- moving WARNING definition from DiagnosticsCommon.def to DiagnosticsSema.def
- improving WARNING message to better align with guidelines described in docs/Diagnostics.md as suggested by @xwu.
- adapting expected warning message in tests.
@@ -82,7 +82,7 @@ func call_yield() {
struct YieldInDefer {
var property: String {
_read {
defer {
defer { // expectd-warning {{'defer' statement before end of scope always executes immediately}}{{7-12=do}}
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: expectd

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😢 ... fixed on ab513c8


StmtChecker(TypeChecker &TC, DeclContext *DC)
: TC(TC), TheFunc(), DC(DC), IsREPL(false) {
: TC(TC), TheFunc(), DC(DC), IsREPL(false),
IsBraceStmtFromTopLevelDecl(true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I missed this earlier. Rather than setting this to true, can you instead check getContextKind() of the DeclContext that is passed in. That is less fragile, since as written this relies on the fact that the only place we currently use this constructor is when we're passing in a top level decl context.

Copy link
Contributor Author

@vguerra vguerra Jul 25, 2018

Choose a reason for hiding this comment

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

I think this is kind of similar to what Robert suggested above about using isa<TopLevelCodeDecl>(DC), to what I responded:

Unfortunately I can not relay on isa<TopLevelCodeDecl>(DC) because in the following code snippet, it would return true when visiting the { that opens the ifs then block:

let x = 1
let y = 3

if (x > y) {
    defer {
        print("not so useful defer stmt.")
    }
}

and I would like to diagnose this block.

when visiting the open brace of the if DC->getContextKind() returns TopLevelCodeDecl, so I can't rely on that 😞

Copy link
Contributor

Choose a reason for hiding this comment

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

For that you should have an outer BraceStmt, with an IfStmt nested within it, and a BraceStmt nested under it. So I would expect that when you hit the outermost BraceStmt you would set IsBraceStmtFromTopLevelDecl to false like you are now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry.. I completely missed your point. I see what you mean now, I should base the initialization of the IsBraceStmtFromTopLevelDecl on DC->getContextKind(). Fixed it on da3149d

If DeclContext's kind is TopLevelCodeDecl we are sure to be visiting
a BraceStmt that opens a TopLevelDecl.
@@ -0,0 +1,23 @@
// RUN: %target-swift-frontend -typecheck -verify %s
Copy link
Contributor

Choose a reason for hiding this comment

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

You can take care of this in a future commit, but this could actually be // RUN: %target-typecheck-verify-swift without explicit options or %s.

Copy link
Contributor

@rudkx rudkx left a comment

Choose a reason for hiding this comment

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

LGTM.

@rudkx
Copy link
Contributor

rudkx commented Jul 26, 2018

@swift-ci Please smoke test

@rudkx rudkx merged commit b275edd into swiftlang:master Jul 27, 2018
@rudkx
Copy link
Contributor

rudkx commented Jul 27, 2018

@vguerra I opened #18277 to fix up a bunch of run lines that could be using %target-typecheck-verify-swift.

@vguerra
Copy link
Contributor Author

vguerra commented Jul 27, 2018

thanks @rudkx !

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.

6 participants