Skip to content

Commit 1ce83df

Browse files
committed
Merge branch 'main' into alex/basic-liskov
2 parents 7f918d7 + 5ca9c15 commit 1ce83df

File tree

44 files changed

+1333
-208
lines changed

Some content is hidden

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

44 files changed

+1333
-208
lines changed

.github/workflows/ty-ecosystem-analyzer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
6868
cd ..
6969
70-
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
70+
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@0aff03414da5d242e97a9f43fb502e085637a4a1"
7171
7272
ecosystem-analyzer \
7373
--repository ruff \

.github/workflows/ty-ecosystem-report.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
5353
cd ..
5454
55-
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
55+
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@0aff03414da5d242e97a9f43fb502e085637a4a1"
5656
5757
ecosystem-analyzer \
5858
--verbose \

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ impl Diagnostic {
6464
id,
6565
severity,
6666
message: message.into_diagnostic_message(),
67+
custom_concise_message: None,
6768
annotations: vec![],
6869
subs: vec![],
6970
fix: None,
@@ -213,6 +214,10 @@ impl Diagnostic {
213214
/// cases, just converting it to a string (or printing it) will do what
214215
/// you want.
215216
pub fn concise_message(&self) -> ConciseMessage<'_> {
217+
if let Some(custom_message) = &self.inner.custom_concise_message {
218+
return ConciseMessage::Custom(custom_message.as_str());
219+
}
220+
216221
let main = self.inner.message.as_str();
217222
let annotation = self
218223
.primary_annotation()
@@ -226,6 +231,15 @@ impl Diagnostic {
226231
}
227232
}
228233

234+
/// Set a custom message for the concise formatting of this diagnostic.
235+
///
236+
/// This overrides the default behavior of generating a concise message
237+
/// from the main diagnostic message and the primary annotation.
238+
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
239+
Arc::make_mut(&mut self.inner).custom_concise_message =
240+
Some(message.into_diagnostic_message());
241+
}
242+
229243
/// Returns the severity of this diagnostic.
230244
///
231245
/// Note that this may be different than the severity of sub-diagnostics.
@@ -532,6 +546,7 @@ struct DiagnosticInner {
532546
id: DiagnosticId,
533547
severity: Severity,
534548
message: DiagnosticMessage,
549+
custom_concise_message: Option<DiagnosticMessage>,
535550
annotations: Vec<Annotation>,
536551
subs: Vec<SubDiagnostic>,
537552
fix: Option<Fix>,
@@ -1520,6 +1535,8 @@ pub enum ConciseMessage<'a> {
15201535
/// This indicates that the diagnostic is probably using the old
15211536
/// model.
15221537
Empty,
1538+
/// A custom concise message has been provided.
1539+
Custom(&'a str),
15231540
}
15241541

15251542
impl std::fmt::Display for ConciseMessage<'_> {
@@ -1535,6 +1552,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
15351552
write!(f, "{main}: {annotation}")
15361553
}
15371554
ConciseMessage::Empty => Ok(()),
1555+
ConciseMessage::Custom(message) => {
1556+
write!(f, "{message}")
1557+
}
15381558
}
15391559
}
15401560
}

crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,9 @@ class AWithQuotes:
132132
final_variable: 'Final[list[int]]' = []
133133
class_variable_without_subscript: 'ClassVar' = []
134134
final_variable_without_subscript: 'Final' = []
135+
136+
137+
# Reassignment of a ClassVar should not trigger RUF012
138+
class P:
139+
class_variable: ClassVar[list] = [10, 20, 30, 40, 50]
140+
class_variable = [*class_variable[0::1], *class_variable[2::3]]

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use ruff_python_ast::{self as ast, Stmt};
1+
use rustc_hash::FxHashSet;
22

33
use ruff_macros::{ViolationMetadata, derive_message_formats};
4+
use ruff_python_ast::{self as ast, Stmt};
45
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
56
use ruff_text_size::Ranged;
67

