-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[pylint] Improve diagnostic range for PLC0206
#22312
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
Conversation
Summary -- This PR fixes #14900 by: - Restricting the diagnostic range (in preview) from the whole `for` loop to only the `target in iter` part - Adding secondary annotations to each use of the `dict[key]` access This produces an example annotation like this: ``` PLC0206 Extracting value from dictionary without calling `.items()` --> dict_index_missing_items.py:59:5 | 58 | # A case with multiple uses of the value to show off the secondary annotations 59 | for instrument in ORCHESTRA: | ^^^^^^^^^^^^^^^^^^^^^^^ 60 | data = json.dumps( 61 | { 62 | "instrument": instrument, 63 | "section": ORCHESTRA[instrument], | --------------------- 64 | } 65 | ) 66 | 67 | print(f"saving data for {instrument} in {ORCHESTRA[instrument]}") | --------------------- 68 | 69 | with open(f"{instrument}/{ORCHESTRA[instrument]}.txt", "w") as f: | --------------------- 70 | f.write(data) | ``` which I think is a big improvement over: ``` PLC0206 Extracting value from dictionary without calling `.items()` --> dict_index_missing_items.py:59:1 | 58 | # A case with multiple uses of the value to show off the secondary annotations 59 | / for instrument in ORCHESTRA: 60 | | data = json.dumps( 61 | | { 62 | | "instrument": instrument, 63 | | "section": ORCHESTRA[instrument], 64 | | } 65 | | ) 66 | | 67 | | print(f"saving data for {instrument} in {ORCHESTRA[instrument]}") 68 | | 69 | | with open(f"{instrument}/{ORCHESTRA[instrument]}.txt", "w") as f: 70 | | f.write(data) | |_____________________^ | ``` The secondary annotation feels a bit bare without a message, but I thought it might be a bit too busy to include one. Something like `value extracted here` or `indexed here` if we do want to include a brief message. To avoid collecting a `Vec` of annotation ranges, I added a `&Checker` to the rule's visitor to emit diagnostics as we go instead of at the end. Test Plan -- Existing tests, plus a new case showing off multiple secondary annotations
|
| code | total | + violation | - violation | + fix | - fix |
|---|---|---|---|---|---|
| PLC0206 | 40 | 20 | 20 | 0 | 0 |
Linter (preview)
ℹ️ ecosystem check detected linter changes. (+20 -20 violations, +0 -0 fixes in 5 projects; 50 projects unchanged)
apache/airflow (+8 -8 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL
+ airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py:1474:13: PLC0206 Extracting value from dictionary without calling `.items()` - airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py:1474:9: PLC0206 Extracting value from dictionary without calling `.items()` + airflow-core/tests/unit/serialization/test_dag_serialization.py:682:13: PLC0206 Extracting value from dictionary without calling `.items()` - airflow-core/tests/unit/serialization/test_dag_serialization.py:682:9: PLC0206 Extracting value from dictionary without calling `.items()` - devel-common/src/docs/utils/conf_constants.py:208:5: PLC0206 Extracting value from dictionary without calling `.items()` + devel-common/src/docs/utils/conf_constants.py:208:9: PLC0206 Extracting value from dictionary without calling `.items()` - performance/src/performance_dags/performance_dag/performance_dag_utils.py:136:5: PLC0206 Extracting value from dictionary without calling `.items()` + performance/src/performance_dags/performance_dag/performance_dag_utils.py:136:9: PLC0206 Extracting value from dictionary without calling `.items()` + providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py:740:13: PLC0206 Extracting value from dictionary without calling `.items()` - providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py:740:9: PLC0206 Extracting value from dictionary without calling `.items()` + providers/amazon/tests/unit/amazon/aws/executors/ecs/test_ecs_executor.py:1329:13: PLC0206 Extracting value from dictionary without calling `.items()` - providers/amazon/tests/unit/amazon/aws/executors/ecs/test_ecs_executor.py:1329:9: PLC0206 Extracting value from dictionary without calling `.items()` - providers/standard/tests/unit/standard/operators/test_hitl.py:332:13: PLC0206 Extracting value from dictionary without calling `.items()` + providers/standard/tests/unit/standard/operators/test_hitl.py:332:17: PLC0206 Extracting value from dictionary without calling `.items()` - scripts/ci/prek/check_shared_distributions_usage.py:409:5: PLC0206 Extracting value from dictionary without calling `.items()` + scripts/ci/prek/check_shared_distributions_usage.py:409:9: PLC0206 Extracting value from dictionary without calling `.items()`
apache/superset (+3 -3 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL
+ RELEASING/changelog.py:227:13: PLC0206 Extracting value from dictionary without calling `.items()` - RELEASING/changelog.py:227:9: PLC0206 Extracting value from dictionary without calling `.items()` - superset/jinja_context.py:546:5: PLC0206 Extracting value from dictionary without calling `.items()` + superset/jinja_context.py:546:9: PLC0206 Extracting value from dictionary without calling `.items()` + tests/integration_tests/reports/api_tests.py:301:13: PLC0206 Extracting value from dictionary without calling `.items()` - tests/integration_tests/reports/api_tests.py:301:9: PLC0206 Extracting value from dictionary without calling `.items()`
aws/aws-sam-cli (+1 -1 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --no-fix --output-format concise --preview
+ tests/integration/pipeline/test_bootstrap_command.py:229:13: PLC0206 Extracting value from dictionary without calling `.items()` - tests/integration/pipeline/test_bootstrap_command.py:229:9: PLC0206 Extracting value from dictionary without calling `.items()`
bokeh/bokeh (+2 -2 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL
- src/bokeh/application/handlers/code.py:201:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/bokeh/application/handlers/code.py:201:9: PLC0206 Extracting value from dictionary without calling `.items()` + src/bokeh/core/property/wrappers.py:470:13: PLC0206 Extracting value from dictionary without calling `.items()` - src/bokeh/core/property/wrappers.py:470:9: PLC0206 Extracting value from dictionary without calling `.items()`
latchbio/latch (+6 -6 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --no-fix --output-format concise --preview
+ src/latch/functions/operators.py:183:13: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:183:9: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:37:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/latch/functions/operators.py:37:9: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:48:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/latch/functions/operators.py:48:9: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:59:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/latch/functions/operators.py:59:9: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:68:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/latch/functions/operators.py:68:9: PLC0206 Extracting value from dictionary without calling `.items()` - src/latch/functions/operators.py:73:5: PLC0206 Extracting value from dictionary without calling `.items()` + src/latch/functions/operators.py:73:9: PLC0206 Extracting value from dictionary without calling `.items()`
Changes by rule (1 rules affected)
| code | total | + violation | - violation | + fix | - fix |
|---|---|---|---|---|---|
| PLC0206 | 40 | 20 | 20 | 0 | 0 |
MichaReiser
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
| | --------------------- | ||
| 11 | | ||
| 12 | for instrument in ORCHESTRA: | ||
| | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of the confusion in the original issue was that it wasn't clear how to fix the violation. Could we add an info message sub-diagnostic suggesting the use of Use for instrument, value in ORCHESTRA.items() instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, that seems like a nice fix_title even without a fix.
| --> dict_index_missing_items.py:9:1 | ||
| | | ||
| 8 | # Errors | ||
| 9 | / for instrument in ORCHESTRA: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to make this a preview-only change? To suppress PLC0206 today, you have to put the noqa on the line where the for statement starts. Will this change with this PR?
The only example that I can think of is:
ORCHESTRA = dict()
for ( # noqa PLC0206
instrument
) in ORCHESTRA:
print(f"{instrument}: {ORCHESTRA[instrument]}")
But we could fix this by using parenthesized_range for the for-target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I guess you're right! I saw the discussion about the breaking change in the issue, but that must have been in case we moved the diagnostic range to the indexing lines. I think that means I can drop the preview checks entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, at least if the suppression ranges indeed remain unchanged (see my example above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I tested out your example and added it as a test. I'll try to come up with any other tricky cases, but that seems to cover what I've tried so far.
|
|
||
| /// A visitor to detect subscript operations on a target dictionary. | ||
| struct SubscriptVisitor<'a> { | ||
| struct SubscriptVisitor<'a, 'b> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the two lifetimes or would using 'a everywhere be sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that first and just checked again, and I believe we do need both here. The compiler wants me to start annotating the parent functions if I use just 'a.
Details
error: lifetime may not live long enough
--> crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs:80:5
|
62 | pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &ast::StmtFor) {
| ------- - let's call the lifetime of this reference `'1`
| |
| has type `&Checker<'2>`
...
80 | SubscriptVisitor::new(stmt_for, dict_name, checker).visit_body(body);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'2`
|
= note: requirement occurs because of the type `Checker<'_>`, which makes the generic argument `'_` invariant
= note: the struct `Checker<'a>` is invariant over the parameter `'a`
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
help: consider introducing a named lifetime parameter
|
62 | pub(crate) fn dict_index_missing_items<'a>(checker: &'a Checker<'a>, stmt_for: &ast::StmtFor) {
| ++++ ++ ++++
error: lifetime may not live long enough
--> crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs:80:5
|
62 | pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &ast::StmtFor) {
| ------- - let's call the lifetime of this reference `'3`
| |
| has type `&Checker<'2>`
...
80 | SubscriptVisitor::new(stmt_for, dict_name, checker).visit_body(body);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'3` must outlive `'2`
|
= note: requirement occurs because of the type `Checker<'_>`, which makes the generic argument `'_` invariant
= note: the struct `Checker<'a>` is invariant over the parameter `'a`
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
help: consider introducing a named lifetime parameter
|
62 | pub(crate) fn dict_index_missing_items<'a>(checker: &Checker<'a>, stmt_for: &'a ast::StmtFor) {
| ++++ ++++ ++
Summary
This PR fixes #14900 by:
forloop to only thetarget in iterpartdict[key]accessesfix_titlesuggesting to usefor key in dict.items()I thought this approach sounded slightly nicer than the alternative of renaming the rule to focus on each indexing operation mentioned in #14900 (comment), but I don't feel too strongly. This was easy to implement with our new diagnostic infrastructure too.
This produces an example annotation like this:
which I think is a big improvement over:
The secondary annotation feels a bit bare without a message, but I thought it
might be too busy to include one. Something like
value extracted hereorindexed heremight work if we do want to include a brief message.To avoid collecting a
Vecof annotation ranges, I added a&Checkerto therule's visitor to emit diagnostics as we go instead of at the end.
Test Plan
Existing tests, plus a new case showing off multiple secondary annotations