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

Python: Modernize the Loop Variable Capture query #19165

Merged

Conversation

joefarebrother
Copy link
Contributor

Rewrites the Loop variable capture query to use dataflow rather than PointsTo.

@joefarebrother joefarebrother force-pushed the python-qual-loop-var-capture branch from 8eb7c36 to c37809a Compare April 2, 2025 09:46
Copy link
Contributor

github-actions bot commented Apr 2, 2025

QHelp previews:

python/ql/src/Variables/LoopVariableCapture/LoopVariableCapture.qhelp

Loop variable capture

In Python, a nested function or lambda expression that captures a variable from its surrounding scope is a late-binding closure, meaning that the value of the variable is determined when the closure is called, not when it is created.

Care must be taken when the captured variable is a loop variable. If the closure is called after the loop ends, it will use the value of the variable on the last iteration of the loop, rather than the value at the iteration at which it was created.

Recommendation

Ensure that closures that capture loop variables aren't used outside of a single iteration of the loop. To capture the value of a loop variable at the time the closure is created, use a default parameter, or functools.partial.

Example

In the following (BAD) example, a tasks list is created, but each task captures the loop variable i, and reads the same value when run.

# BAD: The loop variable `i` is captured.
tasks = []
for i in range(5):
    tasks.append(lambda: print(i))

# This will print `4,4,4,4,4`, rather than `0,1,2,3,4` as likely intended.
for t in tasks:
    t() 

In the following (GOOD) example, each closure has an i default parameter, shadowing the outer i variable, the default value of which is determined as the value of the loop variable i at the time the closure is created.

# GOOD: A default parameter is used, so the variable `i` is not being captured.
tasks = []
for i in range(5):
    tasks.append(lambda i=i: print(i))

# This will print `0,1,2,3,4``.
for t in tasks:
    t() 

In the following (GOOD) example, functools.partial is used to partially evaluate the lambda expression with the value of i.

import functools
# GOOD: `functools.partial` takes care of capturing the _value_ of `i`.
tasks = []
for i in range(5):
    tasks.append(functools.partial(lambda i: print(i), i))

# This will print `0,1,2,3,4``.
for t in tasks:
    t() 

References

if capturing instanceof Lambda then descr = "lambda" else descr = "function"
select capturing, source, sink,
"This " + descr + " captures the loop variable $@, and may escape the loop by being stored at $@.",
loop, var.getId(), sink, "this location"

Check warning

Code scanning / CodeQL

Alert message style violation Warning

Try to more descriptive phrase instead of "this location" if possible.
@joefarebrother joefarebrother force-pushed the python-qual-loop-var-capture branch from 3112a14 to e08072d Compare April 4, 2025 11:52
@joefarebrother joefarebrother marked this pull request as ready for review April 4, 2025 12:39
@joefarebrother joefarebrother requested a review from a team as a code owner April 4, 2025 12:39
@joefarebrother joefarebrother added the no-change-note-required This PR does not need a change note label Apr 4, 2025
@joefarebrother joefarebrother changed the title [Draft] Python: Modernize the Loop Variable Capture query Python: Modernize the Loop Variable Capture query Apr 4, 2025
tausbn
tausbn previously approved these changes Apr 9, 2025
Copy link
Contributor

@tausbn tausbn left a comment

Choose a reason for hiding this comment

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

Other than a small suggestion, this looks good to me. Good use of data-flow! 👍

Co-authored-by: Taus <tausbn@github.com>
@joefarebrother joefarebrother added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Apr 9, 2025
@joefarebrother
Copy link
Contributor Author

Since I made significant changes to the qhelp, I will also request a docs reveiw.

@mchammer01 mchammer01 self-requested a review April 10, 2025 07:18
mchammer01
mchammer01 previously approved these changes Apr 10, 2025
Copy link
Contributor

@mchammer01 mchammer01 left a comment

Choose a reason for hiding this comment

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

@joefarebrother 👋🏻 - approving this on behalf of Docs 👍🏻
I tried to improve the query description slightly and I've made a suggestion. Let me know what you think.

Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com>
@joefarebrother joefarebrother merged commit 7f7fca9 into github:main Apr 10, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation no-change-note-required This PR does not need a change note Python 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.

3 participants