Skip to content

Commit 07636a4

Browse files
committed
Merge remote-tracking branch 'origin/main' into dcreager/unconstrained-typevar
* origin/main: (24 commits) [ty] Remove brittle constraint set reveal tests (#21568) [`ruff`] Catch more dummy variable uses (`RUF052`) (#19799) [ty] Use the same snapshot handling as other tests (#21564) [ty] suppress autocomplete suggestions during variable binding (#21549) Set severity for non-rule diagnostics (#21559) [ty] Add `with_type` convenience to display code (#21563) [ty] Implement docstring rendering to markdown (#21550) [ty] Reduce indentation of `TypeInferenceBuilder::infer_attribute_load` (#21560) Bump 0.14.6 (#21558) [ty] Improve debug messages when imports fail (#21555) [ty] Add support for relative import completions [ty] Refactor detection of import statements for completions [ty] Use dedicated collector for completions [ty] Attach subdiagnostics to `unresolved-import` errors for relative imports as well as absolute imports (#21554) [ty] support PEP 613 type aliases (#21394) [ty] More low-hanging fruit for inlay hint goto-definition (#21548) [ty] implement `TypedDict` structural assignment (#21467) [ty] Add more random TypeDetails and tests (#21546) [ty] Add goto for `Unknown` when it appears in an inlay hint (#21545) [ty] Add type definitions for `Type::SpecialForm`s (#21544) ...
2 parents de391ee + 6cc5027 commit 07636a4

File tree

73 files changed

+8767
-2707
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+8767
-2707
lines changed

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Changelog
22

3+
## 0.14.6
4+
5+
Released on 2025-11-21.
6+
7+
### Preview features
8+
9+
- \[`flake8-bandit`\] Support new PySNMP API paths (`S508`, `S509`) ([#21374](https://github.com/astral-sh/ruff/pull/21374))
10+
11+
### Bug fixes
12+
13+
- Adjust own-line comment placement between branches ([#21185](https://github.com/astral-sh/ruff/pull/21185))
14+
- Avoid syntax error when formatting attribute expressions with outer parentheses, parenthesized value, and trailing comment on value ([#20418](https://github.com/astral-sh/ruff/pull/20418))
15+
- Fix panic when formatting comments in unary expressions ([#21501](https://github.com/astral-sh/ruff/pull/21501))
16+
- Respect `fmt: skip` for compound statements on a single line ([#20633](https://github.com/astral-sh/ruff/pull/20633))
17+
- \[`refurb`\] Fix `FURB103` autofix ([#21454](https://github.com/astral-sh/ruff/pull/21454))
18+
- \[`ruff`\] Fix false positive for complex conversion specifiers in `logging-eager-conversion` (`RUF065`) ([#21464](https://github.com/astral-sh/ruff/pull/21464))
19+
20+
### Rule changes
21+
22+
- \[`ruff`\] Avoid false positive on `ClassVar` reassignment (`RUF012`) ([#21478](https://github.com/astral-sh/ruff/pull/21478))
23+
24+
### CLI
25+
26+
- Render hyperlinks for lint errors ([#21514](https://github.com/astral-sh/ruff/pull/21514))
27+
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING` blocks ([#21472](https://github.com/astral-sh/ruff/pull/21472))
28+
29+
### Documentation
30+
31+
- Limit `eglot-format` hook to eglot-managed Python buffers ([#21459](https://github.com/astral-sh/ruff/pull/21459))
32+
- Mention `force-exclude` in "Configuration > Python file discovery" ([#21500](https://github.com/astral-sh/ruff/pull/21500))
33+
34+
### Contributors
35+
36+
- [@ntBre](https://github.com/ntBre)
37+
- [@dylwil3](https://github.com/dylwil3)
38+
- [@gauthsvenkat](https://github.com/gauthsvenkat)
39+
- [@MichaReiser](https://github.com/MichaReiser)
40+
- [@thamer](https://github.com/thamer)
41+
- [@Ruchir28](https://github.com/Ruchir28)
42+
- [@thejcannon](https://github.com/thejcannon)
43+
- [@danparizher](https://github.com/danparizher)
44+
- [@chirizxc](https://github.com/chirizxc)
45+
346
## 0.14.5
447

548
Released on 2025-11-13.

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
147147
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
148148

149149
# For a specific version.
150-
curl -LsSf https://astral.sh/ruff/0.14.5/install.sh | sh
151-
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
150+
curl -LsSf https://astral.sh/ruff/0.14.6/install.sh | sh
151+
powershell -c "irm https://astral.sh/ruff/0.14.6/install.ps1 | iex"
152152
```
153153

154154
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
181181
```yaml
182182
- repo: https://github.com/astral-sh/ruff-pre-commit
183183
# Ruff version.
184-
rev: v0.14.5
184+
rev: v0.14.6
185185
hooks:
186186
# Run the linter.
187187
- id: ruff-check

crates/ruff/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ruff"
3-
version = "0.14.5"
3+
version = "0.14.6"
44
publish = true
55
authors = { workspace = true }
66
edition = { workspace = true }

crates/ruff_benchmark/benches/ty_walltime.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
143143
max_dep_date: "2025-06-17",
144144
python_version: PythonVersion::PY312,
145145
},
146-
525,
146+
600,
147147
);
148148

149149
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
163163
max_dep_date: "2025-06-17",
164164
python_version: PythonVersion::PY312,
165165
},
166-
3000,
166+
4000,
167167
);
168168

169169
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
181181
max_dep_date: "2025-06-17",
182182
python_version: PythonVersion::PY39,
183183
},
184-
5000,
184+
7000,
185185
);
186186

187187
static SYMPY: Benchmark = Benchmark::new(

crates/ruff_linter/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ruff_linter"
3-
version = "0.14.5"
3+
version = "0.14.6"
44
publish = false
55
authors = { workspace = true }
66
edition = { workspace = true }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Correct usage in loop and comprehension
2+
def process_data():
3+
return 42
4+
def test_correct_dummy_usage():
5+
my_list = [{"foo": 1}, {"foo": 2}]
6+
7+
# Should NOT detect - dummy variable is not used
8+
[process_data() for _ in my_list] # OK: `_` is ignored by rule
9+
10+
# Should NOT detect - dummy variable is not used
11+
[item["foo"] for item in my_list] # OK: not a dummy variable name
12+
13+
# Should NOT detect - dummy variable is not used
14+
[42 for _unused in my_list] # OK: `_unused` is not accessed
15+
16+
# Regular For Loops
17+
def test_for_loops():
18+
my_list = [{"foo": 1}, {"foo": 2}]
19+
20+
# Should detect used dummy variable
21+
for _item in my_list:
22+
print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23+
24+
# Should detect used dummy variable
25+
for _index, _value in enumerate(my_list):
26+
result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
27+
28+
# List Comprehensions
29+
def test_list_comprehensions():
30+
my_list = [{"foo": 1}, {"foo": 2}]
31+
32+
# Should detect used dummy variable
33+
result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
34+
35+
# Should detect used dummy variable in nested comprehension
36+
nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
37+
# RUF052: Both `_item` and `_sublist` are accessed
38+
39+
# Should detect with conditions
40+
filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
41+
# RUF052: Local dummy variable `_item` is accessed
42+
43+
# Dict Comprehensions
44+
def test_dict_comprehensions():
45+
my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
46+
47+
# Should detect used dummy variable
48+
result = {_item["key"]: _item["value"] for _item in my_list}
49+
# RUF052: Local dummy variable `_item` is accessed
50+
51+
# Should detect with enumerate
52+
indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
53+
# RUF052: Both `_index` and `_item` are accessed
54+
55+
# Should detect in nested dict comprehension
56+
nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
57+
for _outer, sublist in enumerate([my_list])}
58+
# RUF052: `_outer`, `_inner` are accessed
59+
60+
# Set Comprehensions
61+
def test_set_comprehensions():
62+
my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
63+
64+
# Should detect used dummy variable
65+
unique_values = {_item["foo"] for _item in my_list}
66+
# RUF052: Local dummy variable `_item` is accessed
67+
68+
# Should detect with conditions
69+
filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
70+
# RUF052: Local dummy variable `_item` is accessed
71+
72+
# Should detect with complex expression
73+
processed = {_item["foo"] * 2 for _item in my_list}
74+
# RUF052: Local dummy variable `_item` is accessed
75+
76+
# Generator Expressions
77+
def test_generator_expressions():
78+
my_list = [{"foo": 1}, {"foo": 2}]
79+
80+
# Should detect used dummy variable
81+
gen = (_item["foo"] for _item in my_list)
82+
# RUF052: Local dummy variable `_item` is accessed
83+
84+
# Should detect when passed to function
85+
total = sum(_item["foo"] for _item in my_list)
86+
# RUF052: Local dummy variable `_item` is accessed
87+
88+
# Should detect with multiple generators
89+
pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
90+
# RUF052: Both `_x` and `_y` are accessed
91+
92+
# Should detect in nested generator
93+
nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
94+
# RUF052: `_inner` and `_sublist` are accessed
95+
96+
# Complex Examples with Multiple Comprehension Types
97+
def test_mixed_comprehensions():
98+
data = [{"items": [1, 2, 3]}, {"items": [4, 5, 6]}]
99+
100+
# Should detect in mixed comprehensions
101+
result = [
102+
{_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
103+
for _record in data
104+
]
105+
# RUF052: `_key`, `_val`, and `_record` are all accessed
106+
107+
# Should detect in generator passed to list constructor
108+
gen_list = list(_item["items"][0] for _item in data)
109+
# RUF052: Local dummy variable `_item` is accessed

crates/ruff_linter/src/rules/ruff/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ mod tests {
9797
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
9898
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
9999
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
100-
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
100+
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"))]
101+
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_1.py"))]
101102
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
102103
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
103104
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))]
@@ -621,8 +622,8 @@ mod tests {
621622
Ok(())
622623
}
623624

624-
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)]
625-
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)]
625+
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"^_+", 1)]
626+
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"", 2)]
626627
fn custom_regexp_preset(
627628
rule_code: Rule,
628629
path: &Path,

crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use ruff_macros::{ViolationMetadata, derive_message_formats};
22
use ruff_python_ast::helpers::is_dunder;
3-
use ruff_python_semantic::{Binding, BindingId};
3+
use ruff_python_semantic::{Binding, BindingId, BindingKind, ScopeKind};
44
use ruff_python_stdlib::identifiers::is_identifier;
55
use ruff_text_size::Ranged;
66

@@ -111,7 +111,7 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
111111
return;
112112
}
113113

114-
// We only emit the lint on variables defined via assignments.
114+
// We only emit the lint on local variables.
115115
//
116116
// ## Why not also emit the lint on function parameters?
117117
//
@@ -127,8 +127,30 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
127127
// autofixing the diagnostic for assignments. See:
128128
// - <https://github.com/astral-sh/ruff/issues/14790>
129129
// - <https://github.com/astral-sh/ruff/issues/14799>
130-
if !binding.kind.is_assignment() {
131-
return;
130+
match binding.kind {
131+
BindingKind::Annotation
132+
| BindingKind::Argument
133+
| BindingKind::NamedExprAssignment
134+
| BindingKind::Assignment
135+
| BindingKind::LoopVar
136+
| BindingKind::WithItemVar
137+
| BindingKind::BoundException
138+
| BindingKind::UnboundException(_) => {}
139+
140+
BindingKind::TypeParam
141+
| BindingKind::Global(_)
142+
| BindingKind::Nonlocal(_, _)
143+
| BindingKind::Builtin
144+
| BindingKind::ClassDefinition(_)
145+
| BindingKind::FunctionDefinition(_)
146+
| BindingKind::Export(_)
147+
| BindingKind::FutureImport
148+
| BindingKind::Import(_)
149+
| BindingKind::FromImport(_)
150+
| BindingKind::SubmoduleImport(_)
151+
| BindingKind::Deletion
152+
| BindingKind::ConditionalDeletion(_)
153+
| BindingKind::DunderClassCell => return,
132154
}
133155

134156
// This excludes `global` and `nonlocal` variables.
@@ -138,9 +160,12 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
138160

139161
let semantic = checker.semantic();
140162

141-
// Only variables defined in function scopes
163+
// Only variables defined in function and generator scopes
142164
let scope = &semantic.scopes[binding.scope];
143-
if !scope.kind.is_function() {
165+
if !matches!(
166+
scope.kind,
167+
ScopeKind::Function(_) | ScopeKind::Generator { .. }
168+
) {
144169
return;
145170
}
146171

0 commit comments

Comments
 (0)