Skip to content

Commit

Permalink
[flake8-type-checking] Adds implementation for TC006 (#14511)
Browse files Browse the repository at this point in the history
Co-authored-by: Micha Reiser <[email protected]>
  • Loading branch information
Daverball and MichaReiser authored Nov 22, 2024
1 parent b80de52 commit e25e704
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
def f():
from typing import cast

cast(int, 3.0) # TC006


def f():
from typing import cast

cast(list[tuple[bool | float | int | str]], 3.0) # TC006


def f():
from typing import Union, cast

cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006


def f():
from typing import cast

cast("int", 3.0) # OK


def f():
from typing import cast

cast("list[tuple[bool | float | int | str]]", 3.0) # OK


def f():
from typing import Union, cast

cast("list[tuple[Union[bool, float, int, str]]]", 3.0) # OK


def f():
from typing import cast as typecast

typecast(int, 3.0) # TC006


def f():
import typing

typing.cast(int, 3.0) # TC006


def f():
import typing as t

t.cast(t.Literal["3.0", '3'], 3.0) # TC006


def f():
from typing import cast

cast(
int # TC006 (unsafe, because it will get rid of this comment)
| None,
3.0
)
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_type_definition(arg);

if self.enabled(Rule::RuntimeCastValue) {
flake8_type_checking::rules::runtime_cast_value(self, arg);
}
}
for arg in args {
self.visit_expr(arg);
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeCastValue),
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),

// tryceratops
Expand Down
17 changes: 17 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,23 @@ pub(crate) fn quote_annotation(
}
}

quote_type_expression(expr, semantic, stylist)
}

