Add flake-pyi PYI033 "Do not use type comments in stubs" (#3302)

This commit is contained in:
konstin 2023-03-03 16:45:34 +01:00 committed by GitHub
parent 98209be8aa
commit 3bcffb5bdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 279 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -7,8 +7,8 @@ use crate::lex::docstring_detection::StateMachine;
use crate::registry::{Diagnostic, Rule}; use crate::registry::{Diagnostic, Rule};
use crate::rules::ruff::rules::Context; use crate::rules::ruff::rules::Context;
use crate::rules::{ use crate::rules::{
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, pyupgrade, eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, pycodestyle,
ruff, pyupgrade, ruff,
}; };
use crate::settings::{flags, Settings}; use crate::settings::{flags, Settings};
use crate::source_code::Locator; use crate::source_code::Locator;
@ -18,6 +18,7 @@ pub fn check_tokens(
tokens: &[LexResult], tokens: &[LexResult],
settings: &Settings, settings: &Settings,
autofix: flags::Autofix, autofix: flags::Autofix,
is_interface_definition: bool,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![]; let mut diagnostics: Vec<Diagnostic> = vec![];
@ -55,6 +56,7 @@ pub fn check_tokens(
.enabled(&Rule::TrailingCommaOnBareTupleProhibited) .enabled(&Rule::TrailingCommaOnBareTupleProhibited)
|| settings.rules.enabled(&Rule::TrailingCommaProhibited); || settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses); let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
let enforce_type_comment_in_stub = settings.rules.enabled(&Rule::TypeCommentInStub);
// RUF001, RUF002, RUF003 // RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character { 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 diagnostics
} }

View file

@ -511,6 +511,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Pyi, "011") => Rule::TypedArgumentSimpleDefaults, (Flake8Pyi, "011") => Rule::TypedArgumentSimpleDefaults,
(Flake8Pyi, "014") => Rule::ArgumentSimpleDefaults, (Flake8Pyi, "014") => Rule::ArgumentSimpleDefaults,
(Flake8Pyi, "021") => Rule::DocstringInStub, (Flake8Pyi, "021") => Rule::DocstringInStub,
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
// flake8-pytest-style // flake8-pytest-style
(Flake8PytestStyle, "001") => Rule::IncorrectFixtureParenthesesStyle, (Flake8PytestStyle, "001") => Rule::IncorrectFixtureParenthesesStyle,

View file

@ -21,6 +21,7 @@ use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::message::{Message, Source}; use crate::message::{Message, Source};
use crate::noqa::{add_noqa, rule_is_ignored}; use crate::noqa::{add_noqa, rule_is_ignored};
use crate::registry::{Diagnostic, Rule}; use crate::registry::{Diagnostic, Rule};
use crate::resolver::is_interface_definition_path;
use crate::rules::pycodestyle; use crate::rules::pycodestyle;
use crate::settings::{flags, Settings}; use crate::settings::{flags, Settings};
use crate::source_code::{Indexer, Locator, Stylist}; use crate::source_code::{Indexer, Locator, Stylist};
@ -83,7 +84,14 @@ pub fn check_path(
.iter_enabled() .iter_enabled()
.any(|rule_code| rule_code.lint_source().is_tokens()) .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. // Run the filesystem-based rules.

View file

@ -485,6 +485,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::DocstringInStub, rules::flake8_pyi::rules::DocstringInStub,
rules::flake8_pyi::rules::TypedArgumentSimpleDefaults, rules::flake8_pyi::rules::TypedArgumentSimpleDefaults,
rules::flake8_pyi::rules::ArgumentSimpleDefaults, rules::flake8_pyi::rules::ArgumentSimpleDefaults,
rules::flake8_pyi::rules::TypeCommentInStub,
// flake8-pytest-style // flake8-pytest-style
rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle, rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle,
rules::flake8_pytest_style::rules::FixturePositionalArgs, rules::flake8_pytest_style::rules::FixturePositionalArgs,
@ -838,7 +839,8 @@ impl Rule {
| Rule::MultipleStatementsOnOneLineColon | Rule::MultipleStatementsOnOneLineColon
| Rule::UselessSemicolon | Rule::UselessSemicolon
| Rule::MultipleStatementsOnOneLineSemicolon | Rule::MultipleStatementsOnOneLineSemicolon
| Rule::TrailingCommaProhibited => &LintSource::Tokens, | Rule::TrailingCommaProhibited
| Rule::TypeCommentInStub => &LintSource::Tokens,
Rule::IOError => &LintSource::Io, Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports, Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => &LintSource::Filesystem, Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => &LintSource::Filesystem,

View file

@ -31,6 +31,8 @@ mod tests {
#[test_case(Rule::ArgumentSimpleDefaults, Path::new("PYI014.pyi"))] #[test_case(Rule::ArgumentSimpleDefaults, Path::new("PYI014.pyi"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[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<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -7,6 +7,7 @@ pub use simple_defaults::{
argument_simple_defaults, typed_argument_simple_defaults, ArgumentSimpleDefaults, argument_simple_defaults, typed_argument_simple_defaults, ArgumentSimpleDefaults,
TypedArgumentSimpleDefaults, TypedArgumentSimpleDefaults,
}; };
pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub};
pub use unrecognized_platform::{ pub use unrecognized_platform::{
unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName,
}; };
@ -17,4 +18,5 @@ mod non_empty_stub_body;
mod pass_statement_stub_body; mod pass_statement_stub_body;
mod prefix_type_params; mod prefix_type_params;
mod simple_defaults; mod simple_defaults;
mod type_comment_in_stub;
mod unrecognized_platform; mod unrecognized_platform;

View file

@ -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<Regex> =
Lazy::new(|| Regex::new(r"^#\s*type:\s*([^#]+)(\s*#.*?)?$").unwrap());
static TYPE_IGNORE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#\s*type:\s*ignore([^#]+)?(\s*#.*?)?$").unwrap());
/// PYI033
pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec<Diagnostic> {
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
}

View file

@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

View file

@ -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: ~

2
ruff.schema.json generated
View file

@ -1948,6 +1948,8 @@
"PYI014", "PYI014",
"PYI02", "PYI02",
"PYI021", "PYI021",
"PYI03",
"PYI033",
"Q", "Q",
"Q0", "Q0",
"Q00", "Q00",