diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.py new file mode 100644 index 0000000000000..7c088352f5bce --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.py @@ -0,0 +1,79 @@ +def f12( + x, + y: str = os.pathsep, # OK +) -> None: + ... + + +def f11(*, x: str = "x") -> None: # OK + ... + + +def f13( + x: list[str] = [ + "foo", + "bar", + "baz", + ] # OK +) -> None: + ... + + +def f14( + x: tuple[str, ...] = ( + "foo", + "bar", + "baz", + ) # OK +) -> None: + ... + + +def f15( + x: set[str] = { + "foo", + "bar", + "baz", + } # OK +) -> None: + ... + + +def f16(x: frozenset[bytes] = frozenset({b"foo", b"bar", b"baz"})) -> None: # OK + ... + + +def f17( + x: str = "foo" + "bar", # OK +) -> None: + ... + + +def f18( + x: str = b"foo" + b"bar", # OK +) -> None: + ... + + +def f19( + x: object = "foo" + 4, # OK +) -> None: + ... + + +def f20( + x: int = 5 + 5, # OK +) -> None: + ... + + +def f21( + x: complex = 3j - 3j, # OK +) -> None: + ... + + +def f22( + x: complex = -42.5j + 4.3j, # OK +) -> None: + ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.pyi new file mode 100644 index 0000000000000..ed40594b546c3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI011.pyi @@ -0,0 +1,63 @@ +def f12( + x, + y: str = os.pathsep, # Error PYI011 Only simple default values allowed for typed arguments +) -> None: ... +def f11(*, x: str = "x") -> None: ... # OK +def f13( + x: list[ + str + ] = [ # Error PYI011 Only simple default values allowed for typed arguments + "foo", + "bar", + "baz", + ] +) -> None: ... +def f14( + x: tuple[ + str, ... + ] = ( # Error PYI011 Only simple default values allowed for typed arguments + "foo", + "bar", + "baz", + ) +) -> None: ... +def f15( + x: set[ + str + ] = { # Error PYI011 Only simple default values allowed for typed arguments + "foo", + "bar", + "baz", + } +) -> None: ... +def f16( + x: frozenset[ + bytes + ] = frozenset( # Error PYI011 Only simple default values allowed for typed arguments + {b"foo", b"bar", b"baz"} + ) +) -> None: ... +def f17( + x: str = "foo" # Error PYI011 Only simple default values allowed for typed arguments + + "bar", +) -> None: ... +def f18( + x: str = b"foo" # Error PYI011 Only simple default values allowed for typed arguments + + b"bar", +) -> None: ... +def f19( + x: object = "foo" # Error PYI011 Only simple default values allowed for typed arguments + + 4, +) -> None: ... +def f20( + x: int = 5 + + 5, # Error PYI011 Only simple default values allowed for typed arguments +) -> None: ... +def f21( + x: complex = 3j + - 3j, # Error PYI011 Only simple default values allowed for typed arguments +) -> None: ... +def f22( + x: complex = -42.5j # Error PYI011 Only simple default values allowed for typed arguments + + 4.3j, +) -> None: ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.py new file mode 100644 index 0000000000000..e47f0a365c729 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.py @@ -0,0 +1,79 @@ +def f12( + x, + y=os.pathsep, # OK +) -> None: + ... + + +def f11(*, x="x") -> None: + ... # OK + + +def f13( + x=[ # OK + "foo", + "bar", + "baz", + ] +) -> None: + ... + + +def f14( + x=( # OK + "foo", + "bar", + "baz", + ) +) -> None: + ... + + +def f15( + x={ # OK + "foo", + "bar", + "baz", + } +) -> None: + ... + + +def f16(x=frozenset({b"foo", b"bar", b"baz"})) -> None: + ... # OK + + +def f17( + x="foo" + "bar", # OK +) -> None: + ... + + +def f18( + x=b"foo" + b"bar", # OK +) -> None: + ... + + +def f19( + x="foo" + 4, # OK +) -> None: + ... + + +def f20( + x=5 + 5, # OK +) -> None: + ... + + +def f21( + x=3j - 3j, # OK +) -> None: + ... + + +def f22( + x=-42.5j + 4.3j, # OK +) -> None: + ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.pyi new file mode 100644 index 0000000000000..ad645dfb5d45b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI014.pyi @@ -0,0 +1,45 @@ +def f12( + x, + y=os.pathsep, # Error PYI014 +) -> None: ... +def f11(*, x="x") -> None: ... # OK +def f13( + x=[ # Error PYI014 + "foo", + "bar", + "baz", + ] +) -> None: ... +def f14( + x=( # Error PYI014 + "foo", + "bar", + "baz", + ) +) -> None: ... +def f15( + x={ # Error PYI014 + "foo", + "bar", + "baz", + } +) -> None: ... +def f16(x=frozenset({b"foo", b"bar", b"baz"})) -> None: ... # Error PYI014 +def f17( + x="foo" + "bar", # Error PYI014 +) -> None: ... +def f18( + x=b"foo" + b"bar", # Error PYI014 +) -> None: ... +def f19( + x="foo" + 4, # Error PYI014 +) -> None: ... +def f20( + x=5 + 5, # Error PYI014 +) -> None: ... +def f21( + x=3j - 3j, # Error PYI014 +) -> None: ... +def f22( + x=-42.5j + 4.3j, # Error PYI014 +) -> None: ... diff --git a/crates/ruff/src/checkers/ast.rs b/crates/ruff/src/checkers/ast.rs index 0d0f2a99aa0ad..30bda4f089456 100644 --- a/crates/ruff/src/checkers/ast.rs +++ b/crates/ruff/src/checkers/ast.rs @@ -3930,6 +3930,21 @@ where flake8_bugbear::rules::function_call_argument_default(self, arguments); } + if self.is_interface_definition { + if self + .settings + .rules + .enabled(&Rule::TypedArgumentSimpleDefaults) + { + flake8_pyi::rules::typed_argument_simple_defaults(self, arguments); + } + } + if self.is_interface_definition { + if self.settings.rules.enabled(&Rule::ArgumentSimpleDefaults) { + flake8_pyi::rules::argument_simple_defaults(self, arguments); + } + } + // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. for arg in &arguments.posonlyargs { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 24487edd1a507..8df9ac8c517cb 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -489,6 +489,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Pyi, "008") => Rule::UnrecognizedPlatformName, (Flake8Pyi, "009") => Rule::PassStatementStubBody, (Flake8Pyi, "010") => Rule::NonEmptyStubBody, + (Flake8Pyi, "011") => Rule::TypedArgumentSimpleDefaults, + (Flake8Pyi, "014") => Rule::ArgumentSimpleDefaults, (Flake8Pyi, "021") => Rule::DocstringInStub, // flake8-pytest-style diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 4638df63194b6..0b80018588ee2 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -464,6 +464,8 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::PassStatementStubBody, rules::flake8_pyi::rules::NonEmptyStubBody, rules::flake8_pyi::rules::DocstringInStub, + rules::flake8_pyi::rules::TypedArgumentSimpleDefaults, + rules::flake8_pyi::rules::ArgumentSimpleDefaults, // flake8-pytest-style rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle, rules::flake8_pytest_style::rules::FixturePositionalArgs, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 7e7995b59ba52..bd73a8e55f85b 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -23,6 +23,10 @@ mod tests { #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))] #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.py"))] #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.pyi"))] + #[test_case(Rule::TypedArgumentSimpleDefaults, Path::new("PYI011.py"))] + #[test_case(Rule::TypedArgumentSimpleDefaults, Path::new("PYI011.pyi"))] + #[test_case(Rule::ArgumentSimpleDefaults, Path::new("PYI014.py"))] + #[test_case(Rule::ArgumentSimpleDefaults, Path::new("PYI014.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 7482ee3529070..ea634b4ed8983 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -2,6 +2,10 @@ pub use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; 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, PrefixTypeParams}; +pub use simple_defaults::{ + argument_simple_defaults, typed_argument_simple_defaults, ArgumentSimpleDefaults, + TypedArgumentSimpleDefaults, +}; pub use unrecognized_platform::{ unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, }; @@ -11,3 +15,5 @@ mod non_empty_stub_body; mod pass_statement_stub_body; mod prefix_type_params; mod unrecognized_platform; + +mod simple_defaults; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs new file mode 100644 index 0000000000000..066ebeff2aaf3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -0,0 +1,220 @@ +use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind, Operator, Unaryop}; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + pub struct TypedArgumentSimpleDefaults; +); +/// PYI011 +impl Violation for TypedArgumentSimpleDefaults { + #[derive_message_formats] + fn message(&self) -> String { + format!("Only simple default values allowed for typed arguments") + } +} + +define_violation!( + pub struct ArgumentSimpleDefaults; +); +/// PYI014 +impl Violation for ArgumentSimpleDefaults { + #[derive_message_formats] + fn message(&self) -> String { + format!("Only simple default values allowed for arguments") + } +} + +const ALLOWED_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ + &["sys", "stdin"], + &["sys", "stdout"], + &["sys", "stderr"], + &["sys", "version"], + &["sys", "version_info"], + &["sys", "platform"], + &["sys", "executable"], + &["sys", "prefix"], + &["sys", "exec_prefix"], + &["sys", "base_prefix"], + &["sys", "byteorder"], + &["sys", "maxsize"], + &["sys", "hexversion"], + &["sys", "winver"], +]; + +fn is_valid_default_value_with_annotation(default: &Expr, checker: &Checker) -> bool { + match &default.node { + ExprKind::Constant { + value: Constant::Ellipsis | Constant::None, + .. + } => { + return true; + } + ExprKind::Constant { + value: Constant::Str(..), + .. + } => return checker.locator.slice(&Range::from_located(default)).len() <= 50, + ExprKind::Constant { + value: Constant::Bytes(..), + .. + } => return checker.locator.slice(&Range::from_located(default)).len() <= 50, + ExprKind::Constant { + value: Constant::Int(..), + .. + } => { + return checker.locator.slice(&Range::from_located(default)).len() <= 10; + } + ExprKind::UnaryOp { + op: Unaryop::USub, + operand, + } => { + if let ExprKind::Constant { + value: Constant::Int(..), + .. + } = &operand.node + { + return checker.locator.slice(&Range::from_located(operand)).len() <= 10; + } + } + ExprKind::BinOp { + left, + op: Operator::Add | Operator::Sub, + right, + } => { + // 1 + 2j + // 1 - 2j + // -1 - 2j + // -1 + 2j + if let ExprKind::Constant { + value: Constant::Complex { .. }, + .. + } = right.node + { + // 1 + 2j + // 1 - 2j + if let ExprKind::Constant { + value: Constant::Int(..), + .. + } = &left.node + { + return checker.locator.slice(&Range::from_located(left)).len() <= 10; + } else if let ExprKind::UnaryOp { + op: Unaryop::USub, + operand, + } = &left.node + { + // -1 + 2j + // -1 - 2j + if let ExprKind::Constant { + value: Constant::Int(..), + .. + } = &operand.node + { + return checker.locator.slice(&Range::from_located(operand)).len() <= 10; + } + } + } + } + // `sys.stdin`, etc. + ExprKind::Attribute { .. } => { + if checker + .resolve_call_path(default) + .map_or(false, |call_path| { + ALLOWED_ATTRIBUTES_IN_DEFAULTS + .iter() + .any(|target| call_path.as_slice() == *target) + }) + { + return true; + } + } + _ => {} + } + false +} + +/// PYI011 +pub fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) { + if !args.defaults.is_empty() { + let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); + for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { + if let Some(default) = i + .checked_sub(defaults_start) + .and_then(|i| args.defaults.get(i)) + { + if arg.node.annotation.is_some() { + if !is_valid_default_value_with_annotation(default, checker) { + checker.diagnostics.push(Diagnostic::new( + TypedArgumentSimpleDefaults, + Range::from_located(default), + )); + } + } + } + } + } + + if !args.kw_defaults.is_empty() { + let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); + for (i, kwarg) in args.kwonlyargs.iter().enumerate() { + if let Some(default) = i + .checked_sub(defaults_start) + .and_then(|i| args.kw_defaults.get(i)) + { + if kwarg.node.annotation.is_some() { + if !is_valid_default_value_with_annotation(default, checker) { + checker.diagnostics.push(Diagnostic::new( + TypedArgumentSimpleDefaults, + Range::from_located(default), + )); + } + } + } + } + } +} + +/// PYI014 +pub fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { + if !args.defaults.is_empty() { + let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); + for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { + if let Some(default) = i + .checked_sub(defaults_start) + .and_then(|i| args.defaults.get(i)) + { + if arg.node.annotation.is_none() { + if !is_valid_default_value_with_annotation(default, checker) { + checker.diagnostics.push(Diagnostic::new( + ArgumentSimpleDefaults, + Range::from_located(default), + )); + } + } + } + } + } + + if !args.kw_defaults.is_empty() { + let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); + for (i, kwarg) in args.kwonlyargs.iter().enumerate() { + if let Some(default) = i + .checked_sub(defaults_start) + .and_then(|i| args.kw_defaults.get(i)) + { + if kwarg.node.annotation.is_none() { + if !is_valid_default_value_with_annotation(default, checker) { + checker.diagnostics.push(Diagnostic::new( + ArgumentSimpleDefaults, + Range::from_located(default), + )); + } + } + } + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI011_PYI011.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI011_PYI011.py.snap new file mode 100644 index 0000000000000..efcc2d0c99b2f --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI011_PYI011.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__PYI011_PYI011.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI011_PYI011.pyi.snap new file mode 100644 index 0000000000000..ca23c73648632 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI011_PYI011.pyi.snap @@ -0,0 +1,115 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 3 + column: 13 + end_location: + row: 3 + column: 23 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 9 + column: 8 + end_location: + row: 13 + column: 5 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 18 + column: 8 + end_location: + row: 22 + column: 5 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 27 + column: 8 + end_location: + row: 31 + column: 5 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 36 + column: 8 + end_location: + row: 38 + column: 5 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 41 + column: 13 + end_location: + row: 42 + column: 11 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 45 + column: 13 + end_location: + row: 46 + column: 12 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 49 + column: 16 + end_location: + row: 50 + column: 7 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 53 + column: 13 + end_location: + row: 54 + column: 7 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 57 + column: 17 + end_location: + row: 58 + column: 8 + fix: ~ + parent: ~ +- kind: + TypedArgumentSimpleDefaults: ~ + location: + row: 61 + column: 17 + end_location: + row: 62 + column: 10 + fix: ~ + parent: ~ + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI014_PYI014.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI014_PYI014.py.snap new file mode 100644 index 0000000000000..efcc2d0c99b2f --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI014_PYI014.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__PYI014_PYI014.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI014_PYI014.pyi.snap new file mode 100644 index 0000000000000..7b6f9d4637346 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI014_PYI014.pyi.snap @@ -0,0 +1,115 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 3 + column: 6 + end_location: + row: 3 + column: 16 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 7 + column: 6 + end_location: + row: 11 + column: 5 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 14 + column: 6 + end_location: + row: 18 + column: 5 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 21 + column: 6 + end_location: + row: 25 + column: 5 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 27 + column: 10 + end_location: + row: 27 + column: 45 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 29 + column: 6 + end_location: + row: 29 + column: 19 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 32 + column: 6 + end_location: + row: 32 + column: 21 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 35 + column: 6 + end_location: + row: 35 + column: 15 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 38 + column: 6 + end_location: + row: 38 + column: 11 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 41 + column: 6 + end_location: + row: 41 + column: 13 + fix: ~ + parent: ~ +- kind: + ArgumentSimpleDefaults: ~ + location: + row: 44 + column: 6 + end_location: + row: 44 + column: 19 + fix: ~ + parent: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 69623ae96faf7..187d608918f8d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1925,6 +1925,8 @@ "PYI009", "PYI01", "PYI010", + "PYI011", + "PYI014", "PYI02", "PYI021", "Q",