/// Wrap a type expression in quotes.
///
/// This function assumes that the callee already expanded expression components
/// to the minimum acceptable range for quoting, i.e. the parent node may not be
/// a [`Expr::Subscript`], [`Expr::Attribute`], `[Expr::Call]` or `[Expr::BinOp]`.
///
/// In most cases you want to call [`quote_annotation`] instead, which provides
/// that guarantee by expanding the expression before calling into this function.
pub(crate) fn quote_type_expression(
expr: &Expr,
semantic: &SemanticModel,
stylist: &Stylist,
) -> Result<Edit> {
// Quote the entire expression.
let quote = stylist.quote();
let mut quote_annotator = QuoteAnnotator::new(semantic, stylist);
quote_annotator.visit_expr(expr);
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod tests {
use crate::{assert_messages, settings};

#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TC005.py"))]
#[test_case(Rule::RuntimeCastValue, Path::new("TC006.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_1.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_11.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pub(crate) use empty_type_checking_block::*;
pub(crate) use runtime_cast_value::*;
pub(crate) use runtime_import_in_type_checking_block::*;
pub(crate) use runtime_string_union::*;
pub(crate) use typing_only_runtime_import::*;

mod empty_type_checking_block;
mod runtime_cast_value;
mod runtime_import_in_type_checking_block;
mod runtime_string_union;
mod typing_only_runtime_import;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use ruff_python_ast::Expr;

use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
use crate::rules::flake8_type_checking::helpers::quote_type_expression;

/// ## What it does
/// Checks for an unquoted type expression in `typing.cast()` calls.
///
/// ## Why is this bad?
/// `typing.cast()` does not do anything at runtime, so the time spent
/// on evaluating the type expression is wasted.
///
/// ## Example
/// ```python
/// from typing import cast
///
/// x = cast(dict[str, int], foo)
/// ```
///
/// Use instead:
/// ```python
/// from typing import cast
///
/// x = cast("dict[str, int]", foo)
/// ```
///
/// ## Fix safety
/// This fix is safe as long as the type expression doesn't span multiple
/// lines and includes comments on any of the lines apart from the last one.
#[violation]
pub struct RuntimeCastValue;

impl Violation for RuntimeCastValue {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;

#[derive_message_formats]
fn message(&self) -> String {
"Add quotes to type expression in `typing.cast()`".to_string()
}

fn fix_title(&self) -> Option<String> {
Some("Add quotes".to_string())
}
}

/// TC006
pub(crate) fn runtime_cast_value(checker: &mut Checker, type_expr: &Expr) {
if type_expr.is_string_literal_expr() {
return;
}

let mut diagnostic = Diagnostic::new(RuntimeCastValue, type_expr.range());
let edit = quote_type_expression(type_expr, checker.semantic(), checker.stylist()).ok();
if let Some(edit) = edit {
if checker
.comment_ranges()
.has_comments(type_expr, checker.source())
{
diagnostic.set_fix(Fix::unsafe_edit(edit));
} else {
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
checker.diagnostics.push(diagnostic);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC006.py:4:10: TC006 [*] Add quotes to type expression in `typing.cast()`
|
2 | from typing import cast
3 |
4 | cast(int, 3.0) # TC006
| ^^^ TC006
|
= help: Add quotes

Safe fix
1 1 | def f():
2 2 | from typing import cast
3 3 |
4 |- cast(int, 3.0) # TC006
4 |+ cast("int", 3.0) # TC006
5 5 |
6 6 |
7 7 | def f():

TC006.py:10:10: TC006 [*] Add quotes to type expression in `typing.cast()`
|
8 | from typing import cast
9 |
10 | cast(list[tuple[bool | float | int | str]], 3.0) # TC006
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TC006
|
= help: Add quotes

Safe fix
7 7 | def f():
8 8 | from typing import cast
9 9 |
10 |- cast(list[tuple[bool | float | int | str]], 3.0) # TC006
10 |+ cast("list[tuple[bool | float | int | str]]", 3.0) # TC006
11 11 |
12 12 |
13 13 | def f():

TC006.py:16:10: TC006 [*] Add quotes to type expression in `typing.cast()`
|
14 | from typing import Union, cast
15 |
16 | cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TC006
|
= help: Add quotes

Safe fix
13 13 | def f():
14 14 | from typing import Union, cast
15 15 |
16 |- cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006
16 |+ cast("list[tuple[Union[bool, float, int, str]]]", 3.0) # TC006
17 17 |
18 18 |
19 19 | def f():

TC006.py:40:14: TC006 [*] Add quotes to type expression in `typing.cast()`
|
38 | from typing import cast as typecast
39 |
40 | typecast(int, 3.0) # TC006
| ^^^ TC006
|
= help: Add quotes

Safe fix
37 37 | def f():
38 38 | from typing import cast as typecast
39 39 |
40 |- typecast(int, 3.0) # TC006
40 |+ typecast("int", 3.0) # TC006
41 41 |
42 42 |
43 43 | def f():

TC006.py:46:17: TC006 [*] Add quotes to type expression in `typing.cast()`
|
44 | import typing
45 |
46 | typing.cast(int, 3.0) # TC006
| ^^^ TC006
|
= help: Add quotes

Safe fix
43 43 | def f():
44 44 | import typing
45 45 |
46 |- typing.cast(int, 3.0) # TC006
46 |+ typing.cast("int", 3.0) # TC006
47 47 |
48 48 |
49 49 | def f():

TC006.py:52:12: TC006 [*] Add quotes to type expression in `typing.cast()`
|
50 | import typing as t
51 |
52 | t.cast(t.Literal["3.0", '3'], 3.0) # TC006
| ^^^^^^^^^^^^^^^^^^^^^ TC006
|
= help: Add quotes

Safe fix
49 49 | def f():
50 50 | import typing as t
51 51 |
52 |- t.cast(t.Literal["3.0", '3'], 3.0) # TC006
52 |+ t.cast("t.Literal['3.0', '3']", 3.0) # TC006
53 53 |
54 54 |
55 55 | def f():

TC006.py:59:9: TC006 [*] Add quotes to type expression in `typing.cast()`
|
58 | cast(
59 | int # TC006 (unsafe, because it will get rid of this comment)
| _________^
60 | | | None,
| |______________^ TC006
61 | 3.0
62 | )
|
= help: Add quotes

Unsafe fix
56 56 | from typing import cast
57 57 |
58 58 | cast(
59 |- int # TC006 (unsafe, because it will get rid of this comment)
60 |- | None,
59 |+ "int | None",
61 60 | 3.0
62 61 | )
1 change: 1 addition & 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 e25e704

Please sign in to comment.