@@ -96,6 +97,9 @@ impl Violation for MutableClassDefault {
9697

9798
/// RUF012
9899
pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) {
100+
// Collect any `ClassVar`s we find in case they get reassigned later.
101+
let mut class_var_targets = FxHashSet::default();
102+
99103
for statement in &class_def.body {
100104
match statement {
101105
Stmt::AnnAssign(ast::StmtAnnAssign {
@@ -104,6 +108,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
104108
value: Some(value),
105109
..
106110
}) => {
111+
if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
112+
if is_class_var_annotation(annotation, checker.semantic()) {
113+
class_var_targets.insert(id);
114+
}
115+
}
116+
107117
if !is_special_attribute(target)
108118
&& is_mutable_expr(value, checker.semantic())
109119
&& !is_class_var_annotation(annotation, checker.semantic())
@@ -123,8 +133,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
123133
}
124134
}
125135
Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
126-
if !targets.iter().all(is_special_attribute)
127-
&& is_mutable_expr(value, checker.semantic())
136+
if !targets.iter().all(|target| {
137+
is_special_attribute(target)
138+
|| target
139+
.as_name_expr()
140+
.is_some_and(|name| class_var_targets.contains(&name.id))
141+
}) && is_mutable_expr(value, checker.semantic())
128142
{
129143
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
130144
if has_default_copy_semantics(class_def, checker.semantic()) {

crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,53 @@
169169
# dangling before dot
170170
.b # trailing end-of-line
171171
)
172+
173+
# Regression test for https://github.com/astral-sh/ruff/issues/19350
174+
variable = (
175+
(something) # a comment
176+
.first_method("some string")
177+
)
178+
179+
variable = (
180+
something # a commentdddddddddddddddddddddddddddddd
181+
.first_method("some string")
182+
)
183+
184+
if (
185+
(something) # a commentdddddddddddddddddddddddddddddd
186+
.first_method("some string")
187+
): pass
188+
189+
variable = (
190+
(something # a comment
191+
).first_method("some string")
192+
)
193+
194+
if (
195+
(something # a comment
196+
).first_method("some string") # second comment
197+
): pass
198+
199+
variable = ( # 1
200+
# 2
201+
(something) # 3
202+
# 4
203+
.first_method("some string") # 5
204+
# 6
205+
) # 7
206+
207+
208+
if (
209+
(something
210+
# trailing own line on value
211+
)
212+
.first_method("some string")
213+
): ...
214+
215+
variable = (
216+
(something
217+
# 1
218+
) # 2
219+
.first_method("some string")
220+
)
221+

crates/ruff_python_formatter/src/expression/expr_attribute.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,22 @@ impl NeedsParentheses for ExprAttribute {
179179
context.comments().ranges(),
180180
context.source(),
181181
) {
182-
OptionalParentheses::Never
182+
// We have to avoid creating syntax errors like
183+
// ```python
184+
// variable = (something) # trailing
185+
// .my_attribute
186+
// ```
187+
// See https://github.com/astral-sh/ruff/issues/19350
188+
if context
189+
.comments()
190+
.trailing(self.value.as_ref())
191+
.iter()
192+
.any(|comment| comment.line_position().is_end_of_line())
193+
{
194+
OptionalParentheses::Multiline
195+
} else {
196+
OptionalParentheses::Never
197+
}
183198
} else {
184199
self.value.needs_parentheses(self.into(), context)
185200
}

crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,56 @@ result = (
175175
# dangling before dot
176176
.b # trailing end-of-line
177177
)
178+
179+
# Regression test for https://github.com/astral-sh/ruff/issues/19350
180+
variable = (
181+
(something) # a comment
182+
.first_method("some string")
183+
)
184+
185+
variable = (
186+
something # a commentdddddddddddddddddddddddddddddd
187+
.first_method("some string")
188+
)
189+
190+
if (
191+
(something) # a commentdddddddddddddddddddddddddddddd
192+
.first_method("some string")
193+
): pass
194+
195+
variable = (
196+
(something # a comment
197+
).first_method("some string")
198+
)
199+
200+
if (
201+
(something # a comment
202+
).first_method("some string") # second comment
203+
): pass
204+
205+
variable = ( # 1
206+
# 2
207+
(something) # 3
208+
# 4
209+
.first_method("some string") # 5
210+
# 6
211+
) # 7
212+
213+
214+
if (
215+
(something
216+
# trailing own line on value
217+
)
218+
.first_method("some string")
219+
): ...
220+
221+
variable = (
222+
(something
223+
# 1
224+
) # 2
225+
.first_method("some string")
226+
)
227+
178228
```
179229

180230
## Output
@@ -328,4 +378,54 @@ result = (
328378
# dangling before dot
329379
.b # trailing end-of-line
330380
)
381+
382+
# Regression test for https://github.com/astral-sh/ruff/issues/19350
383+
variable = (
384+
(something) # a comment
385+
.first_method("some string")
386+
)
387+
388+
variable = something.first_method( # a commentdddddddddddddddddddddddddddddd
389+
"some string"
390+
)
391+
392+
if (
393+
(something) # a commentdddddddddddddddddddddddddddddd
394+
.first_method("some string")
395+
):
396+
pass
397+
398+
variable = (
399+
something # a comment
400+
).first_method("some string")
401+
402+
if (
403+
(
404+
something # a comment
405+
).first_method("some string") # second comment
406+
):
407+
pass
408+
409+
variable = ( # 1
410+
# 2
411+
(something) # 3
412+
# 4
413+
.first_method("some string") # 5
414+
# 6
415+
) # 7
416+
417+
418+
if (
419+
something
420+
# trailing own line on value
421+
).first_method("some string"):
422+
...
423+
424+
variable = (
425+
(
426+
something
427+
# 1
428+
) # 2
429+
.first_method("some string")
430+
)
331431
```

crates/ty/tests/cli/main.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,24 @@ fn test_quiet_output() -> anyhow::Result<()> {
4141
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
4242

4343
// By default, we emit a diagnostic
44-
assert_cmd_snapshot!(case.command(), @r###"
44+
assert_cmd_snapshot!(case.command(), @r#"
4545
success: false
4646
exit_code: 1
4747
----- stdout -----
4848
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
49-
--> test.py:1:1
49+
--> test.py:1:4
5050
|
5151
1 | x: int = 'foo'
52-
| ^
52+
| --- ^^^^^ Incompatible value of type `Literal["foo"]`
53+
| |
54+
| Declared type
5355
|
5456
info: rule `invalid-assignment` is enabled by default
5557
5658
Found 1 diagnostic
5759
5860
----- stderr -----
59-
"###);
61+
"#);
6062

6163
// With `quiet`, the diagnostic is not displayed, just the summary message
6264
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"

0 commit comments

Comments
 (0)