From d66063bb33b15da4bf1ca432c4356110c48b4880 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Fri, 1 Dec 2023 17:18:52 +0000 Subject: [PATCH] [`flake8-pyi`] Check for kwarg and vararg `NoReturn` type annotations (#8948) ## Summary Triggers `no-return-argument-annotation-in-stub` (`PYI050`) for vararg and kwarg `NoReturn` type annotations. Related to #8771. ## Test Plan `cargo test` --- .../test/fixtures/flake8_pyi/PYI050.pyi | 11 ++++ .../rules/no_return_argument_annotation.rs | 45 ++++++++++---- ..._flake8_pyi__tests__PYI050_PYI050.pyi.snap | 61 +++++++++++++++++++ 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi index 4720ee7756b6a..583c96e71b59b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi @@ -10,3 +10,14 @@ def foo_no_return_typing_extensions( def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050 def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050 def foo_never(arg: Never): ... +def foo_args(*args: NoReturn): ... # Error: PYI050 +def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050 +def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050 +def foo_int_args(*args: int): ... +def foo_int_kwargs(**kwargs: int): ... +def foo_int_args_kwargs(*args: int, **kwargs: int): ... +def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050 +def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050 +def foo_args_never(*args: Never): ... +def foo_kwargs_never(**kwargs: Never): ... +def foo_args_kwargs_never(*args: Never, **kwargs: Never): ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index f10946236be2d..01350b9501cc0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -2,7 +2,7 @@ use std::fmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::Parameters; +use ruff_python_ast::{Expr, Parameters}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -51,6 +51,9 @@ impl Violation for NoReturnArgumentAnnotationInStub { /// PYI050 pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &Parameters) { + // Ex) def func(arg: NoReturn): ... + // Ex) def func(arg: NoReturn, /): ... + // Ex) def func(*, arg: NoReturn): ... for annotation in parameters .posonlyargs .iter() @@ -58,21 +61,39 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: & .chain(¶meters.kwonlyargs) .filter_map(|arg| arg.parameter.annotation.as_ref()) { - if checker.semantic().match_typing_expr(annotation, "NoReturn") { - checker.diagnostics.push(Diagnostic::new( - NoReturnArgumentAnnotationInStub { - module: if checker.settings.target_version >= Py311 { - TypingModule::Typing - } else { - TypingModule::TypingExtensions - }, - }, - annotation.range(), - )); + check_no_return_argument_annotation(checker, annotation); + } + + // Ex) def func(*args: NoReturn): ... + if let Some(arg) = ¶meters.vararg { + if let Some(annotation) = &arg.annotation { + check_no_return_argument_annotation(checker, annotation); + } + } + + // Ex) def func(**kwargs: NoReturn): ... + if let Some(arg) = ¶meters.kwarg { + if let Some(annotation) = &arg.annotation { + check_no_return_argument_annotation(checker, annotation); } } } +fn check_no_return_argument_annotation(checker: &mut Checker, annotation: &Expr) { + if checker.semantic().match_typing_expr(annotation, "NoReturn") { + checker.diagnostics.push(Diagnostic::new( + NoReturnArgumentAnnotationInStub { + module: if checker.settings.target_version >= Py311 { + TypingModule::Typing + } else { + TypingModule::TypingExtensions + }, + }, + annotation.range(), + )); + } +} + #[derive(Debug, PartialEq, Eq)] enum TypingModule { Typing, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap index a0cb503548fca..caafc0254ea8c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap @@ -28,6 +28,67 @@ PYI050.pyi:11:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument anno 11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050 | ^^^^^^^^ PYI050 12 | def foo_never(arg: Never): ... +13 | def foo_args(*args: NoReturn): ... # Error: PYI050 + | + +PYI050.pyi:13:21: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050 +12 | def foo_never(arg: Never): ... +13 | def foo_args(*args: NoReturn): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050 +15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050 + | + +PYI050.pyi:14:26: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +12 | def foo_never(arg: Never): ... +13 | def foo_args(*args: NoReturn): ... # Error: PYI050 +14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050 +16 | def foo_int_args(*args: int): ... + | + +PYI050.pyi:15:28: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +13 | def foo_args(*args: NoReturn): ... # Error: PYI050 +14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050 +15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +16 | def foo_int_args(*args: int): ... +17 | def foo_int_kwargs(**kwargs: int): ... + | + +PYI050.pyi:15:48: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +13 | def foo_args(*args: NoReturn): ... # Error: PYI050 +14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050 +15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +16 | def foo_int_args(*args: int): ... +17 | def foo_int_kwargs(**kwargs: int): ... + | + +PYI050.pyi:19:50: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +17 | def foo_int_kwargs(**kwargs: int): ... +18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ... +19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050 +21 | def foo_args_never(*args: Never): ... + | + +PYI050.pyi:20:37: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ... +19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050 +20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050 + | ^^^^^^^^ PYI050 +21 | def foo_args_never(*args: Never): ... +22 | def foo_kwargs_never(**kwargs: Never): ... |