Skip to content
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

Go: Add query to check for deferred calls to functions which may return errors #11410

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

mbg
Copy link
Member

@mbg mbg commented Nov 24, 2022

A simple query which checks that there are no deferred calls to functions which may return errors, since it prevents errors from being handled.

@mbg mbg added the Go label Nov 24, 2022
@mbg mbg requested a review from a team as a code owner Nov 24, 2022
@mbg mbg self-assigned this Nov 24, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Nov 24, 2022

QHelp previews:

go/ql/src/InconsistentCode/DeferredErrorCall.qhelp

Deferred call may return an error

When calling a function which may return an error, we should check whether an error has occurred and handle it in some meaningful way. Deferring a function call does not allow us to perform such a check. Therefore, calls to functions which may return errors should not be deferred.

Recommendation

Examine the deferred function call carefully to check whether any errors that may arise should be handled explicitly.

Example

In the following example, a call to io.WriteLine is deferred with the intention of appendending some data to a file after some other data has been written to it. However, the call may not succeed and return and error, as demonstrated in the other use of io.WriteLine.

package main

import (
	"io"
	"log"
	"os"
)

func example() {
	defer io.WriteString(os.Stdout, "All done!")

	if _, err := io.WriteString(os.Stdout, "Hello"); err != nil {
		log.Fatal(err)
	}
}

To correct this issue, do not defer the function call that may return an error and handle the error explicitly instead:

package main

import (
	"io"
	"log"
	"os"
)

func example() {
	if _, err := io.WriteString(os.Stdout, "Hello"); err != nil {
		log.Fatal(err)
	}

	if _, err := io.WriteString(os.Stdout, "All done!"); err != nil {
		log.Fatal(err)
	}
}

References

  • The Go Programming Language Specification: Defer statements.
  • The Go Programming Language Specification: Errors.

owen-mc
owen-mc previously approved these changes Nov 24, 2022
from DeferStmt defer, SignatureType sig
where
// match all deferred function calls and obtain their type signatures
sig = defer.getCall().getCalleeExpr().getType().(SignatureType) and
Copy link
Contributor

@owen-mc owen-mc Nov 24, 2022

Choose a reason for hiding this comment

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

Suggested change
sig = defer.getCall().getCalleeExpr().getType().(SignatureType) and
sig = defer.getCall().getCalleeExpr().getType() and

(I see ql-for-ql has also picked this up.)

Copy link
Member Author

@mbg mbg Nov 24, 2022

Choose a reason for hiding this comment

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

Fixed in c907a9e

// check that one of the results is an error
sig.getResultType(_).implements(Builtin::error().getType().getUnderlyingType())
select defer,
"Deferred a call to " + defer.getCall().getCalleeName() + ", which may return an error."
Copy link
Contributor

@owen-mc owen-mc Nov 24, 2022

Choose a reason for hiding this comment

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

getCalleeName isn't always defined, so we would lose any alerts for those cases, though I can't remember exactly what they are at the moment. Is it worth trying to provide a different string when getCalleeName doesn't exist, to cover those cases? People defer anonymous functions quite a bit, though it would be weird to write an anonymous function with return values and then defer it.

Copy link
Member Author

@mbg mbg Nov 24, 2022

Choose a reason for hiding this comment

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

🤷🏻‍♂️ I have improved this in 5620d24 in any case.


func deferredCalls() {
defer returnsPair()
defer checkSomething()
Copy link
Contributor

@owen-mc owen-mc Nov 24, 2022

Choose a reason for hiding this comment

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

If you want to be thorough, you can cover some more cases: (1) assign a function to a variable and use the variable in the defer statement; (2) defer a function literal.

Copy link
Member Author

@mbg mbg Nov 24, 2022

Choose a reason for hiding this comment

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

Added in 5620d24

owen-mc
owen-mc previously approved these changes Nov 24, 2022
Copy link
Contributor

@owen-mc owen-mc left a comment

Looks great.

@owen-mc
Copy link
Contributor

owen-mc commented Nov 24, 2022

Oh, I just realised that it needs a change note. Luckily there's an automatic check for that. Oh, and it needs docs review. I think you have to add the ready-for-doc-review label and they will come and do it.

@mbg mbg changed the title Add query to check for deferred calls to functions which may return errors Go: Add query to check for deferred calls to functions which may return errors Nov 25, 2022
@mbg mbg added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Nov 25, 2022
Copy link
Contributor

@hubwriter hubwriter left a comment

Docs review: one typo, otherwise LGTM 👍

<example>
<p>
In the following example, a call to <code>io.WriteLine</code> is deferred with the intention of
appendending some data to a file after some other data has been written to it. However, the call
Copy link
Contributor

@hubwriter hubwriter Nov 25, 2022

Choose a reason for hiding this comment

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

typo

Suggested change
appendending some data to a file after some other data has been written to it. However, the call
appending some data to a file after some other data has been written to it. However, the call

if _, err := io.WriteString(os.Stdout, "Hello"); err != nil {
log.Fatal(err)
}
Copy link
Contributor

@sidshank sidshank Nov 25, 2022

Choose a reason for hiding this comment

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

Would it be possible to present a code example that preserves the behaviour we get from the usage of defer, while still handling any errors that that callee may produce i.e. what if I wanted to defer and also handle errors?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Go ready-for-doc-review This PR requires and is ready for review from the GitHub docs team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants