diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py new file mode 100644 index 0000000000..94df2c4a62 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py @@ -0,0 +1,48 @@ +import builtins +import typing +from typing import TypeAlias, Final + +field1: int +field2: int = ... +field3 = ... # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +field4: int = 0 +field41: int = 0xFFFFFFFF +field42: int = 1234567890 +field43: int = -0xFFFFFFFF +field44: int = -1234567890 +field5 = 0 # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") # Y052 Need type annotation for "field5" +field6 = 0 # Y052 Need type annotation for "field6" +field7 = b"" # Y052 Need type annotation for "field7" +field71 = "foo" # Y052 Need type annotation for "field71" +field72: str = "foo" +field8 = False # Y052 Need type annotation for "field8" +field81 = -1 # Y052 Need type annotation for "field81" +field82: float = -98.43 +field83 = -42j # Y052 Need type annotation for "field83" +field84 = 5 + 42j # Y052 Need type annotation for "field84" +field85 = -5 - 42j # Y052 Need type annotation for "field85" +field9 = None # Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "field9: TypeAlias = None" +Field95: TypeAlias = None +Field96: TypeAlias = int | None +Field97: TypeAlias = None | typing.SupportsInt | builtins.str | float | bool +field19 = [1, 2, 3] # Y052 Need type annotation for "field19" +field191: list[int] = [1, 2, 3] +field20 = (1, 2, 3) # Y052 Need type annotation for "field20" +field201: tuple[int, ...] = (1, 2, 3) +field21 = {1, 2, 3} # Y052 Need type annotation for "field21" +field211: set[int] = {1, 2, 3} +field212 = {"foo": "bar"} # Y052 Need type annotation for "field212" +field213: dict[str, str] = {"foo": "bar"} +field22: Final = {"foo": 5} +field221: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # Y015 Only simple default values are allowed for assignments +field223: list[int] = [*range(10)] # Y015 Only simple default values are allowed for assignments +field224: list[int] = list(range(10)) # Y015 Only simple default values are allowed for assignments +field225: list[object] = [{}, 1, 2] # Y015 Only simple default values are allowed for assignments +field226: tuple[str | tuple[str, ...], ...] = ("foo", ("foo", "bar")) # Y015 Only simple default values are allowed for assignments +field227: dict[str, object] = {"foo": {"foo": "bar"}} # Y015 Only simple default values are allowed for assignments +field228: dict[str, list[object]] = {"foo": []} # Y015 Only simple default values are allowed for assignments +# When parsed, this case results in `None` being placed in the `.keys` list for the `ast.Dict` node +field229: dict[int, int] = {1: 2, **{3: 4}} # Y015 Only simple default values are allowed for assignments +field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments +field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments +field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi new file mode 100644 index 0000000000..9330b6b6e1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi @@ -0,0 +1,51 @@ +import builtins +import typing +from typing import TypeAlias, Final + +# We shouldn't emit Y015 for simple default values +field1: int +field2: int = ... +field3 = ... # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +field4: int = 0 +field41: int = 0xFFFFFFFF +field42: int = 1234567890 +field43: int = -0xFFFFFFFF +field44: int = -1234567890 +field5 = 0 # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") # Y052 Need type annotation for "field5" +field6 = 0 # Y052 Need type annotation for "field6" +field7 = b"" # Y052 Need type annotation for "field7" +field71 = "foo" # Y052 Need type annotation for "field71" +field72: str = "foo" +field8 = False # Y052 Need type annotation for "field8" +field81 = -1 # Y052 Need type annotation for "field81" +field82: float = -98.43 +field83 = -42j # Y052 Need type annotation for "field83" +field84 = 5 + 42j # Y052 Need type annotation for "field84" +field85 = -5 - 42j # Y052 Need type annotation for "field85" +field9 = None # Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "field9: TypeAlias = None" +Field95: TypeAlias = None +Field96: TypeAlias = int | None +Field97: TypeAlias = None | typing.SupportsInt | builtins.str | float | bool +field19 = [1, 2, 3] # Y052 Need type annotation for "field19" +field191: list[int] = [1, 2, 3] +field20 = (1, 2, 3) # Y052 Need type annotation for "field20" +field201: tuple[int, ...] = (1, 2, 3) +field21 = {1, 2, 3} # Y052 Need type annotation for "field21" +field211: set[int] = {1, 2, 3} +field212 = {"foo": "bar"} # Y052 Need type annotation for "field212" +field213: dict[str, str] = {"foo": "bar"} +field22: Final = {"foo": 5} + +# We *should* emit Y015 for more complex default values +field221: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # Y015 Only simple default values are allowed for assignments +field223: list[int] = [*range(10)] # Y015 Only simple default values are allowed for assignments +field224: list[int] = list(range(10)) # Y015 Only simple default values are allowed for assignments +field225: list[object] = [{}, 1, 2] # Y015 Only simple default values are allowed for assignments +field226: tuple[str | tuple[str, ...], ...] = ("foo", ("foo", "bar")) # Y015 Only simple default values are allowed for assignments +field227: dict[str, object] = {"foo": {"foo": "bar"}} # Y015 Only simple default values are allowed for assignments +field228: dict[str, list[object]] = {"foo": []} # Y015 Only simple default values are allowed for assignments +# When parsed, this case results in `None` being placed in the `.keys` list for the `ast.Dict` node +field229: dict[int, int] = {1: 2, **{3: 4}} # Y015 Only simple default values are allowed for assignments +field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments +field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments +field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 80777b2a88..88c67c8aee 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1801,8 +1801,19 @@ where self.diagnostics.push(diagnostic); } } + + if self.is_stub { + if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) { + flake8_pyi::rules::assignment_default_in_stub(self, value, None); + } + } } - StmtKind::AnnAssign { target, value, .. } => { + StmtKind::AnnAssign { + target, + value, + annotation, + .. + } => { if self.settings.rules.enabled(Rule::LambdaAssignment) { if let Some(value) = value { pycodestyle::rules::lambda_assignment(self, target, value, stmt); @@ -1820,6 +1831,17 @@ where stmt, ); } + if self.is_stub { + if let Some(value) = value { + if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) { + flake8_pyi::rules::assignment_default_in_stub( + self, + value, + Some(annotation), + ); + } + } + } } StmtKind::Delete { targets } => { if self.settings.rules.enabled(Rule::GlobalStatement) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index bfb1fbc8e6..35befae6c4 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -565,6 +565,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Pyi, "010") => Rule::NonEmptyStubBody, (Flake8Pyi, "011") => Rule::TypedArgumentDefaultInStub, (Flake8Pyi, "014") => Rule::ArgumentDefaultInStub, + (Flake8Pyi, "015") => Rule::AssignmentDefaultInStub, (Flake8Pyi, "021") => Rule::DocstringInStub, (Flake8Pyi, "033") => Rule::TypeCommentInStub, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index ac203c0362..b6f000bc6d 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -514,16 +514,17 @@ ruff_macros::register_rules!( rules::flake8_errmsg::rules::FStringInException, rules::flake8_errmsg::rules::DotFormatInException, // flake8-pyi - rules::flake8_pyi::rules::UnprefixedTypeParam, + rules::flake8_pyi::rules::ArgumentDefaultInStub, + rules::flake8_pyi::rules::AssignmentDefaultInStub, rules::flake8_pyi::rules::BadVersionInfoComparison, + rules::flake8_pyi::rules::DocstringInStub, + rules::flake8_pyi::rules::NonEmptyStubBody, + rules::flake8_pyi::rules::PassStatementStubBody, + rules::flake8_pyi::rules::TypeCommentInStub, + rules::flake8_pyi::rules::TypedArgumentDefaultInStub, + rules::flake8_pyi::rules::UnprefixedTypeParam, rules::flake8_pyi::rules::UnrecognizedPlatformCheck, rules::flake8_pyi::rules::UnrecognizedPlatformName, - rules::flake8_pyi::rules::PassStatementStubBody, - rules::flake8_pyi::rules::NonEmptyStubBody, - rules::flake8_pyi::rules::DocstringInStub, - rules::flake8_pyi::rules::TypedArgumentDefaultInStub, - rules::flake8_pyi::rules::ArgumentDefaultInStub, - rules::flake8_pyi::rules::TypeCommentInStub, // flake8-pytest-style rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle, rules::flake8_pytest_style::rules::PytestFixturePositionalArgs, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 701318a612..fa8696dd8d 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -13,22 +13,24 @@ mod tests { use crate::settings; use crate::test::test_path; - #[test_case(Rule::UnprefixedTypeParam, Path::new("PYI001.pyi"))] #[test_case(Rule::UnprefixedTypeParam, Path::new("PYI001.py"))] - #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] + #[test_case(Rule::UnprefixedTypeParam, Path::new("PYI001.pyi"))] #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))] - #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))] + #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.py"))] - #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] + #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] - #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))] - #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))] + #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.py"))] #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.pyi"))] + #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))] + #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.pyi"))] #[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.py"))] #[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))] + #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))] + #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 3073a37dec..3ad9de8513 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -4,8 +4,8 @@ pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; pub use simple_defaults::{ - argument_simple_defaults, typed_argument_simple_defaults, ArgumentDefaultInStub, - TypedArgumentDefaultInStub, + argument_simple_defaults, assignment_default_in_stub, typed_argument_simple_defaults, + ArgumentDefaultInStub, AssignmentDefaultInStub, TypedArgumentDefaultInStub, }; pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub}; pub use unrecognized_platform::{ 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 096792917b..ec8251b967 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -37,6 +37,21 @@ impl AlwaysAutofixableViolation for ArgumentDefaultInStub { } } +#[violation] +pub struct AssignmentDefaultInStub; + +/// PYI015 +impl AlwaysAutofixableViolation for AssignmentDefaultInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("Only simple default values allowed for assignments") + } + + fn autofix_title(&self) -> String { + "Replace default value with `...`".to_string() + } +} + const ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ &["math", "inf"], &["math", "nan"], @@ -319,3 +334,25 @@ pub fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { } } } + +/// PYI015 +pub fn assignment_default_in_stub(checker: &mut Checker, value: &Expr, annotation: Option<&Expr>) { + if annotation.map_or(false, |annotation| { + checker.ctx.match_typing_expr(annotation, "TypeAlias") + }) { + return; + } + if !is_valid_default_value_with_annotation(value, checker, true) { + let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value)); + + if checker.patch(diagnostic.kind.rule()) { + diagnostic.amend(Edit::replacement( + "...".to_string(), + value.location, + value.end_location.unwrap(), + )); + } + + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.pyi.snap new file mode 100644 index 0000000000..8424002ee2 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI015_PYI015.pyi.snap @@ -0,0 +1,225 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 40 + column: 22 + end_location: + row: 40 + column: 57 + fix: + content: "..." + location: + row: 40 + column: 22 + end_location: + row: 40 + column: 57 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 41 + column: 22 + end_location: + row: 41 + column: 34 + fix: + content: "..." + location: + row: 41 + column: 22 + end_location: + row: 41 + column: 34 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 42 + column: 22 + end_location: + row: 42 + column: 37 + fix: + content: "..." + location: + row: 42 + column: 22 + end_location: + row: 42 + column: 37 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 43 + column: 25 + end_location: + row: 43 + column: 35 + fix: + content: "..." + location: + row: 43 + column: 25 + end_location: + row: 43 + column: 35 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 44 + column: 46 + end_location: + row: 44 + column: 69 + fix: + content: "..." + location: + row: 44 + column: 46 + end_location: + row: 44 + column: 69 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 45 + column: 30 + end_location: + row: 45 + column: 53 + fix: + content: "..." + location: + row: 45 + column: 30 + end_location: + row: 45 + column: 53 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 46 + column: 36 + end_location: + row: 46 + column: 47 + fix: + content: "..." + location: + row: 46 + column: 36 + end_location: + row: 46 + column: 47 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 48 + column: 27 + end_location: + row: 48 + column: 43 + fix: + content: "..." + location: + row: 48 + column: 27 + end_location: + row: 48 + column: 43 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 49 + column: 10 + end_location: + row: 49 + column: 23 + fix: + content: "..." + location: + row: 49 + column: 10 + end_location: + row: 49 + column: 23 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 50 + column: 10 + end_location: + row: 50 + column: 25 + fix: + content: "..." + location: + row: 50 + column: 10 + end_location: + row: 50 + column: 25 + parent: ~ +- kind: + name: AssignmentDefaultInStub + body: Only simple default values allowed for assignments + suggestion: "Replace default value with `...`" + fixable: true + location: + row: 51 + column: 10 + end_location: + row: 51 + column: 15 + fix: + content: "..." + location: + row: 51 + column: 10 + end_location: + row: 51 + column: 15 + parent: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 19417a0cc4..6c46bd297f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1994,6 +1994,7 @@ "PYI010", "PYI011", "PYI014", + "PYI015", "PYI02", "PYI021", "PYI03",