Skip to content

Commit

Permalink
[flake8-pyi] Implement PYI042 and PYI043 (#4214)
Browse files Browse the repository at this point in the history
  • Loading branch information
arya-k authored May 4, 2023
1 parent 890e630 commit e9e194a
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 2 deletions.
24 changes: 24 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.py
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.pyi
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.pyi
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions crates/ruff/src/registry/rule_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
91 changes: 91 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/rules/type_alias_naming.rs
Original file line number Diff line number Diff line change
@@ -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(),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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
|


Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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
|


3 changes: 3 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e9e194a

Please sign in to comment.