Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flake8-type-checking] Adds implementation for TC006 #14511

Merged
merged 5 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(int, 3.0) # TC006
Daverball marked this conversation as resolved.
Show resolved Hide resolved


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
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod tests {
use test_case::test_case;

use crate::registry::{Linter, Rule};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_messages, settings};

Expand Down Expand Up @@ -62,6 +63,20 @@ mod tests {
Ok(())
}

#[test_case(Rule::RuntimeCastValue, Path::new("TC006.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("preview__{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}

Daverball marked this conversation as resolved.
Show resolved Hide resolved
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote2.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.as_ref() {
if checker
.comment_ranges()
.has_comments(type_expr, checker.source())
{
diagnostic.set_fix(Fix::unsafe_edit(edit.clone()));
} else {
diagnostic.set_fix(Fix::safe_edit(edit.clone()));
}
}
Daverball marked this conversation as resolved.
Show resolved Hide resolved
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(int, 3.0) # TC006
| ^^^ TC006
|
= help: Add quotes

ℹ Safe fix
49 49 | def f():
50 50 | import typing as t
51 51 |
52 |- t.cast(int, 3.0) # TC006
52 |+ t.cast("int", 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.

Loading