mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 12:55:05 +00:00
[flake8-type-checking
] Adds implementation for TC006 (#14511)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
b80de52592
commit
e25e7044ba
9 changed files with 295 additions and 0 deletions
62
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py
vendored
Normal file
62
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py
vendored
Normal file
|
@ -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
|
||||||
|
)
|
|
@ -1280,6 +1280,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
let mut args = arguments.args.iter();
|
let mut args = arguments.args.iter();
|
||||||
if let Some(arg) = args.next() {
|
if let Some(arg) = args.next() {
|
||||||
self.visit_type_definition(arg);
|
self.visit_type_definition(arg);
|
||||||
|
|
||||||
|
if self.enabled(Rule::RuntimeCastValue) {
|
||||||
|
flake8_type_checking::rules::runtime_cast_value(self, arg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for arg in args {
|
for arg in args {
|
||||||
self.visit_expr(arg);
|
self.visit_expr(arg);
|
||||||
|
|
|
@ -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, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
|
||||||
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
||||||
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
|
(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),
|
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||||
|
|
||||||
// tryceratops
|
// tryceratops
|
||||||
|
|
|
@ -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 quote = stylist.quote();
|
||||||
let mut quote_annotator = QuoteAnnotator::new(semantic, stylist);
|
let mut quote_annotator = QuoteAnnotator::new(semantic, stylist);
|
||||||
quote_annotator.visit_expr(expr);
|
quote_annotator.visit_expr(expr);
|
||||||
|
|
|
@ -17,6 +17,7 @@ mod tests {
|
||||||
use crate::{assert_messages, settings};
|
use crate::{assert_messages, settings};
|
||||||
|
|
||||||
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TC005.py"))]
|
#[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_1.py"))]
|
||||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_10.py"))]
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_10.py"))]
|
||||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_11.py"))]
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_11.py"))]
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
pub(crate) use empty_type_checking_block::*;
|
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_import_in_type_checking_block::*;
|
||||||
pub(crate) use runtime_string_union::*;
|
pub(crate) use runtime_string_union::*;
|
||||||
pub(crate) use typing_only_runtime_import::*;
|
pub(crate) use typing_only_runtime_import::*;
|
||||||
|
|
||||||
mod empty_type_checking_block;
|
mod empty_type_checking_block;
|
||||||
|
mod runtime_cast_value;
|
||||||
mod runtime_import_in_type_checking_block;
|
mod runtime_import_in_type_checking_block;
|
||||||
mod runtime_string_union;
|
mod runtime_string_union;
|
||||||
mod typing_only_runtime_import;
|
mod typing_only_runtime_import;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -4003,6 +4003,7 @@
|
||||||
"TC003",
|
"TC003",
|
||||||
"TC004",
|
"TC004",
|
||||||
"TC005",
|
"TC005",
|
||||||
|
"TC006",
|
||||||
"TC01",
|
"TC01",
|
||||||
"TC010",
|
"TC010",
|
||||||
"TD",
|
"TD",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue