mirror of https://github.com/astral-sh/ruff
[`flake8-pyi`] Implement `PYI042` and `PYI043` (#4214)
This commit is contained in:
parent
890e630c41
commit
e9e194ab32
|
|
@ -0,0 +1,24 @@
|
|||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # not PYI042 (not a stubfile)
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # not PYI042 (not a stubfile)
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # not PYI042 (not a stubfile)
|
||||
Snake_case_alias: TypeAlias = int | float # not PYI042 (not a stubfile)
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # PYI042, since not camel case
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # not PYI043 (not a stubfile)
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # PYI043, since this ends in a T
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
|
|
@ -1914,6 +1914,14 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
if self.ctx.match_typing_expr(annotation, "TypeAlias") {
|
||||
if self.settings.rules.enabled(Rule::SnakeCaseTypeAlias) {
|
||||
flake8_pyi::rules::snake_case_type_alias(self, target);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::TSuffixedTypeAlias) {
|
||||
flake8_pyi::rules::t_suffixed_type_alias(self, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { targets } => {
|
||||
|
|
|
|||
|
|
@ -595,6 +595,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
|||
(Flake8Pyi, "020") => Rule::QuotedAnnotationInStub,
|
||||
(Flake8Pyi, "021") => Rule::DocstringInStub,
|
||||
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
|
||||
(Flake8Pyi, "042") => Rule::SnakeCaseTypeAlias,
|
||||
(Flake8Pyi, "043") => Rule::TSuffixedTypeAlias,
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => Rule::PytestFixtureIncorrectParenthesesStyle,
|
||||
|
|
|
|||
|
|
@ -540,6 +540,8 @@ ruff_macros::register_rules!(
|
|||
rules::flake8_pyi::rules::PassInClassBody,
|
||||
rules::flake8_pyi::rules::DuplicateUnionMember,
|
||||
rules::flake8_pyi::rules::QuotedAnnotationInStub,
|
||||
rules::flake8_pyi::rules::SnakeCaseTypeAlias,
|
||||
rules::flake8_pyi::rules::TSuffixedTypeAlias,
|
||||
// flake8-pytest-style
|
||||
rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle,
|
||||
rules::flake8_pytest_style::rules::PytestFixturePositionalArgs,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use std::iter::FusedIterator;
|
|||
///
|
||||
/// Uses a bitset where a bit of one signals that the Rule with that [u16] is in this set.
|
||||
#[derive(Clone, Default, CacheKey, PartialEq, Eq)]
|
||||
pub struct RuleSet([u64; 9]);
|
||||
pub struct RuleSet([u64; 10]);
|
||||
|
||||
impl RuleSet {
|
||||
const EMPTY: [u64; 9] = [0; 9];
|
||||
const EMPTY: [u64; 10] = [0; 10];
|
||||
|
||||
// 64 fits into a u16 without truncation
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ mod tests {
|
|||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
|
||||
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))]
|
||||
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))]
|
||||
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))]
|
||||
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))]
|
||||
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))]
|
||||
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ pub use simple_defaults::{
|
|||
typed_argument_simple_defaults, ArgumentDefaultInStub, AssignmentDefaultInStub,
|
||||
TypedArgumentDefaultInStub,
|
||||
};
|
||||
pub use type_alias_naming::{
|
||||
snake_case_type_alias, t_suffixed_type_alias, SnakeCaseTypeAlias, TSuffixedTypeAlias,
|
||||
};
|
||||
pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub};
|
||||
pub use unrecognized_platform::{
|
||||
unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName,
|
||||
|
|
@ -25,5 +28,6 @@ mod pass_statement_stub_body;
|
|||
mod prefix_type_params;
|
||||
mod quoted_annotation_in_stub;
|
||||
mod simple_defaults;
|
||||
mod type_alias_naming;
|
||||
mod type_comment_in_stub;
|
||||
mod unrecognized_platform;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
use rustpython_parser::ast::{Expr, ExprKind};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct SnakeCaseTypeAlias {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for SnakeCaseTypeAlias {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("Type alias `{name}` should be CamelCase")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct TSuffixedTypeAlias {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for TSuffixedTypeAlias {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("Private type alias `{name}` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the given name is a `snake_case` type alias. In this context, we match against
|
||||
/// any name that begins with an optional underscore, followed by at least one lowercase letter.
|
||||
fn is_snake_case_type_alias(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
matches!(
|
||||
(chars.next(), chars.next()),
|
||||
(Some('_'), Some('0'..='9' | 'a'..='z')) | (Some('0'..='9' | 'a'..='z'), ..)
|
||||
)
|
||||
}
|
||||
|
||||
/// Return `true` if the given name is a T-suffixed type alias. In this context, we match against
|
||||
/// any name that begins with an underscore, and ends in a lowercase letter, followed by `T`,
|
||||
/// followed by an optional digit.
|
||||
fn is_t_suffixed_type_alias(name: &str) -> bool {
|
||||
// A T-suffixed, private type alias must begin with an underscore.
|
||||
if !name.starts_with('_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// It must end in a lowercase letter, followed by `T`, and (optionally) a digit.
|
||||
let mut chars = name.chars().rev();
|
||||
matches!(
|
||||
(chars.next(), chars.next(), chars.next()),
|
||||
(Some('0'..='9'), Some('T'), Some('a'..='z')) | (Some('T'), Some('a'..='z'), _)
|
||||
)
|
||||
}
|
||||
|
||||
/// PYI042
|
||||
pub fn snake_case_type_alias(checker: &mut Checker, target: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = target.node() {
|
||||
if !is_snake_case_type_alias(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SnakeCaseTypeAlias {
|
||||
name: id.to_string(),
|
||||
},
|
||||
target.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI043
|
||||
pub fn t_suffixed_type_alias(checker: &mut Checker, target: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = target.node() {
|
||||
if !is_t_suffixed_type_alias(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TSuffixedTypeAlias {
|
||||
name: id.to_string(),
|
||||
},
|
||||
target.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI042.pyi:10:1: PYI042 Type alias `just_literals_pipe_union` should be CamelCase
|
||||
|
|
||||
10 | )
|
||||
11 |
|
||||
12 | just_literals_pipe_union: TypeAlias = (
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ PYI042
|
||||
13 | Literal[True] | Literal["idk"]
|
||||
14 | ) # PYI042, since not camel case
|
||||
|
|
||||
|
||||
PYI042.pyi:19:1: PYI042 Type alias `snake_case_alias1` should be CamelCase
|
||||
|
|
||||
19 | _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
20 |
|
||||
21 | snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
| ^^^^^^^^^^^^^^^^^ PYI042
|
||||
22 | _snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
23 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
|
||||
|
||||
PYI042.pyi:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
|
||||
|
|
||||
20 | snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
21 | _snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
| ^^^^^^^^^^^^^^^^^^ PYI042
|
||||
22 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI043.pyi:10:1: PYI043 Private type alias `_PrivateAliasT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
10 | )
|
||||
11 |
|
||||
12 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
| ^^^^^^^^^^^^^^ PYI043
|
||||
13 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
14 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
|
|
||||
|
||||
PYI043.pyi:11:1: PYI043 Private type alias `_PrivateAliasT2` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
11 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
12 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
| ^^^^^^^^^^^^^^^ PYI043
|
||||
13 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
14 | "not", "a", "chance"
|
||||
|
|
||||
|
||||
PYI043.pyi:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
12 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
13 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
14 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
| ^^^^^^^^^^^^^^^ PYI043
|
||||
15 | "not", "a", "chance"
|
||||
16 | ] # PYI043, since this ends in a T
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -2128,6 +2128,9 @@
|
|||
"PYI021",
|
||||
"PYI03",
|
||||
"PYI033",
|
||||
"PYI04",
|
||||
"PYI042",
|
||||
"PYI043",
|
||||
"Q",
|
||||
"Q0",
|
||||
"Q00",
|
||||
|
|
|
|||
Loading…
Reference in New Issue