diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.py new file mode 100644 index 0000000000000..4343b93af6a84 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.py @@ -0,0 +1,15 @@ +__all__: list[str] + +__all__: list[str] = ["foo"] + + +class Foo: + __all__: list[str] + __match_args__: tuple[str, ...] + __slots__: tuple[str, ...] + + +class Bar: + __all__: list[str] = ["foo"] + __match_args__: tuple[str, ...] = (1,) + __slots__: tuple[str, ...] = "foo" diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.pyi new file mode 100644 index 0000000000000..81dfe26692acf --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI035.pyi @@ -0,0 +1,13 @@ +__all__: list[str] # Error: PYI035 + +__all__: list[str] = ["foo"] + +class Foo: + __all__: list[str] + __match_args__: tuple[str, ...] # Error: PYI035 + __slots__: tuple[str, ...] # Error: PYI035 + +class Bar: + __all__: list[str] = ["foo"] + __match_args__: tuple[str, ...] = (1,) + __slots__: tuple[str, ...] = "foo" diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c5e547eb93154..d736aa69db73a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1785,6 +1785,12 @@ where ); } } + } else { + if self.enabled(Rule::UnassignedSpecialVariableInStub) { + flake8_pyi::rules::unassigned_special_variable_in_stub( + self, target, stmt, + ); + } } if self .semantic_model diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 7575bf64fc255..e5ab9ae3f1af0 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -607,6 +607,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), + (Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias), (Flake8Pyi, "045") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::IterMethodReturnIterable), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index ed66126659158..e92e12e003867 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -44,6 +44,8 @@ mod tests { #[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] + #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))] + #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index c7a45614d09bd..4f20a04c76ba0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -20,8 +20,9 @@ pub(crate) use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; pub(crate) use quoted_annotation_in_stub::{quoted_annotation_in_stub, QuotedAnnotationInStub}; pub(crate) use simple_defaults::{ annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub, - typed_argument_simple_defaults, unannotated_assignment_in_stub, ArgumentDefaultInStub, - AssignmentDefaultInStub, TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, + typed_argument_simple_defaults, unannotated_assignment_in_stub, + unassigned_special_variable_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, + TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, UnassignedSpecialVariableInStub, }; pub(crate) use string_or_bytes_too_long::{string_or_bytes_too_long, StringOrBytesTooLong}; pub(crate) use stub_body_multiple_statements::{ diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index 933d6d377fe61..609602beab98f 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, Stmt, Unaryop}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -64,6 +64,37 @@ impl Violation for UnannotatedAssignmentInStub { } } +#[violation] +pub struct UnassignedSpecialVariableInStub { + name: String, +} + +/// ## What it does +/// Checks that `__all__`, `__match_args__`, and `__slots__` variables are +/// assigned to values when defined in stub files. +/// +/// ## Why is this bad? +/// Special variables like `__all__` have the same semantics in stub files +/// as they do in Python modules, and so should be consistent with their +/// runtime counterparts. +/// +/// ## Example +/// ```python +/// __all__: list[str] +/// ``` +/// +/// Use instead: +/// ```python +/// __all__: list[str] = ["foo", "bar"] +/// ``` +impl Violation for UnassignedSpecialVariableInStub { + #[derive_message_formats] + fn message(&self) -> String { + let UnassignedSpecialVariableInStub { name } = self; + format!("`{name}` in a stub file must have a value, as it has the same semantics as `{name}` at runtime") + } +} + const ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ &["math", "inf"], &["math", "nan"], @@ -528,3 +559,25 @@ pub(crate) fn unannotated_assignment_in_stub( value.range(), )); } + +/// PYI035 +pub(crate) fn unassigned_special_variable_in_stub( + checker: &mut Checker, + target: &Expr, + stmt: &Stmt, +) { + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + + if !is_special_assignment(checker.semantic_model(), target) { + return; + } + + checker.diagnostics.push(Diagnostic::new( + UnassignedSpecialVariableInStub { + name: id.to_string(), + }, + stmt.range(), + )); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.py.snap new file mode 100644 index 0000000000000..d1aa2e9116558 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.pyi.snap new file mode 100644 index 0000000000000..7df25531e0bb4 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI035_PYI035.pyi.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI035.pyi:1:1: PYI035 `__all__` in a stub file must have a value, as it has the same semantics as `__all__` at runtime + | +1 | __all__: list[str] # Error: PYI035 + | ^^^^^^^^^^^^^^^^^^ PYI035 +2 | +3 | __all__: list[str] = ["foo"] + | + +PYI035.pyi:7:5: PYI035 `__match_args__` in a stub file must have a value, as it has the same semantics as `__match_args__` at runtime + | + 7 | class Foo: + 8 | __all__: list[str] + 9 | __match_args__: tuple[str, ...] # Error: PYI035 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI035 +10 | __slots__: tuple[str, ...] # Error: PYI035 + | + +PYI035.pyi:8:5: PYI035 `__slots__` in a stub file must have a value, as it has the same semantics as `__slots__` at runtime + | + 8 | __all__: list[str] + 9 | __match_args__: tuple[str, ...] # Error: PYI035 +10 | __slots__: tuple[str, ...] # Error: PYI035 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI035 +11 | +12 | class Bar: + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 038af86c1cb64..92f43ef1a303b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2245,6 +2245,7 @@ "PYI032", "PYI033", "PYI034", + "PYI035", "PYI04", "PYI042", "PYI043",