diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.py new file mode 100644 index 0000000000000..25b739cece920 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.py @@ -0,0 +1,33 @@ +# From https://github.com/PyCQA/flake8-pyi/blob/4212bec43dbc4020a59b90e2957c9488575e57ba/tests/type_comments.pyi + +from collections.abc import Sequence +from typing import TypeAlias + +A: TypeAlias = None # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +B: TypeAlias = None # type: str # And here's an extra comment about why it's that type # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +C: TypeAlias = None #type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +D: TypeAlias = None # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +E: TypeAlias = None# type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +F: TypeAlias = None#type:int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + +def func( + arg1, # type: dict[str, int] # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + arg2 # type: Sequence[bytes] # And here's some more info about this arg # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +): ... + +class Foo: + Attr: TypeAlias = None # type: set[str] # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + +G: TypeAlias = None # type: ignore +H: TypeAlias = None # type: ignore[attr-defined] +I: TypeAlias = None #type: ignore +J: TypeAlias = None # type: ignore +K: TypeAlias = None# type: ignore +L: TypeAlias = None#type:ignore + +# Whole line commented out # type: int +M: TypeAlias = None # type: can't parse me! + +class Bar: + N: TypeAlias = None # type: can't parse me either! + # This whole line is commented out and indented # type: str diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.pyi new file mode 100644 index 0000000000000..25b739cece920 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI033.pyi @@ -0,0 +1,33 @@ +# From https://github.com/PyCQA/flake8-pyi/blob/4212bec43dbc4020a59b90e2957c9488575e57ba/tests/type_comments.pyi + +from collections.abc import Sequence +from typing import TypeAlias + +A: TypeAlias = None # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +B: TypeAlias = None # type: str # And here's an extra comment about why it's that type # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +C: TypeAlias = None #type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +D: TypeAlias = None # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +E: TypeAlias = None# type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +F: TypeAlias = None#type:int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + +def func( + arg1, # type: dict[str, int] # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + arg2 # type: Sequence[bytes] # And here's some more info about this arg # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") +): ... + +class Foo: + Attr: TypeAlias = None # type: set[str] # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int") + +G: TypeAlias = None # type: ignore +H: TypeAlias = None # type: ignore[attr-defined] +I: TypeAlias = None #type: ignore +J: TypeAlias = None # type: ignore +K: TypeAlias = None# type: ignore +L: TypeAlias = None#type:ignore + +# Whole line commented out # type: int +M: TypeAlias = None # type: can't parse me! + +class Bar: + N: TypeAlias = None # type: can't parse me either! + # This whole line is commented out and indented # type: str diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 4572b9c765e30..a8f8e0c2d33a2 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -7,8 +7,8 @@ use crate::lex::docstring_detection::StateMachine; use crate::registry::{Diagnostic, Rule}; use crate::rules::ruff::rules::Context; use crate::rules::{ - eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, pyupgrade, - ruff, + eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, pycodestyle, + pyupgrade, ruff, }; use crate::settings::{flags, Settings}; use crate::source_code::Locator; @@ -18,6 +18,7 @@ pub fn check_tokens( tokens: &[LexResult], settings: &Settings, autofix: flags::Autofix, + is_interface_definition: bool, ) -> Vec { let mut diagnostics: Vec = vec![]; @@ -55,6 +56,7 @@ pub fn check_tokens( .enabled(&Rule::TrailingCommaOnBareTupleProhibited) || settings.rules.enabled(&Rule::TrailingCommaProhibited); let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses); + let enforce_type_comment_in_stub = settings.rules.enabled(&Rule::TypeCommentInStub); // RUF001, RUF002, RUF003 if enforce_ambiguous_unicode_character { @@ -161,5 +163,10 @@ pub fn check_tokens( ); } + // PYI033 + if enforce_type_comment_in_stub && is_interface_definition { + diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(tokens)); + } + diagnostics } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 28f11f26054b7..dcef46b1d133d 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -500,6 +500,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Pyi, "011") => Rule::TypedArgumentSimpleDefaults, (Flake8Pyi, "014") => Rule::ArgumentSimpleDefaults, (Flake8Pyi, "021") => Rule::DocstringInStub, + (Flake8Pyi, "033") => Rule::TypeCommentInStub, // flake8-pytest-style (Flake8PytestStyle, "001") => Rule::IncorrectFixtureParenthesesStyle, diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index 799a53921b7b8..07d2390e8fc98 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -21,6 +21,7 @@ use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens}; use crate::message::{Message, Source}; use crate::noqa::{add_noqa, rule_is_ignored}; use crate::registry::{Diagnostic, Rule}; +use crate::resolver::is_interface_definition_path; use crate::rules::pycodestyle; use crate::settings::{flags, Settings}; use crate::source_code::{Indexer, Locator, Stylist}; @@ -83,7 +84,14 @@ pub fn check_path( .iter_enabled() .any(|rule_code| rule_code.lint_source().is_tokens()) { - diagnostics.extend(check_tokens(locator, &tokens, settings, autofix)); + let is_interface_definition = is_interface_definition_path(path); + diagnostics.extend(check_tokens( + locator, + &tokens, + settings, + autofix, + is_interface_definition, + )); } // Run the filesystem-based rules. diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 49c4f84f7f3ed..36f4815d4af86 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -474,6 +474,7 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::DocstringInStub, rules::flake8_pyi::rules::TypedArgumentSimpleDefaults, rules::flake8_pyi::rules::ArgumentSimpleDefaults, + rules::flake8_pyi::rules::TypeCommentInStub, // flake8-pytest-style rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle, rules::flake8_pytest_style::rules::FixturePositionalArgs, @@ -827,7 +828,8 @@ impl Rule { | Rule::MultipleStatementsOnOneLineColon | Rule::UselessSemicolon | Rule::MultipleStatementsOnOneLineSemicolon - | Rule::TrailingCommaProhibited => &LintSource::Tokens, + | Rule::TrailingCommaProhibited + | Rule::TypeCommentInStub => &LintSource::Tokens, Rule::IOError => &LintSource::Io, Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports, Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => &LintSource::Filesystem, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index a699938131b4e..2ee0ec97bae6b 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -31,6 +31,8 @@ mod tests { #[test_case(Rule::ArgumentSimpleDefaults, Path::new("PYI014.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"))] + #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index bda523cf1d444..9327b956b3c56 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -7,6 +7,7 @@ pub use simple_defaults::{ argument_simple_defaults, typed_argument_simple_defaults, ArgumentSimpleDefaults, TypedArgumentSimpleDefaults, }; +pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub}; pub use unrecognized_platform::{ unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, }; @@ -17,4 +18,5 @@ mod non_empty_stub_body; mod pass_statement_stub_body; mod prefix_type_params; mod simple_defaults; +mod type_comment_in_stub; mod unrecognized_platform; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs new file mode 100644 index 0000000000000..cde4262ef2ded --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -0,0 +1,64 @@ +use once_cell::sync::Lazy; +use regex::Regex; +use rustpython_parser::lexer::LexResult; +use rustpython_parser::Tok; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::registry::Diagnostic; +use crate::violation::Violation; +use crate::Range; + +define_violation!( + /// ## What it does + /// Checks for the use of type comments (e.g., `x = 1 # type: int`) in stub + /// files. + /// + /// ## Why is this bad? + /// Stub (`.pyi`) files should use type annotations directly, rather + /// than type comments, even if they're intended to support Python 2, since + /// stub files are not executed at runtime. The one exception is `# type: ignore`. + /// + /// ## Example + /// ```python + /// x = 1 # type: int + /// ``` + /// + /// Use instead: + /// ```python + /// x: int = 1 + /// ``` + pub struct TypeCommentInStub; +); +impl Violation for TypeCommentInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("Don't use type comments in stub file") + } +} + +static TYPE_COMMENT_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^#\s*type:\s*([^#]+)(\s*#.*?)?$").unwrap()); +static TYPE_IGNORE_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^#\s*type:\s*ignore([^#]+)?(\s*#.*?)?$").unwrap()); + +/// PYI033 +pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec { + let mut diagnostics = vec![]; + + for token in tokens.iter().flatten() { + if let (location, Tok::Comment(comment), end_location) = token { + if TYPE_COMMENT_REGEX.is_match(comment) && !TYPE_IGNORE_REGEX.is_match(comment) { + diagnostics.push(Diagnostic::new( + TypeCommentInStub, + Range { + location: *location, + end_location: *end_location, + }, + )); + } + } + } + + diagnostics +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI033_PYI033.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI033_PYI033.py.snap new file mode 100644 index 0000000000000..efcc2d0c99b2f --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI033_PYI033.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__PYI033_PYI033.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI033_PYI033.pyi.snap new file mode 100644 index 0000000000000..eb65887508ae4 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI033_PYI033.pyi.snap @@ -0,0 +1,115 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + TypeCommentInStub: ~ + location: + row: 6 + column: 21 + end_location: + row: 6 + column: 127 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 7 + column: 21 + end_location: + row: 7 + column: 183 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 8 + column: 21 + end_location: + row: 8 + column: 126 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 9 + column: 21 + end_location: + row: 9 + column: 132 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 10 + column: 19 + end_location: + row: 10 + column: 128 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 11 + column: 19 + end_location: + row: 11 + column: 123 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 14 + column: 11 + end_location: + row: 14 + column: 128 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 15 + column: 10 + end_location: + row: 15 + column: 172 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 19 + column: 28 + end_location: + row: 19 + column: 139 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 29 + column: 21 + end_location: + row: 29 + column: 44 + fix: ~ + parent: ~ +- kind: + TypeCommentInStub: ~ + location: + row: 32 + column: 25 + end_location: + row: 32 + column: 55 + fix: ~ + parent: ~ + diff --git a/docs/configuration.md b/docs/configuration.md index 4417c093e690a..5587637c9198d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -206,6 +206,8 @@ Options: Run in watch mode by re-running whenever files change --fix-only Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix` + --ignore-noqa + Ignore any `# noqa` comments --format Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint] --target-version diff --git a/ruff.schema.json b/ruff.schema.json index 82c130e1029fa..1ab755d5da655 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1938,6 +1938,8 @@ "PYI014", "PYI02", "PYI021", + "PYI03", + "PYI033", "Q", "Q0", "Q00",