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

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

Open
wants to merge 3 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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants