Skip to content

Commit cc8b5a5

Browse files
Ignore stub file assignments to value-requiring targets (#4030)
1 parent 10d5415 commit cc8b5a5

File tree

5 files changed

+133
-26
lines changed

5 files changed

+133
-26
lines changed

crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py

+34
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,37 @@
5050
# We shouldn't emit Y015 within functions
5151
def f():
5252
field26: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
53+
54+
55+
# We shouldn't emit Y015 for __slots__ or __match_args__
56+
class Class1:
57+
__slots__ = (
58+
'_one',
59+
'_two',
60+
'_three',
61+
'_four',
62+
'_five',
63+
'_six',
64+
'_seven',
65+
'_eight',
66+
'_nine',
67+
'_ten',
68+
'_eleven',
69+
)
70+
71+
__match_args__ = (
72+
'one',
73+
'two',
74+
'three',
75+
'four',
76+
'five',
77+
'six',
78+
'seven',
79+
'eight',
80+
'nine',
81+
'ten',
82+
'eleven',
83+
)
84+
85+
# We shouldn't emit Y015 for __all__
86+
__all__ = ["Class1"]

crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi

+34
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,37 @@ field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments
5757
# We shouldn't emit Y015 within functions
5858
def f():
5959
field26: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
60+
61+
62+
# We shouldn't emit Y015 for __slots__ or __match_args__
63+
class Class1:
64+
__slots__ = (
65+
'_one',
66+
'_two',
67+
'_three',
68+
'_four',
69+
'_five',
70+
'_six',
71+
'_seven',
72+
'_eight',
73+
'_nine',
74+
'_ten',
75+
'_eleven',
76+
)
77+
78+
__match_args__ = (
79+
'one',
80+
'two',
81+
'three',
82+
'four',
83+
'five',
84+
'six',
85+
'seven',
86+
'eight',
87+
'nine',
88+
'ten',
89+
'eleven',
90+
)
91+
92+
# We shouldn't emit Y015 for __all__
93+
__all__ = ["Class1"]

crates/ruff/src/checkers/ast/mod.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1842,7 +1842,7 @@ where
18421842
flake8_pyi::rules::prefix_type_params(self, value, targets);
18431843
}
18441844
if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) {
1845-
flake8_pyi::rules::assignment_default_in_stub(self, value, None);
1845+
flake8_pyi::rules::assignment_default_in_stub(self, targets, value);
18461846
}
18471847
}
18481848
}
@@ -1882,10 +1882,8 @@ where
18821882
if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) {
18831883
// Ignore assignments in function bodies; those are covered by other rules.
18841884
if !self.ctx.scopes().any(|scope| scope.kind.is_function()) {
1885-
flake8_pyi::rules::assignment_default_in_stub(
1886-
self,
1887-
value,
1888-
Some(annotation),
1885+
flake8_pyi::rules::annotated_assignment_default_in_stub(
1886+
self, target, value, annotation,
18891887
);
18901888
}
18911889
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ pub use pass_in_class_body::{pass_in_class_body, PassInClassBody};
66
pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
77
pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam};
88
pub use simple_defaults::{
9-
argument_simple_defaults, assignment_default_in_stub, typed_argument_simple_defaults,
10-
ArgumentDefaultInStub, AssignmentDefaultInStub, TypedArgumentDefaultInStub,
9+
annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub,
10+
typed_argument_simple_defaults, ArgumentDefaultInStub, AssignmentDefaultInStub,
11+
TypedArgumentDefaultInStub,
1112
};
1213
pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub};
1314
pub use unrecognized_platform::{

crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs

+59-19
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use crate::registry::AsRule;
1111
#[violation]
1212
pub struct TypedArgumentDefaultInStub;
1313

14-
/// PYI011
1514
impl AlwaysAutofixableViolation for TypedArgumentDefaultInStub {
1615
#[derive_message_formats]
1716
fn message(&self) -> String {
@@ -26,7 +25,6 @@ impl AlwaysAutofixableViolation for TypedArgumentDefaultInStub {
2625
#[violation]
2726
pub struct ArgumentDefaultInStub;
2827

29-
/// PYI014
3028
impl AlwaysAutofixableViolation for ArgumentDefaultInStub {
3129
#[derive_message_formats]
3230
fn message(&self) -> String {
@@ -41,7 +39,6 @@ impl AlwaysAutofixableViolation for ArgumentDefaultInStub {
4139
#[violation]
4240
pub struct AssignmentDefaultInStub;
4341

44-
/// PYI015
4542
impl AlwaysAutofixableViolation for AssignmentDefaultInStub {
4643
#[derive_message_formats]
4744
fn message(&self) -> String {
@@ -225,7 +222,7 @@ fn is_valid_default_value_with_annotation(
225222
/// Returns `true` if an [`Expr`] appears to be `TypeVar`, `TypeVarTuple`, `NewType`, or `ParamSpec`
226223
/// call.
227224
fn is_type_var_like_call(context: &Context, expr: &Expr) -> bool {
228-
let ExprKind::Call {func, ..} = &expr.node else {
225+
let ExprKind::Call { func, .. } = &expr.node else {
229226
return false;
230227
};
231228
context.resolve_call_path(func).map_or(false, |call_path| {
@@ -239,6 +236,20 @@ fn is_type_var_like_call(context: &Context, expr: &Expr) -> bool {
239236
})
240237
}
241238

239+
/// Returns `true` if this is a "special" assignment which must have a value (e.g., an assignment to
240+
/// `__all__`).
241+
fn is_special_assignment(context: &Context, target: &Expr) -> bool {
242+
if let ExprKind::Name { id, .. } = &target.node {
243+
match id.as_str() {
244+
"__all__" => context.scope().kind.is_module(),
245+
"__match_args__" | "__slots__" => context.scope().kind.is_class(),
246+
_ => false,
247+
}
248+
} else {
249+
false
250+
}
251+
}
252+
242253
/// PYI011
243254
pub fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) {
244255
if !args.defaults.is_empty() {
@@ -354,26 +365,55 @@ pub fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) {
354365
}
355366

356367
/// PYI015
357-
pub fn assignment_default_in_stub(checker: &mut Checker, value: &Expr, annotation: Option<&Expr>) {
358-
if annotation.map_or(false, |annotation| {
359-
checker.ctx.match_typing_expr(annotation, "TypeAlias")
360-
}) {
368+
pub fn assignment_default_in_stub(checker: &mut Checker, targets: &[Expr], value: &Expr) {
369+
if targets.len() == 1 && is_special_assignment(&checker.ctx, &targets[0]) {
361370
return;
362371
}
363372
if is_type_var_like_call(&checker.ctx, value) {
364373
return;
365374
}
366-
if !is_valid_default_value_with_annotation(value, checker, true) {
367-
let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value));
368-
369-
if checker.patch(diagnostic.kind.rule()) {
370-
diagnostic.set_fix(Edit::replacement(
371-
"...".to_string(),
372-
value.location,
373-
value.end_location.unwrap(),
374-
));
375-
}
375+
if is_valid_default_value_with_annotation(value, checker, true) {
376+
return;
377+
}
378+
379+
let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value));
380+
if checker.patch(diagnostic.kind.rule()) {
381+
diagnostic.set_fix(Edit::replacement(
382+
"...".to_string(),
383+
value.location,
384+
value.end_location.unwrap(),
385+
));
386+
}
387+
checker.diagnostics.push(diagnostic);
388+
}
389+
390+
/// PYI015
391+
pub fn annotated_assignment_default_in_stub(
392+
checker: &mut Checker,
393+
target: &Expr,
394+
value: &Expr,
395+
annotation: &Expr,
396+
) {
397+
if checker.ctx.match_typing_expr(annotation, "TypeAlias") {
398+
return;
399+
}
400+
if is_special_assignment(&checker.ctx, target) {
401+
return;
402+
}
403+
if is_type_var_like_call(&checker.ctx, value) {
404+
return;
405+
}
406+
if is_valid_default_value_with_annotation(value, checker, true) {
407+
return;
408+
}
376409

377-
checker.diagnostics.push(diagnostic);
410+
let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value));
411+
if checker.patch(diagnostic.kind.rule()) {
412+
diagnostic.set_fix(Edit::replacement(
413+
"...".to_string(),
414+
value.location,
415+
value.end_location.unwrap(),
416+
));
378417
}
418+
checker.diagnostics.push(diagnostic);
379419
}

0 commit comments

Comments
 (0)